@odla-ai/cli 0.34.0 → 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/README.md +130 -119
- package/REQUIREMENTS.md +6 -0
- package/dist/bin.cjs +419 -138
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-LGNNX6AP.js → chunk-EG23MPUC.js} +396 -139
- package/dist/chunk-EG23MPUC.js.map +1 -0
- package/dist/{cli-IN6WGMSY.js → cli-Z5NSTS75.js} +2 -2
- package/dist/index.cjs +395 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/skills/odla/SKILL.md +55 -35
- package/skills/odla/references/agent-identity.md +5 -5
- package/skills/odla/references/build.md +16 -15
- package/skills/odla/references/co-owners.md +1 -1
- package/skills/odla/references/pm-work-intake.md +12 -12
- package/skills/odla/references/pm.md +17 -17
- package/skills/odla/references/sdks.md +2 -2
- package/skills/odla-migrate/SKILL.md +23 -5
- package/skills/odla-migrate/references/phase-2-chapter.md +1 -1
- package/skills/odla-migrate/references/phase-2-db.md +11 -10
- package/skills/odla-migrate/references/phase-3-auth.md +2 -2
- package/skills/odla-migrate/references/phase-3b-user-sync.md +2 -2
- package/skills/odla-migrate/references/phase-4-ai.md +3 -3
- package/skills/odla-migrate/references/phase-5-cutover.md +5 -5
- package/skills/odla-migrate/references/project-state.md +4 -4
- package/skills/odla-migrate/references/secrets-map.md +6 -6
- package/skills/odla-migrate/references/troubleshooting.md +23 -23
- package/skills/odla-o11y-debug/SKILL.md +3 -3
- package/dist/chunk-LGNNX6AP.js.map +0 -1
- /package/dist/{cli-IN6WGMSY.js.map → cli-Z5NSTS75.js.map} +0 -0
package/dist/bin.cjs
CHANGED
|
@@ -207,14 +207,15 @@ function approvalLines(prompt) {
|
|
|
207
207
|
lines.push(` No browser was opened (${prompt.browserSkipped}).`);
|
|
208
208
|
}
|
|
209
209
|
lines.push("");
|
|
210
|
-
lines.push(" AGENTS:
|
|
211
|
-
lines.push("
|
|
212
|
-
lines.push("
|
|
210
|
+
lines.push(" AGENTS: immediately give the human this URL as a clickable approval action and repeat the code.");
|
|
211
|
+
lines.push(" Keep this CLI process running and wait on this same process; the CLI owns protocol polling.");
|
|
212
|
+
lines.push(" Do not use OS open, browser control, curl, a shell wait loop, detached execution, or a substitute handshake.");
|
|
213
|
+
lines.push(" You cannot approve it yourself. If this process exits, a later invocation creates a new code.");
|
|
213
214
|
lines.push("");
|
|
214
215
|
return lines;
|
|
215
216
|
}
|
|
216
217
|
function printApproval(out, prompt) {
|
|
217
|
-
for (const
|
|
218
|
+
for (const line2 of approvalLines(prompt)) out.error(line2);
|
|
218
219
|
}
|
|
219
220
|
function reminderLines(prompt) {
|
|
220
221
|
return [
|
|
@@ -320,12 +321,12 @@ function approvalHint(pending) {
|
|
|
320
321
|
}
|
|
321
322
|
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
322
323
|
const timer = setInterval(() => {
|
|
323
|
-
for (const
|
|
324
|
+
for (const line2 of reminderLines({
|
|
324
325
|
userCode: pending.userCode,
|
|
325
326
|
approvalUrl: pending.approvalUrl,
|
|
326
327
|
minutesLeft: minutesLeft(pending.expiresAt)
|
|
327
328
|
}))
|
|
328
|
-
out.log(
|
|
329
|
+
out.log(line2);
|
|
329
330
|
}, periodMs);
|
|
330
331
|
timer.unref?.();
|
|
331
332
|
return () => clearInterval(timer);
|
|
@@ -394,9 +395,9 @@ function mergeCredential(current, update) {
|
|
|
394
395
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
395
396
|
const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
|
|
396
397
|
const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
|
|
397
|
-
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((
|
|
398
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
398
399
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
399
|
-
const missing = wanted.filter((
|
|
400
|
+
const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
|
|
400
401
|
if (missing.length === 0) return;
|
|
401
402
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
402
403
|
(0, import_node_fs5.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
@@ -428,7 +429,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
428
429
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
429
430
|
}
|
|
430
431
|
const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
|
|
431
|
-
const retained = existing.split(/\r?\n/).filter((
|
|
432
|
+
const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
|
|
432
433
|
while (retained.at(-1) === "") retained.pop();
|
|
433
434
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
434
435
|
|
|
@@ -436,8 +437,8 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
436
437
|
writePrivateText(path, `${prefix}${lines.join("\n")}
|
|
437
438
|
`);
|
|
438
439
|
}
|
|
439
|
-
function isManagedDevVar(
|
|
440
|
-
const match =
|
|
440
|
+
function isManagedDevVar(line2) {
|
|
441
|
+
const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
441
442
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
442
443
|
}
|
|
443
444
|
function writePrivateText(path, text3) {
|
|
@@ -2504,7 +2505,7 @@ function chooseIdMode(options, rows) {
|
|
|
2504
2505
|
async function appImport(options) {
|
|
2505
2506
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2506
2507
|
const out = options.stdout ?? console;
|
|
2507
|
-
const say = options.json ? (
|
|
2508
|
+
const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
|
|
2508
2509
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
2509
2510
|
const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs10.readFileSync)(0, "utf8")))() : (0, import_node_fs10.readFileSync)(options.file, "utf8");
|
|
2510
2511
|
const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
|
|
@@ -2881,7 +2882,7 @@ async function designUnpack(parsed, deps) {
|
|
|
2881
2882
|
);
|
|
2882
2883
|
return;
|
|
2883
2884
|
}
|
|
2884
|
-
for (const
|
|
2885
|
+
for (const line2 of describeUnpack(result, outDir)) out.log(line2);
|
|
2885
2886
|
}
|
|
2886
2887
|
async function brandCommand(parsed, deps) {
|
|
2887
2888
|
const subject = parsed.positionals[1];
|
|
@@ -4968,7 +4969,7 @@ async function doctor(options) {
|
|
|
4968
4969
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
|
|
4969
4970
|
const contract = resolveSecretContract(cfg);
|
|
4970
4971
|
out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
|
|
4971
|
-
for (const
|
|
4972
|
+
for (const line2 of formatSecretContract(contract)) out.log(line2);
|
|
4972
4973
|
if (cfg.services.includes("calendar")) {
|
|
4973
4974
|
const calendar = cfg.envs.map((env) => {
|
|
4974
4975
|
const resolved = calendarServiceConfig(cfg, env);
|
|
@@ -5386,10 +5387,10 @@ async function secretsStatus(options) {
|
|
|
5386
5387
|
}
|
|
5387
5388
|
const body = await res.json();
|
|
5388
5389
|
const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
|
|
5389
|
-
const
|
|
5390
|
-
if (options.json) out.log(JSON.stringify(
|
|
5391
|
-
else printReport(
|
|
5392
|
-
return
|
|
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;
|
|
5393
5394
|
}
|
|
5394
5395
|
function buildReport(appId, env, tenant, contract, stored) {
|
|
5395
5396
|
const declared = new Set(contract.map((secret) => secret.name));
|
|
@@ -5406,25 +5407,25 @@ function buildReport(appId, env, tenant, contract, stored) {
|
|
|
5406
5407
|
const ok = rows.every((row) => row.state !== "missing" || !row.required);
|
|
5407
5408
|
return { app: appId, env, tenant, secrets: rows, ok };
|
|
5408
5409
|
}
|
|
5409
|
-
function printReport(
|
|
5410
|
-
out.log(`${
|
|
5411
|
-
if (
|
|
5410
|
+
function printReport(report5, out) {
|
|
5411
|
+
out.log(`${report5.app} (${report5.tenant})`);
|
|
5412
|
+
if (report5.secrets.length === 0) {
|
|
5412
5413
|
out.log(" no secrets declared and none stored");
|
|
5413
5414
|
return;
|
|
5414
5415
|
}
|
|
5415
|
-
for (const row of
|
|
5416
|
+
for (const row of report5.secrets) {
|
|
5416
5417
|
const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
|
|
5417
5418
|
const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
|
|
5418
5419
|
out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
|
|
5419
5420
|
}
|
|
5420
|
-
const missing =
|
|
5421
|
+
const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
|
|
5421
5422
|
if (missing.length) {
|
|
5422
5423
|
out.log("");
|
|
5423
5424
|
for (const row of missing) {
|
|
5424
|
-
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${
|
|
5425
|
+
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report5.env} --stdin"`);
|
|
5425
5426
|
}
|
|
5426
5427
|
}
|
|
5427
|
-
if (
|
|
5428
|
+
if (report5.secrets.some((row) => row.state === "reserved")) {
|
|
5428
5429
|
out.log("");
|
|
5429
5430
|
out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
|
|
5430
5431
|
}
|
|
@@ -5448,12 +5449,12 @@ function claudeAdapter(skill, canonical2) {
|
|
|
5448
5449
|
const lines = match[1].split(/\r?\n/);
|
|
5449
5450
|
const frontmatter = [];
|
|
5450
5451
|
let keepIndented = false;
|
|
5451
|
-
for (const
|
|
5452
|
-
if (
|
|
5453
|
-
frontmatter.push(
|
|
5454
|
-
keepIndented =
|
|
5455
|
-
} else if (keepIndented && /^\s+/.test(
|
|
5456
|
-
frontmatter.push(
|
|
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);
|
|
5457
5458
|
} else {
|
|
5458
5459
|
keepIndented = false;
|
|
5459
5460
|
}
|
|
@@ -5483,18 +5484,29 @@ For work that creates an odla app or adds odla services, read and follow
|
|
|
5483
5484
|
\`.agents/skills/odla-o11y-debug/SKILL.md\`.
|
|
5484
5485
|
|
|
5485
5486
|
Track the work in odla's PM as you go. Before project-mutating work, run
|
|
5486
|
-
\`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
|
|
5487
|
+
\`npx --yes @odla-ai/cli@latest pm next --app <appId>\`, confirm alignment to an open goal,
|
|
5487
5488
|
and atomically claim a refined Ready task. Record decisions when you make them
|
|
5488
5489
|
and file bugs when you notice them. The conventions and the full command set are
|
|
5489
5490
|
in \`.agents/skills/odla/references/pm.md\`.
|
|
5490
5491
|
|
|
5491
|
-
Use the human's signed-in odla account email for device authorization;
|
|
5492
|
-
infer it from git config, commit metadata, or GitHub. If authorization is
|
|
5493
|
-
already active, run
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5492
|
+
Use the human's known signed-in odla account email for device authorization;
|
|
5493
|
+
never infer it from git config, commit metadata, or GitHub. If authorization is
|
|
5494
|
+
not already active, run this exact command as one foreground process:
|
|
5495
|
+
|
|
5496
|
+
\`npx --yes @odla-ai/cli@latest auth login --app <appId> --email <odla-account> --no-open --wait 600\`
|
|
5497
|
+
|
|
5498
|
+
When it prints the approval URL and code, immediately give the human a clickable
|
|
5499
|
+
link, name the code they must verify, and tell them to click **Approve**. Keep
|
|
5500
|
+
that CLI process running and wait on the same tool process for its result. The
|
|
5501
|
+
CLI owns protocol polling: never call the OS \`open\` command, use browser
|
|
5502
|
+
control, curl a handshake endpoint, build a shell wait/poll loop, detach the
|
|
5503
|
+
process, or start another handshake while it is alive. The device code exists
|
|
5504
|
+
only in that process. If it exits 75 before approval, the old code cannot be
|
|
5505
|
+
collected; start one fresh foreground command and surface its new URL.
|
|
5506
|
+
|
|
5507
|
+
Never file an odla project or product defect in GitHub Issues: run
|
|
5508
|
+
\`npx --yes @odla-ai/cli@latest bug report --app <appId> ...\` so the bug lands
|
|
5509
|
+
in odla PM with the rest of the project's goals, tasks, and decisions.
|
|
5498
5510
|
|
|
5499
5511
|
The setup runbooks and their references are installed in this repository, pinned
|
|
5500
5512
|
to this CLI version. Use them as your setup context.
|
|
@@ -5506,9 +5518,9 @@ When this repository has an \`appId\`, pass it: app-scoped discovery includes
|
|
|
5506
5518
|
that project's instructions plus the shared platform procedures.
|
|
5507
5519
|
|
|
5508
5520
|
\`\`\`
|
|
5509
|
-
npx odla-ai runbook ask "<what you are about to do>" --app <appId>
|
|
5510
|
-
npx odla-ai runbook list
|
|
5511
|
-
npx odla-ai runbook get <slug>
|
|
5521
|
+
npx --yes @odla-ai/cli@latest runbook ask "<what you are about to do>" --app <appId>
|
|
5522
|
+
npx --yes @odla-ai/cli@latest runbook list
|
|
5523
|
+
npx --yes @odla-ai/cli@latest runbook get <slug>
|
|
5512
5524
|
\`\`\`
|
|
5513
5525
|
|
|
5514
5526
|
Never scrape odla.ai HTML for any of this. The CLI reads the same content
|
|
@@ -6891,13 +6903,13 @@ async function createConversionRegistry(config) {
|
|
|
6891
6903
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
6892
6904
|
}
|
|
6893
6905
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
6894
|
-
const
|
|
6906
|
+
const get2 = (id2, kind) => {
|
|
6895
6907
|
const policy = policies.get(id2);
|
|
6896
6908
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
6897
6909
|
return policy;
|
|
6898
6910
|
};
|
|
6899
6911
|
const checked = (source, id2, kind) => {
|
|
6900
|
-
const policy =
|
|
6912
|
+
const policy = get2(id2, kind);
|
|
6901
6913
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
6902
6914
|
return policy;
|
|
6903
6915
|
};
|
|
@@ -7733,7 +7745,7 @@ function parseCandidate(value2) {
|
|
|
7733
7745
|
}
|
|
7734
7746
|
function stripPatchEnvelope(patch2) {
|
|
7735
7747
|
if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
|
|
7736
|
-
const kept = patch2.split("\n").filter((
|
|
7748
|
+
const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
|
|
7737
7749
|
const stripped = kept.join("\n");
|
|
7738
7750
|
return /^diff --git /m.test(stripped) ? stripped : patch2;
|
|
7739
7751
|
}
|
|
@@ -7754,9 +7766,9 @@ function validateCodePatch(rawPatch, maxBytes) {
|
|
|
7754
7766
|
const paths = [];
|
|
7755
7767
|
const lines = patch2.split("\n");
|
|
7756
7768
|
for (let index = 0; index < lines.length; index += 1) {
|
|
7757
|
-
const
|
|
7758
|
-
if (!
|
|
7759
|
-
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(
|
|
7769
|
+
const line2 = lines[index];
|
|
7770
|
+
if (!line2.startsWith("diff --git ")) continue;
|
|
7771
|
+
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line2);
|
|
7760
7772
|
const path = match?.[1];
|
|
7761
7773
|
if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
|
|
7762
7774
|
validateRelativePath(path);
|
|
@@ -7789,9 +7801,9 @@ function resolveCodePath(workspaceDir, path) {
|
|
|
7789
7801
|
return target;
|
|
7790
7802
|
}
|
|
7791
7803
|
function describePatchFailure(patch2, detail) {
|
|
7792
|
-
const hunks = patch2.split("\n").filter((
|
|
7804
|
+
const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
|
|
7793
7805
|
const bodies = patch2.split(/^@@.*$/m).slice(1);
|
|
7794
|
-
const contextless = bodies.some((body) => !body.split("\n").some((
|
|
7806
|
+
const contextless = bodies.some((body) => !body.split("\n").some((line2) => line2.startsWith(" ") && line2.trim().length > 0));
|
|
7795
7807
|
const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
|
|
7796
7808
|
return `patch did not apply: ${detail}${hint}`;
|
|
7797
7809
|
}
|
|
@@ -10425,11 +10437,259 @@ var init_code_connect = __esm({
|
|
|
10425
10437
|
}
|
|
10426
10438
|
});
|
|
10427
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
|
+
|
|
10428
10672
|
// src/code-command.ts
|
|
10429
10673
|
async function codeCommand(parsed, dependencies) {
|
|
10430
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
|
+
}
|
|
10431
10689
|
if (sub !== "connect") {
|
|
10432
|
-
throw new Error(
|
|
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
|
+
);
|
|
10433
10693
|
}
|
|
10434
10694
|
assertArgs(parsed, [
|
|
10435
10695
|
"config",
|
|
@@ -10472,6 +10732,8 @@ var init_code_command = __esm({
|
|
|
10472
10732
|
init_cjs_shims();
|
|
10473
10733
|
init_argv();
|
|
10474
10734
|
init_code_connect();
|
|
10735
|
+
init_code_grant_command();
|
|
10736
|
+
init_code_repository_command();
|
|
10475
10737
|
}
|
|
10476
10738
|
});
|
|
10477
10739
|
|
|
@@ -10817,8 +11079,8 @@ Usage:
|
|
|
10817
11079
|
odla-ai runbook revert <slug> --version <n> [--app <id>]
|
|
10818
11080
|
odla-ai runbook rm <slug> [--app <id>]
|
|
10819
11081
|
odla-ai capabilities [--json]
|
|
10820
|
-
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
10821
|
-
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
11082
|
+
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--no-open] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
11083
|
+
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--no-open] [--json]
|
|
10822
11084
|
odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
|
|
10823
11085
|
odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
10824
11086
|
[--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
|
|
@@ -10837,7 +11099,7 @@ Usage:
|
|
|
10837
11099
|
odla-ai security report <job-id> [--json]
|
|
10838
11100
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
10839
11101
|
odla-ai security run [target] --self --ack-redacted-source
|
|
10840
|
-
odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
11102
|
+
odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
10841
11103
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
10842
11104
|
odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
|
|
10843
11105
|
odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
|
|
@@ -10854,7 +11116,7 @@ function printHelp(output = console) {
|
|
|
10854
11116
|
output.log(`odla-ai
|
|
10855
11117
|
${USAGE_SECTION}
|
|
10856
11118
|
Commands:
|
|
10857
|
-
auth Start a fresh, exact-project agent authorization
|
|
11119
|
+
auth Start a fresh, exact-project agent authorization for human review.
|
|
10858
11120
|
The email is the signed-in odla account, never git or GitHub
|
|
10859
11121
|
identity. The approval screen confirms the agent name first.
|
|
10860
11122
|
agent Inspect durable agent wakeups and explicitly requeue a
|
|
@@ -10915,6 +11177,9 @@ Commands:
|
|
|
10915
11177
|
\u2014 no secret is ever copied between people.
|
|
10916
11178
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
10917
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.
|
|
10918
11183
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
10919
11184
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10920
11185
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
@@ -10984,23 +11249,25 @@ Safety:
|
|
|
10984
11249
|
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
10985
11250
|
context, which beats project config. There is no ambient current context.
|
|
10986
11251
|
"context show" reports only provenance and cache state and never authenticates.
|
|
10987
|
-
Every real CLI handshake prints one canonical /studio?code= approval URL
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
11252
|
+
Every real CLI handshake prints one canonical /studio?code= approval URL.
|
|
11253
|
+
Interactive humans may let the CLI attempt its best-effort browser launch.
|
|
11254
|
+
Agent-driven commands must pass --no-open --wait 600, immediately surface the
|
|
11255
|
+
exact URL and code as a clickable human approval action, and keep that CLI
|
|
11256
|
+
process alive. Wait only on that same process: the CLI owns protocol polling.
|
|
11257
|
+
Do not call OS open, use browser control, curl handshake endpoints, build a
|
|
11258
|
+
shell wait loop, detach the command, or start a substitute handshake. The
|
|
11259
|
+
device code remains only in the running process. If the process exits 75, its
|
|
11260
|
+
old code cannot be collected; a later invocation requests a new code. Older
|
|
11261
|
+
clients' persisted pending state is discarded.
|
|
10996
11262
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
10997
11263
|
The email is a non-secret identity hint: never provide a password or session
|
|
10998
11264
|
token. It is the email shown by the signed-in odla account \u2014 never infer it
|
|
10999
11265
|
from git config, a commit author, or GitHub. The matching account must already exist, be signed in, explicitly
|
|
11000
11266
|
review the exact code, and finish any current request before claiming another.
|
|
11001
|
-
Use "auth login --app <id> --email <odla-account>
|
|
11002
|
-
a deliberate fresh request; it ignores cached
|
|
11003
|
-
focused authorization sequence
|
|
11267
|
+
Use "auth login --app <id> --email <odla-account> --no-open --wait 600" when
|
|
11268
|
+
an outside agent needs a deliberate fresh request; it ignores cached
|
|
11269
|
+
credentials and uses the same focused authorization sequence as every
|
|
11270
|
+
first-time command.
|
|
11004
11271
|
If provision reports that the current agent principal has no live app.manage
|
|
11005
11272
|
grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
|
|
11006
11273
|
the local cache, prints and opens a fresh exact-project owner-review URL, then
|
|
@@ -11013,6 +11280,17 @@ Safety:
|
|
|
11013
11280
|
approval and credential hashes live in odla-ai/db. The host
|
|
11014
11281
|
credential is never written under .odla/; it exists only in the foreground
|
|
11015
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.
|
|
11016
11294
|
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
11017
11295
|
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
11018
11296
|
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
@@ -11414,7 +11692,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
11414
11692
|
throw new WatchRemoteError(cursor, error);
|
|
11415
11693
|
}
|
|
11416
11694
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11417
|
-
const result =
|
|
11695
|
+
const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
11418
11696
|
throw new WatchTimeoutError(result.cursor);
|
|
11419
11697
|
}
|
|
11420
11698
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -11455,7 +11733,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
11455
11733
|
});
|
|
11456
11734
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
11457
11735
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
11458
|
-
return
|
|
11736
|
+
return report2(ctx, parsed, {
|
|
11459
11737
|
found: true,
|
|
11460
11738
|
cursor,
|
|
11461
11739
|
events: matching,
|
|
@@ -11482,13 +11760,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
11482
11760
|
}
|
|
11483
11761
|
if (page2.hasMore) continue;
|
|
11484
11762
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11485
|
-
return
|
|
11763
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
11486
11764
|
}
|
|
11487
11765
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
11488
11766
|
await sleep(wait2);
|
|
11489
11767
|
}
|
|
11490
11768
|
}
|
|
11491
|
-
function
|
|
11769
|
+
function report2(ctx, parsed, result) {
|
|
11492
11770
|
if (ctx.json) {
|
|
11493
11771
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
11494
11772
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -12155,7 +12433,7 @@ function eventLabel(event) {
|
|
|
12155
12433
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
12156
12434
|
return body || event.payload.entityId;
|
|
12157
12435
|
}
|
|
12158
|
-
function
|
|
12436
|
+
function report3(ctx, parsed, result) {
|
|
12159
12437
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
12160
12438
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
12161
12439
|
for (const event of result.events ?? []) {
|
|
@@ -12215,7 +12493,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
12215
12493
|
});
|
|
12216
12494
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
12217
12495
|
if (deadline !== void 0 && now() >= deadline) {
|
|
12218
|
-
return
|
|
12496
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
12219
12497
|
}
|
|
12220
12498
|
const backoff = Math.min(
|
|
12221
12499
|
MAX_BACKOFF_MS2,
|
|
@@ -12256,7 +12534,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
12256
12534
|
cursor,
|
|
12257
12535
|
serverTime: current.serverTime
|
|
12258
12536
|
});
|
|
12259
|
-
return
|
|
12537
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
12260
12538
|
}
|
|
12261
12539
|
if (current.events.length > 0) {
|
|
12262
12540
|
jsonl2(ctx, parsed, {
|
|
@@ -12275,7 +12553,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
12275
12553
|
}
|
|
12276
12554
|
if (current.hasMore) continue;
|
|
12277
12555
|
if (deadline !== void 0 && now() >= deadline) {
|
|
12278
|
-
return
|
|
12556
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
12279
12557
|
}
|
|
12280
12558
|
await sleep(
|
|
12281
12559
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -12880,8 +13158,8 @@ function printO11yStatus(status, out) {
|
|
|
12880
13158
|
out.log(
|
|
12881
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`
|
|
12882
13160
|
);
|
|
12883
|
-
for (const
|
|
12884
|
-
out.log(
|
|
13161
|
+
for (const line2 of providerCapacityLines(status.providerCapacity)) {
|
|
13162
|
+
out.log(line2);
|
|
12885
13163
|
}
|
|
12886
13164
|
const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
|
|
12887
13165
|
const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
|
|
@@ -14046,7 +14324,11 @@ var init_surface = __esm({
|
|
|
14046
14324
|
bug: { create: {}, list: {}, report: {} },
|
|
14047
14325
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
14048
14326
|
capabilities: {},
|
|
14049
|
-
code: {
|
|
14327
|
+
code: {
|
|
14328
|
+
connect: {},
|
|
14329
|
+
grant: { request: {}, list: {}, approve: {}, revoke: {} },
|
|
14330
|
+
repository: { show: {}, list: {}, bind: {} }
|
|
14331
|
+
},
|
|
14050
14332
|
config: { diff: {}, plan: {}, apply: {} },
|
|
14051
14333
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
14052
14334
|
credentials: { list: {}, revoke: {} },
|
|
@@ -14286,8 +14568,8 @@ function parseRunbook(text3, slug) {
|
|
|
14286
14568
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
|
|
14287
14569
|
if (fm) {
|
|
14288
14570
|
rest = text3.slice(fm[0].length);
|
|
14289
|
-
for (const
|
|
14290
|
-
const pair = /^(\w+)\s*:\s*(.+)$/.exec(
|
|
14571
|
+
for (const line2 of fm[1].split(/\r?\n/)) {
|
|
14572
|
+
const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
|
|
14291
14573
|
if (!pair) continue;
|
|
14292
14574
|
const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
|
|
14293
14575
|
if (pair[1] === "summary") meta.summary = value2;
|
|
@@ -14408,18 +14690,18 @@ function areaOf(path) {
|
|
|
14408
14690
|
return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
|
|
14409
14691
|
}
|
|
14410
14692
|
function scanHunk(lines, into) {
|
|
14411
|
-
for (const [i,
|
|
14412
|
-
const decl = DECL.exec(
|
|
14693
|
+
for (const [i, line2] of lines.entries()) {
|
|
14694
|
+
const decl = DECL.exec(line2);
|
|
14413
14695
|
if (decl?.[1]) {
|
|
14414
14696
|
into.add(decl[1]);
|
|
14415
14697
|
continue;
|
|
14416
14698
|
}
|
|
14417
|
-
const named = NAMED.exec(
|
|
14699
|
+
const named = NAMED.exec(line2);
|
|
14418
14700
|
if (named?.[1]) {
|
|
14419
14701
|
for (const name of namedExports(named[1])) into.add(name);
|
|
14420
14702
|
continue;
|
|
14421
14703
|
}
|
|
14422
|
-
if (!JSDOC.test(
|
|
14704
|
+
if (!JSDOC.test(line2)) continue;
|
|
14423
14705
|
for (let j = i + 1; j < lines.length && j < i + 40; j++) {
|
|
14424
14706
|
const found = ANY_DECL.exec(lines[j]);
|
|
14425
14707
|
if (found?.[1]) {
|
|
@@ -14437,8 +14719,8 @@ function parseDiff(diff) {
|
|
|
14437
14719
|
if (current && hunk.length) scanHunk(hunk, current.exports);
|
|
14438
14720
|
hunk = [];
|
|
14439
14721
|
};
|
|
14440
|
-
for (const
|
|
14441
|
-
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(
|
|
14722
|
+
for (const line2 of diff.split("\n")) {
|
|
14723
|
+
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
|
|
14442
14724
|
if (header) {
|
|
14443
14725
|
flush();
|
|
14444
14726
|
const path = header[2] ?? header[1];
|
|
@@ -14446,11 +14728,11 @@ function parseDiff(diff) {
|
|
|
14446
14728
|
files.set(path, current);
|
|
14447
14729
|
continue;
|
|
14448
14730
|
}
|
|
14449
|
-
if (
|
|
14731
|
+
if (line2.startsWith("@@")) {
|
|
14450
14732
|
flush();
|
|
14451
14733
|
continue;
|
|
14452
14734
|
}
|
|
14453
|
-
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(
|
|
14735
|
+
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
|
|
14454
14736
|
}
|
|
14455
14737
|
flush();
|
|
14456
14738
|
return [...files.values()];
|
|
@@ -14543,7 +14825,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
14543
14825
|
return "";
|
|
14544
14826
|
}
|
|
14545
14827
|
let out = "";
|
|
14546
|
-
for (const path of listed.split("\n").map((
|
|
14828
|
+
for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
|
|
14547
14829
|
out += `diff --git a/${path} b/${path}
|
|
14548
14830
|
--- /dev/null
|
|
14549
14831
|
+++ b/${path}
|
|
@@ -14556,7 +14838,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
14556
14838
|
continue;
|
|
14557
14839
|
}
|
|
14558
14840
|
out += `@@ -0,0 +1,${body.split("\n").length} @@
|
|
14559
|
-
${body.split("\n").map((
|
|
14841
|
+
${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
14560
14842
|
`;
|
|
14561
14843
|
}
|
|
14562
14844
|
return out;
|
|
@@ -14603,7 +14885,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
14603
14885
|
}
|
|
14604
14886
|
return out;
|
|
14605
14887
|
}
|
|
14606
|
-
function
|
|
14888
|
+
function report4(ctx, impacts) {
|
|
14607
14889
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
14608
14890
|
ctx.out.log(
|
|
14609
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.`
|
|
@@ -14641,7 +14923,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
14641
14923
|
}
|
|
14642
14924
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
14643
14925
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
14644
|
-
|
|
14926
|
+
report4(ctx, impacts);
|
|
14645
14927
|
}
|
|
14646
14928
|
var import_node_child_process6, import_node_fs20, import_node_path18, SOURCE3, editHint;
|
|
14647
14929
|
var init_runbook_impact = __esm({
|
|
@@ -14660,7 +14942,7 @@ var init_runbook_impact = __esm({
|
|
|
14660
14942
|
|
|
14661
14943
|
// src/runbook-lint.ts
|
|
14662
14944
|
function invocationsIn(body) {
|
|
14663
|
-
const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
|
|
14945
|
+
const re = /(`|npx[^\S\n]+(?:--yes[^\S\n]+)?)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
|
|
14664
14946
|
const found = [];
|
|
14665
14947
|
for (const match of body.matchAll(re)) {
|
|
14666
14948
|
if (!match[1]) continue;
|
|
@@ -14741,7 +15023,7 @@ async function runbookSearch(ctx, query, all, limit) {
|
|
|
14741
15023
|
for (const [i, hit] of result.hits.entries()) {
|
|
14742
15024
|
if (i) ctx.out.log("");
|
|
14743
15025
|
ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
|
|
14744
|
-
for (const
|
|
15026
|
+
for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
|
|
14745
15027
|
ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
|
|
14746
15028
|
}
|
|
14747
15029
|
}
|
|
@@ -14894,6 +15176,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
14894
15176
|
};
|
|
14895
15177
|
}
|
|
14896
15178
|
const needsCapability = WRITES2.has(action2) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
|
|
15179
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
14897
15180
|
const token = needsCapability ? await getScopedPlatformToken({
|
|
14898
15181
|
platform: cfg.platformUrl,
|
|
14899
15182
|
scope: "platform:runbook:write",
|
|
@@ -14901,6 +15184,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
14901
15184
|
label: `odla CLI (runbook ${action2})`,
|
|
14902
15185
|
fetch: doFetch,
|
|
14903
15186
|
stdout: out,
|
|
15187
|
+
open,
|
|
14904
15188
|
openApprovalUrl: deps.openUrl,
|
|
14905
15189
|
// A project, named context, or the global operator context owns the
|
|
14906
15190
|
// exact-scope cache; it never follows an arbitrary shell directory.
|
|
@@ -14912,11 +15196,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
14912
15196
|
configPath: cfg.configPath,
|
|
14913
15197
|
token: stringOpt(parsed.options.token),
|
|
14914
15198
|
email: stringOpt(parsed.options.email),
|
|
14915
|
-
|
|
14916
|
-
// CI and SSH). Hardcoding `false` meant a first-time handshake printed
|
|
14917
|
-
// a link and opened nothing — the one moment a browser is the whole
|
|
14918
|
-
// point.
|
|
14919
|
-
open: void 0
|
|
15199
|
+
open
|
|
14920
15200
|
},
|
|
14921
15201
|
doFetch,
|
|
14922
15202
|
out
|
|
@@ -15064,7 +15344,8 @@ var init_runbook_command = __esm({
|
|
|
15064
15344
|
"base",
|
|
15065
15345
|
"requires",
|
|
15066
15346
|
"platform",
|
|
15067
|
-
"context"
|
|
15347
|
+
"context",
|
|
15348
|
+
"open"
|
|
15068
15349
|
];
|
|
15069
15350
|
WRITES2 = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
|
|
15070
15351
|
}
|
|
@@ -15168,31 +15449,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
15168
15449
|
url.searchParams.set("job", job.jobId);
|
|
15169
15450
|
out.log(` Studio: ${url.toString()}`);
|
|
15170
15451
|
}
|
|
15171
|
-
function printHostedReport(out,
|
|
15172
|
-
out.log(`security report ${
|
|
15173
|
-
out.log(` coverage: ${
|
|
15174
|
-
out.log(` findings: confirmed=${
|
|
15175
|
-
out.log(` discovery: ${
|
|
15176
|
-
out.log(` validation: ${
|
|
15177
|
-
for (const finding of
|
|
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) {
|
|
15178
15459
|
const location = finding.locations[0];
|
|
15179
15460
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
15180
15461
|
}
|
|
15181
|
-
for (const limitation of
|
|
15462
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
15182
15463
|
}
|
|
15183
|
-
function enforceHostedReportGate(
|
|
15464
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
15184
15465
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
15185
15466
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
15186
15467
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
15187
15468
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
15188
|
-
const confirmed =
|
|
15189
|
-
const leads = failOnCandidates ?
|
|
15190
|
-
const incomplete =
|
|
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;
|
|
15191
15472
|
if (confirmed.length || leads.length || incomplete) {
|
|
15192
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
15473
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
15193
15474
|
}
|
|
15194
15475
|
if (emitSuccess) {
|
|
15195
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
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.`);
|
|
15196
15477
|
}
|
|
15197
15478
|
}
|
|
15198
15479
|
function printHostedSecurityPlanRoute(out, label, route3) {
|
|
@@ -15277,17 +15558,17 @@ async function runHostedSecurity(options) {
|
|
|
15277
15558
|
allowNetwork: false
|
|
15278
15559
|
}
|
|
15279
15560
|
});
|
|
15280
|
-
const
|
|
15281
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
15282
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
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);
|
|
15283
15564
|
await hosted.complete({
|
|
15284
15565
|
reportDigest,
|
|
15285
|
-
coverageStatus:
|
|
15286
|
-
confirmed:
|
|
15287
|
-
candidates:
|
|
15566
|
+
coverageStatus: report5.coverageStatus,
|
|
15567
|
+
confirmed: report5.metrics.confirmed,
|
|
15568
|
+
candidates: report5.metrics.candidates
|
|
15288
15569
|
}, { signal: options.signal });
|
|
15289
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
15290
|
-
return Object.freeze({ report:
|
|
15570
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
15571
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
15291
15572
|
}
|
|
15292
15573
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
15293
15574
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -15313,14 +15594,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
15313
15594
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
15314
15595
|
return { ...profile, maxHuntTasks };
|
|
15315
15596
|
}
|
|
15316
|
-
function printSummary(out, appId, env, run,
|
|
15317
|
-
const complete =
|
|
15597
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
15598
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
15318
15599
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
15319
15600
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
15320
15601
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
15321
|
-
out.log(` coverage: ${
|
|
15322
|
-
if (
|
|
15323
|
-
out.log(` findings: confirmed=${
|
|
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}`);
|
|
15324
15605
|
out.log(` report: ${(0, import_node_path20.resolve)(output, "REPORT.md")}`);
|
|
15325
15606
|
}
|
|
15326
15607
|
function formatBudget(usage) {
|
|
@@ -15592,13 +15873,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
15592
15873
|
}
|
|
15593
15874
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
15594
15875
|
}
|
|
15595
|
-
const
|
|
15876
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
15596
15877
|
if (parsed.options.json === true) {
|
|
15597
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
15878
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
15598
15879
|
} else {
|
|
15599
|
-
printHostedReport(context.stdout,
|
|
15880
|
+
printHostedReport(context.stdout, report5);
|
|
15600
15881
|
}
|
|
15601
|
-
enforceHostedReportGate(
|
|
15882
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
15602
15883
|
}
|
|
15603
15884
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
15604
15885
|
if (parsed.options.source === true) {
|
|
@@ -15666,13 +15947,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
15666
15947
|
});
|
|
15667
15948
|
enforceLocalGate(result.report, parsed);
|
|
15668
15949
|
}
|
|
15669
|
-
function enforceLocalGate(
|
|
15950
|
+
function enforceLocalGate(report5, parsed) {
|
|
15670
15951
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
15671
15952
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
15672
15953
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
15673
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
15674
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
15675
|
-
const incomplete =
|
|
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;
|
|
15676
15957
|
if (confirmed.length || leads.length || incomplete) {
|
|
15677
15958
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
15678
15959
|
}
|
|
@@ -15727,9 +16008,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
15727
16008
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
15728
16009
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
15729
16010
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
15730
|
-
const
|
|
15731
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
15732
|
-
else printHostedReport(context.stdout,
|
|
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);
|
|
15733
16014
|
return;
|
|
15734
16015
|
}
|
|
15735
16016
|
if (sub !== "run") {
|