@odla-ai/cli 0.28.2 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -461,9 +461,23 @@ the previous bearer.
461
461
  - `src/odla/rules.mjs`
462
462
  - `.gitignore` entries for local credentials
463
463
 
464
- New configs contain only the `dev` environment. Add `prod` explicitly after
465
- the development flow is healthy; `provision` refuses to mutate `prod` or
466
- `production` without `--yes` and supports `--dry-run` for review.
464
+ New configs contain only the `dev` environment. After the development flow is
465
+ healthy, initialize the existing app's live instance without editing the config:
466
+
467
+ ```sh
468
+ npx @odla-ai/cli provision --live --dry-run
469
+ npx @odla-ai/cli provision --live --yes --push-secrets
470
+ ```
471
+
472
+ `--live` targets only `prod`, enables every configured service, provisions its
473
+ credentials/schema/integrations, and refuses to create an app when the sandbox
474
+ does not exist yet. Add `prod` to `envs` only when normal provision runs should
475
+ manage both environments. Calendar apps may predeclare
476
+ `calendar.google.availabilityCalendars.prod` while `envs` remains dev-only;
477
+ live still uses its own Google consent. Every production mutation requires
478
+ `--yes`. Afterwards, explicit live operations such as `smoke --env prod`,
479
+ `secrets push --env prod --yes`, and `app go-live` work while unqualified
480
+ commands continue to default to the sandbox.
467
481
 
468
482
  `provision` performs the standard safe setup flow:
469
483
 
package/REQUIREMENTS.md CHANGED
@@ -28,6 +28,10 @@ Agnacl, but none should mention or special-case Agnacl.
28
28
  - Source semantics remain with the coding agent: package installation, Worker
29
29
  instrumentation, and application-specific signals/rules. Humans approve
30
30
  identity, production mutation, and explicit destructive rotation.
31
+ - An existing dev-first sandbox app must be able to initialize only its live
32
+ environment with one explicit CLI mode. That mode enables the complete
33
+ configured service set, requires production consent, refuses to create a new
34
+ app, and must not make live part of later unqualified provision runs.
31
35
 
32
36
  ## Human Approval
33
37
 
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
 
@@ -4950,10 +4953,7 @@ async function secretsPushAfterPreflight(options) {
4950
4953
  async function secretsPushImpl(options, preflight) {
4951
4954
  const out = options.stdout ?? console;
4952
4955
  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
- }
4956
+ const env = resolveEnv(cfg, options.env);
4957
4957
  if (PROD_ENV_NAMES.has(env) && !options.yes) {
4958
4958
  throw new Error(`refusing to push a secret to "${env}" without --yes`);
4959
4959
  }
@@ -5019,6 +5019,7 @@ var init_secrets = __esm({
5019
5019
  init_config();
5020
5020
  init_local();
5021
5021
  init_redact();
5022
+ init_tenant();
5022
5023
  init_wrangler();
5023
5024
  PROD_ENV_NAMES = /* @__PURE__ */ new Set(["prod", "production"]);
5024
5025
  }
@@ -5072,14 +5073,12 @@ async function resolveVaultWrite(options) {
5072
5073
  const out = options.stdout ?? console;
5073
5074
  const doFetch = options.fetch ?? fetch;
5074
5075
  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`);
5076
+ const env = resolveEnv(cfg, options.env);
5077
+ if (PROD_ENV_NAMES2.has(env) && !options.yes) {
5078
+ throw new Error(`refusing to store a secret for "${env}" without --yes`);
5080
5079
  }
5081
5080
  const value2 = await secretInputValue(options, "secret");
5082
- return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
5081
+ return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, env), value: value2, doFetch, out };
5083
5082
  }
5084
5083
  var import_ai3, import_apps10, PROD_ENV_NAMES2;
5085
5084
  var init_secrets_set = __esm({
@@ -5092,6 +5091,7 @@ var init_secrets_set = __esm({
5092
5091
  init_redact();
5093
5092
  init_secret_input();
5094
5093
  init_token();
5094
+ init_tenant();
5095
5095
  PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
5096
5096
  }
5097
5097
  });
@@ -5393,8 +5393,7 @@ var init_skill = __esm({
5393
5393
  async function smoke(options) {
5394
5394
  const out = options.stdout ?? console;
5395
5395
  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)}`);
5396
+ const env = resolveEnv(cfg, options.env);
5398
5397
  const credentials = readCredentials(cfg.local.credentialsFile);
5399
5398
  if (!credentials) {
5400
5399
  throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
@@ -5540,6 +5539,7 @@ var init_smoke = __esm({
5540
5539
  init_integrations();
5541
5540
  init_redact();
5542
5541
  init_token();
5542
+ init_tenant();
5543
5543
  }
5544
5544
  });
5545
5545
 
@@ -9623,7 +9623,7 @@ Usage:
9623
9623
  odla-ai security report <job-id> [--json]
9624
9624
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
9625
9625
  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]
9626
+ 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]
9627
9627
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
9628
9628
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
9629
9629
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -9725,6 +9725,9 @@ Commands:
9725
9725
  platform Read canonical fleet health, releases, provider load/freshness,
9726
9726
  explicit unknowns, and next actions through a read-only grant.
