@rtrentjones/greenlight 0.3.1 → 0.4.0

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.
@@ -19,9 +19,10 @@ One token, these scopes (a missing scope took down a live apply more than once):
19
19
  - **Account · Cloudflare Tunnel · Edit** — only if a tool uses `target: oci` (the cloudflared
20
20
  tunnel). Without it, the tunnel resource fails with **403 Forbidden** on `cfd_tunnel` at apply.
21
21
 
22
- Create at dash → My Profile → API Tokens → Custom Token. Store in `.greenlight/secrets.env`
23
- (gitignored) and push to GitHub Actions with `greenlight secrets sync`. `greenlight add`
24
- verifies it against `/user/tokens/verify` (status must be `active`) before you commit.
22
+ Create at dash → My Profile → API Tokens → Custom Token. Push it straight to GitHub Actions
23
+ with `greenlight secrets gather` (or `gh secret set CLOUDFLARE_API_TOKEN`) Greenlight keeps
24
+ no local secret file. `greenlight add` verifies it against `/user/tokens/verify` (status must
25
+ be `active`) before you commit.
25
26
 
26
27
  ## Terraform modules
27
28
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: provider-github
3
- description: How GitHub works in a Greenlight setup — secrets sync target (Actions secrets/environments), the repo/branch/protection Terraform module, the develop→main flow, and OIDC-over-PAT preference. Use when syncing tokens, wiring branch protection/environments, or debugging gh/secrets/CI auth.
3
+ description: How GitHub works in a Greenlight setup — the single secret store (Actions secrets/environments), the repo/branch/protection Terraform module, the develop→main flow, and OIDC-over-PAT preference. Use when setting tokens, wiring branch protection/environments, or debugging gh/secrets/CI auth.
4
4
  ---
5
5
 
6
6
  # provider-github
@@ -32,11 +32,12 @@ pushes each to the right repo; see docs/provider-tokens.md):
32
32
  Provider creds (OCI/Cloudflare/…) live **only in the wrapper**; the tool repo holds just the
33
33
  dispatch PAT (its build pushes to GHCR with the built-in `github.token`).
34
34
 
35
- ## Secrets sync
35
+ ## Setting secrets
36
36
 
37
- `greenlight secrets sync [--repo o/r] [--env <env>]` pushes `.greenlight/secrets.env` to the
38
- repo's Actions secrets via `gh` (values piped on stdin never in argv or logs). Run
39
- `gh auth login` first. This is the "init writes to provider stores" piece.
37
+ GitHub Actions secrets are the **single** secret store Greenlight keeps no local secret file.
38
+ `greenlight secrets gather <tool> [--repo o/r] [--env <env>]` prompts the tool's tokens (and the
39
+ always-on base tokens) with hidden input and pipes them straight to `gh secret set` (never on
40
+ disk, never in argv or logs). Run `gh auth login` first. `gh secret set` is the manual alternative.
40
41
 
41
42
  ## Terraform module — `infra/modules/repo`
42
43
 
@@ -12,7 +12,8 @@ name/region are replace-forcing, so the module sets `ignore_changes` to protect
12
12
 
13
13
  ## Token — `SUPABASE_ACCESS_TOKEN`
14
14
 
15
- Dashboard → Account → Access Tokens (Management API). Store in `.greenlight/secrets.env`;
15
+ Dashboard → Account → Access Tokens (Management API). Push it straight to GitHub Actions with
16
+ `greenlight secrets gather` (or `gh secret set`) — Greenlight keeps no local secret file.
16
17
  `greenlight add` verifies it against `/v1/projects` (HTTP 200). The DB password
17
18
  (`TF_VAR_supabase_database_password`) is only used if the project is recreated — ignored on
18
19
  import, so `import-placeholder` is fine for an existing project.
@@ -13,7 +13,8 @@ build). The wrapper owns infra; the tool repo owns deploys.
13
13
  ## Token — `VERCEL_API_TOKEN`
14
14
 
15
15
  Account → Settings → Tokens. **Scope it to the team** that owns the project. The Terraform
16
- `vercel` provider also takes `team` (the `team_…` id). Store in `.greenlight/secrets.env`;
16
+ `vercel` provider also takes `team` (the `team_…` id). Push it straight to GitHub Actions with
17
+ `greenlight secrets gather` (or `gh secret set`) — Greenlight keeps no local secret file.
17
18
  `greenlight add` verifies it against `/v2/user` (HTTP 200) before commit.
18
19
 
19
20
  ## Terraform module — `infra/modules/vercel`
package/dist/bin.js CHANGED
@@ -15,8 +15,8 @@ import "./chunk-XWTOJHLV.js";
15
15
  import "./chunk-QFKE5JKC.js";
16
16
 
17
17
  // src/commands/add.ts
