@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 +172 -189
- package/package.json +5 -5
- package/templates/_template-agent/wrangler.toml +6 -6
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
|
|
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.
|
|
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
|
|
921
|
-
import { resolve as
|
|
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(
|
|
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 &&
|
|
947
|
-
map.set("TF_VAR_OCI_PRIVATE_KEY",
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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-
|
|
35
|
-
"@rtrentjones/greenlight-
|
|
36
|
-
"@rtrentjones/greenlight-
|
|
37
|
-
"@rtrentjones/greenlight-shared": "0.4.
|
|
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
|
|
2
|
-
# .github/workflows/deploy-<name>.yml that (on push to main)
|
|
3
|
-
# the GEMINI_API_KEY + RUN_TOKEN Worker secrets from GitHub secrets, seeds, and verifies.
|
|
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
|
|
7
|
-
#
|
|
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"
|