@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/index.cjs CHANGED
@@ -1293,14 +1293,36 @@ function unique2(values) {
1293
1293
  return [...new Set(values.filter(Boolean))];
1294
1294
  }
1295
1295
 
1296
+ // src/tenant.ts
1297
+ var import_apps = require("@odla-ai/apps");
1298
+ function resolveEnv(cfg, requested) {
1299
+ const env = requested ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
1300
+ if (!env || !cfg.envs.includes(env)) {
1301
+ throw new Error(`env "${env ?? ""}" is not declared in ${cfg.configPath}`);
1302
+ }
1303
+ return env;
1304
+ }
1305
+ function resolveTenant(cfg, requested) {
1306
+ const env = resolveEnv(cfg, requested);
1307
+ return { env, tenant: (0, import_apps.tenantIdFor)(cfg.app.id, env) };
1308
+ }
1309
+ function bothTenants(cfg) {
1310
+ for (const env of ["dev", "prod"]) {
1311
+ if (!cfg.envs.includes(env)) {
1312
+ throw new Error(
1313
+ `env "${env}" is not declared in ${cfg.configPath} \u2014 add it to \`envs\` and run \`odla-ai provision --yes\` before transferring data between environments`
1314
+ );
1315
+ }
1316
+ }
1317
+ return { sandbox: (0, import_apps.tenantIdFor)(cfg.app.id, "dev"), live: (0, import_apps.tenantIdFor)(cfg.app.id, "prod") };
1318
+ }
1319
+
1296
1320
  // src/app-export.ts
