@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.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  exitCodeFor,
4
4
  runCli
5
- } from "./chunk-X3HUN6S3.js";
5
+ } from "./chunk-2UKO2NED.js";
6
6
 
7
7
  // src/bin.ts
8
8
  runCli().catch((err) => {
@@ -1096,7 +1096,7 @@ var CAPABILITIES = {
1096
1096
  human: [
1097
1097
  "provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
1098
1098
  "review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
1099
- "consent to production changes with --yes",
1099
+ "consent to changes against an app's live database with --yes",
1100
1100
  "request destructive credential rotation explicitly",
1101
1101
  "review application semantics, security findings, releases, and merges",
1102
1102
  "approve redacted-source disclosure before hosted security reasoning",
@@ -4786,6 +4786,10 @@ Usage:
4786
4786
  odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
4787
4787
  odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
4788
4788
  odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
4789
+ odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
4790
+ odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
4791
+ odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
4792
+ odla-ai app promote [--dry-run] [--json] --yes
4789
4793
  odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
4790
4794
  odla-ai app owners add <email> [--email <odla-account>] [--json]
4791
4795
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
@@ -4822,17 +4826,29 @@ Commands:
4822
4826
  init Create a generic odla.config.mjs plus starter schema/rules files.
4823
4827
  doctor Validate and summarize the project config without network calls.
4824
4828
  calendar Inspect, connect, or disconnect the live Google booking connection.
4825
- app Archive (suspend, data retained), restore, export, or manage the
4826
- co-owners of the app. Archiving takes every environment's data
4827
- plane down until restored; permanent deletion has NO CLI \u2014 it
4828
- requires a signed-in owner in Studio, and no machine or agent
4829
- credential can purge. "app export" downloads a portable
4830
- gzipped-JSONL snapshot of one environment's database (--fresh
4831
- takes a new snapshot first) \u2014 your data is always yours to take.
4829
+ app Archive (suspend, data retained), restore, export, import, or
4830
+ manage the co-owners of the app. Archiving takes every
4831
+ environment's data plane down until restored; permanent deletion
4832
+ has NO CLI \u2014 it requires a signed-in owner in Studio, and no
4833
+ machine or agent credential can purge. "app export" downloads a
4834
+ portable gzipped-JSONL snapshot of one database (--fresh takes a
4835
+ new snapshot first) \u2014 your data is always yours to take.
4836
+ "app import" upserts rows back in from JSON, JSONL, or a
4837
+ {namespace: rows} map (the shape a query returns); it writes only
4838
+ with --yes, so the bare command is a dry run, and it refuses to
4839
+ guess an id mode that would duplicate rows on a re-run.
4840
+ "app refresh-sandbox" replaces the sandbox with a copy of live
4841
+ (the routine dev loop); "app go-live" copies the sandbox into an
4842
+ EMPTY live database, once, at launch; "app promote" pushes only
4843
+ schema, rules and gates up afterwards, leaving live's rows alone.
4844
+ Each prints its plan and writes nothing without --yes, so the
4845
+ bare command is the dry run. Clerk config, triggers, allowlists,
4846
+ secrets and API keys never travel; identity rows stay behind
4847
+ unless --include-identity.
4832
4848
  "app owners add <email>" grants a signed-up odla member the SAME
4833
4849
  full access as you; they then run their own "provision" to mint
4834
- their own credentials for the shared db (prod included) \u2014 no
4835
- secret is ever copied between people.
4850
+ their own credentials for the shared db (the live one included)
4851
+ \u2014 no secret is ever copied between people.
4836
4852
  capabilities Show what the CLI automates vs agent edits and human checkpoints.
4837
4853
  code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
4838
4854
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
@@ -4847,8 +4863,11 @@ Commands:
4847
4863
  version Print the CLI version.
4848
4864
 
4849
4865
  Safety:
4850
- New projects target dev only. Add prod explicitly to odla.config.mjs and pass
4851
- --yes to provision it; use --dry-run first to inspect the resolved plan.
4866
+ Every app has two databases on odla.ai: a sandbox (env "dev", tenant
4867
+ <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
4868
+ production odla.ai \u2014 there is no separate odla to point at. New projects get
4869
+ the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
4870
+ provision the live database; use --dry-run first to inspect the resolved plan.
4852
4871
  Provision caches the approved developer token and service credentials under
4853
4872
  .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
4854
4873
  preflights Wrangler before any shown-once issuance or destructive rotation.
@@ -4952,9 +4971,9 @@ async function codeConnect(options) {
4952
4971
  }
4953
4972
  const appId = requestedAppId ?? cfg?.app.id;
4954
4973
  const platform = (options.platform ?? cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai").replace(/\/+$/, "");
4955
- const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : "dev");
4974
+ const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : void 0);
4956
4975
  if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
4957
- 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`);
4976
+ 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}`);
4958
4977
  }
4959
4978
  if (process.platform !== "darwin" && process.platform !== "linux") {
4960
4979
  throw new Error("odla Code hosts require macOS or Linux");
@@ -5932,7 +5951,7 @@ async function provision(options) {
5932
5951
  const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
5933
5952
  if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
5934
5953
  throw new Error(
5935
- `refusing to provision production env ${productionEnvs.join(", ")} without --yes; run --dry-run first`
5954
+ `refusing to provision the live database (env ${productionEnvs.join(", ")}) without --yes; run --dry-run first`
5936
5955
  );
5937
5956
  }
5938
5957
  const database = await resolveDatabaseConfig(cfg);
@@ -6957,13 +6976,37 @@ async function adminCommand(parsed) {
6957
6976
  import { createWriteStream } from "fs";
6958
6977
  import { Readable } from "stream";
6959
6978
  import { pipeline } from "stream/promises";
6979
+
6980
+ // src/tenant.ts
6981
+ import { tenantIdFor as tenantIdFor5 } from "@odla-ai/apps";
6982
+ function resolveEnv(cfg, requested) {
6983
+ const env = requested ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
6984
+ if (!env || !cfg.envs.includes(env)) {
6985
+ throw new Error(`env "${env ?? ""}" is not declared in ${cfg.configPath}`);
6986
+ }
6987
+ return env;
6988
+ }
6989
+ function resolveTenant(cfg, requested) {
6990
+ const env = resolveEnv(cfg, requested);
6991
+ return { env, tenant: tenantIdFor5(cfg.app.id, env) };
6992
+ }
6993
+ function bothTenants(cfg) {
6994
+ for (const env of ["dev", "prod"]) {
6995
+ if (!cfg.envs.includes(env)) {
6996
+ throw new Error(
6997
+ `env "${env}" is not declared in ${cfg.configPath} \u2014 add it to \`envs\` and run \`odla-ai provision --yes\` before transferring data between environments`
6998
+ );
6999
+ }
7000
+ }
7001
+ return { sandbox: tenantIdFor5(cfg.app.id, "dev"), live: tenantIdFor5(cfg.app.id, "prod") };
7002
+ }
7003
+
7004
+ // src/app-export.ts
6960
7005
  async function appExport(options) {
6961
7006
  const cfg = await loadProjectConfig(options.configPath);
6962
7007
  const out = options.stdout ?? console;
6963
7008
  const doFetch = options.fetch ?? fetch;
6964
- const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
6965
- if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
6966
- const tenant = env === "prod" ? cfg.app.id : `${cfg.app.id}--${env}`;
7009
+ const { tenant } = resolveTenant(cfg, options.env);
6967
7010
  const token = await getDeveloperToken(
6968
7011
  cfg,
6969
7012
  { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
@@ -6998,6 +7041,89 @@ async function appExport(options) {
6998
7041
  return { file, backup: newest };
6999
7042
  }
7000
7043
 
7044
+ // src/app-import.ts
7045
+ import { readFileSync as readFileSync7 } from "fs";
7046
+ import {
7047
+ buildImportOps,
7048
+ importMutationId,
7049
+ parseImport,
7050
+ planImportChunks
7051
+ } from "@odla-ai/db/import";
7052
+ function chooseIdMode(options, rows) {
7053
+ const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
7054
+ if (chosen.length > 1) throw new Error("choose one of --id-field, --key, or --generate-ids");
7055
+ if (options.key) return { idMode: "key" };
7056
+ if (options.generateIds) return { idMode: "generate" };
7057
+ if (options.idField) return { idMode: "field", idField: options.idField };
7058
+ if (rows.length > 0 && rows.every((row) => typeof row?.id === "string" && row.id)) return { idMode: "field" };
7059
+ throw new Error(
7060
+ "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)"
7061
+ );
7062
+ }
7063
+ async function appImport(options) {
7064
+ const cfg = await loadProjectConfig(options.configPath);
7065
+ const out = options.stdout ?? console;
7066
+ const doFetch = options.fetch ?? fetch;
7067
+ const { tenant } = resolveTenant(cfg, options.env);
7068
+ const text = options.file === "-" ? (options.readStdin ?? (() => readFileSync7(0, "utf8")))() : readFileSync7(options.file, "utf8");
7069
+ const { format, sources } = parseImport(text, options.ns);
7070
+ if (format === "namespace-map" && options.ns) {
7071
+ throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
7072
+ }
7073
+ if (format !== "namespace-map" && !options.ns) throw new Error("--ns is required unless the file is a {namespace: rows} map");
7074
+ if (sources.length === 0) throw new Error(`${options.file} contains no rows`);
7075
+ const mode = chooseIdMode(options, sources.map((s) => s.row));
7076
+ const build = { ...mode, keyAttr: options.key };
7077
+ const { ops, rejected, ignored } = buildImportOps(sources, build);
7078
+ if (rejected.length > 0) {
7079
+ const detail = rejected.slice(0, 10).map((r) => ` row ${r.index}: ${r.reason}`).join("\n");
7080
+ const more = rejected.length > 10 ? `
7081
+ \u2026and ${rejected.length - 10} more` : "";
7082
+ throw new Error(`${rejected.length} row(s) cannot be imported \u2014 fix the input and re-run:
7083
+ ${detail}${more}`);
7084
+ }
7085
+ const chunks = planImportChunks(ops);
7086
+ const result = { tenant, rows: ops.length, chunks: chunks.length, committed: 0, duplicate: 0, txIds: [], ignored };
7087
+ if (ignored.length > 0) out.log(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
7088
+ if (options.dryRun || options.yes !== true) {
7089
+ const why = options.dryRun ? "--dry-run" : "no --yes";
7090
+ out.log(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
7091
+ if (options.json) out.log(JSON.stringify(result, null, 2));
7092
+ return result;
7093
+ }
7094
+ const token = await getDeveloperToken(
7095
+ cfg,
7096
+ { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
7097
+ doFetch,
7098
+ out
7099
+ );
7100
+ const runId = crypto.randomUUID();
7101
+ const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
7102
+ for (const chunk of chunks) {
7103
+ const res = await doFetch(url, {
7104
+ method: "POST",
7105
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
7106
+ body: JSON.stringify({ mutationId: importMutationId(runId, chunk.index), ops: chunk.ops })
7107
+ });
7108
+ const body = await res.json().catch(() => ({}));
7109
+ if (!res.ok) {
7110
+ const code = body.error?.code ? ` (${body.error.code})` : "";
7111
+ throw new Error(
7112
+ `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.`
7113
+ );
7114
+ }
7115
+ result.committed += chunk.ops.length;
7116
+ if (typeof body.txId === "number") result.txIds.push(body.txId);
7117
+ if (body.duplicate) result.duplicate++;
7118
+ }
7119
+ if (options.json) out.log(JSON.stringify(result, null, 2));
7120
+ else {
7121
+ const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
7122
+ out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
7123
+ }
7124
+ return result;
7125
+ }
7126
+
7001
7127
  // src/app-owners.ts
7002
7128
  var sink = (options) => options.stdout ?? console;
7003
7129
  async function ownersRequest(method, suffix, options, body) {
@@ -7085,6 +7211,133 @@ async function appOwnersCommand(parsed, dependencies = {}) {
7085
7211
  );
7086
7212
  }
7087
7213
 
7214
+ // src/app-transfer.ts
7215
+ function endpointsFor(verb, tenants) {
7216
+ const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
7217
+ const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
7218
+ if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
7219
+ return { ...up, mode: "cutover" };
7220
+ }
7221
+ async function api(cfg, token, path, init, doFetch) {
7222
+ const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
7223
+ ...init,
7224
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
7225
+ });
7226
+ return { status: res.status, body: await res.json().catch(() => ({})) };
7227
+ }
7228
+ function describe(side) {
7229
+ if (!side.exists) return "not provisioned";
7230
+ if (side.maxTx === 0) return "empty \u2014 never written";
7231
+ const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
7232
+ const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
7233
+ return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
7234
+ }
7235
+ function printPlan(out, verb, pre, opts) {
7236
+ const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
7237
+ out.log(`${verb} (${arrow})`);
7238
+ out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
7239
+ out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
7240
+ if (verb === "promote") {
7241
+ out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
7242
+ } else {
7243
+ out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
7244
+ out.log(` stays put: ${pre.staysPut.join(", ")}`);
7245
+ const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
7246
+ out.log(` identity: ${identity}`);
7247
+ out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
7248
+ }
7249
+ for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
7250
+ }
7251
+ async function appTransfer(options) {
7252
+ const cfg = await loadProjectConfig(options.configPath);
7253
+ const out = options.stdout ?? console;
7254
+ const doFetch = options.fetch ?? fetch;
7255
+ const tenants = bothTenants(cfg);
7256
+ const route2 = endpointsFor(options.verb, tenants);
7257
+ const token = await getDeveloperToken(
7258
+ cfg,
7259
+ { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
7260
+ doFetch,
7261
+ out
7262
+ );
7263
+ const pre = await api(
7264
+ cfg,
7265
+ token,
7266
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
7267
+ void 0,
7268
+ doFetch
7269
+ );
7270
+ if (pre.status !== 200) {
7271
+ throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
7272
+ }
7273
+ const plan = pre.body;
7274
+ printPlan(out, options.verb, plan, options);
7275
+ if (options.verb === "go-live" && !plan.targetEmpty) {
7276
+ throw new Error(
7277
+ `${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.`
7278
+ );
7279
+ }
7280
+ if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
7281
+ if (options.dryRun || options.yes !== true) {
7282
+ out.log(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
7283
+ if (options.json) out.log(JSON.stringify(plan, null, 2));
7284
+ return { ok: true, plan };
7285
+ }
7286
+ if (options.verb === "promote") {
7287
+ const res2 = await api(
7288
+ cfg,
7289
+ token,
7290
+ `/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
7291
+ { method: "POST", body: JSON.stringify({ from: "dev" }) },
7292
+ doFetch
7293
+ );
7294
+ if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
7295
+ if (options.json) out.log(JSON.stringify(res2.body, null, 2));
7296
+ else out.log(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
7297
+ return { ok: true, plan, result: res2.body };
7298
+ }
7299
+ const res = await api(
7300
+ cfg,
7301
+ token,
7302
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
7303
+ {
7304
+ method: "POST",
7305
+ body: JSON.stringify({
7306
+ from: route2.from,
7307
+ mode: route2.mode,
7308
+ ...options.includeIdentity ? { includeUsers: true } : {},
7309
+ ...options.includeFiles ? { includeFiles: true } : {}
7310
+ })
7311
+ },
7312
+ doFetch
7313
+ );
7314
+ if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
7315
+ out.log(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
7316
+ if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
7317
+ if (options.json) out.log(JSON.stringify(res.body, null, 2));
7318
+ return { ok: true, plan, result: res.body };
7319
+ }
7320
+ async function copyFiles(cfg, token, route2, doFetch, out) {
7321
+ for (let attempt = 1; attempt <= 20; attempt++) {
7322
+ const res = await api(
7323
+ cfg,
7324
+ token,
7325
+ `/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
7326
+ { method: "POST", body: JSON.stringify({ from: route2.from }) },
7327
+ doFetch
7328
+ );
7329
+ if (res.status === 200) {
7330
+ out.log(`${route2.target}: files copied`);
7331
+ return;
7332
+ }
7333
+ if (!res.body.error?.retry) {
7334
+ throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
7335
+ }
7336
+ out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
7337
+ }
7338
+ throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
7339
+ }
7340
+
7088
7341
  // src/app-lifecycle.ts
7089
7342
  async function lifecycleCall(action, options) {
7090
7343
  const cfg = await loadProjectConfig(options.configPath);
@@ -7146,9 +7399,48 @@ async function appCommand(parsed, dependencies = {}) {
7146
7399
  });
7147
7400
  return;
7148
7401
  }
7402
+ if (sub === "refresh-sandbox" || sub === "go-live" || sub === "promote") {
7403
+ assertArgs(parsed, ["config", "include-identity", "include-files", "dry-run", "yes", "token", "email", "json"], 2);
7404
+ await appTransfer({
7405
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
7406
+ verb: sub,
7407
+ includeIdentity: parsed.options["include-identity"] === true,
7408
+ includeFiles: parsed.options["include-files"] === true,
7409
+ dryRun: parsed.options["dry-run"] === true,
7410
+ yes: parsed.options.yes === true,
7411
+ token: stringOpt(parsed.options.token),
7412
+ email: stringOpt(parsed.options.email),
7413
+ json: parsed.options.json === true,
7414
+ fetch: dependencies.fetch,
7415
+ stdout: dependencies.stdout
7416
+ });
7417
+ return;
7418
+ }
7419
+ if (sub === "import") {
7420
+ assertArgs(parsed, ["config", "env", "ns", "id-field", "key", "generate-ids", "dry-run", "yes", "token", "email", "json"], 3);
7421
+ const file = parsed.positionals[2];
7422
+ if (!file) throw new Error('app import needs a file: "odla-ai app import rows.json --ns todos --yes" (or "-" for stdin)');
7423
+ await appImport({
7424
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
7425
+ file,
7426
+ env: stringOpt(parsed.options.env),
7427
+ ns: stringOpt(parsed.options.ns),
7428
+ idField: stringOpt(parsed.options["id-field"]),
7429
+ key: stringOpt(parsed.options.key),
7430
+ generateIds: parsed.options["generate-ids"] === true,
7431
+ dryRun: parsed.options["dry-run"] === true,
7432
+ yes: parsed.options.yes === true,
7433
+ token: stringOpt(parsed.options.token),
7434
+ email: stringOpt(parsed.options.email),
7435
+ json: parsed.options.json === true,
7436
+ fetch: dependencies.fetch,
7437
+ stdout: dependencies.stdout
7438
+ });
7439
+ return;
7440
+ }
7149
7441
  if (sub !== "archive" && sub !== "restore") {
7150
7442
  throw new Error(
7151
- `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.)`
7443
+ `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.)`
7152
7444
  );
7153
7445
  }
7154
7446
  assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
@@ -7910,4 +8202,4 @@ export {
7910
8202
  exitCodeFor,
7911
8203
  runCli
7912
8204
  };
7913
- //# sourceMappingURL=chunk-X3HUN6S3.js.map
8205
+ //# sourceMappingURL=chunk-2UKO2NED.js.map