@rtrentjones/greenlight 0.2.8 → 0.2.9

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.
@@ -42,6 +42,13 @@ wrapper deploy listener. `greenlight adopt … --target vercel` emits, into the
42
42
  `greenlight verify --url <url> --spec <path>` is the **manifest-free** mode that makes this work
43
43
  without carrying the wrapper's `greenlight.config.ts` into the tool repo.
44
44
 
45
+ **Deployment Protection gotcha:** `deployment_status.target_url` is the `*.vercel.app` *deployment*
46
+ URL, which Vercel **Deployment Protection** gates (→ **401**) even though the public custom domain
47
+ is 200. To verify the real app, create a **Protection Bypass for Automation** secret (Vercel →
48
+ project → Settings → Deployment Protection) and set it as `VERCEL_AUTOMATION_BYPASS_SECRET` on the
49
+ tool repo — the api check sends it as `x-vercel-protection-bypass` and asserts 200. Without it the
50
+ generated spec asserts **401** (the deployment is served + protected), so the gate stays green.
51
+
45
52
  ## MCP
46
53
 
47
54
  `.mcp.json` wires `vercel` (hosted, OAuth, read-only). Run `/mcp` and authenticate in the
package/dist/bin.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  resolveUrl,
7
7
  verifyAll
8
- } from "./chunk-VONSDNH4.js";
8
+ } from "./chunk-JRCATCRY.js";
9
9
  import "./chunk-ADS6BJJ5.js";
10
10
  import "./chunk-WFZTRXBF.js";
11
11
  import "./chunk-KP3Y6WRU.js";
@@ -105,6 +105,30 @@ function addTool(config, t) {
105
105
  }
106
106
  return result.data;
107
107
  }
108
+ function upsertTool(config, t) {
109
+ if (t.name === "blog") throw new Error('"blog" is a reserved name');
110
+ const entry = {
111
+ name: t.name,
112
+ lane: t.lane,
113
+ target: t.target,
114
+ data: t.data ?? "none",
115
+ auth: t.auth ?? "none",
116
+ access: t.access ?? "public",
117
+ envs: t.envs ?? ["beta", "prod"],
118
+ ...t.dir !== void 0 ? { dir: t.dir } : {},
119
+ ...t.adopted ? { adopted: true } : {},
120
+ ...t.external ? { external: true } : {},
121
+ ...t.port !== void 0 ? { port: t.port } : {}
122
+ };
123
+ const tools = config.tools.some((x) => x.name === t.name) ? config.tools.map((x) => x.name === t.name ? entry : x) : [...config.tools, entry];
124
+ const result = ConfigSchema.safeParse({ ...config, tools });
125
+ if (!result.success) {
126
+ throw new Error(
127
+ result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
128
+ );
129
+ }
130
+ return result.data;
131
+ }
108
132
 
109
133
  // src/manifest.ts
110
134
  import { existsSync as existsSync2 } from "fs";
@@ -390,7 +414,7 @@ function tokensForTool(tool) {
390
414
  }
391
415
 
392
416
  // src/version.ts
