@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.
|
|
23
|
-
|
|
24
|
-
verifies it against `/user/tokens/verify` (status must
|
|
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 —
|
|
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
|
-
##
|
|
35
|
+
## Setting secrets
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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).
|
|
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).
|
|
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
|
|
19
|
-
import { join, resolve as
|
|
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.
|
|
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((
|
|
980
|
+
ask: (query) => new Promise((resolve10) => {
|
|
1029
981
|
process.stdout.write(query);
|
|
1030
982
|
rl.question("", (val) => {
|
|
1031
983
|
process.stdout.write("\n");
|
|
1032
|
-
|
|
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
|
-
|
|
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
|
|
1108
|
+
if (v !== void 0) out[k] = v;
|
|
1178
1109
|
}
|
|
1179
1110
|
return out;
|
|
1180
1111
|
}
|
|
1181
|
-
function
|
|
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
|
|
1198
|
-
const
|
|
1114
|
+
const env = presentEnv();
|
|
1115
|
+
const already = listGitHubSecrets(repo, opts.env);
|
|
1199
1116
|
const results = [];
|
|
1200
|
-
const
|
|
1117
|
+
const prompt = process.stdin.isTTY ? hiddenPrompter() : null;
|
|
1201
1118
|
try {
|
|
1202
1119
|
for (const spec of tokensForTool(tool)) {
|
|
1203
|
-
|
|
1204
|
-
if (
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
1224
|
-
|
|
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(
|
|
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
|
-
`${
|
|
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
|
-
|
|
1161
|
+
prompt?.close();
|
|
1241
1162
|
}
|
|
1242
1163
|
return results;
|
|
1243
1164
|
}
|
|
1244
1165
|
|
|
1245
1166
|
// src/commands/agent.ts
|
|
1246
|
-
import { cpSync, existsSync as
|
|
1247
|
-
import { resolve as
|
|
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 (!
|
|
1282
|
-
const dest =
|
|
1283
|
-
|
|
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 (!
|
|
1290
|
-
const skillDest =
|
|
1291
|
-
|
|
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 =
|
|
1296
|
-
const existingMcp =
|
|
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
|
-
|
|
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 =
|
|
1222
|
+
const claudePath = resolve4(dir, "CLAUDE.md");
|
|
1302
1223
|
const marker = "Greenlight loop (deploy \u2192 verify \u2192 promote)";
|
|
1303
|
-
const existing =
|
|
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
|
-
|
|
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 =
|
|
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 (!
|
|
1350
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1402
|
-
if (
|
|
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 (
|
|
1325
|
+
if (existsSync5(src)) {
|
|
1405
1326
|
cpSync2(src, dest, { recursive: true });
|
|
1406
1327
|
const pkgPath = join(dest, "package.json");
|
|
1407
|
-
if (
|
|
1408
|
-
const pkg = JSON.parse(
|
|
1328
|
+
if (existsSync5(pkgPath)) {
|
|
1329
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1409
1330
|
pkg.name = name;
|
|
1410
|
-
|
|
1331
|
+
writeFileSync2(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
1411
1332
|
`);
|
|
1412
1333
|
}
|
|
1413
1334
|
const shippedGitignore = join(dest, "gitignore");
|
|
1414
|
-
if (
|
|
1335
|
+
if (existsSync5(shippedGitignore)) renameSync(shippedGitignore, join(dest, ".gitignore"));
|
|
1415
1336
|
const wranglerPath = join(dest, "wrangler.toml");
|
|
1416
|
-
if (
|
|
1417
|
-
let wt =
|
|
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(
|
|
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
|
-
|
|
1351
|
+
writeFileSync2(wranglerPath, wt);
|
|
1431
1352
|
}
|
|
1432
1353
|
console.log(`\u2714 copied ${src} \u2192 tools/${name}`);
|
|
1433
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
1362
|
+
const infraDir = resolve5(cwd, "infra");
|
|
1442
1363
|
const mainTf = join(infraDir, "main.tf");
|
|
1443
|
-
if (!
|
|
1444
|
-
|
|
1445
|
-
|
|
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 (
|
|
1372
|
+
if (existsSync5(toolTf)) {
|
|
1452
1373
|
console.log(`\xB7 infra/${name}.tf exists \u2014 left as-is`);
|
|
1453
1374
|
} else {
|
|
1454
|
-
|
|
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 =
|
|
1392
|
+
const wfDir = resolve5(cwd, ".github/workflows");
|
|
1472
1393
|
const wfPath = join(wfDir, `deploy-${name}.yml`);
|
|
1473
|
-
if (
|
|
1394
|
+
if (existsSync5(wfPath)) {
|
|
1474
1395
|
console.log(`\xB7 .github/workflows/deploy-${name}.yml exists \u2014 left as-is`);
|
|
1475
1396
|
} else {
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
|
1504
|
-
import { join as join2, resolve as
|
|
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 (!
|
|
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 (
|
|
1896
|
+
if (existsSync6(path)) {
|
|
1976
1897
|
console.log(`\xB7 ${label} exists \u2014 left as-is`);
|
|
1977
1898
|
return;
|
|
1978
1899
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
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 =
|
|
1937
|
+
const dest = resolve6(cwd, toolRel);
|
|
2017
1938
|
console.log(`adopting "${name}" (${lane}/${target}) as the submodule ${toolRel}
|
|
2018
1939
|
`);
|
|
2019
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
2126
|
-
if (!
|
|
2127
|
-
const regVendor =
|
|
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 =
|
|
2146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2574
|
-
import { resolve as
|
|
2575
|
-
import { createInterface as
|
|
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/ #
|
|
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 (
|
|
2580
|
+
if (existsSync8(path)) {
|
|
2660
2581
|
console.log(`\xB7 ${label} exists \u2014 left as-is`);
|
|
2661
2582
|
return;
|
|
2662
2583
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
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 =
|
|
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 =
|
|
2686
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2698
|
-
scaffoldIfAbsent(
|
|
2699
|
-
scaffoldIfAbsent(
|
|
2700
|
-
scaffoldIfAbsent(
|
|
2701
|
-
const
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
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
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
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 (
|
|
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
|
|
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) =>
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
+
"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-
|
|
35
|
-
"@rtrentjones/greenlight-
|
|
36
|
-
"@rtrentjones/greenlight-
|
|
37
|
-
"@rtrentjones/greenlight-
|
|
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",
|