@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-STJCWZJU.js";
4
+ } from "./chunk-PPSXHZJI.js";
5
5
  import {
6
6
  exitCodeFor
7
7
  } from "./chunk-UKLSRQ5J.js";
@@ -9,4 +9,4 @@ export {
9
9
  exitCodeFor,
10
10
  runCli
11
11
  };
12
- //# sourceMappingURL=cli-2WZIFXA6.js.map
12
+ //# sourceMappingURL=cli-CKBUVTJM.js.map
package/dist/index.cjs CHANGED
@@ -1218,7 +1218,7 @@ var DEFAULT_ENVS = ["dev"];
1218
1218
  var DEFAULT_SERVICES = ["db", "ai"];
1219
1219
  var configImportSerial = 0;
1220
1220
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1221
- async function loadProjectConfig(configPath = "odla.config.mjs") {
1221
+ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1222
1222
  const resolved = (0, import_node_path4.resolve)(configPath);
1223
1223
  if (!(0, import_node_fs4.existsSync)(resolved)) {
1224
1224
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
@@ -1231,7 +1231,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
1231
1231
  const envs = unique2(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
1232
1232
  const services = unique2(raw.services?.length ? raw.services : DEFAULT_SERVICES);
1233
1233
  validateServices(services, resolved);
1234
- validateCalendarConfig(raw, envs, services, resolved);
1234
+ validateCalendarConfig(raw, unique2([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1235
1235
  const local = {
1236
1236
  tokenFile: (0, import_node_path4.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1237
1237
  credentialsFile: (0, import_node_path4.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
@@ -1281,12 +1281,12 @@ function buildPlan(cfg) {
1281
1281
  }
1282
1282
  function calendarServiceConfig(cfg, env) {
1283
1283
  if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
1284
- if (!cfg.envs.includes(env)) throw new Error(`calendar env "${env}" is not declared in config envs`);
1284
+ if (!cfg.envs.includes(env) && env !== "prod") throw new Error(`calendar env "${env}" is not declared in config envs`);
1285
1285
  const google = cfg.calendar?.google;
1286
1286
  if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
1287
- const availability = unique2(
1288
- (google.availabilityCalendars?.[env] ?? google.calendars?.[env]).map((id) => id.trim())
1289
- );
1287
+ const configured = google.availabilityCalendars?.[env] ?? google.calendars?.[env];
1288
+ if (!configured?.length) throw new Error(`calendar.google.availabilityCalendars.${env} is required`);
1289
+ const availability = unique2(configured.map((id) => id.trim()));
1290
1290
  return {
1291
1291
  provider: "google",
1292
1292
  access: "book",
@@ -1353,13 +1353,18 @@ function validateCalendarConfig(cfg, envs, services, path) {
1353
1353
  }
1354
1354
  const availability = google[availabilityKey];
1355
1355
  if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1356
- const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
1356
+ const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env) && env !== "prod");
1357
1357
  if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1358
1358
  for (const env of envs) {
1359
1359
  const ids = availability[env];
1360
1360
  if (!Array.isArray(ids) || ids.length === 0) {
1361
1361
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1362
1362
  }
1363
+ }
1364
+ for (const [env, ids] of Object.entries(availability)) {
1365
+ if (!Array.isArray(ids) || ids.length === 0) {
1366
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1367
+ }
1363
1368
  if (ids.length > 10) {
1364
1369
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1365
1370
  }
@@ -1369,7 +1374,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1369
1374
  }
1370
1375
  if (google.bookingCalendar !== void 0) {
1371
1376
  if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1372
- const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
1377
+ const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env) && env !== "prod");
1373
1378
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1374
1379
  for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1375
1380
  if (!safeText3(value2, 1024)) {
@@ -1379,7 +1384,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1379
1384
  }
1380
1385
  if (google.bookingPageUrl !== void 0) {
1381
1386
  if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1382
- const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
1387
+ const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env) && env !== "prod");
1383
1388
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1384
1389
  for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
1385
1390
  if (value2 !== null && !safeHttpsUrl(value2)) {
@@ -1925,7 +1930,7 @@ async function authCommand(parsed, deps = {}) {
1925
1930
  var import_apps2 = require("@odla-ai/apps");
1926
1931
  function resolveEnv(cfg, requested) {
1927
1932
  const env = requested ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
1928
- if (!env || !cfg.envs.includes(env)) {
1933
+ if (!env || !cfg.envs.includes(env) && env !== "prod") {
1929
1934
  throw new Error(`env "${env ?? ""}" is not declared in ${cfg.configPath}`);
1930
1935
  }
1931
1936
  return env;
@@ -1935,12 +1940,10 @@ function resolveTenant(cfg, requested) {
1935
1940
  return { env, tenant: (0, import_apps2.tenantIdFor)(cfg.app.id, env) };
1936
1941
  }
1937
1942
  function bothTenants(cfg) {
1938
- for (const env of ["dev", "prod"]) {
1939
- if (!cfg.envs.includes(env)) {
1940
- throw new Error(
1941
- `env "${env}" is not declared in ${cfg.configPath} \u2014 add it to \`envs\` and run \`odla-ai provision --yes\` before transferring data between environments`
1942
- );
1943
- }
1943
+ if (!cfg.envs.includes("dev")) {
1944
+ throw new Error(
1945
+ `env "dev" is not declared in ${cfg.configPath} \u2014 sandbox/live transfers require an existing sandbox`
1946
+ );
1944
1947
  }
1945
1948
  return { sandbox: (0, import_apps2.tenantIdFor)(cfg.app.id, "dev"), live: (0, import_apps2.tenantIdFor)(cfg.app.id, "prod") };
1946
1949
  }
@@ -2028,7 +2031,7 @@ function errorMessage(body) {
2028
2031
  // src/human-session.ts
2029
2032
  async function requireStudioHuman(configPath, action2, destination = "app", env) {
2030
2033
  const cfg = await loadProjectConfig(configPath);
2031
- const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
2034
+ const appEnv = env && (cfg.envs.includes(env) || env === "prod") ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
2032
2035
  const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
2033
2036
  throw new Error(
2034
2037
  `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}`
@@ -2714,8 +2717,7 @@ async function ensureCalendarConnected(ctx, options) {
2714
2717
  async function lifecycleContext(options, optionalProjectCapabilities = []) {
2715
2718
  const cfg = await loadProjectConfig(options.configPath);
2716
2719
  if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
2717
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
2718
- if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
2720
+ const env = resolveEnv(cfg, options.env);
2719
2721
  calendarServiceConfig(cfg, env);
2720
2722
  const out = options.stdout ?? console;
2721
2723
  const doFetch = options.fetch ?? fetch;
@@ -2916,8 +2918,7 @@ function printGroup(out, heading, items) {
2916
2918
  var import_ai = require("@odla-ai/ai");
2917
2919
  async function aiModels(options = {}) {
2918
2920
  const cfg = await loadProjectConfig(options.configPath ?? "odla.config.mjs");
2919
- const env = options.env ?? cfg.envs[0] ?? "dev";
2920
- if (!cfg.envs.includes(env)) throw new Error(`ai models env "${env}" is not declared in config envs`);
2921
+ const env = resolveEnv(cfg, options.env);
2921
2922
  const url = new URL(`/registry/apps/${encodeURIComponent(cfg.app.id)}/public-config`, cfg.platformUrl);
2922
2923
  url.searchParams.set("env", env);
2923
2924
  const response2 = await (options.fetch ?? fetch)(url);
@@ -3960,6 +3961,41 @@ async function wranglerLoggedIn(run, cwd) {
3960
3961
  return false;
3961
3962
  }
3962
3963
  }
3964
+ async function wranglerRuntimeTarget(run, opts) {
3965
+ const configPath = findWranglerConfig(opts.cwd);
3966
+ if (!configPath) throw new Error(`no wrangler config found in ${opts.cwd}`);
3967
+ const config = readWranglerConfig(configPath);
3968
+ if (!config) throw new Error("runtime credential delivery requires wrangler.json or wrangler.jsonc");
3969
+ const baseName = typeof config.name === "string" ? config.name : "";
3970
+ const envs = config.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
3971
+ const envConfig = opts.env && envs[opts.env] && typeof envs[opts.env] === "object" ? envs[opts.env] : {};
3972
+ const explicitName = typeof envConfig.name === "string" ? envConfig.name : "";
3973
+ const serviceEnvironments = config.legacy_env === false;
3974
+ if (serviceEnvironments && explicitName) {
3975
+ throw new Error("env.<name>.name is not allowed when wrangler legacy_env is false");
3976
+ }
3977
+ const scriptName = serviceEnvironments ? baseName : explicitName || (opts.env ? `${baseName}-${opts.env}` : baseName);
3978
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(scriptName)) {
3979
+ throw new Error("wrangler config must resolve an exact Worker name before credentials are issued");
3980
+ }
3981
+ const configuredAccount = typeof envConfig.account_id === "string" ? envConfig.account_id : typeof config.account_id === "string" ? config.account_id : "";
3982
+ const whoami = await run("npx", ["wrangler", "whoami"], { cwd: opts.cwd });
3983
+ if (whoami.code !== 0 || /not authenticated/i.test(`${whoami.stdout}${whoami.stderr}`)) {
3984
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
3985
+ }
3986
+ const discovered = [...new Set(`${whoami.stdout}
3987
+ ${whoami.stderr}`.match(/\b[a-f0-9]{32}\b/gi)?.map((id) => id.toLowerCase()) ?? [])];
3988
+ const accountId = configuredAccount.toLowerCase() || (discovered.length === 1 ? discovered[0] : "");
3989
+ if (!/^[a-f0-9]{32}$/.test(accountId) || configuredAccount && discovered.length > 0 && !discovered.includes(accountId)) {
3990
+ throw new Error("Wrangler account is ambiguous or does not match account_id in the config");
3991
+ }
3992
+ return {
3993
+ provider: "cloudflare",
3994
+ accountId,
3995
+ scriptName,
3996
+ ...opts.env ? { environment: opts.env } : {}
3997
+ };
3998
+ }
3963
3999
  function wranglerPutSecret(run, opts) {
3964
4000
  const args = [
3965
4001
  "wrangler",
@@ -3971,6 +4007,16 @@ function wranglerPutSecret(run, opts) {
3971
4007
  ];
3972
4008
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3973
4009
  }
4010
+ function wranglerBulkSecrets(run, opts) {
4011
+ const args = [
4012
+ "wrangler",
4013
+ "secret",
4014
+ "bulk",
4015
+ ...opts.env ? ["--env", opts.env] : [],
4016
+ ...opts.configPath ? ["--config", opts.configPath] : []
4017
+ ];
4018
+ return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
4019
+ }
3974
4020
 
3975
4021
  // src/doctor-checks.ts
3976
4022
  function lintRules(rules, entities, publicRead) {
@@ -4459,16 +4505,10 @@ var PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
4459
4505
  async function secretsPush(options) {
4460
4506
  await secretsPushImpl(options, true);
4461
4507
  }
4462
- async function secretsPushAfterPreflight(options) {
4463
- await secretsPushImpl(options, false);
4464
- }
4465
4508
  async function secretsPushImpl(options, preflight) {
4466
4509
  const out = options.stdout ?? console;
4467
4510
  const cfg = await loadProjectConfig(options.configPath);
4468
- const env = options.env;
4469
- if (!cfg.envs.includes(env)) {
4470
- throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
4471
- }
4511
+ const env = resolveEnv(cfg, options.env);
4472
4512
  if (PROD_ENV_NAMES.has(env) && !options.yes) {
4473
4513
  throw new Error(`refusing to push a secret to "${env}" without --yes`);
4474
4514
  }
@@ -4578,14 +4618,12 @@ async function resolveVaultWrite(options) {
4578
4618
  const out = options.stdout ?? console;
4579
4619
  const doFetch = options.fetch ?? fetch;
4580
4620
  const cfg = await loadProjectConfig(options.configPath);
4581
- if (!cfg.envs.includes(options.env)) {
4582
- throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
4583
- }
4584
- if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
4585
- throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
4621
+ const env = resolveEnv(cfg, options.env);
4622
+ if (PROD_ENV_NAMES2.has(env) && !options.yes) {
4623
+ throw new Error(`refusing to store a secret for "${env}" without --yes`);
4586
4624
  }
4587
4625
  const value2 = await secretInputValue(options, "secret");
4588
- return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
4626
+ return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, env), value: value2, doFetch, out };
4589
4627
  }
4590
4628
 
4591
4629
  // src/skill.ts
@@ -4872,8 +4910,7 @@ function listFiles(dir) {
4872
4910
  async function smoke(options) {
4873
4911
  const out = options.stdout ?? console;
4874
4912
  const cfg = await loadProjectConfig(options.configPath);
4875
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod");
4876
- if (!cfg.envs.includes(env)) throw new Error(`env "${env}" is not declared in ${displayPath(cfg.configPath, cfg.rootDir)}`);
4913
+ const env = resolveEnv(cfg, options.env);
4877
4914
  const credentials = readCredentials(cfg.local.credentialsFile);
4878
4915
  if (!credentials) {
4879
4916
  throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
@@ -8832,6 +8869,55 @@ function requireName(parsed) {
8832
8869
  return name;
8833
8870
  }
8834
8871
 
8872
+ // src/credential-command.ts
8873
+ async function responseError(response2) {
8874
+ return redactSecrets((await response2.text()).slice(0, 1e3));
8875
+ }
8876
+ async function credentialCommand(parsed, deps = {}) {
8877
+ const action2 = parsed.positionals[1] ?? "list";
8878
+ if (action2 !== "list" && action2 !== "revoke") {
8879
+ throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
8880
+ }
8881
+ assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
8882
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
8883
+ const doFetch = deps.fetch ?? fetch;
8884
+ const out = deps.stdout ?? console;
8885
+ const token = await getDeveloperToken(cfg, {
8886
+ configPath: cfg.configPath,
8887
+ token: stringOpt(parsed.options.token),
8888
+ email: stringOpt(parsed.options.email),
8889
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
8890
+ openApprovalUrl: deps.openUrl
8891
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
8892
+ const base = `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials`;
8893
+ if (action2 === "revoke") {
8894
+ const id = parsed.positionals[2];
8895
+ if (!id) throw new Error("credentials revoke requires the exact receipt id from credentials list");
8896
+ const response3 = await doFetch(`${base}/${encodeURIComponent(id)}`, {
8897
+ method: "DELETE",
8898
+ headers: { authorization: `Bearer ${token}` }
8899
+ });
8900
+ if (!response3.ok) throw new Error(`runtime credential revoke failed (${response3.status}): ${await responseError(response3)}`);
8901
+ const body2 = await response3.json();
8902
+ return out.log(parsed.options.json === true ? JSON.stringify(body2, null, 2) : `revoked ${body2.receipt.id} (${body2.receipt.env} ${body2.receipt.target.scriptName})`);
8903
+ }
8904
+ const query = new URLSearchParams();
8905
+ const requestedEnv = stringOpt(parsed.options.env);
8906
+ if (requestedEnv) query.set("env", requestedEnv);
8907
+ const response2 = await doFetch(`${base}${query.size ? `?${query}` : ""}`, {
8908
+ headers: { authorization: `Bearer ${token}` }
8909
+ });
8910
+ if (!response2.ok) throw new Error(`runtime credential inventory failed (${response2.status}): ${await responseError(response2)}`);
8911
+ const body = await response2.json();
8912
+ const credentials = parsed.options.all === true ? body.credentials : body.credentials.filter((item) => item.state === "committed");
8913
+ if (parsed.options.json === true) return out.log(JSON.stringify({ credentials }, null, 2));
8914
+ if (!credentials.length) return out.log("no active runtime credential receipts");
8915
+ out.log("RECEIPT ENV TARGET CREATED");
8916
+ for (const item of credentials) {
8917
+ out.log(`${item.id} ${item.env} ${item.target.accountId ?? "local"}/${item.target.scriptName} ${new Date(item.createdAt).toISOString()}`);
8918
+ }
8919
+ }
8920
+
8835
8921
  // src/help-usage.ts
8836
8922
  var USAGE_SECTION = `
8837
8923
  Start here:
@@ -8949,7 +9035,9 @@ Usage:
8949
9035
  odla-ai security report <job-id> [--json]
8950
9036
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8951
9037
  odla-ai security run [target] --self --ack-redacted-source
8952
- 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]
9038
+ 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]
9039
+ odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
9040
+ odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
8953
9041
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8954
9042
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8955
9043
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -9049,6 +9137,9 @@ Commands:
9049
9137
  platform Read canonical fleet health, releases, provider load/freshness,
9050
9138
  explicit unknowns, and next actions through a read-only grant.
9051
9139
  provision Register services, compose integrations, persist credentials, optionally push secrets.
9140
+ "provision --live --yes" initializes only the live instance of
9141
+ an existing sandbox app and enables every configured service;
9142
+ no edit to the dev-first envs list is required.
9052
9143
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
9053
9144
  skill Same installer; --agent accepts all, claude, codex, cursor,
9054
9145
  copilot, gemini, or agents (repeatable or comma-separated).
@@ -9061,15 +9152,21 @@ Safety:
9061
9152
  Every app has two databases on odla.ai: a sandbox (env "dev", tenant
9062
9153
  <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
9063
9154
  production odla.ai \u2014 there is no separate odla to point at. New projects get
9064
- the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
9065
- provision the live database; use --dry-run first to inspect the resolved plan.
9155
+ the sandbox only. Run "provision --live --dry-run", then "provision --live
9156
+ --yes" to initialize live from that existing sandbox project's config. Add
9157
+ "prod" to envs only when ordinary provision runs should manage both; use
9158
+ --dry-run first to inspect either resolved plan.
9066
9159
  A real provision first verifies both the released CLI version and the exact
9067
9160
  pinned versions of every external @odla-ai runtime module before importing
9068
9161
  the command graph. A stale workspace module blocks with its resolved path and
9069
9162
  tells the agent to update/rebase, npm ci, and rebuild.
9070
- Provision caches the approved developer token and service credentials under
9071
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
9072
- preflights Wrangler before any shown-once issuance or destructive rotation.
9163
+ Provision caches the approved developer token under .odla/ with mode 0600.
9164
+ Local-only development provisioning may also cache that developer's service
9165
+ credentials there. \`provision --push-secrets\` instead stages a fresh,
9166
+ independently revocable credential set through the approved handshake,
9167
+ transfers the complete set to the exact Worker with \`wrangler secret bulk\`
9168
+ over stdin, and never writes its plaintext under .odla. Secret push preflights
9169
+ Wrangler before shown-once issuance; it never rotates sibling credentials.
9073
9170
  Projectless PM, Discussions, o11y, runbook, and identity commands use
9074
9171
  --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
9075
9172
  ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
@@ -10993,7 +11090,7 @@ async function issueO11yToken(opts) {
10993
11090
  );
10994
11091
  if (res.status === 409 && !opts.rotateO11y) {
10995
11092
  throw new Error(
10996
- `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`
11093
+ `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`
10997
11094
  );
10998
11095
  }
10999
11096
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
@@ -11009,23 +11106,131 @@ async function safeText7(res) {
11009
11106
  }
11010
11107
  }
11011
11108
 
11109
+ // src/runtime-credentials.ts
11110
+ var import_node_crypto5 = require("crypto");
11111
+ function runtimeUrl(cfg, suffix = "") {
11112
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
11113
+ }
11114
+ async function safeError(response2) {
11115
+ const text2 = await response2.text();
11116
+ return redactSecrets(text2.slice(0, 1e3));
11117
+ }
11118
+ async function finish(doFetch, cfg, token, sessionId, method) {
11119
+ return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
11120
+ method,
11121
+ headers: { authorization: `Bearer ${token}` }
11122
+ });
11123
+ }
11124
+ async function deliverRuntimeCredentials(cfg, options) {
11125
+ const doFetch = options.fetch ?? fetch;
11126
+ const run = options.runner ?? defaultRunner;
11127
+ const wranglerEnv = options.env === "prod" || options.env === "production" ? void 0 : options.env;
11128
+ const target = await wranglerRuntimeTarget(run, {
11129
+ cwd: cfg.rootDir,
11130
+ env: wranglerEnv
11131
+ });
11132
+ const issue = await doFetch(runtimeUrl(cfg), {
11133
+ method: "POST",
11134
+ headers: {
11135
+ authorization: `Bearer ${options.developerToken}`,
11136
+ "content-type": "application/json"
11137
+ },
11138
+ body: JSON.stringify({
11139
+ env: options.env,
11140
+ idempotencyKey: `wrangler:${(0, import_node_crypto5.randomUUID)()}`,
11141
+ target
11142
+ })
11143
+ });
11144
+ const body = await issue.json().catch(() => null);
11145
+ if (!issue.ok || typeof body?.sessionId !== "string" || !body.secrets || typeof body.secrets !== "object") {
11146
+ const detail = body?.error?.code ?? body?.error?.message ?? issue.status;
11147
+ throw new Error(`runtime credential staging failed: ${String(detail)}`);
11148
+ }
11149
+ const secrets = body.secrets;
11150
+ const values = {};
11151
+ for (const name of ["ODLA_API_KEY", "ODLA_O11Y_TOKEN"]) {
11152
+ const value2 = secrets[name];
11153
+ if (typeof value2 === "string" && value2) values[name] = value2;
11154
+ }
11155
+ if (Object.keys(values).length === 0) {
11156
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11157
+ throw new Error("runtime credential staging returned no configured service credentials");
11158
+ }
11159
+ const pushed = await wranglerBulkSecrets(run, {
11160
+ secrets: values,
11161
+ env: wranglerEnv,
11162
+ cwd: cfg.rootDir
11163
+ });
11164
+ if (pushed.code !== 0) {
11165
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11166
+ const detail = redactSecrets(`${pushed.stderr || pushed.stdout}`.trim());
11167
+ throw new Error(`wrangler secret bulk failed (exit ${pushed.code}): ${detail}`);
11168
+ }
11169
+ const committed = await finish(
11170
+ doFetch,
11171
+ cfg,
11172
+ options.developerToken,
11173
+ body.sessionId,
11174
+ "POST"
11175
+ );
11176
+ if (!committed.ok) {
11177
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
11178
+ throw new Error(`runtime credential receipt commit failed: ${await safeError(committed)}`);
11179
+ }
11180
+ options.stdout?.log(
11181
+ `${options.env}: installed ${Object.keys(values).join(" + ")} on ${target.accountId}/${target.scriptName}${target.environment ? ` (${target.environment})` : ""}; plaintext stayed in memory`
11182
+ );
11183
+ return {
11184
+ sessionId: body.sessionId,
11185
+ target,
11186
+ ...values.ODLA_API_KEY ? { dbKey: values.ODLA_API_KEY } : {},
11187
+ ...values.ODLA_O11Y_TOKEN ? { o11yToken: values.ODLA_O11Y_TOKEN } : {}
11188
+ };
11189
+ }
11190
+
11191
+ // src/provision-live.ts
11192
+ function liveProvisionConfig(cfg) {
11193
+ if (!cfg.envs.includes("dev")) {
11194
+ throw new Error(
11195
+ `--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`
11196
+ );
11197
+ }
11198
+ if (cfg.services.includes("calendar")) {
11199
+ const google = cfg.calendar?.google;
11200
+ const availability = google?.availabilityCalendars ?? google?.calendars;
11201
+ if (!availability?.prod?.length) {
11202
+ throw new Error(
11203
+ `--live requires calendar.google.availabilityCalendars.prod in ${cfg.configPath}; live uses a separate Google Calendar connection, so sandbox calendar ids are not copied`
11204
+ );
11205
+ }
11206
+ }
11207
+ return { ...cfg, envs: ["prod"] };
11208
+ }
11209
+
11012
11210
  // src/provision.ts
11013
11211
  async function provision(options) {
11014
11212
  const out = options.stdout ?? console;
11015
- const cfg = await loadProjectConfig(options.configPath);
11213
+ const loaded = await loadProjectConfig(
11214
+ options.configPath,
11215
+ options.live ? { additionalEnvs: ["prod"] } : void 0
11216
+ );
11217
+ const cfg = options.live ? liveProvisionConfig(loaded) : loaded;
11016
11218
  const plan = buildPlan(cfg);
11017
11219
  const hasDb = cfg.services.includes("db");
11018
11220
  const hasO11y = cfg.services.includes("o11y");
11019
11221
  if (options.rotateO11yToken && !hasO11y) {
11020
11222
  throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
11021
11223
  }
11022
- if (options.pushSecrets && options.writeCredentials === false) {
11023
- throw new Error("--push-secrets cannot be combined with --no-write-credentials");
11224
+ if (options.pushSecrets && (options.rotateKeys || options.rotateO11yToken)) {
11225
+ throw new Error(
11226
+ "--push-secrets always issues an independent runtime credential set; do not combine it with destructive rotation flags"
11227
+ );
11024
11228
  }
11025
11229
  out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
11026
11230
  out.log(` platform: ${plan.platformUrl}`);
11027
11231
  out.log(` db: ${plan.dbEndpoint}`);
11028
11232
  out.log(` envs: ${plan.envs.join(", ")}`);
11233
+ if (options.live) out.log(" target: live setup (existing sandbox app)");
11029
11234
  out.log(` services: ${plan.services.join(", ")}`);
11030
11235
  out.log(` integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
11031
11236
  const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
@@ -11056,7 +11261,7 @@ async function provision(options) {
11056
11261
  out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
11057
11262
  return;
11058
11263
  }
11059
- let credentials = readCredentials(cfg.local.credentialsFile);
11264
+ let credentials = options.pushSecrets ? null : readCredentials(cfg.local.credentialsFile);
11060
11265
  if (credentials && credentials.appId !== cfg.app.id) {
11061
11266
  throw new Error(
11062
11267
  `credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
@@ -11065,7 +11270,7 @@ async function provision(options) {
11065
11270
  const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
11066
11271
  const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
11067
11272
  const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
11068
- const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
11273
+ const losesShownOnceCredential = !options.pushSecrets && (hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y));
11069
11274
  if (options.writeCredentials === false && losesShownOnceCredential) {
11070
11275
  throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
11071
11276
  }
@@ -11086,6 +11291,10 @@ async function provision(options) {
11086
11291
  const existing = await apps.resolveApp(cfg.app.id);
11087
11292
  if (existing) {
11088
11293
  out.log(`app: ${cfg.app.id} already exists`);
11294
+ } else if (options.live) {
11295
+ throw new Error(
11296
+ `cannot set up live for "${cfg.app.id}": the sandbox app does not exist yet; run "odla-ai provision" first`
11297
+ );
11089
11298
  } else {
11090
11299
  try {
11091
11300
  await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
@@ -11135,38 +11344,42 @@ async function provision(options) {
11135
11344
  await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
11136
11345
  }
11137
11346
  }
11347
+ let devVarsCredentials = credentials;
11138
11348
  for (const env of cfg.envs) {
11139
11349
  const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
11140
- credentials = await provisionEnvCredentials({
11141
- cfg,
11142
- env,
11143
- developerToken: token,
11144
- credentials,
11145
- rotateDb: !!options.rotateKeys,
11146
- rotateO11y: rotatesO11y,
11147
- write: options.writeCredentials !== false,
11148
- fetch: doFetch,
11149
- stdout: out
11150
- });
11151
- const dbKey = credentials.envs[env]?.dbKey;
11350
+ let dbKey;
11152
11351
  if (options.pushSecrets) {
11153
- try {
11154
- await secretsPushAfterPreflight({
11155
- configPath: cfg.configPath,
11156
- env,
11157
- yes: options.yes,
11158
- runner: options.secretRunner,
11159
- stdout: out
11160
- });
11161
- } catch (error) {
11162
- const message2 = error instanceof Error ? error.message : String(error);
11163
- const consent = env === "prod" || env === "production" ? " --yes" : "";
11164
- throw new Error(
11165
- `${message2}
11166
- ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
11167
- { cause: error }
11168
- );
11169
- }
11352
+ const delivered = await deliverRuntimeCredentials(cfg, {
11353
+ env,
11354
+ developerToken: token,
11355
+ fetch: doFetch,
11356
+ runner: options.secretRunner,
11357
+ stdout: out
11358
+ });
11359
+ dbKey = delivered.dbKey;
11360
+ devVarsCredentials = mergeCredential(devVarsCredentials, {
11361
+ appId: cfg.app.id,
11362
+ platformUrl: cfg.platformUrl,
11363
+ dbEndpoint: cfg.dbEndpoint,
11364
+ env,
11365
+ tenantId,
11366
+ ...delivered.dbKey ? { dbKey: delivered.dbKey } : {},
11367
+ ...delivered.o11yToken ? { o11yToken: delivered.o11yToken } : {}
11368
+ });
11369
+ } else {
11370
+ credentials = await provisionEnvCredentials({
11371
+ cfg,
11372
+ env,
11373
+ developerToken: token,
11374
+ credentials,
11375
+ rotateDb: !!options.rotateKeys,
11376
+ rotateO11y: rotatesO11y,
11377
+ write: options.writeCredentials !== false,
11378
+ fetch: doFetch,
11379
+ stdout: out
11380
+ });
11381
+ devVarsCredentials = credentials;
11382
+ dbKey = credentials.envs[env]?.dbKey;
11170
11383
  }
11171
11384
  if (schema && dbKey) {
11172
11385
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
@@ -11190,13 +11403,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
11190
11403
  }
11191
11404
  }
11192
11405
  }
11193
- if (options.writeCredentials !== false && credentials) {
11406
+ if (!options.pushSecrets && options.writeCredentials !== false && credentials) {
11194
11407
  const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
11195
11408
  out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
11196
11409
  }
11197
- if (devVarsTarget && credentials) {
11410
+ if (devVarsTarget && devVarsCredentials) {
11198
11411
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
11199
- writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
11412
+ writeDevVars(devVarsTarget, devVarsCredentials, env, o11yDevVars(cfg));
11200
11413
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
11201
11414
  }
11202
11415
  if (cfg.services.includes("calendar")) {
@@ -11287,6 +11500,7 @@ var COMMAND_SURFACE = {
11287
11500
  code: { connect: {} },
11288
11501
  config: { diff: {}, plan: {}, apply: {} },
11289
11502
  context: { show: {}, list: {}, save: {}, remove: {} },
11503
+ credentials: { list: {}, revoke: {} },
11290
11504
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
11291
11505
  discuss: {
11292
11506
  groups: {},
@@ -13024,6 +13238,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
13024
13238
  await contextCommand(parsed, runtime);
13025
13239
  return;
13026
13240
  }
13241
+ if (command === "credentials") {
13242
+ await credentialCommand(parsed, runtime);
13243
+ return;
13244
+ }
13027
13245
  if (command === "runbook") {
13028
13246
  await runbookCommand(parsed, runtime);
13029
13247
  return;
@@ -13091,6 +13309,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
13091
13309
  async function provisionCommand(parsed, dependencies) {
13092
13310
  assertArgs(parsed, [
13093
13311
  "config",
13312
+ "live",
13094
13313
  "dry-run",
13095
13314
  "rotate-keys",
13096
13315
  "rotate-o11y-token",
@@ -13107,6 +13326,7 @@ async function provisionCommand(parsed, dependencies) {
13107
13326
  const writeDevVars2 = parsed.options["write-dev-vars"];
13108
13327
  const options = {
13109
13328
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
13329
+ live: parsed.options.live === true,
13110
13330
  dryRun: parsed.options["dry-run"] === true,
13111
13331
  rotateKeys: parsed.options["rotate-keys"] === true,
13112
13332
  rotateO11yToken: parsed.options["rotate-o11y-token"] === true,