393
- var MODULE_REF = "v0.2.8";
417
+ var MODULE_REF = "v0.2.9";
394
418
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
395
419
  function moduleSource(module, ref = MODULE_REF) {
396
420
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -1443,14 +1467,14 @@ jobs:
1443
1467
  - uses: actions/setup-node@v4
1444
1468
  with:
1445
1469
  node-version: '24'
1446
- - name: Install deps (for test-mode)
1447
- run: |
1448
- corepack enable || true
1449
- if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile;
1450
- elif [ -f yarn.lock ]; then yarn install --frozen-lockfile;
1451
- else npm ci; fi
1470
+ # agent-web needs browsers: add \`- run: npx -y playwright install --with-deps chromium\` when
1471
+ # you set ANTHROPIC_API_KEY. test-mode needs the tool's deps: add a tolerant install step
1472
+ # (\`pnpm install --no-frozen-lockfile\`) \u2014 but unit tests usually belong in the tool's PR CI.
1452
1473
  - name: Verify the deployment
1453
1474
  env:
1475
+ # Bypass Vercel Deployment Protection on the deployment URL (Vercel \u2192 project \u2192 Deployment
1476
+ # Protection \u2192 Protection Bypass for Automation). Without it the gate asserts 401 (served).
1477
+ VERCEL_AUTOMATION_BYPASS_SECRET: \${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
1454
1478
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
1455
1479
  run: npx -y @rtrentjones/greenlight@latest verify --url "\${{ github.event.deployment_status.target_url }}" --spec verify/${name}.config.ts
1456
1480
  `;
@@ -1458,10 +1482,20 @@ jobs:
1458
1482
  function nextVerifyConfig(name) {
1459
1483
  return `// Greenlight verify spec for ${name} (next/vercel) \u2014 run by .github/workflows/greenlight-verify.yml
1460
1484
  // after Vercel deploys (deployment_status). An array combines modes (allPass):
1461
- // - api: the deployed URL serves (200).
1462
- // - test: this tool's own suite \u2014 set the real command for your package manager.
1485
+ // - api: deployment_status' target_url is the *.vercel.app deployment URL, which Vercel Deployment
1486
+ // Protection gates (401). With VERCEL_AUTOMATION_BYPASS_SECRET set we send the bypass header and
1487
+ // assert 200 (the real app); without it we assert 401 (the deployment is served + protected).
1463
1488
  // - agent-web: an LLM drives the live UI; runs ONLY when ANTHROPIC_API_KEY is set (else omitted,
1464
1489
  // so the gate stays green). Replace the scenario with real user tasks + assertions.
1490
+ // Unit tests belong in this repo's PR CI; to also gate the deploy on them, add
1491
+ // { mode: 'test', command: 'pnpm test' } + a tolerant deps-install step in greenlight-verify.yml.
1492
+ const bypass = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
1493
+ const api = bypass
1494
+ ? {
1495
+ mode: 'api',
1496
+ checks: [{ path: '/', status: 200, requestHeaders: { 'x-vercel-protection-bypass': bypass } }],
1497
+ }
1498
+ : { mode: 'api', checks: [{ path: '/', status: 401 }] };
1465
1499
  const agentWeb = process.env.ANTHROPIC_API_KEY
1466
1500
  ? [
1467
1501
  {
@@ -1477,11 +1511,7 @@ const agentWeb = process.env.ANTHROPIC_API_KEY
1477
1511
  ]
1478
1512
  : [];
1479
1513
 
1480
- export default [
1481
- { mode: 'api', checks: [{ path: '/', status: 200 }] },
1482
- { mode: 'test', command: 'npm test' },
1483
- ...agentWeb,
1484
- ];
1514
+ export default [api, ...agentWeb];
1485
1515
  `;
1486
1516
  }
1487
1517
  function writeIfAbsent(path, contents, label) {
@@ -1514,8 +1544,8 @@ async function adoptCommand(args) {
1514
1544
  "run adopt from your site repo (needs a real greenlight.config.ts; run `greenlight init` first)"
1515
1545
  );
1516
1546
  }
1517
- if (reg.tools.some((t) => t.name === name) || name === "blog") {
1518
- throw new Error(`"${name}" already in the registry`);
1547
+ if (name === "blog") {
1548
+ throw new Error('"blog" is the apex site, not an adopted tool');
1519
1549
  }
1520
1550
  const domain = flag3(args, "--domain") ?? reg.domain;
1521
1551
  const ctx = { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath };
@@ -1542,7 +1572,8 @@ async function adoptWrapper(ctx) {
1542
1572
  } else {
1543
1573
  console.log(`\xB7 ${toolRel} exists \u2014 skipping submodule add`);
1544
1574
  }
1545
- const nextReg = addTool(reg, {
1575
+ const existed = reg.tools.some((x) => x.name === name);
1576
+ const nextReg = upsertTool(reg, {
1546
1577
  name,
1547
1578
  lane,
1548
1579
  target,
@@ -1554,7 +1585,9 @@ async function adoptWrapper(ctx) {
1554
1585
  adopted: true
1555
1586
  });
1556
1587
  writeFileSync4(regPath, serializeConfig(nextReg));
1557
- console.log(`\u2714 registered "${name}" (external, dir ${toolRel}) in the wrapper manifest`);
1588
+ console.log(
1589
+ `\u2714 ${existed ? "updated" : "registered"} "${name}" (external, dir ${toolRel}) in the wrapper manifest`
1590
+ );
1558
1591
  const slug = parseRepo(repoArg) ?? parseRepo(safeGit(dest, ["remote", "get-url", "origin"])) ?? `OWNER/${name}`;
1559
1592
  writeIfAbsent(
1560
1593
  join2(cwd, `infra/${name}.tf`),
@@ -116,7 +116,7 @@ var trimSlash = (s) => s.replace(/\/+$/, "");
116
116
  async function checkRoute(base, c) {
117
117
  const name = `GET ${c.path}`;
118
118
  try {
119
- const res = await fetch(base + c.path, { redirect: "manual" });
119
+ const res = await fetch(base + c.path, { redirect: "manual", headers: c.requestHeaders });
120
120
  const reasons = [];
121
121
  if (c.status !== void 0 && res.status !== c.status) {
122
122
  reasons.push(`status ${res.status} != ${c.status}`);
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  defineConfig,
3
3
  defineVerify,
4
4
  loadConfig
5
- } from "./chunk-VONSDNH4.js";
5
+ } from "./chunk-JRCATCRY.js";
6
6
  import "./chunk-ADS6BJJ5.js";
7
7
  import "./chunk-WFZTRXBF.js";
8
8
  import "./chunk-KP3Y6WRU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Greenlight CLI — setup and lifecycle for the harness.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,9 +32,9 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@rtrentjones/greenlight-adapters": "0.2.4",
35
+ "@rtrentjones/greenlight-loop": "0.2.4",
35
36
  "@rtrentjones/greenlight-shared": "0.2.4",
36
- "@rtrentjones/greenlight-verify": "0.2.4",
37
- "@rtrentjones/greenlight-loop": "0.2.4"
37
+ "@rtrentjones/greenlight-verify": "0.2.4"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-assets.mjs && tsup",