@odla-ai/cli 0.16.6 → 0.17.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
@@ -1239,14 +1239,36 @@ function unique2(values) {
1239
1239
  return [...new Set(values.filter(Boolean))];
1240
1240
  }
1241
1241
 
1242
+ // src/tenant.ts
1243
+ var import_apps = require("@odla-ai/apps");
1244
+ function resolveEnv(cfg, requested) {
1245
+ const env = requested ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
1246
+ if (!env || !cfg.envs.includes(env)) {
1247
+ throw new Error(`env "${env ?? ""}" is not declared in ${cfg.configPath}`);
1248
+ }
1249
+ return env;
1250
+ }
1251
+ function resolveTenant(cfg, requested) {
1252
+ const env = resolveEnv(cfg, requested);
1253
+ return { env, tenant: (0, import_apps.tenantIdFor)(cfg.app.id, env) };
1254
+ }
1255
+ function bothTenants(cfg) {
1256
+ for (const env of ["dev", "prod"]) {
1257
+ if (!cfg.envs.includes(env)) {
1258
+ throw new Error(
1259
+ `env "${env}" is not declared in ${cfg.configPath} \u2014 add it to \`envs\` and run \`odla-ai provision --yes\` before transferring data between environments`
1260
+ );
1261
+ }
1262
+ }
1263
+ return { sandbox: (0, import_apps.tenantIdFor)(cfg.app.id, "dev"), live: (0, import_apps.tenantIdFor)(cfg.app.id, "prod") };
1264
+ }
1265
+
1242
1266
  // src/app-export.ts
