@odla-ai/cli 0.34.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -215,7 +215,7 @@ function approvalLines(prompt) {
215
215
  return lines;
216
216
  }
217
217
  function printApproval(out, prompt) {
218
- for (const line of approvalLines(prompt)) out.error(line);
218
+ for (const line2 of approvalLines(prompt)) out.error(line2);
219
219
  }
220
220
  function reminderLines(prompt) {
221
221
  return [
@@ -321,12 +321,12 @@ function approvalHint(pending) {
321
321
  }
322
322
  function approvalReminder(out, pending, periodMs = 3e4) {
323
323
  const timer = setInterval(() => {
324
- for (const line of reminderLines({
324
+ for (const line2 of reminderLines({
325
325
  userCode: pending.userCode,
326
326
  approvalUrl: pending.approvalUrl,
327
327
  minutesLeft: minutesLeft(pending.expiresAt)
328
328
  }))
329
- out.log(line);
329
+ out.log(line2);
330
330
  }, periodMs);
331
331
  timer.unref?.();
332
332
  return () => clearInterval(timer);
@@ -395,9 +395,9 @@ function mergeCredential(current, update) {
395
395
  function ensureGitignore(rootDir, localPaths = []) {
396
396
  const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
397
397
  const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
398
- const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
398
+ const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
399
399
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
400
- const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
400
+ const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
401
401
  if (missing.length === 0) return;
402
402
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
403
403
  (0, import_node_fs5.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
@@ -429,7 +429,7 @@ function writeDevVars(path, credentials, env, o11y) {
429
429
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
430
430
  }
431
431
  const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
432
- const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
432
+ const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
433
433
  while (retained.at(-1) === "") retained.pop();
434
434
  const prefix = retained.length ? `${retained.join("\n")}
435
435
 
@@ -437,8 +437,8 @@ function writeDevVars(path, credentials, env, o11y) {
437
437
  writePrivateText(path, `${prefix}${lines.join("\n")}
438
438
  `);
439
439
  }
440
- function isManagedDevVar(line) {
441
- const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
440
+ function isManagedDevVar(line2) {
441
+ const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
442
442
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
443
443
  }
444
444
  function writePrivateText(path, text3) {
@@ -2505,7 +2505,7 @@ function chooseIdMode(options, rows) {
2505
2505
  async function appImport(options) {
2506
2506
  const cfg = await loadProjectConfig(options.configPath);
2507
2507
  const out = options.stdout ?? console;
2508
- const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
2508
+ const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
2509
2509
  const { tenant } = resolveTenant(cfg, options.env);
2510
2510
  const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs10.readFileSync)(0, "utf8")))() : (0, import_node_fs10.readFileSync)(options.file, "utf8");
2511
2511
  const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
@@ -2882,7 +2882,7 @@ async function designUnpack(parsed, deps) {
2882
2882
  );
2883
2883
  return;
2884
2884
  }
2885
- for (const line of describeUnpack(result, outDir)) out.log(line);
2885
+ for (const line2 of describeUnpack(result, outDir)) out.log(line2);
2886
2886
  }
2887
2887
  async function brandCommand(parsed, deps) {
2888
2888
  const subject = parsed.positionals[1];
@@ -4969,7 +4969,7 @@ async function doctor(options) {
4969
4969
  out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
4970
4970
  const contract = resolveSecretContract(cfg);
4971
4971
  out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
4972
- for (const line of formatSecretContract(contract)) out.log(line);
4972
+ for (const line2 of formatSecretContract(contract)) out.log(line2);
4973
4973
  if (cfg.services.includes("calendar")) {
4974
4974
  const calendar = cfg.envs.map((env) => {
4975
4975
  const resolved = calendarServiceConfig(cfg, env);
@@ -5387,10 +5387,10 @@ async function secretsStatus(options) {
5387
5387
  }
5388
5388
  const body = await res.json();
5389
5389
  const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
5390
- const report4 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
5391
- if (options.json) out.log(JSON.stringify(report4, null, 2));
5392
- else printReport(report4, out);
5393
- return report4;
5390
+ const report5 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
5391
+ if (options.json) out.log(JSON.stringify(report5, null, 2));
5392
+ else printReport(report5, out);
5393
+ return report5;
5394
5394
  }
5395
5395
  function buildReport(appId, env, tenant, contract, stored) {
5396
5396
  const declared = new Set(contract.map((secret) => secret.name));
@@ -5407,25 +5407,25 @@ function buildReport(appId, env, tenant, contract, stored) {
5407
5407
  const ok = rows.every((row) => row.state !== "missing" || !row.required);
5408
5408
  return { app: appId, env, tenant, secrets: rows, ok };
5409
5409
  }
5410
- function printReport(report4, out) {
5411
- out.log(`${report4.app} (${report4.tenant})`);
5412
- if (report4.secrets.length === 0) {
5410
+ function printReport(report5, out) {
5411
+ out.log(`${report5.app} (${report5.tenant})`);
5412
+ if (report5.secrets.length === 0) {
5413
5413
  out.log(" no secrets declared and none stored");
5414
5414
  return;
5415
5415
  }
5416
- for (const row of report4.secrets) {
5416
+ for (const row of report5.secrets) {
5417
5417
  const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
5418
5418
  const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
5419
5419
  out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
5420
5420
  }
5421
- const missing = report4.secrets.filter((row) => row.state === "missing" && row.required);
5421
+ const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
5422
5422
  if (missing.length) {
5423
5423
  out.log("");
5424
5424
  for (const row of missing) {
5425
- out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report4.env} --stdin"`);
5425
+ out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report5.env} --stdin"`);
5426
5426
  }
5427
5427
  }
5428
- if (report4.secrets.some((row) => row.state === "reserved")) {
5428
+ if (report5.secrets.some((row) => row.state === "reserved")) {
5429
5429
  out.log("");
5430
5430
  out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
5431
5431
  }
@@ -5449,12 +5449,12 @@ function claudeAdapter(skill, canonical2) {
5449
5449
  const lines = match[1].split(/\r?\n/);
5450
5450
  const frontmatter = [];
5451
5451
  let keepIndented = false;
5452
- for (const line of lines) {
5453
- if (line.startsWith("name:") || line.startsWith("description:")) {
5454
- frontmatter.push(line);
5455
- keepIndented = line.startsWith("description:");
5456
- } else if (keepIndented && /^\s+/.test(line)) {
5457
- frontmatter.push(line);
5452
+ for (const line2 of lines) {
5453
+ if (line2.startsWith("name:") || line2.startsWith("description:")) {
5454
+ frontmatter.push(line2);
5455
+ keepIndented = line2.startsWith("description:");
5456
+ } else if (keepIndented && /^\s+/.test(line2)) {
5457
+ frontmatter.push(line2);
5458
5458
  } else {
5459
5459
  keepIndented = false;
5460
5460
  }
@@ -6903,13 +6903,13 @@ async function createConversionRegistry(config) {
6903
6903
  policies.set(policy.conversionId, Object.freeze(policy));
6904
6904
  }
6905
6905
  const outputCounts = /* @__PURE__ */ new Map();
6906
- const get = (id2, kind) => {
6906
+ const get2 = (id2, kind) => {
6907
6907
  const policy = policies.get(id2);
6908
6908
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
6909
6909
  return policy;
6910
6910
  };
6911
6911
  const checked = (source, id2, kind) => {
6912
- const policy = get(id2, kind);
6912
+ const policy = get2(id2, kind);
6913
6913
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
6914
6914
  return policy;
6915
6915
  };
@@ -7745,7 +7745,7 @@ function parseCandidate(value2) {
7745
7745
  }
7746
7746
  function stripPatchEnvelope(patch2) {
7747
7747
  if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
7748
- const kept = patch2.split("\n").filter((line) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line));
7748
+ const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
7749
7749
  const stripped = kept.join("\n");
7750
7750
  return /^diff --git /m.test(stripped) ? stripped : patch2;
7751
7751
  }
@@ -7766,9 +7766,9 @@ function validateCodePatch(rawPatch, maxBytes) {
7766
7766
  const paths = [];
7767
7767
  const lines = patch2.split("\n");
7768
7768
  for (let index = 0; index < lines.length; index += 1) {
7769
- const line = lines[index];
7770
- if (!line.startsWith("diff --git ")) continue;
7771
- const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line);
7769
+ const line2 = lines[index];
7770
+ if (!line2.startsWith("diff --git ")) continue;
7771
+ const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line2);
7772
7772
  const path = match?.[1];
7773
7773
  if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
7774
7774
  validateRelativePath(path);
@@ -7801,9 +7801,9 @@ function resolveCodePath(workspaceDir, path) {
7801
7801
  return target;
7802
7802
  }
7803
7803
  function describePatchFailure(patch2, detail) {
7804
- const hunks = patch2.split("\n").filter((line) => line.startsWith("@@"));
7804
+ const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
7805
7805
  const bodies = patch2.split(/^@@.*$/m).slice(1);
7806
- const contextless = bodies.some((body) => !body.split("\n").some((line) => line.startsWith(" ") && line.trim().length > 0));
7806
+ const contextless = bodies.some((body) => !body.split("\n").some((line2) => line2.startsWith(" ") && line2.trim().length > 0));
7807
7807
  const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
7808
7808
  return `patch did not apply: ${detail}${hint}`;
7809
7809
  }
@@ -10437,11 +10437,259 @@ var init_code_connect = __esm({
10437
10437
  }
10438
10438
  });
10439
10439
 
10440
+ // src/code-grant-command.ts
10441
+ async function fail2(response2, what) {
10442
+ const body = redactSecrets((await response2.text()).slice(0, 1e3));
10443
+ throw new Error(`${what} failed (${response2.status}): ${body}`);
10444
+ }
10445
+ function line(grant) {
10446
+ const who = grant.status === "granted" ? `granted by ${grant.approvedBy ?? "?"}` : `requested by ${grant.requestedBy}`;
10447
+ return `${grant.grantId} ${grant.appEnv} ${grant.owner}/${grant.name} ${grant.status} ${who}`;
10448
+ }
10449
+ function report(grants, out, json) {
10450
+ if (json) return out.log(JSON.stringify({ grants }, null, 2));
10451
+ if (!grants.length) return out.log("no repository grants for this app");
10452
+ out.log("GRANT ENV REPOSITORY STATUS DECIDED");
10453
+ for (const grant of grants) out.log(line(grant));
10454
+ const pending = grants.filter((grant) => grant.status === "requested");
10455
+ if (pending.length) {
10456
+ out.log(`
10457
+ ${pending.length} awaiting approval. A human owner runs:`);
10458
+ for (const grant of pending) out.log(` odla-ai code grant approve ${grant.grantId} --env ${grant.appEnv}`);
10459
+ }
10460
+ }
10461
+ function grantsUrl(cfg, suffix = "") {
10462
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository-grants${suffix}`;
10463
+ }
10464
+ async function codeGrantCommand(parsed, deps = {}) {
10465
+ const action2 = parsed.positionals[2];
10466
+ if (!isAction(action2)) {
10467
+ throw new Error(
10468
+ `unknown code grant action "${action2 ?? ""}". Try "odla-ai code grant list --env dev".`
10469
+ );
10470
+ }
10471
+ const decides = action2 === "approve" || action2 === "revoke";
10472
+ assertArgs(parsed, ["config", "env", "json", "token", "email", "open"], decides ? 4 : 3);
10473
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
10474
+ const doFetch = deps.fetch ?? fetch;
10475
+ const out = deps.stdout ?? console;
10476
+ const env = stringOpt(parsed.options.env) ?? "dev";
10477
+ const json = parsed.options.json === true;
10478
+ const token = await getDeveloperToken(cfg, {
10479
+ configPath: cfg.configPath,
10480
+ token: stringOpt(parsed.options.token),
10481
+ email: stringOpt(parsed.options.email),
10482
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10483
+ openApprovalUrl: deps.openUrl
10484
+ // Every verb here administers grants, which the registry gates on
10485
+ // app.manage — a token without it is refused as "not your app", which reads
10486
+ // like the wrong app rather than the wrong authority.
10487
+ //
10488
+ // Only `request` additionally asks for code.session. Approving is a human
10489
+ // act, and a token minted to approve has no business also carrying the
10490
+ // authority the approval is about.
10491
+ }, doFetch, out, {
10492
+ optionalProjectCapabilities: action2 === "request" ? ["app.manage", "code.session"] : ["app.manage"]
10493
+ });
10494
+ const auth = { authorization: `Bearer ${token}` };
10495
+ if (action2 === "list") {
10496
+ const response3 = await doFetch(`${grantsUrl(cfg)}?env=${encodeURIComponent(env)}`, { headers: auth });
10497
+ if (!response3.ok) await fail2(response3, "repository grant list");
10498
+ const body2 = await response3.json();
10499
+ return report(body2.grants, out, json);
10500
+ }
10501
+ if (action2 === "request") {
10502
+ const response3 = await doFetch(grantsUrl(cfg), {
10503
+ method: "POST",
10504
+ headers: { ...auth, "content-type": "application/json" },
10505
+ body: JSON.stringify({ env })
10506
+ });
10507
+ if (!response3.ok) await fail2(response3, "repository grant request");
10508
+ const { grant } = await response3.json();
10509
+ if (json) return out.log(JSON.stringify({ grant }, null, 2));
10510
+ out.log(`requested ${grant.owner}/${grant.name} (${grant.appEnv}) \u2014 ${grant.grantId}`);
10511
+ return out.log(grant.status === "granted" ? "already granted; nothing to approve" : `a human owner of ${cfg.app.id} approves it with:
10512
+ odla-ai code grant approve ${grant.grantId} --env ${env}`);
10513
+ }
10514
+ const grantId = parsed.positionals[3];
10515
+ if (!grantId) throw new Error(`code grant ${action2} requires the grant id from "odla-ai code grant list"`);
10516
+ const url = `${grantsUrl(cfg, `/${encodeURIComponent(grantId)}`)}`;
10517
+ const response2 = action2 === "approve" ? await doFetch(url, {
10518
+ method: "POST",
10519
+ headers: { ...auth, "content-type": "application/json" },
10520
+ body: JSON.stringify({ env })
10521
+ }) : await doFetch(`${url}?env=${encodeURIComponent(env)}`, { method: "DELETE", headers: auth });
10522
+ if (!response2.ok) await fail2(response2, `repository grant ${action2}`);
10523
+ const body = await response2.json();
10524
+ if (json) return out.log(JSON.stringify(body, null, 2));
10525
+ out.log(action2 === "approve" && body.grant ? `granted ${body.grant.owner}/${body.grant.name} (${body.grant.appEnv}) to ${cfg.app.id}` : `revoked ${grantId}`);
10526
+ }
10527
+ var ACTIONS, isAction;
10528
+ var init_code_grant_command = __esm({
10529
+ "src/code-grant-command.ts"() {
10530
+ "use strict";
10531
+ init_cjs_shims();
10532
+ init_argv();
10533
+ init_config();
10534
+ init_redact();
10535
+ init_token();
10536
+ ACTIONS = ["request", "list", "approve", "revoke"];
10537
+ isAction = (value2) => ACTIONS.includes(value2 ?? "");
10538
+ }
10539
+ });
10540
+
10541
+ // src/code-repository-command.ts
10542
+ async function get(doFetch, url, token, what) {
10543
+ const response2 = await doFetch(url, { headers: { authorization: `Bearer ${token}` } });
10544
+ if (!response2.ok) {
10545
+ const body = redactSecrets((await response2.text()).slice(0, 1e3));
10546
+ if (response2.status === 403 && body.includes("human_session_required")) {
10547
+ throw new Error(
10548
+ `selecting a repository needs a signed-in human session, and every CLI handshake token is a delegated agent credential by design \u2014 so this cannot be done with one, however it was approved. Connect the repository in Studio (Apps \u2192 this app \u2192 Code), then run "odla-ai code grant request".`
10549
+ );
10550
+ }
10551
+ throw new Error(`${what} failed (${response2.status}): ${body}`);
10552
+ }
10553
+ return await response2.json();
10554
+ }
10555
+ async function catalogue(doFetch, platformUrl, token) {
10556
+ const account = await get(
10557
+ doFetch,
10558
+ `${platformUrl}/registry/code/github/account`,
10559
+ token,
10560
+ "GitHub account read"
10561
+ );
10562
+ const active = account.installations.filter((install2) => install2.status === "active");
10563
+ const found = [];
10564
+ for (const install2 of active) {
10565
+ const { repositories } = await get(
10566
+ doFetch,
10567
+ `${platformUrl}/registry/code/github/installations/${install2.installationId}/repositories`,
10568
+ token,
10569
+ `repository catalog for ${install2.accountLogin}`
10570
+ );
10571
+ for (const repository of repositories) {
10572
+ found.push({ ...repository, installationId: install2.installationId });
10573
+ }
10574
+ }
10575
+ if (!active.length) {
10576
+ throw new Error(
10577
+ "no active GitHub App installation. Install odla's GitHub App on the account that owns the repository first, from Studio \u2192 Code."
10578
+ );
10579
+ }
10580
+ return found;
10581
+ }
10582
+ function selectRepository(found, full) {
10583
+ const wanted = full.toLowerCase();
10584
+ const matches = found.filter((repository) => repository.fullName.toLowerCase() === wanted);
10585
+ if (matches.length === 1) return matches[0];
10586
+ if (!matches.length) {
10587
+ const near = found.filter((repository) => repository.name.toLowerCase() === wanted.split("/").pop());
10588
+ throw new Error(near.length ? `no installation exposes ${full}. Did you mean ${near.map((repository) => repository.fullName).join(", ")}?` : `no installation exposes ${full}. Run "odla-ai code repository list" to see what is reachable.`);
10589
+ }
10590
+ throw new Error(
10591
+ `${full} is reachable through ${matches.length} installations (${matches.map((match) => match.installationId).join(", ")}); revoke the one you do not mean.`
10592
+ );
10593
+ }
10594
+ function repositoryUrl(cfg, suffix = "") {
10595
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository${suffix}`;
10596
+ }
10597
+ async function codeRepositoryCommand(parsed, deps = {}) {
10598
+ const action2 = parsed.positionals[2];
10599
+ if (!isAction2(action2)) {
10600
+ throw new Error(
10601
+ `unknown code repository action "${action2 ?? ""}". Try "odla-ai code repository show --env dev".`
10602
+ );
10603
+ }
10604
+ assertArgs(parsed, ["config", "env", "repo", "json", "token", "email", "open"], 3);
10605
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
10606
+ const doFetch = deps.fetch ?? fetch;
10607
+ const out = deps.stdout ?? console;
10608
+ const env = stringOpt(parsed.options.env) ?? "dev";
10609
+ const json = parsed.options.json === true;
10610
+ const token = await getDeveloperToken(cfg, {
10611
+ configPath: cfg.configPath,
10612
+ token: stringOpt(parsed.options.token),
10613
+ email: stringOpt(parsed.options.email),
10614
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
10615
+ openApprovalUrl: deps.openUrl
10616
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
10617
+ if (action2 === "show") {
10618
+ const body2 = await get(
10619
+ doFetch,
10620
+ `${repositoryUrl(cfg)}?env=${encodeURIComponent(env)}`,
10621
+ token,
10622
+ "repository read"
10623
+ );
10624
+ if (json) return out.log(JSON.stringify(body2, null, 2));
10625
+ const bound = body2.environmentRepository;
10626
+ return out.log(bound ? `${cfg.app.id} (${env}) \u2192 ${bound.owner}/${bound.name}` : `${cfg.app.id} (${env}) has no connected repository. Connect one with "odla-ai code repository bind --repo owner/name --env ${env}".`);
10627
+ }
10628
+ const found = await catalogue(doFetch, cfg.platformUrl, token);
10629
+ if (action2 === "list") {
10630
+ if (json) return out.log(JSON.stringify({ repositories: found }, null, 2));
10631
+ if (!found.length) return out.log("no repositories reachable through your installations");
10632
+ out.log("REPOSITORY INSTALL DEFAULT READY");
10633
+ for (const repository of found) {
10634
+ out.log(`${repository.fullName} ${repository.installationId} ${repository.defaultBranch ?? "\u2014"} ${repository.ready ? "yes" : "empty"}`);
10635
+ }
10636
+ return;
10637
+ }
10638
+ const repo = stringOpt(parsed.options.repo);
10639
+ if (!repo?.includes("/")) throw new Error("code repository bind requires --repo owner/name");
10640
+ const selected = selectRepository(found, repo);
10641
+ if (!selected.ready) {
10642
+ throw new Error(`${selected.fullName} has no commits yet; GitHub cannot serve a default branch for an empty repository.`);
10643
+ }
10644
+ const response2 = await doFetch(repositoryUrl(cfg, "/bind"), {
10645
+ method: "POST",
10646
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
10647
+ body: JSON.stringify({ env, installationId: selected.installationId, repositoryId: selected.repositoryId })
10648
+ });
10649
+ if (!response2.ok) {
10650
+ throw new Error(`repository bind failed (${response2.status}): ${redactSecrets((await response2.text()).slice(0, 1e3))}`);
10651
+ }
10652
+ const body = await response2.json();
10653
+ if (json) return out.log(JSON.stringify(body, null, 2));
10654
+ out.log(`connected ${cfg.app.id} (${env}) to ${selected.fullName} @ ${selected.defaultBranch}`);
10655
+ out.log(`Next, so it may be worked on unattended:
10656
+ odla-ai code grant request --env ${env}`);
10657
+ }
10658
+ var ACTIONS2, isAction2;
10659
+ var init_code_repository_command = __esm({
10660
+ "src/code-repository-command.ts"() {
10661
+ "use strict";
10662
+ init_cjs_shims();
10663
+ init_argv();
10664
+ init_config();
10665
+ init_redact();
10666
+ init_token();
10667
+ ACTIONS2 = ["show", "list", "bind"];
10668
+ isAction2 = (value2) => ACTIONS2.includes(value2 ?? "");
10669
+ }
10670
+ });
10671
+
10440
10672
  // src/code-command.ts
10441
10673
  async function codeCommand(parsed, dependencies) {
10442
10674
  const sub = parsed.positionals[1];
10675
+ if (sub === "grant") {
10676
+ return await codeGrantCommand(parsed, {
10677
+ fetch: dependencies.fetch,
10678
+ stdout: dependencies.stdout,
10679
+ openUrl: dependencies.openUrl
10680
+ });
10681
+ }
10682
+ if (sub === "repository") {
10683
+ return await codeRepositoryCommand(parsed, {
10684
+ fetch: dependencies.fetch,
10685
+ stdout: dependencies.stdout,
10686
+ openUrl: dependencies.openUrl
10687
+ });
10688
+ }
10443
10689
  if (sub !== "connect") {
10444
- throw new Error(`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev".`);
10690
+ throw new Error(
10691
+ `unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev" or "odla-ai code grant list --env dev".`
10692
+ );
10445
10693
  }
10446
10694
  assertArgs(parsed, [
10447
10695
  "config",
@@ -10484,6 +10732,8 @@ var init_code_command = __esm({
10484
10732
  init_cjs_shims();
10485
10733
  init_argv();
10486
10734
  init_code_connect();
10735
+ init_code_grant_command();
10736
+ init_code_repository_command();
10487
10737
  }
10488
10738
  });
10489
10739
 
@@ -10927,6 +11177,9 @@ Commands:
10927
11177
  \u2014 no secret is ever copied between people.
10928
11178
  capabilities Show what the CLI automates vs agent edits and human checkpoints.
10929
11179
  code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
11180
+ "code repository show" reports the repository an app works on (Studio or a
11181
+ human session connects one); "code grant request|list|approve|revoke" then
11182
+ governs unattended access: an agent may request, only a human may approve.
10930
11183
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
10931
11184
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10932
11185
  pm Project management (via @odla-ai/pm): Products contain Projects;
@@ -11027,6 +11280,17 @@ Safety:
11027
11280
  approval and credential hashes live in odla-ai/db. The host
11028
11281
  credential is never written under .odla/; it exists only in the foreground
11029
11282
  "code connect" process and is rotated by the next approved connection.
11283
+ "code repository bind" takes owner/name and resolves the two GitHub integers the
11284
+ bind route wants across every installation you have, refusing an ambiguous match
11285
+ rather than choosing one \u2014 the same repository name under two organizations is
11286
+ ordinary, and picking the first would connect an app to source nobody chose. It
11287
+ needs a signed-in human session; a CLI handshake token is a delegated agent
11288
+ credential by design, so an agent cannot choose the source it will be given.
11289
+ Unattended access is per repository and revocable on its own, but the connection
11290
+ IS the authorization: a repository is recorded for one exact app and environment
11291
+ only by a signed-in human choosing it. "code grant list" shows what is authorized;
11292
+ "revoke" stops unattended access without disconnecting the repository, and
11293
+ re-binding never resurrects it. Connecting sandbox never authorizes live.
11030
11294
  Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
11031
11295
  OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
11032
11296
  GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
@@ -11428,7 +11692,7 @@ async function discussWatch(ctx, topicId, parsed) {
11428
11692
  throw new WatchRemoteError(cursor, error);
11429
11693
  }
11430
11694
  if (deadline !== void 0 && now() >= deadline) {
11431
- const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
11695
+ const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
11432
11696
  throw new WatchTimeoutError(result.cursor);
11433
11697
  }
11434
11698
  const base = Math.min(intervalMs, 1e3);
@@ -11469,7 +11733,7 @@ async function discussWatch(ctx, topicId, parsed) {
11469
11733
  });
11470
11734
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
11471
11735
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
11472
- return report(ctx, parsed, {
11736
+ return report2(ctx, parsed, {
11473
11737
  found: true,
11474
11738
  cursor,
11475
11739
  events: matching,
@@ -11496,13 +11760,13 @@ async function discussWatch(ctx, topicId, parsed) {
11496
11760
  }
11497
11761
  if (page2.hasMore) continue;
11498
11762
  if (deadline !== void 0 && now() >= deadline) {
11499
- return report(ctx, parsed, { found: false, cursor });
11763
+ return report2(ctx, parsed, { found: false, cursor });
11500
11764
  }
11501
11765
  const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
11502
11766
  await sleep(wait2);
11503
11767
  }
11504
11768
  }
11505
- function report(ctx, parsed, result) {
11769
+ function report2(ctx, parsed, result) {
11506
11770
  if (ctx.json) {
11507
11771
  ctx.out.log(JSON.stringify(result, null, 2));
11508
11772
  } else if (parsed.options.jsonl !== true && result.found) {
@@ -12169,7 +12433,7 @@ function eventLabel(event) {
12169
12433
  const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
12170
12434
  return body || event.payload.entityId;
12171
12435
  }
12172
- function report2(ctx, parsed, result) {
12436
+ function report3(ctx, parsed, result) {
12173
12437
  if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
12174
12438
  else if (parsed.options.jsonl !== true && result.found) {
12175
12439
  for (const event of result.events ?? []) {
@@ -12229,7 +12493,7 @@ async function pmWatch(ctx, parsed) {
12229
12493
  });
12230
12494
  if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
12231
12495
  if (deadline !== void 0 && now() >= deadline) {
12232
- return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
12496
+ return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
12233
12497
  }
12234
12498
  const backoff = Math.min(
12235
12499
  MAX_BACKOFF_MS2,
@@ -12270,7 +12534,7 @@ async function pmWatch(ctx, parsed) {
12270
12534
  cursor,
12271
12535
  serverTime: current.serverTime
12272
12536
  });
12273
- return report2(ctx, parsed, { found: true, cursor, events: matching });
12537
+ return report3(ctx, parsed, { found: true, cursor, events: matching });
12274
12538
  }
12275
12539
  if (current.events.length > 0) {
12276
12540
  jsonl2(ctx, parsed, {
@@ -12289,7 +12553,7 @@ async function pmWatch(ctx, parsed) {
12289
12553
  }
12290
12554
  if (current.hasMore) continue;
12291
12555
  if (deadline !== void 0 && now() >= deadline) {
12292
- return report2(ctx, parsed, { found: false, cursor });
12556
+ return report3(ctx, parsed, { found: false, cursor });
12293
12557
  }
12294
12558
  await sleep(
12295
12559
  deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
@@ -12894,8 +13158,8 @@ function printO11yStatus(status, out) {
12894
13158
  out.log(
12895
13159
  `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
12896
13160
  );
12897
- for (const line of providerCapacityLines(status.providerCapacity)) {
12898
- out.log(line);
13161
+ for (const line2 of providerCapacityLines(status.providerCapacity)) {
13162
+ out.log(line2);
12899
13163
  }
12900
13164
  const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
12901
13165
  const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
@@ -14060,7 +14324,11 @@ var init_surface = __esm({
14060
14324
  bug: { create: {}, list: {}, report: {} },
14061
14325
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
14062
14326
  capabilities: {},
14063
- code: { connect: {} },
14327
+ code: {
14328
+ connect: {},
14329
+ grant: { request: {}, list: {}, approve: {}, revoke: {} },
14330
+ repository: { show: {}, list: {}, bind: {} }
14331
+ },
14064
14332
  config: { diff: {}, plan: {}, apply: {} },
14065
14333
  context: { show: {}, list: {}, save: {}, remove: {} },
14066
14334
  credentials: { list: {}, revoke: {} },
@@ -14300,8 +14568,8 @@ function parseRunbook(text3, slug) {
14300
14568
  const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
14301
14569
  if (fm) {
14302
14570
  rest = text3.slice(fm[0].length);
14303
- for (const line of fm[1].split(/\r?\n/)) {
14304
- const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
14571
+ for (const line2 of fm[1].split(/\r?\n/)) {
14572
+ const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
14305
14573
  if (!pair) continue;
14306
14574
  const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
14307
14575
  if (pair[1] === "summary") meta.summary = value2;
@@ -14422,18 +14690,18 @@ function areaOf(path) {
14422
14690
  return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
14423
14691
  }
14424
14692
  function scanHunk(lines, into) {
14425
- for (const [i, line] of lines.entries()) {
14426
- const decl = DECL.exec(line);
14693
+ for (const [i, line2] of lines.entries()) {
14694
+ const decl = DECL.exec(line2);
14427
14695
  if (decl?.[1]) {
14428
14696
  into.add(decl[1]);
14429
14697
  continue;
14430
14698
  }
14431
- const named = NAMED.exec(line);
14699
+ const named = NAMED.exec(line2);
14432
14700
  if (named?.[1]) {
14433
14701
  for (const name of namedExports(named[1])) into.add(name);
14434
14702
  continue;
14435
14703
  }
14436
- if (!JSDOC.test(line)) continue;
14704
+ if (!JSDOC.test(line2)) continue;
14437
14705
  for (let j = i + 1; j < lines.length && j < i + 40; j++) {
14438
14706
  const found = ANY_DECL.exec(lines[j]);
14439
14707
  if (found?.[1]) {
@@ -14451,8 +14719,8 @@ function parseDiff(diff) {
14451
14719
  if (current && hunk.length) scanHunk(hunk, current.exports);
14452
14720
  hunk = [];
14453
14721
  };
14454
- for (const line of diff.split("\n")) {
14455
- const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line);
14722
+ for (const line2 of diff.split("\n")) {
14723
+ const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
14456
14724
  if (header) {
14457
14725
  flush();
14458
14726
  const path = header[2] ?? header[1];
@@ -14460,11 +14728,11 @@ function parseDiff(diff) {
14460
14728
  files.set(path, current);
14461
14729
  continue;
14462
14730
  }
14463
- if (line.startsWith("@@")) {
14731
+ if (line2.startsWith("@@")) {
14464
14732
  flush();
14465
14733
  continue;
14466
14734
  }
14467
- if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line);
14735
+ if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
14468
14736
  }
14469
14737
  flush();
14470
14738
  return [...files.values()];
@@ -14557,7 +14825,7 @@ function untrackedDiff(runGit, read3) {
14557
14825
  return "";
14558
14826
  }
14559
14827
  let out = "";
14560
- for (const path of listed.split("\n").map((line) => line.trim()).filter(Boolean)) {
14828
+ for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
14561
14829
  out += `diff --git a/${path} b/${path}
14562
14830
  --- /dev/null
14563
14831
  +++ b/${path}
@@ -14570,7 +14838,7 @@ function untrackedDiff(runGit, read3) {
14570
14838
  continue;
14571
14839
  }
14572
14840
  out += `@@ -0,0 +1,${body.split("\n").length} @@
14573
- ${body.split("\n").map((line) => `+${line}`).join("\n")}
14841
+ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
14574
14842
  `;
14575
14843
  }
14576
14844
  return out;
@@ -14617,7 +14885,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
14617
14885
  }
14618
14886
  return out;
14619
14887
  }
14620
- function report3(ctx, impacts) {
14888
+ function report4(ctx, impacts) {
14621
14889
  const covered = impacts.filter((i) => i.runbooks.length);
14622
14890
  ctx.out.log(
14623
14891
  `${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.`
@@ -14655,7 +14923,7 @@ async function runbookImpact(ctx, options, deps = {}) {
14655
14923
  }
14656
14924
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
14657
14925
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
14658
- report3(ctx, impacts);
14926
+ report4(ctx, impacts);
14659
14927
  }
14660
14928
  var import_node_child_process6, import_node_fs20, import_node_path18, SOURCE3, editHint;
14661
14929
  var init_runbook_impact = __esm({
@@ -14755,7 +15023,7 @@ async function runbookSearch(ctx, query, all, limit) {
14755
15023
  for (const [i, hit] of result.hits.entries()) {
14756
15024
  if (i) ctx.out.log("");
14757
15025
  ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
14758
- for (const line of hit.excerpt.split("\n")) ctx.out.log(` ${line}`);
15026
+ for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
14759
15027
  ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
14760
15028
  }
14761
15029
  }
@@ -15181,31 +15449,31 @@ function printHostedJob(out, job, platform, appId) {
15181
15449
  url.searchParams.set("job", job.jobId);
15182
15450
  out.log(` Studio: ${url.toString()}`);
15183
15451
  }
15184
- function printHostedReport(out, report4) {
15185
- out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
15186
- 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}`);
15187
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
15188
- out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
15189
- out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
15190
- for (const finding of report4.findings) {
15452
+ function printHostedReport(out, report5) {
15453
+ out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
15454
+ 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}`);
15455
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
15456
+ out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
15457
+ out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
15458
+ for (const finding of report5.findings) {
15191
15459
  const location = finding.locations[0];
15192
15460
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
15193
15461
  }
15194
- for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
15462
+ for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
15195
15463
  }
15196
- function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
15464
+ function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
15197
15465
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
15198
15466
  const candidateValue = parsed.options["fail-on-candidates"];
15199
15467
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
15200
15468
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
15201
- const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
15202
- const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
15203
- const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
15469
+ const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
15470
+ const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
15471
+ const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
15204
15472
  if (confirmed.length || leads.length || incomplete) {
15205
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
15473
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
15206
15474
  }
15207
15475
  if (emitSuccess) {
15208
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
15476
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
15209
15477
  }
15210
15478
  }
15211
15479
  function printHostedSecurityPlanRoute(out, label, route3) {
@@ -15290,17 +15558,17 @@ async function runHostedSecurity(options) {
15290
15558
  allowNetwork: false
15291
15559
  }
15292
15560
  });
15293
- const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
15294
- await (0, import_node3.writeSecurityArtifacts)(output, report4);
15295
- const reportDigest = await (0, import_security.securityFingerprint)(report4);
15561
+ const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
15562
+ await (0, import_node3.writeSecurityArtifacts)(output, report5);
15563
+ const reportDigest = await (0, import_security.securityFingerprint)(report5);
15296
15564
  await hosted.complete({
15297
15565
  reportDigest,
15298
- coverageStatus: report4.coverageStatus,
15299
- confirmed: report4.metrics.confirmed,
15300
- candidates: report4.metrics.candidates
15566
+ coverageStatus: report5.coverageStatus,
15567
+ confirmed: report5.metrics.confirmed,
15568
+ candidates: report5.metrics.candidates
15301
15569
  }, { signal: options.signal });
15302
- printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
15303
- return Object.freeze({ report: report4, run: hosted.run, output });
15570
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
15571
+ return Object.freeze({ report: report5, run: hosted.run, output });
15304
15572
  }
15305
15573
  function selectEnv(requested, declared, configPath, rootDir) {
15306
15574
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -15326,14 +15594,14 @@ function profileFor(name, maxHuntTasks) {
15326
15594
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
15327
15595
  return { ...profile, maxHuntTasks };
15328
15596
  }
15329
- function printSummary(out, appId, env, run, report4, output) {
15330
- const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
15597
+ function printSummary(out, appId, env, run, report5, output) {
15598
+ const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
15331
15599
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
15332
15600
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
15333
15601
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
15334
- 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}`);
15335
- if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
15336
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
15602
+ 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}`);
15603
+ if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
15604
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
15337
15605
  out.log(` report: ${(0, import_node_path20.resolve)(output, "REPORT.md")}`);
15338
15606
  }
15339
15607
  function formatBudget(usage) {
@@ -15605,13 +15873,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
15605
15873
  }
15606
15874
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
15607
15875
  }
15608
- const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
15876
+ const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
15609
15877
  if (parsed.options.json === true) {
15610
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
15878
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
15611
15879
  } else {
15612
- printHostedReport(context.stdout, report4);
15880
+ printHostedReport(context.stdout, report5);
15613
15881
  }
15614
- enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
15882
+ enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
15615
15883
  }
15616
15884
  async function runLocalSecurityCommand(parsed, dependencies) {
15617
15885
  if (parsed.options.source === true) {
@@ -15679,13 +15947,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
15679
15947
  });
15680
15948
  enforceLocalGate(result.report, parsed);
15681
15949
  }
15682
- function enforceLocalGate(report4, parsed) {
15950
+ function enforceLocalGate(report5, parsed) {
15683
15951
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
15684
15952
  const candidateValue = parsed.options["fail-on-candidates"];
15685
15953
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
15686
- const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
15687
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
15688
- const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
15954
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
15955
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
15956
+ const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
15689
15957
  if (confirmed.length || leads.length || incomplete) {
15690
15958
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
15691
15959
  }
@@ -15740,9 +16008,9 @@ async function securityCommand(parsed, dependencies) {
15740
16008
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
15741
16009
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
15742
16010
  const context = await hostedSecurityContext(parsed, dependencies);
15743
- const report4 = await getHostedSecurityReport({ ...context, jobId });
15744
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
15745
- else printHostedReport(context.stdout, report4);
16011
+ const report5 = await getHostedSecurityReport({ ...context, jobId });
16012
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
16013
+ else printHostedReport(context.stdout, report5);
15746
16014
  return;
15747
16015
  }
15748
16016
  if (sub !== "run") {