@rtrentjones/greenlight 0.2.22 → 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 +115 -23
- 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,12 +2675,12 @@ 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();
|
|
2609
2684
|
}
|
|
2610
2685
|
function tryRev(repoDir, ref) {
|
|
2611
2686
|
try {
|
|
@@ -2615,9 +2690,16 @@ function tryRev(repoDir, ref) {
|
|
|
2615
2690
|
}
|
|
2616
2691
|
}
|
|
2617
2692
|
function fetchRefs(repoDir, branches) {
|
|
2693
|
+
try {
|
|
2694
|
+
git(repoDir, ["remote", "get-url", "origin"]);
|
|
2695
|
+
} catch {
|
|
2696
|
+
return { ok: false, hasOrigin: false };
|
|
2697
|
+
}
|
|
2618
2698
|
try {
|
|
2619
2699
|
git(repoDir, ["fetch", "--no-tags", "origin", ...branches]);
|
|
2700
|
+
return { ok: true, hasOrigin: true };
|
|
2620
2701
|
} catch {
|
|
2702
|
+
return { ok: false, hasOrigin: true };
|
|
2621
2703
|
}
|
|
2622
2704
|
}
|
|
2623
2705
|
function resolveRef(repoDir, branch) {
|
|
@@ -2644,13 +2726,23 @@ function staleLocalWarnings(repoDir, branches) {
|
|
|
2644
2726
|
return warnings;
|
|
2645
2727
|
}
|
|
2646
2728
|
function canPromote(repoDir, from = "develop", to = "main") {
|
|
2647
|
-
|
|
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
|
+
}
|
|
2648
2736
|
const fromRef = resolveRef(repoDir, from);
|
|
2649
2737
|
const toRef = resolveRef(repoDir, to);
|
|
2650
2738
|
if (!fromRef || !toRef) {
|
|
2651
|
-
return {
|
|
2739
|
+
return {
|
|
2740
|
+
canPromote: false,
|
|
2741
|
+
reason: `branch "${from}" or "${to}" not found in ${repoDir}`,
|
|
2742
|
+
warnings: warnings.length ? warnings : void 0
|
|
2743
|
+
};
|
|
2652
2744
|
}
|
|
2653
|
-
|
|
2745
|
+
warnings.push(...staleLocalWarnings(repoDir, [from, to]));
|
|
2654
2746
|
try {
|
|
2655
2747
|
git(repoDir, ["merge-base", "--is-ancestor", toRef, fromRef]);
|
|
2656
2748
|
return { canPromote: true, reason: `"${to}" can fast-forward to "${from}"`, warnings };
|
|
@@ -2716,10 +2808,10 @@ async function promoteCommand(args) {
|
|
|
2716
2808
|
}
|
|
2717
2809
|
|
|
2718
2810
|
// src/commands/status.ts
|
|
2719
|
-
import { execFileSync as
|
|
2811
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
2720
2812
|
function repoSlug(dir) {
|
|
2721
2813
|
try {
|
|
2722
|
-
const url =
|
|
2814
|
+
const url = execFileSync7("git", ["-C", dir, "remote", "get-url", "origin"], {
|
|
2723
2815
|
encoding: "utf8"
|
|
2724
2816
|
}).trim();
|
|
2725
2817
|
const m = url.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
@@ -2752,7 +2844,7 @@ function workflowsFor(entry, name, wrapper, toolRepo) {
|
|
|
2752
2844
|
}
|
|
2753
2845
|
function lastRun(repo, workflow) {
|
|
2754
2846
|
try {
|
|
2755
|
-
const out =
|
|
2847
|
+
const out = execFileSync7(
|
|
2756
2848
|
"gh",
|
|
2757
2849
|
[
|
|
2758
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-
|
|
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",
|