@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-STJCWZJU.js";
4
+ } from "./chunk-YTVLTADT.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-DNOAYDBR.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);
@@ -4465,10 +4466,7 @@ async function secretsPushAfterPreflight(options) {
4465
4466
  async function secretsPushImpl(options, preflight) {
4466
4467
  const out = options.stdout ?? console;
4467
4468
  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
- }
4469
+ const env = resolveEnv(cfg, options.env);
4472
4470
  if (PROD_ENV_NAMES.has(env) && !options.yes) {
4473
4471
  throw new Error(`refusing to push a secret to "${env}" without --yes`);
4474
4472
  }
@@ -4578,14 +4576,12 @@ async function resolveVaultWrite(options) {
4578
4576
  const out = options.stdout ?? console;
4579
4577
  const doFetch = options.fetch ?? fetch;
4580
4578
  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`);
4579
+ const env = resolveEnv(cfg, options.env);
4580
+ if (PROD_ENV_NAMES2.has(env) && !options.yes) {
4581
+ throw new Error(`refusing to store a secret for "${env}" without --yes`);
4586
4582
  }
4587
4583
  const value2 = await secretInputValue(options, "secret");
4588
- return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
4584
+ return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, env), value: value2, doFetch, out };
4589
4585
  }
4590
4586
 
4591
4587
  // src/skill.ts
@@ -4872,8 +4868,7 @@ function listFiles(dir) {
4872
4868
  async function smoke(options) {
4873
4869
  const out = options.stdout ?? console;
4874
4870
  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)}`);
4871
+ const env = resolveEnv(cfg, options.env);
4877
4872
  const credentials = readCredentials(cfg.local.credentialsFile);
4878
4873
  if (!credentials) {
4879
4874
  throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
@@ -8949,7 +8944,7 @@ Usage:
8949
8944
  odla-ai security report <job-id> [--json]
8950
8945
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8951
8946
  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]
8947
+ 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]
8953
8948
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8954
8949
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8955
8950
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -9049,6 +9044,9 @@ Commands:
9049
9044
  platform Read canonical fleet health, releases, provider load/freshness,
9050
9045
  explicit unknowns, and next actions through a read-only grant.
9051
9046
  provision Register services, compose integrations, persist credentials, optionally push secrets.
9047
+ "provision --live --yes" initializes only the live instance of
9048
+ an existing sandbox app and enables every configured service;
9049
+ no edit to the dev-first envs list is required.
9052
9050
  smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
9053
9051
  skill Same installer; --agent accepts all, claude, codex, cursor,
9054
9052
  copilot, gemini, or agents (repeatable or comma-separated).
@@ -9061,8 +9059,10 @@ Safety:
9061
9059
  Every app has two databases on odla.ai: a sandbox (env "dev", tenant
9062
9060
  <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
9063
9061
  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.
9062
+ the sandbox only. Run "provision --live --dry-run", then "provision --live
9063
+ --yes" to initialize live from that existing sandbox project's config. Add
9064
+ "prod" to envs only when ordinary provision runs should manage both; use
9065
+ --dry-run first to inspect either resolved plan.
9066
9066
  A real provision first verifies both the released CLI version and the exact
9067
9067
  pinned versions of every external @odla-ai runtime module before importing
9068
9068
  the command graph. A stale workspace module blocks with its resolved path and
@@ -11009,10 +11009,33 @@ async function safeText7(res) {
11009
11009
  }
11010
11010
  }
11011
11011
 
11012
+ // src/provision-live.ts
11013
+ function liveProvisionConfig(cfg) {
11014
+ if (!cfg.envs.includes("dev")) {
11015
+ throw new Error(
11016
+ `--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`
11017
+ );
11018
+ }
11019
+ if (cfg.services.includes("calendar")) {
11020
+ const google = cfg.calendar?.google;
11021
+ const availability = google?.availabilityCalendars ?? google?.calendars;
11022
+ if (!availability?.prod?.length) {
11023
+ throw new Error(
11024
+ `--live requires calendar.google.availabilityCalendars.prod in ${cfg.configPath}; live uses a separate Google Calendar connection, so sandbox calendar ids are not copied`
11025
+ );
11026
+ }
11027
+ }
11028
+ return { ...cfg, envs: ["prod"] };
11029
+ }
11030
+
11012
11031
  // src/provision.ts
11013
11032
  async function provision(options) {
11014
11033
  const out = options.stdout ?? console;
11015
- const cfg = await loadProjectConfig(options.configPath);
11034
+ const loaded = await loadProjectConfig(
11035
+ options.configPath,
11036
+ options.live ? { additionalEnvs: ["prod"] } : void 0
11037
+ );
11038
+ const cfg = options.live ? liveProvisionConfig(loaded) : loaded;
11016
11039
  const plan = buildPlan(cfg);
11017
11040
  const hasDb = cfg.services.includes("db");
11018
11041
  const hasO11y = cfg.services.includes("o11y");
@@ -11026,6 +11049,7 @@ async function provision(options) {
11026
11049
  out.log(` platform: ${plan.platformUrl}`);
11027
11050
  out.log(` db: ${plan.dbEndpoint}`);
11028
11051
  out.log(` envs: ${plan.envs.join(", ")}`);
11052
+ if (options.live) out.log(" target: live setup (existing sandbox app)");
11029
11053
  out.log(` services: ${plan.services.join(", ")}`);
11030
11054
  out.log(` integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
11031
11055
  const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
@@ -11086,6 +11110,10 @@ async function provision(options) {
11086
11110
  const existing = await apps.resolveApp(cfg.app.id);
11087
11111
  if (existing) {
11088
11112
  out.log(`app: ${cfg.app.id} already exists`);
11113
+ } else if (options.live) {
11114
+ throw new Error(
11115
+ `cannot set up live for "${cfg.app.id}": the sandbox app does not exist yet; run "odla-ai provision" first`
11116
+ );
11089
11117
  } else {
11090
11118
  try {
11091
11119
  await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
@@ -13091,6 +13119,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
13091
13119
  async function provisionCommand(parsed, dependencies) {
13092
13120
  assertArgs(parsed, [
13093
13121
  "config",
13122
+ "live",
13094
13123
  "dry-run",
13095
13124
  "rotate-keys",
13096
13125
  "rotate-o11y-token",
@@ -13107,6 +13136,7 @@ async function provisionCommand(parsed, dependencies) {
13107
13136
  const writeDevVars2 = parsed.options["write-dev-vars"];
13108
13137
  const options = {
13109
13138
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
13139
+ live: parsed.options.live === true,
13110
13140
  dryRun: parsed.options["dry-run"] === true,
13111
13141
  rotateKeys: parsed.options["rotate-keys"] === true,
13112
13142
  rotateO11yToken: parsed.options["rotate-o11y-token"] === true,