@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.
@@ -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
- const { ctx, out } = await lifecycleContext(options);
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 report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
7821
- await writeSecurityArtifacts(output, report5);
7822
- const reportDigest = await securityFingerprint(report5);
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: report5.coverageStatus,
7826
- confirmed: report5.metrics.confirmed,
7827
- candidates: report5.metrics.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, report5, output);
7830
- return Object.freeze({ report: report5, run: hosted.run, output });
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, report5, output) {
7857
- const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
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: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
7862
- if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
7863
- out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
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", "email"], action2 === "jobs" ? 2 : 3);
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 = await getDeveloperToken(
8437
- cfg,
8438
- {
8439
- configPath: cfg.configPath,
8440
- token: stringOpt(parsed.options.token),
8441
- email: stringOpt(parsed.options.email),
8442
- open: false
8443
- },
8444
- doFetch,
8445
- out
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
- const cfg = await loadProjectConfig(options.configPath);
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
- const token = await getDeveloperToken(
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
- report(options, await ownersRequest("GET", "", options));
8564
+ await requireStudioHuman(options.configPath, "listing app owners", "app");
8670
8565
  }
8671
8566
  async function ownersAdd(email, options) {
8672
- const owners = await ownersRequest("POST", "", options, { email });
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
- let ownerId = target;
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
- const cfg = await loadProjectConfig(options.configPath);
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
- const out = options.stdout ?? console;
8804
- const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
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
- const out = options.stdout ?? console;
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
- const out = options.stdout ?? console;
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] [--email <odla-account>] --yes
9567
- odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
9568
- odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
9569
- odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
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
- odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
9572
- odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
9573
- odla-ai app promote [--dry-run] [--json] --yes
9574
- odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
9575
- odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
9576
- odla-ai app owners add <email> [--email <odla-account>] [--json]
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] [--email <email>] [--json]
9613
- odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
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] [--email <odla-account>] [--no-open]
9650
- odla-ai security github disconnect --source <id> [--env dev] [--yes]
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 = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
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 report2(ctx, parsed, {
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 report2(ctx, parsed, { found: false, cursor });
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 report2(ctx, parsed, result) {
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 report3(ctx, parsed, result) {
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 report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
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 report3(ctx, parsed, { found: true, cursor, events: matching });
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 report3(ctx, parsed, { found: false, cursor });
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 report4(ctx, impacts) {
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
- report4(ctx, impacts);
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, report5) {
12685
- out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
12686
- out.log(` coverage: ${report5.coverageStatus} cells=${report5.metrics.coverageCells} shallow=${report5.metrics.shallowCells} blocked=${report5.metrics.blockedCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
12687
- out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
12688
- out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
12689
- out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
12690
- for (const finding of report5.findings) {
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 report5.limitations) out.log(` limitation: ${limitation}`);
12388
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
12695
12389
  }
12696
- function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
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 = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12702
- const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12703
- const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
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 ${report5.coverageStatus}` : ""}`);
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 ${report5.coverageStatus}. This is not proof that the application is secure.`);
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 report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
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: report5 }, null, 2));
12508
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
12815
12509
  } else {
12816
- printHostedReport(context.stdout, report5);
12510
+ printHostedReport(context.stdout, report4);
12817
12511
  }
12818
- enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
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(report5, parsed) {
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(report5, failOn);
12890
- const leads = failOnCandidates ? findingsAtOrAbove(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12891
- const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
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 report5 = await getHostedSecurityReport({ ...context, jobId });
12931
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
12932
- else printHostedReport(context.stdout, report5);
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
- const confirmed = parsed.options.yes === true || await interactiveConfirmation(
12949
- `Disconnect GitHub security source ${sourceId} from ${context2.appId}/${context2.env}?`,
12950
- dependencies
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
- const context = await hostedSecurityContext(parsed, dependencies);
12964
- const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin).catch(() => void 0);
12965
- const connection = await connectGitHubSecuritySource({
12966
- ...context,
12967
- ...repository === void 0 ? {} : { repository },
12968
- open: parsed.options.open !== false,
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-Y7WFLSTN.js.map
12909
+ //# sourceMappingURL=chunk-ECRBOR66.js.map