@odla-ai/cli 0.27.11 → 0.27.12
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 +230 -612
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-Y7WFLSTN.js → chunk-ECRBOR66.js} +114 -430
- package/dist/chunk-ECRBOR66.js.map +1 -0
- package/dist/index.cjs +230 -545
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-Y7WFLSTN.js.map +0 -1
|
@@ -1372,12 +1372,6 @@ async function pollCalendarConnection(ctx, attemptId) {
|
|
|
1372
1372
|
ctx.env
|
|
1373
1373
|
);
|
|
1374
1374
|
}
|
|
1375
|
-
async function requestCalendarDisconnect(ctx) {
|
|
1376
|
-
return parseCalendarStatus(
|
|
1377
|
-
await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
|
|
1378
|
-
ctx.env
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
1375
|
function parseCalendarStatus(raw, env) {
|
|
1382
1376
|
const outer = wrapped(raw, "calendar");
|
|
1383
1377
|
const value2 = record(outer.attempt) ?? record(outer.status) ?? outer;
|
|
@@ -1528,6 +1522,16 @@ async function waitForCalendarPoll(milliseconds, signal) {
|
|
|
1528
1522
|
});
|
|
1529
1523
|
}
|
|
1530
1524
|
|
|
1525
|
+
// src/human-session.ts
|
|
1526
|
+
async function requireStudioHuman(configPath, action2, destination = "app", env) {
|
|
1527
|
+
const cfg = await loadProjectConfig(configPath);
|
|
1528
|
+
const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
1529
|
+
const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
|
|
1530
|
+
throw new Error(
|
|
1531
|
+
`human_session_required: ${action2} requires the signed-in owner in Studio. A CLI device token is an agent credential, so another approval or retry cannot work. ${new URL(path, cfg.platformUrl).href}`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1531
1535
|
// src/calendar.ts
|
|
1532
1536
|
async function calendarStatus(options) {
|
|
1533
1537
|
const { ctx, out } = await lifecycleContext(options);
|
|
@@ -1561,10 +1565,7 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
|
1561
1565
|
}
|
|
1562
1566
|
async function calendarDisconnect(options) {
|
|
1563
1567
|
if (!options.yes) throw new Error("calendar disconnect requires --yes");
|
|
1564
|
-
|
|
1565
|
-
const status = await requestCalendarDisconnect(ctx);
|
|
1566
|
-
out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
|
|
1567
|
-
return status;
|
|
1568
|
+
return requireStudioHuman(options.configPath, "calendar disconnect", "calendar", options.env);
|
|
1568
1569
|
}
|
|
1569
1570
|
async function ensureCalendarConnected(ctx, options) {
|
|
1570
1571
|
const out = options.stdout ?? console;
|
|
@@ -3345,7 +3346,9 @@ async function secretsSetClerkKey(options) {
|
|
|
3345
3346
|
if (value2.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
3346
3347
|
throw new Error(`refusing to store an sk_live_ Clerk key for "${options.env}" without --yes (live users would sync into a non-prod tenant)`);
|
|
3347
3348
|
}
|
|
3348
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out
|
|
3349
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
3350
|
+
optionalProjectCapabilities: ["app.manage"]
|
|
3351
|
+
});
|
|
3349
3352
|
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
|
|
3350
3353
|
method: "POST",
|
|
3351
3354
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -7817,17 +7820,17 @@ async function runHostedSecurity(options) {
|
|
|
7817
7820
|
allowNetwork: false
|
|
7818
7821
|
}
|
|
7819
7822
|
});
|
|
7820
|
-
const
|
|
7821
|
-
await writeSecurityArtifacts(output,
|
|
7822
|
-
const reportDigest = await securityFingerprint(
|
|
7823
|
+
const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
7824
|
+
await writeSecurityArtifacts(output, report4);
|
|
7825
|
+
const reportDigest = await securityFingerprint(report4);
|
|
7823
7826
|
await hosted.complete({
|
|
7824
7827
|
reportDigest,
|
|
7825
|
-
coverageStatus:
|
|
7826
|
-
confirmed:
|
|
7827
|
-
candidates:
|
|
7828
|
+
coverageStatus: report4.coverageStatus,
|
|
7829
|
+
confirmed: report4.metrics.confirmed,
|
|
7830
|
+
candidates: report4.metrics.candidates
|
|
7828
7831
|
}, { signal: options.signal });
|
|
7829
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
7830
|
-
return Object.freeze({ report:
|
|
7832
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
|
|
7833
|
+
return Object.freeze({ report: report4, run: hosted.run, output });
|
|
7831
7834
|
}
|
|
7832
7835
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
7833
7836
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -7853,14 +7856,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
7853
7856
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
7854
7857
|
return { ...profile, maxHuntTasks };
|
|
7855
7858
|
}
|
|
7856
|
-
function printSummary(out, appId, env, run,
|
|
7857
|
-
const complete =
|
|
7859
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
7860
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
7858
7861
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
7859
7862
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
7860
7863
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
7861
|
-
out.log(` coverage: ${
|
|
7862
|
-
if (
|
|
7863
|
-
out.log(` findings: confirmed=${
|
|
7864
|
+
out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
7865
|
+
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
7866
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
7864
7867
|
out.log(` report: ${resolve9(output, "REPORT.md")}`);
|
|
7865
7868
|
}
|
|
7866
7869
|
function formatBudget(usage) {
|
|
@@ -8425,7 +8428,7 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
8425
8428
|
if (action2 !== "jobs" && action2 !== "retry") {
|
|
8426
8429
|
throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
|
|
8427
8430
|
}
|
|
8428
|
-
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"
|
|
8431
|
+
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
|
|
8429
8432
|
if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
|
|
8430
8433
|
throw new Error('--state and --limit are supported only by "agent jobs"');
|
|
8431
8434
|
}
|
|
@@ -8433,17 +8436,17 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
8433
8436
|
const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
|
|
8434
8437
|
const doFetch = deps.fetch ?? fetch;
|
|
8435
8438
|
const out = deps.stdout ?? console;
|
|
8436
|
-
const credential2 =
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8439
|
+
const credential2 = stringOpt(parsed.options.token) ?? readCredentials(cfg.local.credentialsFile)?.envs[env]?.dbKey;
|
|
8440
|
+
if (!credential2) {
|
|
8441
|
+
throw new Error(
|
|
8442
|
+
`no ${env} app credential found; run \`odla-ai provision --write-dev-vars --yes\` or pass --token <ODLA_API_KEY>`
|
|
8443
|
+
);
|
|
8444
|
+
}
|
|
8445
|
+
if (credential2.startsWith("odla_dev_")) {
|
|
8446
|
+
throw new Error(
|
|
8447
|
+
"agent job administration requires an app credential (ODLA_API_KEY / odla_sk_\u2026), not a developer device token"
|
|
8448
|
+
);
|
|
8449
|
+
}
|
|
8447
8450
|
const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
|
|
8448
8451
|
const headers = { authorization: `Bearer ${credential2}` };
|
|
8449
8452
|
if (action2 === "retry") {
|
|
@@ -8500,53 +8503,14 @@ function errorMessage(body) {
|
|
|
8500
8503
|
}
|
|
8501
8504
|
|
|
8502
8505
|
// src/app-export.ts
|
|
8503
|
-
import { createWriteStream } from "fs";
|
|
8504
|
-
import { Readable } from "stream";
|
|
8505
|
-
import { pipeline } from "stream/promises";
|
|
8506
8506
|
async function appExport(options) {
|
|
8507
|
-
|
|
8508
|
-
const out = options.stdout ?? console;
|
|
8509
|
-
const doFetch = options.fetch ?? fetch;
|
|
8510
|
-
const { tenant } = resolveTenant(cfg, options.env);
|
|
8511
|
-
const token = await getDeveloperToken(
|
|
8512
|
-
cfg,
|
|
8513
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8514
|
-
doFetch,
|
|
8515
|
-
out
|
|
8516
|
-
);
|
|
8517
|
-
const auth = { authorization: `Bearer ${token}` };
|
|
8518
|
-
const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
|
|
8519
|
-
if (options.fresh) {
|
|
8520
|
-
const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
|
|
8521
|
-
const body = await res.json().catch(() => ({}));
|
|
8522
|
-
if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
|
|
8523
|
-
}
|
|
8524
|
-
const list = await doFetch(`${base}/backups`, { headers: auth });
|
|
8525
|
-
if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
|
|
8526
|
-
const { backups } = await list.json();
|
|
8527
|
-
const newest = backups[0];
|
|
8528
|
-
if (!newest) {
|
|
8529
|
-
throw new Error(
|
|
8530
|
-
`${tenant} has no backups yet \u2014 run \`odla-ai app export --fresh\` to take one now (nightly snapshots appear after the first day with writes)`
|
|
8531
|
-
);
|
|
8532
|
-
}
|
|
8533
|
-
const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
|
|
8534
|
-
if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
|
|
8535
|
-
const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
|
|
8536
|
-
await pipeline(Readable.fromWeb(download.body), createWriteStream(file));
|
|
8537
|
-
if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
|
|
8538
|
-
else {
|
|
8539
|
-
out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
|
|
8540
|
-
out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
|
|
8541
|
-
}
|
|
8542
|
-
return { file, backup: newest };
|
|
8507
|
+
return requireStudioHuman(options.configPath, "database export", "database", options.env);
|
|
8543
8508
|
}
|
|
8544
8509
|
|
|
8545
8510
|
// src/app-import.ts
|
|
8546
8511
|
import { readFileSync as readFileSync9 } from "fs";
|
|
8547
8512
|
import {
|
|
8548
8513
|
buildImportOps,
|
|
8549
|
-
importMutationId,
|
|
8550
8514
|
parseImport,
|
|
8551
8515
|
planImportChunks
|
|
8552
8516
|
} from "@odla-ai/db/import";
|
|
@@ -8565,7 +8529,6 @@ async function appImport(options) {
|
|
|
8565
8529
|
const cfg = await loadProjectConfig(options.configPath);
|
|
8566
8530
|
const out = options.stdout ?? console;
|
|
8567
8531
|
const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
|
|
8568
|
-
const doFetch = options.fetch ?? fetch;
|
|
8569
8532
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
8570
8533
|
const text2 = options.file === "-" ? (options.readStdin ?? (() => readFileSync9(0, "utf8")))() : readFileSync9(options.file, "utf8");
|
|
8571
8534
|
const { format, sources } = parseImport(text2, options.ns);
|
|
@@ -8593,100 +8556,18 @@ ${detail}${more}`);
|
|
|
8593
8556
|
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
8594
8557
|
return result;
|
|
8595
8558
|
}
|
|
8596
|
-
|
|
8597
|
-
cfg,
|
|
8598
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8599
|
-
doFetch,
|
|
8600
|
-
out
|
|
8601
|
-
);
|
|
8602
|
-
const runId = crypto.randomUUID();
|
|
8603
|
-
const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
|
|
8604
|
-
for (const chunk of chunks) {
|
|
8605
|
-
const res = await doFetch(url, {
|
|
8606
|
-
method: "POST",
|
|
8607
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
8608
|
-
body: JSON.stringify({ mutationId: importMutationId(runId, chunk.index), ops: chunk.ops })
|
|
8609
|
-
});
|
|
8610
|
-
const body = await res.json().catch(() => ({}));
|
|
8611
|
-
if (!res.ok) {
|
|
8612
|
-
const code = body.error?.code ? ` (${body.error.code})` : "";
|
|
8613
|
-
throw new Error(
|
|
8614
|
-
`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.`
|
|
8615
|
-
);
|
|
8616
|
-
}
|
|
8617
|
-
result.committed += chunk.ops.length;
|
|
8618
|
-
if (typeof body.txId === "number") result.txIds.push(body.txId);
|
|
8619
|
-
if (body.duplicate) result.duplicate++;
|
|
8620
|
-
}
|
|
8621
|
-
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
8622
|
-
else {
|
|
8623
|
-
const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
|
|
8624
|
-
out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
|
|
8625
|
-
}
|
|
8626
|
-
return result;
|
|
8559
|
+
return requireStudioHuman(options.configPath, "database import", "database", options.env);
|
|
8627
8560
|
}
|
|
8628
8561
|
|
|
8629
8562
|
// src/app-owners.ts
|
|
8630
|
-
var sink = (options) => options.stdout ?? console;
|
|
8631
|
-
async function ownersRequest(method, suffix, options, body) {
|
|
8632
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
8633
|
-
const doFetch = options.fetch ?? fetch;
|
|
8634
|
-
const token = await getDeveloperToken(
|
|
8635
|
-
cfg,
|
|
8636
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8637
|
-
doFetch,
|
|
8638
|
-
sink(options)
|
|
8639
|
-
);
|
|
8640
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
|
|
8641
|
-
method,
|
|
8642
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
8643
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
8644
|
-
});
|
|
8645
|
-
const data = await res.json().catch(() => ({}));
|
|
8646
|
-
if (!res.ok) {
|
|
8647
|
-
throw new Error(
|
|
8648
|
-
`owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
8649
|
-
);
|
|
8650
|
-
}
|
|
8651
|
-
return data.owners ?? [];
|
|
8652
|
-
}
|
|
8653
|
-
function report(options, owners, headline) {
|
|
8654
|
-
const out = sink(options);
|
|
8655
|
-
if (options.json === true) {
|
|
8656
|
-
out.log(JSON.stringify(owners, null, 2));
|
|
8657
|
-
return;
|
|
8658
|
-
}
|
|
8659
|
-
if (headline) out.log(headline);
|
|
8660
|
-
out.log(`owners (${owners.length}):`);
|
|
8661
|
-
for (const o of owners) {
|
|
8662
|
-
const name = o.email?.trim() || "Unnamed member";
|
|
8663
|
-
out.log(
|
|
8664
|
-
` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
|
|
8665
|
-
);
|
|
8666
|
-
}
|
|
8667
|
-
}
|
|
8668
8563
|
async function ownersList(options) {
|
|
8669
|
-
|
|
8564
|
+
await requireStudioHuman(options.configPath, "listing app owners", "app");
|
|
8670
8565
|
}
|
|
8671
8566
|
async function ownersAdd(email, options) {
|
|
8672
|
-
|
|
8673
|
-
report(
|
|
8674
|
-
options,
|
|
8675
|
-
owners,
|
|
8676
|
-
`added ${email} as a co-owner \u2014 they share full access. They can now run \`odla-ai provision\` to mint their own credentials (no secret sharing).`
|
|
8677
|
-
);
|
|
8567
|
+
await requireStudioHuman(options.configPath, `adding ${email} as an app owner`, "app");
|
|
8678
8568
|
}
|
|
8679
8569
|
async function ownersRemove(target, options) {
|
|
8680
|
-
|
|
8681
|
-
if (target.includes("@")) {
|
|
8682
|
-
const owners2 = await ownersRequest("GET", "", options);
|
|
8683
|
-
const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
|
|
8684
|
-
if (!match) throw new Error(`no co-owner with email ${target}`);
|
|
8685
|
-
if (match.primary) throw new Error("can't remove the primary owner");
|
|
8686
|
-
ownerId = match.ownerId;
|
|
8687
|
-
}
|
|
8688
|
-
const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
|
|
8689
|
-
report(options, owners, `removed ${target}`);
|
|
8570
|
+
await requireStudioHuman(options.configPath, `removing ${target} as an app owner`, "app");
|
|
8690
8571
|
}
|
|
8691
8572
|
async function appOwnersCommand(parsed, dependencies = {}) {
|
|
8692
8573
|
const sub = parsed.positionals[2] ?? "list";
|
|
@@ -8718,35 +8599,9 @@ async function appOwnersCommand(parsed, dependencies = {}) {
|
|
|
8718
8599
|
|
|
8719
8600
|
// src/app-rename.ts
|
|
8720
8601
|
async function appRename(name, options) {
|
|
8721
|
-
const out = options.stdout ?? console;
|
|
8722
8602
|
const trimmed = name.trim();
|
|
8723
8603
|
if (!trimmed) throw new Error('"app rename" needs a name \u2014 try `odla-ai app rename "Acme Storefront"`.');
|
|
8724
|
-
|
|
8725
|
-
const doFetch = options.fetch ?? fetch;
|
|
8726
|
-
const token = await getDeveloperToken(
|
|
8727
|
-
cfg,
|
|
8728
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8729
|
-
doFetch,
|
|
8730
|
-
out
|
|
8731
|
-
);
|
|
8732
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/name`, {
|
|
8733
|
-
method: "PUT",
|
|
8734
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
8735
|
-
body: JSON.stringify({ name: trimmed })
|
|
8736
|
-
});
|
|
8737
|
-
const data = await res.json().catch(() => ({}));
|
|
8738
|
-
if (!res.ok || !data.app) {
|
|
8739
|
-
throw new Error(
|
|
8740
|
-
`rename failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
8741
|
-
);
|
|
8742
|
-
}
|
|
8743
|
-
if (options.json === true) {
|
|
8744
|
-
out.log(JSON.stringify(data.app, null, 2));
|
|
8745
|
-
return;
|
|
8746
|
-
}
|
|
8747
|
-
out.log(`renamed ${data.app.appId} \u2192 "${data.app.name}"`);
|
|
8748
|
-
out.log("The app id is unchanged, so credentials, tenants, and URLs keep working.");
|
|
8749
|
-
out.log(`Update the "name" in ${cfg.configPath} to match.`);
|
|
8604
|
+
await requireStudioHuman(options.configPath, `renaming the app to "${trimmed}"`, "app");
|
|
8750
8605
|
}
|
|
8751
8606
|
async function appRenameCommand(parsed, dependencies = {}) {
|
|
8752
8607
|
assertArgs(parsed, ["config", "token", "email", "json"], parsed.positionals.length);
|
|
@@ -8762,172 +8617,23 @@ async function appRenameCommand(parsed, dependencies = {}) {
|
|
|
8762
8617
|
}
|
|
8763
8618
|
|
|
8764
8619
|
// src/app-transfer.ts
|
|
8765
|
-
function endpointsFor(verb, tenants) {
|
|
8766
|
-
const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
|
|
8767
|
-
const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
|
|
8768
|
-
if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
|
|
8769
|
-
return { ...up, mode: "cutover" };
|
|
8770
|
-
}
|
|
8771
|
-
async function api(cfg, token, path, init, doFetch) {
|
|
8772
|
-
const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
|
|
8773
|
-
...init,
|
|
8774
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
|
|
8775
|
-
});
|
|
8776
|
-
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
8777
|
-
}
|
|
8778
|
-
function describe(side) {
|
|
8779
|
-
if (!side.exists) return "not provisioned";
|
|
8780
|
-
if (side.maxTx === 0) return "empty \u2014 never written";
|
|
8781
|
-
const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
|
|
8782
|
-
const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
|
|
8783
|
-
return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
|
|
8784
|
-
}
|
|
8785
|
-
function printPlan2(out, verb, pre, opts) {
|
|
8786
|
-
const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
|
|
8787
|
-
out.log(`${verb} (${arrow})`);
|
|
8788
|
-
out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
|
|
8789
|
-
out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
|
|
8790
|
-
if (verb === "promote") {
|
|
8791
|
-
out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
|
|
8792
|
-
} else {
|
|
8793
|
-
out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
|
|
8794
|
-
out.log(` stays put: ${pre.staysPut.join(", ")}`);
|
|
8795
|
-
const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
|
|
8796
|
-
out.log(` identity: ${identity}`);
|
|
8797
|
-
out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
|
|
8798
|
-
}
|
|
8799
|
-
for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
|
|
8800
|
-
}
|
|
8801
8620
|
async function appTransfer(options) {
|
|
8802
8621
|
const cfg = await loadProjectConfig(options.configPath);
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
const doFetch = options.fetch ?? fetch;
|
|
8806
|
-
const tenants = bothTenants(cfg);
|
|
8807
|
-
const route2 = endpointsFor(options.verb, tenants);
|
|
8808
|
-
const token = await getDeveloperToken(
|
|
8809
|
-
cfg,
|
|
8810
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8811
|
-
doFetch,
|
|
8812
|
-
out
|
|
8813
|
-
);
|
|
8814
|
-
const pre = await api(
|
|
8815
|
-
cfg,
|
|
8816
|
-
token,
|
|
8817
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
|
|
8818
|
-
void 0,
|
|
8819
|
-
doFetch
|
|
8820
|
-
);
|
|
8821
|
-
if (pre.status !== 200) {
|
|
8822
|
-
throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
|
|
8823
|
-
}
|
|
8824
|
-
const plan = pre.body;
|
|
8825
|
-
printPlan2({ log: say }, options.verb, plan, options);
|
|
8826
|
-
if (options.verb === "go-live" && !plan.targetEmpty) {
|
|
8827
|
-
throw new Error(
|
|
8828
|
-
`${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.`
|
|
8829
|
-
);
|
|
8830
|
-
}
|
|
8831
|
-
if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
|
|
8832
|
-
if (options.dryRun || options.yes !== true) {
|
|
8833
|
-
say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
|
|
8834
|
-
if (options.json) out.log(JSON.stringify(plan, null, 2));
|
|
8835
|
-
return { ok: true, plan };
|
|
8836
|
-
}
|
|
8837
|
-
if (options.verb === "promote") {
|
|
8838
|
-
const res2 = await api(
|
|
8839
|
-
cfg,
|
|
8840
|
-
token,
|
|
8841
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
|
|
8842
|
-
{ method: "POST", body: JSON.stringify({ from: "dev" }) },
|
|
8843
|
-
doFetch
|
|
8844
|
-
);
|
|
8845
|
-
if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
|
|
8846
|
-
if (options.json) out.log(JSON.stringify(res2.body, null, 2));
|
|
8847
|
-
else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
|
|
8848
|
-
return { ok: true, plan, result: res2.body };
|
|
8849
|
-
}
|
|
8850
|
-
const res = await api(
|
|
8851
|
-
cfg,
|
|
8852
|
-
token,
|
|
8853
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
|
|
8854
|
-
{
|
|
8855
|
-
method: "POST",
|
|
8856
|
-
body: JSON.stringify({
|
|
8857
|
-
from: route2.from,
|
|
8858
|
-
mode: route2.mode,
|
|
8859
|
-
...options.includeIdentity ? { includeUsers: true } : {},
|
|
8860
|
-
...options.includeFiles ? { includeFiles: true } : {}
|
|
8861
|
-
})
|
|
8862
|
-
},
|
|
8863
|
-
doFetch
|
|
8864
|
-
);
|
|
8865
|
-
if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
8866
|
-
say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
|
|
8867
|
-
if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
|
|
8868
|
-
if (options.json) out.log(JSON.stringify(res.body, null, 2));
|
|
8869
|
-
return { ok: true, plan, result: res.body };
|
|
8870
|
-
}
|
|
8871
|
-
async function copyFiles(cfg, token, route2, doFetch, out) {
|
|
8872
|
-
for (let attempt = 1; attempt <= 20; attempt++) {
|
|
8873
|
-
const res = await api(
|
|
8874
|
-
cfg,
|
|
8875
|
-
token,
|
|
8876
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
|
|
8877
|
-
{ method: "POST", body: JSON.stringify({ from: route2.from }) },
|
|
8878
|
-
doFetch
|
|
8879
|
-
);
|
|
8880
|
-
if (res.status === 200) {
|
|
8881
|
-
out.log(`${route2.target}: files copied`);
|
|
8882
|
-
return;
|
|
8883
|
-
}
|
|
8884
|
-
if (!res.body.error?.retry) {
|
|
8885
|
-
throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
8886
|
-
}
|
|
8887
|
-
out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
|
|
8888
|
-
}
|
|
8889
|
-
throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
|
|
8622
|
+
bothTenants(cfg);
|
|
8623
|
+
return requireStudioHuman(options.configPath, `app ${options.verb}`, "database");
|
|
8890
8624
|
}
|
|
8891
8625
|
|
|
8892
8626
|
// src/app-lifecycle.ts
|
|
8893
|
-
async function lifecycleCall(action2, options) {
|
|
8894
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
8895
|
-
const out = options.stdout ?? console;
|
|
8896
|
-
const doFetch = options.fetch ?? fetch;
|
|
8897
|
-
const token = await getDeveloperToken(
|
|
8898
|
-
cfg,
|
|
8899
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8900
|
-
doFetch,
|
|
8901
|
-
out
|
|
8902
|
-
);
|
|
8903
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
|
|
8904
|
-
method: "POST",
|
|
8905
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
8906
|
-
});
|
|
8907
|
-
const body = await res.json().catch(() => ({}));
|
|
8908
|
-
if (!res.ok || !body.ok) {
|
|
8909
|
-
throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
8910
|
-
}
|
|
8911
|
-
return body;
|
|
8912
|
-
}
|
|
8913
8627
|
async function appArchive(options) {
|
|
8914
8628
|
if (options.yes !== true) {
|
|
8915
8629
|
throw new Error(
|
|
8916
8630
|
"app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
|
|
8917
8631
|
);
|
|
8918
8632
|
}
|
|
8919
|
-
|
|
8920
|
-
const body = await lifecycleCall("archive", options);
|
|
8921
|
-
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
8922
|
-
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
|
|
8923
|
-
else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
|
|
8633
|
+
await requireStudioHuman(options.configPath, "app archive", "app");
|
|
8924
8634
|
}
|
|
8925
8635
|
async function appRestore(options) {
|
|
8926
|
-
|
|
8927
|
-
const body = await lifecycleCall("restore", options);
|
|
8928
|
-
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
8929
|
-
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
|
|
8930
|
-
else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
|
|
8636
|
+
await requireStudioHuman(options.configPath, "app restore", "app");
|
|
8931
8637
|
}
|
|
8932
8638
|
async function appCommand(parsed, dependencies = {}) {
|
|
8933
8639
|
const sub = parsed.positionals[1];
|
|
@@ -9563,18 +9269,17 @@ Usage:
|
|
|
9563
9269
|
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
9564
9270
|
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
9565
9271
|
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
9566
|
-
odla-ai calendar disconnect [--env dev] [
|
|
9567
|
-
odla-ai app archive [--config odla.config.mjs] [
|
|
9568
|
-
odla-ai app restore [--config odla.config.mjs]
|
|
9569
|
-
odla-ai app export [--env dev]
|
|
9272
|
+
odla-ai calendar disconnect [--env dev] --yes [continue in Studio; human session required]
|
|
9273
|
+
odla-ai app archive [--config odla.config.mjs] --yes [continue in Studio; human session required]
|
|
9274
|
+
odla-ai app restore [--config odla.config.mjs] [continue in Studio; human session required]
|
|
9275
|
+
odla-ai app export [--env dev] [continue in Studio; human session required]
|
|
9570
9276
|
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
9571
|
-
|
|
9572
|
-
odla-ai app
|
|
9573
|
-
odla-ai app
|
|
9574
|
-
odla-ai app
|
|
9575
|
-
odla-ai app
|
|
9576
|
-
odla-ai app owners add
|
|
9577
|
-
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
9277
|
+
[dry-run is local; writes continue in Studio]
|
|
9278
|
+
odla-ai app refresh-sandbox [continue in Studio; human session required]
|
|
9279
|
+
odla-ai app go-live [continue in Studio; human session required]
|
|
9280
|
+
odla-ai app promote [continue in Studio; human session required]
|
|
9281
|
+
odla-ai app rename <name> [continue in Studio; human session required]
|
|
9282
|
+
odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
|
|
9578
9283
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
9579
9284
|
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
9580
9285
|
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
@@ -9609,8 +9314,8 @@ Usage:
|
|
|
9609
9314
|
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
9610
9315
|
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
9611
9316
|
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
9612
|
-
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--
|
|
9613
|
-
odla-ai agent retry <job-id> [--env dev] [--
|
|
9317
|
+
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--token <ODLA_API_KEY>] [--json]
|
|
9318
|
+
odla-ai agent retry <job-id> [--env dev] [--token <ODLA_API_KEY>] [--json]
|
|
9614
9319
|
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
9615
9320
|
odla-ai context list [--json]
|
|
9616
9321
|
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
@@ -9646,8 +9351,8 @@ Usage:
|
|
|
9646
9351
|
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
9647
9352
|
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
9648
9353
|
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
9649
|
-
odla-ai security github connect [--repo owner/name] [--env dev]
|
|
9650
|
-
odla-ai security github disconnect --source <id> [--env dev]
|
|
9354
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
|
|
9355
|
+
odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
|
|
9651
9356
|
odla-ai security plan [--env dev] [--json]
|
|
9652
9357
|
odla-ai security sources [--env dev] [--json]
|
|
9653
9358
|
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
@@ -10158,7 +9863,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10158
9863
|
throw new WatchRemoteError(cursor, error);
|
|
10159
9864
|
}
|
|
10160
9865
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10161
|
-
const result =
|
|
9866
|
+
const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10162
9867
|
throw new WatchTimeoutError(result.cursor);
|
|
10163
9868
|
}
|
|
10164
9869
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -10199,7 +9904,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10199
9904
|
});
|
|
10200
9905
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
10201
9906
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
10202
|
-
return
|
|
9907
|
+
return report(ctx, parsed, {
|
|
10203
9908
|
found: true,
|
|
10204
9909
|
cursor,
|
|
10205
9910
|
events: matching,
|
|
@@ -10226,13 +9931,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10226
9931
|
}
|
|
10227
9932
|
if (page2.hasMore) continue;
|
|
10228
9933
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10229
|
-
return
|
|
9934
|
+
return report(ctx, parsed, { found: false, cursor });
|
|
10230
9935
|
}
|
|
10231
9936
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
10232
9937
|
await sleep(wait2);
|
|
10233
9938
|
}
|
|
10234
9939
|
}
|
|
10235
|
-
function
|
|
9940
|
+
function report(ctx, parsed, result) {
|
|
10236
9941
|
if (ctx.json) {
|
|
10237
9942
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
10238
9943
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -10733,7 +10438,7 @@ function eventLabel(event) {
|
|
|
10733
10438
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
10734
10439
|
return body || event.payload.entityId;
|
|
10735
10440
|
}
|
|
10736
|
-
function
|
|
10441
|
+
function report2(ctx, parsed, result) {
|
|
10737
10442
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
10738
10443
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
10739
10444
|
for (const event of result.events ?? []) {
|
|
@@ -10793,7 +10498,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10793
10498
|
});
|
|
10794
10499
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
10795
10500
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10796
|
-
return
|
|
10501
|
+
return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10797
10502
|
}
|
|
10798
10503
|
const backoff = Math.min(
|
|
10799
10504
|
MAX_BACKOFF_MS2,
|
|
@@ -10834,7 +10539,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10834
10539
|
cursor,
|
|
10835
10540
|
serverTime: current.serverTime
|
|
10836
10541
|
});
|
|
10837
|
-
return
|
|
10542
|
+
return report2(ctx, parsed, { found: true, cursor, events: matching });
|
|
10838
10543
|
}
|
|
10839
10544
|
if (current.events.length > 0) {
|
|
10840
10545
|
jsonl2(ctx, parsed, {
|
|
@@ -10853,7 +10558,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10853
10558
|
}
|
|
10854
10559
|
if (current.hasMore) continue;
|
|
10855
10560
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10856
|
-
return
|
|
10561
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10857
10562
|
}
|
|
10858
10563
|
await sleep(
|
|
10859
10564
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -12044,7 +11749,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
12044
11749
|
return out;
|
|
12045
11750
|
}
|
|
12046
11751
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
12047
|
-
function
|
|
11752
|
+
function report3(ctx, impacts) {
|
|
12048
11753
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
12049
11754
|
ctx.out.log(
|
|
12050
11755
|
`${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
|
|
@@ -12082,7 +11787,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
12082
11787
|
}
|
|
12083
11788
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
12084
11789
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
12085
|
-
|
|
11790
|
+
report3(ctx, impacts);
|
|
12086
11791
|
}
|
|
12087
11792
|
|
|
12088
11793
|
// src/runbook-lint.ts
|
|
@@ -12582,7 +12287,6 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
12582
12287
|
}
|
|
12583
12288
|
|
|
12584
12289
|
// src/security-command-context.ts
|
|
12585
|
-
import { createInterface } from "readline/promises";
|
|
12586
12290
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
12587
12291
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
12588
12292
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -12601,21 +12305,11 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
12601
12305
|
cfg,
|
|
12602
12306
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12603
12307
|
doFetch,
|
|
12604
|
-
stdout
|
|
12308
|
+
stdout,
|
|
12309
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12605
12310
|
);
|
|
12606
12311
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
12607
12312
|
}
|
|
12608
|
-
async function interactiveConfirmation(message2, dependencies) {
|
|
12609
|
-
if (dependencies.confirm) return dependencies.confirm(message2);
|
|
12610
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
12611
|
-
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
12612
|
-
try {
|
|
12613
|
-
const answer = await prompt.question(`${message2} [y/N] `);
|
|
12614
|
-
return /^y(?:es)?$/i.test(answer.trim());
|
|
12615
|
-
} finally {
|
|
12616
|
-
prompt.close();
|
|
12617
|
-
}
|
|
12618
|
-
}
|
|
12619
12313
|
function requiredSecurityPositional(parsed, index, label) {
|
|
12620
12314
|
const value2 = parsed.positionals[index];
|
|
12621
12315
|
if (!value2) throw new Error(`${label} is required`);
|
|
@@ -12681,31 +12375,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
12681
12375
|
url.searchParams.set("job", job.jobId);
|
|
12682
12376
|
out.log(` Studio: ${url.toString()}`);
|
|
12683
12377
|
}
|
|
12684
|
-
function printHostedReport(out,
|
|
12685
|
-
out.log(`security report ${
|
|
12686
|
-
out.log(` coverage: ${
|
|
12687
|
-
out.log(` findings: confirmed=${
|
|
12688
|
-
out.log(` discovery: ${
|
|
12689
|
-
out.log(` validation: ${
|
|
12690
|
-
for (const finding of
|
|
12378
|
+
function printHostedReport(out, report4) {
|
|
12379
|
+
out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
|
|
12380
|
+
out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
12381
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
|
|
12382
|
+
out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
|
|
12383
|
+
out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
|
|
12384
|
+
for (const finding of report4.findings) {
|
|
12691
12385
|
const location = finding.locations[0];
|
|
12692
12386
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
12693
12387
|
}
|
|
12694
|
-
for (const limitation of
|
|
12388
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
12695
12389
|
}
|
|
12696
|
-
function enforceHostedReportGate(
|
|
12390
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
12697
12391
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12698
12392
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12699
12393
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12700
12394
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
12701
|
-
const confirmed =
|
|
12702
|
-
const leads = failOnCandidates ?
|
|
12703
|
-
const incomplete =
|
|
12395
|
+
const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
12396
|
+
const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
12397
|
+
const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
12704
12398
|
if (confirmed.length || leads.length || incomplete) {
|
|
12705
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
12399
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
|
|
12706
12400
|
}
|
|
12707
12401
|
if (emitSuccess) {
|
|
12708
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
12402
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
|
|
12709
12403
|
}
|
|
12710
12404
|
}
|
|
12711
12405
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -12809,13 +12503,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
12809
12503
|
}
|
|
12810
12504
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
12811
12505
|
}
|
|
12812
|
-
const
|
|
12506
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
12813
12507
|
if (parsed.options.json === true) {
|
|
12814
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
12508
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
|
|
12815
12509
|
} else {
|
|
12816
|
-
printHostedReport(context.stdout,
|
|
12510
|
+
printHostedReport(context.stdout, report4);
|
|
12817
12511
|
}
|
|
12818
|
-
enforceHostedReportGate(
|
|
12512
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
12819
12513
|
}
|
|
12820
12514
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
12821
12515
|
if (parsed.options.source === true) {
|
|
@@ -12876,19 +12570,20 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
12876
12570
|
cfg,
|
|
12877
12571
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12878
12572
|
doFetch,
|
|
12879
|
-
out
|
|
12573
|
+
out,
|
|
12574
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12880
12575
|
);
|
|
12881
12576
|
}
|
|
12882
12577
|
});
|
|
12883
12578
|
enforceLocalGate(result.report, parsed);
|
|
12884
12579
|
}
|
|
12885
|
-
function enforceLocalGate(
|
|
12580
|
+
function enforceLocalGate(report4, parsed) {
|
|
12886
12581
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12887
12582
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12888
12583
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12889
|
-
const confirmed = findingsAtOrAbove(
|
|
12890
|
-
const leads = failOnCandidates ? findingsAtOrAbove(
|
|
12891
|
-
const incomplete =
|
|
12584
|
+
const confirmed = findingsAtOrAbove(report4, failOn);
|
|
12585
|
+
const leads = failOnCandidates ? findingsAtOrAbove(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
12586
|
+
const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
12892
12587
|
if (confirmed.length || leads.length || incomplete) {
|
|
12893
12588
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
12894
12589
|
}
|
|
@@ -12927,9 +12622,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
12927
12622
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
12928
12623
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
12929
12624
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
12930
|
-
const
|
|
12931
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
12932
|
-
else printHostedReport(context.stdout,
|
|
12625
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId });
|
|
12626
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
|
|
12627
|
+
else printHostedReport(context.stdout, report4);
|
|
12933
12628
|
return;
|
|
12934
12629
|
}
|
|
12935
12630
|
if (sub !== "run") {
|
|
@@ -12943,35 +12638,24 @@ async function githubSecurityCommand(parsed, dependencies) {
|
|
|
12943
12638
|
const action2 = parsed.positionals[2];
|
|
12944
12639
|
if (action2 === "disconnect") {
|
|
12945
12640
|
assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
|
|
12946
|
-
const context2 = await hostedSecurityContext(parsed, dependencies);
|
|
12947
12641
|
const sourceId = requiredString(parsed.options.source, "--source");
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12642
|
+
return requireStudioHuman(
|
|
12643
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12644
|
+
`disconnecting GitHub security source ${sourceId}`,
|
|
12645
|
+
"security",
|
|
12646
|
+
stringOpt(parsed.options.env)
|
|
12951
12647
|
);
|
|
12952
|
-
if (!confirmed) {
|
|
12953
|
-
throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
|
|
12954
|
-
}
|
|
12955
|
-
await disconnectGitHubSecuritySource({ ...context2, sourceId });
|
|
12956
|
-
context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
|
|
12957
|
-
return;
|
|
12958
12648
|
}
|
|
12959
12649
|
if (action2 !== "connect") {
|
|
12960
12650
|
throw new Error('unknown security github command. Try "odla-ai security github connect".');
|
|
12961
12651
|
}
|
|
12962
12652
|
assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
openInstallUrl: dependencies.openUrl ?? openUrl,
|
|
12970
|
-
wait: dependencies.pollWait,
|
|
12971
|
-
stdout: context.stdout
|
|
12972
|
-
});
|
|
12973
|
-
context.stdout.log(`github: connected ${connection.repository ?? repository ?? "app repository"} (${connection.sourceId ?? "source pending"})`);
|
|
12974
|
-
context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
|
|
12653
|
+
await requireStudioHuman(
|
|
12654
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12655
|
+
"connecting the GitHub security repository",
|
|
12656
|
+
"security",
|
|
12657
|
+
stringOpt(parsed.options.env)
|
|
12658
|
+
);
|
|
12975
12659
|
}
|
|
12976
12660
|
async function listSecuritySources(parsed, dependencies) {
|
|
12977
12661
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
|
|
@@ -13222,4 +12906,4 @@ export {
|
|
|
13222
12906
|
exitCodeFor,
|
|
13223
12907
|
runCli
|
|
13224
12908
|
};
|
|
13225
|
-
//# sourceMappingURL=chunk-
|
|
12909
|
+
//# sourceMappingURL=chunk-ECRBOR66.js.map
|