diff --git a/.changeset/blue-dragons-wave.md b/.changeset/blue-dragons-wave.md
new file mode 100644
index 000000000000..dae0642210d6
--- /dev/null
+++ b/.changeset/blue-dragons-wave.md
@@ -0,0 +1,5 @@
+---
+"wrangler": patch
+---
+
+Add a warning in the autoconfig logic letting users know that support for projects inside workspaces is limited
diff --git a/.changeset/purple-queens-vanish.md b/.changeset/purple-queens-vanish.md
new file mode 100644
index 000000000000..c4175b1d5f0b
--- /dev/null
+++ b/.changeset/purple-queens-vanish.md
@@ -0,0 +1,6 @@
+---
+"@cloudflare/workers-utils": patch
+"wrangler": patch
+---
+
+Switch to `empathic` for file-system upwards traversal to reduce dependency bloat.
diff --git a/.changeset/soft-apes-ask.md b/.changeset/soft-apes-ask.md
new file mode 100644
index 000000000000..5ed03a889b2f
--- /dev/null
+++ b/.changeset/soft-apes-ask.md
@@ -0,0 +1,5 @@
+---
+"wrangler": patch
+---
+
+Implemented logic within `wrangler containers registries configure` to check if a specified secret name is already in-use and offer to reuse that secret. Also added `--skip-confirmation` flag to the command to skip all interactive prompts.
diff --git a/packages/workers-utils/package.json b/packages/workers-utils/package.json
index cddd8e342d07..bfd6ed7294e1 100644
--- a/packages/workers-utils/package.json
+++ b/packages/workers-utils/package.json
@@ -45,8 +45,8 @@
"@vitest/ui": "catalog:default",
"cloudflare": "^5.2.0",
"concurrently": "^8.2.2",
+ "empathic": "^2.0.0",
"eslint": "catalog:default",
- "find-up": "^6.3.0",
"jsonc-parser": "catalog:default",
"smol-toml": "catalog:default",
"ts-dedent": "^2.2.0",
diff --git a/packages/workers-utils/src/config/config-helpers.ts b/packages/workers-utils/src/config/config-helpers.ts
index 51576631b23f..3bb3964092f4 100644
--- a/packages/workers-utils/src/config/config-helpers.ts
+++ b/packages/workers-utils/src/config/config-helpers.ts
@@ -1,6 +1,6 @@
import { existsSync } from "node:fs";
import path from "node:path";
-import { findUpSync } from "find-up";
+import * as find from "empathic/find";
import dedent from "ts-dedent";
import { PATH_TO_DEPLOY_CONFIG } from "../constants";
import { UserError } from "../errors";
@@ -61,9 +61,9 @@ export function findWranglerConfig(
{ useRedirectIfAvailable = false } = {}
): ConfigPaths {
const userConfigPath =
- findUpSync(`wrangler.json`, { cwd: referencePath }) ??
- findUpSync(`wrangler.jsonc`, { cwd: referencePath }) ??
- findUpSync(`wrangler.toml`, { cwd: referencePath });
+ find.file(`wrangler.json`, { cwd: referencePath }) ??
+ find.file(`wrangler.jsonc`, { cwd: referencePath }) ??
+ find.file(`wrangler.toml`, { cwd: referencePath });
if (!useRedirectIfAvailable) {
return {
@@ -99,7 +99,7 @@ function findRedirectedWranglerConfig(
deployConfigPath: string | undefined;
redirected: boolean;
} {
- const deployConfigPath = findUpSync(PATH_TO_DEPLOY_CONFIG, { cwd });
+ const deployConfigPath = find.file(PATH_TO_DEPLOY_CONFIG, { cwd });
if (deployConfigPath === undefined) {
return { configPath: userConfigPath, deployConfigPath, redirected: false };
}
diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json
index 1b771ad7f55f..d19b9468b33d 100644
--- a/packages/wrangler/package.json
+++ b/packages/wrangler/package.json
@@ -127,10 +127,10 @@
"devtools-protocol": "^0.0.1182435",
"dotenv": "^16.3.1",
"dotenv-expand": "^12.0.2",
+ "empathic": "^2.0.0",
"eslint": "catalog:default",
"esprima": "4.0.1",
"execa": "^6.1.0",
- "find-up": "^6.3.0",
"get-port": "^7.0.0",
"glob-to-regexp": "^0.4.1",
"https-proxy-agent": "7.0.2",
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts
index 9ea1aa95e7b1..573f9ce75642 100644
--- a/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts
+++ b/packages/wrangler/src/__tests__/autoconfig/details/confirm-auto-config-details.test.ts
@@ -56,6 +56,9 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => {
"dlx": [
"npx",
],
+ "lockFiles": [
+ "package-lock.json",
+ ],
"npx": "npx",
"type": "npm",
},
@@ -115,6 +118,9 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => {
"dlx": [
"npx",
],
+ "lockFiles": [
+ "package-lock.json",
+ ],
"npx": "npx",
"type": "npm",
},
@@ -174,6 +180,9 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => {
"dlx": [
"npx",
],
+ "lockFiles": [
+ "package-lock.json",
+ ],
"npx": "npx",
"type": "npm",
},
@@ -255,6 +264,9 @@ describe("autoconfig details - confirmAutoConfigDetails()", () => {
"dlx": [
"npx",
],
+ "lockFiles": [
+ "package-lock.json",
+ ],
"npx": "npx",
"type": "npm",
},
diff --git a/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts b/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts
index 9db343668103..fdc4be0a7ed5 100644
--- a/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts
+++ b/packages/wrangler/src/__tests__/autoconfig/details/get-details-for-auto-config.test.ts
@@ -19,7 +19,7 @@ import type { Mock, MockInstance } from "vitest";
describe("autoconfig details - getDetailsForAutoConfig()", () => {
runInTempDir();
const { setIsTTY } = useMockIsTTY();
- mockConsoleMethods();
+ const std = mockConsoleMethods();
let isNonInteractiveOrCISpy: MockInstance;
beforeEach(() => {
@@ -122,6 +122,27 @@ describe("autoconfig details - getDetailsForAutoConfig()", () => {
);
});
+ it("should warn when no lock file is detected (project may be inside a workspace)", async ({
+ expect,
+ }) => {
+ // Create a project without a lock file - simulating a project inside a workspace
+ // where the lock file is at the workspace root
+ await seed({
+ "package.json": JSON.stringify({
+ name: "my-app",
+ dependencies: {},
+ }),
+ "index.html": "
Hello World
",
+ });
+
+ await details.getDetailsForAutoConfig();
+
+ expect(std.warn).toContain(
+ "No lock file has been detected in the current working directory."
+ );
+ expect(std.warn).toContain("project is part of a workspace");
+ });
+
it("should use npm build instead of framework build if present", async ({
expect,
}) => {
diff --git a/packages/wrangler/src/__tests__/containers/registries.test.ts b/packages/wrangler/src/__tests__/containers/registries.test.ts
index 36df31c58e54..24ab7652983c 100644
--- a/packages/wrangler/src/__tests__/containers/registries.test.ts
+++ b/packages/wrangler/src/__tests__/containers/registries.test.ts
@@ -48,7 +48,7 @@ describe("containers registries configure", () => {
const domain = "123456789012.dkr.ecr.us-west-2.amazonaws.com";
await expect(
runWrangler(
- `containers registries configure ${domain} --public-credential=test-id --disableSecretsStore`
+ `containers registries configure ${domain} --public-credential=test-id --disable-secrets-store`
)
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Secrets Store can only be disabled in FedRAMP compliance regions.]`
@@ -61,23 +61,23 @@ describe("containers registries configure", () => {
`containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-store-id=storeid`
)
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: Secrets Store is not supported in FedRAMP compliance regions. You must set --disableSecretsStore.]`
+ `[Error: Secrets Store is not supported in FedRAMP compliance regions. You must set --disable-secrets-store.]`
);
await expect(
runWrangler(
- `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-store-id=storeid --disableSecretsStore`
+ `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-store-id=storeid --disable-secrets-store`
)
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: Arguments secret-store-id and disableSecretsStore are mutually exclusive]`
+ `[Error: Arguments secret-store-id and disable-secrets-store are mutually exclusive]`
);
await expect(
runWrangler(
- `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-name=secret-name --disableSecretsStore`
+ `containers registries configure ${domain} --aws-access-key-id=test-access-key-id --secret-name=secret-name --disable-secrets-store`
)
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: Arguments secret-name and disableSecretsStore are mutually exclusive]`
+ `[Error: Arguments secret-name and disable-secrets-store are mutually exclusive]`
);
});
@@ -123,7 +123,7 @@ describe("containers registries configure", () => {
});
await runWrangler(
- `containers registries configure ${awsEcrDomain} --aws-access-key-id=test-access-key-id --disableSecretsStore`
+ `containers registries configure ${awsEcrDomain} --aws-access-key-id=test-access-key-id --disable-secrets-store`
);
expect(cliStd.stdout).toMatchInlineSnapshot(`
@@ -161,7 +161,7 @@ describe("containers registries configure", () => {
});
await runWrangler(
- `containers registries configure ${awsEcrDomain} --public-credential=test-access-key-id --disableSecretsStore`
+ `containers registries configure ${awsEcrDomain} --public-credential=test-access-key-id --disable-secrets-store`
);
});
});
@@ -192,6 +192,7 @@ describe("containers registries configure", () => {
modified: "2024-01-01T00:00:00Z",
},
]);
+ mockListSecrets(storeId, []);
mockCreateSecret(storeId);
mockPutRegistry({
domain: "123456789012.dkr.ecr.us-west-2.amazonaws.com",
@@ -235,6 +236,7 @@ describe("containers registries configure", () => {
mockListSecretStores([]);
mockCreateSecretStore(newStoreId);
+ mockListSecrets(newStoreId, []);
mockCreateSecret(newStoreId);
mockPutRegistry({
domain: awsEcrDomain,
@@ -275,6 +277,7 @@ describe("containers registries configure", () => {
result: "AWS_Secret_Access_Key",
});
+ mockListSecrets(providedStoreId, []);
mockCreateSecret(providedStoreId);
mockPutRegistry({
domain: awsEcrDomain,
@@ -307,7 +310,7 @@ describe("containers registries configure", () => {
│
│
│
- │ Container-scoped secret AWS_Secret_Access_Key created in Secrets Store.
+ │ Container-scoped secret "AWS_Secret_Access_Key" created in Secrets Store.
│
╰ Registry configuration completed
@@ -336,6 +339,7 @@ describe("containers registries configure", () => {
modified: "2024-01-01T00:00:00Z",
},
]);
+ mockListSecrets(storeId, []);
mockCreateSecret(storeId);
mockPutRegistry({
domain: awsEcrDomain,
@@ -353,6 +357,53 @@ describe("containers registries configure", () => {
`containers registries configure ${awsEcrDomain} --public-credential=test-access-key-id --secret-name=AWS_Secret_Access_Key`
);
});
+
+ it("should reuse existing secret with --skip-confirmation", async () => {
+ const storeId = "test-store-id-reuse";
+ const secretName = "existing_secret";
+
+ mockStdIn.send("test-secret-value");
+ mockListSecretStores([
+ {
+ id: storeId,
+ account_id: "some-account-id",
+ name: "Default",
+ created: "2024-01-01T00:00:00Z",
+ modified: "2024-01-01T00:00:00Z",
+ },
+ ]);
+ mockListSecrets(storeId, [
+ {
+ id: "existing-secret-id",
+ store_id: storeId,
+ name: secretName,
+ comment: "",
+ scopes: ["containers"],
+ created: "2024-01-01T00:00:00Z",
+ modified: "2024-01-01T00:00:00Z",
+ status: "active",
+ },
+ ]);
+ mockPutRegistry({
+ domain: awsEcrDomain,
+ is_public: false,
+ auth: {
+ public_credential: "test-access-key-id",
+ private_credential: {
+ store_id: storeId,
+ secret_name: secretName,
+ },
+ },
+ kind: "ECR",
+ });
+
+ await runWrangler(
+ `containers registries configure ${awsEcrDomain} --public-credential=test-access-key-id --secret-name=${secretName} --skip-confirmation`
+ );
+
+ // Should not contain "created" message since we reused existing secret
+ expect(cliStd.stdout).not.toContain("created in Secrets Store");
+ });
});
});
});
diff --git a/packages/wrangler/src/__tests__/init.test.ts b/packages/wrangler/src/__tests__/init.test.ts
index 20eb84bfa34f..c2c6416f12de 100644
--- a/packages/wrangler/src/__tests__/init.test.ts
+++ b/packages/wrangler/src/__tests__/init.test.ts
@@ -97,6 +97,7 @@ describe("init", () => {
type: "yarn",
npx: "yarn",
dlx: ["yarn", "dlx"],
+ lockFiles: ["yarn.lock"],
};
(getPackageManager as Mock).mockResolvedValue(mockPackageManager);
diff --git a/packages/wrangler/src/autoconfig/details.ts b/packages/wrangler/src/autoconfig/details.ts
index 0275dcf5e914..30c5f2231e36 100644
--- a/packages/wrangler/src/autoconfig/details.ts
+++ b/packages/wrangler/src/autoconfig/details.ts
@@ -13,6 +13,7 @@ import {
import { Project } from "@netlify/build-info";
import { NodeFS } from "@netlify/build-info/node";
import { captureException } from "@sentry/node";
+import chalk from "chalk";
import dedent from "ts-dedent";
import { getCacheFolder } from "../config-cache";
import { getErrorType } from "../core/handle-errors";
@@ -231,6 +232,20 @@ async function detectFramework(
// This is populated after getBuildSettings() runs, which triggers the full detection chain.
const packageManager = convertDetectedPackageManager(project.packageManager);
+ const lockFileExists = packageManager.lockFiles.some((lockFile) =>
+ existsSync(join(projectPath, lockFile))
+ );
+
+ if (!lockFileExists) {
+ logger.warn(
+ "No lock file has been detected in the current working directory." +
+ " This might indicate that the project is part of a workspace. Auto-configuration of " +
+ `projects inside workspaces is limited. See ${chalk.hex("#3B818D")(
+ "https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/#workspaces"
+ )}`
+ );
+ }
+
if (await isPagesProject(projectPath, wranglerConfig, detectedFramework)) {
return {
detectedFramework: {
diff --git a/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts b/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts
index 7a0390ed3ead..5e721b02cf61 100644
--- a/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts
+++ b/packages/wrangler/src/autoconfig/frameworks/utils/packages.ts
@@ -1,5 +1,5 @@
import { parsePackageJSON, readFileSync } from "@cloudflare/workers-utils";
-import { findUpSync } from "find-up";
+import * as find from "empathic/find";
/**
* Checks wether a package is installed in a target project or not
@@ -35,9 +35,9 @@ export function getInstalledPackageVersion(
if (!packagePath) {
return undefined;
}
- const packageJsonPath = findUpSync("package.json", {
+ const packageJsonPath = find.file("package.json", {
cwd: packagePath,
- stopAt: opts.stopAtProjectPath === true ? projectPath : undefined,
+ last: opts.stopAtProjectPath === true ? projectPath : undefined,
});
if (!packageJsonPath) {
return undefined;
diff --git a/packages/wrangler/src/config-cache.ts b/packages/wrangler/src/config-cache.ts
index c52eef4da70d..09152afb0108 100644
--- a/packages/wrangler/src/config-cache.ts
+++ b/packages/wrangler/src/config-cache.ts
@@ -7,7 +7,7 @@ import {
} from "node:fs";
import * as path from "node:path";
import { getWranglerCacheDirFromEnv } from "@cloudflare/workers-utils";
-import { findUpSync } from "find-up";
+import * as find from "empathic/find";
import { isNonInteractiveOrCI } from "./is-interactive";
import { logger } from "./logger";
@@ -29,9 +29,7 @@ export function getCacheFolder(): string {
}
// Find node_modules using existing find-up logic
- const closestNodeModulesDirectory = findUpSync("node_modules", {
- type: "directory",
- });
+ const closestNodeModulesDirectory = find.dir("node_modules");
const nodeModulesCache = closestNodeModulesDirectory
? path.join(closestNodeModulesDirectory, ".cache", "wrangler")
diff --git a/packages/wrangler/src/containers/registries.ts b/packages/wrangler/src/containers/registries.ts
index e0beae0209b9..49be21d42500 100644
--- a/packages/wrangler/src/containers/registries.ts
+++ b/packages/wrangler/src/containers/registries.ts
@@ -44,44 +44,58 @@ import type { ImageRegistryAuth } from "@cloudflare/containers-shared/src/client
import type { Config } from "@cloudflare/workers-utils";
function _registryConfigureYargs(args: CommonYargsArgv) {
- return (
- args
- .positional("DOMAIN", {
- describe: "Domain to configure for the registry",
- type: "string",
- demandOption: true,
- })
- .option("public-credential", {
- type: "string",
- description:
- "The public part of the registry credentials, e.g. `AWS_ACCESS_KEY_ID` for ECR",
- demandOption: true,
- alias: ["aws-access-key-id"],
- })
- .option("secret-store-id", {
- type: "string",
- description:
- "The ID of the secret store to use to store the registry credentials.",
- demandOption: false,
- conflicts: ["disableSecretsStore"],
- })
- // TODO: allow users to provide an existing secret name
- // but then we can't get secrets by name, only id, so we would need to list all secrets and find the right one
- .option("secret-name", {
- type: "string",
- description:
- "The name for the secret the private registry credentials should be stored under.",
- demandOption: false,
- conflicts: ["disableSecretsStore"],
- })
- .option("disableSecretsStore", {
- type: "boolean",
- description:
- "Whether to disable secrets store integration. This should be set iff the compliance region is FedRAMP High.",
- demandOption: false,
- conflicts: ["secret-store-id", "secret-name"],
- })
- );
+ return args
+ .positional("DOMAIN", {
+ describe: "Domain to configure for the registry",
+ type: "string",
+ demandOption: true,
+ })
+ .option("public-credential", {
+ type: "string",
+ description:
+ "The public part of the registry credentials, e.g. `AWS_ACCESS_KEY_ID` for ECR",
+ demandOption: true,
+ alias: ["aws-access-key-id"],
+ })
+ .option("secret-store-id", {
+ type: "string",
+ description:
+ "The ID of the secret store to use to store the registry credentials.",
+ demandOption: false,
+ conflicts: ["disable-secrets-store"],
+ })
+ .option("secret-name", {
+ type: "string",
+ description:
+ "The name for the secret the private registry credentials should be stored under.",
+ demandOption: false,
+ conflicts: ["disable-secrets-store"],
+ })
+ .option("disable-secrets-store", {
+ type: "boolean",
+ description:
+ "Whether to disable secrets store integration. This should be set iff the compliance region is FedRAMP High.",
+ demandOption: false,
+ conflicts: ["secret-store-id", "secret-name"],
+ })
+ .option("skip-confirmation", {
+ type: "boolean",
+ description: "Skip confirmation prompt",
+ alias: "y",
+ default: false,
+ })
+ .check((yargs) => {
+ if (
+ yargs.skipConfirmation &&
+ !yargs.secretName &&
+ !yargs.disableSecretsStore
+ ) {
+ throw new Error(
+ "--secret-name is required when using --skip-confirmation"
+ );
+ }
+ return true;
+ });
}
async function registryConfigureCommand(
@@ -107,7 +121,7 @@ async function registryConfigureCommand(
if (isFedRAMPHigh) {
if (!configureArgs.disableSecretsStore) {
throw new UserError(
- "Secrets Store is not supported in FedRAMP compliance regions. You must set --disableSecretsStore."
+ "Secrets Store is not supported in FedRAMP compliance regions. You must set --disable-secrets-store."
);
}
} else {
@@ -125,7 +139,9 @@ async function registryConfigureCommand(
}
log(`Getting ${registryType.secretType}...\n`);
- const secret = await getSecret(registryType.secretType);
+ const privateCredential = await promptForRegistryPrivateCredential(
+ registryType.secretType
+ );
// Secret Store is not available in FedRAMP High
let private_credential: ImageRegistryAuth["private_credential"];
@@ -137,12 +153,15 @@ async function registryConfigureCommand(
const stores = await listStores(config, accountId);
if (stores.length === 0) {
const defaultStoreName = "default_secret_store";
- const yes = await confirm(
- `No existing Secret Stores found. Create a Secret Store to store your registry credentials?`
- );
- if (!yes) {
- endSection("Cancelled.");
- return;
+ if (!configureArgs.skipConfirmation) {
+ const yes = await confirm(
+ `No existing Secret Stores found. Create a Secret Store to store your registry credentials?`
+ );
+
+ if (!yes) {
+ endSection("Cancelled.");
+ return;
+ }
}
const res = await promiseSpinner(
createStore(config, accountId, { name: defaultStoreName })
@@ -164,36 +183,22 @@ async function registryConfigureCommand(
log("\n");
- while (!secretName) {
- try {
- const res = await prompt(`Secret name:`, {
- defaultValue: `${registryType.secretType?.replaceAll(" ", "_")}`,
- });
-
- validateSecretName(res);
- secretName = res;
- } catch (e) {
- log((e as Error).message);
- continue;
- }
- }
+ secretName = await getOrCreateSecret({
+ configureArgs: configureArgs,
+ config: config,
+ accountId: accountId,
+ storeId: secretStoreId,
+ privateCredential,
+ secretType: registryType.secretType,
+ });
- await promiseSpinner(
- createSecret(config, accountId, secretStoreId, {
- name: secretName,
- value: secret,
- scopes: ["containers"],
- comment: `Created by Wrangler: credentials for image registry ${configureArgs.DOMAIN}`,
- })
- );
private_credential = {
store_id: secretStoreId,
secret_name: secretName,
};
- log(`Container-scoped secret ${secretName} created in Secrets Store.\n`);
} else {
// If we are not using the secret store, we will be passing in the secret directly
- private_credential = secret;
+ private_credential = privateCredential;
}
try {
@@ -227,7 +232,95 @@ async function registryConfigureCommand(
endSection("Registry configuration completed");
}
-async function getSecret(secretType?: string): Promise {
+async function promptForSecretName(secretType?: string): Promise {
+ while (true) {
+ try {
+ const res = await prompt(`Secret name:`, {
+ defaultValue: `${secretType?.replaceAll(" ", "_")}`,
+ });
+
+ validateSecretName(res);
+ return res;
+ } catch (e) {
+ log((e as Error).message);
+ continue;
+ }
+ }
+}
+
+interface GetOrCreateSecretOptions {
+ configureArgs: StrictYargsOptionsToInterface;
+ config: Config;
+ accountId: string;
+ storeId: string;
+ privateCredential: string;
+ secretType?: string;
+}
+
+async function getOrCreateSecret(
+ options: GetOrCreateSecretOptions
+): Promise {
+ let secretName =
+ options.configureArgs.secretName ??
+ (await promptForSecretName(options.secretType));
+
+ while (true) {
+ const existingSecretId = await getSecretByName(
+ options.config,
+ options.accountId,
+ options.storeId,
+ secretName
+ );
+
+ // secret doesn't exist - make a new one
+ if (!existingSecretId) {
+ await promiseSpinner(
+ createSecret(options.config, options.accountId, options.storeId, {
+ name: secretName,
+ value: options.privateCredential,
+ scopes: ["containers"],
+ comment: `Created by Wrangler: credentials for image registry ${options.configureArgs.DOMAIN}`,
+ })
+ );
+
+ log(
+ `Container-scoped secret "${secretName}" created in Secrets Store.\n`
+ );
+
+ return secretName;
+ }
+
+ // secret exists + skipConfirmation - default to reusing the secret
+ if (options.configureArgs.skipConfirmation) {
+ log(
+ `Using existing secret "${secretName}" from secret store with id: ${options.storeId}.\n`
+ );
+ return secretName;
+ }
+
+ // secret exists but not skipping confirmation - ask user if they want to reuse the secret
+ startSection(
+ `The provided secret name "${secretName}" is already in-use within the secret store. (Store ID: ${options.storeId})`
+ );
+
+ const reuseExisting = await confirm(
+ `Do you want to reuse the existing secret? If not, then you'll be prompted to pick a new name.`
+ );
+
+ if (reuseExisting) {
+ log(
+ `Using existing secret "${secretName}" from secret store with id: ${options.storeId}.\n`
+ );
+ return secretName;
+ }
+
+ secretName = await promptForSecretName(options.secretType);
+ }
+}
+
+async function promptForRegistryPrivateCredential(
+ secretType?: string
+): Promise {
if (isNonInteractiveOrCI()) {
// Non-interactive mode: expect JSON input via stdin
const stdinInput = trimTrailingWhitespace(await readFromStdin());
@@ -422,24 +515,41 @@ export const containersRegistriesConfigureCommand = createCommand({
description:
"The ID of the secret store to use to store the registry credentials.",
demandOption: false,
- conflicts: "disableSecretsStore",
+ conflicts: "disable-secrets-store",
},
"secret-name": {
type: "string",
description:
"The name for the secret the private registry credentials should be stored under.",
demandOption: false,
- conflicts: "disableSecretsStore",
+ conflicts: "disable-secrets-store",
},
- disableSecretsStore: {
+ "disable-secrets-store": {
type: "boolean",
description:
"Whether to disable secrets store integration. This should be set iff the compliance region is FedRAMP High.",
demandOption: false,
conflicts: ["secret-store-id", "secret-name"],
},
+ "skip-confirmation": {
+ type: "boolean",
+ description: "Skip confirmation prompts",
+ alias: "y",
+ default: false,
+ },
},
positionalArgs: ["DOMAIN"],
+ validateArgs(args) {
+ if (
+ args.skipConfirmation &&
+ !args.secretName &&
+ !args.disableSecretsStore
+ ) {
+ throw new UserError(
+ "--secret-name is required when using --skip-confirmation"
+ );
+ }
+ },
async handler(args, { config }) {
await fillOpenAPIConfiguration(config, containersScope);
await registryConfigureCommand(args, config);
diff --git a/packages/wrangler/src/package-manager.ts b/packages/wrangler/src/package-manager.ts
index 32c92397e5a7..9bca8eed8e73 100644
--- a/packages/wrangler/src/package-manager.ts
+++ b/packages/wrangler/src/package-manager.ts
@@ -7,6 +7,7 @@ export interface PackageManager {
type: "npm" | "yarn" | "pnpm" | "bun";
npx: string;
dlx: string[];
+ lockFiles: string[];
}
export async function getPackageManager(): Promise {
@@ -67,38 +68,42 @@ export function getPackageManagerName(packageManager: PackageManager): string {
/**
* Manage packages using npm
*/
-export const NpmPackageManager: PackageManager = {
+export const NpmPackageManager = {
type: "npm",
npx: "npx",
dlx: ["npx"],
-};
+ lockFiles: ["package-lock.json"],
+} as const satisfies PackageManager;
/**
* Manage packages using pnpm
*/
-export const PnpmPackageManager: PackageManager = {
+export const PnpmPackageManager = {
type: "pnpm",
npx: "pnpm",
+ lockFiles: ["pnpm-lock.yaml"],
dlx: ["pnpm", "dlx"],
-};
+} as const satisfies PackageManager;
/**
* Manage packages using yarn
*/
-export const YarnPackageManager: PackageManager = {
+export const YarnPackageManager = {
type: "yarn",
npx: "yarn",
dlx: ["yarn", "dlx"],
-};
+ lockFiles: ["yarn.lock"],
+} as const satisfies PackageManager;
/**
* Manage packages using bun
*/
-export const BunPackageManager: PackageManager = {
+export const BunPackageManager = {
type: "bun",
npx: "bunx",
dlx: ["bunx"],
-};
+ lockFiles: ["bun.lockb", "bun.lock"],
+} as const satisfies PackageManager;
async function supports(name: string): Promise {
try {
diff --git a/packages/wrangler/src/pages/utils.ts b/packages/wrangler/src/pages/utils.ts
index e452bb856416..e14afaa2cfc9 100644
--- a/packages/wrangler/src/pages/utils.ts
+++ b/packages/wrangler/src/pages/utils.ts
@@ -1,5 +1,5 @@
import path from "node:path";
-import { findUpSync } from "find-up";
+import * as find from "empathic/find";
import { getWranglerTmpDir } from "../paths";
import type { BundleResult } from "../deployment-bundle/bundle";
@@ -47,7 +47,7 @@ export function getPagesProjectRoot(): string {
if (projectRootCache !== undefined && projectRootCacheCwd === cwd) {
return projectRootCache;
}
- const packagePath = findUpSync("package.json");
+ const packagePath = find.file("package.json");
projectRootCache = packagePath ? path.dirname(packagePath) : process.cwd();
projectRootCacheCwd = cwd;
return projectRootCache;
diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts
index a842f4f7f760..e8611c779309 100644
--- a/packages/wrangler/src/type-generation/index.ts
+++ b/packages/wrangler/src/type-generation/index.ts
@@ -10,7 +10,7 @@ import {
UserError,
} from "@cloudflare/workers-utils";
import chalk from "chalk";
-import { findUpSync } from "find-up";
+import * as find from "empathic/find";
import { getNodeCompat } from "miniflare";
import { readConfig } from "../config";
import { createCommand } from "../core/create-command";
@@ -1257,7 +1257,7 @@ function generatePerEnvTypeStrings(
* @throws {UserError} If a non-Wrangler .d.ts file already exists at the given path.
*/
const validateTypesFile = (path: string): void => {
- const wranglerOverrideDTSPath = findUpSync(path);
+ const wranglerOverrideDTSPath = find.file(path);
if (wranglerOverrideDTSPath === undefined) {
return;
}
diff --git a/packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts b/packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts
index 09b143292b60..03b09cdb401e 100644
--- a/packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts
+++ b/packages/wrangler/src/type-generation/runtime/log-runtime-types-message.ts
@@ -1,7 +1,6 @@
import { existsSync } from "node:fs";
-import { join } from "node:path";
import chalk from "chalk";
-import { findUpMultipleSync } from "find-up";
+import * as find from "empathic/find";
import { logger } from "../../logger";
/**
@@ -42,14 +41,9 @@ export function logRuntimeTypesMessage(
);
if (!isNodeTypesInstalled && isNodeCompat) {
- const nodeModules = findUpMultipleSync("node_modules", {
- type: "directory",
- });
- for (const folder of nodeModules) {
- if (nodeModules && existsSync(join(folder, "@types/node"))) {
- isNodeTypesInstalled = true;
- break;
- }
+ const nodeModules = find.dir("node_modules/@types/node");
+ if (nodeModules) {
+ isNodeTypesInstalled = true;
}
}
if (isNodeCompat && !isNodeTypesInstalled) {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d13eb814199b..d9a4a1626a6a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4032,12 +4032,12 @@ importers:
concurrently:
specifier: ^8.2.2
version: 8.2.2
+ empathic:
+ specifier: ^2.0.0
+ version: 2.0.0
eslint:
specifier: catalog:default
version: 9.39.1(jiti@2.6.1)
- find-up:
- specifier: ^6.3.0
- version: 6.3.0
jsonc-parser:
specifier: catalog:default
version: 3.2.0
@@ -4290,6 +4290,9 @@ importers:
dotenv-expand:
specifier: ^12.0.2
version: 12.0.2
+ empathic:
+ specifier: ^2.0.0
+ version: 2.0.0
eslint:
specifier: catalog:default
version: 9.39.1(jiti@2.6.1)(supports-color@9.2.2)
@@ -4299,9 +4302,6 @@ importers:
execa:
specifier: ^6.1.0
version: 6.1.0
- find-up:
- specifier: ^6.3.0
- version: 6.3.0
get-port:
specifier: ^7.0.0
version: 7.0.0
@@ -4449,12 +4449,12 @@ importers:
'@typescript-eslint/parser':
specifier: catalog:default
version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
+ empathic:
+ specifier: ^2.0.0
+ version: 2.0.0
eslint:
specifier: catalog:default
version: 9.39.1(jiti@2.6.1)
- find-up:
- specifier: ^6.3.0
- version: 6.3.0
glob:
specifier: ^11.0.3
version: 11.0.3
@@ -10681,10 +10681,6 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
- find-up@6.3.0:
- resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
find-up@7.0.0:
resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==}
engines: {node: '>=18'}
@@ -11812,10 +11808,6 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
- locate-path@7.1.0:
- resolution: {integrity: sha512-HNx5uOnYeK4SxEoid5qnhRfprlJeGMzFRKPLCf/15N3/B4AiofNwC/yq7VBKdVk9dx7m+PiYCJOGg55JYTAqoQ==}
- engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
-
locate-path@7.2.0:
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -22111,11 +22103,6 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
- find-up@6.3.0:
- dependencies:
- locate-path: 7.1.0
- path-exists: 5.0.0
-
find-up@7.0.0:
dependencies:
locate-path: 7.2.0
@@ -23192,10 +23179,6 @@ snapshots:
dependencies:
p-locate: 5.0.0
- locate-path@7.1.0:
- dependencies:
- p-locate: 6.0.0
-
locate-path@7.2.0:
dependencies:
p-locate: 6.0.0
diff --git a/tools/package.json b/tools/package.json
index 1c87ec716eaf..319b26c5ab79 100644
--- a/tools/package.json
+++ b/tools/package.json
@@ -17,8 +17,8 @@
"@types/semver": "^7.5.1",
"@typescript-eslint/eslint-plugin": "catalog:default",
"@typescript-eslint/parser": "catalog:default",
+ "empathic": "^2.0.0",
"eslint": "catalog:default",
- "find-up": "^6.3.0",
"glob": "^11.0.3",
"semver": "^7.7.1",
"ts-dedent": "^2.2.0",
diff --git a/tools/test/run-test-file.ts b/tools/test/run-test-file.ts
index 00645992d605..ce437153f13c 100644
--- a/tools/test/run-test-file.ts
+++ b/tools/test/run-test-file.ts
@@ -1,12 +1,12 @@
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
-import { findUpSync } from "find-up";
+import * as find from "empathic/find";
const currentFile = process.argv[2];
const currentDirectory = path.dirname(currentFile);
-const packageJsonPath = findUpSync("package.json", { cwd: currentDirectory });
+const packageJsonPath = find.file("package.json", { cwd: currentDirectory });
if (!packageJsonPath) {
console.error("No package.json found.");