@rtrentjones/greenlight 0.2.22 → 0.2.24
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 +234 -35
- package/dist/{chunk-GO2RVNOP.js → chunk-2A7ZBBYN.js} +163 -26
- package/dist/{chunk-UXHHLEYO.js → chunk-KVOI4UL2.js} +5 -1
- 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
|
@@ -4,13 +4,14 @@ import {
|
|
|
4
4
|
allPass,
|
|
5
5
|
loadConfig,
|
|
6
6
|
resolveUrl,
|
|
7
|
+
scanSqlFiles,
|
|
7
8
|
verifyAll
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-2A7ZBBYN.js";
|
|
9
10
|
import "./chunk-HX7VA25D.js";
|
|
10
11
|
import "./chunk-N3IKUCSF.js";
|
|
11
12
|
import "./chunk-KP3Y6WRU.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-KVOI4UL2.js";
|
|
14
|
+
import "./chunk-XWTOJHLV.js";
|
|
14
15
|
import "./chunk-QFKE5JKC.js";
|
|
15
16
|
|
|
16
17
|
// src/commands/add.ts
|
|
@@ -443,7 +444,7 @@ function tokensForTool(tool) {
|
|
|
443
444
|
}
|
|
444
445
|
|
|
445
446
|
// src/version.ts
|
|
446
|
-
var MODULE_REF = "v0.2.
|
|
447
|
+
var MODULE_REF = "v0.2.24";
|
|
447
448
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
448
449
|
function moduleSource(module, ref = MODULE_REF) {
|
|
449
450
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -777,10 +778,22 @@ ${CLAUDE_BLOCK}` : CLAUDE_BLOCK);
|
|
|
777
778
|
async function agentCommand(args) {
|
|
778
779
|
if (args[0] !== "sync") {
|
|
779
780
|
console.log(
|
|
780
|
-
"usage: greenlight agent sync
|
|
781
|
+
"usage: greenlight agent sync [<name>]\n (no name) write the generic loop kit into THIS repo (the fallback)\n <name> load the manifest and sync that tool's kit into its dir, with the\n target-specific provider skills (oci/vercel/supabase), not just the always-on ones"
|
|
781
782
|
);
|
|
782
783
|
process.exit(args[0] ? 1 : 0);
|
|
783
784
|
}
|
|
785
|
+
const name = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
|
|
786
|
+
if (name) {
|
|
787
|
+
const { config } = await loadManifest();
|
|
788
|
+
const entry = resolveEntry(config, name);
|
|
789
|
+
const dir = resolve3(process.cwd(), entry.dir ?? ".");
|
|
790
|
+
materializeAgentKit(dir, { target: entry.target, data: entry.data });
|
|
791
|
+
console.log(
|
|
792
|
+
`
|
|
793
|
+
Synced the kit for "${name}" \u2192 ${entry.dir ?? "."} (target=${entry.target}, data=${entry.data}).`
|
|
794
|
+
);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
784
797
|
materializeAgentKit(process.cwd());
|
|
785
798
|
console.log(
|
|
786
799
|
"\nNote: the Greenlight Claude Code plugin (user scope) is the preferred path; this sync is the fallback.\nRun `/mcp` to authenticate the MCP servers."
|
|
@@ -1991,7 +2004,9 @@ async function deployCommand(args) {
|
|
|
1991
2004
|
}
|
|
1992
2005
|
|
|
1993
2006
|
// src/commands/doctor.ts
|
|
1994
|
-
import {
|
|
2007
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
2008
|
+
import { lookup } from "dns/promises";
|
|
2009
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
1995
2010
|
import { join as join4 } from "path";
|
|
1996
2011
|
function dirCheck(label, dir) {
|
|
1997
2012
|
return existsSync7(dir) ? { name: `${label}: directory`, status: "ok" } : { name: `${label}: directory`, status: "fail", detail: `missing ${dir}` };
|
|
@@ -2030,6 +2045,61 @@ function conformanceChecks(t, root) {
|
|
|
2030
2045
|
}
|
|
2031
2046
|
return out;
|
|
2032
2047
|
}
|
|
2048
|
+
function versionDriftCheck(root) {
|
|
2049
|
+
const name = "framework version drift";
|
|
2050
|
+
let installed;
|
|
2051
|
+
try {
|
|
2052
|
+
const pkg = JSON.parse(
|
|
2053
|
+
readFileSync5(join4(root, "node_modules/@rtrentjones/greenlight/package.json"), "utf8")
|
|
2054
|
+
);
|
|
2055
|
+
installed = pkg.version;
|
|
2056
|
+
} catch {
|
|
2057
|
+
}
|
|
2058
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2059
|
+
try {
|
|
2060
|
+
for (const f of readdirSync2(join4(root, "infra")).filter((f2) => f2.endsWith(".tf"))) {
|
|
2061
|
+
const body = readFileSync5(join4(root, "infra", f), "utf8");
|
|
2062
|
+
for (const m of body.matchAll(/greenlight\.git\/\/infra\/modules\/[^?"]+\?ref=(v[0-9.]+)/g)) {
|
|
2063
|
+
if (m[1]) refs.add(m[1]);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
2068
|
+
if (!installed && refs.size === 0) {
|
|
2069
|
+
return {
|
|
2070
|
+
name,
|
|
2071
|
+
status: "skip",
|
|
2072
|
+
detail: "no installed @rtrentjones/greenlight or infra pins here"
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
const refList = [...refs];
|
|
2076
|
+
if (installed) {
|
|
2077
|
+
const want = `v${installed}`;
|
|
2078
|
+
const bad = refList.filter((r) => r !== want);
|
|
2079
|
+
return bad.length === 0 ? { name, status: "ok", detail: `infra pins == installed ${want}` } : {
|
|
2080
|
+
name,
|
|
2081
|
+
status: "warn",
|
|
2082
|
+
detail: `installed ${want}, but infra pins ${bad.join(", ")} \u2014 bump ?ref to ${want}`
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
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(", ")}` };
|
|
2086
|
+
}
|
|
2087
|
+
function submoduleDriftCheck(root) {
|
|
2088
|
+
const name = "submodule drift";
|
|
2089
|
+
let out;
|
|
2090
|
+
try {
|
|
2091
|
+
out = execFileSync4("git", ["submodule", "status"], {
|
|
2092
|
+
cwd: root,
|
|
2093
|
+
encoding: "utf8",
|
|
2094
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2095
|
+
}).trim();
|
|
2096
|
+
} catch {
|
|
2097
|
+
return { name, status: "skip", detail: "no git / not a repo" };
|
|
2098
|
+
}
|
|
2099
|
+
if (!out) return { name, status: "skip", detail: "no submodules" };
|
|
2100
|
+
const dirty = out.split("\n").filter((l) => /^[+\-U]/.test(l));
|
|
2101
|
+
return dirty.length === 0 ? { name, status: "ok", detail: "all submodules match their recorded commit" } : { name, status: "warn", detail: dirty.map((l) => l.trim()).join("; ") };
|
|
2102
|
+
}
|
|
2033
2103
|
function runDoctor(config, root) {
|
|
2034
2104
|
const checks = [];
|
|
2035
2105
|
if (config.blog) checks.push(dirCheck("blog", join4(root, "apps/blog")));
|
|
@@ -2042,6 +2112,15 @@ function runDoctor(config, root) {
|
|
|
2042
2112
|
mcp: t.lane === "mcp"
|
|
2043
2113
|
});
|
|
2044
2114
|
checks.push({ name: `${t.name}: external (registry)`, status: "ok", detail: url });
|
|
2115
|
+
if (t.dir) {
|
|
2116
|
+
checks.push(
|
|
2117
|
+
existsSync7(join4(root, t.dir)) ? { name: `${t.name}: dir present`, status: "ok", detail: t.dir } : {
|
|
2118
|
+
name: `${t.name}: dir present`,
|
|
2119
|
+
status: "warn",
|
|
2120
|
+
detail: `declared dir "${t.dir}" missing \u2014 run \`git submodule update --init\``
|
|
2121
|
+
}
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2045
2124
|
} else {
|
|
2046
2125
|
checks.push(dirCheck(t.name, join4(root, t.dir ?? join4("tools", t.name))));
|
|
2047
2126
|
}
|
|
@@ -2053,20 +2132,54 @@ function runDoctor(config, root) {
|
|
|
2053
2132
|
status: needsKeepalive.length > 0 ? "ok" : "skip",
|
|
2054
2133
|
detail: needsKeepalive.length > 0 ? needsKeepalive.map((t) => `${t.name} (${t.data === "supabase" ? "supabase" : "oci"})`).join(", ") : "no data:supabase / target:oci tools"
|
|
2055
2134
|
});
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2135
|
+
checks.push(versionDriftCheck(root));
|
|
2136
|
+
checks.push(submoduleDriftCheck(root));
|
|
2137
|
+
return checks;
|
|
2138
|
+
}
|
|
2139
|
+
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
2140
|
+
async function livenessCheck(name, url) {
|
|
2141
|
+
try {
|
|
2142
|
+
const res = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(1e4) });
|
|
2143
|
+
return res.status >= 500 ? { name: `${name}: live`, status: "warn", detail: `${url} \u2192 ${res.status} (degraded)` } : { name: `${name}: live`, status: "ok", detail: `${url} \u2192 ${res.status}` };
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
return { name: `${name}: live`, status: "fail", detail: `${url} unreachable: ${errMsg(e)}` };
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
async function dnsCheck(name, url) {
|
|
2149
|
+
const host = new URL(url).hostname;
|
|
2150
|
+
try {
|
|
2151
|
+
await lookup(host);
|
|
2152
|
+
return { name: `${name}: DNS`, status: "ok", detail: host };
|
|
2153
|
+
} catch {
|
|
2154
|
+
return { name: `${name}: DNS`, status: "fail", detail: `${host} does not resolve` };
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
async function runDoctorLive(config) {
|
|
2158
|
+
const targets = [];
|
|
2159
|
+
if (config.blog) targets.push({ name: "blog", url: `https://${config.domain}` });
|
|
2160
|
+
for (const t of config.tools) {
|
|
2161
|
+
if (!t.envs?.includes("prod")) continue;
|
|
2162
|
+
targets.push({
|
|
2163
|
+
name: t.name,
|
|
2164
|
+
url: resolveUrl({ domain: config.domain, name: t.name, env: "prod", mcp: t.lane === "mcp" })
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
const checks = [];
|
|
2168
|
+
for (const tg of targets) {
|
|
2169
|
+
checks.push(await dnsCheck(tg.name, tg.url));
|
|
2170
|
+
checks.push(await livenessCheck(tg.name, tg.url));
|
|
2171
|
+
}
|
|
2172
|
+
for (const name of ["terraform drift", "Vercel cap headroom", "OCI PAYG status"]) {
|
|
2173
|
+
checks.push({
|
|
2174
|
+
name,
|
|
2175
|
+
status: "skip",
|
|
2176
|
+
detail: "not yet implemented \u2014 needs the provider API + creds"
|
|
2177
|
+
});
|
|
2065
2178
|
}
|
|
2066
2179
|
return checks;
|
|
2067
2180
|
}
|
|
2068
2181
|
var ICON = { ok: "\u2714", warn: "!", fail: "\u2718", skip: "\xB7" };
|
|
2069
|
-
async function doctorCommand() {
|
|
2182
|
+
async function doctorCommand(args = []) {
|
|
2070
2183
|
let config;
|
|
2071
2184
|
try {
|
|
2072
2185
|
({ config } = await loadManifest());
|
|
@@ -2075,13 +2188,19 @@ async function doctorCommand() {
|
|
|
2075
2188
|
console.error(`\u2718 manifest: ${e instanceof Error ? e.message : String(e)}`);
|
|
2076
2189
|
process.exit(1);
|
|
2077
2190
|
}
|
|
2191
|
+
const live = args.includes("--live");
|
|
2078
2192
|
const checks = runDoctor(config, process.cwd());
|
|
2193
|
+
if (live) {
|
|
2194
|
+
console.log(" (probing live prod URLs\u2026)");
|
|
2195
|
+
checks.push(...await runDoctorLive(config));
|
|
2196
|
+
}
|
|
2079
2197
|
for (const c of checks) {
|
|
2080
2198
|
console.log(` ${ICON[c.status]} ${c.name}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
|
|
2081
2199
|
}
|
|
2082
2200
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
2083
2201
|
console.log(`
|
|
2084
2202
|
${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}`);
|
|
2203
|
+
if (!live) console.log("\xB7 run `greenlight doctor --live` for DNS + reachability probes");
|
|
2085
2204
|
process.exit(failed === 0 ? 0 : 1);
|
|
2086
2205
|
}
|
|
2087
2206
|
|
|
@@ -2091,7 +2210,7 @@ import { resolve as resolve8 } from "path";
|
|
|
2091
2210
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
2092
2211
|
|
|
2093
2212
|
// src/tokens.ts
|
|
2094
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as
|
|
2213
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
2095
2214
|
import { resolve as resolve7 } from "path";
|
|
2096
2215
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2097
2216
|
var SECRETS_DIR = ".greenlight";
|
|
@@ -2100,7 +2219,7 @@ function presentEnv(cwd) {
|
|
|
2100
2219
|
const out = {};
|
|
2101
2220
|
const p = resolve7(cwd, SECRETS_DIR, SECRETS_FILE);
|
|
2102
2221
|
if (existsSync8(p)) {
|
|
2103
|
-
for (const { key, value } of parseSecretsEnv(
|
|
2222
|
+
for (const { key, value } of parseSecretsEnv(readFileSync6(p, "utf8"))) out[key] = value;
|
|
2104
2223
|
}
|
|
2105
2224
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2106
2225
|
if (v !== void 0 && !(k in out)) out[k] = v;
|
|
@@ -2111,7 +2230,7 @@ function upsertSecret(cwd, key, value) {
|
|
|
2111
2230
|
const dir = resolve7(cwd, SECRETS_DIR);
|
|
2112
2231
|
mkdirSync4(dir, { recursive: true });
|
|
2113
2232
|
const p = resolve7(dir, SECRETS_FILE);
|
|
2114
|
-
const lines = existsSync8(p) ?
|
|
2233
|
+
const lines = existsSync8(p) ? readFileSync6(p, "utf8").split("\n") : [];
|
|
2115
2234
|
const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
|
|
2116
2235
|
if (idx >= 0) lines[idx] = `${key}=${value}`;
|
|
2117
2236
|
else {
|
|
@@ -2337,8 +2456,59 @@ Next:
|
|
|
2337
2456
|
4. greenlight verify <name> --env prod | greenlight doctor`);
|
|
2338
2457
|
}
|
|
2339
2458
|
|
|
2459
|
+
// src/commands/migrations.ts
|
|
2460
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync3 } from "fs";
|
|
2461
|
+
import { join as join5 } from "path";
|
|
2462
|
+
var DEFAULT_DIR = "supabase/migrations";
|
|
2463
|
+
async function migrationsCommand(args) {
|
|
2464
|
+
if (args[0] !== "scan") {
|
|
2465
|
+
console.log(
|
|
2466
|
+
`usage: greenlight migrations scan [<dir>] [--strict]
|
|
2467
|
+
scan SQL migrations for data-destroying / lock-heavy statements (the pre-apply gate).
|
|
2468
|
+
default dir: ${DEFAULT_DIR}. Acknowledge an intentional op with \`-- greenlight:allow\`.`
|
|
2469
|
+
);
|
|
2470
|
+
process.exit(args[0] ? 1 : 0);
|
|
2471
|
+
}
|
|
2472
|
+
const dir = args.slice(1).find((a) => !a.startsWith("-")) ?? DEFAULT_DIR;
|
|
2473
|
+
const strict = args.includes("--strict");
|
|
2474
|
+
let names;
|
|
2475
|
+
try {
|
|
2476
|
+
names = readdirSync3(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
2477
|
+
} catch {
|
|
2478
|
+
console.log(`\xB7 no migrations dir at ${dir} \u2014 nothing to scan`);
|
|
2479
|
+
process.exit(0);
|
|
2480
|
+
}
|
|
2481
|
+
if (names.length === 0) {
|
|
2482
|
+
console.log(`\xB7 no .sql files in ${dir} \u2014 nothing to scan`);
|
|
2483
|
+
process.exit(0);
|
|
2484
|
+
}
|
|
2485
|
+
const files = names.map((f) => ({
|
|
2486
|
+
path: join5(dir, f),
|
|
2487
|
+
content: readFileSync7(join5(dir, f), "utf8")
|
|
2488
|
+
}));
|
|
2489
|
+
const findings = scanSqlFiles(files);
|
|
2490
|
+
if (findings.length === 0) {
|
|
2491
|
+
console.log(`\u2714 migrations scan: ${names.length} file(s) clean (${dir})`);
|
|
2492
|
+
process.exit(0);
|
|
2493
|
+
}
|
|
2494
|
+
for (const f of findings) {
|
|
2495
|
+
console.log(
|
|
2496
|
+
` ${f.severity === "danger" ? "\u2718" : "!"} ${f.file}:${f.line} [${f.rule}] ${f.detail}
|
|
2497
|
+
${f.snippet}`
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
const dangers = findings.filter((f) => f.severity === "danger");
|
|
2501
|
+
const blocking = strict ? findings : dangers;
|
|
2502
|
+
const verdict = blocking.length === 0 ? "\u2714 no blocking findings" : `\u2718 ${blocking.length} blocking finding(s)`;
|
|
2503
|
+
console.log(
|
|
2504
|
+
`
|
|
2505
|
+
${verdict} (${dangers.length} danger, ${findings.length - dangers.length} warn). Acknowledge an intentional op with \`-- greenlight:allow\`.`
|
|
2506
|
+
);
|
|
2507
|
+
process.exit(blocking.length === 0 ? 0 : 1);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2340
2510
|
// src/commands/preview.ts
|
|
2341
|
-
import { execFileSync as
|
|
2511
|
+
import { execFileSync as execFileSync5, spawn } from "child_process";
|
|
2342
2512
|
import { resolve as resolve10 } from "path";
|
|
2343
2513
|
import { setTimeout as sleep } from "timers/promises";
|
|
2344
2514
|
|
|
@@ -2377,6 +2547,15 @@ ${report.logs}
|
|
|
2377
2547
|
}
|
|
2378
2548
|
}
|
|
2379
2549
|
var LOG_TAIL_LINES = 50;
|
|
2550
|
+
function redactSecrets(text, env = process.env) {
|
|
2551
|
+
let out = text;
|
|
2552
|
+
for (const [k, v] of Object.entries(env)) {
|
|
2553
|
+
if (!v || v.length < 6) continue;
|
|
2554
|
+
if (!/TOKEN|KEY|SECRET|PASSWORD|PWD/i.test(k)) continue;
|
|
2555
|
+
out = out.split(v).join("***");
|
|
2556
|
+
}
|
|
2557
|
+
return out;
|
|
2558
|
+
}
|
|
2380
2559
|
function attachFailureLogs(reports, specs, toolDir) {
|
|
2381
2560
|
reports.forEach((report, i) => {
|
|
2382
2561
|
if (report.pass) return;
|
|
@@ -2395,7 +2574,7 @@ function attachFailureLogs(reports, specs, toolDir) {
|
|
|
2395
2574
|
// Let the command target the exact failing URL without hard-coding it.
|
|
2396
2575
|
env: { ...process.env, GREENLIGHT_VERIFY_URL: report.url }
|
|
2397
2576
|
});
|
|
2398
|
-
const out = `${res.stdout ?? ""}${res.stderr ?? ""}`.trimEnd();
|
|
2577
|
+
const out = redactSecrets(`${res.stdout ?? ""}${res.stderr ?? ""}`.trimEnd());
|
|
2399
2578
|
const tail = out.split("\n").slice(-LOG_TAIL_LINES).join("\n");
|
|
2400
2579
|
report.logs = tail || `(logsOnFailure produced no output${res.error ? `: ${res.error.message}` : ""})`;
|
|
2401
2580
|
} catch (e) {
|
|
@@ -2528,7 +2707,7 @@ async function previewViaDescriptor(entry, name, portOverride) {
|
|
|
2528
2707
|
} finally {
|
|
2529
2708
|
if (pv.teardown) {
|
|
2530
2709
|
try {
|
|
2531
|
-
|
|
2710
|
+
execFileSync5(pv.teardown, { cwd: toolDir, shell: true, stdio: "inherit" });
|
|
2532
2711
|
} catch {
|
|
2533
2712
|
}
|
|
2534
2713
|
}
|
|
@@ -2545,7 +2724,7 @@ async function previewViaBuiltIn(entry, name, portOverride) {
|
|
|
2545
2724
|
const plan = servePlan(entry.lane, portOverride);
|
|
2546
2725
|
if (plan.build) {
|
|
2547
2726
|
console.log(`build ${name} (${entry.dir})`);
|
|
2548
|
-
|
|
2727
|
+
execFileSync5("pnpm", ["-C", entry.dir, "run", "build"], { stdio: "inherit" });
|
|
2549
2728
|
}
|
|
2550
2729
|
console.log(`serve ${name} on :${plan.port}`);
|
|
2551
2730
|
const runArgs = ["-C", entry.dir, "run", plan.script];
|
|
@@ -2600,12 +2779,12 @@ async function previewCommand(args) {
|
|
|
2600
2779
|
}
|
|
2601
2780
|
|
|
2602
2781
|
// ../packages/loop/src/promote.ts
|
|
2603
|
-
import { execFileSync as
|
|
2782
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2604
2783
|
function git(repoDir, args) {
|
|
2605
|
-
|
|
2784
|
+
execFileSync6("git", args, { cwd: repoDir, stdio: "ignore" });
|
|
2606
2785
|
}
|
|
2607
2786
|
function gitOut(repoDir, args) {
|
|
2608
|
-
return
|
|
2787
|
+
return execFileSync6("git", args, { cwd: repoDir, encoding: "utf8" }).trim();
|
|
2609
2788
|
}
|
|
2610
2789
|
function tryRev(repoDir, ref) {
|
|
2611
2790
|
try {
|
|
@@ -2615,9 +2794,16 @@ function tryRev(repoDir, ref) {
|
|
|
2615
2794
|
}
|
|
2616
2795
|
}
|
|
2617
2796
|
function fetchRefs(repoDir, branches) {
|
|
2797
|
+
try {
|
|
2798
|
+
git(repoDir, ["remote", "get-url", "origin"]);
|
|
2799
|
+
} catch {
|
|
2800
|
+
return { ok: false, hasOrigin: false };
|
|
2801
|
+
}
|
|
2618
2802
|
try {
|
|
2619
2803
|
git(repoDir, ["fetch", "--no-tags", "origin", ...branches]);
|
|
2804
|
+
return { ok: true, hasOrigin: true };
|
|
2620
2805
|
} catch {
|
|
2806
|
+
return { ok: false, hasOrigin: true };
|
|
2621
2807
|
}
|
|
2622
2808
|
}
|
|
2623
2809
|
function resolveRef(repoDir, branch) {
|
|
@@ -2644,13 +2830,23 @@ function staleLocalWarnings(repoDir, branches) {
|
|
|
2644
2830
|
return warnings;
|
|
2645
2831
|
}
|
|
2646
2832
|
function canPromote(repoDir, from = "develop", to = "main") {
|
|
2647
|
-
|
|
2833
|
+
const warnings = [];
|
|
2834
|
+
const fetched = fetchRefs(repoDir, [from, to]);
|
|
2835
|
+
if (fetched.hasOrigin && !fetched.ok) {
|
|
2836
|
+
warnings.push(
|
|
2837
|
+
"could not `git fetch origin` \u2014 eligibility may be based on stale remote-tracking refs (offline / auth?). Re-run after a successful fetch."
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2648
2840
|
const fromRef = resolveRef(repoDir, from);
|
|
2649
2841
|
const toRef = resolveRef(repoDir, to);
|
|
2650
2842
|
if (!fromRef || !toRef) {
|
|
2651
|
-
return {
|
|
2843
|
+
return {
|
|
2844
|
+
canPromote: false,
|
|
2845
|
+
reason: `branch "${from}" or "${to}" not found in ${repoDir}`,
|
|
2846
|
+
warnings: warnings.length ? warnings : void 0
|
|
2847
|
+
};
|
|
2652
2848
|
}
|
|
2653
|
-
|
|
2849
|
+
warnings.push(...staleLocalWarnings(repoDir, [from, to]));
|
|
2654
2850
|
try {
|
|
2655
2851
|
git(repoDir, ["merge-base", "--is-ancestor", toRef, fromRef]);
|
|
2656
2852
|
return { canPromote: true, reason: `"${to}" can fast-forward to "${from}"`, warnings };
|
|
@@ -2716,10 +2912,10 @@ async function promoteCommand(args) {
|
|
|
2716
2912
|
}
|
|
2717
2913
|
|
|
2718
2914
|
// src/commands/status.ts
|
|
2719
|
-
import { execFileSync as
|
|
2915
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
2720
2916
|
function repoSlug(dir) {
|
|
2721
2917
|
try {
|
|
2722
|
-
const url =
|
|
2918
|
+
const url = execFileSync7("git", ["-C", dir, "remote", "get-url", "origin"], {
|
|
2723
2919
|
encoding: "utf8"
|
|
2724
2920
|
}).trim();
|
|
2725
2921
|
const m = url.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
@@ -2752,7 +2948,7 @@ function workflowsFor(entry, name, wrapper, toolRepo) {
|
|
|
2752
2948
|
}
|
|
2753
2949
|
function lastRun(repo, workflow) {
|
|
2754
2950
|
try {
|
|
2755
|
-
const out =
|
|
2951
|
+
const out = execFileSync7(
|
|
2756
2952
|
"gh",
|
|
2757
2953
|
[
|
|
2758
2954
|
"run",
|
|
@@ -2808,9 +3004,10 @@ var HELP = `greenlight <command>
|
|
|
2808
3004
|
status <name> last ship/deploy/verify run for a tool (via gh)
|
|
2809
3005
|
secrets gather <name> [--repo o/r] [--env e] guided, link-first token prompts -> GitHub secrets (no disk/logs)
|
|
2810
3006
|
secrets sync [--repo o/r] [--env <env>] push .greenlight/secrets.env -> GitHub Actions secrets
|
|
2811
|
-
agent sync
|
|
3007
|
+
agent sync [<name>] write the loop kit (named \u2192 tool-aware, into its dir)
|
|
2812
3008
|
adopt <name> --repo <path> --lane --target onboard an existing tool repo as a thin consumer
|
|
2813
|
-
|
|
3009
|
+
migrations scan [<dir>] [--strict] dangerous-SQL gate for migrations (pre-apply)
|
|
3010
|
+
doctor [--live] consistency checks (--live: DNS + reachability probes)
|
|
2814
3011
|
help show this message
|
|
2815
3012
|
|
|
2816
3013
|
Real cloud deploys need the target's creds (e.g. CLOUDFLARE_API_TOKEN); see docs/archive/greenlight-v1.md \xA716.`;
|
|
@@ -2845,8 +3042,10 @@ async function main() {
|
|
|
2845
3042
|
return agentCommand(args);
|
|
2846
3043
|
case "adopt":
|
|
2847
3044
|
return adoptCommand(args);
|
|
3045
|
+
case "migrations":
|
|
3046
|
+
return migrationsCommand(args);
|
|
2848
3047
|
case "doctor":
|
|
2849
|
-
return doctorCommand();
|
|
3048
|
+
return doctorCommand(args);
|
|
2850
3049
|
default:
|
|
2851
3050
|
throw new Error(`Unknown command "${cmd}".
|
|
2852
3051
|
|
|
@@ -131,15 +131,119 @@ function resolveUrl({ domain, name, env, mcp }) {
|
|
|
131
131
|
return `https://${host}${mcp ? "/mcp" : ""}`;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// ../packages/shared/src/sql-scan.ts
|
|
135
|
+
var RULES = [
|
|
136
|
+
{
|
|
137
|
+
name: "drop-table",
|
|
138
|
+
severity: "danger",
|
|
139
|
+
detail: "DROP TABLE destroys a table and its data",
|
|
140
|
+
test: /\bDROP\s+TABLE\b/i
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "drop-column",
|
|
144
|
+
severity: "danger",
|
|
145
|
+
detail: "DROP COLUMN destroys a column and its data",
|
|
146
|
+
test: /\bDROP\s+COLUMN\b/i
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "drop-schema",
|
|
150
|
+
severity: "danger",
|
|
151
|
+
detail: "DROP SCHEMA destroys a schema",
|
|
152
|
+
test: /\bDROP\s+SCHEMA\b/i
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "drop-database",
|
|
156
|
+
severity: "danger",
|
|
157
|
+
detail: "DROP DATABASE destroys a database",
|
|
158
|
+
test: /\bDROP\s+DATABASE\b/i
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "truncate",
|
|
162
|
+
severity: "danger",
|
|
163
|
+
detail: "TRUNCATE empties a table irreversibly",
|
|
164
|
+
test: /\bTRUNCATE\b/i
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "delete-without-where",
|
|
168
|
+
severity: "danger",
|
|
169
|
+
detail: "DELETE without WHERE removes every row",
|
|
170
|
+
test: /\bDELETE\s+FROM\b(?![\s\S]*\bWHERE\b)/i
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "update-without-where",
|
|
174
|
+
severity: "danger",
|
|
175
|
+
detail: "UPDATE \u2026 SET without WHERE rewrites every row",
|
|
176
|
+
test: /\bUPDATE\s+[^\s;]+\s+SET\b(?![\s\S]*\bWHERE\b)/i
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "non-concurrent-index",
|
|
180
|
+
severity: "warn",
|
|
181
|
+
detail: "CREATE INDEX without CONCURRENTLY locks writes (fine on a new/empty table)",
|
|
182
|
+
test: /\bCREATE\s+(?:UNIQUE\s+)?INDEX\b(?![\s\S]*\bCONCURRENTLY\b)/i
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "alter-column-type",
|
|
186
|
+
severity: "warn",
|
|
187
|
+
detail: "ALTER COLUMN \u2026 TYPE can rewrite + lock the table",
|
|
188
|
+
test: /\bALTER\s+COLUMN\b[\s\S]*?\bTYPE\b/i
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
var ALLOW = /greenlight:\s*allow/i;
|
|
192
|
+
function scanSql(content, file = "<sql>") {
|
|
193
|
+
const allowLines = /* @__PURE__ */ new Set();
|
|
194
|
+
content.split("\n").forEach((ln, i) => {
|
|
195
|
+
if (ALLOW.test(ln)) allowLines.add(i + 1);
|
|
196
|
+
});
|
|
197
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/--[^\n]*/g, (m) => " ".repeat(m.length));
|
|
198
|
+
const findings = [];
|
|
199
|
+
let pos = 0;
|
|
200
|
+
for (const stmt of stripped.split(";")) {
|
|
201
|
+
const lead = stmt.length - stmt.trimStart().length;
|
|
202
|
+
const startLine = content.slice(0, pos + lead).split("\n").length;
|
|
203
|
+
const endLine = content.slice(0, pos + stmt.length).split("\n").length;
|
|
204
|
+
pos += stmt.length + 1;
|
|
205
|
+
if (!stmt.trim()) continue;
|
|
206
|
+
let allowed = false;
|
|
207
|
+
for (let l = startLine; l <= endLine; l++) {
|
|
208
|
+
if (allowLines.has(l)) {
|
|
209
|
+
allowed = true;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (allowed) continue;
|
|
214
|
+
for (const rule of RULES) {
|
|
215
|
+
if (rule.test.test(stmt)) {
|
|
216
|
+
findings.push({
|
|
217
|
+
file,
|
|
218
|
+
line: startLine,
|
|
219
|
+
rule: rule.name,
|
|
220
|
+
severity: rule.severity,
|
|
221
|
+
detail: rule.detail,
|
|
222
|
+
snippet: stmt.replace(/\s+/g, " ").trim().slice(0, 100)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return findings;
|
|
228
|
+
}
|
|
229
|
+
function scanSqlFiles(files) {
|
|
230
|
+
return files.flatMap((f) => scanSql(f.content, f.path));
|
|
231
|
+
}
|
|
232
|
+
|
|
134
233
|
// ../packages/verify/src/index.ts
|
|
135
234
|
import { setTimeout as sleep } from "timers/promises";
|
|
136
235
|
|
|
137
236
|
// ../packages/verify/src/api.ts
|
|
138
237
|
var trimSlash = (s) => s.replace(/\/+$/, "");
|
|
139
|
-
|
|
238
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
239
|
+
var DEFAULT_MAX_LINKS = 50;
|
|
240
|
+
function timedFetch(url, timeoutMs, init) {
|
|
241
|
+
return fetch(url, { redirect: "manual", ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
242
|
+
}
|
|
243
|
+
async function checkRoute(base, c, timeoutMs) {
|
|
140
244
|
const name = `GET ${c.path}`;
|
|
141
245
|
try {
|
|
142
|
-
const res = await
|
|
246
|
+
const res = await timedFetch(base + c.path, timeoutMs, { headers: c.requestHeaders });
|
|
143
247
|
const reasons = [];
|
|
144
248
|
if (c.status !== void 0 && res.status !== c.status) {
|
|
145
249
|
reasons.push(`status ${res.status} != ${c.status}`);
|
|
@@ -160,10 +264,10 @@ async function checkRoute(base, c) {
|
|
|
160
264
|
return { name, pass: false, detail: msg(e) };
|
|
161
265
|
}
|
|
162
266
|
}
|
|
163
|
-
async function checkXml(base, candidates, label, marker) {
|
|
267
|
+
async function checkXml(base, candidates, label, marker, timeoutMs) {
|
|
164
268
|
for (const path of candidates) {
|
|
165
269
|
try {
|
|
166
|
-
const res = await
|
|
270
|
+
const res = await timedFetch(base + path, timeoutMs);
|
|
167
271
|
if (res.status === 200) {
|
|
168
272
|
const body = await res.text();
|
|
169
273
|
const ok = marker.test(body);
|
|
@@ -178,65 +282,97 @@ async function checkXml(base, candidates, label, marker) {
|
|
|
178
282
|
}
|
|
179
283
|
return { name: label, pass: false, detail: `none of ${candidates.join(", ")} returned 200` };
|
|
180
284
|
}
|
|
181
|
-
async function checkInternalLinks(base, max =
|
|
285
|
+
async function checkInternalLinks(base, timeoutMs, max = DEFAULT_MAX_LINKS) {
|
|
182
286
|
try {
|
|
183
|
-
const res = await
|
|
287
|
+
const res = await timedFetch(`${base}/`, timeoutMs);
|
|
184
288
|
const html = await res.text();
|
|
185
289
|
const hrefs = /* @__PURE__ */ new Set();
|
|
290
|
+
let capped = false;
|
|
186
291
|
for (const m of html.matchAll(/href="(\/[^"#?]*)"/g)) {
|
|
187
292
|
const href = m[1];
|
|
188
293
|
if (href && !href.startsWith("//")) hrefs.add(href);
|
|
189
|
-
if (hrefs.size >= max)
|
|
294
|
+
if (hrefs.size >= max) {
|
|
295
|
+
capped = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (hrefs.size === 0) {
|
|
300
|
+
return {
|
|
301
|
+
name: "no broken internal links",
|
|
302
|
+
pass: false,
|
|
303
|
+
detail: `no internal links found on ${base}/ (status ${res.status}) \u2014 page empty or unparseable`
|
|
304
|
+
};
|
|
190
305
|
}
|
|
191
306
|
const broken = [];
|
|
192
307
|
for (const href of hrefs) {
|
|
193
308
|
try {
|
|
194
|
-
const r = await
|
|
309
|
+
const r = await timedFetch(base + href, timeoutMs);
|
|
195
310
|
if (r.status >= 400) broken.push(`${href} (${r.status})`);
|
|
196
311
|
} catch {
|
|
197
312
|
broken.push(`${href} (unreachable)`);
|
|
198
313
|
}
|
|
199
314
|
}
|
|
315
|
+
const capNote = capped ? `; capped at first ${max} \u2014 raise maxLinks to check more` : "";
|
|
200
316
|
return {
|
|
201
|
-
name: `no broken internal links (${hrefs.size} checked)`,
|
|
317
|
+
name: `no broken internal links (${hrefs.size} checked${capped ? `, capped at ${max}` : ""})`,
|
|
202
318
|
pass: broken.length === 0,
|
|
203
|
-
detail: broken.length ? `broken: ${broken.join(", ")}` : void 0
|
|
319
|
+
detail: broken.length ? `broken: ${broken.join(", ")}${capNote}` : capNote || void 0
|
|
204
320
|
};
|
|
205
321
|
} catch (e) {
|
|
206
322
|
return { name: "no broken internal links", pass: false, detail: msg(e) };
|
|
207
323
|
}
|
|
208
324
|
}
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
325
|
+
function buildTasks(base, spec) {
|
|
326
|
+
const timeoutMs = spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
327
|
+
const tasks = [];
|
|
328
|
+
for (const c of spec.checks ?? []) tasks.push(() => checkRoute(base, c, timeoutMs));
|
|
212
329
|
if (spec.rssValid) {
|
|
213
|
-
|
|
214
|
-
|
|
330
|
+
tasks.push(
|
|
331
|
+
() => checkXml(
|
|
332
|
+
base,
|
|
333
|
+
["/rss.xml", "/feed.xml", "/index.xml"],
|
|
334
|
+
"rss",
|
|
335
|
+
/<(rss|feed)[\s>]/i,
|
|
336
|
+
timeoutMs
|
|
337
|
+
)
|
|
215
338
|
);
|
|
216
339
|
}
|
|
217
340
|
if (spec.sitemapValid) {
|
|
218
|
-
|
|
219
|
-
|
|
341
|
+
tasks.push(
|
|
342
|
+
() => checkXml(
|
|
220
343
|
base,
|
|
221
344
|
["/sitemap.xml", "/sitemap-index.xml"],
|
|
222
345
|
"sitemap",
|
|
223
|
-
/<(urlset|sitemapindex)[\s>]/i
|
|
346
|
+
/<(urlset|sitemapindex)[\s>]/i,
|
|
347
|
+
timeoutMs
|
|
224
348
|
)
|
|
225
349
|
);
|
|
226
350
|
}
|
|
227
|
-
if (spec.noBrokenInternalLinks)
|
|
228
|
-
|
|
351
|
+
if (spec.noBrokenInternalLinks) {
|
|
352
|
+
tasks.push(() => checkInternalLinks(base, timeoutMs, spec.maxLinks));
|
|
353
|
+
}
|
|
354
|
+
return tasks;
|
|
229
355
|
}
|
|
230
356
|
async function verifyApi(baseUrl, spec) {
|
|
231
357
|
const base = trimSlash(baseUrl);
|
|
232
358
|
const retries = Math.max(0, spec.settleRetries ?? 0);
|
|
233
359
|
const delayMs = spec.settleMs ?? 5e3;
|
|
234
|
-
|
|
235
|
-
|
|
360
|
+
const state = await Promise.all(
|
|
361
|
+
buildTasks(base, spec).map(async (task) => ({ task, check: await task() }))
|
|
362
|
+
);
|
|
363
|
+
for (let i = 0; i < retries && !state.every((s) => s.check.pass); i++) {
|
|
236
364
|
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
237
|
-
|
|
365
|
+
await Promise.all(
|
|
366
|
+
state.filter((s) => !s.check.pass).map(async (s) => {
|
|
367
|
+
s.check = await s.task();
|
|
368
|
+
})
|
|
369
|
+
);
|
|
238
370
|
}
|
|
239
|
-
return report(
|
|
371
|
+
return report(
|
|
372
|
+
"api",
|
|
373
|
+
baseUrl,
|
|
374
|
+
state.map((s) => s.check)
|
|
375
|
+
);
|
|
240
376
|
}
|
|
241
377
|
|
|
242
378
|
// ../packages/verify/src/index.ts
|
|
@@ -274,11 +410,11 @@ async function verify(baseUrl, spec, opts) {
|
|
|
274
410
|
return verifyTest2(spec, opts?.toolDir ?? process.cwd());
|
|
275
411
|
}
|
|
276
412
|
case "agent-web": {
|
|
277
|
-
const { verifyAgentWeb: verifyAgentWeb2 } = await import("./agent-web-
|
|
413
|
+
const { verifyAgentWeb: verifyAgentWeb2 } = await import("./agent-web-3FTO2TLJ.js");
|
|
278
414
|
return verifyAgentWeb2(baseUrl, spec);
|
|
279
415
|
}
|
|
280
416
|
case "eval": {
|
|
281
|
-
const { verifyEval: verifyEval2 } = await import("./eval-
|
|
417
|
+
const { verifyEval: verifyEval2 } = await import("./eval-44S2BATV.js");
|
|
282
418
|
return verifyEval2(baseUrl, spec);
|
|
283
419
|
}
|
|
284
420
|
}
|
|
@@ -302,6 +438,7 @@ export {
|
|
|
302
438
|
defineConfig,
|
|
303
439
|
loadConfig,
|
|
304
440
|
resolveUrl,
|
|
441
|
+
scanSqlFiles,
|
|
305
442
|
defineVerify,
|
|
306
443
|
verifyAll,
|
|
307
444
|
allPass
|
|
@@ -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 });
|
|
@@ -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-2A7ZBBYN.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.24",
|
|
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-adapters": "0.2.
|
|
35
|
-
"@rtrentjones/greenlight-
|
|
36
|
-
"@rtrentjones/greenlight-
|
|
37
|
-
"@rtrentjones/greenlight-shared": "0.2.
|
|
34
|
+
"@rtrentjones/greenlight-adapters": "0.2.24",
|
|
35
|
+
"@rtrentjones/greenlight-loop": "0.2.24",
|
|
36
|
+
"@rtrentjones/greenlight-verify": "0.2.24",
|
|
37
|
+
"@rtrentjones/greenlight-shared": "0.2.24"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/copy-assets.mjs && tsup",
|