1297
1321
  async function appExport(options) {
1298
1322
  const cfg = await loadProjectConfig(options.configPath);
1299
1323
  const out = options.stdout ?? console;
1300
1324
  const doFetch = options.fetch ?? fetch;
1301
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
1302
- if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
1303
- const tenant = env === "prod" ? cfg.app.id : `${cfg.app.id}--${env}`;
1325
+ const { tenant } = resolveTenant(cfg, options.env);
1304
1326
  const token = await getDeveloperToken(
1305
1327
  cfg,
1306
1328
  { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
@@ -1335,6 +1357,84 @@ async function appExport(options) {
1335
1357
  return { file, backup: newest };
1336
1358
  }
1337
1359
 
1360
+ // src/app-import.ts
1361
+ var import_node_fs6 = require("fs");
1362
+ var import_import = require("@odla-ai/db/import");
1363
+ function chooseIdMode(options, rows) {
1364
+ const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
1365
+ if (chosen.length > 1) throw new Error("choose one of --id-field, --key, or --generate-ids");
1366
+ if (options.key) return { idMode: "key" };
1367
+ if (options.generateIds) return { idMode: "generate" };
1368
+ if (options.idField) return { idMode: "field", idField: options.idField };
1369
+ if (rows.length > 0 && rows.every((row) => typeof row?.id === "string" && row.id)) return { idMode: "field" };
1370
+ throw new Error(
1371
+ "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)"
1372
+ );
1373
+ }
1374
+ async function appImport(options) {
1375
+ const cfg = await loadProjectConfig(options.configPath);
1376
+ const out = options.stdout ?? console;
1377
+ const doFetch = options.fetch ?? fetch;
1378
+ const { tenant } = resolveTenant(cfg, options.env);
1379
+ const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs6.readFileSync)(0, "utf8")))() : (0, import_node_fs6.readFileSync)(options.file, "utf8");
1380
+ const { format, sources } = (0, import_import.parseImport)(text, options.ns);
1381
+ if (format === "namespace-map" && options.ns) {
1382
+ throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
1383
+ }
1384
+ if (format !== "namespace-map" && !options.ns) throw new Error("--ns is required unless the file is a {namespace: rows} map");
1385
+ if (sources.length === 0) throw new Error(`${options.file} contains no rows`);
1386
+ const mode = chooseIdMode(options, sources.map((s) => s.row));
1387
+ const build = { ...mode, keyAttr: options.key };
1388
+ const { ops, rejected, ignored } = (0, import_import.buildImportOps)(sources, build);
1389
+ if (rejected.length > 0) {
1390
+ const detail = rejected.slice(0, 10).map((r) => ` row ${r.index}: ${r.reason}`).join("\n");
1391
+ const more = rejected.length > 10 ? `
1392
+ \u2026and ${rejected.length - 10} more` : "";
1393
+ throw new Error(`${rejected.length} row(s) cannot be imported \u2014 fix the input and re-run:
1394
+ ${detail}${more}`);
1395
+ }
1396
+ const chunks = (0, import_import.planImportChunks)(ops);
1397
+ const result = { tenant, rows: ops.length, chunks: chunks.length, committed: 0, duplicate: 0, txIds: [], ignored };
1398
+ if (ignored.length > 0) out.log(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
1399
+ if (options.dryRun || options.yes !== true) {
1400
+ const why = options.dryRun ? "--dry-run" : "no --yes";
1401
+ out.log(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
1402
+ if (options.json) out.log(JSON.stringify(result, null, 2));
1403
+ return result;
1404
+ }
1405
+ const token = await getDeveloperToken(
1406
+ cfg,
1407
+ { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1408
+ doFetch,
1409
+ out
1410
+ );
1411
+ const runId = crypto.randomUUID();
1412
+ const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
1413
+ for (const chunk of chunks) {
1414
+ const res = await doFetch(url, {
1415
+ method: "POST",
1416
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1417
+ body: JSON.stringify({ mutationId: (0, import_import.importMutationId)(runId, chunk.index), ops: chunk.ops })
1418
+ });
1419
+ const body = await res.json().catch(() => ({}));
1420
+ if (!res.ok) {
1421
+ const code = body.error?.code ? ` (${body.error.code})` : "";
1422
+ throw new Error(
1423
+ `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.`
1424
+ );
1425
+ }
1426
+ result.committed += chunk.ops.length;
1427
+ if (typeof body.txId === "number") result.txIds.push(body.txId);
1428
+ if (body.duplicate) result.duplicate++;
1429
+ }
1430
+ if (options.json) out.log(JSON.stringify(result, null, 2));
1431
+ else {
1432
+ const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
1433
+ out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
1434
+ }
1435
+ return result;
1436
+ }
1437
+
1338
1438
  // src/app-owners.ts
1339
1439
  var sink = (options) => options.stdout ?? console;
1340
1440
  async function ownersRequest(method, suffix, options, body) {
@@ -1422,6 +1522,133 @@ async function appOwnersCommand(parsed, dependencies = {}) {
1422
1522
  );
1423
1523
  }
1424
1524
 
1525
+ // src/app-transfer.ts
1526
+ function endpointsFor(verb, tenants) {
1527
+ const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
1528
+ const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
1529
+ if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
1530
+ return { ...up, mode: "cutover" };
1531
+ }
1532
+ async function api(cfg, token, path, init, doFetch) {
1533
+ const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
1534
+ ...init,
1535
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
1536
+ });
1537
+ return { status: res.status, body: await res.json().catch(() => ({})) };
1538
+ }
1539
+ function describe(side) {
1540
+ if (!side.exists) return "not provisioned";
1541
+ if (side.maxTx === 0) return "empty \u2014 never written";
1542
+ const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
1543
+ const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
1544
+ return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
1545
+ }
1546
+ function printPlan(out, verb, pre, opts) {
1547
+ const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
1548
+ out.log(`${verb} (${arrow})`);
1549
+ out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
1550
+ out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
1551
+ if (verb === "promote") {
1552
+ out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
1553
+ } else {
1554
+ out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
1555
+ out.log(` stays put: ${pre.staysPut.join(", ")}`);
1556
+ const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
1557
+ out.log(` identity: ${identity}`);
1558
+ out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
1559
+ }
1560
+ for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
1561
+ }
1562
+ async function appTransfer(options) {
1563
+ const cfg = await loadProjectConfig(options.configPath);
1564
+ const out = options.stdout ?? console;
1565
+ const doFetch = options.fetch ?? fetch;
1566
+ const tenants = bothTenants(cfg);
1567
+ const route2 = endpointsFor(options.verb, tenants);
1568
+ const token = await getDeveloperToken(
1569
+ cfg,
1570
+ { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1571
+ doFetch,
1572
+ out
1573
+ );
1574
+ const pre = await api(
1575
+ cfg,
1576
+ token,
1577
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
1578
+ void 0,
1579
+ doFetch
1580
+ );
1581
+ if (pre.status !== 200) {
1582
+ throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
1583
+ }
1584
+ const plan = pre.body;
1585
+ printPlan(out, options.verb, plan, options);
1586
+ if (options.verb === "go-live" && !plan.targetEmpty) {
1587
+ throw new Error(
1588
+ `${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.`
1589
+ );
1590
+ }
1591
+ if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
1592
+ if (options.dryRun || options.yes !== true) {
1593
+ out.log(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
1594
+ if (options.json) out.log(JSON.stringify(plan, null, 2));
1595
+ return { ok: true, plan };
1596
+ }
1597
+ if (options.verb === "promote") {
1598
+ const res2 = await api(
1599
+ cfg,
1600
+ token,
1601
+ `/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
1602
+ { method: "POST", body: JSON.stringify({ from: "dev" }) },
1603
+ doFetch
1604
+ );
1605
+ if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
1606
+ if (options.json) out.log(JSON.stringify(res2.body, null, 2));
1607
+ else out.log(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
1608
+ return { ok: true, plan, result: res2.body };
1609
+ }
1610
+ const res = await api(
1611
+ cfg,
1612
+ token,
1613
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
1614
+ {
1615
+ method: "POST",
1616
+ body: JSON.stringify({
1617
+ from: route2.from,
1618
+ mode: route2.mode,
1619
+ ...options.includeIdentity ? { includeUsers: true } : {},
1620
+ ...options.includeFiles ? { includeFiles: true } : {}
1621
+ })
1622
+ },
1623
+ doFetch
1624
+ );
1625
+ if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
1626
+ out.log(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
1627
+ if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
1628
+ if (options.json) out.log(JSON.stringify(res.body, null, 2));
1629
+ return { ok: true, plan, result: res.body };
1630
+ }
1631
+ async function copyFiles(cfg, token, route2, doFetch, out) {
1632
+ for (let attempt = 1; attempt <= 20; attempt++) {
1633
+ const res = await api(
1634
+ cfg,
1635
+ token,
1636
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
1637
+ { method: "POST", body: JSON.stringify({ from: route2.from }) },
1638
+ doFetch
1639
+ );
1640
+ if (res.status === 200) {
1641
+ out.log(`${route2.target}: files copied`);
1642
+ return;
1643
+ }
1644
+ if (!res.body.error?.retry) {
1645
+ throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
1646
+ }
1647
+ out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
1648
+ }
1649
+ throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
1650
+ }
1651
+
1425
1652
  // src/app-lifecycle.ts
1426
1653
  async function lifecycleCall(action, options) {
1427
1654
  const cfg = await loadProjectConfig(options.configPath);
@@ -1483,9 +1710,48 @@ async function appCommand(parsed, dependencies = {}) {
1483
1710
  });
1484
1711
  return;
1485
1712
  }
1713
+ if (sub === "refresh-sandbox" || sub === "go-live" || sub === "promote") {
1714
+ assertArgs(parsed, ["config", "include-identity", "include-files", "dry-run", "yes", "token", "email", "json"], 2);
1715
+ await appTransfer({
1716
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
1717
+ verb: sub,
1718
+ includeIdentity: parsed.options["include-identity"] === true,
1719
+ includeFiles: parsed.options["include-files"] === true,
1720
+ dryRun: parsed.options["dry-run"] === true,
1721
+ yes: parsed.options.yes === true,
1722
+ token: stringOpt(parsed.options.token),
1723
+ email: stringOpt(parsed.options.email),
1724
+ json: parsed.options.json === true,
1725
+ fetch: dependencies.fetch,
1726
+ stdout: dependencies.stdout
1727
+ });
1728
+ return;
1729
+ }
1730
+ if (sub === "import") {
1731
+ assertArgs(parsed, ["config", "env", "ns", "id-field", "key", "generate-ids", "dry-run", "yes", "token", "email", "json"], 3);
1732
+ const file = parsed.positionals[2];
1733
+ if (!file) throw new Error('app import needs a file: "odla-ai app import rows.json --ns todos --yes" (or "-" for stdin)');
1734
+ await appImport({
1735
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
1736
+ file,
1737
+ env: stringOpt(parsed.options.env),
1738
+ ns: stringOpt(parsed.options.ns),
1739
+ idField: stringOpt(parsed.options["id-field"]),
1740
+ key: stringOpt(parsed.options.key),
1741
+ generateIds: parsed.options["generate-ids"] === true,
1742
+ dryRun: parsed.options["dry-run"] === true,
1743
+ yes: parsed.options.yes === true,
1744
+ token: stringOpt(parsed.options.token),
1745
+ email: stringOpt(parsed.options.email),
1746
+ json: parsed.options.json === true,
1747
+ fetch: dependencies.fetch,
1748
+ stdout: dependencies.stdout
1749
+ });
1750
+ return;
1751
+ }
1486
1752
  if (sub !== "archive" && sub !== "restore") {
1487
1753
  throw new Error(
1488
- `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.)`
1754
+ `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.)`
1489
1755
  );
1490
1756
  }
1491
1757
  assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
@@ -1527,7 +1793,7 @@ var CAPABILITIES = {
1527
1793
  human: [
1528
1794
  "provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
1529
1795
  "review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
1530
- "consent to production changes with --yes",
1796
+ "consent to changes against an app's live database with --yes",
1531
1797
  "request destructive credential rotation explicitly",
1532
1798
  "review application semantics, security findings, releases, and merges",
1533
1799
  "approve redacted-source disclosure before hosted security reasoning",
@@ -1992,7 +2258,7 @@ function printStatus(status, json, out) {
1992
2258
  }
1993
2259
 
1994
2260
  // src/code-connect.ts
1995
- var import_node_fs7 = require("fs");
2261
+ var import_node_fs8 = require("fs");
1996
2262
  var import_node_os2 = require("os");
1997
2263
  var import_node_path5 = require("path");
1998
2264
 
@@ -4879,9 +5145,9 @@ var CodePiRuntimeEngine = class {
4879
5145
  };
4880
5146
 
4881
5147
  // src/help.ts
4882
- var import_node_fs6 = require("fs");
5148
+ var import_node_fs7 = require("fs");
4883
5149
  function cliVersion() {
4884
- const pkg = JSON.parse((0, import_node_fs6.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
5150
+ const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
4885
5151
  return pkg.version ?? "unknown";
4886
5152
  }
4887
5153
  function printHelp() {
@@ -4898,6 +5164,10 @@ Usage:
4898
5164
  odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
4899
5165
  odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
4900
5166
  odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
5167
+ odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
5168
+ odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
5169
+ odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
5170
+ odla-ai app promote [--dry-run] [--json] --yes
4901
5171
  odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
4902
5172
  odla-ai app owners add <email> [--email <odla-account>] [--json]
4903
5173
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
@@ -4934,17 +5204,29 @@ Commands:
4934
5204
  init Create a generic odla.config.mjs plus starter schema/rules files.
4935
5205
  doctor Validate and summarize the project config without network calls.
4936
5206
  calendar Inspect, connect, or disconnect the live Google booking connection.
4937
- app Archive (suspend, data retained), restore, export, or manage the
4938
- co-owners of the app. Archiving takes every environment's data
4939
- plane down until restored; permanent deletion has NO CLI \u2014 it
4940
- requires a signed-in owner in Studio, and no machine or agent
4941
- credential can purge. "app export" downloads a portable
4942
- gzipped-JSONL snapshot of one environment's database (--fresh
4943
- takes a new snapshot first) \u2014 your data is always yours to take.
5207
+ app Archive (suspend, data retained), restore, export, import, or
5208
+ manage the co-owners of the app. Archiving takes every
5209
+ environment's data plane down until restored; permanent deletion
5210
+ has NO CLI \u2014 it requires a signed-in owner in Studio, and no
5211
+ machine or agent credential can purge. "app export" downloads a
5212
+ portable gzipped-JSONL snapshot of one database (--fresh takes a
5213
+ new snapshot first) \u2014 your data is always yours to take.
5214
+ "app import" upserts rows back in from JSON, JSONL, or a
5215
+ {namespace: rows} map (the shape a query returns); it writes only
5216
+ with --yes, so the bare command is a dry run, and it refuses to
5217
+ guess an id mode that would duplicate rows on a re-run.
5218
+ "app refresh-sandbox" replaces the sandbox with a copy of live
5219
+ (the routine dev loop); "app go-live" copies the sandbox into an
5220
+ EMPTY live database, once, at launch; "app promote" pushes only
5221
+ schema, rules and gates up afterwards, leaving live's rows alone.
5222
+ Each prints its plan and writes nothing without --yes, so the
5223
+ bare command is the dry run. Clerk config, triggers, allowlists,
5224
+ secrets and API keys never travel; identity rows stay behind
5225
+ unless --include-identity.
4944
5226
  "app owners add <email>" grants a signed-up odla member the SAME
4945
5227
  full access as you; they then run their own "provision" to mint
4946
- their own credentials for the shared db (prod included) \u2014 no
4947
- secret is ever copied between people.
5228
+ their own credentials for the shared db (the live one included)
5229
+ \u2014 no secret is ever copied between people.
4948
5230
  capabilities Show what the CLI automates vs agent edits and human checkpoints.
4949
5231
  code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
4950
5232
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
@@ -4959,8 +5241,11 @@ Commands:
4959
5241
  version Print the CLI version.
4960
5242
 
4961
5243
  Safety:
4962
- New projects target dev only. Add prod explicitly to odla.config.mjs and pass
4963
- --yes to provision it; use --dry-run first to inspect the resolved plan.
5244
+ Every app has two databases on odla.ai: a sandbox (env "dev", tenant
5245
+ <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
5246
+ production odla.ai \u2014 there is no separate odla to point at. New projects get
5247
+ the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
5248
+ provision the live database; use --dry-run first to inspect the resolved plan.
4964
5249
  Provision caches the approved developer token and service credentials under
4965
5250
  .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
4966
5251
  preflights Wrangler before any shown-once issuance or destructive rotation.
@@ -5375,7 +5660,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
5375
5660
  async function codeConnect(options) {
5376
5661
  const cwd = options.cwd ?? process.cwd();
5377
5662
  const configPath = (0, import_node_path5.resolve)(cwd, options.configPath);
5378
- const cfg = (0, import_node_fs7.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
5663
+ const cfg = (0, import_node_fs8.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
5379
5664
  const requestedAppId = options.appId?.trim();
5380
5665
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
5381
5666
  throw new Error("--app-id must be a valid odla app id");
@@ -5385,9 +5670,9 @@ async function codeConnect(options) {
5385
5670
  }
5386
5671
  const appId = requestedAppId ?? cfg?.app.id;
5387
5672
  const platform = (options.platform ?? cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai").replace(/\/+$/, "");
5388
- const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : "dev");
5673
+ const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : void 0);
5389
5674
  if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
5390
- 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`);
5675
+ 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}`);
5391
5676
  }
5392
5677
  if (process.platform !== "darwin" && process.platform !== "linux") {
5393
5678
  throw new Error("odla Code hosts require macOS or Linux");
@@ -5594,12 +5879,12 @@ async function codeCommand(parsed, dependencies) {
5594
5879
 
5595
5880
  // src/doctor-checks.ts
5596
5881
  var import_node_child_process6 = require("child_process");
5597
- var import_node_fs9 = require("fs");
5882
+ var import_node_fs10 = require("fs");
5598
5883
  var import_node_path7 = require("path");
5599
5884
 
5600
5885
  // src/wrangler.ts
5601
5886
  var import_node_child_process5 = require("child_process");
5602
- var import_node_fs8 = require("fs");
5887
+ var import_node_fs9 = require("fs");
5603
5888
  var import_node_path6 = require("path");
5604
5889
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
5605
5890
  const child = (0, import_node_child_process5.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
@@ -5615,14 +5900,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
5615
5900
  function findWranglerConfig(rootDir) {
5616
5901
  for (const name of WRANGLER_CONFIG_FILES) {
5617
5902
  const path = (0, import_node_path6.join)(rootDir, name);
5618
- if ((0, import_node_fs8.existsSync)(path)) return path;
5903
+ if ((0, import_node_fs9.existsSync)(path)) return path;
5619
5904
  }
5620
5905
  return null;
5621
5906
  }
5622
5907
  function readWranglerConfig(path) {
5623
5908
  if (path.endsWith(".toml")) return null;
5624
5909
  try {
5625
- return JSON.parse(stripJsonComments((0, import_node_fs8.readFileSync)(path, "utf8")));
5910
+ return JSON.parse(stripJsonComments((0, import_node_fs9.readFileSync)(path, "utf8")));
5626
5911
  } catch {
5627
5912
  return null;
5628
5913
  }
@@ -5723,7 +6008,7 @@ function wranglerWarnings(rootDir) {
5723
6008
  const dir = (0, import_node_path7.resolve)(rootDir, assets.directory);
5724
6009
  if (dir === (0, import_node_path7.resolve)(rootDir)) {
5725
6010
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
5726
- } else if ((0, import_node_fs9.existsSync)((0, import_node_path7.join)(dir, "node_modules"))) {
6011
+ } else if ((0, import_node_fs10.existsSync)((0, import_node_path7.join)(dir, "node_modules"))) {
5727
6012
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
5728
6013
  }
5729
6014
  }
@@ -5759,12 +6044,12 @@ function o11yProjectWarnings(rootDir) {
5759
6044
  return warnings;
5760
6045
  }
5761
6046
  const main = typeof config.main === "string" ? (0, import_node_path7.resolve)(rootDir, config.main) : null;
5762
- if (!main || !(0, import_node_fs9.existsSync)(main)) {
6047
+ if (!main || !(0, import_node_fs10.existsSync)(main)) {
5763
6048
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
5764
6049
  } else {
5765
6050
  let source = "";
5766
6051
  try {
5767
- source = (0, import_node_fs9.readFileSync)(main, "utf8");
6052
+ source = (0, import_node_fs10.readFileSync)(main, "utf8");
5768
6053
  } catch {
5769
6054
  }
5770
6055
  if (!/\bwithObservability\b/.test(source)) {
@@ -5788,7 +6073,7 @@ function calendarProjectWarnings(rootDir) {
5788
6073
  }
5789
6074
  function readPackageJson(rootDir) {
5790
6075
  try {
5791
- return JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path7.join)(rootDir, "package.json"), "utf8"));
6076
+ return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path7.join)(rootDir, "package.json"), "utf8"));
5792
6077
  } catch {
5793
6078
  return null;
5794
6079
  }
@@ -5989,13 +6274,13 @@ function harnessOption(value, flag) {
5989
6274
  }
5990
6275
 
5991
6276
  // src/init.ts
5992
- var import_node_fs10 = require("fs");
6277
+ var import_node_fs11 = require("fs");
5993
6278
  var import_node_path8 = require("path");
5994
6279
  function initProject(options) {
5995
6280
  const out = options.stdout ?? console;
5996
6281
  const rootDir = (0, import_node_path8.resolve)(options.rootDir ?? process.cwd());
5997
6282
  const configPath = (0, import_node_path8.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
5998
- if ((0, import_node_fs10.existsSync)(configPath) && !options.force) {
6283
+ if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
5999
6284
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
6000
6285
  }
6001
6286
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -6005,10 +6290,10 @@ function initProject(options) {
6005
6290
  const services = options.services?.length ? options.services : ["db", "ai"];
6006
6291
  if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
6007
6292
  const aiProvider = options.aiProvider ?? "anthropic";
6008
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(configPath), { recursive: true });
6009
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.resolve)(rootDir, "src/odla"), { recursive: true });
6010
- (0, import_node_fs10.mkdirSync)((0, import_node_path8.resolve)(rootDir, ".odla"), { recursive: true });
6011
- (0, import_node_fs10.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
6293
+ (0, import_node_fs11.mkdirSync)((0, import_node_path8.dirname)(configPath), { recursive: true });
6294
+ (0, import_node_fs11.mkdirSync)((0, import_node_path8.resolve)(rootDir, "src/odla"), { recursive: true });
6295
+ (0, import_node_fs11.mkdirSync)((0, import_node_path8.resolve)(rootDir, ".odla"), { recursive: true });
6296
+ (0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
6012
6297
  writeIfMissing((0, import_node_path8.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
6013
6298
  writeIfMissing((0, import_node_path8.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
6014
6299
  ensureGitignore(rootDir);
@@ -6017,8 +6302,8 @@ function initProject(options) {
6017
6302
  out.log("updated .gitignore for local odla credentials");
6018
6303
  }
6019
6304
  function writeIfMissing(path, text) {
6020
- if ((0, import_node_fs10.existsSync)(path)) return;
6021
- (0, import_node_fs10.writeFileSync)(path, text);
6305
+ if ((0, import_node_fs11.existsSync)(path)) return;
6306
+ (0, import_node_fs11.writeFileSync)(path, text);
6022
6307
  }
6023
6308
  function configTemplate(input) {
6024
6309
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -6119,7 +6404,7 @@ function relativeDisplay(path, rootDir) {
6119
6404
  }
6120
6405
 
6121
6406
  // src/provision.ts
6122
- var import_apps3 = require("@odla-ai/apps");
6407
+ var import_apps4 = require("@odla-ai/apps");
6123
6408
  var import_ai2 = require("@odla-ai/ai");
6124
6409
  var import_node_process7 = __toESM(require("process"), 1);
6125
6410
 
@@ -6176,9 +6461,9 @@ async function responseText(res) {
6176
6461
  }
6177
6462
 
6178
6463
  // src/provision-credentials.ts
6179
- var import_apps = require("@odla-ai/apps");
6464
+ var import_apps2 = require("@odla-ai/apps");
6180
6465
  async function provisionEnvCredentials(opts) {
6181
- const tenantId = (0, import_apps.tenantIdFor)(opts.cfg.app.id, opts.env);
6466
+ const tenantId = (0, import_apps2.tenantIdFor)(opts.cfg.app.id, opts.env);
6182
6467
  const prior = opts.credentials?.envs[opts.env];
6183
6468
  let credentials = opts.credentials;
6184
6469
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -6272,7 +6557,7 @@ async function safeText3(res) {
6272
6557
 
6273
6558
  // src/provision-helpers.ts
6274
6559
  var import_ai = require("@odla-ai/ai");
6275
- var import_apps2 = require("@odla-ai/apps");
6560
+ var import_apps3 = require("@odla-ai/apps");
6276
6561
  function serviceRank(service) {
6277
6562
  if (service === "db") return 0;
6278
6563
  if (service === "calendar") return 2;
@@ -6283,7 +6568,7 @@ function defaultSecretName(provider) {
6283
6568
  return names[provider] ?? `${provider}_api_key`;
6284
6569
  }
6285
6570
  async function assertTenantAdminAccess(doFetch, cfg, env, token) {
6286
- const tenantId = (0, import_apps2.tenantIdFor)(cfg.app.id, env);
6571
+ const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
6287
6572
  const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
6288
6573
  headers: { authorization: `Bearer ${token}` }
6289
6574
  });
@@ -6426,7 +6711,7 @@ async function provision(options) {
6426
6711
  const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
6427
6712
  if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
6428
6713
  throw new Error(
6429
- `refusing to provision production env ${productionEnvs.join(", ")} without --yes; run --dry-run first`
6714
+ `refusing to provision the live database (env ${productionEnvs.join(", ")}) without --yes; run --dry-run first`
6430
6715
  );
6431
6716
  }
6432
6717
  const database = await resolveDatabaseConfig(cfg);
@@ -6474,7 +6759,7 @@ async function provision(options) {
6474
6759
  }
6475
6760
  const doFetch = options.fetch ?? fetch;
6476
6761
  const token = await getDeveloperToken(cfg, options, doFetch, out);
6477
- const apps = (0, import_apps3.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
6762
+ const apps = (0, import_apps4.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
6478
6763
  const existing = await readRegistryApp(cfg, token, doFetch);
6479
6764
  if (existing) {
6480
6765
  out.log(`app: ${cfg.app.id} already exists`);
@@ -6518,7 +6803,7 @@ async function provision(options) {
6518
6803
  }
6519
6804
  }
6520
6805
  for (const env of cfg.envs) {
6521
- const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
6806
+ const tenantId = (0, import_apps4.tenantIdFor)(cfg.app.id, env);
6522
6807
  credentials = await provisionEnvCredentials({
6523
6808
  cfg,
6524
6809
  env,
@@ -6604,7 +6889,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
6604
6889
 
6605
6890
  // src/secrets-set.ts
6606
6891
  var import_ai3 = require("@odla-ai/ai");
6607
- var import_apps4 = require("@odla-ai/apps");
6892
+ var import_apps5 = require("@odla-ai/apps");
6608
6893
  var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
6609
6894
  async function secretsSet(options) {
6610
6895
  const name = (options.name ?? "").trim();
@@ -6656,7 +6941,7 @@ async function resolveVaultWrite(options) {
6656
6941
  throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
6657
6942
  }
6658
6943
  const value = await secretInputValue(options, "secret");
6659
- return { cfg, tenantId: (0, import_apps4.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
6944
+ return { cfg, tenantId: (0, import_apps5.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
6660
6945
  }
6661
6946
 
6662
6947
  // src/security-command-context.ts
@@ -7359,7 +7644,7 @@ async function securityStatus(parsed, dependencies) {
7359
7644
  }
7360
7645
 
7361
7646
  // src/skill.ts
7362
- var import_node_fs11 = require("fs");
7647
+ var import_node_fs12 = require("fs");
7363
7648
  var import_node_os3 = require("os");
7364
7649
  var import_node_path10 = require("path");
7365
7650
  var import_node_url3 = require("url");
@@ -7437,7 +7722,7 @@ function installSkill(options = {}) {
7437
7722
  plans.set(target, { target, content, boundary, managedMerge });
7438
7723
  };
7439
7724
  const planSkillTree = (targetDir2, boundary = root) => {
7440
- 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);
7725
+ 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);
7441
7726
  };
7442
7727
  let targetDir;
7443
7728
  if (options.global) {
@@ -7457,7 +7742,7 @@ function installSkill(options = {}) {
7457
7742
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
7458
7743
  if (harnesses.includes("claude")) {
7459
7744
  for (const skill of skillNames(files)) {
7460
- const canonical = (0, import_node_fs11.readFileSync)((0, import_node_path10.join)(sourceDir, skill, "SKILL.md"), "utf8");
7745
+ const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, skill, "SKILL.md"), "utf8");
7461
7746
  plan((0, import_node_path10.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
7462
7747
  }
7463
7748
  rememberTarget("claude", claudeRoot);
@@ -7492,11 +7777,11 @@ function installSkill(options = {}) {
7492
7777
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
7493
7778
  continue;
7494
7779
  }
7495
- if (!(0, import_node_fs11.existsSync)(file.target)) {
7780
+ if (!(0, import_node_fs12.existsSync)(file.target)) {
7496
7781
  writtenPaths.add(file.target);
7497
7782
  continue;
7498
7783
  }
7499
- const current = (0, import_node_fs11.readFileSync)(file.target, "utf8");
7784
+ const current = (0, import_node_fs12.readFileSync)(file.target, "utf8");
7500
7785
  if (current === file.content) {
7501
7786
  unchangedPaths.add(file.target);
7502
7787
  } else if (file.managedMerge || options.force) {
@@ -7513,9 +7798,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
7513
7798
  );
7514
7799
  }
7515
7800
  for (const file of plans.values()) {
7516
- if (!(0, import_node_fs11.existsSync)(file.target) || (0, import_node_fs11.readFileSync)(file.target, "utf8") !== file.content) {
7517
- (0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(file.target), { recursive: true });
7518
- (0, import_node_fs11.writeFileSync)(file.target, file.content);
7801
+ if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
7802
+ (0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(file.target), { recursive: true });
7803
+ (0, import_node_fs12.writeFileSync)(file.target, file.content);
7519
7804
  }
7520
7805
  }
7521
7806
  const skills = skillNames(files);
@@ -7556,9 +7841,9 @@ function normalizeHarnesses(values, global) {
7556
7841
  function managedFileContent(path, block, force, boundary) {
7557
7842
  const symlink = symlinkedComponent(boundary, path);
7558
7843
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
7559
- if (!(0, import_node_fs11.existsSync)(path)) return `${block}
7844
+ if (!(0, import_node_fs12.existsSync)(path)) return `${block}
7560
7845
  `;
7561
- const current = (0, import_node_fs11.readFileSync)(path, "utf8");
7846
+ const current = (0, import_node_fs12.readFileSync)(path, "utf8");
7562
7847
  const start = "<!-- odla-ai agent setup:start -->";
7563
7848
  const end = "<!-- odla-ai agent setup:end -->";
7564
7849
  const startAt = current.indexOf(start);
@@ -7587,7 +7872,7 @@ function symlinkedComponent(boundary, target) {
7587
7872
  for (const part of rel.split(import_node_path10.sep).filter(Boolean)) {
7588
7873
  current = (0, import_node_path10.join)(current, part);
7589
7874
  try {
7590
- if ((0, import_node_fs11.lstatSync)(current).isSymbolicLink()) return current;
7875
+ if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
7591
7876
  } catch (error) {
7592
7877
  if (error.code !== "ENOENT") throw error;
7593
7878
  }
@@ -7598,10 +7883,10 @@ function skillNames(files) {
7598
7883
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
7599
7884
  }
7600
7885
  function listFiles(dir) {
7601
- if (!(0, import_node_fs11.existsSync)(dir)) return [];
7886
+ if (!(0, import_node_fs12.existsSync)(dir)) return [];
7602
7887
  const results = [];
7603
7888
  const walk = (current) => {
7604
- for (const entry of (0, import_node_fs11.readdirSync)(current, { withFileTypes: true })) {
7889
+ for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
7605
7890
  const path = (0, import_node_path10.join)(current, entry.name);
7606
7891
  if (entry.isDirectory()) walk(path);
7607
7892
  else results.push((0, import_node_path10.relative)(dir, path));