@odla-ai/cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/bin.cjs +116 -32
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-BHAEDEL2.js → chunk-MJYQ7YDH.js} +112 -26
- package/dist/chunk-MJYQ7YDH.js.map +1 -0
- package/dist/index.cjs +120 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -219
- package/dist/index.d.ts +54 -219
- package/dist/index.js +5 -1
- package/llms.txt +75 -222
- package/package.json +1 -1
- package/skills/odla-migrate/references/phase-3b-user-sync.md +15 -8
- package/skills/odla-migrate/references/secrets-map.md +16 -2
- package/dist/chunk-BHAEDEL2.js.map +0 -1
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ For a project you keep working in, install it as a dev dependency so the
|
|
|
29
29
|
shorter `npx odla-ai` form works and the version is pinned by your lockfile:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npm view @odla-ai/cli@0.
|
|
33
|
-
npm i -D --save-exact @odla-ai/cli@0.
|
|
32
|
+
npm view @odla-ai/cli@0.9.0 version
|
|
33
|
+
npm i -D --save-exact @odla-ai/cli@0.9.0
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Prerequisites
|
|
@@ -59,6 +59,8 @@ npx odla-ai provision --dry-run
|
|
|
59
59
|
npx odla-ai provision --write-dev-vars --push-secrets
|
|
60
60
|
npx odla-ai smoke --env dev
|
|
61
61
|
npx odla-ai secrets push --env dev
|
|
62
|
+
npx odla-ai secrets set clerk_webhook_secret --env dev --stdin
|
|
63
|
+
npx odla-ai secrets set-clerk-key --env dev --from-env CLERK_SECRET_KEY
|
|
62
64
|
npx odla-ai security run . --env dev --ack-redacted-source
|
|
63
65
|
npx odla-ai security github connect --env dev # infers owner/name from git origin
|
|
64
66
|
npx odla-ai security plan --env dev
|
|
@@ -261,6 +263,22 @@ redacted plan without spawning anything. For the normal path, use
|
|
|
261
263
|
`provision --write-dev-vars --push-secrets` so service enablement, credential
|
|
262
264
|
issuance, local config, and deployed secret transfer stay one composition.
|
|
263
265
|
|
|
266
|
+
`secrets set <name> --env <env>` stores one named secret in that env's tenant
|
|
267
|
+
vault — the same write-only slot Studio's Secrets panel fills (for example
|
|
268
|
+
`clerk_webhook_secret` for synced auth). The value may come only from
|
|
269
|
+
`--from-env <NAME>` or `--stdin`, so a producer command pipes straight into the
|
|
270
|
+
vault without the secret ever appearing on argv, in output, or in an agent's
|
|
271
|
+
transcript; it is encrypted at rest and readable back only by that tenant's
|
|
272
|
+
app API key, never by the CLI, Studio, or the developer token. `$`-prefixed
|
|
273
|
+
names are platform-reserved. `secrets set-clerk-key --env <env>` stores the
|
|
274
|
+
app's Clerk secret key in the reserved `$clerk_secret` slot so the platform can
|
|
275
|
+
resolve the app's users (invites, member lookups); reserved secrets are never
|
|
276
|
+
readable by app keys. It refuses an `sk_test_` key for a prod-named env and
|
|
277
|
+
requires `--yes` to put an `sk_live_` key into a non-prod env, since live users
|
|
278
|
+
would sync into a non-prod tenant. Both commands authenticate with the
|
|
279
|
+
developer token (`$ODLA_DEV_TOKEN`, the cached token, or a fresh device
|
|
280
|
+
approval), and prod-named envs require `--yes`, matching `secrets push`.
|
|
281
|
+
|
|
264
282
|
## Who does what
|
|
265
283
|
|
|
266
284
|
If a change is deterministic from `odla.config.mjs`, the CLI owns it: app and
|
package/dist/bin.cjs
CHANGED
|
@@ -28,7 +28,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
28
28
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
29
|
|
|
30
30
|
// src/admin-ai.ts
|
|
31
|
-
var
|
|
31
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
32
32
|
|
|
33
33
|
// src/token.ts
|
|
34
34
|
var import_db = require("@odla-ai/db");
|
|
@@ -258,9 +258,32 @@ function platformAudience(value) {
|
|
|
258
258
|
return url.origin;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
// src/secret-input.ts
|
|
262
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
263
|
+
var MAX_BYTES = 64 * 1024;
|
|
264
|
+
async function secretInputValue(options, kind = "credential") {
|
|
265
|
+
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
266
|
+
let value;
|
|
267
|
+
if (options.fromEnv) value = import_node_process3.default.env[options.fromEnv];
|
|
268
|
+
else if (options.stdin) value = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
269
|
+
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
270
|
+
value = value?.replace(/[\r\n]+$/, "");
|
|
271
|
+
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : `stdin ${kind} is empty`);
|
|
272
|
+
if (new TextEncoder().encode(value).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
async function readSecretStream(kind, stream = import_node_process3.default.stdin) {
|
|
276
|
+
let value = "";
|
|
277
|
+
for await (const chunk of stream) {
|
|
278
|
+
value += String(chunk);
|
|
279
|
+
if (value.length > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
|
|
261
284
|
// src/admin-ai-auth.ts
|
|
262
285
|
var import_node_fs2 = require("fs");
|
|
263
|
-
var
|
|
286
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
264
287
|
var import_db2 = require("@odla-ai/db");
|
|
265
288
|
async function getScopedPlatformToken(options) {
|
|
266
289
|
return resolveAdminPlatformToken(options);
|
|
@@ -268,7 +291,7 @@ async function getScopedPlatformToken(options) {
|
|
|
268
291
|
async function resolveAdminPlatformToken(options) {
|
|
269
292
|
const audience = platformAudience(options.platform);
|
|
270
293
|
if (options.token) return options.token;
|
|
271
|
-
const fromEnv =
|
|
294
|
+
const fromEnv = import_node_process4.default.env.ODLA_ADMIN_TOKEN;
|
|
272
295
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
273
296
|
return scopedToken(
|
|
274
297
|
audience,
|
|
@@ -280,7 +303,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
280
303
|
}
|
|
281
304
|
function audienceBoundEnvToken(token, platform) {
|
|
282
305
|
const audience = platformAudience(platform);
|
|
283
|
-
const declared =
|
|
306
|
+
const declared = import_node_process4.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
284
307
|
if (declared) {
|
|
285
308
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
286
309
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -290,7 +313,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
290
313
|
}
|
|
291
314
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
292
315
|
const audience = platformAudience(platform);
|
|
293
|
-
const tokenFile = options.tokenFile ?? `${
|
|
316
|
+
const tokenFile = options.tokenFile ?? `${import_node_process4.default.cwd()}/.odla/admin-token.local.json`;
|
|
294
317
|
const cache = readJsonFile(tokenFile);
|
|
295
318
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
296
319
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
@@ -305,13 +328,13 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
305
328
|
onCode: async ({ userCode, expiresIn }) => {
|
|
306
329
|
const approvalUrl = handshakeUrl(audience, userCode);
|
|
307
330
|
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
308
|
-
const shouldOpen = options.open === true || options.open !== false && !
|
|
331
|
+
const shouldOpen = options.open === true || options.open !== false && !import_node_process4.default.env.CI && Boolean(import_node_process4.default.stdin.isTTY && import_node_process4.default.stdout.isTTY);
|
|
309
332
|
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
310
333
|
}
|
|
311
334
|
});
|
|
312
335
|
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
313
336
|
tokens[scope] = { token, expiresAt };
|
|
314
|
-
if ((0, import_node_fs2.existsSync)(`${
|
|
337
|
+
if ((0, import_node_fs2.existsSync)(`${import_node_process4.default.cwd()}/.git`)) ensureGitignore(import_node_process4.default.cwd(), [tokenFile]);
|
|
315
338
|
writePrivateJson(tokenFile, { platform: audience, tokens });
|
|
316
339
|
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
317
340
|
return token;
|
|
@@ -472,7 +495,7 @@ function isRecord2(value) {
|
|
|
472
495
|
|
|
473
496
|
// src/admin-ai.ts
|
|
474
497
|
async function adminAi(options) {
|
|
475
|
-
const platform = platformAudience(options.platform ??
|
|
498
|
+
const platform = platformAudience(options.platform ?? import_node_process5.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
476
499
|
const doFetch = options.fetch ?? fetch;
|
|
477
500
|
const out = options.stdout ?? console;
|
|
478
501
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -511,7 +534,7 @@ async function adminAi(options) {
|
|
|
511
534
|
}
|
|
512
535
|
if (options.action === "credential-set") {
|
|
513
536
|
const provider = requireProvider(options.credentialProvider);
|
|
514
|
-
const value = await
|
|
537
|
+
const value = await secretInputValue(options);
|
|
515
538
|
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
|
|
516
539
|
method: "PUT",
|
|
517
540
|
headers,
|
|
@@ -621,25 +644,6 @@ function requireProvider(value) {
|
|
|
621
644
|
}
|
|
622
645
|
return value;
|
|
623
646
|
}
|
|
624
|
-
async function credentialValue(options) {
|
|
625
|
-
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
626
|
-
let value;
|
|
627
|
-
if (options.fromEnv) value = import_node_process4.default.env[options.fromEnv];
|
|
628
|
-
else if (options.stdin) value = await (options.readStdin ?? readStdin)();
|
|
629
|
-
else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
|
|
630
|
-
value = value?.replace(/[\r\n]+$/, "");
|
|
631
|
-
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
|
|
632
|
-
if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
633
|
-
return value;
|
|
634
|
-
}
|
|
635
|
-
async function readStdin() {
|
|
636
|
-
let value = "";
|
|
637
|
-
for await (const chunk of import_node_process4.default.stdin) {
|
|
638
|
-
value += String(chunk);
|
|
639
|
-
if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
640
|
-
}
|
|
641
|
-
return value;
|
|
642
|
-
}
|
|
643
647
|
function policyArray(body) {
|
|
644
648
|
if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
|
|
645
649
|
if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
|
|
@@ -809,6 +813,7 @@ var CAPABILITIES = {
|
|
|
809
813
|
"register the app and enable configured services per environment",
|
|
810
814
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
811
815
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
816
|
+
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
812
817
|
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
813
818
|
"validate config offline and smoke-test a provisioned db environment",
|
|
814
819
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
@@ -1265,6 +1270,8 @@ Usage:
|
|
|
1265
1270
|
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1266
1271
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1267
1272
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1273
|
+
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
|
|
1274
|
+
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
|
|
1268
1275
|
odla-ai version
|
|
1269
1276
|
|
|
1270
1277
|
Commands:
|
|
@@ -1278,7 +1285,9 @@ Commands:
|
|
|
1278
1285
|
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1279
1286
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1280
1287
|
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1281
|
-
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
1288
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
1289
|
+
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
1290
|
+
reserved Clerk secret key, write-only from stdin or an env var.
|
|
1282
1291
|
version Print the CLI version.
|
|
1283
1292
|
|
|
1284
1293
|
Safety:
|
|
@@ -1436,7 +1445,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
1436
1445
|
// src/provision.ts
|
|
1437
1446
|
var import_apps2 = require("@odla-ai/apps");
|
|
1438
1447
|
var import_ai = require("@odla-ai/ai");
|
|
1439
|
-
var
|
|
1448
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
1440
1449
|
|
|
1441
1450
|
// src/provision-credentials.ts
|
|
1442
1451
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -1740,7 +1749,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
1740
1749
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
1741
1750
|
}
|
|
1742
1751
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1743
|
-
const key =
|
|
1752
|
+
const key = import_node_process6.default.env[cfg.ai.keyEnv];
|
|
1744
1753
|
if (key) {
|
|
1745
1754
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
1746
1755
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -1800,6 +1809,63 @@ async function safeText2(res) {
|
|
|
1800
1809
|
}
|
|
1801
1810
|
}
|
|
1802
1811
|
|
|
1812
|
+
// src/secrets-set.ts
|
|
1813
|
+
var import_ai2 = require("@odla-ai/ai");
|
|
1814
|
+
var import_apps3 = require("@odla-ai/apps");
|
|
1815
|
+
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
1816
|
+
async function secretsSet(options) {
|
|
1817
|
+
const name = (options.name ?? "").trim();
|
|
1818
|
+
if (!name) throw new Error('secret name is required, e.g. "odla-ai secrets set clerk_webhook_secret --env dev --stdin"');
|
|
1819
|
+
if (name.startsWith("$")) {
|
|
1820
|
+
throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
|
|
1821
|
+
}
|
|
1822
|
+
const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
|
|
1823
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
1824
|
+
try {
|
|
1825
|
+
await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value);
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value));
|
|
1828
|
+
}
|
|
1829
|
+
out.log(`${name} stored in the ${tenantId} vault (write-only; the value was never echoed and cannot be read back here)`);
|
|
1830
|
+
}
|
|
1831
|
+
async function secretsSetClerkKey(options) {
|
|
1832
|
+
const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
|
|
1833
|
+
if (!value.startsWith("sk_")) throw new Error("the Clerk secret key must start with sk_ (sk_test_\u2026 or sk_live_\u2026)");
|
|
1834
|
+
if (value.startsWith("sk_test_") && PROD_ENV_NAMES2.has(options.env)) {
|
|
1835
|
+
throw new Error(`refusing to store an sk_test_ Clerk key for "${options.env}" \u2014 use the production instance's sk_live_ key`);
|
|
1836
|
+
}
|
|
1837
|
+
if (value.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
1838
|
+
throw new Error(`refusing to store an sk_live_ Clerk key for "${options.env}" without --yes (live users would sync into a non-prod tenant)`);
|
|
1839
|
+
}
|
|
1840
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
1841
|
+
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
|
|
1842
|
+
method: "POST",
|
|
1843
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1844
|
+
body: JSON.stringify({ value })
|
|
1845
|
+
});
|
|
1846
|
+
if (!res.ok) {
|
|
1847
|
+
const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value);
|
|
1848
|
+
throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
|
|
1849
|
+
}
|
|
1850
|
+
out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
|
|
1851
|
+
}
|
|
1852
|
+
function scrubValue(text, value) {
|
|
1853
|
+
return redactSecrets(text).split(value).join("[value redacted]");
|
|
1854
|
+
}
|
|
1855
|
+
async function resolveVaultWrite(options) {
|
|
1856
|
+
const out = options.stdout ?? console;
|
|
1857
|
+
const doFetch = options.fetch ?? fetch;
|
|
1858
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1859
|
+
if (!cfg.envs.includes(options.env)) {
|
|
1860
|
+
throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
|
|
1861
|
+
}
|
|
1862
|
+
if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
1863
|
+
throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
|
|
1864
|
+
}
|
|
1865
|
+
const value = await secretInputValue(options, "secret");
|
|
1866
|
+
return { cfg, tenantId: (0, import_apps3.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1803
1869
|
// src/security-command-context.ts
|
|
1804
1870
|
var import_promises = require("readline/promises");
|
|
1805
1871
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
@@ -3130,8 +3196,26 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
3130
3196
|
}
|
|
3131
3197
|
if (command === "secrets") {
|
|
3132
3198
|
const sub = parsed.positionals[1];
|
|
3199
|
+
if (sub === "set" || sub === "set-clerk-key") {
|
|
3200
|
+
assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
|
|
3201
|
+
const options = {
|
|
3202
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
3203
|
+
name: parsed.positionals[2],
|
|
3204
|
+
env: requiredString(parsed.options.env, "--env"),
|
|
3205
|
+
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
3206
|
+
stdin: parsed.options.stdin === true,
|
|
3207
|
+
token: stringOpt(parsed.options.token),
|
|
3208
|
+
yes: parsed.options.yes === true,
|
|
3209
|
+
fetch: dependencies.fetch,
|
|
3210
|
+
stdout: dependencies.stdout
|
|
3211
|
+
};
|
|
3212
|
+
await (sub === "set" ? secretsSet(options) : secretsSetClerkKey(options));
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3133
3215
|
if (sub !== "push") {
|
|
3134
|
-
throw new Error(
|
|
3216
|
+
throw new Error(
|
|
3217
|
+
`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev", "odla-ai secrets set <name> --env dev --stdin", or "odla-ai secrets set-clerk-key --env dev --stdin".`
|
|
3218
|
+
);
|
|
3135
3219
|
}
|
|
3136
3220
|
assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
|
|
3137
3221
|
await secretsPush({
|