@rtrentjones/greenlight 0.4.0 → 0.4.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/dist/bin.js CHANGED
@@ -62,6 +62,12 @@ jobs:
62
62
  RUN_TOKEN: \${{ secrets.RUN_TOKEN }}
63
63
  run: |
64
64
  cd tools/${name}
65
+ # Account id as code: resolve the (non-secret) account id from the domain's zone and inject
66
+ # it into wrangler.toml \u2014 wrangler can't call /memberships to auto-discover it with a scoped
67
+ # token. Derived, so the repo keeps the REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID placeholder.
68
+ ACCT=$(curl -fsS "https://api.cloudflare.com/client/v4/zones?name=${domain}" -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | jq -r '.result[0].account.id // empty')
69
+ if [ -z "$ACCT" ]; then echo "::error::could not resolve the Cloudflare account id for ${domain} (token needs Zone:Read?)"; exit 1; fi
70
+ sed -i "s/REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID/$ACCT/g" wrangler.toml
65
71
  # KV namespace as code: find-or-create the STATE namespace (idempotent), then inject its id
66
72
  # into wrangler.toml for this deploy. The id is non-secret + derived, so the repo keeps the
67
73
  # REPLACE_WITH_KV_NAMESPACE_ID placeholder \u2014 no manual create, no hardcoded id.
@@ -93,19 +99,6 @@ jobs:
93
99
  run: echo "Missing CLOUDFLARE_API_TOKEN or GEMINI_API_KEY \u2014 ${name} deploy skipped."
