@rtrentjones/greenlight 0.2.23 → 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/bin.js CHANGED
@@ -4,8 +4,9 @@ import {
4
4
  allPass,
5
5
  loadConfig,
6
6
  resolveUrl,
7
+ scanSqlFiles,
7
8
  verifyAll
8
- } from "./chunk-TFWXR7PP.js";
9
+ } from "./chunk-2A7ZBBYN.js";
9
10
  import "./chunk-HX7VA25D.js";
10
11
  import "./chunk-N3IKUCSF.js";
11
12
  import "./chunk-KP3Y6WRU.js";
@@ -443,7 +444,7 @@ function tokensForTool(tool) {
443
444
  }
444
445
 
445
446
  // src/version.ts
446
- var MODULE_REF = "v0.2.23";
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 # write the loop skill + .mcp.json + CLAUDE.md block"
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."
@@ -1992,6 +2005,7 @@ async function deployCommand(args) {
1992
2005
 
1993
2006
  // src/commands/doctor.ts
1994
2007
  import { execFileSync as execFileSync4 } from "child_process";
2008
+ import { lookup } from "dns/promises";
1995
2009
  import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
1996
2010
  import { join as join4 } from "path";
1997
2011
  function dirCheck(label, dir) {
@@ -2120,19 +2134,52 @@ function runDoctor(config, root) {
2120
2134
  });
2121
2135
  checks.push(versionDriftCheck(root));
2122
2136
  checks.push(submoduleDriftCheck(root));
2123
- for (const name of [
2124
- "DNS propagation",
2125
- "terraform drift",
2126
- "Vercel cap headroom",
2127
- "keepalive health (live)",
2128
- "OCI PAYG status"
2129
- ]) {
2130
- checks.push({ name, status: "skip", detail: "needs provider creds / packages (Phase 5/7/8)" });
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
+ });
2131
2178
  }
2132
2179
  return checks;
2133
2180
  }
2134
2181
  var ICON = { ok: "\u2714", warn: "!", fail: "\u2718", skip: "\xB7" };
2135
- async function doctorCommand() {
2182
+ async function doctorCommand(args = []) {
2136
2183
  let config;
2137
2184
  try {
2138
2185
  ({ config } = await loadManifest());
@@ -2141,13 +2188,19 @@ async function doctorCommand() {
2141
2188
  console.error(`\u2718 manifest: ${e instanceof Error ? e.message : String(e)}`);
2142
2189
  process.exit(1);
2143
2190
  }
2191
+ const live = args.includes("--live");
2144
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
+ }
2145
2197
  for (const c of checks) {
2146
2198
  console.log(` ${ICON[c.status]} ${c.name}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
2147
2199
  }
2148
2200
  const failed = checks.filter((c) => c.status === "fail").length;
2149
2201
  console.log(`
2150
2202
  ${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}`);
2203
+ if (!live) console.log("\xB7 run `greenlight doctor --live` for DNS + reachability probes");
2151
2204
  process.exit(failed === 0 ? 0 : 1);
2152
2205
  }
2153
2206
 
@@ -2403,6 +2456,57 @@ Next:
2403
2456
  4. greenlight verify <name> --env prod | greenlight doctor`);
2404
2457
  }
2405
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
+
2406
2510
  // src/commands/preview.ts
2407
2511
  import { execFileSync as execFileSync5, spawn } from "child_process";
2408
2512
  import { resolve as resolve10 } from "path";
@@ -2900,9 +3004,10 @@ var HELP = `greenlight <command>
2900
3004
  status <name> last ship/deploy/verify run for a tool (via gh)
2901
3005
  secrets gather <name> [--repo o/r] [--env e] guided, link-first token prompts -> GitHub secrets (no disk/logs)
2902
3006
  secrets sync [--repo o/r] [--env <env>] push .greenlight/secrets.env -> GitHub Actions secrets
2903
- agent sync write the loop skill + CLAUDE.md block into this repo
3007
+ agent sync [<name>] write the loop kit (named \u2192 tool-aware, into its dir)
2904
3008
  adopt <name> --repo <path> --lane --target onboard an existing tool repo as a thin consumer
2905
- doctor manifest + repo consistency checks
3009
+ migrations scan [<dir>] [--strict] dangerous-SQL gate for migrations (pre-apply)
3010
+ doctor [--live] consistency checks (--live: DNS + reachability probes)
2906
3011
  help show this message
2907
3012
 
2908
3013
  Real cloud deploys need the target's creds (e.g. CLOUDFLARE_API_TOKEN); see docs/archive/greenlight-v1.md \xA716.`;
@@ -2937,8 +3042,10 @@ async function main() {
2937
3042
  return agentCommand(args);
2938
3043
  case "adopt":
2939
3044
  return adoptCommand(args);
3045
+ case "migrations":
3046
+ return migrationsCommand(args);
2940
3047
  case "doctor":
2941
- return doctorCommand();
3048
+ return doctorCommand(args);
2942
3049
  default:
2943
3050
  throw new Error(`Unknown command "${cmd}".
2944
3051
 
@@ -131,6 +131,105 @@ 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
 
@@ -339,6 +438,7 @@ export {
339
438
  defineConfig,
340
439
  loadConfig,
341
440
  resolveUrl,
441
+ scanSqlFiles,
342
442
  defineVerify,
343
443
  verifyAll,
344
444
  allPass
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  defineConfig,
3
3
  defineVerify,
4
4
  loadConfig
5
- } from "./chunk-TFWXR7PP.js";
5
+ } from "./chunk-2A7ZBBYN.js";
6
6
  import "./chunk-HX7VA25D.js";
7
7
  import "./chunk-N3IKUCSF.js";
8
8
  import "./chunk-KP3Y6WRU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.23",
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-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"
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",