@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/index.cjs
CHANGED
|
@@ -127,14 +127,15 @@ function approvalLines(prompt) {
|
|
|
127
127
|
lines.push(` No browser was opened (${prompt.browserSkipped}).`);
|
|
128
128
|
}
|
|
129
129
|
lines.push("");
|
|
130
|
-
lines.push(" AGENTS:
|
|
131
|
-
lines.push("
|
|
132
|
-
lines.push("
|
|
130
|
+
lines.push(" AGENTS: immediately give the human this URL as a clickable approval action and repeat the code.");
|
|
131
|
+
lines.push(" Keep this CLI process running and wait on this same process; the CLI owns protocol polling.");
|
|
132
|
+
lines.push(" Do not use OS open, browser control, curl, a shell wait loop, detached execution, or a substitute handshake.");
|
|
133
|
+
lines.push(" You cannot approve it yourself. If this process exits, a later invocation creates a new code.");
|
|
133
134
|
lines.push("");
|
|
134
135
|
return lines;
|
|
135
136
|
}
|
|
136
137
|
function printApproval(out, prompt) {
|
|
137
|
-
for (const
|
|
138
|
+
for (const line2 of approvalLines(prompt)) out.error(line2);
|
|
138
139
|
}
|
|
139
140
|
function reminderLines(prompt) {
|
|
140
141
|
return [
|
|
@@ -220,12 +221,12 @@ function approvalHint(pending) {
|
|
|
220
221
|
}
|
|
221
222
|
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
222
223
|
const timer = setInterval(() => {
|
|
223
|
-
for (const
|
|
224
|
+
for (const line2 of reminderLines({
|
|
224
225
|
userCode: pending.userCode,
|
|
225
226
|
approvalUrl: pending.approvalUrl,
|
|
226
227
|
minutesLeft: minutesLeft(pending.expiresAt)
|
|
227
228
|
}))
|
|
228
|
-
out.log(
|
|
229
|
+
out.log(line2);
|
|
229
230
|
}, periodMs);
|
|
230
231
|
timer.unref?.();
|
|
231
232
|
return () => clearInterval(timer);
|
|
@@ -286,9 +287,9 @@ function mergeCredential(current, update) {
|
|
|
286
287
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
287
288
|
const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
|
|
288
289
|
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
289
|
-
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((
|
|
290
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
290
291
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
291
|
-
const missing = wanted.filter((
|
|
292
|
+
const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
|
|
292
293
|
if (missing.length === 0) return;
|
|
293
294
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
294
295
|
(0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
@@ -320,7 +321,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
320
321
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
321
322
|
}
|
|
322
323
|
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
323
|
-
const retained = existing.split(/\r?\n/).filter((
|
|
324
|
+
const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
|
|
324
325
|
while (retained.at(-1) === "") retained.pop();
|
|
325
326
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
326
327
|
|
|
@@ -340,8 +341,8 @@ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
|
340
341
|
"ODLA_O11Y_VERSION",
|
|
341
342
|
"ODLA_O11Y_TOKEN"
|
|
342
343
|
]);
|
|
343
|
-
function isManagedDevVar(
|
|
344
|
-
const match =
|
|
344
|
+
function isManagedDevVar(line2) {
|
|
345
|
+
const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
345
346
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
346
347
|
}
|
|
347
348
|
function writePrivateText(path, text3) {
|
|
@@ -2195,7 +2196,7 @@ function chooseIdMode(options, rows) {
|
|
|
2195
2196
|
async function appImport(options) {
|
|
2196
2197
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2197
2198
|
const out = options.stdout ?? console;
|
|
2198
|
-
const say = options.json ? (
|
|
2199
|
+
const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
|
|
2199
2200
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
2200
2201
|
const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
|
|
2201
2202
|
const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
|
|
@@ -2520,7 +2521,7 @@ async function designUnpack(parsed, deps) {
|
|
|
2520
2521
|
);
|
|
2521
2522
|
return;
|
|
2522
2523
|
}
|
|
2523
|
-
for (const
|
|
2524
|
+
for (const line2 of describeUnpack(result, outDir)) out.log(line2);
|
|
2524
2525
|
}
|
|
2525
2526
|
async function brandCommand(parsed, deps) {
|
|
2526
2527
|
const subject = parsed.positionals[1];
|
|
@@ -4481,7 +4482,7 @@ async function doctor(options) {
|
|
|
4481
4482
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
|
|
4482
4483
|
const contract = resolveSecretContract(cfg);
|
|
4483
4484
|
out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
|
|
4484
|
-
for (const
|
|
4485
|
+
for (const line2 of formatSecretContract(contract)) out.log(line2);
|
|
4485
4486
|
if (cfg.services.includes("calendar")) {
|
|
4486
4487
|
const calendar = cfg.envs.map((env) => {
|
|
4487
4488
|
const resolved = calendarServiceConfig(cfg, env);
|
|
@@ -4850,10 +4851,10 @@ async function secretsStatus(options) {
|
|
|
4850
4851
|
}
|
|
4851
4852
|
const body = await res.json();
|
|
4852
4853
|
const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
|
|
4853
|
-
const
|
|
4854
|
-
if (options.json) out.log(JSON.stringify(
|
|
4855
|
-
else printReport(
|
|
4856
|
-
return
|
|
4854
|
+
const report5 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
|
|
4855
|
+
if (options.json) out.log(JSON.stringify(report5, null, 2));
|
|
4856
|
+
else printReport(report5, out);
|
|
4857
|
+
return report5;
|
|
4857
4858
|
}
|
|
4858
4859
|
function buildReport(appId, env, tenant, contract, stored) {
|
|
4859
4860
|
const declared = new Set(contract.map((secret) => secret.name));
|
|
@@ -4870,25 +4871,25 @@ function buildReport(appId, env, tenant, contract, stored) {
|
|
|
4870
4871
|
const ok = rows.every((row) => row.state !== "missing" || !row.required);
|
|
4871
4872
|
return { app: appId, env, tenant, secrets: rows, ok };
|
|
4872
4873
|
}
|
|
4873
|
-
function printReport(
|
|
4874
|
-
out.log(`${
|
|
4875
|
-
if (
|
|
4874
|
+
function printReport(report5, out) {
|
|
4875
|
+
out.log(`${report5.app} (${report5.tenant})`);
|
|
4876
|
+
if (report5.secrets.length === 0) {
|
|
4876
4877
|
out.log(" no secrets declared and none stored");
|
|
4877
4878
|
return;
|
|
4878
4879
|
}
|
|
4879
|
-
for (const row of
|
|
4880
|
+
for (const row of report5.secrets) {
|
|
4880
4881
|
const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
|
|
4881
4882
|
const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
|
|
4882
4883
|
out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
|
|
4883
4884
|
}
|
|
4884
|
-
const missing =
|
|
4885
|
+
const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
|
|
4885
4886
|
if (missing.length) {
|
|
4886
4887
|
out.log("");
|
|
4887
4888
|
for (const row of missing) {
|
|
4888
|
-
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${
|
|
4889
|
+
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report5.env} --stdin"`);
|
|
4889
4890
|
}
|
|
4890
4891
|
}
|
|
4891
|
-
if (
|
|
4892
|
+
if (report5.secrets.some((row) => row.state === "reserved")) {
|
|
4892
4893
|
out.log("");
|
|
4893
4894
|
out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
|
|
4894
4895
|
}
|
|
@@ -4910,18 +4911,29 @@ For work that creates an odla app or adds odla services, read and follow
|
|
|
4910
4911
|
\`.agents/skills/odla-o11y-debug/SKILL.md\`.
|
|
4911
4912
|
|
|
4912
4913
|
Track the work in odla's PM as you go. Before project-mutating work, run
|
|
4913
|
-
\`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
|
|
4914
|
+
\`npx --yes @odla-ai/cli@latest pm next --app <appId>\`, confirm alignment to an open goal,
|
|
4914
4915
|
and atomically claim a refined Ready task. Record decisions when you make them
|
|
4915
4916
|
and file bugs when you notice them. The conventions and the full command set are
|
|
4916
4917
|
in \`.agents/skills/odla/references/pm.md\`.
|
|
4917
4918
|
|
|
4918
|
-
Use the human's signed-in odla account email for device authorization;
|
|
4919
|
-
infer it from git config, commit metadata, or GitHub. If authorization is
|
|
4920
|
-
already active, run
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4919
|
+
Use the human's known signed-in odla account email for device authorization;
|
|
4920
|
+
never infer it from git config, commit metadata, or GitHub. If authorization is
|
|
4921
|
+
not already active, run this exact command as one foreground process:
|
|
4922
|
+
|
|
4923
|
+
\`npx --yes @odla-ai/cli@latest auth login --app <appId> --email <odla-account> --no-open --wait 600\`
|
|
4924
|
+
|
|
4925
|
+
When it prints the approval URL and code, immediately give the human a clickable
|
|
4926
|
+
link, name the code they must verify, and tell them to click **Approve**. Keep
|
|
4927
|
+
that CLI process running and wait on the same tool process for its result. The
|
|
4928
|
+
CLI owns protocol polling: never call the OS \`open\` command, use browser
|
|
4929
|
+
control, curl a handshake endpoint, build a shell wait/poll loop, detach the
|
|
4930
|
+
process, or start another handshake while it is alive. The device code exists
|
|
4931
|
+
only in that process. If it exits 75 before approval, the old code cannot be
|
|
4932
|
+
collected; start one fresh foreground command and surface its new URL.
|
|
4933
|
+
|
|
4934
|
+
Never file an odla project or product defect in GitHub Issues: run
|
|
4935
|
+
\`npx --yes @odla-ai/cli@latest bug report --app <appId> ...\` so the bug lands
|
|
4936
|
+
in odla PM with the rest of the project's goals, tasks, and decisions.
|
|
4925
4937
|
|
|
4926
4938
|
The setup runbooks and their references are installed in this repository, pinned
|
|
4927
4939
|
to this CLI version. Use them as your setup context.
|
|
@@ -4933,9 +4945,9 @@ When this repository has an \`appId\`, pass it: app-scoped discovery includes
|
|
|
4933
4945
|
that project's instructions plus the shared platform procedures.
|
|
4934
4946
|
|
|
4935
4947
|
\`\`\`
|
|
4936
|
-
npx odla-ai runbook ask "<what you are about to do>" --app <appId>
|
|
4937
|
-
npx odla-ai runbook list
|
|
4938
|
-
npx odla-ai runbook get <slug>
|
|
4948
|
+
npx --yes @odla-ai/cli@latest runbook ask "<what you are about to do>" --app <appId>
|
|
4949
|
+
npx --yes @odla-ai/cli@latest runbook list
|
|
4950
|
+
npx --yes @odla-ai/cli@latest runbook get <slug>
|
|
4939
4951
|
\`\`\`
|
|
4940
4952
|
|
|
4941
4953
|
Never scrape odla.ai HTML for any of this. The CLI reads the same content
|
|
@@ -4955,12 +4967,12 @@ function claudeAdapter(skill, canonical2) {
|
|
|
4955
4967
|
const lines = match[1].split(/\r?\n/);
|
|
4956
4968
|
const frontmatter = [];
|
|
4957
4969
|
let keepIndented = false;
|
|
4958
|
-
for (const
|
|
4959
|
-
if (
|
|
4960
|
-
frontmatter.push(
|
|
4961
|
-
keepIndented =
|
|
4962
|
-
} else if (keepIndented && /^\s+/.test(
|
|
4963
|
-
frontmatter.push(
|
|
4970
|
+
for (const line2 of lines) {
|
|
4971
|
+
if (line2.startsWith("name:") || line2.startsWith("description:")) {
|
|
4972
|
+
frontmatter.push(line2);
|
|
4973
|
+
keepIndented = line2.startsWith("description:");
|
|
4974
|
+
} else if (keepIndented && /^\s+/.test(line2)) {
|
|
4975
|
+
frontmatter.push(line2);
|
|
4964
4976
|
} else {
|
|
4965
4977
|
keepIndented = false;
|
|
4966
4978
|
}
|
|
@@ -6281,13 +6293,13 @@ async function createConversionRegistry(config) {
|
|
|
6281
6293
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
6282
6294
|
}
|
|
6283
6295
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
6284
|
-
const
|
|
6296
|
+
const get2 = (id2, kind) => {
|
|
6285
6297
|
const policy = policies.get(id2);
|
|
6286
6298
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
6287
6299
|
return policy;
|
|
6288
6300
|
};
|
|
6289
6301
|
const checked = (source, id2, kind) => {
|
|
6290
|
-
const policy =
|
|
6302
|
+
const policy = get2(id2, kind);
|
|
6291
6303
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
6292
6304
|
return policy;
|
|
6293
6305
|
};
|
|
@@ -7118,7 +7130,7 @@ var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
|
7118
7130
|
var FORBIDDEN = /^(?:GIT binary patch|Binary files |rename (?:from|to) |copy (?:from|to) |similarity index |old mode |new mode |deleted file mode 160000|new file mode 160000)/m;
|
|
7119
7131
|
function stripPatchEnvelope(patch2) {
|
|
7120
7132
|
if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
|
|
7121
|
-
const kept = patch2.split("\n").filter((
|
|
7133
|
+
const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
|
|
7122
7134
|
const stripped = kept.join("\n");
|
|
7123
7135
|
return /^diff --git /m.test(stripped) ? stripped : patch2;
|
|
7124
7136
|
}
|
|
@@ -7139,9 +7151,9 @@ function validateCodePatch(rawPatch, maxBytes) {
|
|
|
7139
7151
|
const paths = [];
|
|
7140
7152
|
const lines = patch2.split("\n");
|
|
7141
7153
|
for (let index = 0; index < lines.length; index += 1) {
|
|
7142
|
-
const
|
|
7143
|
-
if (!
|
|
7144
|
-
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(
|
|
7154
|
+
const line2 = lines[index];
|
|
7155
|
+
if (!line2.startsWith("diff --git ")) continue;
|
|
7156
|
+
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line2);
|
|
7145
7157
|
const path = match?.[1];
|
|
7146
7158
|
if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
|
|
7147
7159
|
validateRelativePath(path);
|
|
@@ -7174,9 +7186,9 @@ function resolveCodePath(workspaceDir, path) {
|
|
|
7174
7186
|
return target;
|
|
7175
7187
|
}
|
|
7176
7188
|
function describePatchFailure(patch2, detail) {
|
|
7177
|
-
const hunks = patch2.split("\n").filter((
|
|
7189
|
+
const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
|
|
7178
7190
|
const bodies = patch2.split(/^@@.*$/m).slice(1);
|
|
7179
|
-
const contextless = bodies.some((body) => !body.split("\n").some((
|
|
7191
|
+
const contextless = bodies.some((body) => !body.split("\n").some((line2) => line2.startsWith(" ") && line2.trim().length > 0));
|
|
7180
7192
|
const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
|
|
7181
7193
|
return `patch did not apply: ${detail}${hint}`;
|
|
7182
7194
|
}
|
|
@@ -9737,11 +9749,237 @@ function record6(value2) {
|
|
|
9737
9749
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
9738
9750
|
}
|
|
9739
9751
|
|
|
9752
|
+
// src/code-grant-command.ts
|
|
9753
|
+
var ACTIONS = ["request", "list", "approve", "revoke"];
|
|
9754
|
+
var isAction = (value2) => ACTIONS.includes(value2 ?? "");
|
|
9755
|
+
async function fail2(response2, what) {
|
|
9756
|
+
const body = redactSecrets((await response2.text()).slice(0, 1e3));
|
|
9757
|
+
throw new Error(`${what} failed (${response2.status}): ${body}`);
|
|
9758
|
+
}
|
|
9759
|
+
function line(grant) {
|
|
9760
|
+
const who = grant.status === "granted" ? `granted by ${grant.approvedBy ?? "?"}` : `requested by ${grant.requestedBy}`;
|
|
9761
|
+
return `${grant.grantId} ${grant.appEnv} ${grant.owner}/${grant.name} ${grant.status} ${who}`;
|
|
9762
|
+
}
|
|
9763
|
+
function report(grants, out, json) {
|
|
9764
|
+
if (json) return out.log(JSON.stringify({ grants }, null, 2));
|
|
9765
|
+
if (!grants.length) return out.log("no repository grants for this app");
|
|
9766
|
+
out.log("GRANT ENV REPOSITORY STATUS DECIDED");
|
|
9767
|
+
for (const grant of grants) out.log(line(grant));
|
|
9768
|
+
const pending = grants.filter((grant) => grant.status === "requested");
|
|
9769
|
+
if (pending.length) {
|
|
9770
|
+
out.log(`
|
|
9771
|
+
${pending.length} awaiting approval. A human owner runs:`);
|
|
9772
|
+
for (const grant of pending) out.log(` odla-ai code grant approve ${grant.grantId} --env ${grant.appEnv}`);
|
|
9773
|
+
}
|
|
9774
|
+
}
|
|
9775
|
+
function grantsUrl(cfg, suffix = "") {
|
|
9776
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository-grants${suffix}`;
|
|
9777
|
+
}
|
|
9778
|
+
async function codeGrantCommand(parsed, deps = {}) {
|
|
9779
|
+
const action2 = parsed.positionals[2];
|
|
9780
|
+
if (!isAction(action2)) {
|
|
9781
|
+
throw new Error(
|
|
9782
|
+
`unknown code grant action "${action2 ?? ""}". Try "odla-ai code grant list --env dev".`
|
|
9783
|
+
);
|
|
9784
|
+
}
|
|
9785
|
+
const decides = action2 === "approve" || action2 === "revoke";
|
|
9786
|
+
assertArgs(parsed, ["config", "env", "json", "token", "email", "open"], decides ? 4 : 3);
|
|
9787
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
9788
|
+
const doFetch = deps.fetch ?? fetch;
|
|
9789
|
+
const out = deps.stdout ?? console;
|
|
9790
|
+
const env = stringOpt(parsed.options.env) ?? "dev";
|
|
9791
|
+
const json = parsed.options.json === true;
|
|
9792
|
+
const token = await getDeveloperToken(cfg, {
|
|
9793
|
+
configPath: cfg.configPath,
|
|
9794
|
+
token: stringOpt(parsed.options.token),
|
|
9795
|
+
email: stringOpt(parsed.options.email),
|
|
9796
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
9797
|
+
openApprovalUrl: deps.openUrl
|
|
9798
|
+
// Every verb here administers grants, which the registry gates on
|
|
9799
|
+
// app.manage — a token without it is refused as "not your app", which reads
|
|
9800
|
+
// like the wrong app rather than the wrong authority.
|
|
9801
|
+
//
|
|
9802
|
+
// Only `request` additionally asks for code.session. Approving is a human
|
|
9803
|
+
// act, and a token minted to approve has no business also carrying the
|
|
9804
|
+
// authority the approval is about.
|
|
9805
|
+
}, doFetch, out, {
|
|
9806
|
+
optionalProjectCapabilities: action2 === "request" ? ["app.manage", "code.session"] : ["app.manage"]
|
|
9807
|
+
});
|
|
9808
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
9809
|
+
if (action2 === "list") {
|
|
9810
|
+
const response3 = await doFetch(`${grantsUrl(cfg)}?env=${encodeURIComponent(env)}`, { headers: auth });
|
|
9811
|
+
if (!response3.ok) await fail2(response3, "repository grant list");
|
|
9812
|
+
const body2 = await response3.json();
|
|
9813
|
+
return report(body2.grants, out, json);
|
|
9814
|
+
}
|
|
9815
|
+
if (action2 === "request") {
|
|
9816
|
+
const response3 = await doFetch(grantsUrl(cfg), {
|
|
9817
|
+
method: "POST",
|
|
9818
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
9819
|
+
body: JSON.stringify({ env })
|
|
9820
|
+
});
|
|
9821
|
+
if (!response3.ok) await fail2(response3, "repository grant request");
|
|
9822
|
+
const { grant } = await response3.json();
|
|
9823
|
+
if (json) return out.log(JSON.stringify({ grant }, null, 2));
|
|
9824
|
+
out.log(`requested ${grant.owner}/${grant.name} (${grant.appEnv}) \u2014 ${grant.grantId}`);
|
|
9825
|
+
return out.log(grant.status === "granted" ? "already granted; nothing to approve" : `a human owner of ${cfg.app.id} approves it with:
|
|
9826
|
+
odla-ai code grant approve ${grant.grantId} --env ${env}`);
|
|
9827
|
+
}
|
|
9828
|
+
const grantId = parsed.positionals[3];
|
|
9829
|
+
if (!grantId) throw new Error(`code grant ${action2} requires the grant id from "odla-ai code grant list"`);
|
|
9830
|
+
const url = `${grantsUrl(cfg, `/${encodeURIComponent(grantId)}`)}`;
|
|
9831
|
+
const response2 = action2 === "approve" ? await doFetch(url, {
|
|
9832
|
+
method: "POST",
|
|
9833
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
9834
|
+
body: JSON.stringify({ env })
|
|
9835
|
+
}) : await doFetch(`${url}?env=${encodeURIComponent(env)}`, { method: "DELETE", headers: auth });
|
|
9836
|
+
if (!response2.ok) await fail2(response2, `repository grant ${action2}`);
|
|
9837
|
+
const body = await response2.json();
|
|
9838
|
+
if (json) return out.log(JSON.stringify(body, null, 2));
|
|
9839
|
+
out.log(action2 === "approve" && body.grant ? `granted ${body.grant.owner}/${body.grant.name} (${body.grant.appEnv}) to ${cfg.app.id}` : `revoked ${grantId}`);
|
|
9840
|
+
}
|
|
9841
|
+
|
|
9842
|
+
// src/code-repository-command.ts
|
|
9843
|
+
var ACTIONS2 = ["show", "list", "bind"];
|
|
9844
|
+
var isAction2 = (value2) => ACTIONS2.includes(value2 ?? "");
|
|
9845
|
+
async function get(doFetch, url, token, what) {
|
|
9846
|
+
const response2 = await doFetch(url, { headers: { authorization: `Bearer ${token}` } });
|
|
9847
|
+
if (!response2.ok) {
|
|
9848
|
+
const body = redactSecrets((await response2.text()).slice(0, 1e3));
|
|
9849
|
+
if (response2.status === 403 && body.includes("human_session_required")) {
|
|
9850
|
+
throw new Error(
|
|
9851
|
+
`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".`
|
|
9852
|
+
);
|
|
9853
|
+
}
|
|
9854
|
+
throw new Error(`${what} failed (${response2.status}): ${body}`);
|
|
9855
|
+
}
|
|
9856
|
+
return await response2.json();
|
|
9857
|
+
}
|
|
9858
|
+
async function catalogue(doFetch, platformUrl, token) {
|
|
9859
|
+
const account = await get(
|
|
9860
|
+
doFetch,
|
|
9861
|
+
`${platformUrl}/registry/code/github/account`,
|
|
9862
|
+
token,
|
|
9863
|
+
"GitHub account read"
|
|
9864
|
+
);
|
|
9865
|
+
const active = account.installations.filter((install2) => install2.status === "active");
|
|
9866
|
+
const found = [];
|
|
9867
|
+
for (const install2 of active) {
|
|
9868
|
+
const { repositories } = await get(
|
|
9869
|
+
doFetch,
|
|
9870
|
+
`${platformUrl}/registry/code/github/installations/${install2.installationId}/repositories`,
|
|
9871
|
+
token,
|
|
9872
|
+
`repository catalog for ${install2.accountLogin}`
|
|
9873
|
+
);
|
|
9874
|
+
for (const repository of repositories) {
|
|
9875
|
+
found.push({ ...repository, installationId: install2.installationId });
|
|
9876
|
+
}
|
|
9877
|
+
}
|
|
9878
|
+
if (!active.length) {
|
|
9879
|
+
throw new Error(
|
|
9880
|
+
"no active GitHub App installation. Install odla's GitHub App on the account that owns the repository first, from Studio \u2192 Code."
|
|
9881
|
+
);
|
|
9882
|
+
}
|
|
9883
|
+
return found;
|
|
9884
|
+
}
|
|
9885
|
+
function selectRepository(found, full) {
|
|
9886
|
+
const wanted = full.toLowerCase();
|
|
9887
|
+
const matches = found.filter((repository) => repository.fullName.toLowerCase() === wanted);
|
|
9888
|
+
if (matches.length === 1) return matches[0];
|
|
9889
|
+
if (!matches.length) {
|
|
9890
|
+
const near = found.filter((repository) => repository.name.toLowerCase() === wanted.split("/").pop());
|
|
9891
|
+
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.`);
|
|
9892
|
+
}
|
|
9893
|
+
throw new Error(
|
|
9894
|
+
`${full} is reachable through ${matches.length} installations (${matches.map((match) => match.installationId).join(", ")}); revoke the one you do not mean.`
|
|
9895
|
+
);
|
|
9896
|
+
}
|
|
9897
|
+
function repositoryUrl(cfg, suffix = "") {
|
|
9898
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository${suffix}`;
|
|
9899
|
+
}
|
|
9900
|
+
async function codeRepositoryCommand(parsed, deps = {}) {
|
|
9901
|
+
const action2 = parsed.positionals[2];
|
|
9902
|
+
if (!isAction2(action2)) {
|
|
9903
|
+
throw new Error(
|
|
9904
|
+
`unknown code repository action "${action2 ?? ""}". Try "odla-ai code repository show --env dev".`
|
|
9905
|
+
);
|
|
9906
|
+
}
|
|
9907
|
+
assertArgs(parsed, ["config", "env", "repo", "json", "token", "email", "open"], 3);
|
|
9908
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
9909
|
+
const doFetch = deps.fetch ?? fetch;
|
|
9910
|
+
const out = deps.stdout ?? console;
|
|
9911
|
+
const env = stringOpt(parsed.options.env) ?? "dev";
|
|
9912
|
+
const json = parsed.options.json === true;
|
|
9913
|
+
const token = await getDeveloperToken(cfg, {
|
|
9914
|
+
configPath: cfg.configPath,
|
|
9915
|
+
token: stringOpt(parsed.options.token),
|
|
9916
|
+
email: stringOpt(parsed.options.email),
|
|
9917
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
9918
|
+
openApprovalUrl: deps.openUrl
|
|
9919
|
+
}, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
|
|
9920
|
+
if (action2 === "show") {
|
|
9921
|
+
const body2 = await get(
|
|
9922
|
+
doFetch,
|
|
9923
|
+
`${repositoryUrl(cfg)}?env=${encodeURIComponent(env)}`,
|
|
9924
|
+
token,
|
|
9925
|
+
"repository read"
|
|
9926
|
+
);
|
|
9927
|
+
if (json) return out.log(JSON.stringify(body2, null, 2));
|
|
9928
|
+
const bound = body2.environmentRepository;
|
|
9929
|
+
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}".`);
|
|
9930
|
+
}
|
|
9931
|
+
const found = await catalogue(doFetch, cfg.platformUrl, token);
|
|
9932
|
+
if (action2 === "list") {
|
|
9933
|
+
if (json) return out.log(JSON.stringify({ repositories: found }, null, 2));
|
|
9934
|
+
if (!found.length) return out.log("no repositories reachable through your installations");
|
|
9935
|
+
out.log("REPOSITORY INSTALL DEFAULT READY");
|
|
9936
|
+
for (const repository of found) {
|
|
9937
|
+
out.log(`${repository.fullName} ${repository.installationId} ${repository.defaultBranch ?? "\u2014"} ${repository.ready ? "yes" : "empty"}`);
|
|
9938
|
+
}
|
|
9939
|
+
return;
|
|
9940
|
+
}
|
|
9941
|
+
const repo = stringOpt(parsed.options.repo);
|
|
9942
|
+
if (!repo?.includes("/")) throw new Error("code repository bind requires --repo owner/name");
|
|
9943
|
+
const selected = selectRepository(found, repo);
|
|
9944
|
+
if (!selected.ready) {
|
|
9945
|
+
throw new Error(`${selected.fullName} has no commits yet; GitHub cannot serve a default branch for an empty repository.`);
|
|
9946
|
+
}
|
|
9947
|
+
const response2 = await doFetch(repositoryUrl(cfg, "/bind"), {
|
|
9948
|
+
method: "POST",
|
|
9949
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
9950
|
+
body: JSON.stringify({ env, installationId: selected.installationId, repositoryId: selected.repositoryId })
|
|
9951
|
+
});
|
|
9952
|
+
if (!response2.ok) {
|
|
9953
|
+
throw new Error(`repository bind failed (${response2.status}): ${redactSecrets((await response2.text()).slice(0, 1e3))}`);
|
|
9954
|
+
}
|
|
9955
|
+
const body = await response2.json();
|
|
9956
|
+
if (json) return out.log(JSON.stringify(body, null, 2));
|
|
9957
|
+
out.log(`connected ${cfg.app.id} (${env}) to ${selected.fullName} @ ${selected.defaultBranch}`);
|
|
9958
|
+
out.log(`Next, so it may be worked on unattended:
|
|
9959
|
+
odla-ai code grant request --env ${env}`);
|
|
9960
|
+
}
|
|
9961
|
+
|
|
9740
9962
|
// src/code-command.ts
|
|
9741
9963
|
async function codeCommand(parsed, dependencies) {
|
|
9742
9964
|
const sub = parsed.positionals[1];
|
|
9965
|
+
if (sub === "grant") {
|
|
9966
|
+
return await codeGrantCommand(parsed, {
|
|
9967
|
+
fetch: dependencies.fetch,
|
|
9968
|
+
stdout: dependencies.stdout,
|
|
9969
|
+
openUrl: dependencies.openUrl
|
|
9970
|
+
});
|
|
9971
|
+
}
|
|
9972
|
+
if (sub === "repository") {
|
|
9973
|
+
return await codeRepositoryCommand(parsed, {
|
|
9974
|
+
fetch: dependencies.fetch,
|
|
9975
|
+
stdout: dependencies.stdout,
|
|
9976
|
+
openUrl: dependencies.openUrl
|
|
9977
|
+
});
|
|
9978
|
+
}
|
|
9743
9979
|
if (sub !== "connect") {
|
|
9744
|
-
throw new Error(
|
|
9980
|
+
throw new Error(
|
|
9981
|
+
`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev" or "odla-ai code grant list --env dev".`
|
|
9982
|
+
);
|
|
9745
9983
|
}
|
|
9746
9984
|
assertArgs(parsed, [
|
|
9747
9985
|
"config",
|
|
@@ -10087,8 +10325,8 @@ Usage:
|
|
|
10087
10325
|
odla-ai runbook revert <slug> --version <n> [--app <id>]
|
|
10088
10326
|
odla-ai runbook rm <slug> [--app <id>]
|
|
10089
10327
|
odla-ai capabilities [--json]
|
|
10090
|
-
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
10091
|
-
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
10328
|
+
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--no-open] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
10329
|
+
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--no-open] [--json]
|
|
10092
10330
|
odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
|
|
10093
10331
|
odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
10094
10332
|
[--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
|
|
@@ -10107,7 +10345,7 @@ Usage:
|
|
|
10107
10345
|
odla-ai security report <job-id> [--json]
|
|
10108
10346
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
10109
10347
|
odla-ai security run [target] --self --ack-redacted-source
|
|
10110
|
-
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]
|
|
10348
|
+
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]
|
|
10111
10349
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
10112
10350
|
odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
|
|
10113
10351
|
odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
|
|
@@ -10122,7 +10360,7 @@ function printHelp(output = console) {
|
|
|
10122
10360
|
output.log(`odla-ai
|
|
10123
10361
|
${USAGE_SECTION}
|
|
10124
10362
|
Commands:
|
|
10125
|
-
auth Start a fresh, exact-project agent authorization
|
|
10363
|
+
auth Start a fresh, exact-project agent authorization for human review.
|
|
10126
10364
|
The email is the signed-in odla account, never git or GitHub
|
|
10127
10365
|
identity. The approval screen confirms the agent name first.
|
|
10128
10366
|
agent Inspect durable agent wakeups and explicitly requeue a
|
|
@@ -10183,6 +10421,9 @@ Commands:
|
|
|
10183
10421
|
\u2014 no secret is ever copied between people.
|
|
10184
10422
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
10185
10423
|
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
10424
|
+
"code repository show" reports the repository an app works on (Studio or a
|
|
10425
|
+
human session connects one); "code grant request|list|approve|revoke" then
|
|
10426
|
+
governs unattended access: an agent may request, only a human may approve.
|
|
10186
10427
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
10187
10428
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10188
10429
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
@@ -10252,23 +10493,25 @@ Safety:
|
|
|
10252
10493
|
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
10253
10494
|
context, which beats project config. There is no ambient current context.
|
|
10254
10495
|
"context show" reports only provenance and cache state and never authenticates.
|
|
10255
|
-
Every real CLI handshake prints one canonical /studio?code= approval URL
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10496
|
+
Every real CLI handshake prints one canonical /studio?code= approval URL.
|
|
10497
|
+
Interactive humans may let the CLI attempt its best-effort browser launch.
|
|
10498
|
+
Agent-driven commands must pass --no-open --wait 600, immediately surface the
|
|
10499
|
+
exact URL and code as a clickable human approval action, and keep that CLI
|
|
10500
|
+
process alive. Wait only on that same process: the CLI owns protocol polling.
|
|
10501
|
+
Do not call OS open, use browser control, curl handshake endpoints, build a
|
|
10502
|
+
shell wait loop, detach the command, or start a substitute handshake. The
|
|
10503
|
+
device code remains only in the running process. If the process exits 75, its
|
|
10504
|
+
old code cannot be collected; a later invocation requests a new code. Older
|
|
10505
|
+
clients' persisted pending state is discarded.
|
|
10264
10506
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
10265
10507
|
The email is a non-secret identity hint: never provide a password or session
|
|
10266
10508
|
token. It is the email shown by the signed-in odla account \u2014 never infer it
|
|
10267
10509
|
from git config, a commit author, or GitHub. The matching account must already exist, be signed in, explicitly
|
|
10268
10510
|
review the exact code, and finish any current request before claiming another.
|
|
10269
|
-
Use "auth login --app <id> --email <odla-account>
|
|
10270
|
-
a deliberate fresh request; it ignores cached
|
|
10271
|
-
focused authorization sequence
|
|
10511
|
+
Use "auth login --app <id> --email <odla-account> --no-open --wait 600" when
|
|
10512
|
+
an outside agent needs a deliberate fresh request; it ignores cached
|
|
10513
|
+
credentials and uses the same focused authorization sequence as every
|
|
10514
|
+
first-time command.
|
|
10272
10515
|
If provision reports that the current agent principal has no live app.manage
|
|
10273
10516
|
grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
|
|
10274
10517
|
the local cache, prints and opens a fresh exact-project owner-review URL, then
|
|
@@ -10281,6 +10524,17 @@ Safety:
|
|
|
10281
10524
|
approval and credential hashes live in odla-ai/db. The host
|
|
10282
10525
|
credential is never written under .odla/; it exists only in the foreground
|
|
10283
10526
|
"code connect" process and is rotated by the next approved connection.
|
|
10527
|
+
"code repository bind" takes owner/name and resolves the two GitHub integers the
|
|
10528
|
+
bind route wants across every installation you have, refusing an ambiguous match
|
|
10529
|
+
rather than choosing one \u2014 the same repository name under two organizations is
|
|
10530
|
+
ordinary, and picking the first would connect an app to source nobody chose. It
|
|
10531
|
+
needs a signed-in human session; a CLI handshake token is a delegated agent
|
|
10532
|
+
credential by design, so an agent cannot choose the source it will be given.
|
|
10533
|
+
Unattended access is per repository and revocable on its own, but the connection
|
|
10534
|
+
IS the authorization: a repository is recorded for one exact app and environment
|
|
10535
|
+
only by a signed-in human choosing it. "code grant list" shows what is authorized;
|
|
10536
|
+
"revoke" stops unattended access without disconnecting the repository, and
|
|
10537
|
+
re-binding never resurrects it. Connecting sandbox never authorizes live.
|
|
10284
10538
|
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
10285
10539
|
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
10286
10540
|
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
@@ -10647,7 +10901,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10647
10901
|
throw new WatchRemoteError(cursor, error);
|
|
10648
10902
|
}
|
|
10649
10903
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10650
|
-
const result =
|
|
10904
|
+
const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10651
10905
|
throw new WatchTimeoutError(result.cursor);
|
|
10652
10906
|
}
|
|
10653
10907
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -10688,7 +10942,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10688
10942
|
});
|
|
10689
10943
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
10690
10944
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
10691
|
-
return
|
|
10945
|
+
return report2(ctx, parsed, {
|
|
10692
10946
|
found: true,
|
|
10693
10947
|
cursor,
|
|
10694
10948
|
events: matching,
|
|
@@ -10715,13 +10969,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10715
10969
|
}
|
|
10716
10970
|
if (page2.hasMore) continue;
|
|
10717
10971
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10718
|
-
return
|
|
10972
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10719
10973
|
}
|
|
10720
10974
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
10721
10975
|
await sleep(wait2);
|
|
10722
10976
|
}
|
|
10723
10977
|
}
|
|
10724
|
-
function
|
|
10978
|
+
function report2(ctx, parsed, result) {
|
|
10725
10979
|
if (ctx.json) {
|
|
10726
10980
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
10727
10981
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -11318,7 +11572,7 @@ function eventLabel(event) {
|
|
|
11318
11572
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
11319
11573
|
return body || event.payload.entityId;
|
|
11320
11574
|
}
|
|
11321
|
-
function
|
|
11575
|
+
function report3(ctx, parsed, result) {
|
|
11322
11576
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
11323
11577
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
11324
11578
|
for (const event of result.events ?? []) {
|
|
@@ -11378,7 +11632,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11378
11632
|
});
|
|
11379
11633
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
11380
11634
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11381
|
-
return
|
|
11635
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
11382
11636
|
}
|
|
11383
11637
|
const backoff = Math.min(
|
|
11384
11638
|
MAX_BACKOFF_MS2,
|
|
@@ -11419,7 +11673,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11419
11673
|
cursor,
|
|
11420
11674
|
serverTime: current.serverTime
|
|
11421
11675
|
});
|
|
11422
|
-
return
|
|
11676
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
11423
11677
|
}
|
|
11424
11678
|
if (current.events.length > 0) {
|
|
11425
11679
|
jsonl2(ctx, parsed, {
|
|
@@ -11438,7 +11692,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11438
11692
|
}
|
|
11439
11693
|
if (current.hasMore) continue;
|
|
11440
11694
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11441
|
-
return
|
|
11695
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
11442
11696
|
}
|
|
11443
11697
|
await sleep(
|
|
11444
11698
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -11975,8 +12229,8 @@ function printO11yStatus(status, out) {
|
|
|
11975
12229
|
out.log(
|
|
11976
12230
|
`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`
|
|
11977
12231
|
);
|
|
11978
|
-
for (const
|
|
11979
|
-
out.log(
|
|
12232
|
+
for (const line2 of providerCapacityLines(status.providerCapacity)) {
|
|
12233
|
+
out.log(line2);
|
|
11980
12234
|
}
|
|
11981
12235
|
const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
|
|
11982
12236
|
const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
|
|
@@ -13019,7 +13273,11 @@ var COMMAND_SURFACE = {
|
|
|
13019
13273
|
bug: { create: {}, list: {}, report: {} },
|
|
13020
13274
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
13021
13275
|
capabilities: {},
|
|
13022
|
-
code: {
|
|
13276
|
+
code: {
|
|
13277
|
+
connect: {},
|
|
13278
|
+
grant: { request: {}, list: {}, approve: {}, revoke: {} },
|
|
13279
|
+
repository: { show: {}, list: {}, bind: {} }
|
|
13280
|
+
},
|
|
13023
13281
|
config: { diff: {}, plan: {}, apply: {} },
|
|
13024
13282
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
13025
13283
|
credentials: { list: {}, revoke: {} },
|
|
@@ -13334,8 +13592,8 @@ function parseRunbook(text3, slug) {
|
|
|
13334
13592
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
|
|
13335
13593
|
if (fm) {
|
|
13336
13594
|
rest = text3.slice(fm[0].length);
|
|
13337
|
-
for (const
|
|
13338
|
-
const pair = /^(\w+)\s*:\s*(.+)$/.exec(
|
|
13595
|
+
for (const line2 of fm[1].split(/\r?\n/)) {
|
|
13596
|
+
const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
|
|
13339
13597
|
if (!pair) continue;
|
|
13340
13598
|
const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
|
|
13341
13599
|
if (pair[1] === "summary") meta.summary = value2;
|
|
@@ -13475,18 +13733,18 @@ function areaOf(path) {
|
|
|
13475
13733
|
return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
|
|
13476
13734
|
}
|
|
13477
13735
|
function scanHunk(lines, into) {
|
|
13478
|
-
for (const [i,
|
|
13479
|
-
const decl = DECL.exec(
|
|
13736
|
+
for (const [i, line2] of lines.entries()) {
|
|
13737
|
+
const decl = DECL.exec(line2);
|
|
13480
13738
|
if (decl?.[1]) {
|
|
13481
13739
|
into.add(decl[1]);
|
|
13482
13740
|
continue;
|
|
13483
13741
|
}
|
|
13484
|
-
const named = NAMED.exec(
|
|
13742
|
+
const named = NAMED.exec(line2);
|
|
13485
13743
|
if (named?.[1]) {
|
|
13486
13744
|
for (const name of namedExports(named[1])) into.add(name);
|
|
13487
13745
|
continue;
|
|
13488
13746
|
}
|
|
13489
|
-
if (!JSDOC.test(
|
|
13747
|
+
if (!JSDOC.test(line2)) continue;
|
|
13490
13748
|
for (let j = i + 1; j < lines.length && j < i + 40; j++) {
|
|
13491
13749
|
const found = ANY_DECL.exec(lines[j]);
|
|
13492
13750
|
if (found?.[1]) {
|
|
@@ -13504,8 +13762,8 @@ function parseDiff(diff) {
|
|
|
13504
13762
|
if (current && hunk.length) scanHunk(hunk, current.exports);
|
|
13505
13763
|
hunk = [];
|
|
13506
13764
|
};
|
|
13507
|
-
for (const
|
|
13508
|
-
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(
|
|
13765
|
+
for (const line2 of diff.split("\n")) {
|
|
13766
|
+
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
|
|
13509
13767
|
if (header) {
|
|
13510
13768
|
flush();
|
|
13511
13769
|
const path = header[2] ?? header[1];
|
|
@@ -13513,11 +13771,11 @@ function parseDiff(diff) {
|
|
|
13513
13771
|
files.set(path, current);
|
|
13514
13772
|
continue;
|
|
13515
13773
|
}
|
|
13516
|
-
if (
|
|
13774
|
+
if (line2.startsWith("@@")) {
|
|
13517
13775
|
flush();
|
|
13518
13776
|
continue;
|
|
13519
13777
|
}
|
|
13520
|
-
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(
|
|
13778
|
+
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
|
|
13521
13779
|
}
|
|
13522
13780
|
flush();
|
|
13523
13781
|
return [...files.values()];
|
|
@@ -13580,7 +13838,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
13580
13838
|
return "";
|
|
13581
13839
|
}
|
|
13582
13840
|
let out = "";
|
|
13583
|
-
for (const path of listed.split("\n").map((
|
|
13841
|
+
for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
|
|
13584
13842
|
out += `diff --git a/${path} b/${path}
|
|
13585
13843
|
--- /dev/null
|
|
13586
13844
|
+++ b/${path}
|
|
@@ -13593,7 +13851,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
13593
13851
|
continue;
|
|
13594
13852
|
}
|
|
13595
13853
|
out += `@@ -0,0 +1,${body.split("\n").length} @@
|
|
13596
|
-
${body.split("\n").map((
|
|
13854
|
+
${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
13597
13855
|
`;
|
|
13598
13856
|
}
|
|
13599
13857
|
return out;
|
|
@@ -13641,7 +13899,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
13641
13899
|
return out;
|
|
13642
13900
|
}
|
|
13643
13901
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
13644
|
-
function
|
|
13902
|
+
function report4(ctx, impacts) {
|
|
13645
13903
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
13646
13904
|
ctx.out.log(
|
|
13647
13905
|
`${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.`
|
|
@@ -13679,12 +13937,12 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
13679
13937
|
}
|
|
13680
13938
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
13681
13939
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
13682
|
-
|
|
13940
|
+
report4(ctx, impacts);
|
|
13683
13941
|
}
|
|
13684
13942
|
|
|
13685
13943
|
// src/runbook-lint.ts
|
|
13686
13944
|
function invocationsIn(body) {
|
|
13687
|
-
const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
|
|
13945
|
+
const re = /(`|npx[^\S\n]+(?:--yes[^\S\n]+)?)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
|
|
13688
13946
|
const found = [];
|
|
13689
13947
|
for (const match of body.matchAll(re)) {
|
|
13690
13948
|
if (!match[1]) continue;
|
|
@@ -13755,7 +14013,7 @@ async function runbookSearch(ctx, query, all, limit) {
|
|
|
13755
14013
|
for (const [i, hit] of result.hits.entries()) {
|
|
13756
14014
|
if (i) ctx.out.log("");
|
|
13757
14015
|
ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
|
|
13758
|
-
for (const
|
|
14016
|
+
for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
|
|
13759
14017
|
ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
|
|
13760
14018
|
}
|
|
13761
14019
|
}
|
|
@@ -13880,7 +14138,8 @@ var ALLOWED2 = [
|
|
|
13880
14138
|
"base",
|
|
13881
14139
|
"requires",
|
|
13882
14140
|
"platform",
|
|
13883
|
-
"context"
|
|
14141
|
+
"context",
|
|
14142
|
+
"open"
|
|
13884
14143
|
];
|
|
13885
14144
|
function requireSlug(slug, action2) {
|
|
13886
14145
|
if (!slug) throw new Error(`"runbook ${action2}" needs a slug, e.g. "odla-ai runbook ${action2} release"`);
|
|
@@ -13908,6 +14167,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
13908
14167
|
};
|
|
13909
14168
|
}
|
|
13910
14169
|
const needsCapability = WRITES2.has(action2) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
|
|
14170
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
13911
14171
|
const token = needsCapability ? await getScopedPlatformToken({
|
|
13912
14172
|
platform: cfg.platformUrl,
|
|
13913
14173
|
scope: "platform:runbook:write",
|
|
@@ -13915,6 +14175,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
13915
14175
|
label: `odla CLI (runbook ${action2})`,
|
|
13916
14176
|
fetch: doFetch,
|
|
13917
14177
|
stdout: out,
|
|
14178
|
+
open,
|
|
13918
14179
|
openApprovalUrl: deps.openUrl,
|
|
13919
14180
|
// A project, named context, or the global operator context owns the
|
|
13920
14181
|
// exact-scope cache; it never follows an arbitrary shell directory.
|
|
@@ -13926,11 +14187,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
13926
14187
|
configPath: cfg.configPath,
|
|
13927
14188
|
token: stringOpt(parsed.options.token),
|
|
13928
14189
|
email: stringOpt(parsed.options.email),
|
|
13929
|
-
|
|
13930
|
-
// CI and SSH). Hardcoding `false` meant a first-time handshake printed
|
|
13931
|
-
// a link and opened nothing — the one moment a browser is the whole
|
|
13932
|
-
// point.
|
|
13933
|
-
open: void 0
|
|
14190
|
+
open
|
|
13934
14191
|
},
|
|
13935
14192
|
doFetch,
|
|
13936
14193
|
out
|
|
@@ -14131,31 +14388,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
14131
14388
|
url.searchParams.set("job", job.jobId);
|
|
14132
14389
|
out.log(` Studio: ${url.toString()}`);
|
|
14133
14390
|
}
|
|
14134
|
-
function printHostedReport(out,
|
|
14135
|
-
out.log(`security report ${
|
|
14136
|
-
out.log(` coverage: ${
|
|
14137
|
-
out.log(` findings: confirmed=${
|
|
14138
|
-
out.log(` discovery: ${
|
|
14139
|
-
out.log(` validation: ${
|
|
14140
|
-
for (const finding of
|
|
14391
|
+
function printHostedReport(out, report5) {
|
|
14392
|
+
out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
|
|
14393
|
+
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}`);
|
|
14394
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
|
|
14395
|
+
out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
|
|
14396
|
+
out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
|
|
14397
|
+
for (const finding of report5.findings) {
|
|
14141
14398
|
const location = finding.locations[0];
|
|
14142
14399
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
14143
14400
|
}
|
|
14144
|
-
for (const limitation of
|
|
14401
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
14145
14402
|
}
|
|
14146
|
-
function enforceHostedReportGate(
|
|
14403
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
14147
14404
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
14148
14405
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
14149
14406
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
14150
14407
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
14151
|
-
const confirmed =
|
|
14152
|
-
const leads = failOnCandidates ?
|
|
14153
|
-
const incomplete =
|
|
14408
|
+
const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
14409
|
+
const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
14410
|
+
const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
14154
14411
|
if (confirmed.length || leads.length || incomplete) {
|
|
14155
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
14412
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
14156
14413
|
}
|
|
14157
14414
|
if (emitSuccess) {
|
|
14158
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
14415
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
|
|
14159
14416
|
}
|
|
14160
14417
|
}
|
|
14161
14418
|
function printHostedSecurityPlanRoute(out, label, route3) {
|
|
@@ -14238,17 +14495,17 @@ async function runHostedSecurity(options) {
|
|
|
14238
14495
|
allowNetwork: false
|
|
14239
14496
|
}
|
|
14240
14497
|
});
|
|
14241
|
-
const
|
|
14242
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
14243
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
14498
|
+
const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
14499
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report5);
|
|
14500
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report5);
|
|
14244
14501
|
await hosted.complete({
|
|
14245
14502
|
reportDigest,
|
|
14246
|
-
coverageStatus:
|
|
14247
|
-
confirmed:
|
|
14248
|
-
candidates:
|
|
14503
|
+
coverageStatus: report5.coverageStatus,
|
|
14504
|
+
confirmed: report5.metrics.confirmed,
|
|
14505
|
+
candidates: report5.metrics.candidates
|
|
14249
14506
|
}, { signal: options.signal });
|
|
14250
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
14251
|
-
return Object.freeze({ report:
|
|
14507
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
14508
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
14252
14509
|
}
|
|
14253
14510
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
14254
14511
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -14274,14 +14531,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
14274
14531
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
14275
14532
|
return { ...profile, maxHuntTasks };
|
|
14276
14533
|
}
|
|
14277
|
-
function printSummary(out, appId, env, run,
|
|
14278
|
-
const complete =
|
|
14534
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
14535
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
14279
14536
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
14280
14537
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
14281
14538
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
14282
|
-
out.log(` coverage: ${
|
|
14283
|
-
if (
|
|
14284
|
-
out.log(` findings: confirmed=${
|
|
14539
|
+
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}`);
|
|
14540
|
+
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
14541
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
14285
14542
|
out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
|
|
14286
14543
|
}
|
|
14287
14544
|
function formatBudget(usage) {
|
|
@@ -14528,13 +14785,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
14528
14785
|
}
|
|
14529
14786
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
14530
14787
|
}
|
|
14531
|
-
const
|
|
14788
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
14532
14789
|
if (parsed.options.json === true) {
|
|
14533
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
14790
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
14534
14791
|
} else {
|
|
14535
|
-
printHostedReport(context.stdout,
|
|
14792
|
+
printHostedReport(context.stdout, report5);
|
|
14536
14793
|
}
|
|
14537
|
-
enforceHostedReportGate(
|
|
14794
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
14538
14795
|
}
|
|
14539
14796
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
14540
14797
|
if (parsed.options.source === true) {
|
|
@@ -14602,13 +14859,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
14602
14859
|
});
|
|
14603
14860
|
enforceLocalGate(result.report, parsed);
|
|
14604
14861
|
}
|
|
14605
|
-
function enforceLocalGate(
|
|
14862
|
+
function enforceLocalGate(report5, parsed) {
|
|
14606
14863
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
14607
14864
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
14608
14865
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
14609
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
14610
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
14611
|
-
const incomplete =
|
|
14866
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
|
|
14867
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
14868
|
+
const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
14612
14869
|
if (confirmed.length || leads.length || incomplete) {
|
|
14613
14870
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
14614
14871
|
}
|
|
@@ -14647,9 +14904,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
14647
14904
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
14648
14905
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
14649
14906
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
14650
|
-
const
|
|
14651
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
14652
|
-
else printHostedReport(context.stdout,
|
|
14907
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId });
|
|
14908
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
|
|
14909
|
+
else printHostedReport(context.stdout, report5);
|
|
14653
14910
|
return;
|
|
14654
14911
|
}
|
|
14655
14912
|
if (sub !== "run") {
|