9727
9727
  provision Register services, compose integrations, persist credentials, optionally push secrets.
9728
+ "provision --live --yes" initializes only the live instance of
9729
+ an existing sandbox app and enables every configured service;
9730
+ no edit to the dev-first envs list is required.
9728
9731
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
9729
9732
  skill Same installer; --agent accepts all, claude, codex, cursor,
9730
9733
  copilot, gemini, or agents (repeatable or comma-separated).
@@ -9737,8 +9740,10 @@ Safety:
9737
9740
  Every app has two databases on odla.ai: a sandbox (env "dev", tenant
9738
9741
  <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
9739
9742
  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.
9743
+ the sandbox only. Run "provision --live --dry-run", then "provision --live
9744
+ --yes" to initialize live from that existing sandbox project's config. Add
9745
+ "prod" to envs only when ordinary provision runs should manage both; use
9746
+ --dry-run first to inspect either resolved plan.
9742
9747
  A real provision first verifies both the released CLI version and the exact
9743
9748
  pinned versions of every external @odla-ai runtime module before importing
9744
9749
  the command graph. A stale workspace module blocks with its resolved path and
@@ -11850,10 +11855,39 @@ var init_provision_credentials = __esm({
11850
11855
  }
11851
11856
  });
11852
11857
 
11858
+ // src/provision-live.ts
11859
+ function liveProvisionConfig(cfg) {
11860
+ if (!cfg.envs.includes("dev")) {
11861
+ throw new Error(
11862
+ `--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`
11863
+ );
11864
+ }
11865
+ if (cfg.services.includes("calendar")) {
11866
+ const google = cfg.calendar?.google;
11867
+ const availability = google?.availabilityCalendars ?? google?.calendars;
11868
+ if (!availability?.prod?.length) {
11869
+ throw new Error(
11870
+ `--live requires calendar.google.availabilityCalendars.prod in ${cfg.configPath}; live uses a separate Google Calendar connection, so sandbox calendar ids are not copied`
11871
+ );
11872
+ }
11873
+ }
11874
+ return { ...cfg, envs: ["prod"] };
11875
+ }
11876
+ var init_provision_live = __esm({
11877
+ "src/provision-live.ts"() {
11878
+ "use strict";
11879
+ init_cjs_shims();
11880
+ }
11881
+ });
11882
+
11853
11883
  // src/provision.ts
11854
11884
  async function provision(options) {
11855
11885
  const out = options.stdout ?? console;
11856
- const cfg = await loadProjectConfig(options.configPath);
11886
+ const loaded = await loadProjectConfig(
11887
+ options.configPath,
11888
+ options.live ? { additionalEnvs: ["prod"] } : void 0
11889
+ );
11890
+ const cfg = options.live ? liveProvisionConfig(loaded) : loaded;
11857
11891
  const plan = buildPlan(cfg);
11858
11892
  const hasDb = cfg.services.includes("db");
11859
11893
  const hasO11y = cfg.services.includes("o11y");
@@ -11867,6 +11901,7 @@ async function provision(options) {
11867
11901
  out.log(` platform: ${plan.platformUrl}`);
11868
11902
  out.log(` db: ${plan.dbEndpoint}`);
11869
11903
  out.log(` envs: ${plan.envs.join(", ")}`);
11904
+ if (options.live) out.log(" target: live setup (existing sandbox app)");
11870
11905
  out.log(` services: ${plan.services.join(", ")}`);
11871
11906
  out.log(` integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
11872
11907
  const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
@@ -11927,6 +11962,10 @@ async function provision(options) {
11927
11962
  const existing = await apps.resolveApp(cfg.app.id);
11928
11963
  if (existing) {
11929
11964
  out.log(`app: ${cfg.app.id} already exists`);
11965
+ } else if (options.live) {
11966
+ throw new Error(
11967
+ `cannot set up live for "${cfg.app.id}": the sandbox app does not exist yet; run "odla-ai provision" first`
11968
+ );
11930
11969
  } else {
11931
11970
  try {
11932
11971
  await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
@@ -12077,6 +12116,7 @@ var init_provision = __esm({
12077
12116
  init_token();
12078
12117
  init_local();
12079
12118
  init_provision_credentials();
12119
+ init_provision_live();
12080
12120
  init_provision_helpers();
12081
12121
  init_secrets();
12082
12122
  }
@@ -14049,6 +14089,7 @@ async function runCli(argv2 = process.argv.slice(2), dependencies = {}) {
14049
14089
  async function provisionCommand(parsed, dependencies) {
14050
14090
  assertArgs(parsed, [
14051
14091
  "config",
14092
+ "live",
14052
14093
  "dry-run",
14053
14094
  "rotate-keys",
14054
14095
  "rotate-o11y-token",
@@ -14065,6 +14106,7 @@ async function provisionCommand(parsed, dependencies) {
14065
14106
  const writeDevVars2 = parsed.options["write-dev-vars"];
14066
14107
  const options = {
14067
14108
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
14109
+ live: parsed.options.live === true,
14068
14110
  dryRun: parsed.options["dry-run"] === true,
14069
14111
  rotateKeys: parsed.options["rotate-keys"] === true,
14070
14112
  rotateO11yToken: parsed.options["rotate-o11y-token"] === true,