@odla-ai/cli 0.34.1 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +384 -105
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-RUEM7ZTA.js → chunk-SRL2TN24.js} +361 -106
- package/dist/chunk-SRL2TN24.js.map +1 -0
- package/dist/{cli-NKNQLWOM.js → cli-HS35QLXR.js} +2 -2
- package/dist/index.cjs +360 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-RUEM7ZTA.js.map +0 -1
- /package/dist/{cli-NKNQLWOM.js.map → cli-HS35QLXR.js.map} +0 -0
|
@@ -48,7 +48,7 @@ function approvalLines(prompt) {
|
|
|
48
48
|
return lines;
|
|
49
49
|
}
|
|
50
50
|
function printApproval(out, prompt) {
|
|
51
|
-
for (const
|
|
51
|
+
for (const line2 of approvalLines(prompt)) out.error(line2);
|
|
52
52
|
}
|
|
53
53
|
function reminderLines(prompt) {
|
|
54
54
|
return [
|
|
@@ -134,12 +134,12 @@ function approvalHint(pending) {
|
|
|
134
134
|
}
|
|
135
135
|
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
136
136
|
const timer = setInterval(() => {
|
|
137
|
-
for (const
|
|
137
|
+
for (const line2 of reminderLines({
|
|
138
138
|
userCode: pending.userCode,
|
|
139
139
|
approvalUrl: pending.approvalUrl,
|
|
140
140
|
minutesLeft: minutesLeft(pending.expiresAt)
|
|
141
141
|
}))
|
|
142
|
-
out.log(
|
|
142
|
+
out.log(line2);
|
|
143
143
|
}, periodMs);
|
|
144
144
|
timer.unref?.();
|
|
145
145
|
return () => clearInterval(timer);
|
|
@@ -200,9 +200,9 @@ function mergeCredential(current, update) {
|
|
|
200
200
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
201
201
|
const path = resolve(rootDir, ".gitignore");
|
|
202
202
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
203
|
-
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((
|
|
203
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
204
204
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
205
|
-
const missing = wanted.filter((
|
|
205
|
+
const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
|
|
206
206
|
if (missing.length === 0) return;
|
|
207
207
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
208
208
|
writeFileSync(path, `${existing}${prefix}${missing.join("\n")}
|
|
@@ -234,7 +234,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
234
234
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
235
235
|
}
|
|
236
236
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
237
|
-
const retained = existing.split(/\r?\n/).filter((
|
|
237
|
+
const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
|
|
238
238
|
while (retained.at(-1) === "") retained.pop();
|
|
239
239
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
240
240
|
|
|
@@ -254,8 +254,8 @@ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
|
254
254
|
"ODLA_O11Y_VERSION",
|
|
255
255
|
"ODLA_O11Y_TOKEN"
|
|
256
256
|
]);
|
|
257
|
-
function isManagedDevVar(
|
|
258
|
-
const match =
|
|
257
|
+
function isManagedDevVar(line2) {
|
|
258
|
+
const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
259
259
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
260
260
|
}
|
|
261
261
|
function writePrivateText(path, text3) {
|
|
@@ -2113,7 +2113,7 @@ function chooseIdMode(options, rows) {
|
|
|
2113
2113
|
async function appImport(options) {
|
|
2114
2114
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2115
2115
|
const out = options.stdout ?? console;
|
|
2116
|
-
const say = options.json ? (
|
|
2116
|
+
const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
|
|
2117
2117
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
2118
2118
|
const text3 = options.file === "-" ? (options.readStdin ?? (() => readFileSync4(0, "utf8")))() : readFileSync4(options.file, "utf8");
|
|
2119
2119
|
const { format, sources } = parseImport(text3, options.ns);
|
|
@@ -2443,7 +2443,7 @@ async function designUnpack(parsed, deps) {
|
|
|
2443
2443
|
);
|
|
2444
2444
|
return;
|
|
2445
2445
|
}
|
|
2446
|
-
for (const
|
|
2446
|
+
for (const line2 of describeUnpack(result, outDir)) out.log(line2);
|
|
2447
2447
|
}
|
|
2448
2448
|
async function brandCommand(parsed, deps) {
|
|
2449
2449
|
const subject = parsed.positionals[1];
|
|
@@ -4352,7 +4352,7 @@ async function doctor(options) {
|
|
|
4352
4352
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
|
|
4353
4353
|
const contract = resolveSecretContract(cfg);
|
|
4354
4354
|
out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
|
|
4355
|
-
for (const
|
|
4355
|
+
for (const line2 of formatSecretContract(contract)) out.log(line2);
|
|
4356
4356
|
if (cfg.services.includes("calendar")) {
|
|
4357
4357
|
const calendar = cfg.envs.map((env) => {
|
|
4358
4358
|
const resolved = calendarServiceConfig(cfg, env);
|
|
@@ -4721,10 +4721,10 @@ async function secretsStatus(options) {
|
|
|
4721
4721
|
}
|
|
4722
4722
|
const body = await res.json();
|
|
4723
4723
|
const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
|
|
4724
|
-
const
|
|
4725
|
-
if (options.json) out.log(JSON.stringify(
|
|
4726
|
-
else printReport(
|
|
4727
|
-
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;
|
|
4728
4728
|
}
|
|
4729
4729
|
function buildReport(appId, env, tenant, contract, stored) {
|
|
4730
4730
|
const declared = new Set(contract.map((secret) => secret.name));
|
|
@@ -4741,25 +4741,25 @@ function buildReport(appId, env, tenant, contract, stored) {
|
|
|
4741
4741
|
const ok = rows.every((row) => row.state !== "missing" || !row.required);
|
|
4742
4742
|
return { app: appId, env, tenant, secrets: rows, ok };
|
|
4743
4743
|
}
|
|
4744
|
-
function printReport(
|
|
4745
|
-
out.log(`${
|
|
4746
|
-
if (
|
|
4744
|
+
function printReport(report5, out) {
|
|
4745
|
+
out.log(`${report5.app} (${report5.tenant})`);
|
|
4746
|
+
if (report5.secrets.length === 0) {
|
|
4747
4747
|
out.log(" no secrets declared and none stored");
|
|
4748
4748
|
return;
|
|
4749
4749
|
}
|
|
4750
|
-
for (const row of
|
|
4750
|
+
for (const row of report5.secrets) {
|
|
4751
4751
|
const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
|
|
4752
4752
|
const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
|
|
4753
4753
|
out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
|
|
4754
4754
|
}
|
|
4755
|
-
const missing =
|
|
4755
|
+
const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
|
|
4756
4756
|
if (missing.length) {
|
|
4757
4757
|
out.log("");
|
|
4758
4758
|
for (const row of missing) {
|
|
4759
|
-
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"`);
|
|
4760
4760
|
}
|
|
4761
4761
|
}
|
|
4762
|
-
if (
|
|
4762
|
+
if (report5.secrets.some((row) => row.state === "reserved")) {
|
|
4763
4763
|
out.log("");
|
|
4764
4764
|
out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
|
|
4765
4765
|
}
|
|
@@ -4837,12 +4837,12 @@ function claudeAdapter(skill, canonical2) {
|
|
|
4837
4837
|
const lines = match[1].split(/\r?\n/);
|
|
4838
4838
|
const frontmatter = [];
|
|
4839
4839
|
let keepIndented = false;
|
|
4840
|
-
for (const
|
|
4841
|
-
if (
|
|
4842
|
-
frontmatter.push(
|
|
4843
|
-
keepIndented =
|
|
4844
|
-
} else if (keepIndented && /^\s+/.test(
|
|
4845
|
-
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);
|
|
4846
4846
|
} else {
|
|
4847
4847
|
keepIndented = false;
|
|
4848
4848
|
}
|
|
@@ -6166,13 +6166,13 @@ async function createConversionRegistry(config) {
|
|
|
6166
6166
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
6167
6167
|
}
|
|
6168
6168
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
6169
|
-
const
|
|
6169
|
+
const get2 = (id2, kind) => {
|
|
6170
6170
|
const policy = policies.get(id2);
|
|
6171
6171
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
6172
6172
|
return policy;
|
|
6173
6173
|
};
|
|
6174
6174
|
const checked = (source, id2, kind) => {
|
|
6175
|
-
const policy =
|
|
6175
|
+
const policy = get2(id2, kind);
|
|
6176
6176
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
6177
6177
|
return policy;
|
|
6178
6178
|
};
|
|
@@ -7003,7 +7003,7 @@ var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
|
7003
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;
|
|
7004
7004
|
function stripPatchEnvelope(patch2) {
|
|
7005
7005
|
if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
|
|
7006
|
-
const kept = patch2.split("\n").filter((
|
|
7006
|
+
const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
|
|
7007
7007
|
const stripped = kept.join("\n");
|
|
7008
7008
|
return /^diff --git /m.test(stripped) ? stripped : patch2;
|
|
7009
7009
|
}
|
|
@@ -7024,9 +7024,9 @@ function validateCodePatch(rawPatch, maxBytes) {
|
|
|
7024
7024
|
const paths = [];
|
|
7025
7025
|
const lines = patch2.split("\n");
|
|
7026
7026
|
for (let index = 0; index < lines.length; index += 1) {
|
|
7027
|
-
const
|
|
7028
|
-
if (!
|
|
7029
|
-
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);
|
|
7030
7030
|
const path = match?.[1];
|
|
7031
7031
|
if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
|
|
7032
7032
|
validateRelativePath(path);
|
|
@@ -7059,9 +7059,9 @@ function resolveCodePath(workspaceDir, path) {
|
|
|
7059
7059
|
return target;
|
|
7060
7060
|
}
|
|
7061
7061
|
function describePatchFailure(patch2, detail) {
|
|
7062
|
-
const hunks = patch2.split("\n").filter((
|
|
7062
|
+
const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
|
|
7063
7063
|
const bodies = patch2.split(/^@@.*$/m).slice(1);
|
|
7064
|
-
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));
|
|
7065
7065
|
const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
|
|
7066
7066
|
return `patch did not apply: ${detail}${hint}`;
|
|
7067
7067
|
}
|
|
@@ -9622,11 +9622,237 @@ function record6(value2) {
|
|
|
9622
9622
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
9623
9623
|
}
|
|
9624
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
|
+
|
|
9625
9835
|
// src/code-command.ts
|
|
9626
9836
|
async function codeCommand(parsed, dependencies) {
|
|
9627
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
|
+
}
|
|
9628
9852
|
if (sub !== "connect") {
|
|
9629
|
-
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
|
+
);
|
|
9630
9856
|
}
|
|
9631
9857
|
assertArgs(parsed, [
|
|
9632
9858
|
"config",
|
|
@@ -10068,6 +10294,9 @@ Commands:
|
|
|
10068
10294
|
\u2014 no secret is ever copied between people.
|
|
10069
10295
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
10070
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.
|
|
10071
10300
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
10072
10301
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10073
10302
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
@@ -10168,6 +10397,17 @@ Safety:
|
|
|
10168
10397
|
approval and credential hashes live in odla-ai/db. The host
|
|
10169
10398
|
credential is never written under .odla/; it exists only in the foreground
|
|
10170
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.
|
|
10171
10411
|
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
10172
10412
|
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
10173
10413
|
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
@@ -10534,7 +10774,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10534
10774
|
throw new WatchRemoteError(cursor, error);
|
|
10535
10775
|
}
|
|
10536
10776
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10537
|
-
const result =
|
|
10777
|
+
const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10538
10778
|
throw new WatchTimeoutError(result.cursor);
|
|
10539
10779
|
}
|
|
10540
10780
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -10575,7 +10815,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10575
10815
|
});
|
|
10576
10816
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
10577
10817
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
10578
|
-
return
|
|
10818
|
+
return report2(ctx, parsed, {
|
|
10579
10819
|
found: true,
|
|
10580
10820
|
cursor,
|
|
10581
10821
|
events: matching,
|
|
@@ -10602,13 +10842,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10602
10842
|
}
|
|
10603
10843
|
if (page2.hasMore) continue;
|
|
10604
10844
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10605
|
-
return
|
|
10845
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10606
10846
|
}
|
|
10607
10847
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
10608
10848
|
await sleep(wait2);
|
|
10609
10849
|
}
|
|
10610
10850
|
}
|
|
10611
|
-
function
|
|
10851
|
+
function report2(ctx, parsed, result) {
|
|
10612
10852
|
if (ctx.json) {
|
|
10613
10853
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
10614
10854
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -11205,7 +11445,7 @@ function eventLabel(event) {
|
|
|
11205
11445
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
11206
11446
|
return body || event.payload.entityId;
|
|
11207
11447
|
}
|
|
11208
|
-
function
|
|
11448
|
+
function report3(ctx, parsed, result) {
|
|
11209
11449
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
11210
11450
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
11211
11451
|
for (const event of result.events ?? []) {
|
|
@@ -11265,7 +11505,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11265
11505
|
});
|
|
11266
11506
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
11267
11507
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11268
|
-
return
|
|
11508
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
11269
11509
|
}
|
|
11270
11510
|
const backoff = Math.min(
|
|
11271
11511
|
MAX_BACKOFF_MS2,
|
|
@@ -11306,7 +11546,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11306
11546
|
cursor,
|
|
11307
11547
|
serverTime: current.serverTime
|
|
11308
11548
|
});
|
|
11309
|
-
return
|
|
11549
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
11310
11550
|
}
|
|
11311
11551
|
if (current.events.length > 0) {
|
|
11312
11552
|
jsonl2(ctx, parsed, {
|
|
@@ -11325,7 +11565,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11325
11565
|
}
|
|
11326
11566
|
if (current.hasMore) continue;
|
|
11327
11567
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11328
|
-
return
|
|
11568
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
11329
11569
|
}
|
|
11330
11570
|
await sleep(
|
|
11331
11571
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -11862,8 +12102,8 @@ function printO11yStatus(status, out) {
|
|
|
11862
12102
|
out.log(
|
|
11863
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`
|
|
11864
12104
|
);
|
|
11865
|
-
for (const
|
|
11866
|
-
out.log(
|
|
12105
|
+
for (const line2 of providerCapacityLines(status.providerCapacity)) {
|
|
12106
|
+
out.log(line2);
|
|
11867
12107
|
}
|
|
11868
12108
|
const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
|
|
11869
12109
|
const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
|
|
@@ -12906,7 +13146,11 @@ var COMMAND_SURFACE = {
|
|
|
12906
13146
|
bug: { create: {}, list: {}, report: {} },
|
|
12907
13147
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
12908
13148
|
capabilities: {},
|
|
12909
|
-
code: {
|
|
13149
|
+
code: {
|
|
13150
|
+
connect: {},
|
|
13151
|
+
grant: { request: {}, list: {}, approve: {}, revoke: {} },
|
|
13152
|
+
repository: { show: {}, list: {}, bind: {} }
|
|
13153
|
+
},
|
|
12910
13154
|
config: { diff: {}, plan: {}, apply: {} },
|
|
12911
13155
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
12912
13156
|
credentials: { list: {}, revoke: {} },
|
|
@@ -13065,9 +13309,20 @@ async function bySlug(ctx, slug) {
|
|
|
13065
13309
|
"GET",
|
|
13066
13310
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
13067
13311
|
);
|
|
13068
|
-
const
|
|
13069
|
-
if (
|
|
13070
|
-
|
|
13312
|
+
const filtered = page2.records.find((record11) => record11.slug === slug);
|
|
13313
|
+
if (filtered) return filtered;
|
|
13314
|
+
const limit = 100;
|
|
13315
|
+
for (let offset = 0; ; offset += limit) {
|
|
13316
|
+
const fallback = await call(
|
|
13317
|
+
ctx,
|
|
13318
|
+
"GET",
|
|
13319
|
+
`/runbook?app=${encodeURIComponent(ctx.appId)}&limit=${limit}&offset=${offset}`
|
|
13320
|
+
);
|
|
13321
|
+
const found = fallback.records.find((record11) => record11.slug === slug);
|
|
13322
|
+
if (found) return found;
|
|
13323
|
+
if (!fallback.records.length || offset + fallback.records.length >= fallback.total) break;
|
|
13324
|
+
}
|
|
13325
|
+
throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
13071
13326
|
}
|
|
13072
13327
|
function readBody(file, inline) {
|
|
13073
13328
|
if (inline !== void 0) return inline;
|
|
@@ -13152,7 +13407,7 @@ async function runbookRevert(ctx, slug, version) {
|
|
|
13152
13407
|
ctx,
|
|
13153
13408
|
"POST",
|
|
13154
13409
|
`/runbook/${encodeURIComponent(runbook.id)}/revert`,
|
|
13155
|
-
{ version }
|
|
13410
|
+
{ version, expectedVersion: runbook.version }
|
|
13156
13411
|
);
|
|
13157
13412
|
if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
|
|
13158
13413
|
ctx.out.log(`${slug} reverted to v${version}, now v${result.record?.version ?? runbook.version + 1}`);
|
|
@@ -13172,8 +13427,8 @@ function parseRunbook(text3, slug) {
|
|
|
13172
13427
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
|
|
13173
13428
|
if (fm) {
|
|
13174
13429
|
rest = text3.slice(fm[0].length);
|
|
13175
|
-
for (const
|
|
13176
|
-
const pair = /^(\w+)\s*:\s*(.+)$/.exec(
|
|
13430
|
+
for (const line2 of fm[1].split(/\r?\n/)) {
|
|
13431
|
+
const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
|
|
13177
13432
|
if (!pair) continue;
|
|
13178
13433
|
const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
|
|
13179
13434
|
if (pair[1] === "summary") meta.summary = value2;
|
|
@@ -13313,18 +13568,18 @@ function areaOf(path) {
|
|
|
13313
13568
|
return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
|
|
13314
13569
|
}
|
|
13315
13570
|
function scanHunk(lines, into) {
|
|
13316
|
-
for (const [i,
|
|
13317
|
-
const decl = DECL.exec(
|
|
13571
|
+
for (const [i, line2] of lines.entries()) {
|
|
13572
|
+
const decl = DECL.exec(line2);
|
|
13318
13573
|
if (decl?.[1]) {
|
|
13319
13574
|
into.add(decl[1]);
|
|
13320
13575
|
continue;
|
|
13321
13576
|
}
|
|
13322
|
-
const named = NAMED.exec(
|
|
13577
|
+
const named = NAMED.exec(line2);
|
|
13323
13578
|
if (named?.[1]) {
|
|
13324
13579
|
for (const name of namedExports(named[1])) into.add(name);
|
|
13325
13580
|
continue;
|
|
13326
13581
|
}
|
|
13327
|
-
if (!JSDOC.test(
|
|
13582
|
+
if (!JSDOC.test(line2)) continue;
|
|
13328
13583
|
for (let j = i + 1; j < lines.length && j < i + 40; j++) {
|
|
13329
13584
|
const found = ANY_DECL.exec(lines[j]);
|
|
13330
13585
|
if (found?.[1]) {
|
|
@@ -13342,8 +13597,8 @@ function parseDiff(diff) {
|
|
|
13342
13597
|
if (current && hunk.length) scanHunk(hunk, current.exports);
|
|
13343
13598
|
hunk = [];
|
|
13344
13599
|
};
|
|
13345
|
-
for (const
|
|
13346
|
-
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(
|
|
13600
|
+
for (const line2 of diff.split("\n")) {
|
|
13601
|
+
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
|
|
13347
13602
|
if (header) {
|
|
13348
13603
|
flush();
|
|
13349
13604
|
const path = header[2] ?? header[1];
|
|
@@ -13351,11 +13606,11 @@ function parseDiff(diff) {
|
|
|
13351
13606
|
files.set(path, current);
|
|
13352
13607
|
continue;
|
|
13353
13608
|
}
|
|
13354
|
-
if (
|
|
13609
|
+
if (line2.startsWith("@@")) {
|
|
13355
13610
|
flush();
|
|
13356
13611
|
continue;
|
|
13357
13612
|
}
|
|
13358
|
-
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(
|
|
13613
|
+
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
|
|
13359
13614
|
}
|
|
13360
13615
|
flush();
|
|
13361
13616
|
return [...files.values()];
|
|
@@ -13418,7 +13673,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
13418
13673
|
return "";
|
|
13419
13674
|
}
|
|
13420
13675
|
let out = "";
|
|
13421
|
-
for (const path of listed.split("\n").map((
|
|
13676
|
+
for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
|
|
13422
13677
|
out += `diff --git a/${path} b/${path}
|
|
13423
13678
|
--- /dev/null
|
|
13424
13679
|
+++ b/${path}
|
|
@@ -13431,7 +13686,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
13431
13686
|
continue;
|
|
13432
13687
|
}
|
|
13433
13688
|
out += `@@ -0,0 +1,${body.split("\n").length} @@
|
|
13434
|
-
${body.split("\n").map((
|
|
13689
|
+
${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
13435
13690
|
`;
|
|
13436
13691
|
}
|
|
13437
13692
|
return out;
|
|
@@ -13479,7 +13734,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
13479
13734
|
return out;
|
|
13480
13735
|
}
|
|
13481
13736
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
13482
|
-
function
|
|
13737
|
+
function report4(ctx, impacts) {
|
|
13483
13738
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
13484
13739
|
ctx.out.log(
|
|
13485
13740
|
`${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.`
|
|
@@ -13517,7 +13772,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
13517
13772
|
}
|
|
13518
13773
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
13519
13774
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
13520
|
-
|
|
13775
|
+
report4(ctx, impacts);
|
|
13521
13776
|
}
|
|
13522
13777
|
|
|
13523
13778
|
// src/runbook-lint.ts
|
|
@@ -13593,7 +13848,7 @@ async function runbookSearch(ctx, query, all, limit) {
|
|
|
13593
13848
|
for (const [i, hit] of result.hits.entries()) {
|
|
13594
13849
|
if (i) ctx.out.log("");
|
|
13595
13850
|
ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
|
|
13596
|
-
for (const
|
|
13851
|
+
for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
|
|
13597
13852
|
ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
|
|
13598
13853
|
}
|
|
13599
13854
|
}
|
|
@@ -13968,31 +14223,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
13968
14223
|
url.searchParams.set("job", job.jobId);
|
|
13969
14224
|
out.log(` Studio: ${url.toString()}`);
|
|
13970
14225
|
}
|
|
13971
|
-
function printHostedReport(out,
|
|
13972
|
-
out.log(`security report ${
|
|
13973
|
-
out.log(` coverage: ${
|
|
13974
|
-
out.log(` findings: confirmed=${
|
|
13975
|
-
out.log(` discovery: ${
|
|
13976
|
-
out.log(` validation: ${
|
|
13977
|
-
for (const finding of
|
|
14226
|
+
function printHostedReport(out, report5) {
|
|
14227
|
+
out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
|
|
14228
|
+
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}`);
|
|
14229
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
|
|
14230
|
+
out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
|
|
14231
|
+
out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
|
|
14232
|
+
for (const finding of report5.findings) {
|
|
13978
14233
|
const location = finding.locations[0];
|
|
13979
14234
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
13980
14235
|
}
|
|
13981
|
-
for (const limitation of
|
|
14236
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
13982
14237
|
}
|
|
13983
|
-
function enforceHostedReportGate(
|
|
14238
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
13984
14239
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
13985
14240
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
13986
14241
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
13987
14242
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
13988
|
-
const confirmed =
|
|
13989
|
-
const leads = failOnCandidates ?
|
|
13990
|
-
const incomplete =
|
|
14243
|
+
const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
14244
|
+
const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
14245
|
+
const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
13991
14246
|
if (confirmed.length || leads.length || incomplete) {
|
|
13992
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
14247
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
13993
14248
|
}
|
|
13994
14249
|
if (emitSuccess) {
|
|
13995
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
14250
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
|
|
13996
14251
|
}
|
|
13997
14252
|
}
|
|
13998
14253
|
function printHostedSecurityPlanRoute(out, label, route3) {
|
|
@@ -14082,17 +14337,17 @@ async function runHostedSecurity(options) {
|
|
|
14082
14337
|
allowNetwork: false
|
|
14083
14338
|
}
|
|
14084
14339
|
});
|
|
14085
|
-
const
|
|
14086
|
-
await writeSecurityArtifacts(output,
|
|
14087
|
-
const reportDigest = await securityFingerprint(
|
|
14340
|
+
const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
14341
|
+
await writeSecurityArtifacts(output, report5);
|
|
14342
|
+
const reportDigest = await securityFingerprint(report5);
|
|
14088
14343
|
await hosted.complete({
|
|
14089
14344
|
reportDigest,
|
|
14090
|
-
coverageStatus:
|
|
14091
|
-
confirmed:
|
|
14092
|
-
candidates:
|
|
14345
|
+
coverageStatus: report5.coverageStatus,
|
|
14346
|
+
confirmed: report5.metrics.confirmed,
|
|
14347
|
+
candidates: report5.metrics.candidates
|
|
14093
14348
|
}, { signal: options.signal });
|
|
14094
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
14095
|
-
return Object.freeze({ report:
|
|
14349
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
14350
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
14096
14351
|
}
|
|
14097
14352
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
14098
14353
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -14118,14 +14373,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
14118
14373
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
14119
14374
|
return { ...profile, maxHuntTasks };
|
|
14120
14375
|
}
|
|
14121
|
-
function printSummary(out, appId, env, run,
|
|
14122
|
-
const complete =
|
|
14376
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
14377
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
14123
14378
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
14124
14379
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
14125
14380
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
14126
|
-
out.log(` coverage: ${
|
|
14127
|
-
if (
|
|
14128
|
-
out.log(` findings: confirmed=${
|
|
14381
|
+
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}`);
|
|
14382
|
+
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
14383
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
14129
14384
|
out.log(` report: ${resolve13(output, "REPORT.md")}`);
|
|
14130
14385
|
}
|
|
14131
14386
|
function formatBudget(usage) {
|
|
@@ -14372,13 +14627,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
14372
14627
|
}
|
|
14373
14628
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
14374
14629
|
}
|
|
14375
|
-
const
|
|
14630
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
14376
14631
|
if (parsed.options.json === true) {
|
|
14377
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
14632
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
14378
14633
|
} else {
|
|
14379
|
-
printHostedReport(context.stdout,
|
|
14634
|
+
printHostedReport(context.stdout, report5);
|
|
14380
14635
|
}
|
|
14381
|
-
enforceHostedReportGate(
|
|
14636
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
14382
14637
|
}
|
|
14383
14638
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
14384
14639
|
if (parsed.options.source === true) {
|
|
@@ -14446,13 +14701,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
14446
14701
|
});
|
|
14447
14702
|
enforceLocalGate(result.report, parsed);
|
|
14448
14703
|
}
|
|
14449
|
-
function enforceLocalGate(
|
|
14704
|
+
function enforceLocalGate(report5, parsed) {
|
|
14450
14705
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
14451
14706
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
14452
14707
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
14453
|
-
const confirmed = findingsAtOrAbove(
|
|
14454
|
-
const leads = failOnCandidates ? findingsAtOrAbove(
|
|
14455
|
-
const incomplete =
|
|
14708
|
+
const confirmed = findingsAtOrAbove(report5, failOn);
|
|
14709
|
+
const leads = failOnCandidates ? findingsAtOrAbove(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
14710
|
+
const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
14456
14711
|
if (confirmed.length || leads.length || incomplete) {
|
|
14457
14712
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
14458
14713
|
}
|
|
@@ -14491,9 +14746,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
14491
14746
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
14492
14747
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
14493
14748
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
14494
|
-
const
|
|
14495
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
14496
|
-
else printHostedReport(context.stdout,
|
|
14749
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId });
|
|
14750
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
|
|
14751
|
+
else printHostedReport(context.stdout, report5);
|
|
14497
14752
|
return;
|
|
14498
14753
|
}
|
|
14499
14754
|
if (sub !== "run") {
|
|
@@ -14790,4 +15045,4 @@ export {
|
|
|
14790
15045
|
isTerminalHostedSecurityStatus,
|
|
14791
15046
|
runCli
|
|
14792
15047
|
};
|
|
14793
|
-
//# sourceMappingURL=chunk-
|
|
15048
|
+
//# sourceMappingURL=chunk-SRL2TN24.js.map
|