1243
1267
  async function appExport(options) {
1244
1268
  const cfg = await loadProjectConfig(options.configPath);
1245
1269
  const out = options.stdout ?? console;
1246
1270
  const doFetch = options.fetch ?? fetch;
1247
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
1248
- if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
1249
- const tenant = env === "prod" ? cfg.app.id : `${cfg.app.id}--${env}`;
1271
+ const { tenant } = resolveTenant(cfg, options.env);
1250
1272
  const token = await getDeveloperToken(
1251
1273
  cfg,
1252
1274
  { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
@@ -1281,6 +1303,84 @@ async function appExport(options) {
1281
1303
  return { file, backup: newest };
1282
1304
  }
1283
1305
 
1306
+ // src/app-import.ts
1307
+ var import_node_fs6 = require("fs");
1308
+ var import_import = require("@odla-ai/db/import");
1309
+ function chooseIdMode(options, rows) {
1310
+ const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
1311
+ if (chosen.length > 1) throw new Error("choose one of --id-field, --key, or --generate-ids");
1312
+ if (options.key) return { idMode: "key" };
1313
+ if (options.generateIds) return { idMode: "generate" };
1314
+ if (options.idField) return { idMode: "field", idField: options.idField };
1315
+ if (rows.length > 0 && rows.every((row) => typeof row?.id === "string" && row.id)) return { idMode: "field" };
1316
+ throw new Error(
1317
+ "rows have no usable `id` \u2014 pass --id-field <field> to name the id column, --key <unique-attr> to match on a unique attribute, or --generate-ids to mint new ids (which is NOT idempotent: re-running duplicates rows)"
1318
+ );
1319
+ }
1320
+ async function appImport(options) {
1321
+ const cfg = await loadProjectConfig(options.configPath);
1322
+ const out = options.stdout ?? console;
1323
+ const doFetch = options.fetch ?? fetch;
1324
+ const { tenant } = resolveTenant(cfg, options.env);
1325
+ const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs6.readFileSync)(0, "utf8")))() : (0, import_node_fs6.readFileSync)(options.file, "utf8");
1326
+ const { format, sources } = (0, import_import.parseImport)(text, options.ns);
1327
+ if (format === "namespace-map" && options.ns) {
1328
+ throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
1329
+ }
1330
+ if (format !== "namespace-map" && !options.ns) throw new Error("--ns is required unless the file is a {namespace: rows} map");
1331
+ if (sources.length === 0) throw new Error(`${options.file} contains no rows`);
1332
+ const mode = chooseIdMode(options, sources.map((s) => s.row));
1333
+ const build = { ...mode, keyAttr: options.key };
1334
+ const { ops, rejected, ignored } = (0, import_import.buildImportOps)(sources, build);
1335
+ if (rejected.length > 0) {
1336
+ const detail = rejected.slice(0, 10).map((r) => ` row ${r.index}: ${r.reason}`).join("\n");
1337
+ const more = rejected.length > 10 ? `
1338
+ \u2026and ${rejected.length - 10} more` : "";
1339
+ throw new Error(`${rejected.length} row(s) cannot be imported \u2014 fix the input and re-run:
1340
+ ${detail}${more}`);
1341
+ }
1342
+ const chunks = (0, import_import.planImportChunks)(ops);
1343
+ const result = { tenant, rows: ops.length, chunks: chunks.length, committed: 0, duplicate: 0, txIds: [], ignored };
1344
+ if (ignored.length > 0) out.log(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
1345
+ if (options.dryRun || options.yes !== true) {
1346
+ const why = options.dryRun ? "--dry-run" : "no --yes";
1347
+ out.log(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
1348
+ if (options.json) out.log(JSON.stringify(result, null, 2));
1349
+ return result;
1350
+ }
1351
+ const token = await getDeveloperToken(
1352
+ cfg,
1353
+ { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1354
+ doFetch,
1355
+ out
1356
+ );
1357
+ const runId = crypto.randomUUID();
1358
+ const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
1359
+ for (const chunk of chunks) {
1360
+ const res = await doFetch(url, {
1361
+ method: "POST",
1362
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1363
+ body: JSON.stringify({ mutationId: (0, import_import.importMutationId)(runId, chunk.index), ops: chunk.ops })
1364
+ });
1365
+ const body = await res.json().catch(() => ({}));
1366
+ if (!res.ok) {
1367
+ const code = body.error?.code ? ` (${body.error.code})` : "";
1368
+ throw new Error(
1369
+ `chunk ${chunk.index + 1} of ${chunks.length} failed${code}: ${body.error?.message ?? res.status}. ${result.committed} row(s) already committed; fix the input and re-run to import the rest.`
1370
+ );
1371
+ }
1372
+ result.committed += chunk.ops.length;
1373
+ if (typeof body.txId === "number") result.txIds.push(body.txId);
1374
+ if (body.duplicate) result.duplicate++;
1375
+ }
1376
+ if (options.json) out.log(JSON.stringify(result, null, 2));
1377
+ else {
1378
+ const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
1379
+ out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
1380
+ }
1381
+ return result;
1382
+ }
1383
+
1284
1384
  // src/app-owners.ts
1285
1385
  var sink = (options) => options.stdout ?? console;
1286
1386
  async function ownersRequest(method, suffix, options, body) {
@@ -1368,6 +1468,133 @@ async function appOwnersCommand(parsed, dependencies = {}) {
1368
1468
  );
1369
1469
  }
1370
1470
 
1471
+ // src/app-transfer.ts
1472
+ function endpointsFor(verb, tenants) {
1473
+ const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
1474
+ const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
1475
+ if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
1476
+ return { ...up, mode: "cutover" };
1477
+ }
1478
+ async function api(cfg, token, path, init, doFetch) {
1479
+ const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
1480
+ ...init,
1481
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
1482
+ });
1483
+ return { status: res.status, body: await res.json().catch(() => ({})) };
1484
+ }
1485
+ function describe(side) {
1486
+ if (!side.exists) return "not provisioned";
1487
+ if (side.maxTx === 0) return "empty \u2014 never written";
1488
+ const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
1489
+ const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
1490
+ return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
1491
+ }
1492
+ function printPlan(out, verb, pre, opts) {
1493
+ const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
1494
+ out.log(`${verb} (${arrow})`);
1495
+ out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
1496
+ out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
1497
+ if (verb === "promote") {
1498
+ out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
1499
+ } else {
1500
+ out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
1501
+ out.log(` stays put: ${pre.staysPut.join(", ")}`);
1502
+ const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
1503
+ out.log(` identity: ${identity}`);
1504
+ out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
1505
+ }
1506
+ for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
1507
+ }
1508
+ async function appTransfer(options) {
1509
+ const cfg = await loadProjectConfig(options.configPath);
1510
+ const out = options.stdout ?? console;
1511
+ const doFetch = options.fetch ?? fetch;
1512
+ const tenants = bothTenants(cfg);
1513
+ const route2 = endpointsFor(options.verb, tenants);
1514
+ const token = await getDeveloperToken(
1515
+ cfg,
1516
+ { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1517
+ doFetch,
1518
+ out
1519
+ );
1520
+ const pre = await api(
1521
+ cfg,
1522
+ token,
1523
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
1524
+ void 0,
1525
+ doFetch
1526
+ );
1527
+ if (pre.status !== 200) {
1528
+ throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
1529
+ }
1530
+ const plan = pre.body;
1531
+ printPlan(out, options.verb, plan, options);
1532
+ if (options.verb === "go-live" && !plan.targetEmpty) {
1533
+ throw new Error(
1534
+ `${route2.target} already has data \u2014 go-live has already happened. Use \`odla-ai app promote --yes\` to push schema and rules, or \`odla-ai app refresh-sandbox --yes\` to pull live back down.`
1535
+ );
1536
+ }
1537
+ if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
1538
+ if (options.dryRun || options.yes !== true) {
1539
+ out.log(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
1540
+ if (options.json) out.log(JSON.stringify(plan, null, 2));
1541
+ return { ok: true, plan };
1542
+ }
1543
+ if (options.verb === "promote") {
1544
+ const res2 = await api(
1545
+ cfg,
1546
+ token,
1547
+ `/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
1548
+ { method: "POST", body: JSON.stringify({ from: "dev" }) },
1549
+ doFetch
1550
+ );
1551
+ if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
1552
+ if (options.json) out.log(JSON.stringify(res2.body, null, 2));
1553
+ else out.log(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
1554
+ return { ok: true, plan, result: res2.body };
1555
+ }
1556
+ const res = await api(
1557
+ cfg,
1558
+ token,
1559
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
1560
+ {
1561
+ method: "POST",
1562
+ body: JSON.stringify({
1563
+ from: route2.from,
1564
+ mode: route2.mode,
1565
+ ...options.includeIdentity ? { includeUsers: true } : {},
1566
+ ...options.includeFiles ? { includeFiles: true } : {}
1567
+ })
1568
+ },
1569
+ doFetch
1570
+ );
1571
+ if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
1572
+ out.log(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
1573
+ if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
1574
+ if (options.json) out.log(JSON.stringify(res.body, null, 2));
1575
+ return { ok: true, plan, result: res.body };
1576
+ }
1577
+ async function copyFiles(cfg, token, route2, doFetch, out) {
1578
+ for (let attempt = 1; attempt <= 20; attempt++) {
1579
+ const res = await api(
1580
+ cfg,
1581
+ token,
1582
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
1583
+ { method: "POST", body: JSON.stringify({ from: route2.from }) },
1584
+ doFetch
1585
+ );
1586
+ if (res.status === 200) {
1587
+ out.log(`${route2.target}: files copied`);
1588
+ return;
1589
+ }
1590
+ if (!res.body.error?.retry) {
1591
+ throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
1592
+ }
1593
+ out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
1594
+ }
1595
+ throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
1596
+ }
1597
+
1371
1598
  // src/app-lifecycle.ts
1372
1599
  async function lifecycleCall(action, options) {
1373
1600
  const cfg = await loadProjectConfig(options.configPath);
@@ -1429,9 +1656,48 @@ async function appCommand(parsed, dependencies = {}) {
1429
1656
  });
1430
1657
  return;
1431
1658
  }
1659
+ if (sub === "refresh-sandbox" || sub === "go-live" || sub === "promote") {
1660
+ assertArgs(parsed, ["config", "include-identity", "include-files", "dry-run", "yes", "token", "email", "json"], 2);
1661
+ await appTransfer({
1662
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
1663
+ verb: sub,
1664
+ includeIdentity: parsed.options["include-identity"] === true,
1665
+ includeFiles: parsed.options["include-files"] === true,
1666
+ dryRun: parsed.options["dry-run"] === true,
1667
+ yes: parsed.options.yes === true,
1668
+ token: stringOpt(parsed.options.token),
1669
+ email: stringOpt(parsed.options.email),
1670
+ json: parsed.options.json === true,
1671
+ fetch: dependencies.fetch,
1672
+ stdout: dependencies.stdout
1673
+ });
1674
+ return;
1675
+ }
1676
+ if (sub === "import") {
1677
+ assertArgs(parsed, ["config", "env", "ns", "id-field", "key", "generate-ids", "dry-run", "yes", "token", "email", "json"], 3);
1678
+ const file = parsed.positionals[2];
1679
+ if (!file) throw new Error('app import needs a file: "odla-ai app import rows.json --ns todos --yes" (or "-" for stdin)');
1680
+ await appImport({
1681
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
1682
+ file,
1683
+ env: stringOpt(parsed.options.env),
1684
+ ns: stringOpt(parsed.options.ns),
1685
+ idField: stringOpt(parsed.options["id-field"]),
1686
+ key: stringOpt(parsed.options.key),
1687
+ generateIds: parsed.options["generate-ids"] === true,
1688
+ dryRun: parsed.options["dry-run"] === true,
1689
+ yes: parsed.options.yes === true,
1690
+ token: stringOpt(parsed.options.token),
1691
+ email: stringOpt(parsed.options.email),
1692
+ json: parsed.options.json === true,
1693
+ fetch: dependencies.fetch,
1694
+ stdout: dependencies.stdout
1695
+ });
1696
+ return;
1697
+ }
1432
1698
  if (sub !== "archive" && sub !== "restore") {
1433
1699
  throw new Error(
1434
- `unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", "odla-ai app export", or "odla-ai app owners <list|add|remove>". (Permanent deletion has no CLI: it requires a signed-in owner in Studio.)`
1700
+ `unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", "odla-ai app export", "odla-ai app import <file>", "odla-ai app refresh-sandbox", "odla-ai app go-live", "odla-ai app promote", or "odla-ai app owners <list|add|remove>". (Permanent deletion has no CLI: it requires a signed-in owner in Studio.)`
1435
1701
  );
1436
1702
  }
1437
1703
  assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
@@ -1473,7 +1739,7 @@ var CAPABILITIES = {
1473
1739
  human: [
1474
1740
  "provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
1475
1741
  "review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
1476
- "consent to production changes with --yes",
1742
+ "consent to changes against an app's live database with --yes",
1477
1743
  "request destructive credential rotation explicitly",
1478
1744
  "review application semantics, security findings, releases, and merges",
1479
1745
  "approve redacted-source disclosure before hosted security reasoning",
@@ -1938,7 +2204,7 @@ function printStatus(status, json, out) {
1938
2204
  }
1939
2205
 
1940
2206
  // src/code-connect.ts
1941
- var import_node_fs7 = require("fs");
2207
+ var import_node_fs8 = require("fs");
1942
2208
  var import_node_os2 = require("os");
1943
2209
  var import_node_path5 = require("path");
1944
2210
 
@@ -4825,9 +5091,9 @@ var CodePiRuntimeEngine = class {
4825
5091
  };
4826
5092
 
4827
5093
  // src/help.ts
4828
- var import_node_fs6 = require("fs");
5094
+ var import_node_fs7 = require("fs");
4829
5095
  function cliVersion() {
4830
- const pkg = JSON.parse((0, import_node_fs6.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
5096
+ const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
4831
5097
  return pkg.version ?? "unknown";
4832
5098
  }
4833
5099
  function printHelp() {
@@ -4844,6 +5110,10 @@ Usage:
4844
5110
  odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
4845
5111
  odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
4846
5112
  odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
5113
+ odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
5114
+ odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
5115
+ odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
5116
+ odla-ai app promote [--dry-run] [--json] --yes
4847
5117
  odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
4848
5118
  odla-ai app owners add <email> [--email <odla-account>] [--json]
4849
5119
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
@@ -4880,17 +5150,29 @@ Commands:
4880
5150
  init Create a generic odla.config.mjs plus starter schema/rules files.
4881
5151
  doctor Validate and summarize the project config without network calls.
4882
5152
  calendar Inspect, connect, or disconnect the live Google booking connection.
4883
- app Archive (suspend, data retained), restore, export, or manage the
4884
- co-owners of the app. Archiving takes every environment's data
4885
- plane down until restored; permanent deletion has NO CLI \u2014 it
4886
- requires a signed-in owner in Studio, and no machine or agent
4887
- credential can purge. "app export" downloads a portable
4888
- gzipped-JSONL snapshot of one environment's database (--fresh
4889
- takes a new snapshot first) \u2014 your data is always yours to take.
5153
+ app Archive (suspend, data retained), restore, export, import, or
5154
+ manage the co-owners of the app. Archiving takes every
5155
+ environment's data plane down until restored; permanent deletion
5156
+ has NO CLI \u2014 it requires a signed-in owner in Studio, and no
5157
+ machine or agent credential can purge. "app export" downloads a
5158
+ portable gzipped-JSONL snapshot of one database (--fresh takes a
5159
+ new snapshot first) \u2014 your data is always yours to take.
5160
+ "app import" upserts rows back in from JSON, JSONL, or a
5161
+ {namespace: rows} map (the shape a query returns); it writes only
5162
+ with --yes, so the bare command is a dry run, and it refuses to
5163
+ guess an id mode that would duplicate rows on a re-run.
5164
+ "app refresh-sandbox" replaces the sandbox with a copy of live
5165
+ (the routine dev loop); "app go-live" copies the sandbox into an
5166
+ EMPTY live database, once, at launch; "app promote" pushes only
5167
+ schema, rules and gates up afterwards, leaving live's rows alone.
5168
+ Each prints its plan and writes nothing without --yes, so the
5169
+ bare command is the dry run. Clerk config, triggers, allowlists,
5170
+ secrets and API keys never travel; identity rows stay behind
5171
+ unless --include-identity.
4890
5172
  "app owners add <email>" grants a signed-up odla member the SAME
4891
5173
  full access as you; they then run their own "provision" to mint
4892
- their own credentials for the shared db (prod included) \u2014 no
4893
- secret is ever copied between people.
5174
+ their own credentials for the shared db (the live one included)
5175
+ \u2014 no secret is ever copied between people.
4894
5176
  capabilities Show what the CLI automates vs agent edits and human checkpoints.
4895
5177
  code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
4896
5178
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
@@ -4905,8 +5187,11 @@ Commands:
4905
5187
  version Print the CLI version.
4906
5188
 
4907
5189
  Safety:
4908
- New projects target dev only. Add prod explicitly to odla.config.mjs and pass
4909
- --yes to provision it; use --dry-run first to inspect the resolved plan.
5190
+ Every app has two databases on odla.ai: a sandbox (env "dev", tenant
5191
+ <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
5192
+ production odla.ai \u2014 there is no separate odla to point at. New projects get
5193
+ the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
5194
+ provision the live database; use --dry-run first to inspect the resolved plan.
4910
5195
  Provision caches the approved developer token and service credentials under
4911
5196
  .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
4912
5197
  preflights Wrangler before any shown-once issuance or destructive rotation.
@@ -5321,7 +5606,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
5321
5606
  async function codeConnect(options) {
5322
5607
  const cwd = options.cwd ?? process.cwd();
5323
5608
  const configPath = (0, import_node_path5.resolve)(cwd, options.configPath);
5324
- const cfg = (0, import_node_fs7.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
5609
+ const cfg = (0, import_node_fs8.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
5325
5610
  const requestedAppId = options.appId?.trim();
5326
5611
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
5327
5612
  throw new Error("--app-id must be a valid odla app id");
@@ -5331,9 +5616,9 @@ async function codeConnect(options) {
5331
5616
  }
5332
5617
  const appId = requestedAppId ?? cfg?.app.id;
5333
5618
  const platform = (options.platform ?? cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai").replace(/\/+$/, "");
5334
- const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : "dev");
5619
+ const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : void 0);
5335
5620
  if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
5336
- throw new Error(cfg ? `Code env "${appEnv ?? ""}" must be dev or prod and declared in ${options.configPath}` : `Code env "${appEnv ?? ""}" must be dev or prod`);
5621
+ throw new Error(cfg ? `Code env "${appEnv ?? ""}" must be dev or prod and declared in ${options.configPath}` : `Code env "${appEnv ?? ""}" must be dev or prod; pass --env explicitly when there is no ${options.configPath}`);
5337
5622
  }
5338
5623
  if (process.platform !== "darwin" && process.platform !== "linux") {
5339
5624
  throw new Error("odla Code hosts require macOS or Linux");
@@ -5540,12 +5825,12 @@ async function codeCommand(parsed, dependencies) {
5540
5825
 
5541
5826
  // src/doctor-checks.ts
5542
5827
  var import_node_child_process6 = require("child_process");
5543
- var import_node_fs9 = require("fs");
5828
+ var import_node_fs10 = require("fs");
5544
5829
  var import_node_path7 = require("path");
5545
5830
 
5546
5831
  // src/wrangler.ts
5547
5832
  var import_node_child_process5 = require("child_process");
5548
- var import_node_fs8 = require("fs");
5833
+ var import_node_fs9 = require("fs");
5549
5834
  var import_node_path6 = require("path");
5550
5835
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
5551
5836
  const child = (0, import_node_child_process5.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
@@ -5561,14 +5846,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
5561
5846
  function findWranglerConfig(rootDir) {
5562
5847
  for (const name of WRANGLER_CONFIG_FILES) {
5563
5848
  const path = (0, import_node_path6.join)(rootDir, name);
5564
- if ((0, import_node_fs8.existsSync)(path)) return path;
5849
+ if ((0, import_node_fs9.existsSync)(path)) return path;
5565
5850
  }
5566
5851
  return null;
5567
5852
  }
5568
5853
  function readWranglerConfig(path) {
5569
5854
  if (path.endsWith(".toml")) return null;
5570
5855
  try {
5571
- return JSON.parse(stripJsonComments((0, import_node_fs8.readFileSync)(path, "utf8")));
5856
+ return JSON.parse(stripJsonComments((0, import_node_fs9.readFileSync)(path, "utf8")));
5572
5857
  } catch {
5573
5858
  return null;
5574
5859
  }
@@ -5669,7 +5954,7 @@ function wranglerWarnings(rootDir) {
5669
5954
  const dir = (0, import_node_path7.resolve)(rootDir, assets.directory);
5670
5955
  if (dir === (0, import_node_path7.resolve)(rootDir)) {
5671
5956
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
5672
- } else if ((0, import_node_fs9.existsSync)((0, import_node_path7.join)(dir, "node_modules"))) {
5957
+ } else if ((0, import_node_fs10.existsSync)((0, import_node_path7.join)(dir, "node_modules"))) {
5673
5958
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
5674
5959
  }
5675
5960
  }
@@ -5705,12 +5990,12 @@ function o11yProjectWarnings(rootDir) {
5705
5990
  return warnings;
5706
5991
  }
5707
5992
  const main = typeof config.main === "string" ? (0, import_node_path7.resolve)(rootDir, config.main) : null;
5708
- if (!main || !(0, import_node_fs9.existsSync)(main)) {
5993
+ if (!main || !(0, import_node_fs10.existsSync)(main)) {
5709
5994
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
5710
5995
  } else {
5711
5996
  let source = "";
5712
5997
  try {
5713
- source = (0, import_node_fs9.readFileSync)(main, "utf8");
5998
+ source = (0, import_node_fs10.readFileSync)(main, "utf8");
5714
5999
  } catch {
5715
6000
  }
5716
6001
  if (!/\bwithObservability\b/.test(source)) {
@@ -5734,7 +6019,7 @@ function calendarProjectWarnings(rootDir) {
5734
6019
  }
5735
6020
  function readPackageJson(rootDir) {
5736
6021
  try {
5737
- return JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path7.join)(rootDir, "package.json"), "utf8"));
6022
+ return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path7.join)(rootDir, "package.json"), "utf8"));
5738
6023
  } catch {
5739
6024
  return null;
5740
6025
  }
@@ -5935,13 +6220,13 @@ function harnessOption(value, flag) {
5935
6220
  }
5936
6221
 
5937
6222
  // src/init.ts
5938
- var import_node_fs10 = require("fs");
6223
+ var import_node_fs11 = require("fs");
5939
6224
  var import_node_path8 = require("path");
5940
6225
  function initProject(options) {
5941
6226
  const out = options.stdout ?? console;
5942
6227
  const rootDir = (0, import_node_path8.resolve)(options.rootDir ?? process.cwd());
5943
6228
  const configPath = (0, import_node_path8.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
5944
- if ((0, import_node_fs10.existsSync)(configPath) && !options.force) {
6229
+ if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
5945
6230
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
5946
6231
  }
5947
6232
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -5951,10 +6236,10 @@ function initProject(options) {
5951
6236
  const services = options.services?.length ? options.services : ["db", "ai"];
5952
6237
  if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
5953
6238
  const aiProvider = options.aiProvider ?? "anthropic";
5954
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(configPath), { recursive: true });
5955
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.resolve)(rootDir, "src/odla"), { recursive: true });
5956
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.resolve)(rootDir, ".odla"), { recursive: true });
5957
- (0, import_node_fs10.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
6239
+ (0, import_node_fs11.mkdirSync)((0, import_node_path8.dirname)(configPath), { recursive: true });
6240
+ (0, import_node_fs11.mkdirSync)((0, import_node_path8.resolve)(rootDir, "src/odla"), { recursive: true });
6241
+ (0, import_node_fs11.mkdirSync)((0, import_node_path8.resolve)(rootDir, ".odla"), { recursive: true });
6242
+ (0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
5958
6243
  writeIfMissing((0, import_node_path8.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
5959
6244
  writeIfMissing((0, import_node_path8.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
5960
6245
  ensureGitignore(rootDir);
@@ -5963,8 +6248,8 @@ function initProject(options) {
5963
6248
  out.log("updated .gitignore for local odla credentials");
5964
6249
  }
5965
6250
  function writeIfMissing(path, text) {
5966
- if ((0, import_node_fs10.existsSync)(path)) return;
5967
- (0, import_node_fs10.writeFileSync)(path, text);
6251
+ if ((0, import_node_fs11.existsSync)(path)) return;
6252
+ (0, import_node_fs11.writeFileSync)(path, text);
5968
6253
  }
5969
6254
  function configTemplate(input) {
5970
6255
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -6065,7 +6350,7 @@ function relativeDisplay(path, rootDir) {
6065
6350
  }
6066
6351
 
6067
6352
  // src/provision.ts
6068
- var import_apps3 = require("@odla-ai/apps");
6353
+ var import_apps4 = require("@odla-ai/apps");
6069
6354
  var import_ai2 = require("@odla-ai/ai");
6070
6355
  var import_node_process7 = __toESM(require("process"), 1);
6071
6356
 
@@ -6122,9 +6407,9 @@ async function responseText(res) {
6122
6407
  }
6123
6408
 
6124
6409
  // src/provision-credentials.ts
6125
- var import_apps = require("@odla-ai/apps");
6410
+ var import_apps2 = require("@odla-ai/apps");
6126
6411
  async function provisionEnvCredentials(opts) {
6127
- const tenantId = (0, import_apps.tenantIdFor)(opts.cfg.app.id, opts.env);
6412
+ const tenantId = (0, import_apps2.tenantIdFor)(opts.cfg.app.id, opts.env);
6128
6413
  const prior = opts.credentials?.envs[opts.env];
6129
6414
  let credentials = opts.credentials;
6130
6415
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -6218,7 +6503,7 @@ async function safeText3(res) {
6218
6503
 
6219
6504
  // src/provision-helpers.ts
6220
6505
  var import_ai = require("@odla-ai/ai");
6221
- var import_apps2 = require("@odla-ai/apps");
6506
+ var import_apps3 = require("@odla-ai/apps");
6222
6507
  function serviceRank(service) {
6223
6508
  if (service === "db") return 0;
6224
6509
  if (service === "calendar") return 2;
@@ -6229,7 +6514,7 @@ function defaultSecretName(provider) {
6229
6514
  return names[provider] ?? `${provider}_api_key`;
6230
6515
  }
6231
6516
  async function assertTenantAdminAccess(doFetch, cfg, env, token) {
6232
- const tenantId = (0, import_apps2.tenantIdFor)(cfg.app.id, env);
6517
+ const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
6233
6518
  const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
6234
6519
  headers: { authorization: `Bearer ${token}` }
6235
6520
  });
@@ -6372,7 +6657,7 @@ async function provision(options) {
6372
6657
  const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
6373
6658
  if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
6374
6659
  throw new Error(
6375
- `refusing to provision production env ${productionEnvs.join(", ")} without --yes; run --dry-run first`
6660
+ `refusing to provision the live database (env ${productionEnvs.join(", ")}) without --yes; run --dry-run first`
6376
6661
  );
6377
6662
  }
6378
6663
  const database = await resolveDatabaseConfig(cfg);
@@ -6420,7 +6705,7 @@ async function provision(options) {
6420
6705
  }
6421
6706
  const doFetch = options.fetch ?? fetch;
6422
6707
  const token = await getDeveloperToken(cfg, options, doFetch, out);
6423
- const apps = (0, import_apps3.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
6708
+ const apps = (0, import_apps4.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
6424
6709
  const existing = await readRegistryApp(cfg, token, doFetch);
6425
6710
  if (existing) {
6426
6711
  out.log(`app: ${cfg.app.id} already exists`);
@@ -6464,7 +6749,7 @@ async function provision(options) {
6464
6749
  }
6465
6750
  }
6466
6751
  for (const env of cfg.envs) {
6467
- const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
6752
+ const tenantId = (0, import_apps4.tenantIdFor)(cfg.app.id, env);
6468
6753
  credentials = await provisionEnvCredentials({
6469
6754
  cfg,
6470
6755
  env,
@@ -6550,7 +6835,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
6550
6835
 
6551
6836
  // src/secrets-set.ts
6552
6837
  var import_ai3 = require("@odla-ai/ai");
6553
- var import_apps4 = require("@odla-ai/apps");
6838
+ var import_apps5 = require("@odla-ai/apps");
6554
6839
  var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
6555
6840
  async function secretsSet(options) {
6556
6841
  const name = (options.name ?? "").trim();
@@ -6602,7 +6887,7 @@ async function resolveVaultWrite(options) {
6602
6887
  throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
6603
6888
  }
6604
6889
  const value = await secretInputValue(options, "secret");
6605
- return { cfg, tenantId: (0, import_apps4.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
6890
+ return { cfg, tenantId: (0, import_apps5.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
6606
6891
  }
6607
6892
 
6608
6893
  // src/security-command-context.ts
@@ -7294,7 +7579,7 @@ async function securityStatus(parsed, dependencies) {
7294
7579
  }
7295
7580
 
7296
7581
  // src/skill.ts
7297
- var import_node_fs11 = require("fs");
7582
+ var import_node_fs12 = require("fs");
7298
7583
  var import_node_os3 = require("os");
7299
7584
  var import_node_path10 = require("path");
7300
7585
  var import_node_url3 = require("url");
@@ -7372,7 +7657,7 @@ function installSkill(options = {}) {
7372
7657
  plans.set(target, { target, content, boundary, managedMerge });
7373
7658
  };
7374
7659
  const planSkillTree = (targetDir2, boundary = root) => {
7375
- for (const rel of files) plan((0, import_node_path10.join)(targetDir2, rel), (0, import_node_fs11.readFileSync)((0, import_node_path10.join)(sourceDir, rel), "utf8"), false, boundary);
7660
+ for (const rel of files) plan((0, import_node_path10.join)(targetDir2, rel), (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, rel), "utf8"), false, boundary);
7376
7661
  };
7377
7662
  let targetDir;
7378
7663
  if (options.global) {
@@ -7392,7 +7677,7 @@ function installSkill(options = {}) {
7392
7677
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
7393
7678
  if (harnesses.includes("claude")) {
7394
7679
  for (const skill of skillNames(files)) {
7395
- const canonical = (0, import_node_fs11.readFileSync)((0, import_node_path10.join)(sourceDir, skill, "SKILL.md"), "utf8");
7680
+ const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, skill, "SKILL.md"), "utf8");
7396
7681
  plan((0, import_node_path10.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
7397
7682
  }
7398
7683
  rememberTarget("claude", claudeRoot);
@@ -7427,11 +7712,11 @@ function installSkill(options = {}) {
7427
7712
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
7428
7713
  continue;
7429
7714
  }
7430
- if (!(0, import_node_fs11.existsSync)(file.target)) {
7715
+ if (!(0, import_node_fs12.existsSync)(file.target)) {
7431
7716
  writtenPaths.add(file.target);
7432
7717
  continue;
7433
7718
  }
7434
- const current = (0, import_node_fs11.readFileSync)(file.target, "utf8");
7719
+ const current = (0, import_node_fs12.readFileSync)(file.target, "utf8");
7435
7720
  if (current === file.content) {
7436
7721
  unchangedPaths.add(file.target);
7437
7722
  } else if (file.managedMerge || options.force) {
@@ -7448,9 +7733,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
7448
7733
  );
7449
7734
  }
7450
7735
  for (const file of plans.values()) {
7451
- if (!(0, import_node_fs11.existsSync)(file.target) || (0, import_node_fs11.readFileSync)(file.target, "utf8") !== file.content) {
7452
- (0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(file.target), { recursive: true });
7453
- (0, import_node_fs11.writeFileSync)(file.target, file.content);
7736
+ if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
7737
+ (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(file.target), { recursive: true });
7738
+ (0, import_node_fs12.writeFileSync)(file.target, file.content);
7454
7739
  }
7455
7740
  }
7456
7741
  const skills = skillNames(files);
@@ -7491,9 +7776,9 @@ function normalizeHarnesses(values, global) {
7491
7776
  function managedFileContent(path, block, force, boundary) {
7492
7777
  const symlink = symlinkedComponent(boundary, path);
7493
7778
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
7494
- if (!(0, import_node_fs11.existsSync)(path)) return `${block}
7779
+ if (!(0, import_node_fs12.existsSync)(path)) return `${block}
7495
7780
  `;
7496
- const current = (0, import_node_fs11.readFileSync)(path, "utf8");
7781
+ const current = (0, import_node_fs12.readFileSync)(path, "utf8");
7497
7782
  const start = "<!-- odla-ai agent setup:start -->";
7498
7783
  const end = "<!-- odla-ai agent setup:end -->";
7499
7784
  const startAt = current.indexOf(start);
@@ -7522,7 +7807,7 @@ function symlinkedComponent(boundary, target) {
7522
7807
  for (const part of rel.split(import_node_path10.sep).filter(Boolean)) {
7523
7808
  current = (0, import_node_path10.join)(current, part);
7524
7809
  try {
7525
- if ((0, import_node_fs11.lstatSync)(current).isSymbolicLink()) return current;
7810
+ if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
7526
7811
  } catch (error) {
7527
7812
  if (error.code !== "ENOENT") throw error;
7528
7813
  }
@@ -7533,10 +7818,10 @@ function skillNames(files) {
7533
7818
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
7534
7819
  }
7535
7820
  function listFiles(dir) {
7536
- if (!(0, import_node_fs11.existsSync)(dir)) return [];
7821
+ if (!(0, import_node_fs12.existsSync)(dir)) return [];
7537
7822
  const results = [];
7538
7823
  const walk = (current) => {
7539
- for (const entry of (0, import_node_fs11.readdirSync)(current, { withFileTypes: true })) {
7824
+ for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
7540
7825
  const path = (0, import_node_path10.join)(current, entry.name);
7541
7826
  if (entry.isDirectory()) walk(path);
7542
7827
  else results.push((0, import_node_path10.relative)(dir, path));