@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
package/dist/index.cjs
CHANGED
|
@@ -135,7 +135,7 @@ function approvalLines(prompt) {
|
|
|
135
135
|
return lines;
|
|
136
136
|
}
|
|
137
137
|
function printApproval(out, prompt) {
|
|
138
|
-
for (const
|
|
138
|
+
for (const line2 of approvalLines(prompt)) out.error(line2);
|
|
139
139
|
}
|
|
140
140
|
function reminderLines(prompt) {
|
|
141
141
|
return [
|
|
@@ -221,12 +221,12 @@ function approvalHint(pending) {
|
|
|
221
221
|
}
|
|
222
222
|
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
223
223
|
const timer = setInterval(() => {
|
|
224
|
-
for (const
|
|
224
|
+
for (const line2 of reminderLines({
|
|
225
225
|
userCode: pending.userCode,
|
|
226
226
|
approvalUrl: pending.approvalUrl,
|
|
227
227
|
minutesLeft: minutesLeft(pending.expiresAt)
|
|
228
228
|
}))
|
|
229
|
-
out.log(
|
|
229
|
+
out.log(line2);
|
|
230
230
|
}, periodMs);
|
|
231
231
|
timer.unref?.();
|
|
232
232
|
return () => clearInterval(timer);
|
|
@@ -287,9 +287,9 @@ function mergeCredential(current, update) {
|
|
|
287
287
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
288
288
|
const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
|
|
289
289
|
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
290
|
-
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((
|
|
290
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
291
291
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
292
|
-
const missing = wanted.filter((
|
|
292
|
+
const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
|
|
293
293
|
if (missing.length === 0) return;
|
|
294
294
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
295
295
|
(0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
@@ -321,7 +321,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
321
321
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
322
322
|
}
|
|
323
323
|
const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
|
|
324
|
-
const retained = existing.split(/\r?\n/).filter((
|
|
324
|
+
const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
|
|
325
325
|
while (retained.at(-1) === "") retained.pop();
|
|
326
326
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
327
327
|
|
|
@@ -341,8 +341,8 @@ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
|
341
341
|
"ODLA_O11Y_VERSION",
|
|
342
342
|
"ODLA_O11Y_TOKEN"
|
|
343
343
|
]);
|
|
344
|
-
function isManagedDevVar(
|
|
345
|
-
const match =
|
|
344
|
+
function isManagedDevVar(line2) {
|
|
345
|
+
const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
346
346
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
347
347
|
}
|
|
348
348
|
function writePrivateText(path, text3) {
|
|
@@ -2196,7 +2196,7 @@ function chooseIdMode(options, rows) {
|
|
|
2196
2196
|
async function appImport(options) {
|
|
2197
2197
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2198
2198
|
const out = options.stdout ?? console;
|
|
2199
|
-
const say = options.json ? (
|
|
2199
|
+
const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
|
|
2200
2200
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
2201
2201
|
const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
|
|
2202
2202
|
const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
|
|
@@ -2521,7 +2521,7 @@ async function designUnpack(parsed, deps) {
|
|
|
2521
2521
|
);
|
|
2522
2522
|
return;
|
|
2523
2523
|
}
|
|
2524
|
-
for (const
|
|
2524
|
+
for (const line2 of describeUnpack(result, outDir)) out.log(line2);
|
|
2525
2525
|
}
|
|
2526
2526
|
async function brandCommand(parsed, deps) {
|
|
2527
2527
|
const subject = parsed.positionals[1];
|
|
@@ -4482,7 +4482,7 @@ async function doctor(options) {
|
|
|
4482
4482
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
|
|
4483
4483
|
const contract = resolveSecretContract(cfg);
|
|
4484
4484
|
out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
|
|
4485
|
-
for (const
|
|
4485
|
+
for (const line2 of formatSecretContract(contract)) out.log(line2);
|
|
4486
4486
|
if (cfg.services.includes("calendar")) {
|
|
4487
4487
|
const calendar = cfg.envs.map((env) => {
|
|
4488
4488
|
const resolved = calendarServiceConfig(cfg, env);
|
|
@@ -4851,10 +4851,10 @@ async function secretsStatus(options) {
|
|
|
4851
4851
|
}
|
|
4852
4852
|
const body = await res.json();
|
|
4853
4853
|
const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
|
|
4854
|
-
const
|
|
4855
|
-
if (options.json) out.log(JSON.stringify(
|
|
4856
|
-
else printReport(
|
|
4857
|
-
return
|
|
4854
|
+
const report5 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
|
|
4855
|
+
if (options.json) out.log(JSON.stringify(report5, null, 2));
|
|
4856
|
+
else printReport(report5, out);
|
|
4857
|
+
return report5;
|
|
4858
4858
|
}
|
|
4859
4859
|
function buildReport(appId, env, tenant, contract, stored) {
|
|
4860
4860
|
const declared = new Set(contract.map((secret) => secret.name));
|
|
@@ -4871,25 +4871,25 @@ function buildReport(appId, env, tenant, contract, stored) {
|
|
|
4871
4871
|
const ok = rows.every((row) => row.state !== "missing" || !row.required);
|
|
4872
4872
|
return { app: appId, env, tenant, secrets: rows, ok };
|
|
4873
4873
|
}
|
|
4874
|
-
function printReport(
|
|
4875
|
-
out.log(`${
|
|
4876
|
-
if (
|
|
4874
|
+
function printReport(report5, out) {
|
|
4875
|
+
out.log(`${report5.app} (${report5.tenant})`);
|
|
4876
|
+
if (report5.secrets.length === 0) {
|
|
4877
4877
|
out.log(" no secrets declared and none stored");
|
|
4878
4878
|
return;
|
|
4879
4879
|
}
|
|
4880
|
-
for (const row of
|
|
4880
|
+
for (const row of report5.secrets) {
|
|
4881
4881
|
const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
|
|
4882
4882
|
const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
|
|
4883
4883
|
out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
|
|
4884
4884
|
}
|
|
4885
|
-
const missing =
|
|
4885
|
+
const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
|
|
4886
4886
|
if (missing.length) {
|
|
4887
4887
|
out.log("");
|
|
4888
4888
|
for (const row of missing) {
|
|
4889
|
-
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${
|
|
4889
|
+
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report5.env} --stdin"`);
|
|
4890
4890
|
}
|
|
4891
4891
|
}
|
|
4892
|
-
if (
|
|
4892
|
+
if (report5.secrets.some((row) => row.state === "reserved")) {
|
|
4893
4893
|
out.log("");
|
|
4894
4894
|
out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
|
|
4895
4895
|
}
|
|
@@ -4967,12 +4967,12 @@ function claudeAdapter(skill, canonical2) {
|
|
|
4967
4967
|
const lines = match[1].split(/\r?\n/);
|
|
4968
4968
|
const frontmatter = [];
|
|
4969
4969
|
let keepIndented = false;
|
|
4970
|
-
for (const
|
|
4971
|
-
if (
|
|
4972
|
-
frontmatter.push(
|
|
4973
|
-
keepIndented =
|
|
4974
|
-
} else if (keepIndented && /^\s+/.test(
|
|
4975
|
-
frontmatter.push(
|
|
4970
|
+
for (const line2 of lines) {
|
|
4971
|
+
if (line2.startsWith("name:") || line2.startsWith("description:")) {
|
|
4972
|
+
frontmatter.push(line2);
|
|
4973
|
+
keepIndented = line2.startsWith("description:");
|
|
4974
|
+
} else if (keepIndented && /^\s+/.test(line2)) {
|
|
4975
|
+
frontmatter.push(line2);
|
|
4976
4976
|
} else {
|
|
4977
4977
|
keepIndented = false;
|
|
4978
4978
|
}
|
|
@@ -6293,13 +6293,13 @@ async function createConversionRegistry(config) {
|
|
|
6293
6293
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
6294
6294
|
}
|
|
6295
6295
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
6296
|
-
const
|
|
6296
|
+
const get2 = (id2, kind) => {
|
|
6297
6297
|
const policy = policies.get(id2);
|
|
6298
6298
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
6299
6299
|
return policy;
|
|
6300
6300
|
};
|
|
6301
6301
|
const checked = (source, id2, kind) => {
|
|
6302
|
-
const policy =
|
|
6302
|
+
const policy = get2(id2, kind);
|
|
6303
6303
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
6304
6304
|
return policy;
|
|
6305
6305
|
};
|
|
@@ -7130,7 +7130,7 @@ var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
|
7130
7130
|
var FORBIDDEN = /^(?:GIT binary patch|Binary files |rename (?:from|to) |copy (?:from|to) |similarity index |old mode |new mode |deleted file mode 160000|new file mode 160000)/m;
|
|
7131
7131
|
function stripPatchEnvelope(patch2) {
|
|
7132
7132
|
if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
|
|
7133
|
-
const kept = patch2.split("\n").filter((
|
|
7133
|
+
const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
|
|
7134
7134
|
const stripped = kept.join("\n");
|
|
7135
7135
|
return /^diff --git /m.test(stripped) ? stripped : patch2;
|
|
7136
7136
|
}
|
|
@@ -7151,9 +7151,9 @@ function validateCodePatch(rawPatch, maxBytes) {
|
|
|
7151
7151
|
const paths = [];
|
|
7152
7152
|
const lines = patch2.split("\n");
|
|
7153
7153
|
for (let index = 0; index < lines.length; index += 1) {
|
|
7154
|
-
const
|
|
7155
|
-
if (!
|
|
7156
|
-
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(
|
|
7154
|
+
const line2 = lines[index];
|
|
7155
|
+
if (!line2.startsWith("diff --git ")) continue;
|
|
7156
|
+
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line2);
|
|
7157
7157
|
const path = match?.[1];
|
|
7158
7158
|
if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
|
|
7159
7159
|
validateRelativePath(path);
|
|
@@ -7186,9 +7186,9 @@ function resolveCodePath(workspaceDir, path) {
|
|
|
7186
7186
|
return target;
|
|
7187
7187
|
}
|
|
7188
7188
|
function describePatchFailure(patch2, detail) {
|
|
7189
|
-
const hunks = patch2.split("\n").filter((
|
|
7189
|
+
const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
|
|
7190
7190
|
const bodies = patch2.split(/^@@.*$/m).slice(1);
|
|
7191
|
-
const contextless = bodies.some((body) => !body.split("\n").some((
|
|
7191
|
+
const contextless = bodies.some((body) => !body.split("\n").some((line2) => line2.startsWith(" ") && line2.trim().length > 0));
|
|
7192
7192
|
const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
|
|
7193
7193
|
return `patch did not apply: ${detail}${hint}`;
|
|
7194
7194
|
}
|
|
@@ -9749,11 +9749,237 @@ function record6(value2) {
|
|
|
9749
9749
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
9750
9750
|
}
|
|
9751
9751
|
|
|
9752
|
+
// src/code-grant-command.ts
|
|
9753
|
+
var ACTIONS = ["request", "list", "approve", "revoke"];
|
|
9754
|
+
var isAction = (value2) => ACTIONS.includes(value2 ?? "");
|
|
9755
|
+
async function fail2(response2, what) {
|
|
9756
|
+
const body = redactSecrets((await response2.text()).slice(0, 1e3));
|
|
9757
|
+
throw new Error(`${what} failed (${response2.status}): ${body}`);
|
|
9758
|
+
}
|
|
9759
|
+
function line(grant) {
|
|
9760
|
+
const who = grant.status === "granted" ? `granted by ${grant.approvedBy ?? "?"}` : `requested by ${grant.requestedBy}`;
|
|
9761
|
+
return `${grant.grantId} ${grant.appEnv} ${grant.owner}/${grant.name} ${grant.status} ${who}`;
|
|
9762
|
+
}
|
|
9763
|
+
function report(grants, out, json) {
|
|
9764
|
+
if (json) return out.log(JSON.stringify({ grants }, null, 2));
|
|
9765
|
+
if (!grants.length) return out.log("no repository grants for this app");
|
|
9766
|
+
out.log("GRANT ENV REPOSITORY STATUS DECIDED");
|
|
9767
|
+
for (const grant of grants) out.log(line(grant));
|
|
9768
|
+
const pending = grants.filter((grant) => grant.status === "requested");
|
|
9769
|
+
if (pending.length) {
|
|
9770
|
+
out.log(`
|
|
9771
|
+
${pending.length} awaiting approval. A human owner runs:`);
|
|
9772
|
+
for (const grant of pending) out.log(` odla-ai code grant approve ${grant.grantId} --env ${grant.appEnv}`);
|
|
9773
|
+
}
|
|
9774
|
+
}
|
|
9775
|
+
function grantsUrl(cfg, suffix = "") {
|
|
9776
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository-grants${suffix}`;
|
|
9777
|
+
}
|
|
9778
|
+
async function codeGrantCommand(parsed, deps = {}) {
|
|
9779
|
+
const action2 = parsed.positionals[2];
|
|
9780
|
+
if (!isAction(action2)) {
|
|
9781
|
+
throw new Error(
|
|
9782
|
+
`unknown code grant action "${action2 ?? ""}". Try "odla-ai code grant list --env dev".`
|
|
9783
|
+
);
|
|
9784
|
+
}
|
|
9785
|
+
const decides = action2 === "approve" || action2 === "revoke";
|
|
9786
|
+
assertArgs(parsed, ["config", "env", "json", "token", "email", "open"], decides ? 4 : 3);
|
|
9787
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
9788
|
+
const doFetch = deps.fetch ?? fetch;
|
|
9789
|
+
const out = deps.stdout ?? console;
|
|
9790
|
+
const env = stringOpt(parsed.options.env) ?? "dev";
|
|
9791
|
+
const json = parsed.options.json === true;
|
|
9792
|
+
const token = await getDeveloperToken(cfg, {
|
|
9793
|
+
configPath: cfg.configPath,
|
|
9794
|
+
token: stringOpt(parsed.options.token),
|
|
9795
|
+
email: stringOpt(parsed.options.email),
|
|
9796
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
9797
|
+
openApprovalUrl: deps.openUrl
|
|
9798
|
+
// Every verb here administers grants, which the registry gates on
|
|
9799
|
+
// app.manage — a token without it is refused as "not your app", which reads
|
|
9800
|
+
// like the wrong app rather than the wrong authority.
|
|
9801
|
+
//
|
|
9802
|
+
// Only `request` additionally asks for code.session. Approving is a human
|
|
9803
|
+
// act, and a token minted to approve has no business also carrying the
|
|
9804
|
+
// authority the approval is about.
|
|
9805
|
+
}, doFetch, out, {
|
|
9806
|
+
optionalProjectCapabilities: action2 === "request" ? ["app.manage", "code.session"] : ["app.manage"]
|
|
9807
|
+
});
|
|
9808
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
9809
|
+
if (action2 === "list") {
|
|
9810
|
+
const response3 = await doFetch(`${grantsUrl(cfg)}?env=${encodeURIComponent(env)}`, { headers: auth });
|
|
9811
|
+
if (!response3.ok) await fail2(response3, "repository grant list");
|
|
9812
|
+
const body2 = await response3.json();
|
|
9813
|
+
return report(body2.grants, out, json);
|
|
9814
|
+
}
|
|
9815
|
+
if (action2 === "request") {
|
|
9816
|
+
const response3 = await doFetch(grantsUrl(cfg), {
|
|
9817
|
+
method: "POST",
|
|
9818
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
9819
|
+
body: JSON.stringify({ env })
|
|
9820
|
+
});
|
|
9821
|
+
if (!response3.ok) await fail2(response3, "repository grant request");
|
|
9822
|
+
const { grant } = await response3.json();
|
|
9823
|
+
if (json) return out.log(JSON.stringify({ grant }, null, 2));
|
|
9824
|
+
out.log(`requested ${grant.owner}/${grant.name} (${grant.appEnv}) \u2014 ${grant.grantId}`);
|
|
9825
|
+
return out.log(grant.status === "granted" ? "already granted; nothing to approve" : `a human owner of ${cfg.app.id} approves it with:
|
|
9826
|
+
odla-ai code grant approve ${grant.grantId} --env ${env}`);
|
|
9827
|
+
}
|
|
9828
|
+
const grantId = parsed.positionals[3];
|
|
9829
|
+
if (!grantId) throw new Error(`code grant ${action2} requires the grant id from "odla-ai code grant list"`);
|
|
9830
|
+
const url = `${grantsUrl(cfg, `/${encodeURIComponent(grantId)}`)}`;
|
|
9831
|
+
const response2 = action2 === "approve" ? await doFetch(url, {
|
|
9832
|
+
method: "POST",
|
|
9833
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
9834
|
+
body: JSON.stringify({ env })
|
|
9835
|
+
}) : await doFetch(`${url}?env=${encodeURIComponent(env)}`, { method: "DELETE", headers: auth });
|
|
9836
|
+
if (!response2.ok) await fail2(response2, `repository grant ${action2}`);
|
|
9837
|
+
const body = await response2.json();
|
|
9838
|
+
if (json) return out.log(JSON.stringify(body, null, 2));
|
|
9839
|
+
out.log(action2 === "approve" && body.grant ? `granted ${body.grant.owner}/${body.grant.name} (${body.grant.appEnv}) to ${cfg.app.id}` : `revoked ${grantId}`);
|
|
9840
|
+
}
|
|
9841
|
+
|
|
9842
|
+
// src/code-repository-command.ts
|
|
9843
|
+
var ACTIONS2 = ["show", "list", "bind"];
|
|
9844
|
+
var isAction2 = (value2) => ACTIONS2.includes(value2 ?? "");
|
|
9845
|
+
async function get(doFetch, url, token, what) {
|
|
9846
|
+
const response2 = await doFetch(url, { headers: { authorization: `Bearer ${token}` } });
|
|
9847
|
+
if (!response2.ok) {
|
|
9848
|
+
const body = redactSecrets((await response2.text()).slice(0, 1e3));
|
|
9849
|
+
if (response2.status === 403 && body.includes("human_session_required")) {
|
|
9850
|
+
throw new Error(
|
|
9851
|
+
`selecting a repository needs a signed-in human session, and every CLI handshake token is a delegated agent credential by design \u2014 so this cannot be done with one, however it was approved. Connect the repository in Studio (Apps \u2192 this app \u2192 Code), then run "odla-ai code grant request".`
|
|
9852
|
+
);
|
|
9853
|
+
}
|
|
9854
|
+
throw new Error(`${what} failed (${response2.status}): ${body}`);
|
|
9855
|
+
}
|
|
9856
|
+
return await response2.json();
|
|
9857
|
+
}
|
|
9858
|
+
async function catalogue(doFetch, platformUrl, token) {
|
|
9859
|
+
const account = await get(
|
|
9860
|
+
doFetch,
|
|
9861
|
+
`${platformUrl}/registry/code/github/account`,
|
|
9862
|
+
token,
|
|
9863
|
+
"GitHub account read"
|
|
9864
|
+
);
|
|
9865
|
+
const active = account.installations.filter((install2) => install2.status === "active");
|
|
9866
|
+
const found = [];
|
|
9867
|
+
for (const install2 of active) {
|
|
9868
|
+
const { repositories } = await get(
|
|
9869
|
+
doFetch,
|
|
9870
|
+
`${platformUrl}/registry/code/github/installations/${install2.installationId}/repositories`,
|
|
9871
|
+
token,
|
|
9872
|
+
`repository catalog for ${install2.accountLogin}`
|
|
9873
|
+
);
|
|
9874
|
+
for (const repository of repositories) {
|
|
9875
|
+
found.push({ ...repository, installationId: install2.installationId });
|
|
9876
|
+
}
|
|
9877
|
+
}
|
|
9878
|
+
if (!active.length) {
|
|
9879
|
+
throw new Error(
|
|
9880
|
+
"no active GitHub App installation. Install odla's GitHub App on the account that owns the repository first, from Studio \u2192 Code."
|
|
9881
|
+
);
|
|
9882
|
+
}
|
|
9883
|
+
return found;
|
|
9884
|
+
}
|
|
9885
|
+
function selectRepository(found, full) {
|
|
9886
|
+
const wanted = full.toLowerCase();
|
|
9887
|
+
const matches = found.filter((repository) => repository.fullName.toLowerCase() === wanted);
|
|
9888
|
+
if (matches.length === 1) return matches[0];
|
|
9889
|
+
if (!matches.length) {
|
|
9890
|
+
const near = found.filter((repository) => repository.name.toLowerCase() === wanted.split("/").pop());
|
|
9891
|
+
throw new Error(near.length ? `no installation exposes ${full}. Did you mean ${near.map((repository) => repository.fullName).join(", ")}?` : `no installation exposes ${full}. Run "odla-ai code repository list" to see what is reachable.`);
|
|
9892
|
+
}
|
|
9893
|
+
throw new Error(
|
|
9894
|
+
`${full} is reachable through ${matches.length} installations (${matches.map((match) => match.installationId).join(", ")}); revoke the one you do not mean.`
|
|
9895
|
+
);
|
|
9896
|
+
}
|
|
9897
|
+
function repositoryUrl(cfg, suffix = "") {
|
|
9898
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository${suffix}`;
|
|
9899
|
+
}
|
|
9900
|
+
async function codeRepositoryCommand(parsed, deps = {}) {
|
|
9901
|
+
const action2 = parsed.positionals[2];
|
|
9902
|
+
if (!isAction2(action2)) {
|
|
9903
|
+
throw new Error(
|
|
9904
|
+
`unknown code repository action "${action2 ?? ""}". Try "odla-ai code repository show --env dev".`
|
|
9905
|
+
);
|
|
9906
|
+
}
|
|
9907
|
+
assertArgs(parsed, ["config", "env", "repo", "json", "token", "email", "open"], 3);
|
|
9908
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
9909
|
+
const doFetch = deps.fetch ?? fetch;
|
|
9910
|
+
const out = deps.stdout ?? console;
|
|
9911
|
+
const env = stringOpt(parsed.options.env) ?? "dev";
|
|
9912
|
+
const json = parsed.options.json === true;
|
|
9913
|
+
const token = await getDeveloperToken(cfg, {
|
|
9914
|
+
configPath: cfg.configPath,
|
|
9915
|
+
token: stringOpt(parsed.options.token),
|
|
9916
|
+
email: stringOpt(parsed.options.email),
|
|
9917
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
9918
|
+
openApprovalUrl: deps.openUrl
|
|
9919
|
+
}, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
|
|
9920
|
+
if (action2 === "show") {
|
|
9921
|
+
const body2 = await get(
|
|
9922
|
+
doFetch,
|
|
9923
|
+
`${repositoryUrl(cfg)}?env=${encodeURIComponent(env)}`,
|
|
9924
|
+
token,
|
|
9925
|
+
"repository read"
|
|
9926
|
+
);
|
|
9927
|
+
if (json) return out.log(JSON.stringify(body2, null, 2));
|
|
9928
|
+
const bound = body2.environmentRepository;
|
|
9929
|
+
return out.log(bound ? `${cfg.app.id} (${env}) \u2192 ${bound.owner}/${bound.name}` : `${cfg.app.id} (${env}) has no connected repository. Connect one with "odla-ai code repository bind --repo owner/name --env ${env}".`);
|
|
9930
|
+
}
|
|
9931
|
+
const found = await catalogue(doFetch, cfg.platformUrl, token);
|
|
9932
|
+
if (action2 === "list") {
|
|
9933
|
+
if (json) return out.log(JSON.stringify({ repositories: found }, null, 2));
|
|
9934
|
+
if (!found.length) return out.log("no repositories reachable through your installations");
|
|
9935
|
+
out.log("REPOSITORY INSTALL DEFAULT READY");
|
|
9936
|
+
for (const repository of found) {
|
|
9937
|
+
out.log(`${repository.fullName} ${repository.installationId} ${repository.defaultBranch ?? "\u2014"} ${repository.ready ? "yes" : "empty"}`);
|
|
9938
|
+
}
|
|
9939
|
+
return;
|
|
9940
|
+
}
|
|
9941
|
+
const repo = stringOpt(parsed.options.repo);
|
|
9942
|
+
if (!repo?.includes("/")) throw new Error("code repository bind requires --repo owner/name");
|
|
9943
|
+
const selected = selectRepository(found, repo);
|
|
9944
|
+
if (!selected.ready) {
|
|
9945
|
+
throw new Error(`${selected.fullName} has no commits yet; GitHub cannot serve a default branch for an empty repository.`);
|
|
9946
|
+
}
|
|
9947
|
+
const response2 = await doFetch(repositoryUrl(cfg, "/bind"), {
|
|
9948
|
+
method: "POST",
|
|
9949
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
9950
|
+
body: JSON.stringify({ env, installationId: selected.installationId, repositoryId: selected.repositoryId })
|
|
9951
|
+
});
|
|
9952
|
+
if (!response2.ok) {
|
|
9953
|
+
throw new Error(`repository bind failed (${response2.status}): ${redactSecrets((await response2.text()).slice(0, 1e3))}`);
|
|
9954
|
+
}
|
|
9955
|
+
const body = await response2.json();
|
|
9956
|
+
if (json) return out.log(JSON.stringify(body, null, 2));
|
|
9957
|
+
out.log(`connected ${cfg.app.id} (${env}) to ${selected.fullName} @ ${selected.defaultBranch}`);
|
|
9958
|
+
out.log(`Next, so it may be worked on unattended:
|
|
9959
|
+
odla-ai code grant request --env ${env}`);
|
|
9960
|
+
}
|
|
9961
|
+
|
|
9752
9962
|
// src/code-command.ts
|
|
9753
9963
|
async function codeCommand(parsed, dependencies) {
|
|
9754
9964
|
const sub = parsed.positionals[1];
|
|
9965
|
+
if (sub === "grant") {
|
|
9966
|
+
return await codeGrantCommand(parsed, {
|
|
9967
|
+
fetch: dependencies.fetch,
|
|
9968
|
+
stdout: dependencies.stdout,
|
|
9969
|
+
openUrl: dependencies.openUrl
|
|
9970
|
+
});
|
|
9971
|
+
}
|
|
9972
|
+
if (sub === "repository") {
|
|
9973
|
+
return await codeRepositoryCommand(parsed, {
|
|
9974
|
+
fetch: dependencies.fetch,
|
|
9975
|
+
stdout: dependencies.stdout,
|
|
9976
|
+
openUrl: dependencies.openUrl
|
|
9977
|
+
});
|
|
9978
|
+
}
|
|
9755
9979
|
if (sub !== "connect") {
|
|
9756
|
-
throw new Error(
|
|
9980
|
+
throw new Error(
|
|
9981
|
+
`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev" or "odla-ai code grant list --env dev".`
|
|
9982
|
+
);
|
|
9757
9983
|
}
|
|
9758
9984
|
assertArgs(parsed, [
|
|
9759
9985
|
"config",
|
|
@@ -10195,6 +10421,9 @@ Commands:
|
|
|
10195
10421
|
\u2014 no secret is ever copied between people.
|
|
10196
10422
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
10197
10423
|
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
10424
|
+
"code repository show" reports the repository an app works on (Studio or a
|
|
10425
|
+
human session connects one); "code grant request|list|approve|revoke" then
|
|
10426
|
+
governs unattended access: an agent may request, only a human may approve.
|
|
10198
10427
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
10199
10428
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10200
10429
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
@@ -10295,6 +10524,17 @@ Safety:
|
|
|
10295
10524
|
approval and credential hashes live in odla-ai/db. The host
|
|
10296
10525
|
credential is never written under .odla/; it exists only in the foreground
|
|
10297
10526
|
"code connect" process and is rotated by the next approved connection.
|
|
10527
|
+
"code repository bind" takes owner/name and resolves the two GitHub integers the
|
|
10528
|
+
bind route wants across every installation you have, refusing an ambiguous match
|
|
10529
|
+
rather than choosing one \u2014 the same repository name under two organizations is
|
|
10530
|
+
ordinary, and picking the first would connect an app to source nobody chose. It
|
|
10531
|
+
needs a signed-in human session; a CLI handshake token is a delegated agent
|
|
10532
|
+
credential by design, so an agent cannot choose the source it will be given.
|
|
10533
|
+
Unattended access is per repository and revocable on its own, but the connection
|
|
10534
|
+
IS the authorization: a repository is recorded for one exact app and environment
|
|
10535
|
+
only by a signed-in human choosing it. "code grant list" shows what is authorized;
|
|
10536
|
+
"revoke" stops unattended access without disconnecting the repository, and
|
|
10537
|
+
re-binding never resurrects it. Connecting sandbox never authorizes live.
|
|
10298
10538
|
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
10299
10539
|
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
10300
10540
|
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
@@ -10661,7 +10901,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10661
10901
|
throw new WatchRemoteError(cursor, error);
|
|
10662
10902
|
}
|
|
10663
10903
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10664
|
-
const result =
|
|
10904
|
+
const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10665
10905
|
throw new WatchTimeoutError(result.cursor);
|
|
10666
10906
|
}
|
|
10667
10907
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -10702,7 +10942,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10702
10942
|
});
|
|
10703
10943
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
10704
10944
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
10705
|
-
return
|
|
10945
|
+
return report2(ctx, parsed, {
|
|
10706
10946
|
found: true,
|
|
10707
10947
|
cursor,
|
|
10708
10948
|
events: matching,
|
|
@@ -10729,13 +10969,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
10729
10969
|
}
|
|
10730
10970
|
if (page2.hasMore) continue;
|
|
10731
10971
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10732
|
-
return
|
|
10972
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10733
10973
|
}
|
|
10734
10974
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
10735
10975
|
await sleep(wait2);
|
|
10736
10976
|
}
|
|
10737
10977
|
}
|
|
10738
|
-
function
|
|
10978
|
+
function report2(ctx, parsed, result) {
|
|
10739
10979
|
if (ctx.json) {
|
|
10740
10980
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
10741
10981
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -11332,7 +11572,7 @@ function eventLabel(event) {
|
|
|
11332
11572
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
11333
11573
|
return body || event.payload.entityId;
|
|
11334
11574
|
}
|
|
11335
|
-
function
|
|
11575
|
+
function report3(ctx, parsed, result) {
|
|
11336
11576
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
11337
11577
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
11338
11578
|
for (const event of result.events ?? []) {
|
|
@@ -11392,7 +11632,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11392
11632
|
});
|
|
11393
11633
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
11394
11634
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11395
|
-
return
|
|
11635
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
11396
11636
|
}
|
|
11397
11637
|
const backoff = Math.min(
|
|
11398
11638
|
MAX_BACKOFF_MS2,
|
|
@@ -11433,7 +11673,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11433
11673
|
cursor,
|
|
11434
11674
|
serverTime: current.serverTime
|
|
11435
11675
|
});
|
|
11436
|
-
return
|
|
11676
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
11437
11677
|
}
|
|
11438
11678
|
if (current.events.length > 0) {
|
|
11439
11679
|
jsonl2(ctx, parsed, {
|
|
@@ -11452,7 +11692,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
11452
11692
|
}
|
|
11453
11693
|
if (current.hasMore) continue;
|
|
11454
11694
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11455
|
-
return
|
|
11695
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
11456
11696
|
}
|
|
11457
11697
|
await sleep(
|
|
11458
11698
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -11989,8 +12229,8 @@ function printO11yStatus(status, out) {
|
|
|
11989
12229
|
out.log(
|
|
11990
12230
|
`cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
|
|
11991
12231
|
);
|
|
11992
|
-
for (const
|
|
11993
|
-
out.log(
|
|
12232
|
+
for (const line2 of providerCapacityLines(status.providerCapacity)) {
|
|
12233
|
+
out.log(line2);
|
|
11994
12234
|
}
|
|
11995
12235
|
const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
|
|
11996
12236
|
const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
|
|
@@ -13033,7 +13273,11 @@ var COMMAND_SURFACE = {
|
|
|
13033
13273
|
bug: { create: {}, list: {}, report: {} },
|
|
13034
13274
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
13035
13275
|
capabilities: {},
|
|
13036
|
-
code: {
|
|
13276
|
+
code: {
|
|
13277
|
+
connect: {},
|
|
13278
|
+
grant: { request: {}, list: {}, approve: {}, revoke: {} },
|
|
13279
|
+
repository: { show: {}, list: {}, bind: {} }
|
|
13280
|
+
},
|
|
13037
13281
|
config: { diff: {}, plan: {}, apply: {} },
|
|
13038
13282
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
13039
13283
|
credentials: { list: {}, revoke: {} },
|
|
@@ -13241,9 +13485,20 @@ async function bySlug(ctx, slug) {
|
|
|
13241
13485
|
"GET",
|
|
13242
13486
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
13243
13487
|
);
|
|
13244
|
-
const
|
|
13245
|
-
if (
|
|
13246
|
-
|
|
13488
|
+
const filtered = page2.records.find((record11) => record11.slug === slug);
|
|
13489
|
+
if (filtered) return filtered;
|
|
13490
|
+
const limit = 100;
|
|
13491
|
+
for (let offset = 0; ; offset += limit) {
|
|
13492
|
+
const fallback = await call(
|
|
13493
|
+
ctx,
|
|
13494
|
+
"GET",
|
|
13495
|
+
`/runbook?app=${encodeURIComponent(ctx.appId)}&limit=${limit}&offset=${offset}`
|
|
13496
|
+
);
|
|
13497
|
+
const found = fallback.records.find((record11) => record11.slug === slug);
|
|
13498
|
+
if (found) return found;
|
|
13499
|
+
if (!fallback.records.length || offset + fallback.records.length >= fallback.total) break;
|
|
13500
|
+
}
|
|
13501
|
+
throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
13247
13502
|
}
|
|
13248
13503
|
function readBody(file, inline) {
|
|
13249
13504
|
if (inline !== void 0) return inline;
|
|
@@ -13328,7 +13583,7 @@ async function runbookRevert(ctx, slug, version) {
|
|
|
13328
13583
|
ctx,
|
|
13329
13584
|
"POST",
|
|
13330
13585
|
`/runbook/${encodeURIComponent(runbook.id)}/revert`,
|
|
13331
|
-
{ version }
|
|
13586
|
+
{ version, expectedVersion: runbook.version }
|
|
13332
13587
|
);
|
|
13333
13588
|
if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
|
|
13334
13589
|
ctx.out.log(`${slug} reverted to v${version}, now v${result.record?.version ?? runbook.version + 1}`);
|
|
@@ -13348,8 +13603,8 @@ function parseRunbook(text3, slug) {
|
|
|
13348
13603
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
|
|
13349
13604
|
if (fm) {
|
|
13350
13605
|
rest = text3.slice(fm[0].length);
|
|
13351
|
-
for (const
|
|
13352
|
-
const pair = /^(\w+)\s*:\s*(.+)$/.exec(
|
|
13606
|
+
for (const line2 of fm[1].split(/\r?\n/)) {
|
|
13607
|
+
const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
|
|
13353
13608
|
if (!pair) continue;
|
|
13354
13609
|
const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
|
|
13355
13610
|
if (pair[1] === "summary") meta.summary = value2;
|
|
@@ -13489,18 +13744,18 @@ function areaOf(path) {
|
|
|
13489
13744
|
return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
|
|
13490
13745
|
}
|
|
13491
13746
|
function scanHunk(lines, into) {
|
|
13492
|
-
for (const [i,
|
|
13493
|
-
const decl = DECL.exec(
|
|
13747
|
+
for (const [i, line2] of lines.entries()) {
|
|
13748
|
+
const decl = DECL.exec(line2);
|
|
13494
13749
|
if (decl?.[1]) {
|
|
13495
13750
|
into.add(decl[1]);
|
|
13496
13751
|
continue;
|
|
13497
13752
|
}
|
|
13498
|
-
const named = NAMED.exec(
|
|
13753
|
+
const named = NAMED.exec(line2);
|
|
13499
13754
|
if (named?.[1]) {
|
|
13500
13755
|
for (const name of namedExports(named[1])) into.add(name);
|
|
13501
13756
|
continue;
|
|
13502
13757
|
}
|
|
13503
|
-
if (!JSDOC.test(
|
|
13758
|
+
if (!JSDOC.test(line2)) continue;
|
|
13504
13759
|
for (let j = i + 1; j < lines.length && j < i + 40; j++) {
|
|
13505
13760
|
const found = ANY_DECL.exec(lines[j]);
|
|
13506
13761
|
if (found?.[1]) {
|
|
@@ -13518,8 +13773,8 @@ function parseDiff(diff) {
|
|
|
13518
13773
|
if (current && hunk.length) scanHunk(hunk, current.exports);
|
|
13519
13774
|
hunk = [];
|
|
13520
13775
|
};
|
|
13521
|
-
for (const
|
|
13522
|
-
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(
|
|
13776
|
+
for (const line2 of diff.split("\n")) {
|
|
13777
|
+
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
|
|
13523
13778
|
if (header) {
|
|
13524
13779
|
flush();
|
|
13525
13780
|
const path = header[2] ?? header[1];
|
|
@@ -13527,11 +13782,11 @@ function parseDiff(diff) {
|
|
|
13527
13782
|
files.set(path, current);
|
|
13528
13783
|
continue;
|
|
13529
13784
|
}
|
|
13530
|
-
if (
|
|
13785
|
+
if (line2.startsWith("@@")) {
|
|
13531
13786
|
flush();
|
|
13532
13787
|
continue;
|
|
13533
13788
|
}
|
|
13534
|
-
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(
|
|
13789
|
+
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
|
|
13535
13790
|
}
|
|
13536
13791
|
flush();
|
|
13537
13792
|
return [...files.values()];
|
|
@@ -13594,7 +13849,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
13594
13849
|
return "";
|
|
13595
13850
|
}
|
|
13596
13851
|
let out = "";
|
|
13597
|
-
for (const path of listed.split("\n").map((
|
|
13852
|
+
for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
|
|
13598
13853
|
out += `diff --git a/${path} b/${path}
|
|
13599
13854
|
--- /dev/null
|
|
13600
13855
|
+++ b/${path}
|
|
@@ -13607,7 +13862,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
13607
13862
|
continue;
|
|
13608
13863
|
}
|
|
13609
13864
|
out += `@@ -0,0 +1,${body.split("\n").length} @@
|
|
13610
|
-
${body.split("\n").map((
|
|
13865
|
+
${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
13611
13866
|
`;
|
|
13612
13867
|
}
|
|
13613
13868
|
return out;
|
|
@@ -13655,7 +13910,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
13655
13910
|
return out;
|
|
13656
13911
|
}
|
|
13657
13912
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
13658
|
-
function
|
|
13913
|
+
function report4(ctx, impacts) {
|
|
13659
13914
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
13660
13915
|
ctx.out.log(
|
|
13661
13916
|
`${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.`
|
|
@@ -13693,7 +13948,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
13693
13948
|
}
|
|
13694
13949
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
13695
13950
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
13696
|
-
|
|
13951
|
+
report4(ctx, impacts);
|
|
13697
13952
|
}
|
|
13698
13953
|
|
|
13699
13954
|
// src/runbook-lint.ts
|
|
@@ -13769,7 +14024,7 @@ async function runbookSearch(ctx, query, all, limit) {
|
|
|
13769
14024
|
for (const [i, hit] of result.hits.entries()) {
|
|
13770
14025
|
if (i) ctx.out.log("");
|
|
13771
14026
|
ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
|
|
13772
|
-
for (const
|
|
14027
|
+
for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
|
|
13773
14028
|
ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
|
|
13774
14029
|
}
|
|
13775
14030
|
}
|
|
@@ -14144,31 +14399,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
14144
14399
|
url.searchParams.set("job", job.jobId);
|
|
14145
14400
|
out.log(` Studio: ${url.toString()}`);
|
|
14146
14401
|
}
|
|
14147
|
-
function printHostedReport(out,
|
|
14148
|
-
out.log(`security report ${
|
|
14149
|
-
out.log(` coverage: ${
|
|
14150
|
-
out.log(` findings: confirmed=${
|
|
14151
|
-
out.log(` discovery: ${
|
|
14152
|
-
out.log(` validation: ${
|
|
14153
|
-
for (const finding of
|
|
14402
|
+
function printHostedReport(out, report5) {
|
|
14403
|
+
out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
|
|
14404
|
+
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}`);
|
|
14405
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
|
|
14406
|
+
out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
|
|
14407
|
+
out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
|
|
14408
|
+
for (const finding of report5.findings) {
|
|
14154
14409
|
const location = finding.locations[0];
|
|
14155
14410
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
14156
14411
|
}
|
|
14157
|
-
for (const limitation of
|
|
14412
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
14158
14413
|
}
|
|
14159
|
-
function enforceHostedReportGate(
|
|
14414
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
14160
14415
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
14161
14416
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
14162
14417
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
14163
14418
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
14164
|
-
const confirmed =
|
|
14165
|
-
const leads = failOnCandidates ?
|
|
14166
|
-
const incomplete =
|
|
14419
|
+
const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
14420
|
+
const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
14421
|
+
const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
14167
14422
|
if (confirmed.length || leads.length || incomplete) {
|
|
14168
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
14423
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
14169
14424
|
}
|
|
14170
14425
|
if (emitSuccess) {
|
|
14171
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
14426
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
|
|
14172
14427
|
}
|
|
14173
14428
|
}
|
|
14174
14429
|
function printHostedSecurityPlanRoute(out, label, route3) {
|
|
@@ -14251,17 +14506,17 @@ async function runHostedSecurity(options) {
|
|
|
14251
14506
|
allowNetwork: false
|
|
14252
14507
|
}
|
|
14253
14508
|
});
|
|
14254
|
-
const
|
|
14255
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
14256
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
14509
|
+
const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
14510
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report5);
|
|
14511
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report5);
|
|
14257
14512
|
await hosted.complete({
|
|
14258
14513
|
reportDigest,
|
|
14259
|
-
coverageStatus:
|
|
14260
|
-
confirmed:
|
|
14261
|
-
candidates:
|
|
14514
|
+
coverageStatus: report5.coverageStatus,
|
|
14515
|
+
confirmed: report5.metrics.confirmed,
|
|
14516
|
+
candidates: report5.metrics.candidates
|
|
14262
14517
|
}, { signal: options.signal });
|
|
14263
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
14264
|
-
return Object.freeze({ report:
|
|
14518
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
14519
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
14265
14520
|
}
|
|
14266
14521
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
14267
14522
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -14287,14 +14542,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
14287
14542
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
14288
14543
|
return { ...profile, maxHuntTasks };
|
|
14289
14544
|
}
|
|
14290
|
-
function printSummary(out, appId, env, run,
|
|
14291
|
-
const complete =
|
|
14545
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
14546
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
14292
14547
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
14293
14548
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
14294
14549
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
14295
|
-
out.log(` coverage: ${
|
|
14296
|
-
if (
|
|
14297
|
-
out.log(` findings: confirmed=${
|
|
14550
|
+
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}`);
|
|
14551
|
+
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
14552
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
14298
14553
|
out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
|
|
14299
14554
|
}
|
|
14300
14555
|
function formatBudget(usage) {
|
|
@@ -14541,13 +14796,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
14541
14796
|
}
|
|
14542
14797
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
14543
14798
|
}
|
|
14544
|
-
const
|
|
14799
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
14545
14800
|
if (parsed.options.json === true) {
|
|
14546
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
14801
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
14547
14802
|
} else {
|
|
14548
|
-
printHostedReport(context.stdout,
|
|
14803
|
+
printHostedReport(context.stdout, report5);
|
|
14549
14804
|
}
|
|
14550
|
-
enforceHostedReportGate(
|
|
14805
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
14551
14806
|
}
|
|
14552
14807
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
14553
14808
|
if (parsed.options.source === true) {
|
|
@@ -14615,13 +14870,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
14615
14870
|
});
|
|
14616
14871
|
enforceLocalGate(result.report, parsed);
|
|
14617
14872
|
}
|
|
14618
|
-
function enforceLocalGate(
|
|
14873
|
+
function enforceLocalGate(report5, parsed) {
|
|
14619
14874
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
14620
14875
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
14621
14876
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
14622
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
14623
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
14624
|
-
const incomplete =
|
|
14877
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
|
|
14878
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
14879
|
+
const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
14625
14880
|
if (confirmed.length || leads.length || incomplete) {
|
|
14626
14881
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
14627
14882
|
}
|
|
@@ -14660,9 +14915,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
14660
14915
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
14661
14916
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
14662
14917
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
14663
|
-
const
|
|
14664
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
14665
|
-
else printHostedReport(context.stdout,
|
|
14918
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId });
|
|
14919
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
|
|
14920
|
+
else printHostedReport(context.stdout, report5);
|
|
14666
14921
|
return;
|
|
14667
14922
|
}
|
|
14668
14923
|
if (sub !== "run") {
|