94
100
  `;
95
101
  }
96
- async function resolveCloudflareAccountId(domain, token) {
97
- try {
98
- const res = await fetch(
99
- `https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent(domain)}`,
100
- { headers: { Authorization: `Bearer ${token}` } }
101
- );
102
- if (!res.ok) return null;
103
- const data = await res.json();
104
- return data.result?.[0]?.account?.id ?? null;
105
- } catch {
106
- return null;
107
- }
108
- }
109
102
 
110
103
  // src/asset-paths.ts
111
104
  import { existsSync } from "fs";
@@ -489,7 +482,7 @@ var PACKS = [
489
482
  id: "github",
490
483
  name: "GitHub",
491
484
  always: true,
492
- // secrets sync + repo/branch infra
485
+ // the single secret store (Actions secrets) + repo/branch infra
493
486
  appliesTo: () => true,
494
487
  guide: "docs/provider-tokens.md \u2014 GitHub (gh auth, or a fine-grained PAT)",
495
488
  setupUrl: "https://github.com/settings/personal-access-tokens/new",
@@ -590,7 +583,7 @@ function tokensForTool(tool) {
590
583
  }
591
584
 
592
585
  // src/version.ts
593
- var MODULE_REF = "v0.4.0";
586
+ var MODULE_REF = "v0.4.1";
594
587
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
595
588
  function moduleSource(module, ref = MODULE_REF) {
596
589
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -915,10 +908,103 @@ function providersForTool(tool) {
915
908
  return out;
916
909
  }
917
910
 
911
+ // src/commands/agent.ts
912
+ import { cpSync, existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
913
+ import { resolve as resolve3 } from "path";
914
+
915
+ // src/agent-kit.ts
916
+ function recommendedMcp(tool) {
917
+ return mcpForTool(tool);
918
+ }
919
+ function mergeMcpServers(existing, add) {
920
+ const out = { mcpServers: { ...existing?.mcpServers ?? {} } };
921
+ for (const [name, val] of Object.entries(add)) {
922
+ if (out.mcpServers[name]) continue;
923
+ out.mcpServers[name] = typeof val === "string" ? { type: "http", url: val } : val;
924
+ }
925
+ return out;
926
+ }
927
+
928
+ // src/commands/agent.ts
929
+ var CLAUDE_BLOCK = `## Greenlight loop (deploy \u2192 verify \u2192 promote)
930
+
931
+ This repo uses Greenlight. Deliver every change through the ONE model (same shape for web + MCP
932
+ tools \u2014 the deploy-verify-promote skill has the lane\xD7target matrix):
933
+ branch \u2192 change \u2192 \`greenlight preview <name>\` (local gate) \u2192 add it to the tool's verify.config \u2192
934
+ push (CI gates on the tool's own tests) \u2192 deploy \u2192 \`greenlight verify <name> --env prod\`.
935
+ Web tools also get beta + \`greenlight promote\`; oci is direct-to-prod (the local gate is the
936
+ pre-prod safety). \`greenlight status <name>\` shows the run chain; \`greenlight doctor\` flags drift.
937
+
938
+ Agentic kit:
939
+ - Skill: \`.claude/skills/deploy-verify-promote/SKILL.md\` (the one model + the matrix).
940
+ - MCP servers: \`.mcp.json\` recommends the relevant providers \u2014 run \`/mcp\` to authenticate.
941
+ Vercel is OAuth; Supabase needs \`SUPABASE_ACCESS_TOKEN\` (+ \`SUPABASE_PROJECT_REF\`) in your env.
942
+ - Best-practice skills (one-time, user scope):
943
+ \`claude plugin marketplace add cloudflare/skills && claude plugin install cloudflare@cloudflare\`
944
+ `;
945
+ function materializeAgentKit(dir, tool) {
946
+ const src = skillAssetDir();
947
+ if (!existsSync3(src)) throw new Error(`skill asset not found at ${src}`);
948
+ const dest = resolve3(dir, ".claude/skills/deploy-verify-promote");
949
+ mkdirSync(dest, { recursive: true });
950
+ cpSync(src, dest, { recursive: true });
951
+ console.log("\u2714 .claude/skills/deploy-verify-promote/SKILL.md");
952
+ for (const pack of packsForTool(tool)) {
953
+ if (!pack.skill) continue;
954
+ const skillSrc = skillAssetDir(pack.skill);
955
+ if (!existsSync3(skillSrc)) continue;
956
+ const skillDest = resolve3(dir, ".claude/skills", pack.skill);
957
+ mkdirSync(skillDest, { recursive: true });
958
+ cpSync(skillSrc, skillDest, { recursive: true });
959
+ console.log(`\u2714 .claude/skills/${pack.skill}/SKILL.md`);
960
+ }
961
+ const mcpPath = resolve3(dir, ".mcp.json");
962
+ const existingMcp = existsSync3(mcpPath) ? JSON.parse(readFileSync(mcpPath, "utf8")) : null;
963
+ const servers = recommendedMcp(tool);
964
+ writeFileSync(mcpPath, `${JSON.stringify(mergeMcpServers(existingMcp, servers), null, 2)}
965
+ `);
966
+ console.log(`\u2714 .mcp.json (${Object.keys(servers).length} recommended MCP server(s))`);
967
+ const claudePath = resolve3(dir, "CLAUDE.md");
968
+ const marker = "Greenlight loop (deploy \u2192 verify \u2192 promote)";
969
+ const existing = existsSync3(claudePath) ? readFileSync(claudePath, "utf8") : "";
970
+ if (existing.includes(marker)) {
971
+ console.log("\xB7 CLAUDE.md already has the loop block");
972
+ } else {
973
+ writeFileSync(claudePath, existing ? `${existing.trimEnd()}
974
+
975
+ ${CLAUDE_BLOCK}` : CLAUDE_BLOCK);
976
+ console.log(`\u2714 CLAUDE.md (${existing ? "appended" : "created"})`);
977
+ }
978
+ }
979
+ async function agentCommand(args) {
980
+ if (args[0] !== "sync") {
981
+ console.log(
982
+ "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"
983
+ );
984
+ process.exit(args[0] ? 1 : 0);
985
+ }
986
+ const name = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
987
+ if (name) {
988
+ const { config } = await loadManifest();
989
+ const entry = resolveEntry(config, name);
990
+ const dir = resolve3(process.cwd(), entry.dir ?? ".");
991
+ materializeAgentKit(dir, { lane: entry.lane, target: entry.target, data: entry.data });
992
+ console.log(
993
+ `
994
+ Synced the kit for "${name}" \u2192 ${entry.dir ?? "."} (lane=${entry.lane}, target=${entry.target}, data=${entry.data}).`
995
+ );
996
+ return;
997
+ }
998
+ materializeAgentKit(process.cwd());
999
+ console.log(
1000
+ "\nNote: the Greenlight Claude Code plugin (user scope) is the preferred path; this sync is the fallback.\nRun `/mcp` to authenticate the MCP servers."
1001
+ );
1002
+ }
1003
+
918
1004
  // src/commands/secrets.ts
