@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/bin.cjs
CHANGED
|
@@ -215,7 +215,7 @@ function approvalLines(prompt) {
|
|
|
215
215
|
return lines;
|
|
216
216
|
}
|
|
217
217
|
function printApproval(out, prompt) {
|
|
218
|
-
for (const
|
|
218
|
+
for (const line2 of approvalLines(prompt)) out.error(line2);
|
|
219
219
|
}
|
|
220
220
|
function reminderLines(prompt) {
|
|
221
221
|
return [
|
|
@@ -321,12 +321,12 @@ function approvalHint(pending) {
|
|
|
321
321
|
}
|
|
322
322
|
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
323
323
|
const timer = setInterval(() => {
|
|
324
|
-
for (const
|
|
324
|
+
for (const line2 of reminderLines({
|
|
325
325
|
userCode: pending.userCode,
|
|
326
326
|
approvalUrl: pending.approvalUrl,
|
|
327
327
|
minutesLeft: minutesLeft(pending.expiresAt)
|
|
328
328
|
}))
|
|
329
|
-
out.log(
|
|
329
|
+
out.log(line2);
|
|
330
330
|
}, periodMs);
|
|
331
331
|
timer.unref?.();
|
|
332
332
|
return () => clearInterval(timer);
|
|
@@ -395,9 +395,9 @@ function mergeCredential(current, update) {
|
|
|
395
395
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
396
396
|
const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
|
|
397
397
|
const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
|
|
398
|
-
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((
|
|
398
|
+
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
399
399
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
400
|
-
const missing = wanted.filter((
|
|
400
|
+
const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
|
|
401
401
|
if (missing.length === 0) return;
|
|
402
402
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
403
403
|
(0, import_node_fs5.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
@@ -429,7 +429,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
429
429
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
430
430
|
}
|
|
431
431
|
const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
|
|
432
|
-
const retained = existing.split(/\r?\n/).filter((
|
|
432
|
+
const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
|
|
433
433
|
while (retained.at(-1) === "") retained.pop();
|
|
434
434
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
435
435
|
|
|
@@ -437,8 +437,8 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
437
437
|
writePrivateText(path, `${prefix}${lines.join("\n")}
|
|
438
438
|
`);
|
|
439
439
|
}
|
|
440
|
-
function isManagedDevVar(
|
|
441
|
-
const match =
|
|
440
|
+
function isManagedDevVar(line2) {
|
|
441
|
+
const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
|
|
442
442
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
443
443
|
}
|
|
444
444
|
function writePrivateText(path, text3) {
|
|
@@ -2505,7 +2505,7 @@ function chooseIdMode(options, rows) {
|
|
|
2505
2505
|
async function appImport(options) {
|
|
2506
2506
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2507
2507
|
const out = options.stdout ?? console;
|
|
2508
|
-
const say = options.json ? (
|
|
2508
|
+
const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
|
|
2509
2509
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
2510
2510
|
const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs10.readFileSync)(0, "utf8")))() : (0, import_node_fs10.readFileSync)(options.file, "utf8");
|
|
2511
2511
|
const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
|
|
@@ -2882,7 +2882,7 @@ async function designUnpack(parsed, deps) {
|
|
|
2882
2882
|
);
|
|
2883
2883
|
return;
|
|
2884
2884
|
}
|
|
2885
|
-
for (const
|
|
2885
|
+
for (const line2 of describeUnpack(result, outDir)) out.log(line2);
|
|
2886
2886
|
}
|
|
2887
2887
|
async function brandCommand(parsed, deps) {
|
|
2888
2888
|
const subject = parsed.positionals[1];
|
|
@@ -4969,7 +4969,7 @@ async function doctor(options) {
|
|
|
4969
4969
|
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
|
|
4970
4970
|
const contract = resolveSecretContract(cfg);
|
|
4971
4971
|
out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
|
|
4972
|
-
for (const
|
|
4972
|
+
for (const line2 of formatSecretContract(contract)) out.log(line2);
|
|
4973
4973
|
if (cfg.services.includes("calendar")) {
|
|
4974
4974
|
const calendar = cfg.envs.map((env) => {
|
|
4975
4975
|
const resolved = calendarServiceConfig(cfg, env);
|
|
@@ -5387,10 +5387,10 @@ async function secretsStatus(options) {
|
|
|
5387
5387
|
}
|
|
5388
5388
|
const body = await res.json();
|
|
5389
5389
|
const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
|
|
5390
|
-
const
|
|
5391
|
-
if (options.json) out.log(JSON.stringify(
|
|
5392
|
-
else printReport(
|
|
5393
|
-
return
|
|
5390
|
+
const report5 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
|
|
5391
|
+
if (options.json) out.log(JSON.stringify(report5, null, 2));
|
|
5392
|
+
else printReport(report5, out);
|
|
5393
|
+
return report5;
|
|
5394
5394
|
}
|
|
5395
5395
|
function buildReport(appId, env, tenant, contract, stored) {
|
|
5396
5396
|
const declared = new Set(contract.map((secret) => secret.name));
|
|
@@ -5407,25 +5407,25 @@ function buildReport(appId, env, tenant, contract, stored) {
|
|
|
5407
5407
|
const ok = rows.every((row) => row.state !== "missing" || !row.required);
|
|
5408
5408
|
return { app: appId, env, tenant, secrets: rows, ok };
|
|
5409
5409
|
}
|
|
5410
|
-
function printReport(
|
|
5411
|
-
out.log(`${
|
|
5412
|
-
if (
|
|
5410
|
+
function printReport(report5, out) {
|
|
5411
|
+
out.log(`${report5.app} (${report5.tenant})`);
|
|
5412
|
+
if (report5.secrets.length === 0) {
|
|
5413
5413
|
out.log(" no secrets declared and none stored");
|
|
5414
5414
|
return;
|
|
5415
5415
|
}
|
|
5416
|
-
for (const row of
|
|
5416
|
+
for (const row of report5.secrets) {
|
|
5417
5417
|
const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
|
|
5418
5418
|
const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
|
|
5419
5419
|
out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
|
|
5420
5420
|
}
|
|
5421
|
-
const missing =
|
|
5421
|
+
const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
|
|
5422
5422
|
if (missing.length) {
|
|
5423
5423
|
out.log("");
|
|
5424
5424
|
for (const row of missing) {
|
|
5425
|
-
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${
|
|
5425
|
+
out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report5.env} --stdin"`);
|
|
5426
5426
|
}
|
|
5427
5427
|
}
|
|
5428
|
-
if (
|
|
5428
|
+
if (report5.secrets.some((row) => row.state === "reserved")) {
|
|
5429
5429
|
out.log("");
|
|
5430
5430
|
out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
|
|
5431
5431
|
}
|
|
@@ -5449,12 +5449,12 @@ function claudeAdapter(skill, canonical2) {
|
|
|
5449
5449
|
const lines = match[1].split(/\r?\n/);
|
|
5450
5450
|
const frontmatter = [];
|
|
5451
5451
|
let keepIndented = false;
|
|
5452
|
-
for (const
|
|
5453
|
-
if (
|
|
5454
|
-
frontmatter.push(
|
|
5455
|
-
keepIndented =
|
|
5456
|
-
} else if (keepIndented && /^\s+/.test(
|
|
5457
|
-
frontmatter.push(
|
|
5452
|
+
for (const line2 of lines) {
|
|
5453
|
+
if (line2.startsWith("name:") || line2.startsWith("description:")) {
|
|
5454
|
+
frontmatter.push(line2);
|
|
5455
|
+
keepIndented = line2.startsWith("description:");
|
|
5456
|
+
} else if (keepIndented && /^\s+/.test(line2)) {
|
|
5457
|
+
frontmatter.push(line2);
|
|
5458
5458
|
} else {
|
|
5459
5459
|
keepIndented = false;
|
|
5460
5460
|
}
|
|
@@ -6903,13 +6903,13 @@ async function createConversionRegistry(config) {
|
|
|
6903
6903
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
6904
6904
|
}
|
|
6905
6905
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
6906
|
-
const
|
|
6906
|
+
const get2 = (id2, kind) => {
|
|
6907
6907
|
const policy = policies.get(id2);
|
|
6908
6908
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
6909
6909
|
return policy;
|
|
6910
6910
|
};
|
|
6911
6911
|
const checked = (source, id2, kind) => {
|
|
6912
|
-
const policy =
|
|
6912
|
+
const policy = get2(id2, kind);
|
|
6913
6913
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
6914
6914
|
return policy;
|
|
6915
6915
|
};
|
|
@@ -7745,7 +7745,7 @@ function parseCandidate(value2) {
|
|
|
7745
7745
|
}
|
|
7746
7746
|
function stripPatchEnvelope(patch2) {
|
|
7747
7747
|
if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
|
|
7748
|
-
const kept = patch2.split("\n").filter((
|
|
7748
|
+
const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
|
|
7749
7749
|
const stripped = kept.join("\n");
|
|
7750
7750
|
return /^diff --git /m.test(stripped) ? stripped : patch2;
|
|
7751
7751
|
}
|
|
@@ -7766,9 +7766,9 @@ function validateCodePatch(rawPatch, maxBytes) {
|
|
|
7766
7766
|
const paths = [];
|
|
7767
7767
|
const lines = patch2.split("\n");
|
|
7768
7768
|
for (let index = 0; index < lines.length; index += 1) {
|
|
7769
|
-
const
|
|
7770
|
-
if (!
|
|
7771
|
-
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(
|
|
7769
|
+
const line2 = lines[index];
|
|
7770
|
+
if (!line2.startsWith("diff --git ")) continue;
|
|
7771
|
+
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line2);
|
|
7772
7772
|
const path = match?.[1];
|
|
7773
7773
|
if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
|
|
7774
7774
|
validateRelativePath(path);
|
|
@@ -7801,9 +7801,9 @@ function resolveCodePath(workspaceDir, path) {
|
|
|
7801
7801
|
return target;
|
|
7802
7802
|
}
|
|
7803
7803
|
function describePatchFailure(patch2, detail) {
|
|
7804
|
-
const hunks = patch2.split("\n").filter((
|
|
7804
|
+
const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
|
|
7805
7805
|
const bodies = patch2.split(/^@@.*$/m).slice(1);
|
|
7806
|
-
const contextless = bodies.some((body) => !body.split("\n").some((
|
|
7806
|
+
const contextless = bodies.some((body) => !body.split("\n").some((line2) => line2.startsWith(" ") && line2.trim().length > 0));
|
|
7807
7807
|
const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
|
|
7808
7808
|
return `patch did not apply: ${detail}${hint}`;
|
|
7809
7809
|
}
|
|
@@ -10437,11 +10437,259 @@ var init_code_connect = __esm({
|
|
|
10437
10437
|
}
|
|
10438
10438
|
});
|
|
10439
10439
|
|
|
10440
|
+
// src/code-grant-command.ts
|
|
10441
|
+
async function fail2(response2, what) {
|
|
10442
|
+
const body = redactSecrets((await response2.text()).slice(0, 1e3));
|
|
10443
|
+
throw new Error(`${what} failed (${response2.status}): ${body}`);
|
|
10444
|
+
}
|
|
10445
|
+
function line(grant) {
|
|
10446
|
+
const who = grant.status === "granted" ? `granted by ${grant.approvedBy ?? "?"}` : `requested by ${grant.requestedBy}`;
|
|
10447
|
+
return `${grant.grantId} ${grant.appEnv} ${grant.owner}/${grant.name} ${grant.status} ${who}`;
|
|
10448
|
+
}
|
|
10449
|
+
function report(grants, out, json) {
|
|
10450
|
+
if (json) return out.log(JSON.stringify({ grants }, null, 2));
|
|
10451
|
+
if (!grants.length) return out.log("no repository grants for this app");
|
|
10452
|
+
out.log("GRANT ENV REPOSITORY STATUS DECIDED");
|
|
10453
|
+
for (const grant of grants) out.log(line(grant));
|
|
10454
|
+
const pending = grants.filter((grant) => grant.status === "requested");
|
|
10455
|
+
if (pending.length) {
|
|
10456
|
+
out.log(`
|
|
10457
|
+
${pending.length} awaiting approval. A human owner runs:`);
|
|
10458
|
+
for (const grant of pending) out.log(` odla-ai code grant approve ${grant.grantId} --env ${grant.appEnv}`);
|
|
10459
|
+
}
|
|
10460
|
+
}
|
|
10461
|
+
function grantsUrl(cfg, suffix = "") {
|
|
10462
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository-grants${suffix}`;
|
|
10463
|
+
}
|
|
10464
|
+
async function codeGrantCommand(parsed, deps = {}) {
|
|
10465
|
+
const action2 = parsed.positionals[2];
|
|
10466
|
+
if (!isAction(action2)) {
|
|
10467
|
+
throw new Error(
|
|
10468
|
+
`unknown code grant action "${action2 ?? ""}". Try "odla-ai code grant list --env dev".`
|
|
10469
|
+
);
|
|
10470
|
+
}
|
|
10471
|
+
const decides = action2 === "approve" || action2 === "revoke";
|
|
10472
|
+
assertArgs(parsed, ["config", "env", "json", "token", "email", "open"], decides ? 4 : 3);
|
|
10473
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
10474
|
+
const doFetch = deps.fetch ?? fetch;
|
|
10475
|
+
const out = deps.stdout ?? console;
|
|
10476
|
+
const env = stringOpt(parsed.options.env) ?? "dev";
|
|
10477
|
+
const json = parsed.options.json === true;
|
|
10478
|
+
const token = await getDeveloperToken(cfg, {
|
|
10479
|
+
configPath: cfg.configPath,
|
|
10480
|
+
token: stringOpt(parsed.options.token),
|
|
10481
|
+
email: stringOpt(parsed.options.email),
|
|
10482
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
10483
|
+
openApprovalUrl: deps.openUrl
|
|
10484
|
+
// Every verb here administers grants, which the registry gates on
|
|
10485
|
+
// app.manage — a token without it is refused as "not your app", which reads
|
|
10486
|
+
// like the wrong app rather than the wrong authority.
|
|
10487
|
+
//
|
|
10488
|
+
// Only `request` additionally asks for code.session. Approving is a human
|
|
10489
|
+
// act, and a token minted to approve has no business also carrying the
|
|
10490
|
+
// authority the approval is about.
|
|
10491
|
+
}, doFetch, out, {
|
|
10492
|
+
optionalProjectCapabilities: action2 === "request" ? ["app.manage", "code.session"] : ["app.manage"]
|
|
10493
|
+
});
|
|
10494
|
+
const auth = { authorization: `Bearer ${token}` };
|
|
10495
|
+
if (action2 === "list") {
|
|
10496
|
+
const response3 = await doFetch(`${grantsUrl(cfg)}?env=${encodeURIComponent(env)}`, { headers: auth });
|
|
10497
|
+
if (!response3.ok) await fail2(response3, "repository grant list");
|
|
10498
|
+
const body2 = await response3.json();
|
|
10499
|
+
return report(body2.grants, out, json);
|
|
10500
|
+
}
|
|
10501
|
+
if (action2 === "request") {
|
|
10502
|
+
const response3 = await doFetch(grantsUrl(cfg), {
|
|
10503
|
+
method: "POST",
|
|
10504
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
10505
|
+
body: JSON.stringify({ env })
|
|
10506
|
+
});
|
|
10507
|
+
if (!response3.ok) await fail2(response3, "repository grant request");
|
|
10508
|
+
const { grant } = await response3.json();
|
|
10509
|
+
if (json) return out.log(JSON.stringify({ grant }, null, 2));
|
|
10510
|
+
out.log(`requested ${grant.owner}/${grant.name} (${grant.appEnv}) \u2014 ${grant.grantId}`);
|
|
10511
|
+
return out.log(grant.status === "granted" ? "already granted; nothing to approve" : `a human owner of ${cfg.app.id} approves it with:
|
|
10512
|
+
odla-ai code grant approve ${grant.grantId} --env ${env}`);
|
|
10513
|
+
}
|
|
10514
|
+
const grantId = parsed.positionals[3];
|
|
10515
|
+
if (!grantId) throw new Error(`code grant ${action2} requires the grant id from "odla-ai code grant list"`);
|
|
10516
|
+
const url = `${grantsUrl(cfg, `/${encodeURIComponent(grantId)}`)}`;
|
|
10517
|
+
const response2 = action2 === "approve" ? await doFetch(url, {
|
|
10518
|
+
method: "POST",
|
|
10519
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
10520
|
+
body: JSON.stringify({ env })
|
|
10521
|
+
}) : await doFetch(`${url}?env=${encodeURIComponent(env)}`, { method: "DELETE", headers: auth });
|
|
10522
|
+
if (!response2.ok) await fail2(response2, `repository grant ${action2}`);
|
|
10523
|
+
const body = await response2.json();
|
|
10524
|
+
if (json) return out.log(JSON.stringify(body, null, 2));
|
|
10525
|
+
out.log(action2 === "approve" && body.grant ? `granted ${body.grant.owner}/${body.grant.name} (${body.grant.appEnv}) to ${cfg.app.id}` : `revoked ${grantId}`);
|
|
10526
|
+
}
|
|
10527
|
+
var ACTIONS, isAction;
|
|
10528
|
+
var init_code_grant_command = __esm({
|
|
10529
|
+
"src/code-grant-command.ts"() {
|
|
10530
|
+
"use strict";
|
|
10531
|
+
init_cjs_shims();
|
|
10532
|
+
init_argv();
|
|
10533
|
+
init_config();
|
|
10534
|
+
init_redact();
|
|
10535
|
+
init_token();
|
|
10536
|
+
ACTIONS = ["request", "list", "approve", "revoke"];
|
|
10537
|
+
isAction = (value2) => ACTIONS.includes(value2 ?? "");
|
|
10538
|
+
}
|
|
10539
|
+
});
|
|
10540
|
+
|
|
10541
|
+
// src/code-repository-command.ts
|
|
10542
|
+
async function get(doFetch, url, token, what) {
|
|
10543
|
+
const response2 = await doFetch(url, { headers: { authorization: `Bearer ${token}` } });
|
|
10544
|
+
if (!response2.ok) {
|
|
10545
|
+
const body = redactSecrets((await response2.text()).slice(0, 1e3));
|
|
10546
|
+
if (response2.status === 403 && body.includes("human_session_required")) {
|
|
10547
|
+
throw new Error(
|
|
10548
|
+
`selecting a repository needs a signed-in human session, and every CLI handshake token is a delegated agent credential by design \u2014 so this cannot be done with one, however it was approved. Connect the repository in Studio (Apps \u2192 this app \u2192 Code), then run "odla-ai code grant request".`
|
|
10549
|
+
);
|
|
10550
|
+
}
|
|
10551
|
+
throw new Error(`${what} failed (${response2.status}): ${body}`);
|
|
10552
|
+
}
|
|
10553
|
+
return await response2.json();
|
|
10554
|
+
}
|
|
10555
|
+
async function catalogue(doFetch, platformUrl, token) {
|
|
10556
|
+
const account = await get(
|
|
10557
|
+
doFetch,
|
|
10558
|
+
`${platformUrl}/registry/code/github/account`,
|
|
10559
|
+
token,
|
|
10560
|
+
"GitHub account read"
|
|
10561
|
+
);
|
|
10562
|
+
const active = account.installations.filter((install2) => install2.status === "active");
|
|
10563
|
+
const found = [];
|
|
10564
|
+
for (const install2 of active) {
|
|
10565
|
+
const { repositories } = await get(
|
|
10566
|
+
doFetch,
|
|
10567
|
+
`${platformUrl}/registry/code/github/installations/${install2.installationId}/repositories`,
|
|
10568
|
+
token,
|
|
10569
|
+
`repository catalog for ${install2.accountLogin}`
|
|
10570
|
+
);
|
|
10571
|
+
for (const repository of repositories) {
|
|
10572
|
+
found.push({ ...repository, installationId: install2.installationId });
|
|
10573
|
+
}
|
|
10574
|
+
}
|
|
10575
|
+
if (!active.length) {
|
|
10576
|
+
throw new Error(
|
|
10577
|
+
"no active GitHub App installation. Install odla's GitHub App on the account that owns the repository first, from Studio \u2192 Code."
|
|
10578
|
+
);
|
|
10579
|
+
}
|
|
10580
|
+
return found;
|
|
10581
|
+
}
|
|
10582
|
+
function selectRepository(found, full) {
|
|
10583
|
+
const wanted = full.toLowerCase();
|
|
10584
|
+
const matches = found.filter((repository) => repository.fullName.toLowerCase() === wanted);
|
|
10585
|
+
if (matches.length === 1) return matches[0];
|
|
10586
|
+
if (!matches.length) {
|
|
10587
|
+
const near = found.filter((repository) => repository.name.toLowerCase() === wanted.split("/").pop());
|
|
10588
|
+
throw new Error(near.length ? `no installation exposes ${full}. Did you mean ${near.map((repository) => repository.fullName).join(", ")}?` : `no installation exposes ${full}. Run "odla-ai code repository list" to see what is reachable.`);
|
|
10589
|
+
}
|
|
10590
|
+
throw new Error(
|
|
10591
|
+
`${full} is reachable through ${matches.length} installations (${matches.map((match) => match.installationId).join(", ")}); revoke the one you do not mean.`
|
|
10592
|
+
);
|
|
10593
|
+
}
|
|
10594
|
+
function repositoryUrl(cfg, suffix = "") {
|
|
10595
|
+
return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository${suffix}`;
|
|
10596
|
+
}
|
|
10597
|
+
async function codeRepositoryCommand(parsed, deps = {}) {
|
|
10598
|
+
const action2 = parsed.positionals[2];
|
|
10599
|
+
if (!isAction2(action2)) {
|
|
10600
|
+
throw new Error(
|
|
10601
|
+
`unknown code repository action "${action2 ?? ""}". Try "odla-ai code repository show --env dev".`
|
|
10602
|
+
);
|
|
10603
|
+
}
|
|
10604
|
+
assertArgs(parsed, ["config", "env", "repo", "json", "token", "email", "open"], 3);
|
|
10605
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
10606
|
+
const doFetch = deps.fetch ?? fetch;
|
|
10607
|
+
const out = deps.stdout ?? console;
|
|
10608
|
+
const env = stringOpt(parsed.options.env) ?? "dev";
|
|
10609
|
+
const json = parsed.options.json === true;
|
|
10610
|
+
const token = await getDeveloperToken(cfg, {
|
|
10611
|
+
configPath: cfg.configPath,
|
|
10612
|
+
token: stringOpt(parsed.options.token),
|
|
10613
|
+
email: stringOpt(parsed.options.email),
|
|
10614
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
10615
|
+
openApprovalUrl: deps.openUrl
|
|
10616
|
+
}, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
|
|
10617
|
+
if (action2 === "show") {
|
|
10618
|
+
const body2 = await get(
|
|
10619
|
+
doFetch,
|
|
10620
|
+
`${repositoryUrl(cfg)}?env=${encodeURIComponent(env)}`,
|
|
10621
|
+
token,
|
|
10622
|
+
"repository read"
|
|
10623
|
+
);
|
|
10624
|
+
if (json) return out.log(JSON.stringify(body2, null, 2));
|
|
10625
|
+
const bound = body2.environmentRepository;
|
|
10626
|
+
return out.log(bound ? `${cfg.app.id} (${env}) \u2192 ${bound.owner}/${bound.name}` : `${cfg.app.id} (${env}) has no connected repository. Connect one with "odla-ai code repository bind --repo owner/name --env ${env}".`);
|
|
10627
|
+
}
|
|
10628
|
+
const found = await catalogue(doFetch, cfg.platformUrl, token);
|
|
10629
|
+
if (action2 === "list") {
|
|
10630
|
+
if (json) return out.log(JSON.stringify({ repositories: found }, null, 2));
|
|
10631
|
+
if (!found.length) return out.log("no repositories reachable through your installations");
|
|
10632
|
+
out.log("REPOSITORY INSTALL DEFAULT READY");
|
|
10633
|
+
for (const repository of found) {
|
|
10634
|
+
out.log(`${repository.fullName} ${repository.installationId} ${repository.defaultBranch ?? "\u2014"} ${repository.ready ? "yes" : "empty"}`);
|
|
10635
|
+
}
|
|
10636
|
+
return;
|
|
10637
|
+
}
|
|
10638
|
+
const repo = stringOpt(parsed.options.repo);
|
|
10639
|
+
if (!repo?.includes("/")) throw new Error("code repository bind requires --repo owner/name");
|
|
10640
|
+
const selected = selectRepository(found, repo);
|
|
10641
|
+
if (!selected.ready) {
|
|
10642
|
+
throw new Error(`${selected.fullName} has no commits yet; GitHub cannot serve a default branch for an empty repository.`);
|
|
10643
|
+
}
|
|
10644
|
+
const response2 = await doFetch(repositoryUrl(cfg, "/bind"), {
|
|
10645
|
+
method: "POST",
|
|
10646
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
10647
|
+
body: JSON.stringify({ env, installationId: selected.installationId, repositoryId: selected.repositoryId })
|
|
10648
|
+
});
|
|
10649
|
+
if (!response2.ok) {
|
|
10650
|
+
throw new Error(`repository bind failed (${response2.status}): ${redactSecrets((await response2.text()).slice(0, 1e3))}`);
|
|
10651
|
+
}
|
|
10652
|
+
const body = await response2.json();
|
|
10653
|
+
if (json) return out.log(JSON.stringify(body, null, 2));
|
|
10654
|
+
out.log(`connected ${cfg.app.id} (${env}) to ${selected.fullName} @ ${selected.defaultBranch}`);
|
|
10655
|
+
out.log(`Next, so it may be worked on unattended:
|
|
10656
|
+
odla-ai code grant request --env ${env}`);
|
|
10657
|
+
}
|
|
10658
|
+
var ACTIONS2, isAction2;
|
|
10659
|
+
var init_code_repository_command = __esm({
|
|
10660
|
+
"src/code-repository-command.ts"() {
|
|
10661
|
+
"use strict";
|
|
10662
|
+
init_cjs_shims();
|
|
10663
|
+
init_argv();
|
|
10664
|
+
init_config();
|
|
10665
|
+
init_redact();
|
|
10666
|
+
init_token();
|
|
10667
|
+
ACTIONS2 = ["show", "list", "bind"];
|
|
10668
|
+
isAction2 = (value2) => ACTIONS2.includes(value2 ?? "");
|
|
10669
|
+
}
|
|
10670
|
+
});
|
|
10671
|
+
|
|
10440
10672
|
// src/code-command.ts
|
|
10441
10673
|
async function codeCommand(parsed, dependencies) {
|
|
10442
10674
|
const sub = parsed.positionals[1];
|
|
10675
|
+
if (sub === "grant") {
|
|
10676
|
+
return await codeGrantCommand(parsed, {
|
|
10677
|
+
fetch: dependencies.fetch,
|
|
10678
|
+
stdout: dependencies.stdout,
|
|
10679
|
+
openUrl: dependencies.openUrl
|
|
10680
|
+
});
|
|
10681
|
+
}
|
|
10682
|
+
if (sub === "repository") {
|
|
10683
|
+
return await codeRepositoryCommand(parsed, {
|
|
10684
|
+
fetch: dependencies.fetch,
|
|
10685
|
+
stdout: dependencies.stdout,
|
|
10686
|
+
openUrl: dependencies.openUrl
|
|
10687
|
+
});
|
|
10688
|
+
}
|
|
10443
10689
|
if (sub !== "connect") {
|
|
10444
|
-
throw new Error(
|
|
10690
|
+
throw new Error(
|
|
10691
|
+
`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev" or "odla-ai code grant list --env dev".`
|
|
10692
|
+
);
|
|
10445
10693
|
}
|
|
10446
10694
|
assertArgs(parsed, [
|
|
10447
10695
|
"config",
|
|
@@ -10484,6 +10732,8 @@ var init_code_command = __esm({
|
|
|
10484
10732
|
init_cjs_shims();
|
|
10485
10733
|
init_argv();
|
|
10486
10734
|
init_code_connect();
|
|
10735
|
+
init_code_grant_command();
|
|
10736
|
+
init_code_repository_command();
|
|
10487
10737
|
}
|
|
10488
10738
|
});
|
|
10489
10739
|
|
|
@@ -10927,6 +11177,9 @@ Commands:
|
|
|
10927
11177
|
\u2014 no secret is ever copied between people.
|
|
10928
11178
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
10929
11179
|
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
11180
|
+
"code repository show" reports the repository an app works on (Studio or a
|
|
11181
|
+
human session connects one); "code grant request|list|approve|revoke" then
|
|
11182
|
+
governs unattended access: an agent may request, only a human may approve.
|
|
10930
11183
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
10931
11184
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10932
11185
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
@@ -11027,6 +11280,17 @@ Safety:
|
|
|
11027
11280
|
approval and credential hashes live in odla-ai/db. The host
|
|
11028
11281
|
credential is never written under .odla/; it exists only in the foreground
|
|
11029
11282
|
"code connect" process and is rotated by the next approved connection.
|
|
11283
|
+
"code repository bind" takes owner/name and resolves the two GitHub integers the
|
|
11284
|
+
bind route wants across every installation you have, refusing an ambiguous match
|
|
11285
|
+
rather than choosing one \u2014 the same repository name under two organizations is
|
|
11286
|
+
ordinary, and picking the first would connect an app to source nobody chose. It
|
|
11287
|
+
needs a signed-in human session; a CLI handshake token is a delegated agent
|
|
11288
|
+
credential by design, so an agent cannot choose the source it will be given.
|
|
11289
|
+
Unattended access is per repository and revocable on its own, but the connection
|
|
11290
|
+
IS the authorization: a repository is recorded for one exact app and environment
|
|
11291
|
+
only by a signed-in human choosing it. "code grant list" shows what is authorized;
|
|
11292
|
+
"revoke" stops unattended access without disconnecting the repository, and
|
|
11293
|
+
re-binding never resurrects it. Connecting sandbox never authorizes live.
|
|
11030
11294
|
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
11031
11295
|
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
11032
11296
|
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
@@ -11428,7 +11692,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
11428
11692
|
throw new WatchRemoteError(cursor, error);
|
|
11429
11693
|
}
|
|
11430
11694
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11431
|
-
const result =
|
|
11695
|
+
const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
11432
11696
|
throw new WatchTimeoutError(result.cursor);
|
|
11433
11697
|
}
|
|
11434
11698
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -11469,7 +11733,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
11469
11733
|
});
|
|
11470
11734
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
11471
11735
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
11472
|
-
return
|
|
11736
|
+
return report2(ctx, parsed, {
|
|
11473
11737
|
found: true,
|
|
11474
11738
|
cursor,
|
|
11475
11739
|
events: matching,
|
|
@@ -11496,13 +11760,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
11496
11760
|
}
|
|
11497
11761
|
if (page2.hasMore) continue;
|
|
11498
11762
|
if (deadline !== void 0 && now() >= deadline) {
|
|
11499
|
-
return
|
|
11763
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
11500
11764
|
}
|
|
11501
11765
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
11502
11766
|
await sleep(wait2);
|
|
11503
11767
|
}
|
|
11504
11768
|
}
|
|
11505
|
-
function
|
|
11769
|
+
function report2(ctx, parsed, result) {
|
|
11506
11770
|
if (ctx.json) {
|
|
11507
11771
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
11508
11772
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -12169,7 +12433,7 @@ function eventLabel(event) {
|
|
|
12169
12433
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
12170
12434
|
return body || event.payload.entityId;
|
|
12171
12435
|
}
|
|
12172
|
-
function
|
|
12436
|
+
function report3(ctx, parsed, result) {
|
|
12173
12437
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
12174
12438
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
12175
12439
|
for (const event of result.events ?? []) {
|
|
@@ -12229,7 +12493,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
12229
12493
|
});
|
|
12230
12494
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
12231
12495
|
if (deadline !== void 0 && now() >= deadline) {
|
|
12232
|
-
return
|
|
12496
|
+
return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
12233
12497
|
}
|
|
12234
12498
|
const backoff = Math.min(
|
|
12235
12499
|
MAX_BACKOFF_MS2,
|
|
@@ -12270,7 +12534,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
12270
12534
|
cursor,
|
|
12271
12535
|
serverTime: current.serverTime
|
|
12272
12536
|
});
|
|
12273
|
-
return
|
|
12537
|
+
return report3(ctx, parsed, { found: true, cursor, events: matching });
|
|
12274
12538
|
}
|
|
12275
12539
|
if (current.events.length > 0) {
|
|
12276
12540
|
jsonl2(ctx, parsed, {
|
|
@@ -12289,7 +12553,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
12289
12553
|
}
|
|
12290
12554
|
if (current.hasMore) continue;
|
|
12291
12555
|
if (deadline !== void 0 && now() >= deadline) {
|
|
12292
|
-
return
|
|
12556
|
+
return report3(ctx, parsed, { found: false, cursor });
|
|
12293
12557
|
}
|
|
12294
12558
|
await sleep(
|
|
12295
12559
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -12894,8 +13158,8 @@ function printO11yStatus(status, out) {
|
|
|
12894
13158
|
out.log(
|
|
12895
13159
|
`cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
|
|
12896
13160
|
);
|
|
12897
|
-
for (const
|
|
12898
|
-
out.log(
|
|
13161
|
+
for (const line2 of providerCapacityLines(status.providerCapacity)) {
|
|
13162
|
+
out.log(line2);
|
|
12899
13163
|
}
|
|
12900
13164
|
const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
|
|
12901
13165
|
const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
|
|
@@ -14060,7 +14324,11 @@ var init_surface = __esm({
|
|
|
14060
14324
|
bug: { create: {}, list: {}, report: {} },
|
|
14061
14325
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
14062
14326
|
capabilities: {},
|
|
14063
|
-
code: {
|
|
14327
|
+
code: {
|
|
14328
|
+
connect: {},
|
|
14329
|
+
grant: { request: {}, list: {}, approve: {}, revoke: {} },
|
|
14330
|
+
repository: { show: {}, list: {}, bind: {} }
|
|
14331
|
+
},
|
|
14064
14332
|
config: { diff: {}, plan: {}, apply: {} },
|
|
14065
14333
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
14066
14334
|
credentials: { list: {}, revoke: {} },
|
|
@@ -14184,9 +14452,20 @@ async function bySlug(ctx, slug) {
|
|
|
14184
14452
|
"GET",
|
|
14185
14453
|
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
14186
14454
|
);
|
|
14187
|
-
const
|
|
14188
|
-
if (
|
|
14189
|
-
|
|
14455
|
+
const filtered = page2.records.find((record11) => record11.slug === slug);
|
|
14456
|
+
if (filtered) return filtered;
|
|
14457
|
+
const limit = 100;
|
|
14458
|
+
for (let offset = 0; ; offset += limit) {
|
|
14459
|
+
const fallback = await call(
|
|
14460
|
+
ctx,
|
|
14461
|
+
"GET",
|
|
14462
|
+
`/runbook?app=${encodeURIComponent(ctx.appId)}&limit=${limit}&offset=${offset}`
|
|
14463
|
+
);
|
|
14464
|
+
const found = fallback.records.find((record11) => record11.slug === slug);
|
|
14465
|
+
if (found) return found;
|
|
14466
|
+
if (!fallback.records.length || offset + fallback.records.length >= fallback.total) break;
|
|
14467
|
+
}
|
|
14468
|
+
throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
14190
14469
|
}
|
|
14191
14470
|
function readBody(file, inline) {
|
|
14192
14471
|
if (inline !== void 0) return inline;
|
|
@@ -14270,7 +14549,7 @@ async function runbookRevert(ctx, slug, version) {
|
|
|
14270
14549
|
ctx,
|
|
14271
14550
|
"POST",
|
|
14272
14551
|
`/runbook/${encodeURIComponent(runbook.id)}/revert`,
|
|
14273
|
-
{ version }
|
|
14552
|
+
{ version, expectedVersion: runbook.version }
|
|
14274
14553
|
);
|
|
14275
14554
|
if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
|
|
14276
14555
|
ctx.out.log(`${slug} reverted to v${version}, now v${result.record?.version ?? runbook.version + 1}`);
|
|
@@ -14300,8 +14579,8 @@ function parseRunbook(text3, slug) {
|
|
|
14300
14579
|
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
|
|
14301
14580
|
if (fm) {
|
|
14302
14581
|
rest = text3.slice(fm[0].length);
|
|
14303
|
-
for (const
|
|
14304
|
-
const pair = /^(\w+)\s*:\s*(.+)$/.exec(
|
|
14582
|
+
for (const line2 of fm[1].split(/\r?\n/)) {
|
|
14583
|
+
const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
|
|
14305
14584
|
if (!pair) continue;
|
|
14306
14585
|
const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
|
|
14307
14586
|
if (pair[1] === "summary") meta.summary = value2;
|
|
@@ -14422,18 +14701,18 @@ function areaOf(path) {
|
|
|
14422
14701
|
return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
|
|
14423
14702
|
}
|
|
14424
14703
|
function scanHunk(lines, into) {
|
|
14425
|
-
for (const [i,
|
|
14426
|
-
const decl = DECL.exec(
|
|
14704
|
+
for (const [i, line2] of lines.entries()) {
|
|
14705
|
+
const decl = DECL.exec(line2);
|
|
14427
14706
|
if (decl?.[1]) {
|
|
14428
14707
|
into.add(decl[1]);
|
|
14429
14708
|
continue;
|
|
14430
14709
|
}
|
|
14431
|
-
const named = NAMED.exec(
|
|
14710
|
+
const named = NAMED.exec(line2);
|
|
14432
14711
|
if (named?.[1]) {
|
|
14433
14712
|
for (const name of namedExports(named[1])) into.add(name);
|
|
14434
14713
|
continue;
|
|
14435
14714
|
}
|
|
14436
|
-
if (!JSDOC.test(
|
|
14715
|
+
if (!JSDOC.test(line2)) continue;
|
|
14437
14716
|
for (let j = i + 1; j < lines.length && j < i + 40; j++) {
|
|
14438
14717
|
const found = ANY_DECL.exec(lines[j]);
|
|
14439
14718
|
if (found?.[1]) {
|
|
@@ -14451,8 +14730,8 @@ function parseDiff(diff) {
|
|
|
14451
14730
|
if (current && hunk.length) scanHunk(hunk, current.exports);
|
|
14452
14731
|
hunk = [];
|
|
14453
14732
|
};
|
|
14454
|
-
for (const
|
|
14455
|
-
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(
|
|
14733
|
+
for (const line2 of diff.split("\n")) {
|
|
14734
|
+
const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
|
|
14456
14735
|
if (header) {
|
|
14457
14736
|
flush();
|
|
14458
14737
|
const path = header[2] ?? header[1];
|
|
@@ -14460,11 +14739,11 @@ function parseDiff(diff) {
|
|
|
14460
14739
|
files.set(path, current);
|
|
14461
14740
|
continue;
|
|
14462
14741
|
}
|
|
14463
|
-
if (
|
|
14742
|
+
if (line2.startsWith("@@")) {
|
|
14464
14743
|
flush();
|
|
14465
14744
|
continue;
|
|
14466
14745
|
}
|
|
14467
|
-
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(
|
|
14746
|
+
if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
|
|
14468
14747
|
}
|
|
14469
14748
|
flush();
|
|
14470
14749
|
return [...files.values()];
|
|
@@ -14557,7 +14836,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
14557
14836
|
return "";
|
|
14558
14837
|
}
|
|
14559
14838
|
let out = "";
|
|
14560
|
-
for (const path of listed.split("\n").map((
|
|
14839
|
+
for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
|
|
14561
14840
|
out += `diff --git a/${path} b/${path}
|
|
14562
14841
|
--- /dev/null
|
|
14563
14842
|
+++ b/${path}
|
|
@@ -14570,7 +14849,7 @@ function untrackedDiff(runGit, read3) {
|
|
|
14570
14849
|
continue;
|
|
14571
14850
|
}
|
|
14572
14851
|
out += `@@ -0,0 +1,${body.split("\n").length} @@
|
|
14573
|
-
${body.split("\n").map((
|
|
14852
|
+
${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
14574
14853
|
`;
|
|
14575
14854
|
}
|
|
14576
14855
|
return out;
|
|
@@ -14617,7 +14896,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
14617
14896
|
}
|
|
14618
14897
|
return out;
|
|
14619
14898
|
}
|
|
14620
|
-
function
|
|
14899
|
+
function report4(ctx, impacts) {
|
|
14621
14900
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
14622
14901
|
ctx.out.log(
|
|
14623
14902
|
`${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.`
|
|
@@ -14655,7 +14934,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
14655
14934
|
}
|
|
14656
14935
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
14657
14936
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
14658
|
-
|
|
14937
|
+
report4(ctx, impacts);
|
|
14659
14938
|
}
|
|
14660
14939
|
var import_node_child_process6, import_node_fs20, import_node_path18, SOURCE3, editHint;
|
|
14661
14940
|
var init_runbook_impact = __esm({
|
|
@@ -14755,7 +15034,7 @@ async function runbookSearch(ctx, query, all, limit) {
|
|
|
14755
15034
|
for (const [i, hit] of result.hits.entries()) {
|
|
14756
15035
|
if (i) ctx.out.log("");
|
|
14757
15036
|
ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
|
|
14758
|
-
for (const
|
|
15037
|
+
for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
|
|
14759
15038
|
ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
|
|
14760
15039
|
}
|
|
14761
15040
|
}
|
|
@@ -15181,31 +15460,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
15181
15460
|
url.searchParams.set("job", job.jobId);
|
|
15182
15461
|
out.log(` Studio: ${url.toString()}`);
|
|
15183
15462
|
}
|
|
15184
|
-
function printHostedReport(out,
|
|
15185
|
-
out.log(`security report ${
|
|
15186
|
-
out.log(` coverage: ${
|
|
15187
|
-
out.log(` findings: confirmed=${
|
|
15188
|
-
out.log(` discovery: ${
|
|
15189
|
-
out.log(` validation: ${
|
|
15190
|
-
for (const finding of
|
|
15463
|
+
function printHostedReport(out, report5) {
|
|
15464
|
+
out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
|
|
15465
|
+
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}`);
|
|
15466
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
|
|
15467
|
+
out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
|
|
15468
|
+
out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
|
|
15469
|
+
for (const finding of report5.findings) {
|
|
15191
15470
|
const location = finding.locations[0];
|
|
15192
15471
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
15193
15472
|
}
|
|
15194
|
-
for (const limitation of
|
|
15473
|
+
for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
|
|
15195
15474
|
}
|
|
15196
|
-
function enforceHostedReportGate(
|
|
15475
|
+
function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
|
|
15197
15476
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
15198
15477
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
15199
15478
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
15200
15479
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
15201
|
-
const confirmed =
|
|
15202
|
-
const leads = failOnCandidates ?
|
|
15203
|
-
const incomplete =
|
|
15480
|
+
const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
15481
|
+
const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
15482
|
+
const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
15204
15483
|
if (confirmed.length || leads.length || incomplete) {
|
|
15205
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
15484
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
|
|
15206
15485
|
}
|
|
15207
15486
|
if (emitSuccess) {
|
|
15208
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
15487
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
|
|
15209
15488
|
}
|
|
15210
15489
|
}
|
|
15211
15490
|
function printHostedSecurityPlanRoute(out, label, route3) {
|
|
@@ -15290,17 +15569,17 @@ async function runHostedSecurity(options) {
|
|
|
15290
15569
|
allowNetwork: false
|
|
15291
15570
|
}
|
|
15292
15571
|
});
|
|
15293
|
-
const
|
|
15294
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
15295
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
15572
|
+
const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
15573
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report5);
|
|
15574
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report5);
|
|
15296
15575
|
await hosted.complete({
|
|
15297
15576
|
reportDigest,
|
|
15298
|
-
coverageStatus:
|
|
15299
|
-
confirmed:
|
|
15300
|
-
candidates:
|
|
15577
|
+
coverageStatus: report5.coverageStatus,
|
|
15578
|
+
confirmed: report5.metrics.confirmed,
|
|
15579
|
+
candidates: report5.metrics.candidates
|
|
15301
15580
|
}, { signal: options.signal });
|
|
15302
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
15303
|
-
return Object.freeze({ report:
|
|
15581
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
|
|
15582
|
+
return Object.freeze({ report: report5, run: hosted.run, output });
|
|
15304
15583
|
}
|
|
15305
15584
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
15306
15585
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -15326,14 +15605,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
15326
15605
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
15327
15606
|
return { ...profile, maxHuntTasks };
|
|
15328
15607
|
}
|
|
15329
|
-
function printSummary(out, appId, env, run,
|
|
15330
|
-
const complete =
|
|
15608
|
+
function printSummary(out, appId, env, run, report5, output) {
|
|
15609
|
+
const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
|
|
15331
15610
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
15332
15611
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
15333
15612
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
15334
|
-
out.log(` coverage: ${
|
|
15335
|
-
if (
|
|
15336
|
-
out.log(` findings: confirmed=${
|
|
15613
|
+
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}`);
|
|
15614
|
+
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
15615
|
+
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
15337
15616
|
out.log(` report: ${(0, import_node_path20.resolve)(output, "REPORT.md")}`);
|
|
15338
15617
|
}
|
|
15339
15618
|
function formatBudget(usage) {
|
|
@@ -15605,13 +15884,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
15605
15884
|
}
|
|
15606
15885
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
15607
15886
|
}
|
|
15608
|
-
const
|
|
15887
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
15609
15888
|
if (parsed.options.json === true) {
|
|
15610
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
15889
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
|
|
15611
15890
|
} else {
|
|
15612
|
-
printHostedReport(context.stdout,
|
|
15891
|
+
printHostedReport(context.stdout, report5);
|
|
15613
15892
|
}
|
|
15614
|
-
enforceHostedReportGate(
|
|
15893
|
+
enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
|
|
15615
15894
|
}
|
|
15616
15895
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
15617
15896
|
if (parsed.options.source === true) {
|
|
@@ -15679,13 +15958,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
15679
15958
|
});
|
|
15680
15959
|
enforceLocalGate(result.report, parsed);
|
|
15681
15960
|
}
|
|
15682
|
-
function enforceLocalGate(
|
|
15961
|
+
function enforceLocalGate(report5, parsed) {
|
|
15683
15962
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
15684
15963
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
15685
15964
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
15686
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
15687
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
15688
|
-
const incomplete =
|
|
15965
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
|
|
15966
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
15967
|
+
const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
15689
15968
|
if (confirmed.length || leads.length || incomplete) {
|
|
15690
15969
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
15691
15970
|
}
|
|
@@ -15740,9 +16019,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
15740
16019
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
15741
16020
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
15742
16021
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
15743
|
-
const
|
|
15744
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
15745
|
-
else printHostedReport(context.stdout,
|
|
16022
|
+
const report5 = await getHostedSecurityReport({ ...context, jobId });
|
|
16023
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
|
|
16024
|
+
else printHostedReport(context.stdout, report5);
|
|
15746
16025
|
return;
|
|
15747
16026
|
}
|
|
15748
16027
|
if (sub !== "run") {
|