@odla-ai/cli 0.27.10 → 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 +244 -617
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-OGZELVS5.js → chunk-ECRBOR66.js} +128 -435
- package/dist/chunk-ECRBOR66.js.map +1 -0
- package/dist/index.cjs +244 -550
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-OGZELVS5.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);
|
|
@@ -1546,7 +1550,7 @@ async function calendarCalendars(options) {
|
|
|
1546
1550
|
return calendars;
|
|
1547
1551
|
}
|
|
1548
1552
|
async function calendarConnect(options) {
|
|
1549
|
-
const { cfg, ctx, out } = await lifecycleContext(options);
|
|
1553
|
+
const { cfg, ctx, out } = await lifecycleContext(options, ["app.manage"]);
|
|
1550
1554
|
productionConsent(ctx.env, options.yes, "connect calendar");
|
|
1551
1555
|
const page2 = calendarBookingPageUrl(cfg, ctx.env);
|
|
1552
1556
|
const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
|
|
@@ -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;
|
|
@@ -1572,7 +1573,7 @@ async function ensureCalendarConnected(ctx, options) {
|
|
|
1572
1573
|
const connectOptions = connectionOptions(options, out);
|
|
1573
1574
|
return await continueConnectedCalendar(ctx, current, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
1574
1575
|
}
|
|
1575
|
-
async function lifecycleContext(options) {
|
|
1576
|
+
async function lifecycleContext(options, optionalProjectCapabilities = []) {
|
|
1576
1577
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1577
1578
|
if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
|
|
1578
1579
|
const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
|
|
@@ -1590,7 +1591,8 @@ async function lifecycleContext(options) {
|
|
|
1590
1591
|
openApprovalUrl: options.openConsentUrl
|
|
1591
1592
|
},
|
|
1592
1593
|
doFetch,
|
|
1593
|
-
out
|
|
1594
|
+
out,
|
|
1595
|
+
{ optionalProjectCapabilities }
|
|
1594
1596
|
);
|
|
1595
1597
|
return { cfg, ctx: { platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch }, out };
|
|
1596
1598
|
}
|
|
@@ -1800,11 +1802,17 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
|
1800
1802
|
if (res.ok || res.status === 404) return;
|
|
1801
1803
|
if (res.status === 403) {
|
|
1802
1804
|
const detail = await safeText4(res);
|
|
1803
|
-
|
|
1805
|
+
const code = errorCode(detail);
|
|
1806
|
+
if (code === "human_session_required") {
|
|
1804
1807
|
throw new Error(
|
|
1805
1808
|
`${env}: odla-db rejected the provision credential before checking ownership for "${cfg.app.id}" (tenant ${tenantId}): human_session_required. Retrying or changing app owners will not help; the deployed odla-db must accept owner-approved app.manage credentials on provisioning routes`
|
|
1806
1809
|
);
|
|
1807
1810
|
}
|
|
1811
|
+
if (code === "provision_approval_required") {
|
|
1812
|
+
throw new Error(
|
|
1813
|
+
`${env}: the agent credential does not carry the owner-reviewed app.manage grant required to provision "${cfg.app.id}" (tenant ${tenantId}). The human owner id on the token is accountability, not agent authority. Discard the cached or supplied token and run this command with the current CLI to approve one fresh exact-project provisioning handshake`
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1808
1816
|
throw new Error(
|
|
1809
1817
|
`${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; re-run provision with a fresh owner-approved provision handshake. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
|
|
1810
1818
|
);
|
|
@@ -3319,7 +3327,9 @@ async function secretsSet(options) {
|
|
|
3319
3327
|
throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
|
|
3320
3328
|
}
|
|
3321
3329
|
const { cfg, tenantId, value: value2, doFetch, out } = await resolveVaultWrite(options);
|
|
3322
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out
|
|
3330
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
3331
|
+
optionalProjectCapabilities: ["app.manage"]
|
|
3332
|
+
});
|
|
3323
3333
|
try {
|
|
3324
3334
|
await putSecret({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
|
|
3325
3335
|
} catch (err) {
|
|
@@ -3336,7 +3346,9 @@ async function secretsSetClerkKey(options) {
|
|
|
3336
3346
|
if (value2.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
3337
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)`);
|
|
3338
3348
|
}
|
|
3339
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out
|
|
3349
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
3350
|
+
optionalProjectCapabilities: ["app.manage"]
|
|
3351
|
+
});
|
|
3340
3352
|
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
|
|
3341
3353
|
method: "POST",
|
|
3342
3354
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -7808,17 +7820,17 @@ async function runHostedSecurity(options) {
|
|
|
7808
7820
|
allowNetwork: false
|
|
7809
7821
|
}
|
|
7810
7822
|
});
|
|
7811
|
-
const
|
|
7812
|
-
await writeSecurityArtifacts(output,
|
|
7813
|
-
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);
|
|
7814
7826
|
await hosted.complete({
|
|
7815
7827
|
reportDigest,
|
|
7816
|
-
coverageStatus:
|
|
7817
|
-
confirmed:
|
|
7818
|
-
candidates:
|
|
7828
|
+
coverageStatus: report4.coverageStatus,
|
|
7829
|
+
confirmed: report4.metrics.confirmed,
|
|
7830
|
+
candidates: report4.metrics.candidates
|
|
7819
7831
|
}, { signal: options.signal });
|
|
7820
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
7821
|
-
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 });
|
|
7822
7834
|
}
|
|
7823
7835
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
7824
7836
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -7844,14 +7856,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
7844
7856
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
7845
7857
|
return { ...profile, maxHuntTasks };
|
|
7846
7858
|
}
|
|
7847
|
-
function printSummary(out, appId, env, run,
|
|
7848
|
-
const complete =
|
|
7859
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
7860
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
7849
7861
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
7850
7862
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
7851
7863
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
7852
|
-
out.log(` coverage: ${
|
|
7853
|
-
if (
|
|
7854
|
-
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}`);
|
|
7855
7867
|
out.log(` report: ${resolve9(output, "REPORT.md")}`);
|
|
7856
7868
|
}
|
|
7857
7869
|
function formatBudget(usage) {
|
|
@@ -8416,7 +8428,7 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
8416
8428
|
if (action2 !== "jobs" && action2 !== "retry") {
|
|
8417
8429
|
throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
|
|
8418
8430
|
}
|
|
8419
|
-
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"
|
|
8431
|
+
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
|
|
8420
8432
|
if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
|
|
8421
8433
|
throw new Error('--state and --limit are supported only by "agent jobs"');
|
|
8422
8434
|
}
|
|
@@ -8424,17 +8436,17 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
8424
8436
|
const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
|
|
8425
8437
|
const doFetch = deps.fetch ?? fetch;
|
|
8426
8438
|
const out = deps.stdout ?? console;
|
|
8427
|
-
const credential2 =
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
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
|
+
}
|
|
8438
8450
|
const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
|
|
8439
8451
|
const headers = { authorization: `Bearer ${credential2}` };
|
|
8440
8452
|
if (action2 === "retry") {
|
|
@@ -8491,53 +8503,14 @@ function errorMessage(body) {
|
|
|
8491
8503
|
}
|
|
8492
8504
|
|
|
8493
8505
|
// src/app-export.ts
|
|
8494
|
-
import { createWriteStream } from "fs";
|
|
8495
|
-
import { Readable } from "stream";
|
|
8496
|
-
import { pipeline } from "stream/promises";
|
|
8497
8506
|
async function appExport(options) {
|
|
8498
|
-
|
|
8499
|
-
const out = options.stdout ?? console;
|
|
8500
|
-
const doFetch = options.fetch ?? fetch;
|
|
8501
|
-
const { tenant } = resolveTenant(cfg, options.env);
|
|
8502
|
-
const token = await getDeveloperToken(
|
|
8503
|
-
cfg,
|
|
8504
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8505
|
-
doFetch,
|
|
8506
|
-
out
|
|
8507
|
-
);
|
|
8508
|
-
const auth = { authorization: `Bearer ${token}` };
|
|
8509
|
-
const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
|
|
8510
|
-
if (options.fresh) {
|
|
8511
|
-
const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
|
|
8512
|
-
const body = await res.json().catch(() => ({}));
|
|
8513
|
-
if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
|
|
8514
|
-
}
|
|
8515
|
-
const list = await doFetch(`${base}/backups`, { headers: auth });
|
|
8516
|
-
if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
|
|
8517
|
-
const { backups } = await list.json();
|
|
8518
|
-
const newest = backups[0];
|
|
8519
|
-
if (!newest) {
|
|
8520
|
-
throw new Error(
|
|
8521
|
-
`${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)`
|
|
8522
|
-
);
|
|
8523
|
-
}
|
|
8524
|
-
const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
|
|
8525
|
-
if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
|
|
8526
|
-
const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
|
|
8527
|
-
await pipeline(Readable.fromWeb(download.body), createWriteStream(file));
|
|
8528
|
-
if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
|
|
8529
|
-
else {
|
|
8530
|
-
out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
|
|
8531
|
-
out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
|
|
8532
|
-
}
|
|
8533
|
-
return { file, backup: newest };
|
|
8507
|
+
return requireStudioHuman(options.configPath, "database export", "database", options.env);
|
|
8534
8508
|
}
|
|
8535
8509
|
|
|
8536
8510
|
// src/app-import.ts
|
|
8537
8511
|
import { readFileSync as readFileSync9 } from "fs";
|
|
8538
8512
|
import {
|
|
8539
8513
|
buildImportOps,
|
|
8540
|
-
importMutationId,
|
|
8541
8514
|
parseImport,
|
|
8542
8515
|
planImportChunks
|
|
8543
8516
|
} from "@odla-ai/db/import";
|
|
@@ -8556,7 +8529,6 @@ async function appImport(options) {
|
|
|
8556
8529
|
const cfg = await loadProjectConfig(options.configPath);
|
|
8557
8530
|
const out = options.stdout ?? console;
|
|
8558
8531
|
const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
|
|
8559
|
-
const doFetch = options.fetch ?? fetch;
|
|
8560
8532
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
8561
8533
|
const text2 = options.file === "-" ? (options.readStdin ?? (() => readFileSync9(0, "utf8")))() : readFileSync9(options.file, "utf8");
|
|
8562
8534
|
const { format, sources } = parseImport(text2, options.ns);
|
|
@@ -8584,100 +8556,18 @@ ${detail}${more}`);
|
|
|
8584
8556
|
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
8585
8557
|
return result;
|
|
8586
8558
|
}
|
|
8587
|
-
|
|
8588
|
-
cfg,
|
|
8589
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8590
|
-
doFetch,
|
|
8591
|
-
out
|
|
8592
|
-
);
|
|
8593
|
-
const runId = crypto.randomUUID();
|
|
8594
|
-
const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
|
|
8595
|
-
for (const chunk of chunks) {
|
|
8596
|
-
const res = await doFetch(url, {
|
|
8597
|
-
method: "POST",
|
|
8598
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
8599
|
-
body: JSON.stringify({ mutationId: importMutationId(runId, chunk.index), ops: chunk.ops })
|
|
8600
|
-
});
|
|
8601
|
-
const body = await res.json().catch(() => ({}));
|
|
8602
|
-
if (!res.ok) {
|
|
8603
|
-
const code = body.error?.code ? ` (${body.error.code})` : "";
|
|
8604
|
-
throw new Error(
|
|
8605
|
-
`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.`
|
|
8606
|
-
);
|
|
8607
|
-
}
|
|
8608
|
-
result.committed += chunk.ops.length;
|
|
8609
|
-
if (typeof body.txId === "number") result.txIds.push(body.txId);
|
|
8610
|
-
if (body.duplicate) result.duplicate++;
|
|
8611
|
-
}
|
|
8612
|
-
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
8613
|
-
else {
|
|
8614
|
-
const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
|
|
8615
|
-
out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
|
|
8616
|
-
}
|
|
8617
|
-
return result;
|
|
8559
|
+
return requireStudioHuman(options.configPath, "database import", "database", options.env);
|
|
8618
8560
|
}
|
|
8619
8561
|
|
|
8620
8562
|
// src/app-owners.ts
|
|
8621
|
-
var sink = (options) => options.stdout ?? console;
|
|
8622
|
-
async function ownersRequest(method, suffix, options, body) {
|
|
8623
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
8624
|
-
const doFetch = options.fetch ?? fetch;
|
|
8625
|
-
const token = await getDeveloperToken(
|
|
8626
|
-
cfg,
|
|
8627
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8628
|
-
doFetch,
|
|
8629
|
-
sink(options)
|
|
8630
|
-
);
|
|
8631
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
|
|
8632
|
-
method,
|
|
8633
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
8634
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
8635
|
-
});
|
|
8636
|
-
const data = await res.json().catch(() => ({}));
|
|
8637
|
-
if (!res.ok) {
|
|
8638
|
-
throw new Error(
|
|
8639
|
-
`owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
8640
|
-
);
|
|
8641
|
-
}
|
|
8642
|
-
return data.owners ?? [];
|
|
8643
|
-
}
|
|
8644
|
-
function report(options, owners, headline) {
|
|
8645
|
-
const out = sink(options);
|
|
8646
|
-
if (options.json === true) {
|
|
8647
|
-
out.log(JSON.stringify(owners, null, 2));
|
|
8648
|
-
return;
|
|
8649
|
-
}
|
|
8650
|
-
if (headline) out.log(headline);
|
|
8651
|
-
out.log(`owners (${owners.length}):`);
|
|
8652
|
-
for (const o of owners) {
|
|
8653
|
-
const name = o.email?.trim() || "Unnamed member";
|
|
8654
|
-
out.log(
|
|
8655
|
-
` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
|
|
8656
|
-
);
|
|
8657
|
-
}
|
|
8658
|
-
}
|
|
8659
8563
|
async function ownersList(options) {
|
|
8660
|
-
|
|
8564
|
+
await requireStudioHuman(options.configPath, "listing app owners", "app");
|
|
8661
8565
|
}
|
|
8662
8566
|
async function ownersAdd(email, options) {
|
|
8663
|
-
|
|
8664
|
-
report(
|
|
8665
|
-
options,
|
|
8666
|
-
owners,
|
|
8667
|
-
`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).`
|
|
8668
|
-
);
|
|
8567
|
+
await requireStudioHuman(options.configPath, `adding ${email} as an app owner`, "app");
|
|
8669
8568
|
}
|
|
8670
8569
|
async function ownersRemove(target, options) {
|
|
8671
|
-
|
|
8672
|
-
if (target.includes("@")) {
|
|
8673
|
-
const owners2 = await ownersRequest("GET", "", options);
|
|
8674
|
-
const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
|
|
8675
|
-
if (!match) throw new Error(`no co-owner with email ${target}`);
|
|
8676
|
-
if (match.primary) throw new Error("can't remove the primary owner");
|
|
8677
|
-
ownerId = match.ownerId;
|
|
8678
|
-
}
|
|
8679
|
-
const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
|
|
8680
|
-
report(options, owners, `removed ${target}`);
|
|
8570
|
+
await requireStudioHuman(options.configPath, `removing ${target} as an app owner`, "app");
|
|
8681
8571
|
}
|
|
8682
8572
|
async function appOwnersCommand(parsed, dependencies = {}) {
|
|
8683
8573
|
const sub = parsed.positionals[2] ?? "list";
|
|
@@ -8709,35 +8599,9 @@ async function appOwnersCommand(parsed, dependencies = {}) {
|
|
|
8709
8599
|
|
|
8710
8600
|
// src/app-rename.ts
|
|
8711
8601
|
async function appRename(name, options) {
|
|
8712
|
-
const out = options.stdout ?? console;
|
|
8713
8602
|
const trimmed = name.trim();
|
|
8714
8603
|
if (!trimmed) throw new Error('"app rename" needs a name \u2014 try `odla-ai app rename "Acme Storefront"`.');
|
|
8715
|
-
|
|
8716
|
-
const doFetch = options.fetch ?? fetch;
|
|
8717
|
-
const token = await getDeveloperToken(
|
|
8718
|
-
cfg,
|
|
8719
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8720
|
-
doFetch,
|
|
8721
|
-
out
|
|
8722
|
-
);
|
|
8723
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/name`, {
|
|
8724
|
-
method: "PUT",
|
|
8725
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
8726
|
-
body: JSON.stringify({ name: trimmed })
|
|
8727
|
-
});
|
|
8728
|
-
const data = await res.json().catch(() => ({}));
|
|
8729
|
-
if (!res.ok || !data.app) {
|
|
8730
|
-
throw new Error(
|
|
8731
|
-
`rename failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
8732
|
-
);
|
|
8733
|
-
}
|
|
8734
|
-
if (options.json === true) {
|
|
8735
|
-
out.log(JSON.stringify(data.app, null, 2));
|
|
8736
|
-
return;
|
|
8737
|
-
}
|
|
8738
|
-
out.log(`renamed ${data.app.appId} \u2192 "${data.app.name}"`);
|
|
8739
|
-
out.log("The app id is unchanged, so credentials, tenants, and URLs keep working.");
|
|
8740
|
-
out.log(`Update the "name" in ${cfg.configPath} to match.`);
|
|
8604
|
+
await requireStudioHuman(options.configPath, `renaming the app to "${trimmed}"`, "app");
|
|
8741
8605
|
}
|
|
8742
8606
|
async function appRenameCommand(parsed, dependencies = {}) {
|
|
8743
8607
|
assertArgs(parsed, ["config", "token", "email", "json"], parsed.positionals.length);
|
|
@@ -8753,172 +8617,23 @@ async function appRenameCommand(parsed, dependencies = {}) {
|
|
|
8753
8617
|
}
|
|
8754
8618
|
|
|
8755
8619
|
// src/app-transfer.ts
|
|
8756
|
-
function endpointsFor(verb, tenants) {
|
|
8757
|
-
const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
|
|
8758
|
-
const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
|
|
8759
|
-
if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
|
|
8760
|
-
return { ...up, mode: "cutover" };
|
|
8761
|
-
}
|
|
8762
|
-
async function api(cfg, token, path, init, doFetch) {
|
|
8763
|
-
const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
|
|
8764
|
-
...init,
|
|
8765
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
|
|
8766
|
-
});
|
|
8767
|
-
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
8768
|
-
}
|
|
8769
|
-
function describe(side) {
|
|
8770
|
-
if (!side.exists) return "not provisioned";
|
|
8771
|
-
if (side.maxTx === 0) return "empty \u2014 never written";
|
|
8772
|
-
const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
|
|
8773
|
-
const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
|
|
8774
|
-
return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
|
|
8775
|
-
}
|
|
8776
|
-
function printPlan2(out, verb, pre, opts) {
|
|
8777
|
-
const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
|
|
8778
|
-
out.log(`${verb} (${arrow})`);
|
|
8779
|
-
out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
|
|
8780
|
-
out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
|
|
8781
|
-
if (verb === "promote") {
|
|
8782
|
-
out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
|
|
8783
|
-
} else {
|
|
8784
|
-
out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
|
|
8785
|
-
out.log(` stays put: ${pre.staysPut.join(", ")}`);
|
|
8786
|
-
const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
|
|
8787
|
-
out.log(` identity: ${identity}`);
|
|
8788
|
-
out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
|
|
8789
|
-
}
|
|
8790
|
-
for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
|
|
8791
|
-
}
|
|
8792
8620
|
async function appTransfer(options) {
|
|
8793
8621
|
const cfg = await loadProjectConfig(options.configPath);
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
const doFetch = options.fetch ?? fetch;
|
|
8797
|
-
const tenants = bothTenants(cfg);
|
|
8798
|
-
const route2 = endpointsFor(options.verb, tenants);
|
|
8799
|
-
const token = await getDeveloperToken(
|
|
8800
|
-
cfg,
|
|
8801
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8802
|
-
doFetch,
|
|
8803
|
-
out
|
|
8804
|
-
);
|
|
8805
|
-
const pre = await api(
|
|
8806
|
-
cfg,
|
|
8807
|
-
token,
|
|
8808
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
|
|
8809
|
-
void 0,
|
|
8810
|
-
doFetch
|
|
8811
|
-
);
|
|
8812
|
-
if (pre.status !== 200) {
|
|
8813
|
-
throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
|
|
8814
|
-
}
|
|
8815
|
-
const plan = pre.body;
|
|
8816
|
-
printPlan2({ log: say }, options.verb, plan, options);
|
|
8817
|
-
if (options.verb === "go-live" && !plan.targetEmpty) {
|
|
8818
|
-
throw new Error(
|
|
8819
|
-
`${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.`
|
|
8820
|
-
);
|
|
8821
|
-
}
|
|
8822
|
-
if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
|
|
8823
|
-
if (options.dryRun || options.yes !== true) {
|
|
8824
|
-
say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
|
|
8825
|
-
if (options.json) out.log(JSON.stringify(plan, null, 2));
|
|
8826
|
-
return { ok: true, plan };
|
|
8827
|
-
}
|
|
8828
|
-
if (options.verb === "promote") {
|
|
8829
|
-
const res2 = await api(
|
|
8830
|
-
cfg,
|
|
8831
|
-
token,
|
|
8832
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
|
|
8833
|
-
{ method: "POST", body: JSON.stringify({ from: "dev" }) },
|
|
8834
|
-
doFetch
|
|
8835
|
-
);
|
|
8836
|
-
if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
|
|
8837
|
-
if (options.json) out.log(JSON.stringify(res2.body, null, 2));
|
|
8838
|
-
else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
|
|
8839
|
-
return { ok: true, plan, result: res2.body };
|
|
8840
|
-
}
|
|
8841
|
-
const res = await api(
|
|
8842
|
-
cfg,
|
|
8843
|
-
token,
|
|
8844
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
|
|
8845
|
-
{
|
|
8846
|
-
method: "POST",
|
|
8847
|
-
body: JSON.stringify({
|
|
8848
|
-
from: route2.from,
|
|
8849
|
-
mode: route2.mode,
|
|
8850
|
-
...options.includeIdentity ? { includeUsers: true } : {},
|
|
8851
|
-
...options.includeFiles ? { includeFiles: true } : {}
|
|
8852
|
-
})
|
|
8853
|
-
},
|
|
8854
|
-
doFetch
|
|
8855
|
-
);
|
|
8856
|
-
if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
8857
|
-
say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
|
|
8858
|
-
if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
|
|
8859
|
-
if (options.json) out.log(JSON.stringify(res.body, null, 2));
|
|
8860
|
-
return { ok: true, plan, result: res.body };
|
|
8861
|
-
}
|
|
8862
|
-
async function copyFiles(cfg, token, route2, doFetch, out) {
|
|
8863
|
-
for (let attempt = 1; attempt <= 20; attempt++) {
|
|
8864
|
-
const res = await api(
|
|
8865
|
-
cfg,
|
|
8866
|
-
token,
|
|
8867
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
|
|
8868
|
-
{ method: "POST", body: JSON.stringify({ from: route2.from }) },
|
|
8869
|
-
doFetch
|
|
8870
|
-
);
|
|
8871
|
-
if (res.status === 200) {
|
|
8872
|
-
out.log(`${route2.target}: files copied`);
|
|
8873
|
-
return;
|
|
8874
|
-
}
|
|
8875
|
-
if (!res.body.error?.retry) {
|
|
8876
|
-
throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
8877
|
-
}
|
|
8878
|
-
out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
|
|
8879
|
-
}
|
|
8880
|
-
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");
|
|
8881
8624
|
}
|
|
8882
8625
|
|
|
8883
8626
|
// src/app-lifecycle.ts
|
|
8884
|
-
async function lifecycleCall(action2, options) {
|
|
8885
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
8886
|
-
const out = options.stdout ?? console;
|
|
8887
|
-
const doFetch = options.fetch ?? fetch;
|
|
8888
|
-
const token = await getDeveloperToken(
|
|
8889
|
-
cfg,
|
|
8890
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
8891
|
-
doFetch,
|
|
8892
|
-
out
|
|
8893
|
-
);
|
|
8894
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
|
|
8895
|
-
method: "POST",
|
|
8896
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
8897
|
-
});
|
|
8898
|
-
const body = await res.json().catch(() => ({}));
|
|
8899
|
-
if (!res.ok || !body.ok) {
|
|
8900
|
-
throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
8901
|
-
}
|
|
8902
|
-
return body;
|
|
8903
|
-
}
|
|
8904
8627
|
async function appArchive(options) {
|
|
8905
8628
|
if (options.yes !== true) {
|
|
8906
8629
|
throw new Error(
|
|
8907
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."
|
|
8908
8631
|
);
|
|
8909
8632
|
}
|
|
8910
|
-
|
|
8911
|
-
const body = await lifecycleCall("archive", options);
|
|
8912
|
-
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
8913
|
-
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
|
|
8914
|
-
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");
|
|
8915
8634
|
}
|
|
8916
8635
|
async function appRestore(options) {
|
|
8917
|
-
|
|
8918
|
-
const body = await lifecycleCall("restore", options);
|
|
8919
|
-
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
8920
|
-
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
|
|
8921
|
-
else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
|
|
8636
|
+
await requireStudioHuman(options.configPath, "app restore", "app");
|
|
8922
8637
|
}
|
|
8923
8638
|
async function appCommand(parsed, dependencies = {}) {
|
|
8924
8639
|
const sub = parsed.positionals[1];
|
|
@@ -9554,18 +9269,17 @@ Usage:
|
|
|
9554
9269
|
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
9555
9270
|
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
9556
9271
|
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
9557
|
-
odla-ai calendar disconnect [--env dev] [
|
|
9558
|
-
odla-ai app archive [--config odla.config.mjs] [
|
|
9559
|
-
odla-ai app restore [--config odla.config.mjs]
|
|
9560
|
-
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]
|
|
9561
9276
|
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
9562
|
-
|
|
9563
|
-
odla-ai app
|
|
9564
|
-
odla-ai app
|
|
9565
|
-
odla-ai app
|
|
9566
|
-
odla-ai app
|
|
9567
|
-
odla-ai app owners add
|
|
9568
|
-
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]
|
|
9569
9283
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
9570
9284
|
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
9571
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]
|
|
@@ -9600,8 +9314,8 @@ Usage:
|
|
|
9600
9314
|
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
9601
9315
|
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
9602
9316
|
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
9603
|
-
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--
|
|
9604
|
-
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]
|
|
9605
9319
|
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
9606
9320
|
odla-ai context list [--json]
|
|
9607
9321
|
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
@@ -9637,8 +9351,8 @@ Usage:
|
|
|
9637
9351
|
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
9638
9352
|
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
9639
9353
|
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
9640
|
-
odla-ai security github connect [--repo owner/name] [--env dev]
|
|
9641
|
-
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]
|
|
9642
9356
|
odla-ai security plan [--env dev] [--json]
|
|
9643
9357
|
odla-ai security sources [--env dev] [--json]
|
|
9644
9358
|
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
@@ -10149,7 +9863,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10149
9863
|
throw new WatchRemoteError(cursor, error);
|
|
10150
9864
|
}
|
|
10151
9865
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10152
|
-
const result =
|
|
9866
|
+
const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10153
9867
|
throw new WatchTimeoutError(result.cursor);
|
|
10154
9868
|
}
|
|
10155
9869
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -10190,7 +9904,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10190
9904
|
});
|
|
10191
9905
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
10192
9906
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
10193
|
-
return
|
|
9907
|
+
return report(ctx, parsed, {
|
|
10194
9908
|
found: true,
|
|
10195
9909
|
cursor,
|
|
10196
9910
|
events: matching,
|
|
@@ -10217,13 +9931,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10217
9931
|
}
|
|
10218
9932
|
if (page2.hasMore) continue;
|
|
10219
9933
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10220
|
-
return
|
|
9934
|
+
return report(ctx, parsed, { found: false, cursor });
|
|
10221
9935
|
}
|
|
10222
9936
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
10223
9937
|
await sleep(wait2);
|
|
10224
9938
|
}
|
|
10225
9939
|
}
|
|
10226
|
-
function
|
|
9940
|
+
function report(ctx, parsed, result) {
|
|
10227
9941
|
if (ctx.json) {
|
|
10228
9942
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
10229
9943
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -10724,7 +10438,7 @@ function eventLabel(event) {
|
|
|
10724
10438
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
10725
10439
|
return body || event.payload.entityId;
|
|
10726
10440
|
}
|
|
10727
|
-
function
|
|
10441
|
+
function report2(ctx, parsed, result) {
|
|
10728
10442
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
10729
10443
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
10730
10444
|
for (const event of result.events ?? []) {
|
|
@@ -10784,7 +10498,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10784
10498
|
});
|
|
10785
10499
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
10786
10500
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10787
|
-
return
|
|
10501
|
+
return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10788
10502
|
}
|
|
10789
10503
|
const backoff = Math.min(
|
|
10790
10504
|
MAX_BACKOFF_MS2,
|
|
@@ -10825,7 +10539,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10825
10539
|
cursor,
|
|
10826
10540
|
serverTime: current.serverTime
|
|
10827
10541
|
});
|
|
10828
|
-
return
|
|
10542
|
+
return report2(ctx, parsed, { found: true, cursor, events: matching });
|
|
10829
10543
|
}
|
|
10830
10544
|
if (current.events.length > 0) {
|
|
10831
10545
|
jsonl2(ctx, parsed, {
|
|
@@ -10844,7 +10558,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10844
10558
|
}
|
|
10845
10559
|
if (current.hasMore) continue;
|
|
10846
10560
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10847
|
-
return
|
|
10561
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10848
10562
|
}
|
|
10849
10563
|
await sleep(
|
|
10850
10564
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -12035,7 +11749,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
12035
11749
|
return out;
|
|
12036
11750
|
}
|
|
12037
11751
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
12038
|
-
function
|
|
11752
|
+
function report3(ctx, impacts) {
|
|
12039
11753
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
12040
11754
|
ctx.out.log(
|
|
12041
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.`
|
|
@@ -12073,7 +11787,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
12073
11787
|
}
|
|
12074
11788
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
12075
11789
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
12076
|
-
|
|
11790
|
+
report3(ctx, impacts);
|
|
12077
11791
|
}
|
|
12078
11792
|
|
|
12079
11793
|
// src/runbook-lint.ts
|
|
@@ -12573,7 +12287,6 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
12573
12287
|
}
|
|
12574
12288
|
|
|
12575
12289
|
// src/security-command-context.ts
|
|
12576
|
-
import { createInterface } from "readline/promises";
|
|
12577
12290
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
12578
12291
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
12579
12292
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -12592,21 +12305,11 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
12592
12305
|
cfg,
|
|
12593
12306
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12594
12307
|
doFetch,
|
|
12595
|
-
stdout
|
|
12308
|
+
stdout,
|
|
12309
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12596
12310
|
);
|
|
12597
12311
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
12598
12312
|
}
|
|
12599
|
-
async function interactiveConfirmation(message2, dependencies) {
|
|
12600
|
-
if (dependencies.confirm) return dependencies.confirm(message2);
|
|
12601
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
12602
|
-
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
12603
|
-
try {
|
|
12604
|
-
const answer = await prompt.question(`${message2} [y/N] `);
|
|
12605
|
-
return /^y(?:es)?$/i.test(answer.trim());
|
|
12606
|
-
} finally {
|
|
12607
|
-
prompt.close();
|
|
12608
|
-
}
|
|
12609
|
-
}
|
|
12610
12313
|
function requiredSecurityPositional(parsed, index, label) {
|
|
12611
12314
|
const value2 = parsed.positionals[index];
|
|
12612
12315
|
if (!value2) throw new Error(`${label} is required`);
|
|
@@ -12672,31 +12375,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
12672
12375
|
url.searchParams.set("job", job.jobId);
|
|
12673
12376
|
out.log(` Studio: ${url.toString()}`);
|
|
12674
12377
|
}
|
|
12675
|
-
function printHostedReport(out,
|
|
12676
|
-
out.log(`security report ${
|
|
12677
|
-
out.log(` coverage: ${
|
|
12678
|
-
out.log(` findings: confirmed=${
|
|
12679
|
-
out.log(` discovery: ${
|
|
12680
|
-
out.log(` validation: ${
|
|
12681
|
-
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) {
|
|
12682
12385
|
const location = finding.locations[0];
|
|
12683
12386
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
12684
12387
|
}
|
|
12685
|
-
for (const limitation of
|
|
12388
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
12686
12389
|
}
|
|
12687
|
-
function enforceHostedReportGate(
|
|
12390
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
12688
12391
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12689
12392
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12690
12393
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12691
12394
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
12692
|
-
const confirmed =
|
|
12693
|
-
const leads = failOnCandidates ?
|
|
12694
|
-
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;
|
|
12695
12398
|
if (confirmed.length || leads.length || incomplete) {
|
|
12696
|
-
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}` : ""}`);
|
|
12697
12400
|
}
|
|
12698
12401
|
if (emitSuccess) {
|
|
12699
|
-
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.`);
|
|
12700
12403
|
}
|
|
12701
12404
|
}
|
|
12702
12405
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -12800,13 +12503,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
12800
12503
|
}
|
|
12801
12504
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
12802
12505
|
}
|
|
12803
|
-
const
|
|
12506
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
12804
12507
|
if (parsed.options.json === true) {
|
|
12805
|
-
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));
|
|
12806
12509
|
} else {
|
|
12807
|
-
printHostedReport(context.stdout,
|
|
12510
|
+
printHostedReport(context.stdout, report4);
|
|
12808
12511
|
}
|
|
12809
|
-
enforceHostedReportGate(
|
|
12512
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
12810
12513
|
}
|
|
12811
12514
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
12812
12515
|
if (parsed.options.source === true) {
|
|
@@ -12867,19 +12570,20 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
12867
12570
|
cfg,
|
|
12868
12571
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12869
12572
|
doFetch,
|
|
12870
|
-
out
|
|
12573
|
+
out,
|
|
12574
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12871
12575
|
);
|
|
12872
12576
|
}
|
|
12873
12577
|
});
|
|
12874
12578
|
enforceLocalGate(result.report, parsed);
|
|
12875
12579
|
}
|
|
12876
|
-
function enforceLocalGate(
|
|
12580
|
+
function enforceLocalGate(report4, parsed) {
|
|
12877
12581
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12878
12582
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12879
12583
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12880
|
-
const confirmed = findingsAtOrAbove(
|
|
12881
|
-
const leads = failOnCandidates ? findingsAtOrAbove(
|
|
12882
|
-
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;
|
|
12883
12587
|
if (confirmed.length || leads.length || incomplete) {
|
|
12884
12588
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
12885
12589
|
}
|
|
@@ -12918,9 +12622,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
12918
12622
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
12919
12623
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
12920
12624
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
12921
|
-
const
|
|
12922
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
12923
|
-
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);
|
|
12924
12628
|
return;
|
|
12925
12629
|
}
|
|
12926
12630
|
if (sub !== "run") {
|
|
@@ -12934,35 +12638,24 @@ async function githubSecurityCommand(parsed, dependencies) {
|
|
|
12934
12638
|
const action2 = parsed.positionals[2];
|
|
12935
12639
|
if (action2 === "disconnect") {
|
|
12936
12640
|
assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
|
|
12937
|
-
const context2 = await hostedSecurityContext(parsed, dependencies);
|
|
12938
12641
|
const sourceId = requiredString(parsed.options.source, "--source");
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12642
|
+
return requireStudioHuman(
|
|
12643
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12644
|
+
`disconnecting GitHub security source ${sourceId}`,
|
|
12645
|
+
"security",
|
|
12646
|
+
stringOpt(parsed.options.env)
|
|
12942
12647
|
);
|
|
12943
|
-
if (!confirmed) {
|
|
12944
|
-
throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
|
|
12945
|
-
}
|
|
12946
|
-
await disconnectGitHubSecuritySource({ ...context2, sourceId });
|
|
12947
|
-
context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
|
|
12948
|
-
return;
|
|
12949
12648
|
}
|
|
12950
12649
|
if (action2 !== "connect") {
|
|
12951
12650
|
throw new Error('unknown security github command. Try "odla-ai security github connect".');
|
|
12952
12651
|
}
|
|
12953
12652
|
assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
|
|
12954
|
-
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
openInstallUrl: dependencies.openUrl ?? openUrl,
|
|
12961
|
-
wait: dependencies.pollWait,
|
|
12962
|
-
stdout: context.stdout
|
|
12963
|
-
});
|
|
12964
|
-
context.stdout.log(`github: connected ${connection.repository ?? repository ?? "app repository"} (${connection.sourceId ?? "source pending"})`);
|
|
12965
|
-
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
|
+
);
|
|
12966
12659
|
}
|
|
12967
12660
|
async function listSecuritySources(parsed, dependencies) {
|
|
12968
12661
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
|
|
@@ -13213,4 +12906,4 @@ export {
|
|
|
13213
12906
|
exitCodeFor,
|
|
13214
12907
|
runCli
|
|
13215
12908
|
};
|
|
13216
|
-
//# sourceMappingURL=chunk-
|
|
12909
|
+
//# sourceMappingURL=chunk-ECRBOR66.js.map
|