@rtrentjones/greenlight 0.4.1 → 0.5.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/assets/skills/provider-cloudflare/SKILL.md +27 -26
- package/assets/skills/provider-gemini/SKILL.md +36 -50
- package/assets/skills/provider-github/SKILL.md +26 -25
- package/assets/skills/provider-hcp/SKILL.md +17 -18
- package/assets/skills/provider-neon/SKILL.md +32 -32
- package/assets/skills/provider-oci/SKILL.md +42 -53
- package/assets/skills/provider-supabase/SKILL.md +21 -16
- package/assets/skills/provider-vercel/SKILL.md +36 -33
- package/dist/bin.js +319 -131
- package/dist/{chunk-P6FRYOOV.js → chunk-OBWWE7GE.js} +14 -8
- package/dist/index.js +1 -1
- package/package.json +5 -5
package/dist/bin.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ConfigSchema,
|
|
4
|
+
MATRIX,
|
|
4
5
|
allPass,
|
|
6
|
+
describeMatrix,
|
|
5
7
|
loadConfig,
|
|
6
8
|
resolveUrl,
|
|
7
9
|
scanSqlFiles,
|
|
8
10
|
verifyAll
|
|
9
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-OBWWE7GE.js";
|
|
10
12
|
import "./chunk-HX7VA25D.js";
|
|
11
13
|
import "./chunk-N3IKUCSF.js";
|
|
12
14
|
import "./chunk-KP3Y6WRU.js";
|
|
@@ -333,11 +335,27 @@ var PACKS = [
|
|
|
333
335
|
"Account \xB7 Cloudflare Tunnel \xB7 Edit (only if a tool uses target: oci)"
|
|
334
336
|
],
|
|
335
337
|
verify: async (t) => {
|
|
338
|
+
const auth = { Authorization: `Bearer ${t}` };
|
|
336
339
|
const r = await fetch("https://api.cloudflare.com/client/v4/user/tokens/verify", {
|
|
337
|
-
headers:
|
|
340
|
+
headers: auth
|
|
338
341
|
});
|
|
339
342
|
const j = await r.json().catch(() => ({}));
|
|
340
|
-
|
|
343
|
+
if (!r.ok || j.result?.status !== "active") {
|
|
344
|
+
return { ok: false, detail: j.result?.status ?? `HTTP ${r.status}` };
|
|
345
|
+
}
|
|
346
|
+
const ar = await fetch("https://api.cloudflare.com/client/v4/accounts?per_page=1", {
|
|
347
|
+
headers: auth
|
|
348
|
+
}).catch(() => null);
|
|
349
|
+
if (ar?.ok) {
|
|
350
|
+
const aj = await ar.json().catch(() => ({}));
|
|
351
|
+
if (Array.isArray(aj.result) && aj.result.length === 0) {
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
detail: "active but no account access \u2014 a Zone-DNS-only token can't resolve account_id; needs Account \xB7 Workers Scripts:Edit + Workers KV Storage:Edit + Account Settings:Read"
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return { ok: true, detail: "active" };
|
|
341
359
|
}
|
|
342
360
|
}
|
|
343
361
|
],
|
|
@@ -583,7 +601,7 @@ function tokensForTool(tool) {
|
|
|
583
601
|
}
|
|
584
602
|
|
|
585
603
|
// src/version.ts
|
|
586
|
-
var MODULE_REF = "v0.
|
|
604
|
+
var MODULE_REF = "v0.5.1";
|
|
587
605
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
588
606
|
function moduleSource(module, ref = MODULE_REF) {
|
|
589
607
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -1063,11 +1081,11 @@ function hiddenPrompter() {
|
|
|
1063
1081
|
if (tty) rl._writeToOutput = () => {
|
|
1064
1082
|
};
|
|
1065
1083
|
return {
|
|
1066
|
-
ask: (query) => new Promise((
|
|
1084
|
+
ask: (query) => new Promise((resolve11) => {
|
|
1067
1085
|
process.stdout.write(query);
|
|
1068
1086
|
rl.question("", (val) => {
|
|
1069
1087
|
process.stdout.write("\n");
|
|
1070
|
-
|
|
1088
|
+
resolve11(val.trim());
|
|
1071
1089
|
});
|
|
1072
1090
|
}),
|
|
1073
1091
|
close: () => rl.close()
|
|
@@ -1166,8 +1184,50 @@ async function gatherSecrets(name, repo, env, prefill) {
|
|
|
1166
1184
|
console.log(`
|
|
1167
1185
|
${pushed} secret(s) pushed to ${repo}. (None written to disk.)`);
|
|
1168
1186
|
}
|
|
1187
|
+
async function secretsCheck(name, repo) {
|
|
1188
|
+
const { config } = await loadManifest();
|
|
1189
|
+
const tools = name ? config.tools.filter((t) => t.name === name) : config.tools;
|
|
1190
|
+
if (name && tools.length === 0) throw new Error(`no tool "${name}" in the manifest`);
|
|
1191
|
+
const present = listGitHubSecrets(repo, void 0);
|
|
1192
|
+
console.log(`Secrets check \u2192 ${repo}`);
|
|
1193
|
+
if (!present) console.log("! could not list secrets (gh unauth/no access) \u2014 names only\n");
|
|
1194
|
+
else console.log("");
|
|
1195
|
+
let missing = 0;
|
|
1196
|
+
for (const t of tools) {
|
|
1197
|
+
const expected = /* @__PURE__ */ new Set();
|
|
1198
|
+
for (const pack of packsForTool({ lane: t.lane, target: t.target, data: t.data })) {
|
|
1199
|
+
for (const tok of pack.tokens) {
|
|
1200
|
+
if (!tok.optional) expected.add(secretKeyFor(tok, t.name, t.tokenOverrides));
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (t.lane === "agent") {
|
|
1204
|
+
expected.add("GEMINI_API_KEY");
|
|
1205
|
+
expected.add("RUN_TOKEN");
|
|
1206
|
+
}
|
|
1207
|
+
for (const s of t.tokens ?? []) expected.add(s);
|
|
1208
|
+
console.log(
|
|
1209
|
+
`\u2500\u2500 ${t.name} (${t.lane}/${t.target}${t.data && t.data !== "none" ? `/${t.data}` : ""})`
|
|
1210
|
+
);
|
|
1211
|
+
for (const key of [...expected].sort()) {
|
|
1212
|
+
const have = present ? present.has(key) : void 0;
|
|
1213
|
+
if (have === false) missing++;
|
|
1214
|
+
console.log(` ${have === void 0 ? "?" : have ? "\u2714" : "\u2718"} ${key}`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (present)
|
|
1218
|
+
console.log(`
|
|
1219
|
+
${missing === 0 ? "\u2714 all required secrets present" : `\u2718 ${missing} missing`}`);
|
|
1220
|
+
process.exit(missing > 0 ? 1 : 0);
|
|
1221
|
+
}
|
|
1169
1222
|
async function secretsCommand(args) {
|
|
1170
1223
|
const sub = args[0];
|
|
1224
|
+
if (sub === "check") {
|
|
1225
|
+
const name = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
|
|
1226
|
+
const repo = flag(args, "--repo") ?? detectRepo(process.cwd());
|
|
1227
|
+
if (!repo) throw new Error("could not determine the repo \u2014 pass --repo owner/repo");
|
|
1228
|
+
await secretsCheck(name, repo);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1171
1231
|
if (sub === "gather") {
|
|
1172
1232
|
const name = args[1];
|
|
1173
1233
|
if (!name || name.startsWith("-")) {
|
|
@@ -1229,7 +1289,23 @@ async function addCommand(args) {
|
|
|
1229
1289
|
}
|
|
1230
1290
|
const lane = flag2(args, "--lane");
|
|
1231
1291
|
const target = flag2(args, "--target");
|
|
1232
|
-
if (!lane || !target)
|
|
1292
|
+
if (!lane || !target) {
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
`add needs --lane and --target. Valid combinations:
|
|
1295
|
+
${describeMatrix()}
|
|
1296
|
+
(defaults: next\u2192vercel; astro/mcp/agent\u2192workers)`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
if (!(lane in MATRIX)) {
|
|
1300
|
+
throw new Error(`unknown lane "${lane}". Valid lanes:
|
|
1301
|
+
${describeMatrix()}`);
|
|
1302
|
+
}
|
|
1303
|
+
const rule = MATRIX[lane];
|
|
1304
|
+
if (!rule.targets.includes(target)) {
|
|
1305
|
+
throw new Error(
|
|
1306
|
+
`lane "${lane}" can't target "${target}" \u2014 valid target(s): ${rule.targets.join(" | ")}`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1233
1309
|
const { config, path } = await loadManifest();
|
|
1234
1310
|
if (path.endsWith(".example.ts")) {
|
|
1235
1311
|
throw new Error("no greenlight.config.ts \u2014 run `greenlight init` first");
|
|
@@ -2066,6 +2142,89 @@ function safeGit(cwd, gitArgs) {
|
|
|
2066
2142
|
}
|
|
2067
2143
|
}
|
|
2068
2144
|
|
|
2145
|
+
// src/commands/bump.ts
|
|
2146
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2147
|
+
import { resolve as resolve7 } from "path";
|
|
2148
|
+
|
|
2149
|
+
// src/refs.ts
|
|
2150
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2151
|
+
import { join as join3 } from "path";
|
|
2152
|
+
var REF_RE = /greenlight\.git\/\/infra\/modules\/[^?"]+\?ref=(v[0-9.]+)/g;
|
|
2153
|
+
function installedVersion(root) {
|
|
2154
|
+
try {
|
|
2155
|
+
const pkg = JSON.parse(
|
|
2156
|
+
readFileSync5(join3(root, "node_modules/@rtrentjones/greenlight/package.json"), "utf8")
|
|
2157
|
+
);
|
|
2158
|
+
return pkg.version;
|
|
2159
|
+
} catch {
|
|
2160
|
+
return void 0;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function infraRefs(root) {
|
|
2164
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2165
|
+
try {
|
|
2166
|
+
for (const f of readdirSync2(join3(root, "infra")).filter((f2) => f2.endsWith(".tf"))) {
|
|
2167
|
+
for (const m of readFileSync5(join3(root, "infra", f), "utf8").matchAll(REF_RE)) {
|
|
2168
|
+
if (m[1]) refs.add(m[1]);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
return [...refs];
|
|
2174
|
+
}
|
|
2175
|
+
function rewriteInfraRefs(root, target) {
|
|
2176
|
+
const want = target.startsWith("v") ? target : `v${target}`;
|
|
2177
|
+
const changed = [];
|
|
2178
|
+
let files;
|
|
2179
|
+
try {
|
|
2180
|
+
files = readdirSync2(join3(root, "infra")).filter((f) => f.endsWith(".tf"));
|
|
2181
|
+
} catch {
|
|
2182
|
+
return changed;
|
|
2183
|
+
}
|
|
2184
|
+
for (const f of files) {
|
|
2185
|
+
const p = join3(root, "infra", f);
|
|
2186
|
+
const body = readFileSync5(p, "utf8");
|
|
2187
|
+
const next = body.replace(
|
|
2188
|
+
/(greenlight\.git\/\/infra\/modules\/[^?"]+\?ref=)v[0-9.]+/g,
|
|
2189
|
+
`$1${want}`
|
|
2190
|
+
);
|
|
2191
|
+
if (next !== body) {
|
|
2192
|
+
writeFileSync4(p, next);
|
|
2193
|
+
changed.push(f);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return changed;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/commands/bump.ts
|
|
2200
|
+
function bumpCommand(_args) {
|
|
2201
|
+
const root = process.cwd();
|
|
2202
|
+
const version = installedVersion(root);
|
|
2203
|
+
if (!version) {
|
|
2204
|
+
throw new Error(
|
|
2205
|
+
"no installed @rtrentjones/greenlight here \u2014 run from a consumer wrapper after `pnpm install`"
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
const want = `v${version}`;
|
|
2209
|
+
const before = infraRefs(root);
|
|
2210
|
+
const changed = rewriteInfraRefs(root, version);
|
|
2211
|
+
if (changed.length) {
|
|
2212
|
+
console.log(`\u2714 re-pinned ${changed.length} infra file(s) \u2192 ${want}: ${changed.join(", ")}`);
|
|
2213
|
+
} else {
|
|
2214
|
+
console.log(`\xB7 infra ?ref already ${want}${before.length ? "" : " (no infra pins)"}`);
|
|
2215
|
+
}
|
|
2216
|
+
const pkgPath = resolve7(root, "package.json");
|
|
2217
|
+
if (existsSync7(pkgPath)) {
|
|
2218
|
+
const raw = readFileSync6(pkgPath, "utf8");
|
|
2219
|
+
const next = raw.replace(/("@rtrentjones\/greenlight":\s*")[^"]+(")/, `$1^${version}$2`);
|
|
2220
|
+
if (next !== raw) {
|
|
2221
|
+
writeFileSync5(pkgPath, next);
|
|
2222
|
+
console.log(`\u2714 package.json dep \u2192 ^${version}`);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
console.log("\nNext: pnpm install && pnpm greenlight doctor --strict, then commit + push.");
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2069
2228
|
// src/commands/config.ts
|
|
2070
2229
|
import { relative } from "path";
|
|
2071
2230
|
async function configCommand() {
|
|
@@ -2077,7 +2236,7 @@ async function configCommand() {
|
|
|
2077
2236
|
|
|
2078
2237
|
// ../packages/adapters/src/index.ts
|
|
2079
2238
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2080
|
-
import { join as
|
|
2239
|
+
import { join as join4 } from "path";
|
|
2081
2240
|
function run(cmd, args, cwd, extraEnv) {
|
|
2082
2241
|
execFileSync3(cmd, args, { cwd, stdio: "inherit", env: { ...process.env, ...extraEnv } });
|
|
2083
2242
|
}
|
|
@@ -2093,7 +2252,7 @@ function workersAdapter(ctx) {
|
|
|
2093
2252
|
siteEnv = void 0;
|
|
2094
2253
|
}
|
|
2095
2254
|
run("pnpm", ["run", "build"], toolDir, siteEnv);
|
|
2096
|
-
return { artifactDir:
|
|
2255
|
+
return { artifactDir: join4(toolDir, "dist") };
|
|
2097
2256
|
},
|
|
2098
2257
|
async deploy(toolDir, env) {
|
|
2099
2258
|
run("pnpm", ["exec", "wrangler", "deploy", "--env", env], toolDir);
|
|
@@ -2195,20 +2354,84 @@ async function deployCommand(args) {
|
|
|
2195
2354
|
// src/commands/doctor.ts
|
|
2196
2355
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2197
2356
|
import { lookup } from "dns/promises";
|
|
2198
|
-
import { existsSync as
|
|
2199
|
-
import { join as
|
|
2357
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
|
|
2358
|
+
import { join as join6 } from "path";
|
|
2359
|
+
|
|
2360
|
+
// src/commands/migrations.ts
|
|
2361
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "fs";
|
|
2362
|
+
import { join as join5 } from "path";
|
|
2363
|
+
var DEFAULT_DIR = "supabase/migrations";
|
|
2364
|
+
var CANDIDATE_DIRS = [
|
|
2365
|
+
DEFAULT_DIR,
|
|
2366
|
+
"migrations",
|
|
2367
|
+
"drizzle/migrations",
|
|
2368
|
+
"drizzle",
|
|
2369
|
+
"db/migrations"
|
|
2370
|
+
];
|
|
2371
|
+
function resolveMigrationsDir(explicit, root = process.cwd()) {
|
|
2372
|
+
if (explicit) return explicit;
|
|
2373
|
+
return CANDIDATE_DIRS.find((d) => existsSync8(join5(root, d))) ?? DEFAULT_DIR;
|
|
2374
|
+
}
|
|
2375
|
+
async function migrationsCommand(args) {
|
|
2376
|
+
if (args[0] !== "scan") {
|
|
2377
|
+
console.log(
|
|
2378
|
+
`usage: greenlight migrations scan [<dir>] [--strict]
|
|
2379
|
+
scan SQL migrations for data-destroying / lock-heavy statements (the pre-apply gate).
|
|
2380
|
+
no <dir> \u2192 auto-detects ${CANDIDATE_DIRS.join(" | ")}. Acknowledge an intentional op with \`-- greenlight:allow\`.`
|
|
2381
|
+
);
|
|
2382
|
+
process.exit(args[0] ? 1 : 0);
|
|
2383
|
+
}
|
|
2384
|
+
const dir = resolveMigrationsDir(args.slice(1).find((a) => !a.startsWith("-")));
|
|
2385
|
+
const strict = args.includes("--strict");
|
|
2386
|
+
let names;
|
|
2387
|
+
try {
|
|
2388
|
+
names = readdirSync3(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
2389
|
+
} catch {
|
|
2390
|
+
console.log(`\xB7 no migrations dir at ${dir} \u2014 nothing to scan`);
|
|
2391
|
+
process.exit(0);
|
|
2392
|
+
}
|
|
2393
|
+
if (names.length === 0) {
|
|
2394
|
+
console.log(`\xB7 no .sql files in ${dir} \u2014 nothing to scan`);
|
|
2395
|
+
process.exit(0);
|
|
2396
|
+
}
|
|
2397
|
+
const files = names.map((f) => ({
|
|
2398
|
+
path: join5(dir, f),
|
|
2399
|
+
content: readFileSync7(join5(dir, f), "utf8")
|
|
2400
|
+
}));
|
|
2401
|
+
const findings = scanSqlFiles(files);
|
|
2402
|
+
if (findings.length === 0) {
|
|
2403
|
+
console.log(`\u2714 migrations scan: ${names.length} file(s) clean (${dir})`);
|
|
2404
|
+
process.exit(0);
|
|
2405
|
+
}
|
|
2406
|
+
for (const f of findings) {
|
|
2407
|
+
console.log(
|
|
2408
|
+
` ${f.severity === "danger" ? "\u2718" : "!"} ${f.file}:${f.line} [${f.rule}] ${f.detail}
|
|
2409
|
+
${f.snippet}`
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
const dangers = findings.filter((f) => f.severity === "danger");
|
|
2413
|
+
const blocking = strict ? findings : dangers;
|
|
2414
|
+
const verdict = blocking.length === 0 ? "\u2714 no blocking findings" : `\u2718 ${blocking.length} blocking finding(s)`;
|
|
2415
|
+
console.log(
|
|
2416
|
+
`
|
|
2417
|
+
${verdict} (${dangers.length} danger, ${findings.length - dangers.length} warn). Acknowledge an intentional op with \`-- greenlight:allow\`.`
|
|
2418
|
+
);
|
|
2419
|
+
process.exit(blocking.length === 0 ? 0 : 1);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// src/commands/doctor.ts
|
|
2200
2423
|
function dirCheck(label, dir) {
|
|
2201
|
-
return
|
|
2424
|
+
return existsSync9(dir) ? { name: `${label}: directory`, status: "ok" } : { name: `${label}: directory`, status: "fail", detail: `missing ${dir}` };
|
|
2202
2425
|
}
|
|
2203
2426
|
function conformanceChecks(t, root) {
|
|
2204
2427
|
const out = [];
|
|
2205
|
-
const toolDir = t.dir ??
|
|
2428
|
+
const toolDir = t.dir ?? join6("tools", t.name);
|
|
2206
2429
|
const specCandidates = t.external ? [
|
|
2207
2430
|
`verify/${t.name}.config.ts`,
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
] : [
|
|
2211
|
-
const found = specCandidates.find((p) =>
|
|
2431
|
+
join6(toolDir, `verify/${t.name}.config.ts`),
|
|
2432
|
+
join6(toolDir, "verify.config.ts")
|
|
2433
|
+
] : [join6(toolDir, "verify.config.ts")];
|
|
2434
|
+
const found = specCandidates.find((p) => existsSync9(join6(root, p)));
|
|
2212
2435
|
out.push({
|
|
2213
2436
|
name: `${t.name}: in the verify loop`,
|
|
2214
2437
|
status: found ? "ok" : "warn",
|
|
@@ -2233,58 +2456,70 @@ function conformanceChecks(t, root) {
|
|
|
2233
2456
|
});
|
|
2234
2457
|
}
|
|
2235
2458
|
if (!t.external && t.lane === "next" && t.target === "vercel") {
|
|
2236
|
-
const wsPath =
|
|
2237
|
-
const ws =
|
|
2459
|
+
const wsPath = join6(root, "pnpm-workspace.yaml");
|
|
2460
|
+
const ws = existsSync9(wsPath) ? readFileSync8(wsPath, "utf8") : "";
|
|
2238
2461
|
const member = ws.includes(toolDir) || /^\s*-\s*["']?tools\/\*/m.test(ws);
|
|
2239
2462
|
out.push({
|
|
2240
2463
|
name: `${t.name}: pnpm workspace member`,
|
|
2241
2464
|
status: member ? "ok" : "warn",
|
|
2242
2465
|
detail: member ? void 0 : `add "${toolDir}" to pnpm-workspace.yaml \u2014 else Vercel's root install skips its deps`
|
|
2243
2466
|
});
|
|
2244
|
-
const hasVercelJson =
|
|
2467
|
+
const hasVercelJson = existsSync9(join6(root, toolDir, "vercel.json"));
|
|
2245
2468
|
out.push({
|
|
2246
2469
|
name: `${t.name}: vercel.json framework`,
|
|
2247
2470
|
status: hasVercelJson ? "ok" : "warn",
|
|
2248
|
-
detail: hasVercelJson ? void 0 : `no ${
|
|
2471
|
+
detail: hasVercelJson ? void 0 : `no ${join6(toolDir, "vercel.json")} (framework: "nextjs") \u2014 Vercel may treat the build as static`
|
|
2249
2472
|
});
|
|
2250
2473
|
}
|
|
2474
|
+
if (t.data === "supabase" || t.data === "neon") {
|
|
2475
|
+
const migBase = join6(root, toolDir);
|
|
2476
|
+
const migDir = resolveMigrationsDir(void 0, migBase);
|
|
2477
|
+
if (existsSync9(join6(migBase, migDir))) {
|
|
2478
|
+
const inWorkflow = [join6(migBase, ".github/workflows"), join6(root, ".github/workflows")].some(
|
|
2479
|
+
(d) => {
|
|
2480
|
+
try {
|
|
2481
|
+
return readdirSync4(d).filter((f) => /\.ya?ml$/.test(f)).some((f) => readFileSync8(join6(d, f), "utf8").includes("migrations scan"));
|
|
2482
|
+
} catch {
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
);
|
|
2487
|
+
const inScripts = (() => {
|
|
2488
|
+
try {
|
|
2489
|
+
const pkg = JSON.parse(readFileSync8(join6(migBase, "package.json"), "utf8"));
|
|
2490
|
+
return Object.values(pkg.scripts ?? {}).some((s) => s.includes("migrations scan"));
|
|
2491
|
+
} catch {
|
|
2492
|
+
return false;
|
|
2493
|
+
}
|
|
2494
|
+
})();
|
|
2495
|
+
const wired = inWorkflow || inScripts;
|
|
2496
|
+
out.push({
|
|
2497
|
+
name: `${t.name}: migrations gate`,
|
|
2498
|
+
status: wired ? "ok" : "warn",
|
|
2499
|
+
detail: wired ? `${migDir} scanned before apply (${inWorkflow ? "CI workflow" : "build script"})` : `${migDir} present but no workflow or build script runs \`greenlight migrations scan\` \u2014 wire the dangerous-SQL gate before the apply step`
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2251
2503
|
return out;
|
|
2252
2504
|
}
|
|
2253
2505
|
function versionDriftCheck(root) {
|
|
2254
2506
|
const name = "framework version drift";
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
readFileSync5(join4(root, "node_modules/@rtrentjones/greenlight/package.json"), "utf8")
|
|
2259
|
-
);
|
|
2260
|
-
installed = pkg.version;
|
|
2261
|
-
} catch {
|
|
2262
|
-
}
|
|
2263
|
-
const refs = /* @__PURE__ */ new Set();
|
|
2264
|
-
try {
|
|
2265
|
-
for (const f of readdirSync2(join4(root, "infra")).filter((f2) => f2.endsWith(".tf"))) {
|
|
2266
|
-
const body = readFileSync5(join4(root, "infra", f), "utf8");
|
|
2267
|
-
for (const m of body.matchAll(/greenlight\.git\/\/infra\/modules\/[^?"]+\?ref=(v[0-9.]+)/g)) {
|
|
2268
|
-
if (m[1]) refs.add(m[1]);
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
} catch {
|
|
2272
|
-
}
|
|
2273
|
-
if (!installed && refs.size === 0) {
|
|
2507
|
+
const installed = installedVersion(root);
|
|
2508
|
+
const refList = infraRefs(root);
|
|
2509
|
+
if (!installed && refList.length === 0) {
|
|
2274
2510
|
return {
|
|
2275
2511
|
name,
|
|
2276
2512
|
status: "skip",
|
|
2277
2513
|
detail: "no installed @rtrentjones/greenlight or infra pins here"
|
|
2278
2514
|
};
|
|
2279
2515
|
}
|
|
2280
|
-
const refList = [...refs];
|
|
2281
2516
|
if (installed) {
|
|
2282
2517
|
const want = `v${installed}`;
|
|
2283
2518
|
const bad = refList.filter((r) => r !== want);
|
|
2284
2519
|
return bad.length === 0 ? { name, status: "ok", detail: `infra pins == installed ${want}` } : {
|
|
2285
2520
|
name,
|
|
2286
2521
|
status: "warn",
|
|
2287
|
-
detail: `installed ${want}, but infra pins ${bad.join(", ")} \u2014
|
|
2522
|
+
detail: `installed ${want}, but infra pins ${bad.join(", ")} \u2014 run \`greenlight bump\``
|
|
2288
2523
|
};
|
|
2289
2524
|
}
|
|
2290
2525
|
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(", ")}` };
|
|
@@ -2307,7 +2542,7 @@ function submoduleDriftCheck(root) {
|
|
|
2307
2542
|
}
|
|
2308
2543
|
function runDoctor(config, root) {
|
|
2309
2544
|
const checks = [];
|
|
2310
|
-
if (config.blog) checks.push(dirCheck("blog",
|
|
2545
|
+
if (config.blog) checks.push(dirCheck("blog", join6(root, "apps/blog")));
|
|
2311
2546
|
for (const t of config.tools) {
|
|
2312
2547
|
if (t.external) {
|
|
2313
2548
|
const url = resolveUrl({
|
|
@@ -2319,7 +2554,7 @@ function runDoctor(config, root) {
|
|
|
2319
2554
|
checks.push({ name: `${t.name}: external (registry)`, status: "ok", detail: url });
|
|
2320
2555
|
if (t.dir) {
|
|
2321
2556
|
checks.push(
|
|
2322
|
-
|
|
2557
|
+
existsSync9(join6(root, t.dir)) ? { name: `${t.name}: dir present`, status: "ok", detail: t.dir } : {
|
|
2323
2558
|
name: `${t.name}: dir present`,
|
|
2324
2559
|
status: "warn",
|
|
2325
2560
|
detail: `declared dir "${t.dir}" missing \u2014 run \`git submodule update --init\``
|
|
@@ -2327,7 +2562,7 @@ function runDoctor(config, root) {
|
|
|
2327
2562
|
);
|
|
2328
2563
|
}
|
|
2329
2564
|
} else {
|
|
2330
|
-
checks.push(dirCheck(t.name,
|
|
2565
|
+
checks.push(dirCheck(t.name, join6(root, t.dir ?? join6("tools", t.name))));
|
|
2331
2566
|
}
|
|
2332
2567
|
checks.push(...conformanceChecks(t, root));
|
|
2333
2568
|
}
|
|
@@ -2394,6 +2629,7 @@ async function doctorCommand(args = []) {
|
|
|
2394
2629
|
process.exit(1);
|
|
2395
2630
|
}
|
|
2396
2631
|
const live = args.includes("--live");
|
|
2632
|
+
const strict = args.includes("--strict");
|
|
2397
2633
|
const checks = runDoctor(config, process.cwd());
|
|
2398
2634
|
if (live) {
|
|
2399
2635
|
console.log(" (probing live prod URLs\u2026)");
|
|
@@ -2403,15 +2639,19 @@ async function doctorCommand(args = []) {
|
|
|
2403
2639
|
console.log(` ${ICON[c.status]} ${c.name}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
|
|
2404
2640
|
}
|
|
2405
2641
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
2406
|
-
|
|
2407
|
-
|
|
2642
|
+
const warned = checks.filter((c) => c.status === "warn").length;
|
|
2643
|
+
console.log(
|
|
2644
|
+
`
|
|
2645
|
+
${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}${warned ? ` \xB7 ${warned} warning(s)` : ""}`
|
|
2646
|
+
);
|
|
2408
2647
|
if (!live) console.log("\xB7 run `greenlight doctor --live` for DNS + reachability probes");
|
|
2409
|
-
|
|
2648
|
+
if (!strict && warned) console.log("\xB7 run `greenlight doctor --strict` to fail on warnings (CI)");
|
|
2649
|
+
process.exit(failed > 0 || strict && warned > 0 ? 1 : 0);
|
|
2410
2650
|
}
|
|
2411
2651
|
|
|
2412
2652
|
// src/commands/init.ts
|
|
2413
|
-
import { existsSync as
|
|
2414
|
-
import { resolve as
|
|
2653
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
2654
|
+
import { resolve as resolve8 } from "path";
|
|
2415
2655
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2416
2656
|
|
|
2417
2657
|
// src/tokens.ts
|
|
@@ -2534,8 +2774,9 @@ jobs:
|
|
|
2534
2774
|
TF_TOKEN_app_terraform_io: \${{ secrets.TF_API_TOKEN }} # HCP state backend auth
|
|
2535
2775
|
GITHUB_TOKEN: \${{ github.token }} # github provider (branch/protection); creates nothing risky
|
|
2536
2776
|
CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
2537
|
-
|
|
2538
|
-
|
|
2777
|
+
# zone/account ids are enumerable identifiers, not secrets \u2014 repo VARIABLES (vars.*)
|
|
2778
|
+
TF_VAR_cloudflare_zone_id: \${{ vars.CLOUDFLARE_ZONE_ID }}
|
|
2779
|
+
TF_VAR_cloudflare_account_id: \${{ vars.CLOUDFLARE_ACCOUNT_ID }}
|
|
2539
2780
|
# vercel (target: vercel tools)
|
|
2540
2781
|
VERCEL_API_TOKEN: \${{ secrets.VERCEL_API_TOKEN }}
|
|
2541
2782
|
# supabase (data: supabase tools)
|
|
@@ -2560,12 +2801,12 @@ jobs:
|
|
|
2560
2801
|
`;
|
|
2561
2802
|
}
|
|
2562
2803
|
function scaffoldIfAbsent(path, contents, label) {
|
|
2563
|
-
if (
|
|
2804
|
+
if (existsSync10(path)) {
|
|
2564
2805
|
console.log(`\xB7 ${label} exists \u2014 left as-is`);
|
|
2565
2806
|
return;
|
|
2566
2807
|
}
|
|
2567
|
-
mkdirSync4(
|
|
2568
|
-
|
|
2808
|
+
mkdirSync4(resolve8(path, ".."), { recursive: true });
|
|
2809
|
+
writeFileSync6(path, contents);
|
|
2569
2810
|
console.log(`\u2714 wrote ${label}`);
|
|
2570
2811
|
}
|
|
2571
2812
|
var TOKEN_FLAGS = {
|
|
@@ -2586,22 +2827,22 @@ async function initCommand(args) {
|
|
|
2586
2827
|
}
|
|
2587
2828
|
if (!domain) throw new Error("a domain is required");
|
|
2588
2829
|
const cwd = process.cwd();
|
|
2589
|
-
const configPath =
|
|
2590
|
-
if (
|
|
2830
|
+
const configPath = resolve8(cwd, "greenlight.config.ts");
|
|
2831
|
+
if (existsSync10(configPath) && !force) {
|
|
2591
2832
|
throw new Error("greenlight.config.ts already exists \u2014 pass --force to overwrite");
|
|
2592
2833
|
}
|
|
2593
|
-
|
|
2834
|
+
writeFileSync6(configPath, scaffoldConfig(domain));
|
|
2594
2835
|
console.log(`\u2714 wrote greenlight.config.ts (domain: ${domain})`);
|
|
2595
2836
|
const repoName = domain.replace(/\./g, "-");
|
|
2596
2837
|
scaffoldIfAbsent(
|
|
2597
|
-
|
|
2838
|
+
resolve8(cwd, ".github/workflows/infra.yml"),
|
|
2598
2839
|
wrapperInfraYml(),
|
|
2599
2840
|
".github/workflows/infra.yml (HCP-backed terraform apply on push)"
|
|
2600
2841
|
);
|
|
2601
|
-
scaffoldIfAbsent(
|
|
2602
|
-
scaffoldIfAbsent(
|
|
2603
|
-
scaffoldIfAbsent(
|
|
2604
|
-
scaffoldIfAbsent(
|
|
2842
|
+
scaffoldIfAbsent(resolve8(cwd, ".gitignore"), WRAPPER_GITIGNORE, ".gitignore");
|
|
2843
|
+
scaffoldIfAbsent(resolve8(cwd, "package.json"), wrapperPackageJson(repoName), "package.json");
|
|
2844
|
+
scaffoldIfAbsent(resolve8(cwd, "mise.toml"), WRAPPER_MISE, "mise.toml");
|
|
2845
|
+
scaffoldIfAbsent(resolve8(cwd, ".node-version"), "24\n", ".node-version");
|
|
2605
2846
|
const repo = flag5(args, "--repo") ?? detectRepo(cwd);
|
|
2606
2847
|
let pushed = 0;
|
|
2607
2848
|
if (repo && !args.includes("--no-push")) {
|
|
@@ -2646,76 +2887,14 @@ Next:
|
|
|
2646
2887
|
4. greenlight verify <name> --env prod | greenlight doctor`);
|
|
2647
2888
|
}
|
|
2648
2889
|
|
|
2649
|
-
// src/commands/migrations.ts
|
|
2650
|
-
import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
|
|
2651
|
-
import { join as join5 } from "path";
|
|
2652
|
-
var DEFAULT_DIR = "supabase/migrations";
|
|
2653
|
-
var CANDIDATE_DIRS = [
|
|
2654
|
-
DEFAULT_DIR,
|
|
2655
|
-
"migrations",
|
|
2656
|
-
"drizzle/migrations",
|
|
2657
|
-
"drizzle",
|
|
2658
|
-
"db/migrations"
|
|
2659
|
-
];
|
|
2660
|
-
function resolveMigrationsDir(explicit, root = process.cwd()) {
|
|
2661
|
-
if (explicit) return explicit;
|
|
2662
|
-
return CANDIDATE_DIRS.find((d) => existsSync9(join5(root, d))) ?? DEFAULT_DIR;
|
|
2663
|
-
}
|
|
2664
|
-
async function migrationsCommand(args) {
|
|
2665
|
-
if (args[0] !== "scan") {
|
|
2666
|
-
console.log(
|
|
2667
|
-
`usage: greenlight migrations scan [<dir>] [--strict]
|
|
2668
|
-
scan SQL migrations for data-destroying / lock-heavy statements (the pre-apply gate).
|
|
2669
|
-
no <dir> \u2192 auto-detects ${CANDIDATE_DIRS.join(" | ")}. Acknowledge an intentional op with \`-- greenlight:allow\`.`
|
|
2670
|
-
);
|
|
2671
|
-
process.exit(args[0] ? 1 : 0);
|
|
2672
|
-
}
|
|
2673
|
-
const dir = resolveMigrationsDir(args.slice(1).find((a) => !a.startsWith("-")));
|
|
2674
|
-
const strict = args.includes("--strict");
|
|
2675
|
-
let names;
|
|
2676
|
-
try {
|
|
2677
|
-
names = readdirSync3(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
2678
|
-
} catch {
|
|
2679
|
-
console.log(`\xB7 no migrations dir at ${dir} \u2014 nothing to scan`);
|
|
2680
|
-
process.exit(0);
|
|
2681
|
-
}
|
|
2682
|
-
if (names.length === 0) {
|
|
2683
|
-
console.log(`\xB7 no .sql files in ${dir} \u2014 nothing to scan`);
|
|
2684
|
-
process.exit(0);
|
|
2685
|
-
}
|
|
2686
|
-
const files = names.map((f) => ({
|
|
2687
|
-
path: join5(dir, f),
|
|
2688
|
-
content: readFileSync6(join5(dir, f), "utf8")
|
|
2689
|
-
}));
|
|
2690
|
-
const findings = scanSqlFiles(files);
|
|
2691
|
-
if (findings.length === 0) {
|
|
2692
|
-
console.log(`\u2714 migrations scan: ${names.length} file(s) clean (${dir})`);
|
|
2693
|
-
process.exit(0);
|
|
2694
|
-
}
|
|
2695
|
-
for (const f of findings) {
|
|
2696
|
-
console.log(
|
|
2697
|
-
` ${f.severity === "danger" ? "\u2718" : "!"} ${f.file}:${f.line} [${f.rule}] ${f.detail}
|
|
2698
|
-
${f.snippet}`
|
|
2699
|
-
);
|
|
2700
|
-
}
|
|
2701
|
-
const dangers = findings.filter((f) => f.severity === "danger");
|
|
2702
|
-
const blocking = strict ? findings : dangers;
|
|
2703
|
-
const verdict = blocking.length === 0 ? "\u2714 no blocking findings" : `\u2718 ${blocking.length} blocking finding(s)`;
|
|
2704
|
-
console.log(
|
|
2705
|
-
`
|
|
2706
|
-
${verdict} (${dangers.length} danger, ${findings.length - dangers.length} warn). Acknowledge an intentional op with \`-- greenlight:allow\`.`
|
|
2707
|
-
);
|
|
2708
|
-
process.exit(blocking.length === 0 ? 0 : 1);
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
2890
|
// src/commands/preview.ts
|
|
2712
2891
|
import { execFileSync as execFileSync5, spawn } from "child_process";
|
|
2713
|
-
import { resolve as
|
|
2892
|
+
import { resolve as resolve10 } from "path";
|
|
2714
2893
|
import { setTimeout as sleep } from "timers/promises";
|
|
2715
2894
|
|
|
2716
2895
|
// src/commands/verify.ts
|
|
2717
2896
|
import { spawnSync } from "child_process";
|
|
2718
|
-
import { resolve as
|
|
2897
|
+
import { resolve as resolve9 } from "path";
|
|
2719
2898
|
function defaultSpec(lane) {
|
|
2720
2899
|
switch (lane) {
|
|
2721
2900
|
case "astro":
|
|
@@ -2843,7 +3022,7 @@ ${pass2 ? "\u2714 ALL PASS" : "\u2718 FAIL"} (${reports2.length} specs)`);
|
|
|
2843
3022
|
if (reachableTimeoutMs > 0) {
|
|
2844
3023
|
console.log(`waiting up to ${reachableTimeoutMs / 1e3}s for ${url} to become reachable\u2026`);
|
|
2845
3024
|
}
|
|
2846
|
-
const toolDir =
|
|
3025
|
+
const toolDir = resolve9(process.cwd(), entry.dir ?? ".");
|
|
2847
3026
|
const reports = await verifyAll(url, specs, { reachableTimeoutMs, toolDir });
|
|
2848
3027
|
attachFailureLogs(reports, specs, toolDir);
|
|
2849
3028
|
for (const report of reports) printReport(report);
|
|
@@ -2887,7 +3066,7 @@ async function verifyLocal(entry, url) {
|
|
|
2887
3066
|
process.env.GREENLIGHT_PREVIEW = "1";
|
|
2888
3067
|
process.env.GREENLIGHT_VERIFY_URL = url;
|
|
2889
3068
|
const specs = await loadSpecs(entry);
|
|
2890
|
-
const toolDir =
|
|
3069
|
+
const toolDir = resolve10(process.cwd(), entry.dir ?? ".");
|
|
2891
3070
|
const reports = await verifyAll(url, specs, { toolDir });
|
|
2892
3071
|
for (const report of reports) printReport(report);
|
|
2893
3072
|
return allPass(reports);
|
|
@@ -2898,7 +3077,7 @@ async function previewViaDescriptor(entry, name, portOverride) {
|
|
|
2898
3077
|
const port = portOverride ?? pv.port ?? entry.port ?? lane.port;
|
|
2899
3078
|
const path = pv.path ?? lane.path;
|
|
2900
3079
|
const url = `http://localhost:${port}${path}`;
|
|
2901
|
-
const toolDir =
|
|
3080
|
+
const toolDir = resolve10(process.cwd(), entry.dir ?? ".");
|
|
2902
3081
|
console.log(`preview ${name}: ${pv.command} (\u2192 ${url})`);
|
|
2903
3082
|
const child = spawn(pv.command, {
|
|
2904
3083
|
cwd: toolDir,
|
|
@@ -3204,6 +3383,7 @@ var HELP = `greenlight <command>
|
|
|
3204
3383
|
|
|
3205
3384
|
init --domain <d> [--cf-token ..] [--force] scaffold manifest + secrets, push to GitHub Actions
|
|
3206
3385
|
add <name> --lane <l> --target <t> [..] scaffold a tool from a lane template + manifest entry
|
|
3386
|
+
lanes list the valid lane \xD7 target \xD7 data combinations
|
|
3207
3387
|
config load & validate the manifest, then print it
|
|
3208
3388
|
deploy <name> --env <env> build + deploy an entry via its target adapter
|
|
3209
3389
|
preview <name> [--port <n>] build + serve locally + verify (one command)
|
|
@@ -3211,10 +3391,12 @@ var HELP = `greenlight <command>
|
|
|
3211
3391
|
promote <name> [--perform] [--push] gated develop -> main fast-forward
|
|
3212
3392
|
status <name> last ship/deploy/verify run for a tool (via gh)
|
|
3213
3393
|
secrets gather <name> [--repo o/r] [--env e] guided, link-first token prompts -> GitHub secrets (no disk/logs)
|
|
3394
|
+
secrets check [<name>] [--repo o/r] list the GitHub secrets a tool's deploy needs + flag missing
|
|
3214
3395
|
agent sync [<name>] write the loop kit (named \u2192 tool-aware, into its dir)
|
|
3215
3396
|
adopt <name> --repo <path> --lane --target onboard an existing tool repo as a thin consumer
|
|
3216
3397
|
migrations scan [<dir>] [--strict] dangerous-SQL gate for migrations (pre-apply)
|
|
3217
|
-
|
|
3398
|
+
bump re-pin a consumer's infra ?ref + dep to the installed version
|
|
3399
|
+
doctor [--live] [--strict] consistency checks (--live: probes; --strict: fail on warnings)
|
|
3218
3400
|
help show this message
|
|
3219
3401
|
|
|
3220
3402
|
Real cloud deploys need the target's creds (e.g. CLOUDFLARE_API_TOKEN); see docs/archive/greenlight-v1.md \xA716.`;
|
|
@@ -3231,6 +3413,10 @@ async function main() {
|
|
|
3231
3413
|
return initCommand(args);
|
|
3232
3414
|
case "add":
|
|
3233
3415
|
return addCommand(args);
|
|
3416
|
+
case "lanes":
|
|
3417
|
+
console.log(`Valid lane \xD7 target \xD7 data combinations:
|
|
3418
|
+
${describeMatrix()}`);
|
|
3419
|
+
return;
|
|
3234
3420
|
case "config":
|
|
3235
3421
|
return configCommand();
|
|
3236
3422
|
case "deploy":
|
|
@@ -3251,6 +3437,8 @@ async function main() {
|
|
|
3251
3437
|
return adoptCommand(args);
|
|
3252
3438
|
case "migrations":
|
|
3253
3439
|
return migrationsCommand(args);
|
|
3440
|
+
case "bump":
|
|
3441
|
+
return bumpCommand(args);
|
|
3254
3442
|
case "doctor":
|
|
3255
3443
|
return doctorCommand(args);
|
|
3256
3444
|
default:
|