@rtrentjones/greenlight 0.3.1 → 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/assets/skills/provider-cloudflare/SKILL.md +4 -3
- package/assets/skills/provider-github/SKILL.md +6 -5
- package/assets/skills/provider-supabase/SKILL.md +2 -1
- package/assets/skills/provider-vercel/SKILL.md +2 -1
- package/dist/bin.js +287 -380
- package/package.json +5 -5
- package/templates/_template-agent/wrangler.toml +6 -6
|
@@ -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) {
|
|
@@ -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.
|
|
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,27 +908,104 @@ function providersForTool(tool) {
|
|
|
915
908
|
return out;
|
|
916
909
|
}
|
|
917
910
|
|
|
918
|
-
// src/
|
|
919
|
-
import { existsSync as
|
|
920
|
-
import { resolve as resolve4 } from "path";
|
|
921
|
-
import { createInterface as createInterface2 } from "readline/promises";
|
|
922
|
-
|
|
923
|
-
// src/commands/secrets.ts
|
|
924
|
-
import { execFileSync } from "child_process";
|
|
925
|
-
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
911
|
+
// src/commands/agent.ts
|
|
912
|
+
import { cpSync, existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
926
913
|
import { resolve as resolve3 } from "path";
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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;
|
|
936
924
|
}
|
|
937
925
|
return out;
|
|
938
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
|
+
|
|
1004
|
+
// src/commands/secrets.ts
|
|
1005
|
+
import { execFileSync } from "child_process";
|
|
1006
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
1007
|
+
import { resolve as resolve4 } from "path";
|
|
1008
|
+
import { createInterface } from "readline";
|
|
939
1009
|
function parseOciConfig(text) {
|
|
940
1010
|
const out = {};
|
|
941
1011
|
for (const raw of text.split("\n")) {
|
|
@@ -949,7 +1019,7 @@ function parseOciConfig(text) {
|
|
|
949
1019
|
return out;
|
|
950
1020
|
}
|
|
951
1021
|
function ociPrefill(configPath, keyPath) {
|
|
952
|
-
const cfg = parseOciConfig(
|
|
1022
|
+
const cfg = parseOciConfig(readFileSync2(configPath, "utf8"));
|
|
953
1023
|
const map = /* @__PURE__ */ new Map();
|
|
954
1024
|
const set = (k, v) => {
|
|
955
1025
|
if (v) map.set(k, v);
|
|
@@ -959,8 +1029,8 @@ function ociPrefill(configPath, keyPath) {
|
|
|
959
1029
|
set("TF_VAR_OCI_TENANCY_OCID", cfg.tenancy);
|
|
960
1030
|
set("TF_VAR_OCI_REGION", cfg.region);
|
|
961
1031
|
const pem = keyPath ?? cfg.key_file;
|
|
962
|
-
if (pem &&
|
|
963
|
-
map.set("TF_VAR_OCI_PRIVATE_KEY",
|
|
1032
|
+
if (pem && existsSync4(pem)) {
|
|
1033
|
+
map.set("TF_VAR_OCI_PRIVATE_KEY", readFileSync2(pem, "utf8"));
|
|
964
1034
|
} else if (pem) {
|
|
965
1035
|
console.log(` ! PEM not found at ${pem} \u2014 set TF_VAR_OCI_PRIVATE_KEY manually (--oci-key)`);
|
|
966
1036
|
}
|
|
@@ -987,49 +1057,17 @@ function detectRepo(cwd) {
|
|
|
987
1057
|
return null;
|
|
988
1058
|
}
|
|
989
1059
|
}
|
|
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
1060
|
function hiddenPrompter() {
|
|
1023
1061
|
const tty = Boolean(process.stdin.isTTY);
|
|
1024
1062
|
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: tty });
|
|
1025
1063
|
if (tty) rl._writeToOutput = () => {
|
|
1026
1064
|
};
|
|
1027
1065
|
return {
|
|
1028
|
-
ask: (query) => new Promise((
|
|
1066
|
+
ask: (query) => new Promise((resolve10) => {
|
|
1029
1067
|
process.stdout.write(query);
|
|
1030
1068
|
rl.question("", (val) => {
|
|
1031
1069
|
process.stdout.write("\n");
|
|
1032
|
-
|
|
1070
|
+
resolve10(val.trim());
|
|
1033
1071
|
});
|
|
1034
1072
|
}),
|
|
1035
1073
|
close: () => rl.close()
|
|
@@ -1139,200 +1177,14 @@ async function secretsCommand(args) {
|
|
|
1139
1177
|
if (!repo) throw new Error("could not determine the repo \u2014 pass --repo owner/repo");
|
|
1140
1178
|
const ociConfig2 = flag(args, "--oci-config");
|
|
1141
1179
|
const ociKey = flag(args, "--oci-key");
|
|
1142
|
-
const prefill = ociConfig2 ? ociPrefill(
|
|
1180
|
+
const prefill = ociConfig2 ? ociPrefill(resolve4(process.cwd(), ociConfig2), ociKey && resolve4(process.cwd(), ociKey)) : void 0;
|
|
1143
1181
|
await gatherSecrets(name, repo, flag(args, "--env"), prefill);
|
|
1144
1182
|
return;
|
|
1145
1183
|
}
|
|
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
|
-
console.log(
|
|
1162
|
-
`
|
|
1163
|
-
${count} secret(s) synced. (Prefer GitHub OIDC over long-lived tokens where supported.)`
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// src/tokens.ts
|
|
1168
|
-
var SECRETS_DIR = ".greenlight";
|
|
1169
|
-
var SECRETS_FILE = "secrets.env";
|
|
1170
|
-
function presentEnv(cwd) {
|
|
1171
|
-
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
|
-
for (const [k, v] of Object.entries(process.env)) {
|
|
1177
|
-
if (v !== void 0 && !(k in out)) out[k] = v;
|
|
1178
|
-
}
|
|
1179
|
-
return out;
|
|
1180
|
-
}
|
|
1181
|
-
function upsertSecret(cwd, key, value) {
|
|
1182
|
-
const dir = resolve4(cwd, SECRETS_DIR);
|
|
1183
|
-
mkdirSync(dir, { recursive: true });
|
|
1184
|
-
const p = resolve4(dir, SECRETS_FILE);
|
|
1185
|
-
const lines = existsSync4(p) ? readFileSync2(p, "utf8").split("\n") : [];
|
|
1186
|
-
const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
|
|
1187
|
-
if (idx >= 0) lines[idx] = `${key}=${value}`;
|
|
1188
|
-
else {
|
|
1189
|
-
while (lines.length && (lines[lines.length - 1] ?? "").trim() === "") lines.pop();
|
|
1190
|
-
lines.push(`${key}=${value}`);
|
|
1191
|
-
}
|
|
1192
|
-
writeFileSync(p, `${lines.join("\n").replace(/\n*$/, "")}
|
|
1193
|
-
`, { mode: 384 });
|
|
1194
|
-
}
|
|
1195
|
-
async function ensureTokensForTool(cwd, tool, opts = {}) {
|
|
1196
|
-
const doVerify = opts.verify !== false;
|
|
1197
|
-
const interactive = Boolean(process.stdin.isTTY);
|
|
1198
|
-
const env = presentEnv(cwd);
|
|
1199
|
-
const results = [];
|
|
1200
|
-
const rl = interactive ? createInterface2({ input: process.stdin, output: process.stdout }) : null;
|
|
1201
|
-
try {
|
|
1202
|
-
for (const spec of tokensForTool(tool)) {
|
|
1203
|
-
let value = env[spec.envVar];
|
|
1204
|
-
if (value) {
|
|
1205
|
-
results.push({ envVar: spec.envVar, outcome: "present" });
|
|
1206
|
-
} else if (rl) {
|
|
1207
|
-
console.log(`
|
|
1208
|
-
${spec.envVar} \u2014 ${spec.label}`);
|
|
1209
|
-
if (spec.scopes?.length) console.log(` scopes: ${spec.scopes.join(", ")}`);
|
|
1210
|
-
const entered = (await rl.question(` paste value${spec.optional ? " (optional, Enter to skip)" : ""}: `)).trim();
|
|
1211
|
-
if (!entered) {
|
|
1212
|
-
results.push({ envVar: spec.envVar, outcome: spec.optional ? "skipped" : "missing" });
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1215
|
-
upsertSecret(cwd, spec.envVar, entered);
|
|
1216
|
-
env[spec.envVar] = entered;
|
|
1217
|
-
value = entered;
|
|
1218
|
-
results.push({ envVar: spec.envVar, outcome: "entered" });
|
|
1219
|
-
} else {
|
|
1220
|
-
results.push({ envVar: spec.envVar, outcome: spec.optional ? "skipped" : "missing" });
|
|
1221
|
-
continue;
|
|
1222
|
-
}
|
|
1223
|
-
if (value && doVerify && spec.verify) {
|
|
1224
|
-
let check;
|
|
1225
|
-
try {
|
|
1226
|
-
check = await spec.verify(value, env);
|
|
1227
|
-
} catch (e) {
|
|
1228
|
-
check = { ok: false, detail: e instanceof Error ? e.message : String(e) };
|
|
1229
|
-
}
|
|
1230
|
-
const last = results[results.length - 1];
|
|
1231
|
-
if (last) last.verify = check;
|
|
1232
|
-
if (!check.ok && !spec.optional) {
|
|
1233
|
-
throw new Error(
|
|
1234
|
-
`${spec.envVar} failed verification${check.detail ? ` (${check.detail})` : ""} \u2014 check the token's scopes (${spec.label}).`
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
} finally {
|
|
1240
|
-
rl?.close();
|
|
1241
|
-
}
|
|
1242
|
-
return results;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// src/commands/agent.ts
|
|
1246
|
-
import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1247
|
-
import { resolve as resolve5 } from "path";
|
|
1248
|
-
|
|
1249
|
-
// src/agent-kit.ts
|
|
1250
|
-
function recommendedMcp(tool) {
|
|
1251
|
-
return mcpForTool(tool);
|
|
1252
|
-
}
|
|
1253
|
-
function mergeMcpServers(existing, add) {
|
|
1254
|
-
const out = { mcpServers: { ...existing?.mcpServers ?? {} } };
|
|
1255
|
-
for (const [name, val] of Object.entries(add)) {
|
|
1256
|
-
if (out.mcpServers[name]) continue;
|
|
1257
|
-
out.mcpServers[name] = typeof val === "string" ? { type: "http", url: val } : val;
|
|
1258
|
-
}
|
|
1259
|
-
return out;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// src/commands/agent.ts
|
|
1263
|
-
var CLAUDE_BLOCK = `## Greenlight loop (deploy \u2192 verify \u2192 promote)
|
|
1264
|
-
|
|
1265
|
-
This repo uses Greenlight. Deliver every change through the ONE model (same shape for web + MCP
|
|
1266
|
-
tools \u2014 the deploy-verify-promote skill has the lane\xD7target matrix):
|
|
1267
|
-
branch \u2192 change \u2192 \`greenlight preview <name>\` (local gate) \u2192 add it to the tool's verify.config \u2192
|
|
1268
|
-
push (CI gates on the tool's own tests) \u2192 deploy \u2192 \`greenlight verify <name> --env prod\`.
|
|
1269
|
-
Web tools also get beta + \`greenlight promote\`; oci is direct-to-prod (the local gate is the
|
|
1270
|
-
pre-prod safety). \`greenlight status <name>\` shows the run chain; \`greenlight doctor\` flags drift.
|
|
1271
|
-
|
|
1272
|
-
Agentic kit:
|
|
1273
|
-
- Skill: \`.claude/skills/deploy-verify-promote/SKILL.md\` (the one model + the matrix).
|
|
1274
|
-
- MCP servers: \`.mcp.json\` recommends the relevant providers \u2014 run \`/mcp\` to authenticate.
|
|
1275
|
-
Vercel is OAuth; Supabase needs \`SUPABASE_ACCESS_TOKEN\` (+ \`SUPABASE_PROJECT_REF\`) in your env.
|
|
1276
|
-
- Best-practice skills (one-time, user scope):
|
|
1277
|
-
\`claude plugin marketplace add cloudflare/skills && claude plugin install cloudflare@cloudflare\`
|
|
1278
|
-
`;
|
|
1279
|
-
function materializeAgentKit(dir, tool) {
|
|
1280
|
-
const src = skillAssetDir();
|
|
1281
|
-
if (!existsSync5(src)) throw new Error(`skill asset not found at ${src}`);
|
|
1282
|
-
const dest = resolve5(dir, ".claude/skills/deploy-verify-promote");
|
|
1283
|
-
mkdirSync2(dest, { recursive: true });
|
|
1284
|
-
cpSync(src, dest, { recursive: true });
|
|
1285
|
-
console.log("\u2714 .claude/skills/deploy-verify-promote/SKILL.md");
|
|
1286
|
-
for (const pack of packsForTool(tool)) {
|
|
1287
|
-
if (!pack.skill) continue;
|
|
1288
|
-
const skillSrc = skillAssetDir(pack.skill);
|
|
1289
|
-
if (!existsSync5(skillSrc)) continue;
|
|
1290
|
-
const skillDest = resolve5(dir, ".claude/skills", pack.skill);
|
|
1291
|
-
mkdirSync2(skillDest, { recursive: true });
|
|
1292
|
-
cpSync(skillSrc, skillDest, { recursive: true });
|
|
1293
|
-
console.log(`\u2714 .claude/skills/${pack.skill}/SKILL.md`);
|
|
1294
|
-
}
|
|
1295
|
-
const mcpPath = resolve5(dir, ".mcp.json");
|
|
1296
|
-
const existingMcp = existsSync5(mcpPath) ? JSON.parse(readFileSync3(mcpPath, "utf8")) : null;
|
|
1297
|
-
const servers = recommendedMcp(tool);
|
|
1298
|
-
writeFileSync2(mcpPath, `${JSON.stringify(mergeMcpServers(existingMcp, servers), null, 2)}
|
|
1299
|
-
`);
|
|
1300
|
-
console.log(`\u2714 .mcp.json (${Object.keys(servers).length} recommended MCP server(s))`);
|
|
1301
|
-
const claudePath = resolve5(dir, "CLAUDE.md");
|
|
1302
|
-
const marker = "Greenlight loop (deploy \u2192 verify \u2192 promote)";
|
|
1303
|
-
const existing = existsSync5(claudePath) ? readFileSync3(claudePath, "utf8") : "";
|
|
1304
|
-
if (existing.includes(marker)) {
|
|
1305
|
-
console.log("\xB7 CLAUDE.md already has the loop block");
|
|
1306
|
-
} else {
|
|
1307
|
-
writeFileSync2(claudePath, existing ? `${existing.trimEnd()}
|
|
1308
|
-
|
|
1309
|
-
${CLAUDE_BLOCK}` : CLAUDE_BLOCK);
|
|
1310
|
-
console.log(`\u2714 CLAUDE.md (${existing ? "appended" : "created"})`);
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
async function agentCommand(args) {
|
|
1314
|
-
if (args[0] !== "sync") {
|
|
1315
|
-
console.log(
|
|
1316
|
-
"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"
|
|
1317
|
-
);
|
|
1318
|
-
process.exit(args[0] ? 1 : 0);
|
|
1319
|
-
}
|
|
1320
|
-
const name = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
|
|
1321
|
-
if (name) {
|
|
1322
|
-
const { config } = await loadManifest();
|
|
1323
|
-
const entry = resolveEntry(config, name);
|
|
1324
|
-
const dir = resolve5(process.cwd(), entry.dir ?? ".");
|
|
1325
|
-
materializeAgentKit(dir, { lane: entry.lane, target: entry.target, data: entry.data });
|
|
1326
|
-
console.log(
|
|
1327
|
-
`
|
|
1328
|
-
Synced the kit for "${name}" \u2192 ${entry.dir ?? "."} (lane=${entry.lane}, target=${entry.target}, data=${entry.data}).`
|
|
1329
|
-
);
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
materializeAgentKit(process.cwd());
|
|
1333
1184
|
console.log(
|
|
1334
|
-
"
|
|
1185
|
+
"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"
|
|
1335
1186
|
);
|
|
1187
|
+
process.exit(sub ? 1 : 0);
|
|
1336
1188
|
}
|
|
1337
1189
|
|
|
1338
1190
|
// src/commands/add.ts
|
|
@@ -1346,25 +1198,25 @@ function templateDir(lane, target) {
|
|
|
1346
1198
|
}
|
|
1347
1199
|
function registerWorkspaceMember(cwd, member) {
|
|
1348
1200
|
const wsPath = join(cwd, "pnpm-workspace.yaml");
|
|
1349
|
-
if (!
|
|
1350
|
-
|
|
1201
|
+
if (!existsSync5(wsPath)) {
|
|
1202
|
+
writeFileSync2(wsPath, `packages:
|
|
1351
1203
|
- "${member}"
|
|
1352
1204
|
`);
|
|
1353
1205
|
console.log(`\u2714 created pnpm-workspace.yaml (member ${member})`);
|
|
1354
1206
|
return;
|
|
1355
1207
|
}
|
|
1356
|
-
const text =
|
|
1208
|
+
const text = readFileSync3(wsPath, "utf8");
|
|
1357
1209
|
if (text.includes(member) || /^\s*-\s*["']?tools\/\*/m.test(text)) return;
|
|
1358
1210
|
const lines = text.split("\n");
|
|
1359
1211
|
const pkgIdx = lines.findIndex((l) => /^packages\s*:/.test(l));
|
|
1360
1212
|
if (pkgIdx === -1) {
|
|
1361
|
-
|
|
1213
|
+
writeFileSync2(wsPath, `${text.replace(/\s*$/, "")}
|
|
1362
1214
|
packages:
|
|
1363
1215
|
- "${member}"
|
|
1364
1216
|
`);
|
|
1365
1217
|
} else {
|
|
1366
1218
|
lines.splice(pkgIdx + 1, 0, ` - "${member}"`);
|
|
1367
|
-
|
|
1219
|
+
writeFileSync2(wsPath, lines.join("\n"));
|
|
1368
1220
|
}
|
|
1369
1221
|
console.log(`\u2714 registered ${member} in pnpm-workspace.yaml`);
|
|
1370
1222
|
}
|
|
@@ -1398,60 +1250,48 @@ async function addCommand(args) {
|
|
|
1398
1250
|
const data = entry?.data ?? "none";
|
|
1399
1251
|
const envs = entry?.envs ?? ["beta", "prod"];
|
|
1400
1252
|
const toolInfo = { lane, target, data };
|
|
1401
|
-
const dest =
|
|
1402
|
-
if (
|
|
1253
|
+
const dest = resolve5(process.cwd(), "tools", name);
|
|
1254
|
+
if (existsSync5(dest)) throw new Error(`tools/${name} already exists`);
|
|
1403
1255
|
const src = templateDir(lane, target);
|
|
1404
|
-
if (
|
|
1256
|
+
if (existsSync5(src)) {
|
|
1405
1257
|
cpSync2(src, dest, { recursive: true });
|
|
1406
1258
|
const pkgPath = join(dest, "package.json");
|
|
1407
|
-
if (
|
|
1408
|
-
const pkg = JSON.parse(
|
|
1259
|
+
if (existsSync5(pkgPath)) {
|
|
1260
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1409
1261
|
pkg.name = name;
|
|
1410
|
-
|
|
1262
|
+
writeFileSync2(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
1411
1263
|
`);
|
|
1412
1264
|
}
|
|
1413
1265
|
const shippedGitignore = join(dest, "gitignore");
|
|
1414
|
-
if (
|
|
1266
|
+
if (existsSync5(shippedGitignore)) renameSync(shippedGitignore, join(dest, ".gitignore"));
|
|
1415
1267
|
const wranglerPath = join(dest, "wrangler.toml");
|
|
1416
|
-
if (
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
const token = presentEnv(process.cwd()).CLOUDFLARE_API_TOKEN;
|
|
1420
|
-
const acct = token ? await resolveCloudflareAccountId(config.domain, token) : null;
|
|
1421
|
-
if (acct) {
|
|
1422
|
-
wt = wt.replaceAll("REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID", acct);
|
|
1423
|
-
console.log("\u2714 resolved the Cloudflare account id into wrangler.toml");
|
|
1424
|
-
} else {
|
|
1425
|
-
console.log(
|
|
1426
|
-
"\xB7 could not resolve the Cloudflare account id \u2014 set account_id in wrangler.toml"
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
writeFileSync3(wranglerPath, wt);
|
|
1268
|
+
if (existsSync5(wranglerPath)) {
|
|
1269
|
+
const wt = readFileSync3(wranglerPath, "utf8").replaceAll("agent-tool", name).replaceAll("example.dev", config.domain);
|
|
1270
|
+
writeFileSync2(wranglerPath, wt);
|
|
1431
1271
|
}
|
|
1432
1272
|
console.log(`\u2714 copied ${src} \u2192 tools/${name}`);
|
|
1433
|
-
if (
|
|
1273
|
+
if (existsSync5(pkgPath)) registerWorkspaceMember(process.cwd(), `tools/${name}`);
|
|
1434
1274
|
} else {
|
|
1435
1275
|
console.log(`! no template at ${src} \u2014 manifest entry added without scaffolding`);
|
|
1436
1276
|
}
|
|
1437
|
-
|
|
1277
|
+
writeFileSync2(path, serializeConfig(next));
|
|
1438
1278
|
console.log(`\u2714 added "${name}" (${lane}/${target}) to the manifest`);
|
|
1439
1279
|
const cwd = process.cwd();
|
|
1440
1280
|
const providers = providersForTool(toolInfo);
|
|
1441
|
-
const infraDir =
|
|
1281
|
+
const infraDir = resolve5(cwd, "infra");
|
|
1442
1282
|
const mainTf = join(infraDir, "main.tf");
|
|
1443
|
-
if (!
|
|
1444
|
-
|
|
1445
|
-
|
|
1283
|
+
if (!existsSync5(mainTf)) {
|
|
1284
|
+
mkdirSync2(infraDir, { recursive: true });
|
|
1285
|
+
writeFileSync2(mainTf, emitWrapperMainTf({ domain: config.domain, providers }));
|
|
1446
1286
|
console.log("\u2714 scaffolded infra/main.tf (providers + HCP backend placeholder)");
|
|
1447
1287
|
} else if (providers.some((p) => p !== "cloudflare" && p !== "github")) {
|
|
1448
1288
|
console.log(`\xB7 infra/main.tf exists \u2014 ensure it declares provider(s): ${providers.join(", ")}`);
|
|
1449
1289
|
}
|
|
1450
1290
|
const toolTf = join(infraDir, `${name}.tf`);
|
|
1451
|
-
if (
|
|
1291
|
+
if (existsSync5(toolTf)) {
|
|
1452
1292
|
console.log(`\xB7 infra/${name}.tf exists \u2014 left as-is`);
|
|
1453
1293
|
} else {
|
|
1454
|
-
|
|
1294
|
+
writeFileSync2(
|
|
1455
1295
|
toolTf,
|
|
1456
1296
|
emitToolTf({
|
|
1457
1297
|
name,
|
|
@@ -1468,13 +1308,13 @@ async function addCommand(args) {
|
|
|
1468
1308
|
console.log(`\u2714 wrote infra/${name}.tf (modules: ${providers.join(", ")})`);
|
|
1469
1309
|
}
|
|
1470
1310
|
if (lane === "agent") {
|
|
1471
|
-
const wfDir =
|
|
1311
|
+
const wfDir = resolve5(cwd, ".github/workflows");
|
|
1472
1312
|
const wfPath = join(wfDir, `deploy-${name}.yml`);
|
|
1473
|
-
if (
|
|
1313
|
+
if (existsSync5(wfPath)) {
|
|
1474
1314
|
console.log(`\xB7 .github/workflows/deploy-${name}.yml exists \u2014 left as-is`);
|
|
1475
1315
|
} else {
|
|
1476
|
-
|
|
1477
|
-
|
|
1316
|
+
mkdirSync2(wfDir, { recursive: true });
|
|
1317
|
+
writeFileSync2(wfPath, emitAgentDeployWorkflow(name, config.domain));
|
|
1478
1318
|
console.log(`\u2714 wrote .github/workflows/deploy-${name}.yml`);
|
|
1479
1319
|
}
|
|
1480
1320
|
}
|
|
@@ -1500,8 +1340,8 @@ Next:${gather ? "" : `
|
|
|
1500
1340
|
|
|
1501
1341
|
// src/commands/adopt.ts
|
|
1502
1342
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1503
|
-
import { cpSync as cpSync3, existsSync as
|
|
1504
|
-
import { join as join2, resolve as
|
|
1343
|
+
import { cpSync as cpSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1344
|
+
import { join as join2, resolve as resolve6 } from "path";
|
|
1505
1345
|
var REF = MODULE_REF;
|
|
1506
1346
|
function flag3(args, name) {
|
|
1507
1347
|
const i = args.indexOf(name);
|
|
@@ -1517,7 +1357,7 @@ function mergePackageJson(existing, repoName, vendor) {
|
|
|
1517
1357
|
}
|
|
1518
1358
|
function vendorDeps(vendorDir) {
|
|
1519
1359
|
const out = {};
|
|
1520
|
-
if (!
|
|
1360
|
+
if (!existsSync6(vendorDir)) return out;
|
|
1521
1361
|
for (const f of readdirSync(vendorDir)) {
|
|
1522
1362
|
if (!f.endsWith(".tgz")) continue;
|
|
1523
1363
|
const base = f.replace(/-\d+\.\d+\.\d+(-[\w.]+)?\.tgz$/, "");
|
|
@@ -1972,12 +1812,12 @@ export default [api, ...agentWeb];
|
|
|
1972
1812
|
`;
|
|
1973
1813
|
}
|
|
1974
1814
|
function writeIfAbsent(path, contents, label) {
|
|
1975
|
-
if (
|
|
1815
|
+
if (existsSync6(path)) {
|
|
1976
1816
|
console.log(`\xB7 ${label} exists \u2014 left as-is`);
|
|
1977
1817
|
return;
|
|
1978
1818
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1819
|
+
mkdirSync3(resolve6(path, ".."), { recursive: true });
|
|
1820
|
+
writeFileSync3(path, contents);
|
|
1981
1821
|
console.log(`\u2714 ${label}`);
|
|
1982
1822
|
}
|
|
1983
1823
|
async function adoptCommand(args) {
|
|
@@ -2013,10 +1853,10 @@ async function adoptWrapper(ctx) {
|
|
|
2013
1853
|
const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
|
|
2014
1854
|
const cwd = process.cwd();
|
|
2015
1855
|
const toolRel = `tools/${name}`;
|
|
2016
|
-
const dest =
|
|
1856
|
+
const dest = resolve6(cwd, toolRel);
|
|
2017
1857
|
console.log(`adopting "${name}" (${lane}/${target}) as the submodule ${toolRel}
|
|
2018
1858
|
`);
|
|
2019
|
-
if (!
|
|
1859
|
+
if (!existsSync6(dest)) {
|
|
2020
1860
|
try {
|
|
2021
1861
|
execFileSync2("git", ["submodule", "add", repoArg, toolRel], { cwd, stdio: "inherit" });
|
|
2022
1862
|
console.log(`\u2714 git submodule add ${repoArg} ${toolRel}`);
|
|
@@ -2052,7 +1892,7 @@ async function adoptWrapper(ctx) {
|
|
|
2052
1892
|
}
|
|
2053
1893
|
} : {}
|
|
2054
1894
|
});
|
|
2055
|
-
|
|
1895
|
+
writeFileSync3(regPath, serializeConfig(nextReg));
|
|
2056
1896
|
console.log(
|
|
2057
1897
|
`\u2714 ${existed ? "updated" : "registered"} "${name}" (external, dir ${toolRel}) in the wrapper manifest`
|
|
2058
1898
|
);
|
|
@@ -2070,7 +1910,7 @@ async function adoptWrapper(ctx) {
|
|
|
2070
1910
|
);
|
|
2071
1911
|
}
|
|
2072
1912
|
const providers = providersForTool({ lane, target, data });
|
|
2073
|
-
if (
|
|
1913
|
+
if (existsSync6(join2(cwd, "infra/main.tf")) && providers.some((p) => p !== "cloudflare" && p !== "github")) {
|
|
2074
1914
|
console.log(`\xB7 ensure infra/main.tf declares provider(s): ${providers.join(", ")}`);
|
|
2075
1915
|
}
|
|
2076
1916
|
materializeAgentKit(dest, { lane, target, data });
|
|
@@ -2122,9 +1962,9 @@ Next:
|
|
|
2122
1962
|
}
|
|
2123
1963
|
async function adoptStandalone(ctx) {
|
|
2124
1964
|
const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
|
|
2125
|
-
const repo =
|
|
2126
|
-
if (!
|
|
2127
|
-
const regVendor =
|
|
1965
|
+
const repo = resolve6(process.cwd(), repoArg);
|
|
1966
|
+
if (!existsSync6(repo)) throw new Error(`no such repo: ${repo} (--standalone needs a local path)`);
|
|
1967
|
+
const regVendor = resolve6(process.cwd(), "vendor");
|
|
2128
1968
|
const vendor = vendorDeps(regVendor);
|
|
2129
1969
|
if (Object.keys(vendor).length === 0) {
|
|
2130
1970
|
throw new Error(
|
|
@@ -2142,15 +1982,15 @@ async function adoptStandalone(ctx) {
|
|
|
2142
1982
|
);
|
|
2143
1983
|
const slug = parseRepo(safeGit(repo, ["remote", "get-url", "origin"])) ?? `OWNER/${name}`;
|
|
2144
1984
|
const pkgPath = join2(repo, "package.json");
|
|
2145
|
-
const existingPkg =
|
|
2146
|
-
|
|
1985
|
+
const existingPkg = existsSync6(pkgPath) ? JSON.parse(readFileSync4(pkgPath, "utf8")) : null;
|
|
1986
|
+
writeFileSync3(
|
|
2147
1987
|
pkgPath,
|
|
2148
1988
|
`${JSON.stringify(mergePackageJson(existingPkg, name, vendor), null, 2)}
|
|
2149
1989
|
`
|
|
2150
1990
|
);
|
|
2151
1991
|
console.log("\u2714 package.json (merged framework deps + overrides)");
|
|
2152
1992
|
const repoVendor = join2(repo, "vendor");
|
|
2153
|
-
|
|
1993
|
+
mkdirSync3(repoVendor, { recursive: true });
|
|
2154
1994
|
for (const f of readdirSync(regVendor)) {
|
|
2155
1995
|
if (f.endsWith(".tgz")) cpSync3(join2(regVendor, f), join2(repoVendor, f));
|
|
2156
1996
|
}
|
|
@@ -2184,7 +2024,7 @@ async function adoptStandalone(ctx) {
|
|
|
2184
2024
|
external: true,
|
|
2185
2025
|
adopted: true
|
|
2186
2026
|
});
|
|
2187
|
-
|
|
2027
|
+
writeFileSync3(regPath, serializeConfig(nextReg));
|
|
2188
2028
|
console.log(`\u2714 registered "${name}" in ${regPath.replace(`${process.cwd()}/`, "")} (external)`);
|
|
2189
2029
|
console.log(`
|
|
2190
2030
|
Next (in the adopted repo):
|
|
@@ -2197,20 +2037,20 @@ Note: deploying ${target} needs the ${target} adapter (workers is built; oci/ver
|
|
|
2197
2037
|
}
|
|
2198
2038
|
function addGreenlightScript(dir) {
|
|
2199
2039
|
const pkgPath = join2(dir, "package.json");
|
|
2200
|
-
if (!
|
|
2040
|
+
if (!existsSync6(pkgPath)) {
|
|
2201
2041
|
console.log(
|
|
2202
2042
|
"\xB7 no package.json (non-Node tool) \u2014 run the loop via `npx @rtrentjones/greenlight`"
|
|
2203
2043
|
);
|
|
2204
2044
|
return;
|
|
2205
2045
|
}
|
|
2206
|
-
const pkg = JSON.parse(
|
|
2046
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
2207
2047
|
pkg.scripts = { ...pkg.scripts ?? {} };
|
|
2208
2048
|
if (pkg.scripts.greenlight) {
|
|
2209
2049
|
console.log("\xB7 package.json already has a greenlight script");
|
|
2210
2050
|
return;
|
|
2211
2051
|
}
|
|
2212
2052
|
pkg.scripts.greenlight = "npx @rtrentjones/greenlight";
|
|
2213
|
-
|
|
2053
|
+
writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
2214
2054
|
`);
|
|
2215
2055
|
console.log("\u2714 package.json (greenlight script \u2014 runnable via npx post-publish)");
|
|
2216
2056
|
}
|
|
@@ -2355,10 +2195,10 @@ async function deployCommand(args) {
|
|
|
2355
2195
|
// src/commands/doctor.ts
|
|
2356
2196
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2357
2197
|
import { lookup } from "dns/promises";
|
|
2358
|
-
import { existsSync as
|
|
2198
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
2359
2199
|
import { join as join4 } from "path";
|
|
2360
2200
|
function dirCheck(label, dir) {
|
|
2361
|
-
return
|
|
2201
|
+
return existsSync7(dir) ? { name: `${label}: directory`, status: "ok" } : { name: `${label}: directory`, status: "fail", detail: `missing ${dir}` };
|
|
2362
2202
|
}
|
|
2363
2203
|
function conformanceChecks(t, root) {
|
|
2364
2204
|
const out = [];
|
|
@@ -2368,7 +2208,7 @@ function conformanceChecks(t, root) {
|
|
|
2368
2208
|
join4(toolDir, `verify/${t.name}.config.ts`),
|
|
2369
2209
|
join4(toolDir, "verify.config.ts")
|
|
2370
2210
|
] : [join4(toolDir, "verify.config.ts")];
|
|
2371
|
-
const found = specCandidates.find((p) =>
|
|
2211
|
+
const found = specCandidates.find((p) => existsSync7(join4(root, p)));
|
|
2372
2212
|
out.push({
|
|
2373
2213
|
name: `${t.name}: in the verify loop`,
|
|
2374
2214
|
status: found ? "ok" : "warn",
|
|
@@ -2394,14 +2234,14 @@ function conformanceChecks(t, root) {
|
|
|
2394
2234
|
}
|
|
2395
2235
|
if (!t.external && t.lane === "next" && t.target === "vercel") {
|
|
2396
2236
|
const wsPath = join4(root, "pnpm-workspace.yaml");
|
|
2397
|
-
const ws =
|
|
2237
|
+
const ws = existsSync7(wsPath) ? readFileSync5(wsPath, "utf8") : "";
|
|
2398
2238
|
const member = ws.includes(toolDir) || /^\s*-\s*["']?tools\/\*/m.test(ws);
|
|
2399
2239
|
out.push({
|
|
2400
2240
|
name: `${t.name}: pnpm workspace member`,
|
|
2401
2241
|
status: member ? "ok" : "warn",
|
|
2402
2242
|
detail: member ? void 0 : `add "${toolDir}" to pnpm-workspace.yaml \u2014 else Vercel's root install skips its deps`
|
|
2403
2243
|
});
|
|
2404
|
-
const hasVercelJson =
|
|
2244
|
+
const hasVercelJson = existsSync7(join4(root, toolDir, "vercel.json"));
|
|
2405
2245
|
out.push({
|
|
2406
2246
|
name: `${t.name}: vercel.json framework`,
|
|
2407
2247
|
status: hasVercelJson ? "ok" : "warn",
|
|
@@ -2415,7 +2255,7 @@ function versionDriftCheck(root) {
|
|
|
2415
2255
|
let installed;
|
|
2416
2256
|
try {
|
|
2417
2257
|
const pkg = JSON.parse(
|
|
2418
|
-
|
|
2258
|
+
readFileSync5(join4(root, "node_modules/@rtrentjones/greenlight/package.json"), "utf8")
|
|
2419
2259
|
);
|
|
2420
2260
|
installed = pkg.version;
|
|
2421
2261
|
} catch {
|
|
@@ -2423,7 +2263,7 @@ function versionDriftCheck(root) {
|
|
|
2423
2263
|
const refs = /* @__PURE__ */ new Set();
|
|
2424
2264
|
try {
|
|
2425
2265
|
for (const f of readdirSync2(join4(root, "infra")).filter((f2) => f2.endsWith(".tf"))) {
|
|
2426
|
-
const body =
|
|
2266
|
+
const body = readFileSync5(join4(root, "infra", f), "utf8");
|
|
2427
2267
|
for (const m of body.matchAll(/greenlight\.git\/\/infra\/modules\/[^?"]+\?ref=(v[0-9.]+)/g)) {
|
|
2428
2268
|
if (m[1]) refs.add(m[1]);
|
|
2429
2269
|
}
|
|
@@ -2479,7 +2319,7 @@ function runDoctor(config, root) {
|
|
|
2479
2319
|
checks.push({ name: `${t.name}: external (registry)`, status: "ok", detail: url });
|
|
2480
2320
|
if (t.dir) {
|
|
2481
2321
|
checks.push(
|
|
2482
|
-
|
|
2322
|
+
existsSync7(join4(root, t.dir)) ? { name: `${t.name}: dir present`, status: "ok", detail: t.dir } : {
|
|
2483
2323
|
name: `${t.name}: dir present`,
|
|
2484
2324
|
status: "warn",
|
|
2485
2325
|
detail: `declared dir "${t.dir}" missing \u2014 run \`git submodule update --init\``
|
|
@@ -2570,9 +2410,73 @@ ${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}`);
|
|
|
2570
2410
|
}
|
|
2571
2411
|
|
|
2572
2412
|
// src/commands/init.ts
|
|
2573
|
-
import { existsSync as
|
|
2574
|
-
import { resolve as
|
|
2575
|
-
import { createInterface as
|
|
2413
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2414
|
+
import { resolve as resolve7 } from "path";
|
|
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
|
|
2576
2480
|
function flag5(args, name) {
|
|
2577
2481
|
const i = args.indexOf(name);
|
|
2578
2482
|
return i >= 0 ? args[i + 1] : void 0;
|
|
@@ -2594,7 +2498,7 @@ function wrapperPackageJson(name) {
|
|
|
2594
2498
|
}
|
|
2595
2499
|
var WRAPPER_GITIGNORE = `# Greenlight wrapper
|
|
2596
2500
|
node_modules/
|
|
2597
|
-
.greenlight/ #
|
|
2501
|
+
.greenlight/ # local scratch \u2014 never committed (tokens live in GitHub Actions)
|
|
2598
2502
|
.terraform/
|
|
2599
2503
|
*.tfplan
|
|
2600
2504
|
tf.plan
|
|
@@ -2656,12 +2560,12 @@ jobs:
|
|
|
2656
2560
|
`;
|
|
2657
2561
|
}
|
|
2658
2562
|
function scaffoldIfAbsent(path, contents, label) {
|
|
2659
|
-
if (
|
|
2563
|
+
if (existsSync8(path)) {
|
|
2660
2564
|
console.log(`\xB7 ${label} exists \u2014 left as-is`);
|
|
2661
2565
|
return;
|
|
2662
2566
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2567
|
+
mkdirSync4(resolve7(path, ".."), { recursive: true });
|
|
2568
|
+
writeFileSync4(path, contents);
|
|
2665
2569
|
console.log(`\u2714 wrote ${label}`);
|
|
2666
2570
|
}
|
|
2667
2571
|
var TOKEN_FLAGS = {
|
|
@@ -2676,70 +2580,74 @@ async function initCommand(args) {
|
|
|
2676
2580
|
let domain = flag5(args, "--domain");
|
|
2677
2581
|
if (!domain) {
|
|
2678
2582
|
if (!process.stdin.isTTY) throw new Error("init needs --domain <domain> (no TTY for prompts)");
|
|
2679
|
-
const rl =
|
|
2583
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2680
2584
|
domain = (await rl.question("Domain (e.g. example.dev): ")).trim();
|
|
2681
2585
|
rl.close();
|
|
2682
2586
|
}
|
|
2683
2587
|
if (!domain) throw new Error("a domain is required");
|
|
2684
2588
|
const cwd = process.cwd();
|
|
2685
|
-
const configPath =
|
|
2686
|
-
if (
|
|
2589
|
+
const configPath = resolve7(cwd, "greenlight.config.ts");
|
|
2590
|
+
if (existsSync8(configPath) && !force) {
|
|
2687
2591
|
throw new Error("greenlight.config.ts already exists \u2014 pass --force to overwrite");
|
|
2688
2592
|
}
|
|
2689
|
-
|
|
2593
|
+
writeFileSync4(configPath, scaffoldConfig(domain));
|
|
2690
2594
|
console.log(`\u2714 wrote greenlight.config.ts (domain: ${domain})`);
|
|
2691
2595
|
const repoName = domain.replace(/\./g, "-");
|
|
2692
2596
|
scaffoldIfAbsent(
|
|
2693
|
-
|
|
2597
|
+
resolve7(cwd, ".github/workflows/infra.yml"),
|
|
2694
2598
|
wrapperInfraYml(),
|
|
2695
2599
|
".github/workflows/infra.yml (HCP-backed terraform apply on push)"
|
|
2696
2600
|
);
|
|
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)}`);
|
|
2601
|
+
scaffoldIfAbsent(resolve7(cwd, ".gitignore"), WRAPPER_GITIGNORE, ".gitignore");
|
|
2602
|
+
scaffoldIfAbsent(resolve7(cwd, "package.json"), wrapperPackageJson(repoName), "package.json");
|
|
2603
|
+
scaffoldIfAbsent(resolve7(cwd, "mise.toml"), WRAPPER_MISE, "mise.toml");
|
|
2604
|
+
scaffoldIfAbsent(resolve7(cwd, ".node-version"), "24\n", ".node-version");
|
|
2605
|
+
const repo = flag5(args, "--repo") ?? detectRepo(cwd);
|
|
2606
|
+
let pushed = 0;
|
|
2607
|
+
if (repo && !args.includes("--no-push")) {
|
|
2608
|
+
for (const [f, key] of Object.entries(TOKEN_FLAGS)) {
|
|
2609
|
+
const v = flag5(args, f);
|
|
2610
|
+
if (!v || key.startsWith("GITHUB_")) continue;
|
|
2611
|
+
try {
|
|
2612
|
+
setGitHubSecret(repo, void 0, key, v);
|
|
2613
|
+
console.log(`\u2714 set ${key} \u2192 ${repo} (GitHub Actions)`);
|
|
2614
|
+
pushed++;
|
|
2615
|
+
} catch (e) {
|
|
2616
|
+
console.log(`! could not set ${key}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2617
|
+
}
|
|
2719
2618
|
}
|
|
2720
2619
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2620
|
+
if (process.stdin.isTTY && !args.includes("--no-tokens")) {
|
|
2621
|
+
if (repo) {
|
|
2622
|
+
try {
|
|
2623
|
+
const results = await ensureTokensForTool(
|
|
2624
|
+
repo,
|
|
2625
|
+
{},
|
|
2626
|
+
{
|
|
2627
|
+
verify: !args.includes("--no-verify")
|
|
2628
|
+
}
|
|
2629
|
+
);
|
|
2630
|
+
pushed += results.filter((r) => r.outcome === "entered").length;
|
|
2631
|
+
} catch (e) {
|
|
2632
|
+
console.log(`\u2716 ${e instanceof Error ? e.message : String(e)}`);
|
|
2633
|
+
}
|
|
2634
|
+
} else {
|
|
2635
|
+
console.log(
|
|
2636
|
+
"\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`."
|
|
2637
|
+
);
|
|
2730
2638
|
}
|
|
2731
2639
|
}
|
|
2732
2640
|
console.log(`
|
|
2733
2641
|
Next:
|
|
2734
2642
|
1. greenlight add <name> --lane <lane> --target <target> # scaffold a tool, emit infra, and
|
|
2735
|
-
# gather THAT tool's keys \u2192 GitHub${pushed ? "" : "\n (
|
|
2643
|
+
# gather THAT tool's keys \u2192 GitHub${pushed ? "" : "\n (it also prompts the base tokens if they are not set yet)"}
|
|
2736
2644
|
2. set the HCP backend (cloud{} org + workspace) in infra/main.tf # docs/terraform-state.md
|
|
2737
2645
|
3. commit + push \u2192 CI (.github/workflows/infra.yml) runs \`terraform apply\`
|
|
2738
2646
|
4. greenlight verify <name> --env prod | greenlight doctor`);
|
|
2739
2647
|
}
|
|
2740
2648
|
|
|
2741
2649
|
// src/commands/migrations.ts
|
|
2742
|
-
import { existsSync as
|
|
2650
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
|
|
2743
2651
|
import { join as join5 } from "path";
|
|
2744
2652
|
var DEFAULT_DIR = "supabase/migrations";
|
|
2745
2653
|
var CANDIDATE_DIRS = [
|
|
@@ -2751,7 +2659,7 @@ var CANDIDATE_DIRS = [
|
|
|
2751
2659
|
];
|
|
2752
2660
|
function resolveMigrationsDir(explicit, root = process.cwd()) {
|
|
2753
2661
|
if (explicit) return explicit;
|
|
2754
|
-
return CANDIDATE_DIRS.find((d) =>
|
|
2662
|
+
return CANDIDATE_DIRS.find((d) => existsSync9(join5(root, d))) ?? DEFAULT_DIR;
|
|
2755
2663
|
}
|
|
2756
2664
|
async function migrationsCommand(args) {
|
|
2757
2665
|
if (args[0] !== "scan") {
|
|
@@ -2777,7 +2685,7 @@ async function migrationsCommand(args) {
|
|
|
2777
2685
|
}
|
|
2778
2686
|
const files = names.map((f) => ({
|
|
2779
2687
|
path: join5(dir, f),
|
|
2780
|
-
content:
|
|
2688
|
+
content: readFileSync6(join5(dir, f), "utf8")
|
|
2781
2689
|
}));
|
|
2782
2690
|
const findings = scanSqlFiles(files);
|
|
2783
2691
|
if (findings.length === 0) {
|
|
@@ -2802,12 +2710,12 @@ ${verdict} (${dangers.length} danger, ${findings.length - dangers.length} warn).
|
|
|
2802
2710
|
|
|
2803
2711
|
// src/commands/preview.ts
|
|
2804
2712
|
import { execFileSync as execFileSync5, spawn } from "child_process";
|
|
2805
|
-
import { resolve as
|
|
2713
|
+
import { resolve as resolve9 } from "path";
|
|
2806
2714
|
import { setTimeout as sleep } from "timers/promises";
|
|
2807
2715
|
|
|
2808
2716
|
// src/commands/verify.ts
|
|
2809
2717
|
import { spawnSync } from "child_process";
|
|
2810
|
-
import { resolve as
|
|
2718
|
+
import { resolve as resolve8 } from "path";
|
|
2811
2719
|
function defaultSpec(lane) {
|
|
2812
2720
|
switch (lane) {
|
|
2813
2721
|
case "astro":
|
|
@@ -2935,7 +2843,7 @@ ${pass2 ? "\u2714 ALL PASS" : "\u2718 FAIL"} (${reports2.length} specs)`);
|
|
|
2935
2843
|
if (reachableTimeoutMs > 0) {
|
|
2936
2844
|
console.log(`waiting up to ${reachableTimeoutMs / 1e3}s for ${url} to become reachable\u2026`);
|
|
2937
2845
|
}
|
|
2938
|
-
const toolDir =
|
|
2846
|
+
const toolDir = resolve8(process.cwd(), entry.dir ?? ".");
|
|
2939
2847
|
const reports = await verifyAll(url, specs, { reachableTimeoutMs, toolDir });
|
|
2940
2848
|
attachFailureLogs(reports, specs, toolDir);
|
|
2941
2849
|
for (const report of reports) printReport(report);
|
|
@@ -2979,7 +2887,7 @@ async function verifyLocal(entry, url) {
|
|
|
2979
2887
|
process.env.GREENLIGHT_PREVIEW = "1";
|
|
2980
2888
|
process.env.GREENLIGHT_VERIFY_URL = url;
|
|
2981
2889
|
const specs = await loadSpecs(entry);
|
|
2982
|
-
const toolDir =
|
|
2890
|
+
const toolDir = resolve9(process.cwd(), entry.dir ?? ".");
|
|
2983
2891
|
const reports = await verifyAll(url, specs, { toolDir });
|
|
2984
2892
|
for (const report of reports) printReport(report);
|
|
2985
2893
|
return allPass(reports);
|
|
@@ -2990,7 +2898,7 @@ async function previewViaDescriptor(entry, name, portOverride) {
|
|
|
2990
2898
|
const port = portOverride ?? pv.port ?? entry.port ?? lane.port;
|
|
2991
2899
|
const path = pv.path ?? lane.path;
|
|
2992
2900
|
const url = `http://localhost:${port}${path}`;
|
|
2993
|
-
const toolDir =
|
|
2901
|
+
const toolDir = resolve9(process.cwd(), entry.dir ?? ".");
|
|
2994
2902
|
console.log(`preview ${name}: ${pv.command} (\u2192 ${url})`);
|
|
2995
2903
|
const child = spawn(pv.command, {
|
|
2996
2904
|
cwd: toolDir,
|
|
@@ -3303,7 +3211,6 @@ var HELP = `greenlight <command>
|
|
|
3303
3211
|
promote <name> [--perform] [--push] gated develop -> main fast-forward
|
|
3304
3212
|
status <name> last ship/deploy/verify run for a tool (via gh)
|
|
3305
3213
|
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
3214
|
agent sync [<name>] write the loop kit (named \u2192 tool-aware, into its dir)
|
|
3308
3215
|
adopt <name> --repo <path> --lane --target onboard an existing tool repo as a thin consumer
|
|
3309
3216
|
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.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-adapters": "0.
|
|
35
|
-
"@rtrentjones/greenlight-
|
|
36
|
-
"@rtrentjones/greenlight-
|
|
37
|
-
"@rtrentjones/greenlight-
|
|
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"
|