@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
|
@@ -40,14 +40,15 @@ function approvalLines(prompt) {
|
|
|
40
40
|
lines.push(` No browser was opened (${prompt.browserSkipped}).`);
|
|
41
41
|
}
|
|
42
42
|
lines.push("");
|
|
43
|
-
lines.push(" AGENTS:
|
|
44
|
-
lines.push("
|
|
45
|
-
lines.push("
|
|
43
|
+
lines.push(" AGENTS: immediately give the human this URL as a clickable approval action and repeat the code.");
|
|
44
|
+
lines.push(" Keep this CLI process running and wait on this same process; the CLI owns protocol polling.");
|
|
45
|
+
lines.push(" Do not use OS open, browser control, curl, a shell wait loop, detached execution, or a substitute handshake.");
|
|
46
|
+
lines.push(" You cannot approve it yourself. If this process exits, a later invocation creates a new code.");
|
|
46
47
|
lines.push("");
|
|
47
48
|
return lines;
|
|
48
49
|
}
|
|
49
50
|
function printApproval(out, prompt) {
|
|
50
|
-
for (const
|
|
51
|
+
for (const line2 of approvalLines(prompt)) out.error(line2);
|
|
51
52
|
}
|
|
52
53
|
function reminderLines(prompt) {
|
|
53
54
|
return [
|
|
@@ -133,12 +134,12 @@ function approvalHint(pending) {
|
|
|
133
134
|
}
|
|
134
135
|
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
135
136
|
const timer = setInterval(() => {
|
|
136
|
-
for (const
|
|
137
|
+
for (const line2 of reminderLines({
|
|
137
138
|
userCode: pending.userCode,
|
|
138
139
|
approvalUrl: pending.approvalUrl,
|
|
139
140
|
minutesLeft: minutesLeft(pending.expiresAt)
|
|
140
141
|
}))
|
|
141
|
-
out.log(
|
|
142
|
+
out.log(line2);
|
|
142
143
|
}, periodMs);
|
|
143
144
|
timer.unref?.();
|
|
144
145
|
return () => clearInterval(timer);
|
|
@@ -199,9 +200,9 @@ function mergeCredential(current, update) {
|
|
|
199
200
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
200
201
|
const path = resolve(rootDir, ".gitignore");
|
|
201
202
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
202
|
-
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((
|
|
203
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
203
204
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
204
|
-
const missing = wanted.filter((
|
|
205
|
+
const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
|
|
205
206
|
if (missing.length === 0) return;
|
|
206
207
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
207
208
|
writeFileSync(path, `${existing}${prefix}${missing.join("\n")}
|
|
@@ -233,7 +234,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
233
234
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
234
235
|
}
|
|
235
236
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
236
|
-
const retained = existing.split(/\r?\n/).filter((
|
|
237
|
+
const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
|
|
237
238
|
while (retained.at(-1) === "") retained.pop();
|
|
238
239
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
239
240
|
|
|
@@ -253,8 +254,8 @@ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
|
253
254
|
"ODLA_O11Y_VERSION",
|
|
254
255
|
"ODLA_O11Y_TOKEN"
|
|
255
256
|
]);
|
|
256
|
-
function isManagedDevVar(
|
|
257
|
-
const match =
|
|
257
|
+
function isManagedDevVar(line2) {
|
|
258
|
+
const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
258
259
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
259
260
|
}
|
|
260
261
|
function writePrivateText(path, text3) {
|
|
@@ -2112,7 +2113,7 @@ function chooseIdMode(options, rows) {
|
|
|
2112
2113
|
async function appImport(options) {
|
|
2113
2114
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2114
2115
|
const out = options.stdout ?? console;
|
|
2115
|
-
const say = options.json ? (
|
|
2116
|
+
const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
|
|
2116
2117
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
2117
2118
|
const text3 = options.file === "-" ? (options.readStdin ?? (() => readFileSync4(0, "utf8")))() : readFileSync4(options.file, "utf8");
|
|
2118
2119
|
const { format, sources } = parseImport(text3, options.ns);
|
|
@@ -2442,7 +2443,7 @@ async function designUnpack(parsed, deps) {
|
|
|
2442
2443
|
);
|
|
2443
2444
|
return;
|
|
2444
2445
|
}
|
|
2445
|
-
for (const
|
|
2446
|
+
for (const line2 of describeUnpack(result, outDir)) out.log(line2);
|
|
2446
2447
|
}
|
|
2447
2448
|
async function brandCommand(parsed, deps) {
|
|
2448
2449
|
const subject = parsed.positionals[1];
|
|
@@ -4351,7 +4352,7 @@ async function doctor(options) {
|
|
|
4351
4352
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
|
|
4352
4353
|
const contract = resolveSecretContract(cfg);
|
|
4353
4354
|
out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
|
|
4354
|
-
for (const
|
|
4355
|
+
for (const line2 of formatSecretContract(contract)) out.log(line2);
|
|
4355
4356
|
if (cfg.services.includes("calendar")) {
|
|
4356
4357
|
const calendar = cfg.envs.map((env) => {
|
|
4357
4358
|
const resolved = calendarServiceConfig(cfg, env);
|
|
@@ -4720,10 +4721,10 @@ async function secretsStatus(options) {
|
|
|
4720
4721
|
}
|
|
4721
4722
|
const body = await res.json();
|
|
4722
4723
|
const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
|
|
4723
|
-
const
|
|
4724
|
-
if (options.json) out.log(JSON.stringify(
|
|
4725
|
-
else printReport(
|
|
4726
|
-
return
|
|
4724
|
+
const report5 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
|
|
4725
|
+
if (options.json) out.log(JSON.stringify(report5, null, 2));
|
|
4726
|
+
else printReport(report5, out);
|
|
4727
|
+
return report5;
|
|
4727
4728
|
}
|
|
4728
4729
|
function buildReport(appId, env, tenant, contract, stored) {
|
|
4729
4730
|
const declared = new Set(contract.map((secret) => secret.name));
|
|
@@ -4740,25 +4741,25 @@ function buildReport(appId, env, tenant, contract, stored) {
|
|
|
4740
4741
|
const ok = rows.every((row) => row.state !== "missing" || !row.required);
|
|
4741
4742
|
return { app: appId, env, tenant, secrets: rows, ok };
|
|
4742
4743
|
}
|
|
4743
|
-
function printReport(
|
|
4744
|
-
out.log(`${
|
|
4745
|
-
if (
|
|
4744
|
+
function printReport(report5, out) {
|
|
4745
|
+
out.log(`${report5.app} (${report5.tenant})`);
|
|
4746
|
+
if (report5.secrets.length === 0) {
|
|
4746
4747
|
out.log(" no secrets declared and none stored");
|
|
4747
4748
|
return;
|
|
4748
4749
|
}
|
|
4749
|
-
for (const row of
|
|
4750
|
+
for (const row of report5.secrets) {
|
|
4750
4751
|
const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
|
|
4751
4752
|
const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
|
|
4752
4753
|
out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
|
|
4753
4754
|
}
|
|
4754
|
-
const missing =
|
|
4755
|
+
const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
|
|
4755
4756
|
if (missing.length) {
|
|
4756
4757
|
out.log("");
|
|
4757
4758
|
for (const row of missing) {
|
|
4758
|
-
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${
|
|
4759
|
+
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report5.env} --stdin"`);
|
|
4759
4760
|
}
|
|
4760
4761
|
}
|
|
4761
|
-
if (
|
|
4762
|
+
if (report5.secrets.some((row) => row.state === "reserved")) {
|
|
4762
4763
|
out.log("");
|
|
4763
4764
|
out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
|
|
4764
4765
|
}
|
|
@@ -4780,18 +4781,29 @@ For work that creates an odla app or adds odla services, read and follow
|
|
|
4780
4781
|
\`.agents/skills/odla-o11y-debug/SKILL.md\`.
|
|
4781
4782
|
|
|
4782
4783
|
Track the work in odla's PM as you go. Before project-mutating work, run
|
|
4783
|
-
\`npx @odla-ai/cli pm next --app <appId>\`, confirm alignment to an open goal,
|
|
4784
|
+
\`npx --yes @odla-ai/cli@latest pm next --app <appId>\`, confirm alignment to an open goal,
|
|
4784
4785
|
and atomically claim a refined Ready task. Record decisions when you make them
|
|
4785
4786
|
and file bugs when you notice them. The conventions and the full command set are
|
|
4786
4787
|
in \`.agents/skills/odla/references/pm.md\`.
|
|
4787
4788
|
|
|
4788
|
-
Use the human's signed-in odla account email for device authorization;
|
|
4789
|
-
infer it from git config, commit metadata, or GitHub. If authorization is
|
|
4790
|
-
already active, run
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4789
|
+
Use the human's known signed-in odla account email for device authorization;
|
|
4790
|
+
never infer it from git config, commit metadata, or GitHub. If authorization is
|
|
4791
|
+
not already active, run this exact command as one foreground process:
|
|
4792
|
+
|
|
4793
|
+
\`npx --yes @odla-ai/cli@latest auth login --app <appId> --email <odla-account> --no-open --wait 600\`
|
|
4794
|
+
|
|
4795
|
+
When it prints the approval URL and code, immediately give the human a clickable
|
|
4796
|
+
link, name the code they must verify, and tell them to click **Approve**. Keep
|
|
4797
|
+
that CLI process running and wait on the same tool process for its result. The
|
|
4798
|
+
CLI owns protocol polling: never call the OS \`open\` command, use browser
|
|
4799
|
+
control, curl a handshake endpoint, build a shell wait/poll loop, detach the
|
|
4800
|
+
process, or start another handshake while it is alive. The device code exists
|
|
4801
|
+
only in that process. If it exits 75 before approval, the old code cannot be
|
|
4802
|
+
collected; start one fresh foreground command and surface its new URL.
|
|
4803
|
+
|
|
4804
|
+
Never file an odla project or product defect in GitHub Issues: run
|
|
4805
|
+
\`npx --yes @odla-ai/cli@latest bug report --app <appId> ...\` so the bug lands
|
|
4806
|
+
in odla PM with the rest of the project's goals, tasks, and decisions.
|
|
4795
4807
|
|
|
4796
4808
|
The setup runbooks and their references are installed in this repository, pinned
|
|
4797
4809
|
to this CLI version. Use them as your setup context.
|
|
@@ -4803,9 +4815,9 @@ When this repository has an \`appId\`, pass it: app-scoped discovery includes
|
|
|
4803
4815
|
that project's instructions plus the shared platform procedures.
|
|
4804
4816
|
|
|
4805
4817
|
\`\`\`
|
|
4806
|
-
npx odla-ai runbook ask "<what you are about to do>" --app <appId>
|
|
4807
|
-
npx odla-ai runbook list
|
|
4808
|
-
npx odla-ai runbook get <slug>
|
|
4818
|
+
npx --yes @odla-ai/cli@latest runbook ask "<what you are about to do>" --app <appId>
|
|
4819
|
+
npx --yes @odla-ai/cli@latest runbook list
|
|
4820
|
+
npx --yes @odla-ai/cli@latest runbook get <slug>
|
|
4809
4821
|
\`\`\`
|
|
4810
4822
|
|
|
4811
4823
|
Never scrape odla.ai HTML for any of this. The CLI reads the same content
|
|
@@ -4825,12 +4837,12 @@ function claudeAdapter(skill, canonical2) {
|
|
|
4825
4837
|
const lines = match[1].split(/\r?\n/);
|
|
4826
4838
|
const frontmatter = [];
|
|
4827
4839
|
let keepIndented = false;
|
|
4828
|
-
for (const
|
|
4829
|
-
if (
|
|
4830
|
-
frontmatter.push(
|
|
4831
|
-
keepIndented =
|
|
4832
|
-
} else if (keepIndented && /^\s+/.test(
|
|
4833
|
-
frontmatter.push(
|
|
4840
|
+
for (const line2 of lines) {
|
|
4841
|
+
if (line2.startsWith("name:") || line2.startsWith("description:")) {
|
|
4842
|
+
frontmatter.push(line2);
|
|
4843
|
+
keepIndented = line2.startsWith("description:");
|
|
4844
|
+
} else if (keepIndented && /^\s+/.test(line2)) {
|
|
4845
|
+
frontmatter.push(line2);
|
|
4834
4846
|
} else {
|
|
4835
4847
|
keepIndented = false;
|
|
4836
4848
|
}
|
|
@@ -6154,13 +6166,13 @@ async function createConversionRegistry(config) {
|
|
|
6154
6166
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
6155
6167
|
}
|
|
6156
6168
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
6157
|
-
const
|
|
6169
|
+
const get2 = (id2, kind) => {
|
|
6158
6170
|
const policy = policies.get(id2);
|
|
6159
6171
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
6160
6172
|
return policy;
|
|
6161
6173
|
};
|
|
6162
6174
|
const checked = (source, id2, kind) => {
|
|
6163
|
-
const policy =
|
|
6175
|
+
const policy = get2(id2, kind);
|
|
6164
6176
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
6165
6177
|
return policy;
|
|
6166
6178
|
};
|
|
@@ -6991,7 +7003,7 @@ var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
|
6991
7003
|
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;
|
|
6992
7004
|
function stripPatchEnvelope(patch2) {
|
|
6993
7005
|
if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
|
|
6994
|
-
const kept = patch2.split("\n").filter((
|
|
7006
|
+
const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
|
|
6995
7007
|
const stripped = kept.join("\n");
|
|
6996
7008
|
return /^diff --git /m.test(stripped) ? stripped : patch2;
|
|
6997
7009
|
}
|
|
@@ -7012,9 +7024,9 @@ function validateCodePatch(rawPatch, maxBytes) {
|
|
|
7012
7024
|
const paths = [];
|
|
7013
7025
|
const lines = patch2.split("\n");
|
|
7014
7026
|
for (let index = 0; index < lines.length; index += 1) {
|
|
7015
|
-
const
|
|
7016
|
-
if (!
|
|
7017
|
-
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(
|
|
7027
|
+
const line2 = lines[index];
|
|
7028
|
+
if (!line2.startsWith("diff --git ")) continue;
|
|
7029
|
+
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line2);
|
|
7018
7030
|
const path = match?.[1];
|
|
7019
7031
|
if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
|
|
7020
7032
|
validateRelativePath(path);
|
|
@@ -7047,9 +7059,9 @@ function resolveCodePath(workspaceDir, path) {
|
|
|
7047
7059
|
return target;
|
|
7048
7060
|
}
|
|
7049
7061
|
function describePatchFailure(patch2, detail) {
|
|
7050
|
-
const hunks = patch2.split("\n").filter((
|
|
7062
|
+
const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
|
|
7051
7063
|
const bodies = patch2.split(/^@@.*$/m).slice(1);
|
|
7052
|
-
const contextless = bodies.some((body) => !body.split("\n").some((
|
|
7064
|
+
const contextless = bodies.some((body) => !body.split("\n").some((line2) => line2.startsWith(" ") && line2.trim().length > 0));
|
|
7053
7065
|
const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
|
|
7054
7066
|
return `patch did not apply: ${detail}${hint}`;
|
|
7055
7067
|
}
|
|
@@ -9610,11 +9622,237 @@ function record6(value2) {
|
|
|
9610
9622
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
9611
9623
|
}
|
|
9612
9624
|
|
|
9625
|
+
// src/code-grant-command.ts
|
|
9626
|
+
var ACTIONS = ["request", "list", "approve", "revoke"];
|
|
9627
|
+
var isAction = (value2) => ACTIONS.includes(value2 ?? "");
|
|
9628
|
+
async function fail2(response2, what) {
|
|
9629
|
+
const body = redactSecrets((await response2.text()).slice(0, 1e3));
|
|
9630
|
+
throw new Error(`${what} failed (${response2.status}): ${body}`);
|
|
9631
|
+
}
|
|
9632
|
+
function line(grant) {
|
|
9633
|
+
const who = grant.status === "granted" ? `granted by ${grant.approvedBy ?? "?"}` : `requested by ${grant.requestedBy}`;
|
|
9634
|
+
return `${grant.grantId} ${grant.appEnv} ${grant.owner}/${grant.name} ${grant.status} ${who}`;
|
|
9635
|
+
}
|
|
9636
|
+
function report(grants, out, json) {
|
|
9637
|
+
if (json) return out.log(JSON.stringify({ grants }, null, 2));
|
|
9638
|
+
if (!grants.length) return out.log("no repository grants for this app");
|
|
9639
|
+
out.log("GRANT ENV REPOSITORY STATUS DECIDED");
|
|
9640
|
+
for (const grant of grants) out.log(line(grant));
|
|
9641
|
+
const pending = grants.filter((grant) => grant.status === "requested");
|
|
9642
|
+
if (pending.length) {
|
|
9643
|
+
out.log(`
|
|
9644
|
+
${pending.length} awaiting approval. A human owner runs:`);
|
|
9645
|
+
for (const grant of pending) out.log(` odla-ai code grant approve ${grant.grantId} --env ${grant.appEnv}`);
|
|
9646
|
+
}
|
|
9647
|
+
}
|
|
9648
|
+
function grantsUrl(cfg, suffix = "") {
|
|
9649
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository-grants${suffix}`;
|
|
9650
|
+
}
|
|
9651
|
+
async function codeGrantCommand(parsed, deps = {}) {
|
|
9652
|
+
const action2 = parsed.positionals[2];
|
|
9653
|
+
if (!isAction(action2)) {
|
|
9654
|
+
throw new Error(
|
|
9655
|
+
`unknown code grant action "${action2 ?? ""}". Try "odla-ai code grant list --env dev".`
|
|
9656
|
+
);
|
|
9657
|
+
}
|
|
9658
|
+
const decides = action2 === "approve" || action2 === "revoke";
|
|
9659
|
+
assertArgs(parsed, ["config", "env", "json", "token", "email", "open"], decides ? 4 : 3);
|
|
9660
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
9661
|
+
const doFetch = deps.fetch ?? fetch;
|
|
9662
|
+
const out = deps.stdout ?? console;
|
|
9663
|
+
const env = stringOpt(parsed.options.env) ?? "dev";
|
|
9664
|
+
const json = parsed.options.json === true;
|
|
9665
|
+
const token = await getDeveloperToken(cfg, {
|
|
9666
|
+
configPath: cfg.configPath,
|
|
9667
|
+
token: stringOpt(parsed.options.token),
|
|
9668
|
+
email: stringOpt(parsed.options.email),
|
|
9669
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
9670
|
+
openApprovalUrl: deps.openUrl
|
|
9671
|
+
// Every verb here administers grants, which the registry gates on
|
|
9672
|
+
// app.manage — a token without it is refused as "not your app", which reads
|
|
9673
|
+
// like the wrong app rather than the wrong authority.
|
|
9674
|
+
//
|
|
9675
|
+
// Only `request` additionally asks for code.session. Approving is a human
|
|
9676
|
+
// act, and a token minted to approve has no business also carrying the
|
|
9677
|
+
// authority the approval is about.
|
|
9678
|
+
}, doFetch, out, {
|
|
9679
|
+
optionalProjectCapabilities: action2 === "request" ? ["app.manage", "code.session"] : ["app.manage"]
|
|
9680
|
+
});
|
|
9681
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
9682
|
+
if (action2 === "list") {
|
|
9683
|
+
const response3 = await doFetch(`${grantsUrl(cfg)}?env=${encodeURIComponent(env)}`, { headers: auth });
|
|
9684
|
+
if (!response3.ok) await fail2(response3, "repository grant list");
|
|
9685
|
+
const body2 = await response3.json();
|
|
9686
|
+
return report(body2.grants, out, json);
|
|
9687
|
+
}
|
|
9688
|
+
if (action2 === "request") {
|
|
9689
|
+
const response3 = await doFetch(grantsUrl(cfg), {
|
|
9690
|
+
method: "POST",
|
|
9691
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
9692
|
+
body: JSON.stringify({ env })
|
|
9693
|
+
});
|
|
9694
|
+
if (!response3.ok) await fail2(response3, "repository grant request");
|
|
9695
|
+
const { grant } = await response3.json();
|
|
9696
|
+
if (json) return out.log(JSON.stringify({ grant }, null, 2));
|
|
9697
|
+
out.log(`requested ${grant.owner}/${grant.name} (${grant.appEnv}) \u2014 ${grant.grantId}`);
|
|
9698
|
+
return out.log(grant.status === "granted" ? "already granted; nothing to approve" : `a human owner of ${cfg.app.id} approves it with:
|
|
9699
|
+
odla-ai code grant approve ${grant.grantId} --env ${env}`);
|
|
9700
|
+
}
|
|
9701
|
+
const grantId = parsed.positionals[3];
|
|
9702
|
+
if (!grantId) throw new Error(`code grant ${action2} requires the grant id from "odla-ai code grant list"`);
|
|
9703
|
+
const url = `${grantsUrl(cfg, `/${encodeURIComponent(grantId)}`)}`;
|
|
9704
|
+
const response2 = action2 === "approve" ? await doFetch(url, {
|
|
9705
|
+
method: "POST",
|
|
9706
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
9707
|
+
body: JSON.stringify({ env })
|
|
9708
|
+
}) : await doFetch(`${url}?env=${encodeURIComponent(env)}`, { method: "DELETE", headers: auth });
|
|
9709
|
+
if (!response2.ok) await fail2(response2, `repository grant ${action2}`);
|
|
9710
|
+
const body = await response2.json();
|
|
9711
|
+
if (json) return out.log(JSON.stringify(body, null, 2));
|
|
9712
|
+
out.log(action2 === "approve" && body.grant ? `granted ${body.grant.owner}/${body.grant.name} (${body.grant.appEnv}) to ${cfg.app.id}` : `revoked ${grantId}`);
|
|
9713
|
+
}
|
|
9714
|
+
|
|
9715
|
+
// src/code-repository-command.ts
|
|
9716
|
+
var ACTIONS2 = ["show", "list", "bind"];
|
|
9717
|
+
var isAction2 = (value2) => ACTIONS2.includes(value2 ?? "");
|
|
9718
|
+
async function get(doFetch, url, token, what) {
|
|
9719
|
+
const response2 = await doFetch(url, { headers: { authorization: `Bearer ${token}` } });
|
|
9720
|
+
if (!response2.ok) {
|
|
9721
|
+
const body = redactSecrets((await response2.text()).slice(0, 1e3));
|
|
9722
|
+
if (response2.status === 403 && body.includes("human_session_required")) {
|
|
9723
|
+
throw new Error(
|
|
9724
|
+
`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".`
|
|
9725
|
+
);
|
|
9726
|
+
}
|
|
9727
|
+
throw new Error(`${what} failed (${response2.status}): ${body}`);
|
|
9728
|
+
}
|
|
9729
|
+
return await response2.json();
|
|
9730
|
+
}
|
|
9731
|
+
async function catalogue(doFetch, platformUrl, token) {
|
|
9732
|
+
const account = await get(
|
|
9733
|
+
doFetch,
|
|
9734
|
+
`${platformUrl}/registry/code/github/account`,
|
|
9735
|
+
token,
|
|
9736
|
+
"GitHub account read"
|
|
9737
|
+
);
|
|
9738
|
+
const active = account.installations.filter((install2) => install2.status === "active");
|
|
9739
|
+
const found = [];
|
|
9740
|
+
for (const install2 of active) {
|
|
9741
|
+
const { repositories } = await get(
|
|
9742
|
+
doFetch,
|
|
9743
|
+
`${platformUrl}/registry/code/github/installations/${install2.installationId}/repositories`,
|
|
9744
|
+
token,
|
|
9745
|
+
`repository catalog for ${install2.accountLogin}`
|
|
9746
|
+
);
|
|
9747
|
+
for (const repository of repositories) {
|
|
9748
|
+
found.push({ ...repository, installationId: install2.installationId });
|
|
9749
|
+
}
|
|
9750
|
+
}
|
|
9751
|
+
if (!active.length) {
|
|
9752
|
+
throw new Error(
|
|
9753
|
+
"no active GitHub App installation. Install odla's GitHub App on the account that owns the repository first, from Studio \u2192 Code."
|
|
9754
|
+
);
|
|
9755
|
+
}
|
|
9756
|
+
return found;
|
|
9757
|
+
}
|
|
9758
|
+
function selectRepository(found, full) {
|
|
9759
|
+
const wanted = full.toLowerCase();
|
|
9760
|
+
const matches = found.filter((repository) => repository.fullName.toLowerCase() === wanted);
|
|
9761
|
+
if (matches.length === 1) return matches[0];
|
|
9762
|
+
if (!matches.length) {
|
|
9763
|
+
const near = found.filter((repository) => repository.name.toLowerCase() === wanted.split("/").pop());
|
|
9764
|
+
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.`);
|
|
9765
|
+
}
|
|
9766
|
+
throw new Error(
|
|
9767
|
+
`${full} is reachable through ${matches.length} installations (${matches.map((match) => match.installationId).join(", ")}); revoke the one you do not mean.`
|
|
9768
|
+
);
|
|
9769
|
+
}
|
|
9770
|
+
function repositoryUrl(cfg, suffix = "") {
|
|
9771
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository${suffix}`;
|
|
9772
|
+
}
|
|
9773
|
+
async function codeRepositoryCommand(parsed, deps = {}) {
|
|
9774
|
+
const action2 = parsed.positionals[2];
|
|
9775
|
+
if (!isAction2(action2)) {
|
|
9776
|
+
throw new Error(
|
|
9777
|
+
`unknown code repository action "${action2 ?? ""}". Try "odla-ai code repository show --env dev".`
|
|
9778
|
+
);
|
|
9779
|
+
}
|
|
9780
|
+
assertArgs(parsed, ["config", "env", "repo", "json", "token", "email", "open"], 3);
|
|
9781
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
9782
|
+
const doFetch = deps.fetch ?? fetch;
|
|
9783
|
+
const out = deps.stdout ?? console;
|
|
9784
|
+
const env = stringOpt(parsed.options.env) ?? "dev";
|
|
9785
|
+
const json = parsed.options.json === true;
|
|
9786
|
+
const token = await getDeveloperToken(cfg, {
|
|
9787
|
+
configPath: cfg.configPath,
|
|
9788
|
+
token: stringOpt(parsed.options.token),
|
|
9789
|
+
email: stringOpt(parsed.options.email),
|
|
9790
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
9791
|
+
openApprovalUrl: deps.openUrl
|
|
9792
|
+
}, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
|
|
9793
|
+
if (action2 === "show") {
|
|
9794
|
+
const body2 = await get(
|
|
9795
|
+
doFetch,
|
|
9796
|
+
`${repositoryUrl(cfg)}?env=${encodeURIComponent(env)}`,
|
|
9797
|
+
token,
|
|
9798
|
+
"repository read"
|
|
9799
|
+
);
|
|
9800
|
+
if (json) return out.log(JSON.stringify(body2, null, 2));
|
|
9801
|
+
const bound = body2.environmentRepository;
|
|
9802
|
+
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}".`);
|
|
9803
|
+
}
|
|
9804
|
+
const found = await catalogue(doFetch, cfg.platformUrl, token);
|
|
9805
|
+
if (action2 === "list") {
|
|
9806
|
+
if (json) return out.log(JSON.stringify({ repositories: found }, null, 2));
|
|
9807
|
+
if (!found.length) return out.log("no repositories reachable through your installations");
|
|
9808
|
+
out.log("REPOSITORY INSTALL DEFAULT READY");
|
|
9809
|
+
for (const repository of found) {
|
|
9810
|
+
out.log(`${repository.fullName} ${repository.installationId} ${repository.defaultBranch ?? "\u2014"} ${repository.ready ? "yes" : "empty"}`);
|
|
9811
|
+
}
|
|
9812
|
+
return;
|
|
9813
|
+
}
|
|
9814
|
+
const repo = stringOpt(parsed.options.repo);
|
|
9815
|
+
if (!repo?.includes("/")) throw new Error("code repository bind requires --repo owner/name");
|
|
9816
|
+
const selected = selectRepository(found, repo);
|
|
9817
|
+
if (!selected.ready) {
|
|
9818
|
+
throw new Error(`${selected.fullName} has no commits yet; GitHub cannot serve a default branch for an empty repository.`);
|
|
9819
|
+
}
|
|
9820
|
+
const response2 = await doFetch(repositoryUrl(cfg, "/bind"), {
|
|
9821
|
+
method: "POST",
|
|
9822
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
9823
|
+
body: JSON.stringify({ env, installationId: selected.installationId, repositoryId: selected.repositoryId })
|
|
9824
|
+
});
|
|
9825
|
+
if (!response2.ok) {
|
|
9826
|
+
throw new Error(`repository bind failed (${response2.status}): ${redactSecrets((await response2.text()).slice(0, 1e3))}`);
|
|
9827
|
+
}
|
|
9828
|
+
const body = await response2.json();
|
|
9829
|
+
if (json) return out.log(JSON.stringify(body, null, 2));
|
|
9830
|
+
out.log(`connected ${cfg.app.id} (${env}) to ${selected.fullName} @ ${selected.defaultBranch}`);
|
|
9831
|
+
out.log(`Next, so it may be worked on unattended:
|
|
9832
|
+
odla-ai code grant request --env ${env}`);
|
|
9833
|
+
}
|
|
9834
|
+
|
|
9613
9835
|
// src/code-command.ts
|
|
9614
9836
|
async function codeCommand(parsed, dependencies) {
|
|
9615
9837
|
const sub = parsed.positionals[1];
|
|
9838
|
+
if (sub === "grant") {
|
|
9839
|
+
return await codeGrantCommand(parsed, {
|
|
9840
|
+
fetch: dependencies.fetch,
|
|
9841
|
+
stdout: dependencies.stdout,
|
|
9842
|
+
openUrl: dependencies.openUrl
|
|
9843
|
+
});
|
|
9844
|
+
}
|
|
9845
|
+
if (sub === "repository") {
|
|
9846
|
+
return await codeRepositoryCommand(parsed, {
|
|
9847
|
+
fetch: dependencies.fetch,
|
|
9848
|
+
stdout: dependencies.stdout,
|
|
9849
|
+
openUrl: dependencies.openUrl
|
|
9850
|
+
});
|
|
9851
|
+
}
|
|
9616
9852
|
if (sub !== "connect") {
|
|
9617
|
-
throw new Error(
|
|
9853
|
+
throw new Error(
|
|
9854
|
+
`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev" or "odla-ai code grant list --env dev".`
|
|
9855
|
+
);
|
|
9618
9856
|
}
|
|
9619
9857
|
assertArgs(parsed, [
|
|
9620
9858
|
"config",
|
|
@@ -9960,8 +10198,8 @@ Usage:
|
|
|
9960
10198
|
odla-ai runbook revert <slug> --version <n> [--app <id>]
|
|
9961
10199
|
odla-ai runbook rm <slug> [--app <id>]
|
|
9962
10200
|
odla-ai capabilities [--json]
|
|
9963
|
-
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
9964
|
-
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
10201
|
+
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--no-open] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
10202
|
+
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--no-open] [--json]
|
|
9965
10203
|
odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
|
|
9966
10204
|
odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
9967
10205
|
[--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
|
|
@@ -9980,7 +10218,7 @@ Usage:
|
|
|
9980
10218
|
odla-ai security report <job-id> [--json]
|
|
9981
10219
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
9982
10220
|
odla-ai security run [target] --self --ack-redacted-source
|
|
9983
|
-
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]
|
|
10221
|
+
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]
|
|
9984
10222
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
9985
10223
|
odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
|
|
9986
10224
|
odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
|
|
@@ -9995,7 +10233,7 @@ function printHelp(output = console) {
|
|
|
9995
10233
|
output.log(`odla-ai
|
|
9996
10234
|
${USAGE_SECTION}
|
|
9997
10235
|
Commands:
|
|
9998
|
-
auth Start a fresh, exact-project agent authorization
|
|
10236
|
+
auth Start a fresh, exact-project agent authorization for human review.
|
|
9999
10237
|
The email is the signed-in odla account, never git or GitHub
|
|
10000
10238
|
identity. The approval screen confirms the agent name first.
|
|
10001
10239
|
agent Inspect durable agent wakeups and explicitly requeue a
|
|
@@ -10056,6 +10294,9 @@ Commands:
|
|
|
10056
10294
|
\u2014 no secret is ever copied between people.
|
|
10057
10295
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
10058
10296
|
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
10297
|
+
"code repository show" reports the repository an app works on (Studio or a
|
|
10298
|
+
human session connects one); "code grant request|list|approve|revoke" then
|
|
10299
|
+
governs unattended access: an agent may request, only a human may approve.
|
|
10059
10300
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
10060
10301
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10061
10302
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
@@ -10125,23 +10366,25 @@ Safety:
|
|
|
10125
10366
|
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
10126
10367
|
context, which beats project config. There is no ambient current context.
|
|
10127
10368
|
"context show" reports only provenance and cache state and never authenticates.
|
|
10128
|
-
Every real CLI handshake prints one canonical /studio?code= approval URL
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10369
|
+
Every real CLI handshake prints one canonical /studio?code= approval URL.
|
|
10370
|
+
Interactive humans may let the CLI attempt its best-effort browser launch.
|
|
10371
|
+
Agent-driven commands must pass --no-open --wait 600, immediately surface the
|
|
10372
|
+
exact URL and code as a clickable human approval action, and keep that CLI
|
|
10373
|
+
process alive. Wait only on that same process: the CLI owns protocol polling.
|
|
10374
|
+
Do not call OS open, use browser control, curl handshake endpoints, build a
|
|
10375
|
+
shell wait loop, detach the command, or start a substitute handshake. The
|
|
10376
|
+
device code remains only in the running process. If the process exits 75, its
|
|
10377
|
+
old code cannot be collected; a later invocation requests a new code. Older
|
|
10378
|
+
clients' persisted pending state is discarded.
|
|
10137
10379
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
10138
10380
|
The email is a non-secret identity hint: never provide a password or session
|
|
10139
10381
|
token. It is the email shown by the signed-in odla account \u2014 never infer it
|
|
10140
10382
|
from git config, a commit author, or GitHub. The matching account must already exist, be signed in, explicitly
|
|
10141
10383
|
review the exact code, and finish any current request before claiming another.
|
|
10142
|
-
Use "auth login --app <id> --email <odla-account>
|
|
10143
|
-
a deliberate fresh request; it ignores cached
|
|
10144
|
-
focused authorization sequence
|
|
10384
|
+
Use "auth login --app <id> --email <odla-account> --no-open --wait 600" when
|
|
10385
|
+
an outside agent needs a deliberate fresh request; it ignores cached
|
|
10386
|
+
credentials and uses the same focused authorization sequence as every
|
|
10387
|
+
first-time command.
|
|
10145
10388
|
If provision reports that the current agent principal has no live app.manage
|
|
10146
10389
|
grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
|
|
10147
10390
|
the local cache, prints and opens a fresh exact-project owner-review URL, then
|
|
@@ -10154,6 +10397,17 @@ Safety:
|
|
|
10154
10397
|
approval and credential hashes live in odla-ai/db. The host
|
|
10155
10398
|
credential is never written under .odla/; it exists only in the foreground
|
|
10156
10399
|
"code connect" process and is rotated by the next approved connection.
|
|
10400
|
+
"code repository bind" takes owner/name and resolves the two GitHub integers the
|
|
10401
|
+
bind route wants across every installation you have, refusing an ambiguous match
|
|
10402
|
+
rather than choosing one \u2014 the same repository name under two organizations is
|
|
10403
|
+
ordinary, and picking the first would connect an app to source nobody chose. It
|
|
10404
|
+
needs a signed-in human session; a CLI handshake token is a delegated agent
|
|
10405
|
+
credential by design, so an agent cannot choose the source it will be given.
|
|
10406
|
+
Unattended access is per repository and revocable on its own, but the connection
|
|
10407
|
+
IS the authorization: a repository is recorded for one exact app and environment
|
|
10408
|
+
only by a signed-in human choosing it. "code grant list" shows what is authorized;
|
|
10409
|
+
"revoke" stops unattended access without disconnecting the repository, and
|
|
10410
|
+
re-binding never resurrects it. Connecting sandbox never authorizes live.
|
|
10157
10411
|
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
10158
10412
|
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
10159
10413
|
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
@@ -10520,7 +10774,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10520
10774
|
throw new WatchRemoteError(cursor, error);
|
|
10521
10775
|
}
|
|
10522
10776
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10523
|
-
const result =
|
|
10777
|
+
const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10524
10778
|
throw new WatchTimeoutError(result.cursor);
|
|
10525
10779
|
}
|
|
10526
10780
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -10561,7 +10815,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10561
10815
|
});
|
|
10562
10816
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
10563
10817
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
10564
|
-
return
|
|
10818
|
+
return report2(ctx, parsed, {
|
|
10565
10819
|
found: true,
|
|
10566
10820
|
cursor,
|
|
10567
10821
|
events: matching,
|
|
@@ -10588,13 +10842,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10588
10842
|
}
|
|
10589
10843
|
if (page2.hasMore) continue;
|
|
10590
10844
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10591
|
-
return
|
|
10845
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10592
10846
|
}
|
|
10593
10847
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
10594
10848
|
await sleep(wait2);
|
|
10595
10849
|
}
|
|
10596
10850
|
}
|
|
10597
|
-
function
|
|
10851
|
+
function report2(ctx, parsed, result) {
|
|
10598
10852
|
if (ctx.json) {
|
|
10599
10853
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
10600
10854
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -11191,7 +11445,7 @@ function eventLabel(event) {
|
|
|
11191
11445
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
11192
11446
|
return body || event.payload.entityId;
|
|
11193
11447
|
}
|
|
11194
|
-
function
|
|
11448
|
+
function report3(ctx, parsed, result) {
|
|
11195
11449
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
11196
11450
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
11197
11451
|
for (const event of result.events ?? []) {
|
|
@@ -11251,7 +11505,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11251
11505
|
});
|
|
11252
11506
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
11253
11507
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11254
|
-
return
|
|
11508
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
11255
11509
|
}
|
|
11256
11510
|
const backoff = Math.min(
|
|
11257
11511
|
MAX_BACKOFF_MS2,
|
|
@@ -11292,7 +11546,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11292
11546
|
cursor,
|
|
11293
11547
|
serverTime: current.serverTime
|
|
11294
11548
|
});
|
|
11295
|
-
return
|
|
11549
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
11296
11550
|
}
|
|
11297
11551
|
if (current.events.length > 0) {
|
|
11298
11552
|
jsonl2(ctx, parsed, {
|
|
@@ -11311,7 +11565,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11311
11565
|
}
|
|
11312
11566
|
if (current.hasMore) continue;
|
|
11313
11567
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11314
|
-
return
|
|
11568
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
11315
11569
|
}
|
|
11316
11570
|
await sleep(
|
|
11317
11571
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -11848,8 +12102,8 @@ function printO11yStatus(status, out) {
|
|
|
11848
12102
|
out.log(
|
|
11849
12103
|
`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`
|
|
11850
12104
|
);
|
|
11851
|
-
for (const
|
|
11852
|
-
out.log(
|
|
12105
|
+
for (const line2 of providerCapacityLines(status.providerCapacity)) {
|
|
12106
|
+
out.log(line2);
|
|
11853
12107
|
}
|
|
11854
12108
|
const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
|
|
11855
12109
|
const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
|
|
@@ -12892,7 +13146,11 @@ var COMMAND_SURFACE = {
|
|
|
12892
13146
|
bug: { create: {}, list: {}, report: {} },
|
|
12893
13147
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
12894
13148
|
capabilities: {},
|
|
12895
|
-
code: {
|
|
13149
|
+
code: {
|
|
13150
|
+
connect: {},
|
|
13151
|
+
grant: { request: {}, list: {}, approve: {}, revoke: {} },
|
|
13152
|
+
repository: { show: {}, list: {}, bind: {} }
|
|
13153
|
+
},
|
|
12896
13154
|
config: { diff: {}, plan: {}, apply: {} },
|
|
12897
13155
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
12898
13156
|
credentials: { list: {}, revoke: {} },
|
|
@@ -13158,8 +13416,8 @@ function parseRunbook(text3, slug) {
|
|
|
13158
13416
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
|
|
13159
13417
|
if (fm) {
|
|
13160
13418
|
rest = text3.slice(fm[0].length);
|
|
13161
|
-
for (const
|
|
13162
|
-
const pair = /^(\w+)\s*:\s*(.+)$/.exec(
|
|
13419
|
+
for (const line2 of fm[1].split(/\r?\n/)) {
|
|
13420
|
+
const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
|
|
13163
13421
|
if (!pair) continue;
|
|
13164
13422
|
const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
|
|
13165
13423
|
if (pair[1] === "summary") meta.summary = value2;
|
|
@@ -13299,18 +13557,18 @@ function areaOf(path) {
|
|
|
13299
13557
|
return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
|
|
13300
13558
|
}
|
|
13301
13559
|
function scanHunk(lines, into) {
|
|
13302
|
-
for (const [i,
|
|
13303
|
-
const decl = DECL.exec(
|
|
13560
|
+
for (const [i, line2] of lines.entries()) {
|
|
13561
|
+
const decl = DECL.exec(line2);
|
|
13304
13562
|
if (decl?.[1]) {
|
|
13305
13563
|
into.add(decl[1]);
|
|
13306
13564
|
continue;
|
|
13307
13565
|
}
|
|
13308
|
-
const named = NAMED.exec(
|
|
13566
|
+
const named = NAMED.exec(line2);
|
|
13309
13567
|
if (named?.[1]) {
|
|
13310
13568
|
for (const name of namedExports(named[1])) into.add(name);
|
|
13311
13569
|
continue;
|
|
13312
13570
|
}
|
|
13313
|
-
if (!JSDOC.test(
|
|
13571
|
+
if (!JSDOC.test(line2)) continue;
|
|
13314
13572
|
for (let j = i + 1; j < lines.length && j < i + 40; j++) {
|
|
13315
13573
|
const found = ANY_DECL.exec(lines[j]);
|
|
13316
13574
|
if (found?.[1]) {
|
|
@@ -13328,8 +13586,8 @@ function parseDiff(diff) {
|
|
|
13328
13586
|
if (current && hunk.length) scanHunk(hunk, current.exports);
|
|
13329
13587
|
hunk = [];
|
|
13330
13588
|
};
|
|
13331
|
-
for (const
|
|
13332
|
-
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(
|
|
13589
|
+
for (const line2 of diff.split("\n")) {
|
|
13590
|
+
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
|
|
13333
13591
|
if (header) {
|
|
13334
13592
|
flush();
|
|
13335
13593
|
const path = header[2] ?? header[1];
|
|
@@ -13337,11 +13595,11 @@ function parseDiff(diff) {
|
|
|
13337
13595
|
files.set(path, current);
|
|
13338
13596
|
continue;
|
|
13339
13597
|
}
|
|
13340
|
-
if (
|
|
13598
|
+
if (line2.startsWith("@@")) {
|
|
13341
13599
|
flush();
|
|
13342
13600
|
continue;
|
|
13343
13601
|
}
|
|
13344
|
-
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(
|
|
13602
|
+
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
|
|
13345
13603
|
}
|
|
13346
13604
|
flush();
|
|
13347
13605
|
return [...files.values()];
|
|
@@ -13404,7 +13662,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
13404
13662
|
return "";
|
|
13405
13663
|
}
|
|
13406
13664
|
let out = "";
|
|
13407
|
-
for (const path of listed.split("\n").map((
|
|
13665
|
+
for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
|
|
13408
13666
|
out += `diff --git a/${path} b/${path}
|
|
13409
13667
|
--- /dev/null
|
|
13410
13668
|
+++ b/${path}
|
|
@@ -13417,7 +13675,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
13417
13675
|
continue;
|
|
13418
13676
|
}
|
|
13419
13677
|
out += `@@ -0,0 +1,${body.split("\n").length} @@
|
|
13420
|
-
${body.split("\n").map((
|
|
13678
|
+
${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
13421
13679
|
`;
|
|
13422
13680
|
}
|
|
13423
13681
|
return out;
|
|
@@ -13465,7 +13723,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
13465
13723
|
return out;
|
|
13466
13724
|
}
|
|
13467
13725
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
13468
|
-
function
|
|
13726
|
+
function report4(ctx, impacts) {
|
|
13469
13727
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
13470
13728
|
ctx.out.log(
|
|
13471
13729
|
`${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.`
|
|
@@ -13503,12 +13761,12 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
13503
13761
|
}
|
|
13504
13762
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
13505
13763
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
13506
|
-
|
|
13764
|
+
report4(ctx, impacts);
|
|
13507
13765
|
}
|
|
13508
13766
|
|
|
13509
13767
|
// src/runbook-lint.ts
|
|
13510
13768
|
function invocationsIn(body) {
|
|
13511
|
-
const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
|
|
13769
|
+
const re = /(`|npx[^\S\n]+(?:--yes[^\S\n]+)?)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
|
|
13512
13770
|
const found = [];
|
|
13513
13771
|
for (const match of body.matchAll(re)) {
|
|
13514
13772
|
if (!match[1]) continue;
|
|
@@ -13579,7 +13837,7 @@ async function runbookSearch(ctx, query, all, limit) {
|
|
|
13579
13837
|
for (const [i, hit] of result.hits.entries()) {
|
|
13580
13838
|
if (i) ctx.out.log("");
|
|
13581
13839
|
ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
|
|
13582
|
-
for (const
|
|
13840
|
+
for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
|
|
13583
13841
|
ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
|
|
13584
13842
|
}
|
|
13585
13843
|
}
|
|
@@ -13704,7 +13962,8 @@ var ALLOWED2 = [
|
|
|
13704
13962
|
"base",
|
|
13705
13963
|
"requires",
|
|
13706
13964
|
"platform",
|
|
13707
|
-
"context"
|
|
13965
|
+
"context",
|
|
13966
|
+
"open"
|
|
13708
13967
|
];
|
|
13709
13968
|
function requireSlug(slug, action2) {
|
|
13710
13969
|
if (!slug) throw new Error(`"runbook ${action2}" needs a slug, e.g. "odla-ai runbook ${action2} release"`);
|
|
@@ -13732,6 +13991,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
13732
13991
|
};
|
|
13733
13992
|
}
|
|
13734
13993
|
const needsCapability = WRITES2.has(action2) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
|
|
13994
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
13735
13995
|
const token = needsCapability ? await getScopedPlatformToken({
|
|
13736
13996
|
platform: cfg.platformUrl,
|
|
13737
13997
|
scope: "platform:runbook:write",
|
|
@@ -13739,6 +13999,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
13739
13999
|
label: `odla CLI (runbook ${action2})`,
|
|
13740
14000
|
fetch: doFetch,
|
|
13741
14001
|
stdout: out,
|
|
14002
|
+
open,
|
|
13742
14003
|
openApprovalUrl: deps.openUrl,
|
|
13743
14004
|
// A project, named context, or the global operator context owns the
|
|
13744
14005
|
// exact-scope cache; it never follows an arbitrary shell directory.
|
|
@@ -13750,11 +14011,7 @@ async function buildContext3(parsed, deps, action2) {
|
|
|
13750
14011
|
configPath: cfg.configPath,
|
|
13751
14012
|
token: stringOpt(parsed.options.token),
|
|
13752
14013
|
email: stringOpt(parsed.options.email),
|
|
13753
|
-
|
|
13754
|
-
// CI and SSH). Hardcoding `false` meant a first-time handshake printed
|
|
13755
|
-
// a link and opened nothing — the one moment a browser is the whole
|
|
13756
|
-
// point.
|
|
13757
|
-
open: void 0
|
|
14014
|
+
open
|
|
13758
14015
|
},
|
|
13759
14016
|
doFetch,
|
|
13760
14017
|
out
|
|
@@ -13955,31 +14212,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
13955
14212
|
url.searchParams.set("job", job.jobId);
|
|
13956
14213
|
out.log(` Studio: ${url.toString()}`);
|
|
13957
14214
|
}
|
|
13958
|
-
function printHostedReport(out,
|
|
13959
|
-
out.log(`security report ${
|
|
13960
|
-
out.log(` coverage: ${
|
|
13961
|
-
out.log(` findings: confirmed=${
|
|
13962
|
-
out.log(` discovery: ${
|
|
13963
|
-
out.log(` validation: ${
|
|
13964
|
-
for (const finding of
|
|
14215
|
+
function printHostedReport(out, report5) {
|
|
14216
|
+
out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
|
|
14217
|
+
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}`);
|
|
14218
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
|
|
14219
|
+
out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
|
|
14220
|
+
out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
|
|
14221
|
+
for (const finding of report5.findings) {
|
|
13965
14222
|
const location = finding.locations[0];
|
|
13966
14223
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
13967
14224
|
}
|
|
13968
|
-
for (const limitation of
|
|
14225
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
13969
14226
|
}
|
|
13970
|
-
function enforceHostedReportGate(
|
|
14227
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
13971
14228
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
13972
14229
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
13973
14230
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
13974
14231
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
13975
|
-
const confirmed =
|
|
13976
|
-
const leads = failOnCandidates ?
|
|
13977
|
-
const incomplete =
|
|
14232
|
+
const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
14233
|
+
const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
14234
|
+
const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
13978
14235
|
if (confirmed.length || leads.length || incomplete) {
|
|
13979
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
14236
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
13980
14237
|
}
|
|
13981
14238
|
if (emitSuccess) {
|
|
13982
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
14239
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
|
|
13983
14240
|
}
|
|
13984
14241
|
}
|
|
13985
14242
|
function printHostedSecurityPlanRoute(out, label, route3) {
|
|
@@ -14069,17 +14326,17 @@ async function runHostedSecurity(options) {
|
|
|
14069
14326
|
allowNetwork: false
|
|
14070
14327
|
}
|
|
14071
14328
|
});
|
|
14072
|
-
const
|
|
14073
|
-
await writeSecurityArtifacts(output,
|
|
14074
|
-
const reportDigest = await securityFingerprint(
|
|
14329
|
+
const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
14330
|
+
await writeSecurityArtifacts(output, report5);
|
|
14331
|
+
const reportDigest = await securityFingerprint(report5);
|
|
14075
14332
|
await hosted.complete({
|
|
14076
14333
|
reportDigest,
|
|
14077
|
-
coverageStatus:
|
|
14078
|
-
confirmed:
|
|
14079
|
-
candidates:
|
|
14334
|
+
coverageStatus: report5.coverageStatus,
|
|
14335
|
+
confirmed: report5.metrics.confirmed,
|
|
14336
|
+
candidates: report5.metrics.candidates
|
|
14080
14337
|
}, { signal: options.signal });
|
|
14081
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
14082
|
-
return Object.freeze({ report:
|
|
14338
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
14339
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
14083
14340
|
}
|
|
14084
14341
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
14085
14342
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -14105,14 +14362,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
14105
14362
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
14106
14363
|
return { ...profile, maxHuntTasks };
|
|
14107
14364
|
}
|
|
14108
|
-
function printSummary(out, appId, env, run,
|
|
14109
|
-
const complete =
|
|
14365
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
14366
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
14110
14367
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
14111
14368
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
14112
14369
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
14113
|
-
out.log(` coverage: ${
|
|
14114
|
-
if (
|
|
14115
|
-
out.log(` findings: confirmed=${
|
|
14370
|
+
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}`);
|
|
14371
|
+
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
14372
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
14116
14373
|
out.log(` report: ${resolve13(output, "REPORT.md")}`);
|
|
14117
14374
|
}
|
|
14118
14375
|
function formatBudget(usage) {
|
|
@@ -14359,13 +14616,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
14359
14616
|
}
|
|
14360
14617
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
14361
14618
|
}
|
|
14362
|
-
const
|
|
14619
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
14363
14620
|
if (parsed.options.json === true) {
|
|
14364
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
14621
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
14365
14622
|
} else {
|
|
14366
|
-
printHostedReport(context.stdout,
|
|
14623
|
+
printHostedReport(context.stdout, report5);
|
|
14367
14624
|
}
|
|
14368
|
-
enforceHostedReportGate(
|
|
14625
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
14369
14626
|
}
|
|
14370
14627
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
14371
14628
|
if (parsed.options.source === true) {
|
|
@@ -14433,13 +14690,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
14433
14690
|
});
|
|
14434
14691
|
enforceLocalGate(result.report, parsed);
|
|
14435
14692
|
}
|
|
14436
|
-
function enforceLocalGate(
|
|
14693
|
+
function enforceLocalGate(report5, parsed) {
|
|
14437
14694
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
14438
14695
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
14439
14696
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
14440
|
-
const confirmed = findingsAtOrAbove(
|
|
14441
|
-
const leads = failOnCandidates ? findingsAtOrAbove(
|
|
14442
|
-
const incomplete =
|
|
14697
|
+
const confirmed = findingsAtOrAbove(report5, failOn);
|
|
14698
|
+
const leads = failOnCandidates ? findingsAtOrAbove(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
14699
|
+
const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
14443
14700
|
if (confirmed.length || leads.length || incomplete) {
|
|
14444
14701
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
14445
14702
|
}
|
|
@@ -14478,9 +14735,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
14478
14735
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
14479
14736
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
14480
14737
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
14481
|
-
const
|
|
14482
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
14483
|
-
else printHostedReport(context.stdout,
|
|
14738
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId });
|
|
14739
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
|
|
14740
|
+
else printHostedReport(context.stdout, report5);
|
|
14484
14741
|
return;
|
|
14485
14742
|
}
|
|
14486
14743
|
if (sub !== "run") {
|
|
@@ -14777,4 +15034,4 @@ export {
|
|
|
14777
15034
|
isTerminalHostedSecurityStatus,
|
|
14778
15035
|
runCli
|
|
14779
15036
|
};
|
|
14780
|
-
//# sourceMappingURL=chunk-
|
|
15037
|
+
//# sourceMappingURL=chunk-EG23MPUC.js.map
|