@odla-ai/cli 0.28.2 → 0.30.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/dist/bin.js CHANGED
@@ -174,7 +174,7 @@ function absoluteEntryPath(entryPath) {
174
174
 
175
175
  // src/bin.ts
176
176
  var argv = process.argv.slice(2);
177
- requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-2WZIFXA6.js")).runCli()).catch((err) => {
177
+ requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-CKBUVTJM.js")).runCli()).catch((err) => {
178
178
  console.error(redactSecrets(`odla-ai: ${err instanceof Error ? err.message : String(err)}`));
179
179
  process.exitCode = exitCodeFor(err);
180
180
  });
@@ -1131,7 +1131,7 @@ var DEFAULT_ENVS = ["dev"];
1131
1131
  var DEFAULT_SERVICES = ["db", "ai"];
1132
1132
  var configImportSerial = 0;
1133
1133
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1134
- async function loadProjectConfig(configPath = "odla.config.mjs") {
1134
+ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1135
1135
  const resolved = resolve2(configPath);
1136
1136
  if (!existsSync3(resolved)) {
1137
1137
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
@@ -1144,7 +1144,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
1144
1144
  const envs = unique2(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
1145
1145
  const services = unique2(raw.services?.length ? raw.services : DEFAULT_SERVICES);
1146
1146
  validateServices(services, resolved);
1147
- validateCalendarConfig(raw, envs, services, resolved);
1147
+ validateCalendarConfig(raw, unique2([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1148
1148
  const local = {
1149
1149
  tokenFile: resolve2(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1150
1150
  credentialsFile: resolve2(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
@@ -1194,12 +1194,12 @@ function buildPlan(cfg) {
1194
1194
  }
1195
1195
  function calendarServiceConfig(cfg, env) {
1196
1196
  if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
1197
- if (!cfg.envs.includes(env)) throw new Error(`calendar env "${env}" is not declared in config envs`);
1197
+ if (!cfg.envs.includes(env) && env !== "prod") throw new Error(`calendar env "${env}" is not declared in config envs`);
1198
1198
  const google = cfg.calendar?.google;
1199
1199
  if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
1200
- const availability = unique2(
1201
- (google.availabilityCalendars?.[env] ?? google.calendars?.[env]).map((id) => id.trim())
1202
- );
1200
+ const configured = google.availabilityCalendars?.[env] ?? google.calendars?.[env];
1201
+ if (!configured?.length) throw new Error(`calendar.google.availabilityCalendars.${env} is required`);
1202
+ const availability = unique2(configured.map((id) => id.trim()));
1203
1203
  return {
1204
1204
  provider: "google",
1205
1205
  access: "book",
@@ -1266,13 +1266,18 @@ function validateCalendarConfig(cfg, envs, services, path) {
1266
1266
  }
1267
1267
  const availability = google[availabilityKey];
1268
1268
  if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1269
- const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
1269
+ const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env) && env !== "prod");
1270
1270
  if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1271
1271
  for (const env of envs) {
1272
1272
  const ids = availability[env];
1273
1273
  if (!Array.isArray(ids) || ids.length === 0) {
1274
1274
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1275
1275
  }
1276
+ }
1277
+ for (const [env, ids] of Object.entries(availability)) {
1278
+ if (!Array.isArray(ids) || ids.length === 0) {
1279
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1280
+ }
1276
1281
  if (ids.length > 10) {
1277
1282
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1278
1283
  }
@@ -1282,7 +1287,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1282
1287
  }
1283
1288
  if (google.bookingCalendar !== void 0) {
1284
1289
  if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1285
- const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
1290
+ const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env) && env !== "prod");
1286
1291
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1287
1292
  for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1288
1293
  if (!safeText3(value2, 1024)) {
@@ -1292,7 +1297,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1292
1297
  }
1293
1298
  if (google.bookingPageUrl !== void 0) {
1294
1299
  if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1295
- const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
1300
+ const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env) && env !== "prod");
1296
1301
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1297
1302
  for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
1298
1303
  if (value2 !== null && !safeHttpsUrl(value2)) {
@@ -1838,7 +1843,7 @@ async function authCommand(parsed, deps = {}) {
1838
1843
  import { tenantIdFor } from "@odla-ai/apps";
1839
1844
  function resolveEnv(cfg, requested) {
1840
1845
  const env = requested ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
1841
- if (!env || !cfg.envs.includes(env)) {
1846
+ if (!env || !cfg.envs.includes(env) && env !== "prod") {
1842
1847
  throw new Error(`env "${env ?? ""}" is not declared in ${cfg.configPath}`);
1843
1848
  }
1844
1849
  return env;
@@ -1848,12 +1853,10 @@ function resolveTenant(cfg, requested) {
1848
1853
  return { env, tenant: tenantIdFor(cfg.app.id, env) };
1849
1854
  }
1850
1855
  function bothTenants(cfg) {
1851
- for (const env of ["dev", "prod"]) {
1852
- if (!cfg.envs.includes(env)) {
1853
- throw new Error(
1854
- `env "${env}" is not declared in ${cfg.configPath} \u2014 add it to \`envs\` and run \`odla-ai provision --yes\` before transferring data between environments`
1855
- );
1856
- }
1856
+ if (!cfg.envs.includes("dev")) {
1857
+ throw new Error(
1858
+ `env "dev" is not declared in ${cfg.configPath} \u2014 sandbox/live transfers require an existing sandbox`
1859
+ );
1857
1860
  }
1858
1861
  return { sandbox: tenantIdFor(cfg.app.id, "dev"), live: tenantIdFor(cfg.app.id, "prod") };
1859
1862
  }
@@ -1941,7 +1944,7 @@ function errorMessage(body) {
1941
1944
  // src/human-session.ts
1942
1945
  async function requireStudioHuman(configPath, action2, destination = "app", env) {
1943
1946
  const cfg = await loadProjectConfig(configPath);
1944
- const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
1947
+ const appEnv = env && (cfg.envs.includes(env) || env === "prod") ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
1945
1948
  const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
1946
1949
  throw new Error(
1947
1950
  `human_session_required: ${action2} requires the signed-in owner in Studio. A CLI device token is an agent credential, so another approval or retry cannot work. ${new URL(path, cfg.platformUrl).href}`
@@ -2586,8 +2589,7 @@ async function ensureCalendarConnected(ctx, options) {
2586
2589
  async function lifecycleContext(options, optionalProjectCapabilities = []) {
2587
2590
  const cfg = await loadProjectConfig(options.configPath);
2588
2591
  if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
2589
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
2590
- if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
2592
+ const env = resolveEnv(cfg, options.env);
2591
2593
  calendarServiceConfig(cfg, env);
2592
2594
  const out = options.stdout ?? console;
2593
2595
  const doFetch = options.fetch ?? fetch;
@@ -2788,8 +2790,7 @@ function printGroup(out, heading, items) {
2788
2790
  import { DEFAULT_CATALOG } from "@odla-ai/ai";
2789
2791
  async function aiModels(options = {}) {
2790
2792
  const cfg = await loadProjectConfig(options.configPath ?? "odla.config.mjs");
2791
- const env = options.env ?? cfg.envs[0] ?? "dev";
2792
- if (!cfg.envs.includes(env)) throw new Error(`ai models env "${env}" is not declared in config envs`);
2793
+ const env = resolveEnv(cfg, options.env);
2793
2794
  const url = new URL(`/registry/apps/${encodeURIComponent(cfg.app.id)}/public-config`, cfg.platformUrl);
2794
2795
  url.searchParams.set("env", env);
2795
2796
  const response2 = await (options.fetch ?? fetch)(url);
@@ -3830,6 +3831,41 @@ async function wranglerLoggedIn(run, cwd) {
3830
3831
  return false;
3831
3832
  }
3832
3833
  }
3834
+ async function wranglerRuntimeTarget(run, opts) {
3835
+ const configPath = findWranglerConfig(opts.cwd);
3836
+ if (!configPath) throw new Error(`no wrangler config found in ${opts.cwd}`);
3837
+ const config = readWranglerConfig(configPath);
3838
+ if (!config) throw new Error("runtime credential delivery requires wrangler.json or wrangler.jsonc");
3839
+ const baseName = typeof config.name === "string" ? config.name : "";
3840
+ const envs = config.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
3841
+ const envConfig = opts.env && envs[opts.env] && typeof envs[opts.env] === "object" ? envs[opts.env] : {};
3842
+ const explicitName = typeof envConfig.name === "string" ? envConfig.name : "";
3843
+ const serviceEnvironments = config.legacy_env === false;
3844
+ if (serviceEnvironments && explicitName) {
3845
+ throw new Error("env.<name>.name is not allowed when wrangler legacy_env is false");
3846
+ }
3847
+ const scriptName = serviceEnvironments ? baseName : explicitName || (opts.env ? `${baseName}-${opts.env}` : baseName);
3848
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(scriptName)) {
3849
+ throw new Error("wrangler config must resolve an exact Worker name before credentials are issued");
3850
+ }
3851
+ const configuredAccount = typeof envConfig.account_id === "string" ? envConfig.account_id : typeof config.account_id === "string" ? config.account_id : "";
3852
+ const whoami = await run("npx", ["wrangler", "whoami"], { cwd: opts.cwd });
3853
+ if (whoami.code !== 0 || /not authenticated/i.test(`${whoami.stdout}${whoami.stderr}`)) {
3854
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
3855
+ }
3856
+ const discovered = [...new Set(`${whoami.stdout}
3857
+ ${whoami.stderr}`.match(/\b[a-f0-9]{32}\b/gi)?.map((id) => id.toLowerCase()) ?? [])];
3858
+ const accountId = configuredAccount.toLowerCase() || (discovered.length === 1 ? discovered[0] : "");
3859
+ if (!/^[a-f0-9]{32}$/.test(accountId) || configuredAccount && discovered.length > 0 && !discovered.includes(accountId)) {
3860
+ throw new Error("Wrangler account is ambiguous or does not match account_id in the config");
3861
+ }
3862
+ return {
3863
+ provider: "cloudflare",
3864
+ accountId,
3865
+ scriptName,
3866
+ ...opts.env ? { environment: opts.env } : {}
3867
+ };
3868
+ }
3833
3869
  function wranglerPutSecret(run, opts) {
3834
3870
  const args = [
3835
3871
  "wrangler",
@@ -3841,6 +3877,16 @@ function wranglerPutSecret(run, opts) {
3841
3877
  ];
3842
3878
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3843
3879
  }
3880
+ function wranglerBulkSecrets(run, opts) {
3881
+ const args = [
3882
+ "wrangler",
3883
+ "secret",
3884
+ "bulk",
3885
+ ...opts.env ? ["--env", opts.env] : [],
3886
+ ...opts.configPath ? ["--config", opts.configPath] : []
3887
+ ];
3888
+ return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
3889
+ }
3844
3890
 
3845
3891
  // src/doctor-checks.ts
3846
3892
  function lintRules(rules, entities, publicRead) {
@@ -4329,16 +4375,10 @@ var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
4329
4375
  async function secretsPush(options) {
4330
4376
  await secretsPushImpl(options, true);
4331
4377
  }
4332
- async function secretsPushAfterPreflight(options) {
4333
- await secretsPushImpl(options, false);
4334
- }
4335
4378
  async function secretsPushImpl(options, preflight) {
4336
4379
  const out = options.stdout ?? console;
4337
4380
  const cfg = await loadProjectConfig(options.configPath);
4338
- const env = options.env;
4339
- if (!cfg.envs.includes(env)) {
4340
- throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
4341
- }
4381
+ const env = resolveEnv(cfg, options.env);
4342
4382
  if (PROD_ENV_NAMES.has(env) && !options.yes) {
4343
4383
  throw new Error(`refusing to push a secret to "${env}" without --yes`);
4344
4384
  }
@@ -4448,14 +4488,12 @@ async function resolveVaultWrite(options) {
4448
4488
  const out = options.stdout ?? console;
4449
4489
  const doFetch = options.fetch ?? fetch;
4450
4490
  const cfg = await loadProjectConfig(options.configPath);
4451
- if (!cfg.envs.includes(options.env)) {
4452
- throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
4453
- }
4454
- if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
4455
- throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
4491
+ const env = resolveEnv(cfg, options.env);
4492
+ if (PROD_ENV_NAMES2.has(env) && !options.yes) {
4493
+ throw new Error(`refusing to store a secret for "${env}" without --yes`);
4456
4494
  }
4457
4495
  const value2 = await secretInputValue(options, "secret");
4458
- return { cfg, tenantId: tenantIdFor3(cfg.app.id, options.env), value: value2, doFetch, out };
4496
+ return { cfg, tenantId: tenantIdFor3(cfg.app.id, env), value: value2, doFetch, out };
4459
4497
  }
4460
4498
 
4461
4499
  // src/skill.ts
@@ -4742,8 +4780,7 @@ function listFiles(dir) {
4742
4780
  async function smoke(options) {
4743
4781
  const out = options.stdout ?? console;
4744
4782
  const cfg = await loadProjectConfig(options.configPath);
4745
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod");
4746
- if (!cfg.envs.includes(env)) throw new Error(`env "${env}" is not declared in ${displayPath(cfg.configPath, cfg.rootDir)}`);
4783
+ const env = resolveEnv(cfg, options.env);
4747
4784
  const credentials = readCredentials(cfg.local.credentialsFile);
4748
4785
  if (!credentials) {
4749
4786
  throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
@@ -8702,6 +8739,55 @@ function requireName(parsed) {
8702
8739
  return name;
8703
8740
  }
8704
8741
 
8742
+ // src/credential-command.ts
8743
+ async function responseError(response2) {
8744
+ return redactSecrets((await response2.text()).slice(0, 1e3));
8745
+ }
8746
+ async function credentialCommand(parsed, deps = {}) {
8747
+ const action2 = parsed.positionals[1] ?? "list";
8748
+ if (action2 !== "list" && action2 !== "revoke") {
8749
+ throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
8750
+ }
8751
+ assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
8752
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8753
+ const doFetch = deps.fetch ?? fetch;
8754
+ const out = deps.stdout ?? console;
8755
+ const token = await getDeveloperToken(cfg, {
8756
+ configPath: cfg.configPath,
8757
+ token: stringOpt(parsed.options.token),
8758
+ email: stringOpt(parsed.options.email),
8759
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
8760
+ openApprovalUrl: deps.openUrl
8761
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
8762
+ const base = `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials`;
8763
+ if (action2 === "revoke") {
8764
+ const id = parsed.positionals[2];
8765
+ if (!id) throw new Error("credentials revoke requires the exact receipt id from credentials list");
8766
+ const response3 = await doFetch(`${base}/${encodeURIComponent(id)}`, {
8767
+ method: "DELETE",
8768
+ headers: { authorization: `Bearer ${token}` }
8769
+ });
8770
+ if (!response3.ok) throw new Error(`runtime credential revoke failed (${response3.status}): ${await responseError(response3)}`);
8771
+ const body2 = await response3.json();
8772
+ return out.log(parsed.options.json === true ? JSON.stringify(body2, null, 2) : `revoked ${body2.receipt.id} (${body2.receipt.env} ${body2.receipt.target.scriptName})`);
8773
+ }
8774
+ const query = new URLSearchParams();
8775
+ const requestedEnv = stringOpt(parsed.options.env);
8776
+ if (requestedEnv) query.set("env", requestedEnv);
8777
+ const response2 = await doFetch(`${base}${query.size ? `?${query}` : ""}`, {
8778
+ headers: { authorization: `Bearer ${token}` }
8779
+ });
8780
+ if (!response2.ok) throw new Error(`runtime credential inventory failed (${response2.status}): ${await responseError(response2)}`);
8781
+ const body = await response2.json();
8782
+ const credentials = parsed.options.all === true ? body.credentials : body.credentials.filter((item) => item.state === "committed");
8783
+ if (parsed.options.json === true) return out.log(JSON.stringify({ credentials }, null, 2));
8784
+ if (!credentials.length) return out.log("no active runtime credential receipts");
8785
+ out.log("RECEIPT ENV TARGET CREATED");
8786
+ for (const item of credentials) {
8787
+ out.log(`${item.id} ${item.env} ${item.target.accountId ?? "local"}/${item.target.scriptName} ${new Date(item.createdAt).toISOString()}`);
8788
+ }
8789
+ }
8790
+
8705
8791
  // src/help-usage.ts
8706
8792
  var USAGE_SECTION = `
8707
8793
  Start here:
@@ -8819,7 +8905,9 @@ Usage:
8819
8905
  odla-ai security report <job-id> [--json]
8820
8906
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8821
8907
  odla-ai security run [target] --self --ack-redacted-source
8822
- odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
8908
+ odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
8909
+ odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
8910
+ odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
8823
8911
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8824
8912
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8825
8913
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -8919,6 +9007,9 @@ Commands:
8919
9007
  platform Read canonical fleet health, releases, provider load/freshness,
8920
9008
  explicit unknowns, and next actions through a read-only grant.
8921
9009
  provision Register services, compose integrations, persist credentials, optionally push secrets.
9010
+ "provision --live --yes" initializes only the live instance of
9011
+ an existing sandbox app and enables every configured service;
9012
+ no edit to the dev-first envs list is required.
8922
9013
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
8923
9014
  skill Same installer; --agent accepts all, claude, codex, cursor,
8924
9015
  copilot, gemini, or agents (repeatable or comma-separated).
@@ -8931,15 +9022,21 @@ Safety:
8931
9022
  Every app has two databases on odla.ai: a sandbox (env "dev", tenant
8932
9023
  <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
8933
9024
  production odla.ai \u2014 there is no separate odla to point at. New projects get
8934
- the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
8935
- provision the live database; use --dry-run first to inspect the resolved plan.
9025
+ the sandbox only. Run "provision --live --dry-run", then "provision --live
9026
+ --yes" to initialize live from that existing sandbox project's config. Add
9027
+ "prod" to envs only when ordinary provision runs should manage both; use
9028
+ --dry-run first to inspect either resolved plan.
8936
9029
  A real provision first verifies both the released CLI version and the exact
8937
9030
  pinned versions of every external @odla-ai runtime module before importing
8938
9031
  the command graph. A stale workspace module blocks with its resolved path and
8939
9032
  tells the agent to update/rebase, npm ci, and rebuild.
8940
- Provision caches the approved developer token and service credentials under
8941
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
8942
- preflights Wrangler before any shown-once issuance or destructive rotation.
9033
+ Provision caches the approved developer token under .odla/ with mode 0600.
9034
+ Local-only development provisioning may also cache that developer's service
9035
+ credentials there. \`provision --push-secrets\` instead stages a fresh,
9036
+ independently revocable credential set through the approved handshake,
9037
+ transfers the complete set to the exact Worker with \`wrangler secret bulk\`
9038
+ over stdin, and never writes its plaintext under .odla. Secret push preflights
9039
+ Wrangler before shown-once issuance; it never rotates sibling credentials.
8943
9040
  Projectless PM, Discussions, o11y, runbook, and identity commands use
8944
9041
  --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
8945
9042
  ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
@@ -10863,7 +10960,7 @@ async function issueO11yToken(opts) {
10863
10960
  );
10864
10961
  if (res.status === 409 && !opts.rotateO11y) {
10865
10962
  throw new Error(
10866
- `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
10963
+ `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --push-secrets" to install a separate runtime credential without rotating siblings`
10867
10964
  );
10868
10965
  }
10869
10966
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
@@ -10879,23 +10976,131 @@ async function safeText7(res) {
10879
10976
  }
10880
10977
  }
10881
10978
 
10979
+ // src/runtime-credentials.ts
10980
+ import { randomUUID as randomUUID3 } from "crypto";
10981
+ function runtimeUrl(cfg, suffix = "") {
10982
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
10983
+ }
10984
+ async function safeError(response2) {
10985
+ const text2 = await response2.text();
10986
+ return redactSecrets(text2.slice(0, 1e3));
10987
+ }
10988
+ async function finish(doFetch, cfg, token, sessionId, method) {
10989
+ return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
10990
+ method,
10991
+ headers: { authorization: `Bearer ${token}` }
10992
+ });
10993
+ }
10994
+ async function deliverRuntimeCredentials(cfg, options) {
10995
+ const doFetch = options.fetch ?? fetch;
10996
+ const run = options.runner ?? defaultRunner;
10997
+ const wranglerEnv = options.env === "prod" || options.env === "production" ? void 0 : options.env;
10998
+ const target = await wranglerRuntimeTarget(run, {
10999
+ cwd: cfg.rootDir,
11000
+ env: wranglerEnv
11001
+ });
11002
+ const issue = await doFetch(runtimeUrl(cfg), {
11003
+ method: "POST",
11004
+ headers: {
11005
+ authorization: `Bearer ${options.developerToken}`,
11006
+ "content-type": "application/json"
11007
+ },
11008
+ body: JSON.stringify({
11009
+ env: options.env,
11010
+ idempotencyKey: `wrangler:${randomUUID3()}`,
11011
+ target
11012
+ })
11013
+ });
11014
+ const body = await issue.json().catch(() => null);
11015
+ if (!issue.ok || typeof body?.sessionId !== "string" || !body.secrets || typeof body.secrets !== "object") {
11016
+ const detail = body?.error?.code ?? body?.error?.message ?? issue.status;
11017
+ throw new Error(`runtime credential staging failed: ${String(detail)}`);
11018
+ }
11019
+ const secrets = body.secrets;
11020
+ const values = {};
11021
+ for (const name of ["ODLA_API_KEY", "ODLA_O11Y_TOKEN"]) {
11022
+ const value2 = secrets[name];
11023
+ if (typeof value2 === "string" && value2) values[name] = value2;
11024
+ }
11025
+ if (Object.keys(values).length === 0) {
11026
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11027
+ throw new Error("runtime credential staging returned no configured service credentials");
11028
+ }
11029
+ const pushed = await wranglerBulkSecrets(run, {
11030
+ secrets: values,
11031
+ env: wranglerEnv,
11032
+ cwd: cfg.rootDir
11033
+ });
11034
+ if (pushed.code !== 0) {
11035
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11036
+ const detail = redactSecrets(`${pushed.stderr || pushed.stdout}`.trim());
11037
+ throw new Error(`wrangler secret bulk failed (exit ${pushed.code}): ${detail}`);
11038
+ }
11039
+ const committed = await finish(
11040
+ doFetch,
11041
+ cfg,
11042
+ options.developerToken,
11043
+ body.sessionId,
11044
+ "POST"
11045
+ );
11046
+ if (!committed.ok) {
11047
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11048
+ throw new Error(`runtime credential receipt commit failed: ${await safeError(committed)}`);
11049
+ }
11050
+ options.stdout?.log(
11051
+ `${options.env}: installed ${Object.keys(values).join(" + ")} on ${target.accountId}/${target.scriptName}${target.environment ? ` (${target.environment})` : ""}; plaintext stayed in memory`
11052
+ );
11053
+ return {
11054
+ sessionId: body.sessionId,
11055
+ target,
11056
+ ...values.ODLA_API_KEY ? { dbKey: values.ODLA_API_KEY } : {},
11057
+ ...values.ODLA_O11Y_TOKEN ? { o11yToken: values.ODLA_O11Y_TOKEN } : {}
11058
+ };
11059
+ }
11060
+
11061
+ // src/provision-live.ts
11062
+ function liveProvisionConfig(cfg) {
11063
+ if (!cfg.envs.includes("dev")) {
11064
+ throw new Error(
11065
+ `--live initializes an existing sandbox, but env "dev" is not declared in ${cfg.configPath}; use normal "odla-ai provision --yes" for a prod-only project`
11066
+ );
11067
+ }
11068
+ if (cfg.services.includes("calendar")) {
11069
+ const google = cfg.calendar?.google;
11070
+ const availability = google?.availabilityCalendars ?? google?.calendars;
11071
+ if (!availability?.prod?.length) {
11072
+ throw new Error(
11073
+ `--live requires calendar.google.availabilityCalendars.prod in ${cfg.configPath}; live uses a separate Google Calendar connection, so sandbox calendar ids are not copied`
11074
+ );
11075
+ }
11076
+ }
11077
+ return { ...cfg, envs: ["prod"] };
11078
+ }
11079
+
10882
11080
  // src/provision.ts
10883
11081
  async function provision(options) {
10884
11082
  const out = options.stdout ?? console;
10885
- const cfg = await loadProjectConfig(options.configPath);
11083
+ const loaded = await loadProjectConfig(
11084
+ options.configPath,
11085
+ options.live ? { additionalEnvs: ["prod"] } : void 0
11086
+ );
11087
+ const cfg = options.live ? liveProvisionConfig(loaded) : loaded;
10886
11088
  const plan = buildPlan(cfg);
10887
11089
  const hasDb = cfg.services.includes("db");
10888
11090
  const hasO11y = cfg.services.includes("o11y");
10889
11091
  if (options.rotateO11yToken && !hasO11y) {
10890
11092
  throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
10891
11093
  }
10892
- if (options.pushSecrets && options.writeCredentials === false) {
10893
- throw new Error("--push-secrets cannot be combined with --no-write-credentials");
11094
+ if (options.pushSecrets && (options.rotateKeys || options.rotateO11yToken)) {
11095
+ throw new Error(
11096
+ "--push-secrets always issues an independent runtime credential set; do not combine it with destructive rotation flags"
11097
+ );
10894
11098
  }
10895
11099
  out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
10896
11100
  out.log(` platform: ${plan.platformUrl}`);
10897
11101
  out.log(` db: ${plan.dbEndpoint}`);
10898
11102
  out.log(` envs: ${plan.envs.join(", ")}`);
11103
+ if (options.live) out.log(" target: live setup (existing sandbox app)");
10899
11104
  out.log(` services: ${plan.services.join(", ")}`);
10900
11105
  out.log(` integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
10901
11106
  const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
@@ -10926,7 +11131,7 @@ async function provision(options) {
10926
11131
  out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
10927
11132
  return;
10928
11133
  }
10929
- let credentials = readCredentials(cfg.local.credentialsFile);
11134
+ let credentials = options.pushSecrets ? null : readCredentials(cfg.local.credentialsFile);
10930
11135
  if (credentials && credentials.appId !== cfg.app.id) {
10931
11136
  throw new Error(
10932
11137
  `credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
@@ -10935,7 +11140,7 @@ async function provision(options) {
10935
11140
  const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
10936
11141
  const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
10937
11142
  const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
10938
- const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
11143
+ const losesShownOnceCredential = !options.pushSecrets && (hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y));
10939
11144
  if (options.writeCredentials === false && losesShownOnceCredential) {
10940
11145
  throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
10941
11146
  }
@@ -10956,6 +11161,10 @@ async function provision(options) {
10956
11161
  const existing = await apps.resolveApp(cfg.app.id);
10957
11162
  if (existing) {
10958
11163
  out.log(`app: ${cfg.app.id} already exists`);
11164
+ } else if (options.live) {
11165
+ throw new Error(
11166
+ `cannot set up live for "${cfg.app.id}": the sandbox app does not exist yet; run "odla-ai provision" first`
11167
+ );
10959
11168
  } else {
10960
11169
  try {
10961
11170
  await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
@@ -11005,38 +11214,42 @@ async function provision(options) {
11005
11214
  await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
11006
11215
  }
11007
11216
  }
11217
+ let devVarsCredentials = credentials;
11008
11218
  for (const env of cfg.envs) {
11009
11219
  const tenantId = tenantIdFor5(cfg.app.id, env);
11010
- credentials = await provisionEnvCredentials({
11011
- cfg,
11012
- env,
11013
- developerToken: token,
11014
- credentials,
11015
- rotateDb: !!options.rotateKeys,
11016
- rotateO11y: rotatesO11y,
11017
- write: options.writeCredentials !== false,
11018
- fetch: doFetch,
11019
- stdout: out
11020
- });
11021
- const dbKey = credentials.envs[env]?.dbKey;
11220
+ let dbKey;
11022
11221
  if (options.pushSecrets) {
11023
- try {
11024
- await secretsPushAfterPreflight({
11025
- configPath: cfg.configPath,
11026
- env,
11027
- yes: options.yes,
11028
- runner: options.secretRunner,
11029
- stdout: out
11030
- });
11031
- } catch (error) {
11032
- const message2 = error instanceof Error ? error.message : String(error);
11033
- const consent = env === "prod" || env === "production" ? " --yes" : "";
11034
- throw new Error(
11035
- `${message2}
11036
- ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
11037
- { cause: error }
11038
- );
11039
- }
11222
+ const delivered = await deliverRuntimeCredentials(cfg, {
11223
+ env,
11224
+ developerToken: token,
11225
+ fetch: doFetch,
11226
+ runner: options.secretRunner,
11227
+ stdout: out
11228
+ });
11229
+ dbKey = delivered.dbKey;
11230
+ devVarsCredentials = mergeCredential(devVarsCredentials, {
11231
+ appId: cfg.app.id,
11232
+ platformUrl: cfg.platformUrl,
11233
+ dbEndpoint: cfg.dbEndpoint,
11234
+ env,
11235
+ tenantId,
11236
+ ...delivered.dbKey ? { dbKey: delivered.dbKey } : {},
11237
+ ...delivered.o11yToken ? { o11yToken: delivered.o11yToken } : {}
11238
+ });
11239
+ } else {
11240
+ credentials = await provisionEnvCredentials({
11241
+ cfg,
11242
+ env,
11243
+ developerToken: token,
11244
+ credentials,
11245
+ rotateDb: !!options.rotateKeys,
11246
+ rotateO11y: rotatesO11y,
11247
+ write: options.writeCredentials !== false,
11248
+ fetch: doFetch,
11249
+ stdout: out
11250
+ });
11251
+ devVarsCredentials = credentials;
11252
+ dbKey = credentials.envs[env]?.dbKey;
11040
11253
  }
11041
11254
  if (schema && dbKey) {
11042
11255
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
@@ -11060,13 +11273,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
11060
11273
  }
11061
11274
  }
11062
11275
  }
11063
- if (options.writeCredentials !== false && credentials) {
11276
+ if (!options.pushSecrets && options.writeCredentials !== false && credentials) {
11064
11277
  const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
11065
11278
  out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
11066
11279
  }
11067
- if (devVarsTarget && credentials) {
11280
+ if (devVarsTarget && devVarsCredentials) {
11068
11281
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
11069
- writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
11282
+ writeDevVars(devVarsTarget, devVarsCredentials, env, o11yDevVars(cfg));
11070
11283
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
11071
11284
  }
11072
11285
  if (cfg.services.includes("calendar")) {
@@ -11157,6 +11370,7 @@ var COMMAND_SURFACE = {
11157
11370
  code: { connect: {} },
11158
11371
  config: { diff: {}, plan: {}, apply: {} },
11159
11372
  context: { show: {}, list: {}, save: {}, remove: {} },
11373
+ credentials: { list: {}, revoke: {} },
11160
11374
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
11161
11375
  discuss: {
11162
11376
  groups: {},
@@ -12841,6 +13055,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12841
13055
  await contextCommand(parsed, runtime);
12842
13056
  return;
12843
13057
  }
13058
+ if (command === "credentials") {
13059
+ await credentialCommand(parsed, runtime);
13060
+ return;
13061
+ }
12844
13062
  if (command === "runbook") {
12845
13063
  await runbookCommand(parsed, runtime);
12846
13064
  return;
@@ -12908,6 +13126,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
12908
13126
  async function provisionCommand(parsed, dependencies) {
12909
13127
  assertArgs(parsed, [
12910
13128
  "config",
13129
+ "live",
12911
13130
  "dry-run",
12912
13131
  "rotate-keys",
12913
13132
  "rotate-o11y-token",
@@ -12924,6 +13143,7 @@ async function provisionCommand(parsed, dependencies) {
12924
13143
  const writeDevVars2 = parsed.options["write-dev-vars"];
12925
13144
  const options = {
12926
13145
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
13146
+ live: parsed.options.live === true,
12927
13147
  dryRun: parsed.options["dry-run"] === true,
12928
13148
  rotateKeys: parsed.options["rotate-keys"] === true,
12929
13149
  rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
@@ -13026,4 +13246,4 @@ export {
13026
13246
  isTerminalHostedSecurityStatus,
13027
13247
  runCli
13028
13248
  };
13029
- //# sourceMappingURL=chunk-STJCWZJU.js.map
13249
+ //# sourceMappingURL=chunk-PPSXHZJI.js.map