@rtrentjones/greenlight 0.2.21 → 0.2.23
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/{agent-web-I4LXW4SR.js → agent-web-3FTO2TLJ.js} +1 -1
- package/dist/bin.js +159 -27
- package/dist/{chunk-UXHHLEYO.js → chunk-KVOI4UL2.js} +5 -1
- package/dist/{chunk-GO2RVNOP.js → chunk-TFWXR7PP.js} +63 -26
- package/dist/{chunk-6N7MD6FR.js → chunk-XWTOJHLV.js} +5 -1
- package/dist/{eval-LLQPOEQX.js → eval-44S2BATV.js} +1 -1
- package/dist/index.js +3 -3
- package/package.json +5 -5
package/dist/bin.js
CHANGED
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
resolveUrl,
|
|
7
7
|
verifyAll
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-TFWXR7PP.js";
|
|
9
9
|
import "./chunk-HX7VA25D.js";
|
|
10
10
|
import "./chunk-N3IKUCSF.js";
|
|
11
11
|
import "./chunk-KP3Y6WRU.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-KVOI4UL2.js";
|
|
13
|
+
import "./chunk-XWTOJHLV.js";
|
|
14
14
|
import "./chunk-QFKE5JKC.js";
|
|
15
15
|
|
|
16
16
|
// src/commands/add.ts
|
|
@@ -443,7 +443,7 @@ function tokensForTool(tool) {
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
// src/version.ts
|
|
446
|
-
var MODULE_REF = "v0.2.
|
|
446
|
+
var MODULE_REF = "v0.2.23";
|
|
447
447
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
448
448
|
function moduleSource(module, ref = MODULE_REF) {
|
|
449
449
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -1991,7 +1991,8 @@ async function deployCommand(args) {
|
|
|
1991
1991
|
}
|
|
1992
1992
|
|
|
1993
1993
|
// src/commands/doctor.ts
|
|
1994
|
-
import {
|
|
1994
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1995
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
1995
1996
|
import { join as join4 } from "path";
|
|
1996
1997
|
function dirCheck(label, dir) {
|
|
1997
1998
|
return existsSync7(dir) ? { name: `${label}: directory`, status: "ok" } : { name: `${label}: directory`, status: "fail", detail: `missing ${dir}` };
|
|
@@ -2030,6 +2031,61 @@ function conformanceChecks(t, root) {
|
|
|
2030
2031
|
}
|
|
2031
2032
|
return out;
|
|
2032
2033
|
}
|
|
2034
|
+
function versionDriftCheck(root) {
|
|
2035
|
+
const name = "framework version drift";
|
|
2036
|
+
let installed;
|
|
2037
|
+
try {
|
|
2038
|
+
const pkg = JSON.parse(
|
|
2039
|
+
readFileSync5(join4(root, "node_modules/@rtrentjones/greenlight/package.json"), "utf8")
|
|
2040
|
+
);
|
|
2041
|
+
installed = pkg.version;
|
|
2042
|
+
} catch {
|
|
2043
|
+
}
|
|
2044
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2045
|
+
try {
|
|
2046
|
+
for (const f of readdirSync2(join4(root, "infra")).filter((f2) => f2.endsWith(".tf"))) {
|
|
2047
|
+
const body = readFileSync5(join4(root, "infra", f), "utf8");
|
|
2048
|
+
for (const m of body.matchAll(/greenlight\.git\/\/infra\/modules\/[^?"]+\?ref=(v[0-9.]+)/g)) {
|
|
2049
|
+
if (m[1]) refs.add(m[1]);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
} catch {
|
|
2053
|
+
}
|
|
2054
|
+
if (!installed && refs.size === 0) {
|
|
2055
|
+
return {
|
|
2056
|
+
name,
|
|
2057
|
+
status: "skip",
|
|
2058
|
+
detail: "no installed @rtrentjones/greenlight or infra pins here"
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
const refList = [...refs];
|
|
2062
|
+
if (installed) {
|
|
2063
|
+
const want = `v${installed}`;
|
|
2064
|
+
const bad = refList.filter((r) => r !== want);
|
|
2065
|
+
return bad.length === 0 ? { name, status: "ok", detail: `infra pins == installed ${want}` } : {
|
|
2066
|
+
name,
|
|
2067
|
+
status: "warn",
|
|
2068
|
+
detail: `installed ${want}, but infra pins ${bad.join(", ")} \u2014 bump ?ref to ${want}`
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
return refList.length <= 1 ? { name, status: "ok", detail: `infra pins uniform (${refList[0] ?? "none"})` } : { name, status: "warn", detail: `infra ?ref pins not uniform: ${refList.join(", ")}` };
|
|
2072
|
+
}
|
|
2073
|
+
function submoduleDriftCheck(root) {
|
|
2074
|
+
const name = "submodule drift";
|
|
2075
|
+
let out;
|
|
2076
|
+
try {
|
|
2077
|
+
out = execFileSync4("git", ["submodule", "status"], {
|
|
2078
|
+
cwd: root,
|
|
2079
|
+
encoding: "utf8",
|
|
2080
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2081
|
+
}).trim();
|
|
2082
|
+
} catch {
|
|
2083
|
+
return { name, status: "skip", detail: "no git / not a repo" };
|
|
2084
|
+
}
|
|
2085
|
+
if (!out) return { name, status: "skip", detail: "no submodules" };
|
|
2086
|
+
const dirty = out.split("\n").filter((l) => /^[+\-U]/.test(l));
|
|
2087
|
+
return dirty.length === 0 ? { name, status: "ok", detail: "all submodules match their recorded commit" } : { name, status: "warn", detail: dirty.map((l) => l.trim()).join("; ") };
|
|
2088
|
+
}
|
|
2033
2089
|
function runDoctor(config, root) {
|
|
2034
2090
|
const checks = [];
|
|
2035
2091
|
if (config.blog) checks.push(dirCheck("blog", join4(root, "apps/blog")));
|
|
@@ -2042,6 +2098,15 @@ function runDoctor(config, root) {
|
|
|
2042
2098
|
mcp: t.lane === "mcp"
|
|
2043
2099
|
});
|
|
2044
2100
|
checks.push({ name: `${t.name}: external (registry)`, status: "ok", detail: url });
|
|
2101
|
+
if (t.dir) {
|
|
2102
|
+
checks.push(
|
|
2103
|
+
existsSync7(join4(root, t.dir)) ? { name: `${t.name}: dir present`, status: "ok", detail: t.dir } : {
|
|
2104
|
+
name: `${t.name}: dir present`,
|
|
2105
|
+
status: "warn",
|
|
2106
|
+
detail: `declared dir "${t.dir}" missing \u2014 run \`git submodule update --init\``
|
|
2107
|
+
}
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2045
2110
|
} else {
|
|
2046
2111
|
checks.push(dirCheck(t.name, join4(root, t.dir ?? join4("tools", t.name))));
|
|
2047
2112
|
}
|
|
@@ -2053,13 +2118,14 @@ function runDoctor(config, root) {
|
|
|
2053
2118
|
status: needsKeepalive.length > 0 ? "ok" : "skip",
|
|
2054
2119
|
detail: needsKeepalive.length > 0 ? needsKeepalive.map((t) => `${t.name} (${t.data === "supabase" ? "supabase" : "oci"})`).join(", ") : "no data:supabase / target:oci tools"
|
|
2055
2120
|
});
|
|
2121
|
+
checks.push(versionDriftCheck(root));
|
|
2122
|
+
checks.push(submoduleDriftCheck(root));
|
|
2056
2123
|
for (const name of [
|
|
2057
2124
|
"DNS propagation",
|
|
2058
2125
|
"terraform drift",
|
|
2059
2126
|
"Vercel cap headroom",
|
|
2060
2127
|
"keepalive health (live)",
|
|
2061
|
-
"OCI PAYG status"
|
|
2062
|
-
"framework version drift"
|
|
2128
|
+
"OCI PAYG status"
|
|
2063
2129
|
]) {
|
|
2064
2130
|
checks.push({ name, status: "skip", detail: "needs provider creds / packages (Phase 5/7/8)" });
|
|
2065
2131
|
}
|
|
@@ -2091,7 +2157,7 @@ import { resolve as resolve8 } from "path";
|
|
|
2091
2157
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
2092
2158
|
|
|
2093
2159
|
// src/tokens.ts
|
|
2094
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as
|
|
2160
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
2095
2161
|
import { resolve as resolve7 } from "path";
|
|
2096
2162
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2097
2163
|
var SECRETS_DIR = ".greenlight";
|
|
@@ -2100,7 +2166,7 @@ function presentEnv(cwd) {
|
|
|
2100
2166
|
const out = {};
|
|
2101
2167
|
const p = resolve7(cwd, SECRETS_DIR, SECRETS_FILE);
|
|
2102
2168
|
if (existsSync8(p)) {
|
|
2103
|
-
for (const { key, value } of parseSecretsEnv(
|
|
2169
|
+
for (const { key, value } of parseSecretsEnv(readFileSync6(p, "utf8"))) out[key] = value;
|
|
2104
2170
|
}
|
|
2105
2171
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2106
2172
|
if (v !== void 0 && !(k in out)) out[k] = v;
|
|
@@ -2111,7 +2177,7 @@ function upsertSecret(cwd, key, value) {
|
|
|
2111
2177
|
const dir = resolve7(cwd, SECRETS_DIR);
|
|
2112
2178
|
mkdirSync4(dir, { recursive: true });
|
|
2113
2179
|
const p = resolve7(dir, SECRETS_FILE);
|
|
2114
|
-
const lines = existsSync8(p) ?
|
|
2180
|
+
const lines = existsSync8(p) ? readFileSync6(p, "utf8").split("\n") : [];
|
|
2115
2181
|
const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
|
|
2116
2182
|
if (idx >= 0) lines[idx] = `${key}=${value}`;
|
|
2117
2183
|
else {
|
|
@@ -2338,7 +2404,7 @@ Next:
|
|
|
2338
2404
|
}
|
|
2339
2405
|
|
|
2340
2406
|
// src/commands/preview.ts
|
|
2341
|
-
import { execFileSync as
|
|
2407
|
+
import { execFileSync as execFileSync5, spawn } from "child_process";
|
|
2342
2408
|
import { resolve as resolve10 } from "path";
|
|
2343
2409
|
import { setTimeout as sleep } from "timers/promises";
|
|
2344
2410
|
|
|
@@ -2377,6 +2443,15 @@ ${report.logs}
|
|
|
2377
2443
|
}
|
|
2378
2444
|
}
|
|
2379
2445
|
var LOG_TAIL_LINES = 50;
|
|
2446
|
+
function redactSecrets(text, env = process.env) {
|
|
2447
|
+
let out = text;
|
|
2448
|
+
for (const [k, v] of Object.entries(env)) {
|
|
2449
|
+
if (!v || v.length < 6) continue;
|
|
2450
|
+
if (!/TOKEN|KEY|SECRET|PASSWORD|PWD/i.test(k)) continue;
|
|
2451
|
+
out = out.split(v).join("***");
|
|
2452
|
+
}
|
|
2453
|
+
return out;
|
|
2454
|
+
}
|
|
2380
2455
|
function attachFailureLogs(reports, specs, toolDir) {
|
|
2381
2456
|
reports.forEach((report, i) => {
|
|
2382
2457
|
if (report.pass) return;
|
|
@@ -2395,7 +2470,7 @@ function attachFailureLogs(reports, specs, toolDir) {
|
|
|
2395
2470
|
// Let the command target the exact failing URL without hard-coding it.
|
|
2396
2471
|
env: { ...process.env, GREENLIGHT_VERIFY_URL: report.url }
|
|
2397
2472
|
});
|
|
2398
|
-
const out = `${res.stdout ?? ""}${res.stderr ?? ""}`.trimEnd();
|
|
2473
|
+
const out = redactSecrets(`${res.stdout ?? ""}${res.stderr ?? ""}`.trimEnd());
|
|
2399
2474
|
const tail = out.split("\n").slice(-LOG_TAIL_LINES).join("\n");
|
|
2400
2475
|
report.logs = tail || `(logsOnFailure produced no output${res.error ? `: ${res.error.message}` : ""})`;
|
|
2401
2476
|
} catch (e) {
|
|
@@ -2528,7 +2603,7 @@ async function previewViaDescriptor(entry, name, portOverride) {
|
|
|
2528
2603
|
} finally {
|
|
2529
2604
|
if (pv.teardown) {
|
|
2530
2605
|
try {
|
|
2531
|
-
|
|
2606
|
+
execFileSync5(pv.teardown, { cwd: toolDir, shell: true, stdio: "inherit" });
|
|
2532
2607
|
} catch {
|
|
2533
2608
|
}
|
|
2534
2609
|
}
|
|
@@ -2545,7 +2620,7 @@ async function previewViaBuiltIn(entry, name, portOverride) {
|
|
|
2545
2620
|
const plan = servePlan(entry.lane, portOverride);
|
|
2546
2621
|
if (plan.build) {
|
|
2547
2622
|
console.log(`build ${name} (${entry.dir})`);
|
|
2548
|
-
|
|
2623
|
+
execFileSync5("pnpm", ["-C", entry.dir, "run", "build"], { stdio: "inherit" });
|
|
2549
2624
|
}
|
|
2550
2625
|
console.log(`serve ${name} on :${plan.port}`);
|
|
2551
2626
|
const runArgs = ["-C", entry.dir, "run", plan.script];
|
|
@@ -2600,15 +2675,35 @@ async function previewCommand(args) {
|
|
|
2600
2675
|
}
|
|
2601
2676
|
|
|
2602
2677
|
// ../packages/loop/src/promote.ts
|
|
2603
|
-
import { execFileSync as
|
|
2678
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2604
2679
|
function git(repoDir, args) {
|
|
2605
|
-
|
|
2680
|
+
execFileSync6("git", args, { cwd: repoDir, stdio: "ignore" });
|
|
2606
2681
|
}
|
|
2607
2682
|
function gitOut(repoDir, args) {
|
|
2608
|
-
return
|
|
2683
|
+
return execFileSync6("git", args, { cwd: repoDir, encoding: "utf8" }).trim();
|
|
2684
|
+
}
|
|
2685
|
+
function tryRev(repoDir, ref) {
|
|
2686
|
+
try {
|
|
2687
|
+
return gitOut(repoDir, ["rev-parse", "--verify", "--quiet", ref]);
|
|
2688
|
+
} catch {
|
|
2689
|
+
return null;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
function fetchRefs(repoDir, branches) {
|
|
2693
|
+
try {
|
|
2694
|
+
git(repoDir, ["remote", "get-url", "origin"]);
|
|
2695
|
+
} catch {
|
|
2696
|
+
return { ok: false, hasOrigin: false };
|
|
2697
|
+
}
|
|
2698
|
+
try {
|
|
2699
|
+
git(repoDir, ["fetch", "--no-tags", "origin", ...branches]);
|
|
2700
|
+
return { ok: true, hasOrigin: true };
|
|
2701
|
+
} catch {
|
|
2702
|
+
return { ok: false, hasOrigin: true };
|
|
2703
|
+
}
|
|
2609
2704
|
}
|
|
2610
2705
|
function resolveRef(repoDir, branch) {
|
|
2611
|
-
for (const ref of [
|
|
2706
|
+
for (const ref of [`origin/${branch}`, branch]) {
|
|
2612
2707
|
try {
|
|
2613
2708
|
git(repoDir, ["rev-parse", "--verify", "--quiet", ref]);
|
|
2614
2709
|
return ref;
|
|
@@ -2617,19 +2712,45 @@ function resolveRef(repoDir, branch) {
|
|
|
2617
2712
|
}
|
|
2618
2713
|
return null;
|
|
2619
2714
|
}
|
|
2715
|
+
function staleLocalWarnings(repoDir, branches) {
|
|
2716
|
+
const warnings = [];
|
|
2717
|
+
for (const branch of branches) {
|
|
2718
|
+
const local = tryRev(repoDir, branch);
|
|
2719
|
+
const origin = tryRev(repoDir, `origin/${branch}`);
|
|
2720
|
+
if (local && origin && local !== origin) {
|
|
2721
|
+
warnings.push(
|
|
2722
|
+
`local "${branch}" (${local.slice(0, 7)}) differs from origin/${branch} (${origin.slice(0, 7)}) \u2014 promoting the origin (verified) state. Sync with \`git fetch && git branch -f ${branch} origin/${branch}\`.`
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
return warnings;
|
|
2727
|
+
}
|
|
2620
2728
|
function canPromote(repoDir, from = "develop", to = "main") {
|
|
2729
|
+
const warnings = [];
|
|
2730
|
+
const fetched = fetchRefs(repoDir, [from, to]);
|
|
2731
|
+
if (fetched.hasOrigin && !fetched.ok) {
|
|
2732
|
+
warnings.push(
|
|
2733
|
+
"could not `git fetch origin` \u2014 eligibility may be based on stale remote-tracking refs (offline / auth?). Re-run after a successful fetch."
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2621
2736
|
const fromRef = resolveRef(repoDir, from);
|
|
2622
2737
|
const toRef = resolveRef(repoDir, to);
|
|
2623
2738
|
if (!fromRef || !toRef) {
|
|
2624
|
-
return {
|
|
2739
|
+
return {
|
|
2740
|
+
canPromote: false,
|
|
2741
|
+
reason: `branch "${from}" or "${to}" not found in ${repoDir}`,
|
|
2742
|
+
warnings: warnings.length ? warnings : void 0
|
|
2743
|
+
};
|
|
2625
2744
|
}
|
|
2745
|
+
warnings.push(...staleLocalWarnings(repoDir, [from, to]));
|
|
2626
2746
|
try {
|
|
2627
2747
|
git(repoDir, ["merge-base", "--is-ancestor", toRef, fromRef]);
|
|
2628
|
-
return { canPromote: true, reason: `"${to}" can fast-forward to "${from}"
|
|
2748
|
+
return { canPromote: true, reason: `"${to}" can fast-forward to "${from}"`, warnings };
|
|
2629
2749
|
} catch {
|
|
2630
2750
|
return {
|
|
2631
2751
|
canPromote: false,
|
|
2632
|
-
reason: `"${to}" has diverged from "${from}" \u2014 fast-forward refused. Reconcile first (rebase "${from}" onto "${to}", or merge "${to}" into "${from}") before promoting
|
|
2752
|
+
reason: `"${to}" has diverged from "${from}" \u2014 fast-forward refused. Reconcile first (rebase "${from}" onto "${to}", or merge "${to}" into "${from}") before promoting.`,
|
|
2753
|
+
warnings
|
|
2633
2754
|
};
|
|
2634
2755
|
}
|
|
2635
2756
|
}
|
|
@@ -2637,7 +2758,10 @@ function promote(repoDir, opts = {}) {
|
|
|
2637
2758
|
const from = opts.from ?? "develop";
|
|
2638
2759
|
const to = opts.to ?? "main";
|
|
2639
2760
|
const check = canPromote(repoDir, from, to);
|
|
2640
|
-
if (!check.canPromote)
|
|
2761
|
+
if (!check.canPromote) {
|
|
2762
|
+
return { promoted: false, from, to, reason: check.reason, warnings: check.warnings };
|
|
2763
|
+
}
|
|
2764
|
+
const warnings = check.warnings;
|
|
2641
2765
|
const fromRef = resolveRef(repoDir, from);
|
|
2642
2766
|
const fromCommit = gitOut(repoDir, ["rev-parse", fromRef]);
|
|
2643
2767
|
const current = gitOut(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -2649,14 +2773,20 @@ function promote(repoDir, opts = {}) {
|
|
|
2649
2773
|
} catch {
|
|
2650
2774
|
}
|
|
2651
2775
|
}
|
|
2652
|
-
return {
|
|
2776
|
+
return {
|
|
2777
|
+
promoted: true,
|
|
2778
|
+
from,
|
|
2779
|
+
to,
|
|
2780
|
+
reason: `"${to}" fast-forwarded to "${from}" and pushed`,
|
|
2781
|
+
warnings
|
|
2782
|
+
};
|
|
2653
2783
|
}
|
|
2654
2784
|
if (current === to) {
|
|
2655
2785
|
git(repoDir, ["merge", "--ff-only", fromRef]);
|
|
2656
2786
|
} else {
|
|
2657
2787
|
git(repoDir, ["update-ref", `refs/heads/${to}`, fromCommit]);
|
|
2658
2788
|
}
|
|
2659
|
-
return { promoted: true, from, to, reason: `"${to}" fast-forwarded to "${from}"
|
|
2789
|
+
return { promoted: true, from, to, reason: `"${to}" fast-forwarded to "${from}"`, warnings };
|
|
2660
2790
|
}
|
|
2661
2791
|
|
|
2662
2792
|
// src/commands/promote.ts
|
|
@@ -2666,20 +2796,22 @@ async function promoteCommand(args) {
|
|
|
2666
2796
|
const cwd = process.cwd();
|
|
2667
2797
|
if (!perform) {
|
|
2668
2798
|
const check = canPromote(cwd);
|
|
2799
|
+
for (const w of check.warnings ?? []) console.warn(`\u26A0 ${w}`);
|
|
2669
2800
|
console.log(`${check.canPromote ? "\u2714" : "\u2718"} ${check.reason}`);
|
|
2670
2801
|
if (check.canPromote) console.log("\nEligible. Re-run with --perform (and --push) to promote.");
|
|
2671
2802
|
process.exit(check.canPromote ? 0 : 1);
|
|
2672
2803
|
}
|
|
2673
2804
|
const result = promote(cwd, { push });
|
|
2805
|
+
for (const w of result.warnings ?? []) console.warn(`\u26A0 ${w}`);
|
|
2674
2806
|
console.log(`${result.promoted ? "\u2714" : "\u2718"} ${result.reason}`);
|
|
2675
2807
|
process.exit(result.promoted ? 0 : 1);
|
|
2676
2808
|
}
|
|
2677
2809
|
|
|
2678
2810
|
// src/commands/status.ts
|
|
2679
|
-
import { execFileSync as
|
|
2811
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
2680
2812
|
function repoSlug(dir) {
|
|
2681
2813
|
try {
|
|
2682
|
-
const url =
|
|
2814
|
+
const url = execFileSync7("git", ["-C", dir, "remote", "get-url", "origin"], {
|
|
2683
2815
|
encoding: "utf8"
|
|
2684
2816
|
}).trim();
|
|
2685
2817
|
const m = url.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
@@ -2712,7 +2844,7 @@ function workflowsFor(entry, name, wrapper, toolRepo) {
|
|
|
2712
2844
|
}
|
|
2713
2845
|
function lastRun(repo, workflow) {
|
|
2714
2846
|
try {
|
|
2715
|
-
const out =
|
|
2847
|
+
const out = execFileSync7(
|
|
2716
2848
|
"gh",
|
|
2717
2849
|
[
|
|
2718
2850
|
"run",
|
|
@@ -195,7 +195,11 @@ async function verifyAgentWeb(baseUrl, spec) {
|
|
|
195
195
|
{ name: "@anthropic-ai/sdk available", pass: false, detail: "pnpm add @anthropic-ai/sdk" }
|
|
196
196
|
]);
|
|
197
197
|
}
|
|
198
|
-
const client = new Anthropic({
|
|
198
|
+
const client = new Anthropic({
|
|
199
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
200
|
+
timeout: 6e4,
|
|
201
|
+
maxRetries: 1
|
|
202
|
+
});
|
|
199
203
|
let browser;
|
|
200
204
|
try {
|
|
201
205
|
browser = await chromium.launch({ headless: !spec.headed });
|
|
@@ -136,10 +136,15 @@ import { setTimeout as sleep } from "timers/promises";
|
|
|
136
136
|
|
|
137
137
|
// ../packages/verify/src/api.ts
|
|
138
138
|
var trimSlash = (s) => s.replace(/\/+$/, "");
|
|
139
|
-
|
|
139
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
140
|
+
var DEFAULT_MAX_LINKS = 50;
|
|
141
|
+
function timedFetch(url, timeoutMs, init) {
|
|
142
|
+
return fetch(url, { redirect: "manual", ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
143
|
+
}
|
|
144
|
+
async function checkRoute(base, c, timeoutMs) {
|
|
140
145
|
const name = `GET ${c.path}`;
|
|
141
146
|
try {
|
|
142
|
-
const res = await
|
|
147
|
+
const res = await timedFetch(base + c.path, timeoutMs, { headers: c.requestHeaders });
|
|
143
148
|
const reasons = [];
|
|
144
149
|
if (c.status !== void 0 && res.status !== c.status) {
|
|
145
150
|
reasons.push(`status ${res.status} != ${c.status}`);
|
|
@@ -160,10 +165,10 @@ async function checkRoute(base, c) {
|
|
|
160
165
|
return { name, pass: false, detail: msg(e) };
|
|
161
166
|
}
|
|
162
167
|
}
|
|
163
|
-
async function checkXml(base, candidates, label, marker) {
|
|
168
|
+
async function checkXml(base, candidates, label, marker, timeoutMs) {
|
|
164
169
|
for (const path of candidates) {
|
|
165
170
|
try {
|
|
166
|
-
const res = await
|
|
171
|
+
const res = await timedFetch(base + path, timeoutMs);
|
|
167
172
|
if (res.status === 200) {
|
|
168
173
|
const body = await res.text();
|
|
169
174
|
const ok = marker.test(body);
|
|
@@ -178,65 +183,97 @@ async function checkXml(base, candidates, label, marker) {
|
|
|
178
183
|
}
|
|
179
184
|
return { name: label, pass: false, detail: `none of ${candidates.join(", ")} returned 200` };
|
|
180
185
|
}
|
|
181
|
-
async function checkInternalLinks(base, max =
|
|
186
|
+
async function checkInternalLinks(base, timeoutMs, max = DEFAULT_MAX_LINKS) {
|
|
182
187
|
try {
|
|
183
|
-
const res = await
|
|
188
|
+
const res = await timedFetch(`${base}/`, timeoutMs);
|
|
184
189
|
const html = await res.text();
|
|
185
190
|
const hrefs = /* @__PURE__ */ new Set();
|
|
191
|
+
let capped = false;
|
|
186
192
|
for (const m of html.matchAll(/href="(\/[^"#?]*)"/g)) {
|
|
187
193
|
const href = m[1];
|
|
188
194
|
if (href && !href.startsWith("//")) hrefs.add(href);
|
|
189
|
-
if (hrefs.size >= max)
|
|
195
|
+
if (hrefs.size >= max) {
|
|
196
|
+
capped = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (hrefs.size === 0) {
|
|
201
|
+
return {
|
|
202
|
+
name: "no broken internal links",
|
|
203
|
+
pass: false,
|
|
204
|
+
detail: `no internal links found on ${base}/ (status ${res.status}) \u2014 page empty or unparseable`
|
|
205
|
+
};
|
|
190
206
|
}
|
|
191
207
|
const broken = [];
|
|
192
208
|
for (const href of hrefs) {
|
|
193
209
|
try {
|
|
194
|
-
const r = await
|
|
210
|
+
const r = await timedFetch(base + href, timeoutMs);
|
|
195
211
|
if (r.status >= 400) broken.push(`${href} (${r.status})`);
|
|
196
212
|
} catch {
|
|
197
213
|
broken.push(`${href} (unreachable)`);
|
|
198
214
|
}
|
|
199
215
|
}
|
|
216
|
+
const capNote = capped ? `; capped at first ${max} \u2014 raise maxLinks to check more` : "";
|
|
200
217
|
return {
|
|
201
|
-
name: `no broken internal links (${hrefs.size} checked)`,
|
|
218
|
+
name: `no broken internal links (${hrefs.size} checked${capped ? `, capped at ${max}` : ""})`,
|
|
202
219
|
pass: broken.length === 0,
|
|
203
|
-
detail: broken.length ? `broken: ${broken.join(", ")}` : void 0
|
|
220
|
+
detail: broken.length ? `broken: ${broken.join(", ")}${capNote}` : capNote || void 0
|
|
204
221
|
};
|
|
205
222
|
} catch (e) {
|
|
206
223
|
return { name: "no broken internal links", pass: false, detail: msg(e) };
|
|
207
224
|
}
|
|
208
225
|
}
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
226
|
+
function buildTasks(base, spec) {
|
|
227
|
+
const timeoutMs = spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
228
|
+
const tasks = [];
|
|
229
|
+
for (const c of spec.checks ?? []) tasks.push(() => checkRoute(base, c, timeoutMs));
|
|
212
230
|
if (spec.rssValid) {
|
|
213
|
-
|
|
214
|
-
|
|
231
|
+
tasks.push(
|
|
232
|
+
() => checkXml(
|
|
233
|
+
base,
|
|
234
|
+
["/rss.xml", "/feed.xml", "/index.xml"],
|
|
235
|
+
"rss",
|
|
236
|
+
/<(rss|feed)[\s>]/i,
|
|
237
|
+
timeoutMs
|
|
238
|
+
)
|
|
215
239
|
);
|
|
216
240
|
}
|
|
217
241
|
if (spec.sitemapValid) {
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
tasks.push(
|
|
243
|
+
() => checkXml(
|
|
220
244
|
base,
|
|
221
245
|
["/sitemap.xml", "/sitemap-index.xml"],
|
|
222
246
|
"sitemap",
|
|
223
|
-
/<(urlset|sitemapindex)[\s>]/i
|
|
247
|
+
/<(urlset|sitemapindex)[\s>]/i,
|
|
248
|
+
timeoutMs
|
|
224
249
|
)
|
|
225
250
|
);
|
|
226
251
|
}
|
|
227
|
-
if (spec.noBrokenInternalLinks)
|
|
228
|
-
|
|
252
|
+
if (spec.noBrokenInternalLinks) {
|
|
253
|
+
tasks.push(() => checkInternalLinks(base, timeoutMs, spec.maxLinks));
|
|
254
|
+
}
|
|
255
|
+
return tasks;
|
|
229
256
|
}
|
|
230
257
|
async function verifyApi(baseUrl, spec) {
|
|
231
258
|
const base = trimSlash(baseUrl);
|
|
232
259
|
const retries = Math.max(0, spec.settleRetries ?? 0);
|
|
233
260
|
const delayMs = spec.settleMs ?? 5e3;
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
const state = await Promise.all(
|
|
262
|
+
buildTasks(base, spec).map(async (task) => ({ task, check: await task() }))
|
|
263
|
+
);
|
|
264
|
+
for (let i = 0; i < retries && !state.every((s) => s.check.pass); i++) {
|
|
236
265
|
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
237
|
-
|
|
266
|
+
await Promise.all(
|
|
267
|
+
state.filter((s) => !s.check.pass).map(async (s) => {
|
|
268
|
+
s.check = await s.task();
|
|
269
|
+
})
|
|
270
|
+
);
|
|
238
271
|
}
|
|
239
|
-
return report(
|
|
272
|
+
return report(
|
|
273
|
+
"api",
|
|
274
|
+
baseUrl,
|
|
275
|
+
state.map((s) => s.check)
|
|
276
|
+
);
|
|
240
277
|
}
|
|
241
278
|
|
|
242
279
|
// ../packages/verify/src/index.ts
|
|
@@ -274,11 +311,11 @@ async function verify(baseUrl, spec, opts) {
|
|
|
274
311
|
return verifyTest2(spec, opts?.toolDir ?? process.cwd());
|
|
275
312
|
}
|
|
276
313
|
case "agent-web": {
|
|
277
|
-
const { verifyAgentWeb: verifyAgentWeb2 } = await import("./agent-web-
|
|
314
|
+
const { verifyAgentWeb: verifyAgentWeb2 } = await import("./agent-web-3FTO2TLJ.js");
|
|
278
315
|
return verifyAgentWeb2(baseUrl, spec);
|
|
279
316
|
}
|
|
280
317
|
case "eval": {
|
|
281
|
-
const { verifyEval: verifyEval2 } = await import("./eval-
|
|
318
|
+
const { verifyEval: verifyEval2 } = await import("./eval-44S2BATV.js");
|
|
282
319
|
return verifyEval2(baseUrl, spec);
|
|
283
320
|
}
|
|
284
321
|
}
|
|
@@ -19,7 +19,11 @@ function llmJudge(model) {
|
|
|
19
19
|
if (!process.env.ANTHROPIC_API_KEY) throw new Error("ANTHROPIC_API_KEY not set");
|
|
20
20
|
const sdkName = "@anthropic-ai/sdk";
|
|
21
21
|
const Anthropic = (await import(sdkName)).default;
|
|
22
|
-
const client = new Anthropic({
|
|
22
|
+
const client = new Anthropic({
|
|
23
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
24
|
+
timeout: 6e4,
|
|
25
|
+
maxRetries: 1
|
|
26
|
+
});
|
|
23
27
|
const resp = await client.messages.create({
|
|
24
28
|
model,
|
|
25
29
|
max_tokens: 512,
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,12 @@ import {
|
|
|
2
2
|
defineConfig,
|
|
3
3
|
defineVerify,
|
|
4
4
|
loadConfig
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-TFWXR7PP.js";
|
|
6
6
|
import "./chunk-HX7VA25D.js";
|
|
7
7
|
import "./chunk-N3IKUCSF.js";
|
|
8
8
|
import "./chunk-KP3Y6WRU.js";
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-KVOI4UL2.js";
|
|
10
|
+
import "./chunk-XWTOJHLV.js";
|
|
11
11
|
import "./chunk-QFKE5JKC.js";
|
|
12
12
|
export {
|
|
13
13
|
defineConfig,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtrentjones/greenlight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "Greenlight CLI — setup and lifecycle for the harness.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"@anthropic-ai/sdk": "^0.69.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@rtrentjones/greenlight-
|
|
35
|
-
"@rtrentjones/greenlight-shared": "0.2.
|
|
36
|
-
"@rtrentjones/greenlight-
|
|
37
|
-
"@rtrentjones/greenlight-
|
|
34
|
+
"@rtrentjones/greenlight-loop": "0.2.23",
|
|
35
|
+
"@rtrentjones/greenlight-shared": "0.2.23",
|
|
36
|
+
"@rtrentjones/greenlight-adapters": "0.2.23",
|
|
37
|
+
"@rtrentjones/greenlight-verify": "0.2.23"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/copy-assets.mjs && tsup",
|