@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.cjs CHANGED
@@ -1410,7 +1410,7 @@ var init_integration_validation = __esm({
1410
1410
  });
1411
1411
 
1412
1412
  // src/config.ts
1413
- async function loadProjectConfig(configPath = "odla.config.mjs") {
1413
+ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1414
1414
  const resolved = (0, import_node_path5.resolve)(configPath);
1415
1415
  if (!(0, import_node_fs7.existsSync)(resolved)) {
1416
1416
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
@@ -1423,7 +1423,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs") {
1423
1423
  const envs = unique2(raw.envs?.length ? raw.envs : DEFAULT_ENVS);
1424
1424
  const services = unique2(raw.services?.length ? raw.services : DEFAULT_SERVICES);
1425
1425
  validateServices(services, resolved);
1426
- validateCalendarConfig(raw, envs, services, resolved);
1426
+ validateCalendarConfig(raw, unique2([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1427
1427
  const local = {
1428
1428
  tokenFile: (0, import_node_path5.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1429
1429
  credentialsFile: (0, import_node_path5.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
@@ -1473,12 +1473,12 @@ function buildPlan(cfg) {
1473
1473
  }
1474
1474
  function calendarServiceConfig(cfg, env) {
1475
1475
  if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
1476
- if (!cfg.envs.includes(env)) throw new Error(`calendar env "${env}" is not declared in config envs`);
1476
+ if (!cfg.envs.includes(env) && env !== "prod") throw new Error(`calendar env "${env}" is not declared in config envs`);
1477
1477
  const google = cfg.calendar?.google;
1478
1478
  if (!google) throw new Error("calendar.google is required when the calendar service is enabled");
1479
- const availability = unique2(
1480
- (google.availabilityCalendars?.[env] ?? google.calendars?.[env]).map((id) => id.trim())
1481
- );
1479
+ const configured = google.availabilityCalendars?.[env] ?? google.calendars?.[env];
1480
+ if (!configured?.length) throw new Error(`calendar.google.availabilityCalendars.${env} is required`);
1481
+ const availability = unique2(configured.map((id) => id.trim()));
1482
1482
  return {
1483
1483
  provider: "google",
1484
1484
  access: "book",
@@ -1545,13 +1545,18 @@ function validateCalendarConfig(cfg, envs, services, path) {
1545
1545
  }
1546
1546
  const availability = google[availabilityKey];
1547
1547
  if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
1548
- const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
1548
+ const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env) && env !== "prod");
1549
1549
  if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
1550
1550
  for (const env of envs) {
1551
1551
  const ids = availability[env];
1552
1552
  if (!Array.isArray(ids) || ids.length === 0) {
1553
1553
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1554
1554
  }
1555
+ }
1556
+ for (const [env, ids] of Object.entries(availability)) {
1557
+ if (!Array.isArray(ids) || ids.length === 0) {
1558
+ throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must be a non-empty array`);
1559
+ }
1555
1560
  if (ids.length > 10) {
1556
1561
  throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
1557
1562
  }
@@ -1561,7 +1566,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1561
1566
  }
1562
1567
  if (google.bookingCalendar !== void 0) {
1563
1568
  if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
1564
- const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
1569
+ const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env) && env !== "prod");
1565
1570
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
1566
1571
  for (const [env, value2] of Object.entries(google.bookingCalendar)) {
1567
1572
  if (!safeText3(value2, 1024)) {
@@ -1571,7 +1576,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
1571
1576
  }
1572
1577
  if (google.bookingPageUrl !== void 0) {
1573
1578
  if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
1574
- const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
1579
+ const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env) && env !== "prod");
1575
1580
  if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
1576
1581
  for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
1577
1582
  if (value2 !== null && !safeHttpsUrl(value2)) {
@@ -2187,7 +2192,7 @@ var init_auth_command = __esm({
2187
2192
  // src/tenant.ts
2188
2193
  function resolveEnv(cfg, requested) {
2189
2194
  const env = requested ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
2190
- if (!env || !cfg.envs.includes(env)) {
2195
+ if (!env || !cfg.envs.includes(env) && env !== "prod") {
2191
2196
  throw new Error(`env "${env ?? ""}" is not declared in ${cfg.configPath}`);
2192
2197
  }
2193
2198
  return env;
@@ -2197,12 +2202,10 @@ function resolveTenant(cfg, requested) {
2197
2202
  return { env, tenant: (0, import_apps2.tenantIdFor)(cfg.app.id, env) };
2198
2203
  }
2199
2204
  function bothTenants(cfg) {
2200
- for (const env of ["dev", "prod"]) {
2201
- if (!cfg.envs.includes(env)) {
2202
- throw new Error(
2203
- `env "${env}" is not declared in ${cfg.configPath} \u2014 add it to \`envs\` and run \`odla-ai provision --yes\` before transferring data between environments`
2204
- );
2205
- }
2205
+ if (!cfg.envs.includes("dev")) {
2206
+ throw new Error(
2207
+ `env "dev" is not declared in ${cfg.configPath} \u2014 sandbox/live transfers require an existing sandbox`
2208
+ );
2206
2209
  }
2207
2210
  return { sandbox: (0, import_apps2.tenantIdFor)(cfg.app.id, "dev"), live: (0, import_apps2.tenantIdFor)(cfg.app.id, "prod") };
2208
2211
  }
@@ -2308,7 +2311,7 @@ var init_agent_command = __esm({
2308
2311
  // src/human-session.ts
2309
2312
  async function requireStudioHuman(configPath, action2, destination = "app", env) {
2310
2313
  const cfg = await loadProjectConfig(configPath);
2311
- const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
2314
+ const appEnv = env && (cfg.envs.includes(env) || env === "prod") ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
2312
2315
  const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
2313
2316
  throw new Error(
2314
2317
  `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}`
@@ -3043,8 +3046,7 @@ async function ensureCalendarConnected(ctx, options) {
3043
3046
  async function lifecycleContext(options, optionalProjectCapabilities = []) {
3044
3047
  const cfg = await loadProjectConfig(options.configPath);
3045
3048
  if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
3046
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
3047
- if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
3049
+ const env = resolveEnv(cfg, options.env);
3048
3050
  calendarServiceConfig(cfg, env);
3049
3051
  const out = options.stdout ?? console;
3050
3052
  const doFetch = options.fetch ?? fetch;
@@ -3188,6 +3190,7 @@ var init_calendar = __esm({
3188
3190
  init_handshake_approval();
3189
3191
  init_token();
3190
3192
  init_human_session();
3193
+ init_tenant();
3191
3194
  }
3192
3195
  });
3193
3196
 
@@ -3264,8 +3267,7 @@ var init_capabilities = __esm({
3264
3267
  // src/ai-models.ts
3265
3268
  async function aiModels(options = {}) {
3266
3269
  const cfg = await loadProjectConfig(options.configPath ?? "odla.config.mjs");
3267
- const env = options.env ?? cfg.envs[0] ?? "dev";
3268
- if (!cfg.envs.includes(env)) throw new Error(`ai models env "${env}" is not declared in config envs`);
3270
+ const env = resolveEnv(cfg, options.env);
3269
3271
  const url = new URL(`/registry/apps/${encodeURIComponent(cfg.app.id)}/public-config`, cfg.platformUrl);
3270
3272
  url.searchParams.set("env", env);
3271
3273
  const response2 = await (options.fetch ?? fetch)(url);
@@ -3323,6 +3325,7 @@ var init_ai_models = __esm({
3323
3325
  init_cjs_shims();
3324
3326
  import_ai = require("@odla-ai/ai");
3325
3327
  init_config();
3328
+ init_tenant();
3326
3329
  }
3327
3330
  });
3328
3331
 
@@ -4373,6 +4376,41 @@ async function wranglerLoggedIn(run, cwd) {
4373
4376
  return false;
4374
4377
  }
4375
4378
  }
4379
+ async function wranglerRuntimeTarget(run, opts) {
4380
+ const configPath = findWranglerConfig(opts.cwd);
4381
+ if (!configPath) throw new Error(`no wrangler config found in ${opts.cwd}`);
4382
+ const config = readWranglerConfig(configPath);
4383
+ if (!config) throw new Error("runtime credential delivery requires wrangler.json or wrangler.jsonc");
4384
+ const baseName = typeof config.name === "string" ? config.name : "";
4385
+ const envs = config.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
4386
+ const envConfig = opts.env && envs[opts.env] && typeof envs[opts.env] === "object" ? envs[opts.env] : {};
4387
+ const explicitName = typeof envConfig.name === "string" ? envConfig.name : "";
4388
+ const serviceEnvironments = config.legacy_env === false;
4389
+ if (serviceEnvironments && explicitName) {
4390
+ throw new Error("env.<name>.name is not allowed when wrangler legacy_env is false");
4391
+ }
4392
+ const scriptName = serviceEnvironments ? baseName : explicitName || (opts.env ? `${baseName}-${opts.env}` : baseName);
4393
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(scriptName)) {
4394
+ throw new Error("wrangler config must resolve an exact Worker name before credentials are issued");
4395
+ }
4396
+ const configuredAccount = typeof envConfig.account_id === "string" ? envConfig.account_id : typeof config.account_id === "string" ? config.account_id : "";
4397
+ const whoami = await run("npx", ["wrangler", "whoami"], { cwd: opts.cwd });
4398
+ if (whoami.code !== 0 || /not authenticated/i.test(`${whoami.stdout}${whoami.stderr}`)) {
4399
+ throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
4400
+ }
4401
+ const discovered = [...new Set(`${whoami.stdout}
4402
+ ${whoami.stderr}`.match(/\b[a-f0-9]{32}\b/gi)?.map((id) => id.toLowerCase()) ?? [])];
4403
+ const accountId = configuredAccount.toLowerCase() || (discovered.length === 1 ? discovered[0] : "");
4404
+ if (!/^[a-f0-9]{32}$/.test(accountId) || configuredAccount && discovered.length > 0 && !discovered.includes(accountId)) {
4405
+ throw new Error("Wrangler account is ambiguous or does not match account_id in the config");
4406
+ }
4407
+ return {
4408
+ provider: "cloudflare",
4409
+ accountId,
4410
+ scriptName,
4411
+ ...opts.env ? { environment: opts.env } : {}
4412
+ };
4413
+ }
4376
4414
  function wranglerPutSecret(run, opts) {
4377
4415
  const args = [
4378
4416
  "wrangler",
@@ -4384,6 +4422,16 @@ function wranglerPutSecret(run, opts) {
4384
4422
  ];
4385
4423
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
4386
4424
  }
4425
+ function wranglerBulkSecrets(run, opts) {
4426
+ const args = [
4427
+ "wrangler",
4428
+ "secret",
4429
+ "bulk",
4430
+ ...opts.env ? ["--env", opts.env] : [],
4431
+ ...opts.configPath ? ["--config", opts.configPath] : []
4432
+ ];
4433
+ return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
4434
+ }
4387
4435
  var import_node_child_process2, import_node_fs12, import_node_path11, defaultRunner, WRANGLER_CONFIG_FILES;
4388
4436
  var init_wrangler = __esm({
4389
4437
  "src/wrangler.ts"() {
@@ -4944,16 +4992,10 @@ var init_init = __esm({
4944
4992
  async function secretsPush(options) {
4945
4993
  await secretsPushImpl(options, true);
4946
4994
  }
4947
- async function secretsPushAfterPreflight(options) {
4948
- await secretsPushImpl(options, false);
4949
- }
4950
4995
  async function secretsPushImpl(options, preflight) {
4951
4996
  const out = options.stdout ?? console;
4952
4997
  const cfg = await loadProjectConfig(options.configPath);
4953
- const env = options.env;
4954
- if (!cfg.envs.includes(env)) {
4955
- throw new Error(`env "${env}" is not in config envs (${cfg.envs.join(", ")})`);
4956
- }
4998
+ const env = resolveEnv(cfg, options.env);
4957
4999
  if (PROD_ENV_NAMES.has(env) && !options.yes) {
4958
5000
  throw new Error(`refusing to push a secret to "${env}" without --yes`);
4959
5001
  }
@@ -5019,6 +5061,7 @@ var init_secrets = __esm({
5019
5061
  init_config();
5020
5062
  init_local();
5021
5063
  init_redact();
5064
+ init_tenant();
5022
5065
  init_wrangler();
5023
5066
  PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
5024
5067
  }
@@ -5072,14 +5115,12 @@ async function resolveVaultWrite(options) {
5072
5115
  const out = options.stdout ?? console;
5073
5116
  const doFetch = options.fetch ?? fetch;
5074
5117
  const cfg = await loadProjectConfig(options.configPath);
5075
- if (!cfg.envs.includes(options.env)) {
5076
- throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
5077
- }
5078
- if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
5079
- throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
5118
+ const env = resolveEnv(cfg, options.env);
5119
+ if (PROD_ENV_NAMES2.has(env) && !options.yes) {
5120
+ throw new Error(`refusing to store a secret for "${env}" without --yes`);
5080
5121
  }
5081
5122
  const value2 = await secretInputValue(options, "secret");
5082
- return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
5123
+ return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, env), value: value2, doFetch, out };
5083
5124
  }
5084
5125
  var import_ai3, import_apps10, PROD_ENV_NAMES2;
5085
5126
  var init_secrets_set = __esm({
@@ -5092,6 +5133,7 @@ var init_secrets_set = __esm({
5092
5133
  init_redact();
5093
5134
  init_secret_input();
5094
5135
  init_token();
5136
+ init_tenant();
5095
5137
  PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
5096
5138
  }
5097
5139
  });
@@ -5393,8 +5435,7 @@ var init_skill = __esm({
5393
5435
  async function smoke(options) {
5394
5436
  const out = options.stdout ?? console;
5395
5437
  const cfg = await loadProjectConfig(options.configPath);
5396
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod");
5397
- if (!cfg.envs.includes(env)) throw new Error(`env "${env}" is not declared in ${displayPath(cfg.configPath, cfg.rootDir)}`);
5438
+ const env = resolveEnv(cfg, options.env);
5398
5439
  const credentials = readCredentials(cfg.local.credentialsFile);
5399
5440
  if (!credentials) {
5400
5441
  throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
@@ -5540,6 +5581,7 @@ var init_smoke = __esm({
5540
5581
  init_integrations();
5541
5582
  init_redact();
5542
5583
  init_token();
5584
+ init_tenant();
5543
5585
  }
5544
5586
  });
5545
5587
 
@@ -9501,6 +9543,65 @@ var init_context_command = __esm({
9501
9543
  }
9502
9544
  });
9503
9545
 
9546
+ // src/credential-command.ts
9547
+ async function responseError(response2) {
9548
+ return redactSecrets((await response2.text()).slice(0, 1e3));
9549
+ }
9550
+ async function credentialCommand(parsed, deps = {}) {
9551
+ const action2 = parsed.positionals[1] ?? "list";
9552
+ if (action2 !== "list" && action2 !== "revoke") {
9553
+ throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
9554
+ }
9555
+ assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
9556
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9557
+ const doFetch = deps.fetch ?? fetch;
9558
+ const out = deps.stdout ?? console;
9559
+ const token = await getDeveloperToken(cfg, {
9560
+ configPath: cfg.configPath,
9561
+ token: stringOpt(parsed.options.token),
9562
+ email: stringOpt(parsed.options.email),
9563
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9564
+ openApprovalUrl: deps.openUrl
9565
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
9566
+ const base = `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials`;
9567
+ if (action2 === "revoke") {
9568
+ const id = parsed.positionals[2];
9569
+ if (!id) throw new Error("credentials revoke requires the exact receipt id from credentials list");
9570
+ const response3 = await doFetch(`${base}/${encodeURIComponent(id)}`, {
9571
+ method: "DELETE",
9572
+ headers: { authorization: `Bearer ${token}` }
9573
+ });
9574
+ if (!response3.ok) throw new Error(`runtime credential revoke failed (${response3.status}): ${await responseError(response3)}`);
9575
+ const body2 = await response3.json();
9576
+ return out.log(parsed.options.json === true ? JSON.stringify(body2, null, 2) : `revoked ${body2.receipt.id} (${body2.receipt.env} ${body2.receipt.target.scriptName})`);
9577
+ }
9578
+ const query = new URLSearchParams();
9579
+ const requestedEnv = stringOpt(parsed.options.env);
9580
+ if (requestedEnv) query.set("env", requestedEnv);
9581
+ const response2 = await doFetch(`${base}${query.size ? `?${query}` : ""}`, {
9582
+ headers: { authorization: `Bearer ${token}` }
9583
+ });
9584
+ if (!response2.ok) throw new Error(`runtime credential inventory failed (${response2.status}): ${await responseError(response2)}`);
9585
+ const body = await response2.json();
9586
+ const credentials = parsed.options.all === true ? body.credentials : body.credentials.filter((item) => item.state === "committed");
9587
+ if (parsed.options.json === true) return out.log(JSON.stringify({ credentials }, null, 2));
9588
+ if (!credentials.length) return out.log("no active runtime credential receipts");
9589
+ out.log("RECEIPT ENV TARGET CREATED");
9590
+ for (const item of credentials) {
9591
+ out.log(`${item.id} ${item.env} ${item.target.accountId ?? "local"}/${item.target.scriptName} ${new Date(item.createdAt).toISOString()}`);
9592
+ }
9593
+ }
9594
+ var init_credential_command = __esm({
9595
+ "src/credential-command.ts"() {
9596
+ "use strict";
9597
+ init_cjs_shims();
9598
+ init_argv();
9599
+ init_config();
9600
+ init_redact();
9601
+ init_token();
9602
+ }
9603
+ });
9604
+
9504
9605
  // src/help-usage.ts
9505
9606
  var USAGE_SECTION;
9506
9607
  var init_help_usage = __esm({
@@ -9623,7 +9724,9 @@ Usage:
9623
9724
  odla-ai security report <job-id> [--json]
9624
9725
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
9625
9726
  odla-ai security run [target] --self --ack-redacted-source
9626
- 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]
9727
+ 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]
9728
+ odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
9729
+ odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
9627
9730
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
9628
9731
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
9629
9732
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -9725,6 +9828,9 @@ Commands:
9725
9828
  platform Read canonical fleet health, releases, provider load/freshness,
9726
9829
  explicit unknowns, and next actions through a read-only grant.
9727
9830
  provision Register services, compose integrations, persist credentials, optionally push secrets.
9831
+ "provision --live --yes" initializes only the live instance of
9832
+ an existing sandbox app and enables every configured service;
9833
+ no edit to the dev-first envs list is required.
9728
9834
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
9729
9835
  skill Same installer; --agent accepts all, claude, codex, cursor,
9730
9836
  copilot, gemini, or agents (repeatable or comma-separated).
@@ -9737,15 +9843,21 @@ Safety:
9737
9843
  Every app has two databases on odla.ai: a sandbox (env "dev", tenant
9738
9844
  <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
9739
9845
  production odla.ai \u2014 there is no separate odla to point at. New projects get
9740
- the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
9741
- provision the live database; use --dry-run first to inspect the resolved plan.
9846
+ the sandbox only. Run "provision --live --dry-run", then "provision --live
9847
+ --yes" to initialize live from that existing sandbox project's config. Add
9848
+ "prod" to envs only when ordinary provision runs should manage both; use
9849
+ --dry-run first to inspect either resolved plan.
9742
9850
  A real provision first verifies both the released CLI version and the exact
9743
9851
  pinned versions of every external @odla-ai runtime module before importing
9744
9852
  the command graph. A stale workspace module blocks with its resolved path and
9745
9853
  tells the agent to update/rebase, npm ci, and rebuild.
9746
- Provision caches the approved developer token and service credentials under
9747
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
9748
- preflights Wrangler before any shown-once issuance or destructive rotation.
9854
+ Provision caches the approved developer token under .odla/ with mode 0600.
9855
+ Local-only development provisioning may also cache that developer's service
9856
+ credentials there. \`provision --push-secrets\` instead stages a fresh,
9857
+ independently revocable credential set through the approved handshake,
9858
+ transfers the complete set to the exact Worker with \`wrangler secret bulk\`
9859
+ over stdin, and never writes its plaintext under .odla. Secret push preflights
9860
+ Wrangler before shown-once issuance; it never rotates sibling credentials.
9749
9861
  Projectless PM, Discussions, o11y, runbook, and identity commands use
9750
9862
  --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
9751
9863
  ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
@@ -11824,7 +11936,7 @@ async function issueO11yToken(opts) {
11824
11936
  );
11825
11937
  if (res.status === 409 && !opts.rotateO11y) {
11826
11938
  throw new Error(
11827
- `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`
11939
+ `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`
11828
11940
  );
11829
11941
  }
11830
11942
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
@@ -11850,23 +11962,146 @@ var init_provision_credentials = __esm({
11850
11962
  }
11851
11963
  });
11852
11964
 
11965
+ // src/runtime-credentials.ts
11966
+ function runtimeUrl(cfg, suffix = "") {
11967
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/runtime-credentials${suffix}`;
11968
+ }
11969
+ async function safeError(response2) {
11970
+ const text2 = await response2.text();
11971
+ return redactSecrets(text2.slice(0, 1e3));
11972
+ }
11973
+ async function finish(doFetch, cfg, token, sessionId, method) {
11974
+ return doFetch(runtimeUrl(cfg, `/${encodeURIComponent(sessionId)}`), {
11975
+ method,
11976
+ headers: { authorization: `Bearer ${token}` }
11977
+ });
11978
+ }
11979
+ async function deliverRuntimeCredentials(cfg, options) {
11980
+ const doFetch = options.fetch ?? fetch;
11981
+ const run = options.runner ?? defaultRunner;
11982
+ const wranglerEnv = options.env === "prod" || options.env === "production" ? void 0 : options.env;
11983
+ const target = await wranglerRuntimeTarget(run, {
11984
+ cwd: cfg.rootDir,
11985
+ env: wranglerEnv
11986
+ });
11987
+ const issue = await doFetch(runtimeUrl(cfg), {
11988
+ method: "POST",
11989
+ headers: {
11990
+ authorization: `Bearer ${options.developerToken}`,
11991
+ "content-type": "application/json"
11992
+ },
11993
+ body: JSON.stringify({
11994
+ env: options.env,
11995
+ idempotencyKey: `wrangler:${(0, import_node_crypto5.randomUUID)()}`,
11996
+ target
11997
+ })
11998
+ });
11999
+ const body = await issue.json().catch(() => null);
12000
+ if (!issue.ok || typeof body?.sessionId !== "string" || !body.secrets || typeof body.secrets !== "object") {
12001
+ const detail = body?.error?.code ?? body?.error?.message ?? issue.status;
12002
+ throw new Error(`runtime credential staging failed: ${String(detail)}`);
12003
+ }
12004
+ const secrets = body.secrets;
12005
+ const values = {};
12006
+ for (const name of ["ODLA_API_KEY", "ODLA_O11Y_TOKEN"]) {
12007
+ const value2 = secrets[name];
12008
+ if (typeof value2 === "string" && value2) values[name] = value2;
12009
+ }
12010
+ if (Object.keys(values).length === 0) {
12011
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
12012
+ throw new Error("runtime credential staging returned no configured service credentials");
12013
+ }
12014
+ const pushed = await wranglerBulkSecrets(run, {
12015
+ secrets: values,
12016
+ env: wranglerEnv,
12017
+ cwd: cfg.rootDir
12018
+ });
12019
+ if (pushed.code !== 0) {
12020
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
12021
+ const detail = redactSecrets(`${pushed.stderr || pushed.stdout}`.trim());
12022
+ throw new Error(`wrangler secret bulk failed (exit ${pushed.code}): ${detail}`);
12023
+ }
12024
+ const committed = await finish(
12025
+ doFetch,
12026
+ cfg,
12027
+ options.developerToken,
12028
+ body.sessionId,
12029
+ "POST"
12030
+ );
12031
+ if (!committed.ok) {
12032
+ await finish(doFetch, cfg, options.developerToken, body.sessionId, "DELETE");
12033
+ throw new Error(`runtime credential receipt commit failed: ${await safeError(committed)}`);
12034
+ }
12035
+ options.stdout?.log(
12036
+ `${options.env}: installed ${Object.keys(values).join(" + ")} on ${target.accountId}/${target.scriptName}${target.environment ? ` (${target.environment})` : ""}; plaintext stayed in memory`
12037
+ );
12038
+ return {
12039
+ sessionId: body.sessionId,
12040
+ target,
12041
+ ...values.ODLA_API_KEY ? { dbKey: values.ODLA_API_KEY } : {},
12042
+ ...values.ODLA_O11Y_TOKEN ? { o11yToken: values.ODLA_O11Y_TOKEN } : {}
12043
+ };
12044
+ }
12045
+ var import_node_crypto5;
12046
+ var init_runtime_credentials = __esm({
12047
+ "src/runtime-credentials.ts"() {
12048
+ "use strict";
12049
+ init_cjs_shims();
12050
+ import_node_crypto5 = require("crypto");
12051
+ init_redact();
12052
+ init_wrangler();
12053
+ }
12054
+ });
12055
+
12056
+ // src/provision-live.ts
12057
+ function liveProvisionConfig(cfg) {
12058
+ if (!cfg.envs.includes("dev")) {
12059
+ throw new Error(
12060
+ `--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`
12061
+ );
12062
+ }
12063
+ if (cfg.services.includes("calendar")) {
12064
+ const google = cfg.calendar?.google;
12065
+ const availability = google?.availabilityCalendars ?? google?.calendars;
12066
+ if (!availability?.prod?.length) {
12067
+ throw new Error(
12068
+ `--live requires calendar.google.availabilityCalendars.prod in ${cfg.configPath}; live uses a separate Google Calendar connection, so sandbox calendar ids are not copied`
12069
+ );
12070
+ }
12071
+ }
12072
+ return { ...cfg, envs: ["prod"] };
12073
+ }
12074
+ var init_provision_live = __esm({
12075
+ "src/provision-live.ts"() {
12076
+ "use strict";
12077
+ init_cjs_shims();
12078
+ }
12079
+ });
12080
+
11853
12081
  // src/provision.ts
11854
12082
  async function provision(options) {
11855
12083
  const out = options.stdout ?? console;
11856
- const cfg = await loadProjectConfig(options.configPath);
12084
+ const loaded = await loadProjectConfig(
12085
+ options.configPath,
12086
+ options.live ? { additionalEnvs: ["prod"] } : void 0
12087
+ );
12088
+ const cfg = options.live ? liveProvisionConfig(loaded) : loaded;
11857
12089
  const plan = buildPlan(cfg);
11858
12090
  const hasDb = cfg.services.includes("db");
11859
12091
  const hasO11y = cfg.services.includes("o11y");
11860
12092
  if (options.rotateO11yToken && !hasO11y) {
11861
12093
  throw new Error("--rotate-o11y-token requires the o11y service in odla.config.mjs");
11862
12094
  }
11863
- if (options.pushSecrets && options.writeCredentials === false) {
11864
- throw new Error("--push-secrets cannot be combined with --no-write-credentials");
12095
+ if (options.pushSecrets && (options.rotateKeys || options.rotateO11yToken)) {
12096
+ throw new Error(
12097
+ "--push-secrets always issues an independent runtime credential set; do not combine it with destructive rotation flags"
12098
+ );
11865
12099
  }
11866
12100
  out.log(`odla-ai: ${plan.appName} (${plan.appId})`);
11867
12101
  out.log(` platform: ${plan.platformUrl}`);
11868
12102
  out.log(` db: ${plan.dbEndpoint}`);
11869
12103
  out.log(` envs: ${plan.envs.join(", ")}`);
12104
+ if (options.live) out.log(" target: live setup (existing sandbox app)");
11870
12105
  out.log(` services: ${plan.services.join(", ")}`);
11871
12106
  out.log(` integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
11872
12107
  const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
@@ -11897,7 +12132,7 @@ async function provision(options) {
11897
12132
  out.log(` secrets: ${options.pushSecrets ? "push configured Worker secrets" : "local only"}`);
11898
12133
  return;
11899
12134
  }
11900
- let credentials = readCredentials(cfg.local.credentialsFile);
12135
+ let credentials = options.pushSecrets ? null : readCredentials(cfg.local.credentialsFile);
11901
12136
  if (credentials && credentials.appId !== cfg.app.id) {
11902
12137
  throw new Error(
11903
12138
  `credentials at ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} are for "${credentials.appId}", not "${cfg.app.id}"`
@@ -11906,7 +12141,7 @@ async function provision(options) {
11906
12141
  const rotatesO11y = !!(options.rotateKeys || options.rotateO11yToken);
11907
12142
  const missingO11y = cfg.envs.some((env) => !credentials?.envs[env]?.o11yToken);
11908
12143
  const missingDb = cfg.envs.some((env) => !credentials?.envs[env]?.dbKey);
11909
- const losesShownOnceCredential = hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y);
12144
+ const losesShownOnceCredential = !options.pushSecrets && (hasDb && (!!options.rotateKeys || missingDb) || hasO11y && (rotatesO11y || missingO11y));
11910
12145
  if (options.writeCredentials === false && losesShownOnceCredential) {
11911
12146
  throw new Error("credential issuance/rotation requires the private credentials file; remove --no-write-credentials");
11912
12147
  }
@@ -11927,6 +12162,10 @@ async function provision(options) {
11927
12162
  const existing = await apps.resolveApp(cfg.app.id);
11928
12163
  if (existing) {
11929
12164
  out.log(`app: ${cfg.app.id} already exists`);
12165
+ } else if (options.live) {
12166
+ throw new Error(
12167
+ `cannot set up live for "${cfg.app.id}": the sandbox app does not exist yet; run "odla-ai provision" first`
12168
+ );
11930
12169
  } else {
11931
12170
  try {
11932
12171
  await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
@@ -11976,38 +12215,42 @@ async function provision(options) {
11976
12215
  await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
11977
12216
  }
11978
12217
  }
12218
+ let devVarsCredentials = credentials;
11979
12219
  for (const env of cfg.envs) {
11980
12220
  const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
11981
- credentials = await provisionEnvCredentials({
11982
- cfg,
11983
- env,
11984
- developerToken: token,
11985
- credentials,
11986
- rotateDb: !!options.rotateKeys,
11987
- rotateO11y: rotatesO11y,
11988
- write: options.writeCredentials !== false,
11989
- fetch: doFetch,
11990
- stdout: out
11991
- });
11992
- const dbKey = credentials.envs[env]?.dbKey;
12221
+ let dbKey;
11993
12222
  if (options.pushSecrets) {
11994
- try {
11995
- await secretsPushAfterPreflight({
11996
- configPath: cfg.configPath,
11997
- env,
11998
- yes: options.yes,
11999
- runner: options.secretRunner,
12000
- stdout: out
12001
- });
12002
- } catch (error) {
12003
- const message2 = error instanceof Error ? error.message : String(error);
12004
- const consent = env === "prod" || env === "production" ? " --yes" : "";
12005
- throw new Error(
12006
- `${message2}
12007
- ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
12008
- { cause: error }
12009
- );
12010
- }
12223
+ const delivered = await deliverRuntimeCredentials(cfg, {
12224
+ env,
12225
+ developerToken: token,
12226
+ fetch: doFetch,
12227
+ runner: options.secretRunner,
12228
+ stdout: out
12229
+ });
12230
+ dbKey = delivered.dbKey;
12231
+ devVarsCredentials = mergeCredential(devVarsCredentials, {
12232
+ appId: cfg.app.id,
12233
+ platformUrl: cfg.platformUrl,
12234
+ dbEndpoint: cfg.dbEndpoint,
12235
+ env,
12236
+ tenantId,
12237
+ ...delivered.dbKey ? { dbKey: delivered.dbKey } : {},
12238
+ ...delivered.o11yToken ? { o11yToken: delivered.o11yToken } : {}
12239
+ });
12240
+ } else {
12241
+ credentials = await provisionEnvCredentials({
12242
+ cfg,
12243
+ env,
12244
+ developerToken: token,
12245
+ credentials,
12246
+ rotateDb: !!options.rotateKeys,
12247
+ rotateO11y: rotatesO11y,
12248
+ write: options.writeCredentials !== false,
12249
+ fetch: doFetch,
12250
+ stdout: out
12251
+ });
12252
+ devVarsCredentials = credentials;
12253
+ dbKey = credentials.envs[env]?.dbKey;
12011
12254
  }
12012
12255
  if (schema && dbKey) {
12013
12256
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
@@ -12031,13 +12274,13 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
12031
12274
  }
12032
12275
  }
12033
12276
  }
12034
- if (options.writeCredentials !== false && credentials) {
12277
+ if (!options.pushSecrets && options.writeCredentials !== false && credentials) {
12035
12278
  const ignored = cfg.local.gitignore && gitignoreEntry(cfg.rootDir, cfg.local.credentialsFile);
12036
12279
  out.log(`credentials: wrote ${displayPath(cfg.local.credentialsFile, cfg.rootDir)} (0600${ignored ? ", gitignored" : ""})`);
12037
12280
  }
12038
- if (devVarsTarget && credentials) {
12281
+ if (devVarsTarget && devVarsCredentials) {
12039
12282
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
12040
- writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
12283
+ writeDevVars(devVarsTarget, devVarsCredentials, env, o11yDevVars(cfg));
12041
12284
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
12042
12285
  }
12043
12286
  if (cfg.services.includes("calendar")) {
@@ -12077,6 +12320,8 @@ var init_provision = __esm({
12077
12320
  init_token();
12078
12321
  init_local();
12079
12322
  init_provision_credentials();
12323
+ init_runtime_credentials();
12324
+ init_provision_live();
12080
12325
  init_provision_helpers();
12081
12326
  init_secrets();
12082
12327
  }
@@ -12186,6 +12431,7 @@ var init_surface = __esm({
12186
12431
  code: { connect: {} },
12187
12432
  config: { diff: {}, plan: {}, apply: {} },
12188
12433
  context: { show: {}, list: {}, save: {}, remove: {} },
12434
+ credentials: { list: {}, revoke: {} },
12189
12435
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
12190
12436
  discuss: {
12191
12437
  groups: {},
@@ -13982,6 +14228,10 @@ async function runCli(argv2 = process.argv.slice(2), dependencies = {}) {
13982
14228
  await contextCommand(parsed, runtime);
13983
14229
  return;
13984
14230
  }
14231
+ if (command === "credentials") {
14232
+ await credentialCommand(parsed, runtime);
14233
+ return;
14234
+ }
13985
14235
  if (command === "runbook") {
13986
14236
  await runbookCommand(parsed, runtime);
13987
14237
  return;
@@ -14049,6 +14299,7 @@ async function runCli(argv2 = process.argv.slice(2), dependencies = {}) {
14049
14299
  async function provisionCommand(parsed, dependencies) {
14050
14300
  assertArgs(parsed, [
14051
14301
  "config",
14302
+ "live",
14052
14303
  "dry-run",
14053
14304
  "rotate-keys",
14054
14305
  "rotate-o11y-token",
@@ -14065,6 +14316,7 @@ async function provisionCommand(parsed, dependencies) {
14065
14316
  const writeDevVars2 = parsed.options["write-dev-vars"];
14066
14317
  const options = {
14067
14318
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
14319
+ live: parsed.options.live === true,
14068
14320
  dryRun: parsed.options["dry-run"] === true,
14069
14321
  rotateKeys: parsed.options["rotate-keys"] === true,
14070
14322
  rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
@@ -14122,6 +14374,7 @@ var init_cli = __esm({
14122
14374
  init_cli_project();
14123
14375
  init_code_command();
14124
14376
  init_context_command();
14377
+ init_credential_command();
14125
14378
  init_help();
14126
14379
  init_version();
14127
14380
  init_discuss_command();