919
1005
  import { execFileSync } from "child_process";
920
- import { existsSync as existsSync3, readFileSync } from "fs";
921
- import { resolve as resolve3 } from "path";
1006
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1007
+ import { resolve as resolve4 } from "path";
922
1008
  import { createInterface } from "readline";
923
1009
  function parseOciConfig(text) {
924
1010
  const out = {};
@@ -933,7 +1019,7 @@ function parseOciConfig(text) {
933
1019
  return out;
934
1020
  }
935
1021
  function ociPrefill(configPath, keyPath) {
936
- const cfg = parseOciConfig(readFileSync(configPath, "utf8"));
1022
+ const cfg = parseOciConfig(readFileSync2(configPath, "utf8"));
937
1023
  const map = /* @__PURE__ */ new Map();
938
1024
  const set = (k, v) => {
939
1025
  if (v) map.set(k, v);
@@ -943,8 +1029,8 @@ function ociPrefill(configPath, keyPath) {
943
1029
  set("TF_VAR_OCI_TENANCY_OCID", cfg.tenancy);
944
1030
  set("TF_VAR_OCI_REGION", cfg.region);
945
1031
  const pem = keyPath ?? cfg.key_file;
946
- if (pem && existsSync3(pem)) {
947
- map.set("TF_VAR_OCI_PRIVATE_KEY", readFileSync(pem, "utf8"));
1032
+ if (pem && existsSync4(pem)) {
1033
+ map.set("TF_VAR_OCI_PRIVATE_KEY", readFileSync2(pem, "utf8"));
948
1034
  } else if (pem) {
949
1035
  console.log(` ! PEM not found at ${pem} \u2014 set TF_VAR_OCI_PRIVATE_KEY manually (--oci-key)`);
950
1036
  }
@@ -1091,7 +1177,7 @@ async function secretsCommand(args) {
1091
1177
  if (!repo) throw new Error("could not determine the repo \u2014 pass --repo owner/repo");
1092
1178
  const ociConfig2 = flag(args, "--oci-config");
1093
1179
  const ociKey = flag(args, "--oci-key");
1094
- const prefill = ociConfig2 ? ociPrefill(resolve3(process.cwd(), ociConfig2), ociKey && resolve3(process.cwd(), ociKey)) : void 0;
1180
+ const prefill = ociConfig2 ? ociPrefill(resolve4(process.cwd(), ociConfig2), ociKey && resolve4(process.cwd(), ociKey)) : void 0;
1095
1181
  await gatherSecrets(name, repo, flag(args, "--env"), prefill);
1096
1182
  return;
1097
1183
  }
@@ -1101,161 +1187,6 @@ async function secretsCommand(args) {
1101
1187
  process.exit(sub ? 1 : 0);
1102
1188
  }
1103
1189
 
1104
- // src/tokens.ts
1105
- function presentEnv() {
1106
- const out = {};
1107
- for (const [k, v] of Object.entries(process.env)) {
1108
- if (v !== void 0) out[k] = v;
1109
- }
1110
- return out;
1111
- }
1112
- async function ensureTokensForTool(repo, tool, opts = {}) {
1113
- const doVerify = opts.verify !== false;
1114
- const env = presentEnv();
1115
- const already = listGitHubSecrets(repo, opts.env);
1116
- const results = [];
1117
- const prompt = process.stdin.isTTY ? hiddenPrompter() : null;
1118
- try {
1119
- for (const spec of tokensForTool(tool)) {
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)) {
1126
- results.push({ envVar: spec.envVar, outcome: "present" });
1127
- continue;
1128
- }
1129
- if (!prompt) {
1130
- results.push({ envVar: spec.envVar, outcome: spec.optional ? "skipped" : "missing" });
1131
- continue;
1132
- }
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) {
1146
- try {
1147
- check = await spec.verify(entered, env);
1148
- } catch (e) {
1149
- check = { ok: false, detail: e instanceof Error ? e.message : String(e) };
1150
- }
1151
- if (!check.ok && !spec.optional) {
1152
- throw new Error(
1153
- `${key} failed verification${check.detail ? ` (${check.detail})` : ""} \u2014 check the token's scopes (${spec.label}).`
1154
- );
1155
- }
1156
- }
1157
- setGitHubSecret(repo, opts.env, key, entered);
1158
- results.push({ envVar: spec.envVar, outcome: "entered", verify: check });
1159
- }
1160
- } finally {
1161
- prompt?.close();
1162
- }
1163
- return results;
1164
- }
1165
-
1166
- // src/commands/agent.ts
1167
- import { cpSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1168
- import { resolve as resolve4 } from "path";
1169
-
1170
- // src/agent-kit.ts
1171
- function recommendedMcp(tool) {
1172
- return mcpForTool(tool);
1173
- }
1174
- function mergeMcpServers(existing, add) {
1175
- const out = { mcpServers: { ...existing?.mcpServers ?? {} } };
1176
- for (const [name, val] of Object.entries(add)) {
1177
- if (out.mcpServers[name]) continue;
1178
- out.mcpServers[name] = typeof val === "string" ? { type: "http", url: val } : val;
1179
- }
1180
- return out;
1181
- }
1182
-
1183
- // src/commands/agent.ts
1184
- var CLAUDE_BLOCK = `## Greenlight loop (deploy \u2192 verify \u2192 promote)
1185
-
1186
- This repo uses Greenlight. Deliver every change through the ONE model (same shape for web + MCP
1187
- tools \u2014 the deploy-verify-promote skill has the lane\xD7target matrix):
1188
- branch \u2192 change \u2192 \`greenlight preview <name>\` (local gate) \u2192 add it to the tool's verify.config \u2192
1189
- push (CI gates on the tool's own tests) \u2192 deploy \u2192 \`greenlight verify <name> --env prod\`.
1190
- Web tools also get beta + \`greenlight promote\`; oci is direct-to-prod (the local gate is the
1191
- pre-prod safety). \`greenlight status <name>\` shows the run chain; \`greenlight doctor\` flags drift.
1192
-
1193
- Agentic kit:
1194
- - Skill: \`.claude/skills/deploy-verify-promote/SKILL.md\` (the one model + the matrix).
1195
- - MCP servers: \`.mcp.json\` recommends the relevant providers \u2014 run \`/mcp\` to authenticate.
1196
- Vercel is OAuth; Supabase needs \`SUPABASE_ACCESS_TOKEN\` (+ \`SUPABASE_PROJECT_REF\`) in your env.
1197
- - Best-practice skills (one-time, user scope):
1198
- \`claude plugin marketplace add cloudflare/skills && claude plugin install cloudflare@cloudflare\`
1199
- `;
1200
- function materializeAgentKit(dir, tool) {
1201
- const src = skillAssetDir();
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 });
1205
- cpSync(src, dest, { recursive: true });
1206
- console.log("\u2714 .claude/skills/deploy-verify-promote/SKILL.md");
1207
- for (const pack of packsForTool(tool)) {
1208
- if (!pack.skill) continue;
1209
- const skillSrc = skillAssetDir(pack.skill);
1210
- if (!existsSync4(skillSrc)) continue;
1211
- const skillDest = resolve4(dir, ".claude/skills", pack.skill);
1212
- mkdirSync(skillDest, { recursive: true });
1213
- cpSync(skillSrc, skillDest, { recursive: true });
1214
- console.log(`\u2714 .claude/skills/${pack.skill}/SKILL.md`);
1215
- }
1216
- const mcpPath = resolve4(dir, ".mcp.json");
1217
- const existingMcp = existsSync4(mcpPath) ? JSON.parse(readFileSync2(mcpPath, "utf8")) : null;
1218
- const servers = recommendedMcp(tool);
1219
- writeFileSync(mcpPath, `${JSON.stringify(mergeMcpServers(existingMcp, servers), null, 2)}
1220
- `);
1221
- console.log(`\u2714 .mcp.json (${Object.keys(servers).length} recommended MCP server(s))`);
1222
- const claudePath = resolve4(dir, "CLAUDE.md");
1223
- const marker = "Greenlight loop (deploy \u2192 verify \u2192 promote)";
1224
- const existing = existsSync4(claudePath) ? readFileSync2(claudePath, "utf8") : "";
1225
- if (existing.includes(marker)) {
1226
- console.log("\xB7 CLAUDE.md already has the loop block");
1227
- } else {
1228
- writeFileSync(claudePath, existing ? `${existing.trimEnd()}
1229
-
1230
- ${CLAUDE_BLOCK}` : CLAUDE_BLOCK);
1231
- console.log(`\u2714 CLAUDE.md (${existing ? "appended" : "created"})`);
1232
- }
1233
- }
1234
- async function agentCommand(args) {
1235
- if (args[0] !== "sync") {
1236
- console.log(
1237
- "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"
1238
- );
1239
- process.exit(args[0] ? 1 : 0);
1240
- }
1241
- const name = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
1242
- if (name) {
1243
- const { config } = await loadManifest();
1244
- const entry = resolveEntry(config, name);
1245
- const dir = resolve4(process.cwd(), entry.dir ?? ".");
1246
- materializeAgentKit(dir, { lane: entry.lane, target: entry.target, data: entry.data });
1247
- console.log(
1248
- `
1249
- Synced the kit for "${name}" \u2192 ${entry.dir ?? "."} (lane=${entry.lane}, target=${entry.target}, data=${entry.data}).`
1250
- );
1251
- return;
1252
- }
1253
- materializeAgentKit(process.cwd());
1254
- console.log(
1255
- "\nNote: the Greenlight Claude Code plugin (user scope) is the preferred path; this sync is the fallback.\nRun `/mcp` to authenticate the MCP servers."
1256
- );
1257
- }
1258
-
1259
1190
  // src/commands/add.ts
1260
1191
  function flag2(args, name) {
1261
1192
  const i = args.indexOf(name);
@@ -1335,19 +1266,7 @@ async function addCommand(args) {
1335
1266
  if (existsSync5(shippedGitignore)) renameSync(shippedGitignore, join(dest, ".gitignore"));
1336
1267
  const wranglerPath = join(dest, "wrangler.toml");
1337
1268
  if (existsSync5(wranglerPath)) {
1338
- let wt = readFileSync3(wranglerPath, "utf8").replaceAll("agent-tool", name).replaceAll("example.dev", config.domain);
1339
- if (wt.includes("REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID")) {
1340
- const token = presentEnv().CLOUDFLARE_API_TOKEN;
1341
- const acct = token ? await resolveCloudflareAccountId(config.domain, token) : null;
1342
- if (acct) {
1343
- wt = wt.replaceAll("REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID", acct);
1344
- console.log("\u2714 resolved the Cloudflare account id into wrangler.toml");
1345
- } else {
1346
- console.log(
1347
- "\xB7 could not resolve the Cloudflare account id \u2014 set account_id in wrangler.toml"
1348
- );
1349
- }
1350
- }
1269
+ const wt = readFileSync3(wranglerPath, "utf8").replaceAll("agent-tool", name).replaceAll("example.dev", config.domain);
1351
1270
  writeFileSync2(wranglerPath, wt);
1352
1271
  }
1353
1272
  console.log(`\u2714 copied ${src} \u2192 tools/${name}`);
@@ -2494,6 +2413,70 @@ ${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}`);
2494
2413
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2495
2414
  import { resolve as resolve7 } from "path";
2496
2415
  import { createInterface as createInterface2 } from "readline/promises";
2416
+
2417
+ // src/tokens.ts
2418
+ function presentEnv() {
2419
+ const out = {};
2420
+ for (const [k, v] of Object.entries(process.env)) {
2421
+ if (v !== void 0) out[k] = v;
2422
+ }
2423
+ return out;
2424
+ }
2425
+ async function ensureTokensForTool(repo, tool, opts = {}) {
2426
+ const doVerify = opts.verify !== false;
2427
+ const env = presentEnv();
2428
+ const already = listGitHubSecrets(repo, opts.env);
2429
+ const results = [];
2430
+ const prompt = process.stdin.isTTY ? hiddenPrompter() : null;
2431
+ try {
2432
+ for (const spec of tokensForTool(tool)) {
2433
+ const key = secretKeyFor(spec, "", void 0);
2434
+ if (key === "GITHUB_TOKEN") {
2435
+ results.push({ envVar: spec.envVar, outcome: "skipped" });
2436
+ continue;
2437
+ }
2438
+ if (env[spec.envVar] || already?.has(key)) {
2439
+ results.push({ envVar: spec.envVar, outcome: "present" });
2440
+ continue;
2441
+ }
2442
+ if (!prompt) {
2443
+ results.push({ envVar: spec.envVar, outcome: spec.optional ? "skipped" : "missing" });
2444
+ continue;
2445
+ }
2446
+ console.log(`
2447
+ ${key} \u2014 ${spec.label}`);
2448
+ if (spec.scopes?.length) console.log(` scopes: ${spec.scopes.join(", ")}`);
2449
+ const entered = await prompt.ask(
2450
+ ` value${spec.optional ? " (optional, Enter to skip)" : ""}: `
2451
+ );
2452
+ if (!entered) {
2453
+ results.push({ envVar: spec.envVar, outcome: spec.optional ? "skipped" : "missing" });
2454
+ continue;
2455
+ }
2456
+ env[spec.envVar] = entered;
2457
+ let check;
2458
+ if (doVerify && spec.verify) {
2459
+ try {
2460
+ check = await spec.verify(entered, env);
2461
+ } catch (e) {
2462
+ check = { ok: false, detail: e instanceof Error ? e.message : String(e) };
2463
+ }
2464
+ if (!check.ok && !spec.optional) {
2465
+ throw new Error(
2466
+ `${key} failed verification${check.detail ? ` (${check.detail})` : ""} \u2014 check the token's scopes (${spec.label}).`
2467
+ );
2468
+ }
2469
+ }
2470
+ setGitHubSecret(repo, opts.env, key, entered);
2471
+ results.push({ envVar: spec.envVar, outcome: "entered", verify: check });
2472
+ }
2473
+ } finally {
2474
+ prompt?.close();
2475
+ }
2476
+ return results;
2477
+ }
2478
+
2479
+ // src/commands/init.ts
2497
2480
  function flag5(args, name) {
2498
2481
  const i = args.indexOf(name);
2499
2482
  return i >= 0 ? args[i + 1] : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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.4.0",
35
- "@rtrentjones/greenlight-adapters": "0.4.0",
36
- "@rtrentjones/greenlight-verify": "0.4.0",
37
- "@rtrentjones/greenlight-shared": "0.4.0"
34
+ "@rtrentjones/greenlight-adapters": "0.4.1",
35
+ "@rtrentjones/greenlight-verify": "0.4.1",
36
+ "@rtrentjones/greenlight-loop": "0.4.1",
37
+ "@rtrentjones/greenlight-shared": "0.4.1"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-assets.mjs && tsup",
@@ -1,10 +1,10 @@
1
- # Agent Worker. `greenlight add` rewrites `name` + the route domain + the account_id, and emits a
2
- # .github/workflows/deploy-<name>.yml that (on push to main) creates the KV namespace, deploys, sets
3
- # the GEMINI_API_KEY + RUN_TOKEN Worker secrets from GitHub secrets, seeds, and verifies. So you only
4
- # add those two GitHub secrets — no manual wrangler.
1
+ # Agent Worker. `greenlight add` rewrites `name` + the route domain, and emits a
2
+ # .github/workflows/deploy-<name>.yml that (on push to main) resolves the account id + KV namespace,
3
+ # deploys, sets the GEMINI_API_KEY + RUN_TOKEN Worker secrets from GitHub secrets, seeds, and verifies.
4
+ # So you only add those two GitHub secrets — no manual wrangler, no local secrets.
5
5
  name = "agent-tool"
6
- # Non-secret account id (committed config). `greenlight add` resolves + fills this from your domain's
7
- # zone; without it wrangler calls /memberships, which a scoped API token can't do.
6
+ # Non-secret account id. The emitted deploy workflow resolves it from your domain's zone in CI and
7
+ # fills this placeholder (wrangler can't call /memberships to auto-discover it with a scoped token).
8
8
  account_id = "REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID"
9
9
  main = "src/index.ts"
10
10
  compatibility_date = "2025-06-01"