@odla-ai/cli 0.16.7 → 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 +346 -61
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-X3HUN6S3.js → chunk-2UKO2NED.js} +312 -20
- package/dist/chunk-2UKO2NED.js.map +1 -0
- package/dist/index.cjs +346 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/skills/odla/SKILL.md +7 -5
- package/skills/odla/references/sdks.md +9 -0
- package/skills/odla-migrate/SKILL.md +4 -2
- package/skills/odla-migrate/references/phase-2-chapter.md +88 -0
- package/skills/odla-migrate/references/phase-2-db.md +10 -1
- package/dist/chunk-X3HUN6S3.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -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
|
|
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
|
|
4826
|
-
co-owners of the app. Archiving takes every
|
|
4827
|
-
plane down until restored; permanent deletion
|
|
4828
|
-
requires a signed-in owner in Studio, and no
|
|
4829
|
-
credential can purge. "app export" downloads a
|
|
4830
|
-
gzipped-JSONL snapshot of one
|
|
4831
|
-
|
|
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 (
|
|
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
|
-
|
|
4851
|
-
|
|
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] :
|
|
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
|
|
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
|
|
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-
|
|
8205
|
+
//# sourceMappingURL=chunk-2UKO2NED.js.map
|