18
- import { cpSync as cpSync2, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync3 } from "fs";
19
- import { join, resolve as resolve6 } from "path";
18
+ import { cpSync as cpSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
19
+ import { join, resolve as resolve5 } from "path";
20
20
 
21
21
  // src/agent-deploy.ts
22
22
  function emitAgentDeployWorkflow(name, domain) {
@@ -590,7 +590,7 @@ function tokensForTool(tool) {
590
590
  }
591
591
 
592
592
  // src/version.ts
593
- var MODULE_REF = "v0.3.1";
593
+ var MODULE_REF = "v0.4.0";
594
594
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
595
595
  function moduleSource(module, ref = MODULE_REF) {
596
596
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -915,27 +915,11 @@ function providersForTool(tool) {
915
915
  return out;
916
916
  }
917
917
 
918
- // src/tokens.ts
919
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
920
- import { resolve as resolve4 } from "path";
921
- import { createInterface as createInterface2 } from "readline/promises";
922
-
923
918
  // src/commands/secrets.ts
924
919
  import { execFileSync } from "child_process";
925
920
  import { existsSync as existsSync3, readFileSync } from "fs";
926
921
  import { resolve as resolve3 } from "path";
927
922
  import { createInterface } from "readline";
928
- function parseSecretsEnv(text) {
929
- const out = [];
930
- for (const raw of text.split("\n")) {
931
- const line = raw.trim();
932
- if (line === "" || line.startsWith("#")) continue;
933
- const eq = line.indexOf("=");
934
- if (eq <= 0) continue;
935
- out.push({ key: line.slice(0, eq).trim(), value: line.slice(eq + 1) });
936
- }
937
- return out;
938
- }
939
923
  function parseOciConfig(text) {
940
924
  const out = {};
941
925
  for (const raw of text.split("\n")) {
@@ -987,49 +971,17 @@ function detectRepo(cwd) {
987
971
  return null;
988
972
  }
989
973
  }
990
- function syncSecrets(opts) {
991
- const repo = opts.repo ?? detectRepo(opts.cwd);
992
- if (!repo) {
993
- throw new Error(
994
- "could not determine the repo \u2014 pass --repo owner/repo (no github.com origin remote)"
995
- );
996
- }
997
- const path = resolve3(opts.cwd, ".greenlight/secrets.env");
998
- if (!existsSync3(path)) {
999
- throw new Error("no .greenlight/secrets.env \u2014 run `greenlight init` with tokens first");
1000
- }
1001
- const entries = parseSecretsEnv(readFileSync(path, "utf8"));
1002
- const target = opts.env ? `env "${opts.env}"` : "repo";
1003
- for (const { key, value } of entries) {
1004
- const ghArgs = ["secret", "set", key, "--repo", repo];
1005
- if (opts.env) ghArgs.push("--env", opts.env);
1006
- try {
1007
- execFileSync("gh", ghArgs, { input: value });
1008
- } catch (e) {
1009
- const err = e;
1010
- if (err.code === "ENOENT") {
1011
- throw new Error("the GitHub CLI `gh` is required \u2014 install it and run `gh auth login`");
1012
- }
1013
- const detail = err.stderr?.toString().trim();
1014
- throw new Error(
1015
- `failed to set ${key}${detail ? `: ${detail}` : " (check `gh auth status`)"}`
1016
- );
1017
- }
1018
- console.log(`\u2714 set ${key} \u2192 ${repo} ${target}`);
1019
- }
1020
- return { repo, count: entries.length };
1021
- }
1022
974
  function hiddenPrompter() {
1023
975
  const tty = Boolean(process.stdin.isTTY);
1024
976
  const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: tty });
1025
977
  if (tty) rl._writeToOutput = () => {
1026
978
  };
1027
979
  return {
1028
- ask: (query) => new Promise((resolve11) => {
980
+ ask: (query) => new Promise((resolve10) => {
1029
981
  process.stdout.write(query);
1030
982
  rl.question("", (val) => {
1031
983
  process.stdout.write("\n");
1032
- resolve11(val.trim());
984
+ resolve10(val.trim());
1033
985
  });
1034
986
  }),
1035
987
  close: () => rl.close()
@@ -1143,108 +1095,77 @@ async function secretsCommand(args) {
1143
1095
  await gatherSecrets(name, repo, flag(args, "--env"), prefill);
1144
1096
  return;
1145
1097
  }
1146
- if (sub !== "sync") {
1147
- console.log(
1148
- "usage:\n greenlight secrets sync [--repo owner/repo] [--env <env>] # push .greenlight/secrets.env\n greenlight secrets gather <name> [--repo owner/repo] [--env <env>] # guided, link-first, straight to GitHub (no disk/logs)\n [--oci-config <path>] [--oci-key <path>] # auto-fill OCI auth from the API-key config preview + .pem"
1149
- );
1150
- process.exit(sub ? 1 : 0);
1151
- }
1152
- const { count } = syncSecrets({
1153
- cwd: process.cwd(),
1154
- repo: flag(args, "--repo"),
1155
- env: flag(args, "--env")
1156
- });
1157
- if (count === 0) {
1158
- console.log("no secrets to sync");
1159
- return;
1160
- }
1161
1098
  console.log(
1162
- `
1163
- ${count} secret(s) synced. (Prefer GitHub OIDC over long-lived tokens where supported.)`
1099
+ "usage:\n greenlight secrets gather <name> [--repo owner/repo] [--env <env>] # guided, link-first, straight to GitHub (no disk/logs)\n [--oci-config <path>] [--oci-key <path>] # auto-fill OCI auth from the API-key config preview + .pem"
1164
1100
  );
1101
+ process.exit(sub ? 1 : 0);
1165
1102
  }
1166
1103
 
1167
1104
  // src/tokens.ts
1168
- var SECRETS_DIR = ".greenlight";
1169
- var SECRETS_FILE = "secrets.env";
1170
- function presentEnv(cwd) {
1105
+ function presentEnv() {
1171
1106
  const out = {};
1172
- const p = resolve4(cwd, SECRETS_DIR, SECRETS_FILE);
1173
- if (existsSync4(p)) {
1174
- for (const { key, value } of parseSecretsEnv(readFileSync2(p, "utf8"))) out[key] = value;
1175
- }
1176
1107
  for (const [k, v] of Object.entries(process.env)) {
1177
- if (v !== void 0 && !(k in out)) out[k] = v;
1108
+ if (v !== void 0) out[k] = v;
1178
1109
  }
1179
1110
  return out;
1180
1111
  }
1181
- function upsertSecret(cwd, key, value) {
1182
- const dir = resolve4(cwd, SECRETS_DIR);
1183
- mkdirSync(dir, { recursive: true });
1184
- const p = resolve4(dir, SECRETS_FILE);
1185
- const lines = existsSync4(p) ? readFileSync2(p, "utf8").split("\n") : [];
1186
- const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
1187
- if (idx >= 0) lines[idx] = `${key}=${value}`;
1188
- else {
1189
- while (lines.length && (lines[lines.length - 1] ?? "").trim() === "") lines.pop();
1190
- lines.push(`${key}=${value}`);
1191
- }
1192
- writeFileSync(p, `${lines.join("\n").replace(/\n*$/, "")}
1193
- `, { mode: 384 });
1194
- }
1195
- async function ensureTokensForTool(cwd, tool, opts = {}) {
1112
+ async function ensureTokensForTool(repo, tool, opts = {}) {
1196
1113
  const doVerify = opts.verify !== false;
1197
- const interactive = Boolean(process.stdin.isTTY);
1198
- const env = presentEnv(cwd);
1114
+ const env = presentEnv();
1115
+ const already = listGitHubSecrets(repo, opts.env);
1199
1116
  const results = [];
1200
- const rl = interactive ? createInterface2({ input: process.stdin, output: process.stdout }) : null;
1117
+ const prompt = process.stdin.isTTY ? hiddenPrompter() : null;
1201
1118
  try {
1202
1119
  for (const spec of tokensForTool(tool)) {
1203
- let value = env[spec.envVar];
1204
- if (value) {
1120
+ const key = secretKeyFor(spec, "", void 0);
1121
+ if (key === "GITHUB_TOKEN") {
1122
+ results.push({ envVar: spec.envVar, outcome: "skipped" });
1123
+ continue;
1124
+ }
1125
+ if (env[spec.envVar] || already?.has(key)) {
1205
1126
  results.push({ envVar: spec.envVar, outcome: "present" });
1206
- } else if (rl) {
1207
- console.log(`
1208
- ${spec.envVar} \u2014 ${spec.label}`);
1209
- if (spec.scopes?.length) console.log(` scopes: ${spec.scopes.join(", ")}`);
1210
- const entered = (await rl.question(` paste value${spec.optional ? " (optional, Enter to skip)" : ""}: `)).trim();
1211
- if (!entered) {
1212
- results.push({ envVar: spec.envVar, outcome: spec.optional ? "skipped" : "missing" });
1213
- continue;
1214
- }
1215
- upsertSecret(cwd, spec.envVar, entered);
1216
- env[spec.envVar] = entered;
1217
- value = entered;
1218
- results.push({ envVar: spec.envVar, outcome: "entered" });
1219
- } else {
1127
+ continue;
1128
+ }
1129
+ if (!prompt) {
1220
1130
  results.push({ envVar: spec.envVar, outcome: spec.optional ? "skipped" : "missing" });
1221
1131
  continue;
1222
1132
  }
1223
- if (value && doVerify && spec.verify) {
1224
- let check;
1133
+ console.log(`
1134
+ ${key} \u2014 ${spec.label}`);
1135
+ if (spec.scopes?.length) console.log(` scopes: ${spec.scopes.join(", ")}`);
1136
+ const entered = await prompt.ask(
1137
+ ` value${spec.optional ? " (optional, Enter to skip)" : ""}: `
1138
+ );
1139
+ if (!entered) {
1140
+ results.push({ envVar: spec.envVar, outcome: spec.optional ? "skipped" : "missing" });
1141
+ continue;
1142
+ }
1143
+ env[spec.envVar] = entered;
1144
+ let check;
1145
+ if (doVerify && spec.verify) {
1225
1146
  try {
1226
- check = await spec.verify(value, env);
1147
+ check = await spec.verify(entered, env);
1227
1148
  } catch (e) {
1228
1149
  check = { ok: false, detail: e instanceof Error ? e.message : String(e) };
1229
1150
  }
1230
- const last = results[results.length - 1];
1231
- if (last) last.verify = check;
1232
1151
  if (!check.ok && !spec.optional) {
1233
1152
  throw new Error(
1234
- `${spec.envVar} failed verification${check.detail ? ` (${check.detail})` : ""} \u2014 check the token's scopes (${spec.label}).`
1153
+ `${key} failed verification${check.detail ? ` (${check.detail})` : ""} \u2014 check the token's scopes (${spec.label}).`
1235
1154
  );
1236
1155
  }
1237
1156
  }
1157
+ setGitHubSecret(repo, opts.env, key, entered);
1158
+ results.push({ envVar: spec.envVar, outcome: "entered", verify: check });
1238
1159
  }
1239
1160
  } finally {
1240
- rl?.close();
1161
+ prompt?.close();
1241
1162
  }
1242
1163
  return results;
1243
1164
  }
1244
1165
 
1245
1166
  // src/commands/agent.ts
1246
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1247
- import { resolve as resolve5 } from "path";
1167
+ import { cpSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1168
+ import { resolve as resolve4 } from "path";
1248
1169
 
1249
1170
  // src/agent-kit.ts
1250
1171
  function recommendedMcp(tool) {
@@ -1278,33 +1199,33 @@ Agentic kit:
1278
1199
  `;
1279
1200
  function materializeAgentKit(dir, tool) {
1280
1201
  const src = skillAssetDir();
1281
- if (!existsSync5(src)) throw new Error(`skill asset not found at ${src}`);
1282
- const dest = resolve5(dir, ".claude/skills/deploy-verify-promote");
1283
- mkdirSync2(dest, { recursive: true });
1202
+ if (!existsSync4(src)) throw new Error(`skill asset not found at ${src}`);
1203
+ const dest = resolve4(dir, ".claude/skills/deploy-verify-promote");
1204
+ mkdirSync(dest, { recursive: true });
1284
1205
  cpSync(src, dest, { recursive: true });
1285
1206
  console.log("\u2714 .claude/skills/deploy-verify-promote/SKILL.md");
1286
1207
  for (const pack of packsForTool(tool)) {
1287
1208
  if (!pack.skill) continue;
1288
1209
  const skillSrc = skillAssetDir(pack.skill);
1289
- if (!existsSync5(skillSrc)) continue;
1290
- const skillDest = resolve5(dir, ".claude/skills", pack.skill);
1291
- mkdirSync2(skillDest, { recursive: true });
1210
+ if (!existsSync4(skillSrc)) continue;
1211
+ const skillDest = resolve4(dir, ".claude/skills", pack.skill);
1212
+ mkdirSync(skillDest, { recursive: true });
1292
1213
  cpSync(skillSrc, skillDest, { recursive: true });
1293
1214
  console.log(`\u2714 .claude/skills/${pack.skill}/SKILL.md`);
1294
1215
  }
1295
- const mcpPath = resolve5(dir, ".mcp.json");
1296
- const existingMcp = existsSync5(mcpPath) ? JSON.parse(readFileSync3(mcpPath, "utf8")) : null;
1216
+ const mcpPath = resolve4(dir, ".mcp.json");
1217
+ const existingMcp = existsSync4(mcpPath) ? JSON.parse(readFileSync2(mcpPath, "utf8")) : null;
1297
1218
  const servers = recommendedMcp(tool);
1298
- writeFileSync2(mcpPath, `${JSON.stringify(mergeMcpServers(existingMcp, servers), null, 2)}
1219
+ writeFileSync(mcpPath, `${JSON.stringify(mergeMcpServers(existingMcp, servers), null, 2)}
1299
1220
  `);
1300
1221
  console.log(`\u2714 .mcp.json (${Object.keys(servers).length} recommended MCP server(s))`);
1301
- const claudePath = resolve5(dir, "CLAUDE.md");
1222
+ const claudePath = resolve4(dir, "CLAUDE.md");
1302
1223
  const marker = "Greenlight loop (deploy \u2192 verify \u2192 promote)";
1303
- const existing = existsSync5(claudePath) ? readFileSync3(claudePath, "utf8") : "";
1224
+ const existing = existsSync4(claudePath) ? readFileSync2(claudePath, "utf8") : "";
1304
1225
  if (existing.includes(marker)) {
1305
1226
  console.log("\xB7 CLAUDE.md already has the loop block");
1306
1227
  } else {
1307
- writeFileSync2(claudePath, existing ? `${existing.trimEnd()}
1228
+ writeFileSync(claudePath, existing ? `${existing.trimEnd()}
1308
1229
 
1309
1230
  ${CLAUDE_BLOCK}` : CLAUDE_BLOCK);
1310
1231
  console.log(`\u2714 CLAUDE.md (${existing ? "appended" : "created"})`);
@@ -1321,7 +1242,7 @@ async function agentCommand(args) {
1321
1242
  if (name) {
1322
1243
  const { config } = await loadManifest();
1323
1244
  const entry = resolveEntry(config, name);
1324
- const dir = resolve5(process.cwd(), entry.dir ?? ".");
1245
+ const dir = resolve4(process.cwd(), entry.dir ?? ".");
1325
1246
  materializeAgentKit(dir, { lane: entry.lane, target: entry.target, data: entry.data });
1326
1247
  console.log(
1327
1248
  `
@@ -1346,25 +1267,25 @@ function templateDir(lane, target) {
1346
1267
  }
1347
1268
  function registerWorkspaceMember(cwd, member) {
1348
1269
  const wsPath = join(cwd, "pnpm-workspace.yaml");
1349
- if (!existsSync6(wsPath)) {
1350
- writeFileSync3(wsPath, `packages:
1270
+ if (!existsSync5(wsPath)) {
1271
+ writeFileSync2(wsPath, `packages:
1351
1272
  - "${member}"
1352
1273
  `);
1353
1274
  console.log(`\u2714 created pnpm-workspace.yaml (member ${member})`);
1354
1275
  return;
1355
1276
  }
1356
- const text = readFileSync4(wsPath, "utf8");
1277
+ const text = readFileSync3(wsPath, "utf8");
1357
1278
  if (text.includes(member) || /^\s*-\s*["']?tools\/\*/m.test(text)) return;
1358
1279
  const lines = text.split("\n");
1359
1280
  const pkgIdx = lines.findIndex((l) => /^packages\s*:/.test(l));
1360
1281
  if (pkgIdx === -1) {
1361
- writeFileSync3(wsPath, `${text.replace(/\s*$/, "")}
1282
+ writeFileSync2(wsPath, `${text.replace(/\s*$/, "")}
1362
1283
  packages:
1363
1284
  - "${member}"
1364
1285
  `);
1365
1286
  } else {
1366
1287
  lines.splice(pkgIdx + 1, 0, ` - "${member}"`);
1367
- writeFileSync3(wsPath, lines.join("\n"));
1288
+ writeFileSync2(wsPath, lines.join("\n"));
1368
1289
  }
1369
1290
  console.log(`\u2714 registered ${member} in pnpm-workspace.yaml`);
1370
1291
  }
@@ -1398,25 +1319,25 @@ async function addCommand(args) {
1398
1319
  const data = entry?.data ?? "none";
1399
1320
  const envs = entry?.envs ?? ["beta", "prod"];
1400
1321
  const toolInfo = { lane, target, data };
1401
- const dest = resolve6(process.cwd(), "tools", name);
1402
- if (existsSync6(dest)) throw new Error(`tools/${name} already exists`);
1322
+ const dest = resolve5(process.cwd(), "tools", name);
1323
+ if (existsSync5(dest)) throw new Error(`tools/${name} already exists`);
1403
1324
  const src = templateDir(lane, target);
1404
- if (existsSync6(src)) {
1325
+ if (existsSync5(src)) {
1405
1326
  cpSync2(src, dest, { recursive: true });
1406
1327
  const pkgPath = join(dest, "package.json");
1407
- if (existsSync6(pkgPath)) {
1408
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
1328
+ if (existsSync5(pkgPath)) {
1329
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
1409
1330
  pkg.name = name;
1410
- writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
1331
+ writeFileSync2(pkgPath, `${JSON.stringify(pkg, null, 2)}
1411
1332
  `);
1412
1333
  }
1413
1334
  const shippedGitignore = join(dest, "gitignore");
1414
- if (existsSync6(shippedGitignore)) renameSync(shippedGitignore, join(dest, ".gitignore"));
1335
+ if (existsSync5(shippedGitignore)) renameSync(shippedGitignore, join(dest, ".gitignore"));
1415
1336
  const wranglerPath = join(dest, "wrangler.toml");
1416
- if (existsSync6(wranglerPath)) {
1417
- let wt = readFileSync4(wranglerPath, "utf8").replaceAll("agent-tool", name).replaceAll("example.dev", config.domain);
1337
+ if (existsSync5(wranglerPath)) {
1338
+ let wt = readFileSync3(wranglerPath, "utf8").replaceAll("agent-tool", name).replaceAll("example.dev", config.domain);
1418
1339
  if (wt.includes("REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID")) {
1419
- const token = presentEnv(process.cwd()).CLOUDFLARE_API_TOKEN;
1340
+ const token = presentEnv().CLOUDFLARE_API_TOKEN;
1420
1341
  const acct = token ? await resolveCloudflareAccountId(config.domain, token) : null;
1421
1342
  if (acct) {
1422
1343
  wt = wt.replaceAll("REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID", acct);
@@ -1427,31 +1348,31 @@ async function addCommand(args) {
1427
1348
  );
1428
1349
  }
1429
1350
  }
1430
- writeFileSync3(wranglerPath, wt);
1351
+ writeFileSync2(wranglerPath, wt);
1431
1352
  }
1432
1353
  console.log(`\u2714 copied ${src} \u2192 tools/${name}`);
1433
- if (existsSync6(pkgPath)) registerWorkspaceMember(process.cwd(), `tools/${name}`);
1354
+ if (existsSync5(pkgPath)) registerWorkspaceMember(process.cwd(), `tools/${name}`);
1434
1355
  } else {
1435
1356
  console.log(`! no template at ${src} \u2014 manifest entry added without scaffolding`);
1436
1357
  }
1437
- writeFileSync3(path, serializeConfig(next));
1358
+ writeFileSync2(path, serializeConfig(next));
1438
1359
  console.log(`\u2714 added "${name}" (${lane}/${target}) to the manifest`);
1439
1360
  const cwd = process.cwd();
1440
1361
  const providers = providersForTool(toolInfo);
1441
- const infraDir = resolve6(cwd, "infra");
1362
+ const infraDir = resolve5(cwd, "infra");
1442
1363
  const mainTf = join(infraDir, "main.tf");
1443
- if (!existsSync6(mainTf)) {
1444
- mkdirSync3(infraDir, { recursive: true });
1445
- writeFileSync3(mainTf, emitWrapperMainTf({ domain: config.domain, providers }));
1364
+ if (!existsSync5(mainTf)) {
1365
+ mkdirSync2(infraDir, { recursive: true });
1366
+ writeFileSync2(mainTf, emitWrapperMainTf({ domain: config.domain, providers }));
1446
1367
  console.log("\u2714 scaffolded infra/main.tf (providers + HCP backend placeholder)");
1447
1368
  } else if (providers.some((p) => p !== "cloudflare" && p !== "github")) {
1448
1369
  console.log(`\xB7 infra/main.tf exists \u2014 ensure it declares provider(s): ${providers.join(", ")}`);
1449
1370
  }
1450
1371
  const toolTf = join(infraDir, `${name}.tf`);
1451
- if (existsSync6(toolTf)) {
1372
+ if (existsSync5(toolTf)) {
1452
1373
  console.log(`\xB7 infra/${name}.tf exists \u2014 left as-is`);
1453
1374
  } else {
1454
- writeFileSync3(
1375
+ writeFileSync2(
1455
1376
  toolTf,
1456
1377
  emitToolTf({
1457
1378
  name,
@@ -1468,13 +1389,13 @@ async function addCommand(args) {
1468
1389
  console.log(`\u2714 wrote infra/${name}.tf (modules: ${providers.join(", ")})`);
1469
1390
  }
1470
1391
  if (lane === "agent") {
1471
- const wfDir = resolve6(cwd, ".github/workflows");
1392
+ const wfDir = resolve5(cwd, ".github/workflows");
1472
1393
  const wfPath = join(wfDir, `deploy-${name}.yml`);
1473
- if (existsSync6(wfPath)) {
1394
+ if (existsSync5(wfPath)) {
1474
1395
  console.log(`\xB7 .github/workflows/deploy-${name}.yml exists \u2014 left as-is`);
1475
1396
  } else {
1476
- mkdirSync3(wfDir, { recursive: true });
1477
- writeFileSync3(wfPath, emitAgentDeployWorkflow(name, config.domain));
1397
+ mkdirSync2(wfDir, { recursive: true });
1398
+ writeFileSync2(wfPath, emitAgentDeployWorkflow(name, config.domain));
1478
1399
  console.log(`\u2714 wrote .github/workflows/deploy-${name}.yml`);
1479
1400
  }
1480
1401
  }
@@ -1500,8 +1421,8 @@ Next:${gather ? "" : `
1500
1421
 
1501
1422
  // src/commands/adopt.ts
1502
1423
  import { execFileSync as execFileSync2 } from "child_process";
1503
- import { cpSync as cpSync3, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync4 } from "fs";
1504
- import { join as join2, resolve as resolve7 } from "path";
1424
+ import { cpSync as cpSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync3 } from "fs";
1425
+ import { join as join2, resolve as resolve6 } from "path";
1505
1426
  var REF = MODULE_REF;
1506
1427
  function flag3(args, name) {
1507
1428
  const i = args.indexOf(name);
@@ -1517,7 +1438,7 @@ function mergePackageJson(existing, repoName, vendor) {
1517
1438
  }
1518
1439
  function vendorDeps(vendorDir) {
1519
1440
  const out = {};
1520
- if (!existsSync7(vendorDir)) return out;
1441
+ if (!existsSync6(vendorDir)) return out;
1521
1442
  for (const f of readdirSync(vendorDir)) {
1522
1443
  if (!f.endsWith(".tgz")) continue;
1523
1444
  const base = f.replace(/-\d+\.\d+\.\d+(-[\w.]+)?\.tgz$/, "");
@@ -1972,12 +1893,12 @@ export default [api, ...agentWeb];
1972
1893
  `;
1973
1894
  }
1974
1895
  function writeIfAbsent(path, contents, label) {
1975
- if (existsSync7(path)) {
1896
+ if (existsSync6(path)) {
1976
1897
  console.log(`\xB7 ${label} exists \u2014 left as-is`);
1977
1898
  return;
1978
1899
  }
1979
- mkdirSync4(resolve7(path, ".."), { recursive: true });
1980
- writeFileSync4(path, contents);
1900
+ mkdirSync3(resolve6(path, ".."), { recursive: true });
1901
+ writeFileSync3(path, contents);
1981
1902
  console.log(`\u2714 ${label}`);
1982
1903
  }
1983
1904
  async function adoptCommand(args) {
@@ -2013,10 +1934,10 @@ async function adoptWrapper(ctx) {
2013
1934
  const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
2014
1935
  const cwd = process.cwd();
2015
1936
  const toolRel = `tools/${name}`;
2016
- const dest = resolve7(cwd, toolRel);
1937
+ const dest = resolve6(cwd, toolRel);
2017
1938
  console.log(`adopting "${name}" (${lane}/${target}) as the submodule ${toolRel}
2018
1939
  `);
2019
- if (!existsSync7(dest)) {
1940
+ if (!existsSync6(dest)) {
2020
1941
  try {
2021
1942
  execFileSync2("git", ["submodule", "add", repoArg, toolRel], { cwd, stdio: "inherit" });
2022
1943
  console.log(`\u2714 git submodule add ${repoArg} ${toolRel}`);
@@ -2052,7 +1973,7 @@ async function adoptWrapper(ctx) {
2052
1973
  }
2053
1974
  } : {}
2054
1975
  });
2055
- writeFileSync4(regPath, serializeConfig(nextReg));
1976
+ writeFileSync3(regPath, serializeConfig(nextReg));
2056
1977
  console.log(
2057
1978
  `\u2714 ${existed ? "updated" : "registered"} "${name}" (external, dir ${toolRel}) in the wrapper manifest`
2058
1979
  );
@@ -2070,7 +1991,7 @@ async function adoptWrapper(ctx) {
2070
1991
  );
2071
1992
  }
2072
1993
  const providers = providersForTool({ lane, target, data });
2073
- if (existsSync7(join2(cwd, "infra/main.tf")) && providers.some((p) => p !== "cloudflare" && p !== "github")) {
1994
+ if (existsSync6(join2(cwd, "infra/main.tf")) && providers.some((p) => p !== "cloudflare" && p !== "github")) {
2074
1995
  console.log(`\xB7 ensure infra/main.tf declares provider(s): ${providers.join(", ")}`);
2075
1996
  }
2076
1997
  materializeAgentKit(dest, { lane, target, data });
@@ -2122,9 +2043,9 @@ Next:
2122
2043
  }
2123
2044
  async function adoptStandalone(ctx) {
2124
2045
  const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
2125
- const repo = resolve7(process.cwd(), repoArg);
2126
- if (!existsSync7(repo)) throw new Error(`no such repo: ${repo} (--standalone needs a local path)`);
2127
- const regVendor = resolve7(process.cwd(), "vendor");
2046
+ const repo = resolve6(process.cwd(), repoArg);
2047
+ if (!existsSync6(repo)) throw new Error(`no such repo: ${repo} (--standalone needs a local path)`);
2048
+ const regVendor = resolve6(process.cwd(), "vendor");
2128
2049
  const vendor = vendorDeps(regVendor);
2129
2050
  if (Object.keys(vendor).length === 0) {
2130
2051
  throw new Error(
@@ -2142,15 +2063,15 @@ async function adoptStandalone(ctx) {
2142
2063
  );
2143
2064
  const slug = parseRepo(safeGit(repo, ["remote", "get-url", "origin"])) ?? `OWNER/${name}`;
2144
2065
  const pkgPath = join2(repo, "package.json");
2145
- const existingPkg = existsSync7(pkgPath) ? JSON.parse(readFileSync5(pkgPath, "utf8")) : null;
2146
- writeFileSync4(
2066
+ const existingPkg = existsSync6(pkgPath) ? JSON.parse(readFileSync4(pkgPath, "utf8")) : null;
2067
+ writeFileSync3(
2147
2068
  pkgPath,
2148
2069
  `${JSON.stringify(mergePackageJson(existingPkg, name, vendor), null, 2)}
2149
2070
  `
2150
2071
  );
2151
2072
  console.log("\u2714 package.json (merged framework deps + overrides)");
2152
2073
  const repoVendor = join2(repo, "vendor");
2153
- mkdirSync4(repoVendor, { recursive: true });
2074
+ mkdirSync3(repoVendor, { recursive: true });
2154
2075
  for (const f of readdirSync(regVendor)) {
2155
2076
  if (f.endsWith(".tgz")) cpSync3(join2(regVendor, f), join2(repoVendor, f));
2156
2077
  }
@@ -2184,7 +2105,7 @@ async function adoptStandalone(ctx) {
2184
2105
  external: true,
2185
2106
  adopted: true
2186
2107
  });
2187
- writeFileSync4(regPath, serializeConfig(nextReg));
2108
+ writeFileSync3(regPath, serializeConfig(nextReg));
2188
2109
  console.log(`\u2714 registered "${name}" in ${regPath.replace(`${process.cwd()}/`, "")} (external)`);
2189
2110
  console.log(`
2190
2111
  Next (in the adopted repo):
@@ -2197,20 +2118,20 @@ Note: deploying ${target} needs the ${target} adapter (workers is built; oci/ver
2197
2118
  }
2198
2119
  function addGreenlightScript(dir) {
2199
2120
  const pkgPath = join2(dir, "package.json");
2200
- if (!existsSync7(pkgPath)) {
2121
+ if (!existsSync6(pkgPath)) {
2201
2122
  console.log(
2202
2123
  "\xB7 no package.json (non-Node tool) \u2014 run the loop via `npx @rtrentjones/greenlight`"
2203
2124
  );
2204
2125
  return;
2205
2126
  }
2206
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
2127
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
2207
2128
  pkg.scripts = { ...pkg.scripts ?? {} };
2208
2129
  if (pkg.scripts.greenlight) {
2209
2130
  console.log("\xB7 package.json already has a greenlight script");
2210
2131
  return;
2211
2132
  }
2212
2133
  pkg.scripts.greenlight = "npx @rtrentjones/greenlight";
2213
- writeFileSync4(pkgPath, `${JSON.stringify(pkg, null, 2)}
2134
+ writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
2214
2135
  `);
2215
2136
  console.log("\u2714 package.json (greenlight script \u2014 runnable via npx post-publish)");
2216
2137
  }
@@ -2355,10 +2276,10 @@ async function deployCommand(args) {
2355
2276
  // src/commands/doctor.ts
2356
2277
  import { execFileSync as execFileSync4 } from "child_process";
2357
2278
  import { lookup } from "dns/promises";
2358
- import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
2279
+ import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
2359
2280
  import { join as join4 } from "path";
2360
2281
  function dirCheck(label, dir) {
2361
- return existsSync8(dir) ? { name: `${label}: directory`, status: "ok" } : { name: `${label}: directory`, status: "fail", detail: `missing ${dir}` };
2282
+ return existsSync7(dir) ? { name: `${label}: directory`, status: "ok" } : { name: `${label}: directory`, status: "fail", detail: `missing ${dir}` };
2362
2283
  }
2363
2284
  function conformanceChecks(t, root) {
2364
2285
  const out = [];
@@ -2368,7 +2289,7 @@ function conformanceChecks(t, root) {
2368
2289
  join4(toolDir, `verify/${t.name}.config.ts`),
2369
2290
  join4(toolDir, "verify.config.ts")
2370
2291
  ] : [join4(toolDir, "verify.config.ts")];
2371
- const found = specCandidates.find((p) => existsSync8(join4(root, p)));
2292
+ const found = specCandidates.find((p) => existsSync7(join4(root, p)));
2372
2293
  out.push({
2373
2294
  name: `${t.name}: in the verify loop`,
2374
2295
  status: found ? "ok" : "warn",
@@ -2394,14 +2315,14 @@ function conformanceChecks(t, root) {
2394
2315
  }
2395
2316
  if (!t.external && t.lane === "next" && t.target === "vercel") {
2396
2317
  const wsPath = join4(root, "pnpm-workspace.yaml");
2397
- const ws = existsSync8(wsPath) ? readFileSync6(wsPath, "utf8") : "";
2318
+ const ws = existsSync7(wsPath) ? readFileSync5(wsPath, "utf8") : "";
2398
2319
  const member = ws.includes(toolDir) || /^\s*-\s*["']?tools\/\*/m.test(ws);
2399
2320
  out.push({
2400
2321
  name: `${t.name}: pnpm workspace member`,
2401
2322
  status: member ? "ok" : "warn",
2402
2323
  detail: member ? void 0 : `add "${toolDir}" to pnpm-workspace.yaml \u2014 else Vercel's root install skips its deps`
2403
2324
  });
2404
- const hasVercelJson = existsSync8(join4(root, toolDir, "vercel.json"));
2325
+ const hasVercelJson = existsSync7(join4(root, toolDir, "vercel.json"));
2405
2326
  out.push({
2406
2327
  name: `${t.name}: vercel.json framework`,
2407
2328
  status: hasVercelJson ? "ok" : "warn",
@@ -2415,7 +2336,7 @@ function versionDriftCheck(root) {
2415
2336
  let installed;
2416
2337
  try {
2417
2338
  const pkg = JSON.parse(
2418
- readFileSync6(join4(root, "node_modules/@rtrentjones/greenlight/package.json"), "utf8")
2339
+ readFileSync5(join4(root, "node_modules/@rtrentjones/greenlight/package.json"), "utf8")
2419
2340
  );
2420
2341
  installed = pkg.version;
2421
2342
  } catch {
@@ -2423,7 +2344,7 @@ function versionDriftCheck(root) {
2423
2344
  const refs = /* @__PURE__ */ new Set();
2424
2345
  try {
2425
2346
  for (const f of readdirSync2(join4(root, "infra")).filter((f2) => f2.endsWith(".tf"))) {
2426
- const body = readFileSync6(join4(root, "infra", f), "utf8");
2347
+ const body = readFileSync5(join4(root, "infra", f), "utf8");
2427
2348
  for (const m of body.matchAll(/greenlight\.git\/\/infra\/modules\/[^?"]+\?ref=(v[0-9.]+)/g)) {
2428
2349
  if (m[1]) refs.add(m[1]);
2429
2350
  }
@@ -2479,7 +2400,7 @@ function runDoctor(config, root) {
2479
2400
  checks.push({ name: `${t.name}: external (registry)`, status: "ok", detail: url });
2480
2401
  if (t.dir) {
2481
2402
  checks.push(
2482
- existsSync8(join4(root, t.dir)) ? { name: `${t.name}: dir present`, status: "ok", detail: t.dir } : {
2403
+ existsSync7(join4(root, t.dir)) ? { name: `${t.name}: dir present`, status: "ok", detail: t.dir } : {
2483
2404
  name: `${t.name}: dir present`,
2484
2405
  status: "warn",
2485
2406
  detail: `declared dir "${t.dir}" missing \u2014 run \`git submodule update --init\``
@@ -2570,9 +2491,9 @@ ${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}`);
2570
2491
  }
2571
2492
 
2572
2493
  // src/commands/init.ts
2573
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
2574
- import { resolve as resolve8 } from "path";
2575
- import { createInterface as createInterface3 } from "readline/promises";
2494
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2495
+ import { resolve as resolve7 } from "path";
2496
+ import { createInterface as createInterface2 } from "readline/promises";
2576
2497
  function flag5(args, name) {
2577
2498
  const i = args.indexOf(name);
2578
2499
  return i >= 0 ? args[i + 1] : void 0;
@@ -2594,7 +2515,7 @@ function wrapperPackageJson(name) {
2594
2515
  }
2595
2516
  var WRAPPER_GITIGNORE = `# Greenlight wrapper
2596
2517
  node_modules/
2597
- .greenlight/ # gathered tokens \u2014 never committed
2518
+ .greenlight/ # local scratch \u2014 never committed (tokens live in GitHub Actions)
2598
2519
  .terraform/
2599
2520
  *.tfplan
2600
2521
  tf.plan
@@ -2656,12 +2577,12 @@ jobs:
2656
2577
  `;
2657
2578
  }
2658
2579
  function scaffoldIfAbsent(path, contents, label) {
2659
- if (existsSync9(path)) {
2580
+ if (existsSync8(path)) {
2660
2581
  console.log(`\xB7 ${label} exists \u2014 left as-is`);
2661
2582
  return;
2662
2583
  }
2663
- mkdirSync5(resolve8(path, ".."), { recursive: true });
2664
- writeFileSync5(path, contents);
2584
+ mkdirSync4(resolve7(path, ".."), { recursive: true });
2585
+ writeFileSync4(path, contents);
2665
2586
  console.log(`\u2714 wrote ${label}`);
2666
2587
  }
2667
2588
  var TOKEN_FLAGS = {
@@ -2676,70 +2597,74 @@ async function initCommand(args) {
2676
2597
  let domain = flag5(args, "--domain");
2677
2598
  if (!domain) {
2678
2599
  if (!process.stdin.isTTY) throw new Error("init needs --domain <domain> (no TTY for prompts)");
2679
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
2600
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2680
2601
  domain = (await rl.question("Domain (e.g. example.dev): ")).trim();
2681
2602
  rl.close();
2682
2603
  }
2683
2604
  if (!domain) throw new Error("a domain is required");
2684
2605
  const cwd = process.cwd();
2685
- const configPath = resolve8(cwd, "greenlight.config.ts");
2686
- if (existsSync9(configPath) && !force) {
2606
+ const configPath = resolve7(cwd, "greenlight.config.ts");
2607
+ if (existsSync8(configPath) && !force) {
2687
2608
  throw new Error("greenlight.config.ts already exists \u2014 pass --force to overwrite");
2688
2609
  }
2689
- writeFileSync5(configPath, scaffoldConfig(domain));
2610
+ writeFileSync4(configPath, scaffoldConfig(domain));
2690
2611
  console.log(`\u2714 wrote greenlight.config.ts (domain: ${domain})`);
2691
2612
  const repoName = domain.replace(/\./g, "-");
2692
2613
  scaffoldIfAbsent(
2693
- resolve8(cwd, ".github/workflows/infra.yml"),
2614
+ resolve7(cwd, ".github/workflows/infra.yml"),
2694
2615
  wrapperInfraYml(),
2695
2616
  ".github/workflows/infra.yml (HCP-backed terraform apply on push)"
2696
2617
  );
2697
- scaffoldIfAbsent(resolve8(cwd, ".gitignore"), WRAPPER_GITIGNORE, ".gitignore");
2698
- scaffoldIfAbsent(resolve8(cwd, "package.json"), wrapperPackageJson(repoName), "package.json");
2699
- scaffoldIfAbsent(resolve8(cwd, "mise.toml"), WRAPPER_MISE, "mise.toml");
2700
- scaffoldIfAbsent(resolve8(cwd, ".node-version"), "24\n", ".node-version");
2701
- const secrets = [];
2702
- for (const [f, key] of Object.entries(TOKEN_FLAGS)) {
2703
- const v = flag5(args, f);
2704
- if (v) secrets.push(`${key}=${v}`);
2705
- }
2706
- if (secrets.length > 0) {
2707
- mkdirSync5(resolve8(cwd, ".greenlight"), { recursive: true });
2708
- writeFileSync5(resolve8(cwd, ".greenlight/secrets.env"), `${secrets.join("\n")}
2709
- `, {
2710
- mode: 384
2711
- });
2712
- console.log(`\u2714 wrote .greenlight/secrets.env (${secrets.length} token(s), gitignored)`);
2713
- }
2714
- if (process.stdin.isTTY && !args.includes("--no-tokens")) {
2715
- try {
2716
- await ensureTokensForTool(cwd, {}, { verify: !args.includes("--no-verify") });
2717
- } catch (e) {
2718
- console.log(`\u2716 ${e instanceof Error ? e.message : String(e)}`);
2618
+ scaffoldIfAbsent(resolve7(cwd, ".gitignore"), WRAPPER_GITIGNORE, ".gitignore");
2619
+ scaffoldIfAbsent(resolve7(cwd, "package.json"), wrapperPackageJson(repoName), "package.json");
2620
+ scaffoldIfAbsent(resolve7(cwd, "mise.toml"), WRAPPER_MISE, "mise.toml");
2621
+ scaffoldIfAbsent(resolve7(cwd, ".node-version"), "24\n", ".node-version");
2622
+ const repo = flag5(args, "--repo") ?? detectRepo(cwd);
2623
+ let pushed = 0;
2624
+ if (repo && !args.includes("--no-push")) {
2625
+ for (const [f, key] of Object.entries(TOKEN_FLAGS)) {
2626
+ const v = flag5(args, f);
2627
+ if (!v || key.startsWith("GITHUB_")) continue;
2628
+ try {
2629
+ setGitHubSecret(repo, void 0, key, v);
2630
+ console.log(`\u2714 set ${key} \u2192 ${repo} (GitHub Actions)`);
2631
+ pushed++;
2632
+ } catch (e) {
2633
+ console.log(`! could not set ${key}: ${e instanceof Error ? e.message : String(e)}`);
2634
+ }
2719
2635
  }
2720
2636
  }
2721
- let pushed = false;
2722
- if (existsSync9(resolve8(cwd, ".greenlight/secrets.env")) && !args.includes("--no-push")) {
2723
- try {
2724
- const { repo, count } = syncSecrets({ cwd, repo: flag5(args, "--repo") });
2725
- console.log(`\u2714 pushed ${count} secret(s) to ${repo} (GitHub Actions)`);
2726
- pushed = true;
2727
- } catch (e) {
2728
- console.log(`! skipped pushing secrets: ${e instanceof Error ? e.message : String(e)}`);
2729
- console.log(" run `greenlight secrets sync` once `gh` is authenticated.");
2637
+ if (process.stdin.isTTY && !args.includes("--no-tokens")) {
2638
+ if (repo) {
2639
+ try {
2640
+ const results = await ensureTokensForTool(
2641
+ repo,
2642
+ {},
2643
+ {
2644
+ verify: !args.includes("--no-verify")
2645
+ }
2646
+ );
2647
+ pushed += results.filter((r) => r.outcome === "entered").length;
2648
+ } catch (e) {
2649
+ console.log(`\u2716 ${e instanceof Error ? e.message : String(e)}`);
2650
+ }
2651
+ } else {
2652
+ console.log(
2653
+ "\n\xB7 no GitHub repo detected yet \u2014 create it + `gh auth login`, then set the base secrets\n (CLOUDFLARE_API_TOKEN, TF_API_TOKEN) via `greenlight add <tool>` (prompts them) or `gh secret set`."
2654
+ );
2730
2655
  }
2731
2656
  }
2732
2657
  console.log(`
2733
2658
  Next:
2734
2659
  1. greenlight add <name> --lane <lane> --target <target> # scaffold a tool, emit infra, and
2735
- # gather THAT tool's keys \u2192 GitHub${pushed ? "" : "\n (run `greenlight secrets sync` if base tokens were not pushed)"}
2660
+ # gather THAT tool's keys \u2192 GitHub${pushed ? "" : "\n (it also prompts the base tokens if they are not set yet)"}
2736
2661
  2. set the HCP backend (cloud{} org + workspace) in infra/main.tf # docs/terraform-state.md
2737
2662
  3. commit + push \u2192 CI (.github/workflows/infra.yml) runs \`terraform apply\`
2738
2663
  4. greenlight verify <name> --env prod | greenlight doctor`);
2739
2664
  }
2740
2665
 
2741
2666
  // src/commands/migrations.ts
2742
- import { existsSync as existsSync10, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "fs";
2667
+ import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
2743
2668
  import { join as join5 } from "path";
2744
2669
  var DEFAULT_DIR = "supabase/migrations";
2745
2670
  var CANDIDATE_DIRS = [
@@ -2751,7 +2676,7 @@ var CANDIDATE_DIRS = [
2751
2676
  ];
2752
2677
  function resolveMigrationsDir(explicit, root = process.cwd()) {
2753
2678
  if (explicit) return explicit;
2754
- return CANDIDATE_DIRS.find((d) => existsSync10(join5(root, d))) ?? DEFAULT_DIR;
2679
+ return CANDIDATE_DIRS.find((d) => existsSync9(join5(root, d))) ?? DEFAULT_DIR;
2755
2680
  }
2756
2681
  async function migrationsCommand(args) {
2757
2682
  if (args[0] !== "scan") {
@@ -2777,7 +2702,7 @@ async function migrationsCommand(args) {
2777
2702
  }
2778
2703
  const files = names.map((f) => ({
2779
2704
  path: join5(dir, f),
2780
- content: readFileSync7(join5(dir, f), "utf8")
2705
+ content: readFileSync6(join5(dir, f), "utf8")
2781
2706
  }));
2782
2707
  const findings = scanSqlFiles(files);
2783
2708
  if (findings.length === 0) {
@@ -2802,12 +2727,12 @@ ${verdict} (${dangers.length} danger, ${findings.length - dangers.length} warn).
2802
2727
 
2803
2728
  // src/commands/preview.ts
2804
2729
  import { execFileSync as execFileSync5, spawn } from "child_process";
2805
- import { resolve as resolve10 } from "path";
2730
+ import { resolve as resolve9 } from "path";
2806
2731
  import { setTimeout as sleep } from "timers/promises";
2807
2732
 
2808
2733
  // src/commands/verify.ts
2809
2734
  import { spawnSync } from "child_process";
2810
- import { resolve as resolve9 } from "path";
2735
+ import { resolve as resolve8 } from "path";
2811
2736
  function defaultSpec(lane) {
2812
2737
  switch (lane) {
2813
2738
  case "astro":
@@ -2935,7 +2860,7 @@ ${pass2 ? "\u2714 ALL PASS" : "\u2718 FAIL"} (${reports2.length} specs)`);
2935
2860
  if (reachableTimeoutMs > 0) {
2936
2861
  console.log(`waiting up to ${reachableTimeoutMs / 1e3}s for ${url} to become reachable\u2026`);
2937
2862
  }
2938
- const toolDir = resolve9(process.cwd(), entry.dir ?? ".");
2863
+ const toolDir = resolve8(process.cwd(), entry.dir ?? ".");
2939
2864
  const reports = await verifyAll(url, specs, { reachableTimeoutMs, toolDir });
2940
2865
  attachFailureLogs(reports, specs, toolDir);
2941
2866
  for (const report of reports) printReport(report);
@@ -2979,7 +2904,7 @@ async function verifyLocal(entry, url) {
2979
2904
  process.env.GREENLIGHT_PREVIEW = "1";
2980
2905
  process.env.GREENLIGHT_VERIFY_URL = url;
2981
2906
  const specs = await loadSpecs(entry);
2982
- const toolDir = resolve10(process.cwd(), entry.dir ?? ".");
2907
+ const toolDir = resolve9(process.cwd(), entry.dir ?? ".");
2983
2908
  const reports = await verifyAll(url, specs, { toolDir });
2984
2909
  for (const report of reports) printReport(report);
2985
2910
  return allPass(reports);
@@ -2990,7 +2915,7 @@ async function previewViaDescriptor(entry, name, portOverride) {
2990
2915
  const port = portOverride ?? pv.port ?? entry.port ?? lane.port;
2991
2916
  const path = pv.path ?? lane.path;
2992
2917
  const url = `http://localhost:${port}${path}`;
2993
- const toolDir = resolve10(process.cwd(), entry.dir ?? ".");
2918
+ const toolDir = resolve9(process.cwd(), entry.dir ?? ".");
2994
2919
  console.log(`preview ${name}: ${pv.command} (\u2192 ${url})`);
2995
2920
  const child = spawn(pv.command, {
2996
2921
  cwd: toolDir,
@@ -3303,7 +3228,6 @@ var HELP = `greenlight <command>
3303
3228
  promote <name> [--perform] [--push] gated develop -> main fast-forward
3304
3229
  status <name> last ship/deploy/verify run for a tool (via gh)
3305
3230
  secrets gather <name> [--repo o/r] [--env e] guided, link-first token prompts -> GitHub secrets (no disk/logs)
3306
- secrets sync [--repo o/r] [--env <env>] push .greenlight/secrets.env -> GitHub Actions secrets
3307
3231
  agent sync [<name>] write the loop kit (named \u2192 tool-aware, into its dir)
3308
3232
  adopt <name> --repo <path> --lane --target onboard an existing tool repo as a thin consumer
3309
3233
  migrations scan [<dir>] [--strict] dangerous-SQL gate for migrations (pre-apply)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
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.3.1",
35
- "@rtrentjones/greenlight-loop": "0.3.1",
36
- "@rtrentjones/greenlight-shared": "0.3.1",
37
- "@rtrentjones/greenlight-verify": "0.3.1"
34
+ "@rtrentjones/greenlight-loop": "0.4.0",
35
+ "@rtrentjones/greenlight-adapters": "0.4.0",
36
+ "@rtrentjones/greenlight-verify": "0.4.0",
37
+ "@rtrentjones/greenlight-shared": "0.4.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-assets.mjs && tsup",