@rtrentjones/greenlight 0.2.5 → 0.2.7

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/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # @rtrentjones/greenlight
2
+
3
+ The Greenlight CLI — setup and lifecycle for the [Greenlight](https://github.com/RTrentJones/greenlight)
4
+ harness. Greenlight is a **clone-and-own** baseline that turns a domain + API tokens into a live
5
+ personal site plus a self-verifying agentic deploy loop, with plug-and-play subdomain tools (web apps
6
+ or MCP servers). It is provider-agnostic and free-tier-first, and it **edits declarative
7
+ infrastructure-as-code — your CI/CD applies it**. It is not a hosted PaaS.
8
+
9
+ This is the **single published package**: the CLI, with the framework libraries
10
+ (`shared`/`verify`/`adapters`/`loop`) bundled in. The Terraform modules are distributed as git tags
11
+ (pinned in lockstep with this package's version); the skills ship as a Claude Code plugin.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @rtrentjones/greenlight # or npm i / yarn add
17
+ ```
18
+
19
+ A personal site is a **thin consumer** that depends on this package and owns only its manifest
20
+ (`greenlight.config.ts`) + content. Update with `pnpm update @rtrentjones/greenlight` — no framework
21
+ code to merge.
22
+
23
+ Optional peer features lazy-load and degrade to a failing check if absent (never a crash):
24
+
25
+ ```bash
26
+ pnpm add -D playwright @anthropic-ai/sdk # only for verify modes agent-web / eval
27
+ ```
28
+
29
+ ## CLI
30
+
31
+ ```bash
32
+ greenlight <command>
33
+ ```
34
+
35
+ | Command | What it does |
36
+ |---|---|
37
+ | `init --domain <d>` | scaffold the manifest + secrets store |
38
+ | `add <name> --lane <l> --target <t> [--data --auth --envs]` | **infra editor**: manifest entry → emit `infra/<name>.tf` + gather/verify tokens + wire the kit (never applies) |
39
+ | `adopt <name> --repo <url\|path> --lane --target` | onboard an existing tool repo (submodule wrapper, or `--standalone`) |
40
+ | `secrets gather <name> [--repo o/r] [--oci-config <path>]` | guided, link-first token prompts straight to GitHub secrets (no disk, no logs) |
41
+ | `secrets sync [--repo o/r] [--env <env>]` | push `.greenlight/secrets.env` → GitHub Actions secrets |
42
+ | `agent sync` | materialize the agent loop kit (skill + `.mcp.json` + CLAUDE block) into a repo |
43
+ | `preview <name>` | build + serve locally + verify in one command |
44
+ | `verify <name> --env <beta\|prod>` (or `--url`) | run the shared verify harness |
45
+ | `promote <name>` | gated `develop → main` fast-forward (after beta verify) |
46
+ | `deploy <name>` | target deploy hook (e.g. OCI restart = re-pull) |
47
+ | `doctor` / `config` | health checks / load + validate + print the manifest |
48
+
49
+ ## The loop
50
+
51
+ ```
52
+ greenlight add notes --lane mcp --target oci # one entry → Terraform + tokens + kit
53
+ greenlight verify notes --env beta # api | mcp | playwright | test | agent-web | eval
54
+ greenlight promote notes # gated develop → main, after beta verify passes
55
+ ```
56
+
57
+ The `verify` gate is the same code CI **and** the agent run — so changes ship with objective
58
+ confidence, not vibes.
59
+
60
+ ## Programmatic API
61
+
62
+ Typed helpers for your `greenlight.config.ts` and `verify.config.ts`:
63
+
64
+ ```ts
65
+ import { defineConfig, defineVerify } from '@rtrentjones/greenlight';
66
+
67
+ export default defineConfig({
68
+ domain: 'you.dev',
69
+ tools: { notes: { lane: 'mcp', target: 'oci', data: 'none', auth: 'bearer' } },
70
+ });
71
+ ```
72
+
73
+ Also exported: `loadConfig`, and the `GreenlightConfig` / `VerifySpec` types.
74
+
75
+ ## Links
76
+
77
+ - **Repo + full docs:** <https://github.com/RTrentJones/greenlight>
78
+ - **Architecture:** [docs/architecture.md](https://github.com/RTrentJones/greenlight/blob/main/docs/architecture.md)
79
+ - **Spec:** [greenlight-v1.md](https://github.com/RTrentJones/greenlight/blob/main/greenlight-v1.md)
80
+
81
+ MIT
@@ -12,10 +12,12 @@ blog and throwaway MCP dev targets.
12
12
 
13
13
  ## Token — `CLOUDFLARE_API_TOKEN`
14
14
 
15
- One token, **two scopes** (the trap that took down a live apply):
15
+ One token, these scopes (a missing scope took down a live apply more than once):
16
16
  - **Account · Workers Scripts · Edit** — deploy the keepalive worker / workers-target tools.
17
17
  - **Zone · DNS · Edit** — the subdomain CNAMEs.
18
18
  - **Account · Account Settings · Read** — read account id.
19
+ - **Account · Cloudflare Tunnel · Edit** — only if a tool uses `target: oci` (the cloudflared
20
+ tunnel). Without it, the tunnel resource fails with **403 Forbidden** on `cfd_tunnel` at apply.
19
21
 
20
22
  Create at dash → My Profile → API Tokens → Custom Token. Store in `.greenlight/secrets.env`
21
23
  (gitignored) and push to GitHub Actions with `greenlight secrets sync`. `greenlight add`
@@ -18,6 +18,20 @@ workflows. The `develop → main` flow is standardized (PR → preview, `develop
18
18
  use a fine-grained **PAT** with the minimal scopes (e.g. `Secrets: write`, `Administration`
19
19
  for protection). Prefer **GitHub OIDC → cloud** over long-lived cloud tokens where supported.
20
20
 
21
+ ### Poly-repo deploy loop — the two option-B PATs
22
+
23
+ An adopted tool (submodule) and its wrapper hand off via two fine-grained PATs (`secrets gather`
24
+ pushes each to the right repo; see docs/provider-tokens.md):
25
+
26
+ - **`GREENLIGHT_DISPATCH_TOKEN`** — on the **tool** repo, scoped **Contents: write** on the
27
+ **wrapper** → the tool's build fires `repository_dispatch` so the wrapper deploys.
28
+ - **`GREENLIGHT_STATUS_TOKEN_<TOOL>`** — on the **wrapper** repo, scoped **Commit statuses: write**
29
+ on the **tool** → the wrapper posts deploy/verify status back to the tool's commit. **Per-tool
30
+ suffix** (e.g. `…_BAMCP`) because it lives on the shared wrapper alongside other tools' tokens.
31
+
32
+ Provider creds (OCI/Cloudflare/…) live **only in the wrapper**; the tool repo holds just the
33
+ dispatch PAT (its build pushes to GHCR with the built-in `github.token`).
34
+
21
35
  ## Secrets sync
22
36
 
23
37
  `greenlight secrets sync [--repo o/r] [--env <env>]` pushes `.greenlight/secrets.env` to the
@@ -37,9 +37,10 @@ The `oci` provider (auth below) is added to `infra/main.tf`.
37
37
  `greenlight secrets gather <tool> --repo <o/r>` pushes the OCI creds straight to GitHub secrets
38
38
  (hidden prompts, no disk/logs). **The only manual OCI inputs are the API-key auth values** —
39
39
  `TF_VAR_OCI_TENANCY_OCID`, `TF_VAR_OCI_USER_OCID`, `TF_VAR_OCI_FINGERPRINT`, `TF_VAR_OCI_PRIVATE_KEY`
40
- (PEM), `TF_VAR_OCI_REGION` — plus `OCI_CONTAINER_INSTANCE_OCID` (the Terraform output, set after the
41
- first apply, for deploy). `TF_VAR_OCI_COMPARTMENT_ID` is **optional** (blank the tenancy/root
42
- compartment). Auth is API-key request signingno bearer, so no fetch-verify.
40
+ (PEM), `TF_VAR_OCI_REGION`. `TF_VAR_OCI_COMPARTMENT_ID` is **optional** (blank the tenancy/root
41
+ compartment). Auth is API-key request signing no bearer, so no fetch-verify. The container
42
+ instance OCID is **not** a manual input the deploy workflow resolves it at deploy time from OCI
43
+ by the instance's display name (= the tool name), so it's abstracted from the developer.
43
44
 
44
45
  **The VCN, subnet, and availability domain are NOT manual** — they're Terraform: the `oci-network`
45
46
  module creates the VCN + a public (egress-only) subnet, and the container-instance module looks the
package/dist/bin.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  loadConfig,
6
6
  resolveUrl,
7
7
  verifyAll
8
- } from "./chunk-KFKYLGFX.js";
9
- import "./chunk-XBDQJVAX.js";
8
+ } from "./chunk-VONSDNH4.js";
9
+ import "./chunk-ADS6BJJ5.js";
10
10
  import "./chunk-WFZTRXBF.js";
11
11
  import "./chunk-KP3Y6WRU.js";
12
12
  import "./chunk-UXHHLEYO.js";
@@ -92,7 +92,8 @@ function addTool(config, t) {
92
92
  envs: t.envs ?? ["beta", "prod"],
93
93
  ...t.dir !== void 0 ? { dir: t.dir } : {},
94
94
  ...t.adopted ? { adopted: true } : {},
95
- ...t.external ? { external: true } : {}
95
+ ...t.external ? { external: true } : {},
96
+ ...t.port !== void 0 ? { port: t.port } : {}
96
97
  }
97
98
  ]
98
99
  };
@@ -185,16 +186,17 @@ var PACKS = [
185
186
  always: true,
186
187
  // the zone/DNS provider + Workers (keepalive) for every Greenlight setup
187
188
  appliesTo: () => true,
188
- guide: "docs/provider-tokens.md \u2014 CLOUDFLARE_API_TOKEN (Workers Scripts:Edit + Zone DNS:Edit)",
189
+ guide: "docs/provider-tokens.md \u2014 CLOUDFLARE_API_TOKEN (Workers Scripts:Edit + Zone DNS:Edit + Cloudflare Tunnel:Edit for oci tools)",
189
190
  setupUrl: "https://dash.cloudflare.com/profile/api-tokens",
190
191
  tokens: [
191
192
  {
192
193
  envVar: "CLOUDFLARE_API_TOKEN",
193
- label: "API token \u2014 Workers Scripts:Edit + Zone DNS:Edit",
194
+ label: "API token \u2014 Workers Scripts:Edit + Zone DNS:Edit (+ Cloudflare Tunnel:Edit for oci)",
194
195
  scopes: [
195
196
  "Account \xB7 Workers Scripts \xB7 Edit",
196
197
  "Zone \xB7 DNS \xB7 Edit",
197
- "Account \xB7 Account Settings \xB7 Read"
198
+ "Account \xB7 Account Settings \xB7 Read",
199
+ "Account \xB7 Cloudflare Tunnel \xB7 Edit (only if a tool uses target: oci)"
198
200
  ],
199
201
  verify: async (t) => {
200
202
  const r = await fetch("https://api.cloudflare.com/client/v4/user/tokens/verify", {
@@ -349,9 +351,12 @@ var PACKS = [
349
351
  setupUrl: "https://github.com/settings/personal-access-tokens/new"
350
352
  },
351
353
  {
354
+ // Stored on the shared wrapper, scoped to THIS tool's repo → per-tool name
355
+ // (GREENLIGHT_STATUS_TOKEN_<TOOL>) so multiple tools' status tokens don't collide.
352
356
  envVar: "GREENLIGHT_STATUS_TOKEN",
353
- label: "GitHub PAT, Commits:write on the TOOL (WRAPPER posts deploy status back)",
357
+ label: "GitHub PAT, Commit statuses:write on the TOOL (WRAPPER posts deploy status back)",
354
358
  optional: true,
359
+ perTool: true,
355
360
  setupUrl: "https://github.com/settings/personal-access-tokens/new"
356
361
  }
357
362
  ],
@@ -385,7 +390,7 @@ function tokensForTool(tool) {
385
390
  }
386
391
 
387
392
  // src/version.ts
388
- var MODULE_REF = "v0.2.5";
393
+ var MODULE_REF = "v0.2.7";
389
394
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
390
395
  function moduleSource(module, ref = MODULE_REF) {
391
396
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -395,6 +400,7 @@ function moduleSource(module, ref = MODULE_REF) {
395
400
  var hcl = (s) => s.replace(/\n{3,}/g, "\n\n").trimEnd();
396
401
  function emitToolTf(opts) {
397
402
  const { name, domain, lane, target, data, envs, ref = MODULE_REF } = opts;
403
+ const port = opts.port ?? 8e3;
398
404
  const slug = opts.slug ?? `OWNER/${name}`;
399
405
  const useSupabase = data === "supabase";
400
406
  const useVercel = target === "vercel";
@@ -467,7 +473,7 @@ variable "${name}_vercel_project_id" {
467
473
  }
468
474
  if (useOci) {
469
475
  blocks.push(`# OCI Container Instance (Always-Free Ampere A1) running the tool's GHCR image + a cloudflared
470
- # sidecar; the tunnel routes ${name}.${domain} \u2192 the container at localhost:8000. The tool's OWN
476
+ # sidecar; the tunnel routes ${name}.${domain} \u2192 the container at localhost:${port}. The tool's OWN
471
477
  # CI builds + pushes the image (provider-agnostic); deploy = restart the instance (re-pull).
472
478
  # beta would be a second instance + tunnel route \u2014 mind the free 2-OCPU / 12-GB A1 cap.
473
479
  module "${name}_tunnel" {
@@ -476,7 +482,7 @@ module "${name}_tunnel" {
476
482
  account_id = var.cloudflare_account_id
477
483
  name = "${name}-tunnel"
478
484
  ingress = [
479
- { hostname = "${name}.${domain}", service = "http://localhost:8000" },
485
+ { hostname = "${name}.${domain}", service = "http://localhost:${port}" },
480
486
  ]
481
487
  }
482
488
 
@@ -560,11 +566,13 @@ function emitWrapperMainTf(opts) {
560
566
  if (need.has("supabase")) providerBlocks.push('provider "supabase" {}');
561
567
  if (need.has("oci")) {
562
568
  providerBlocks.push(`provider "oci" {
563
- tenancy_ocid = var.oci_tenancy_ocid
564
- user_ocid = var.oci_user_ocid
565
- fingerprint = var.oci_fingerprint
569
+ # trimspace guards against a trailing newline/space in a pasted secret (a malformed region
570
+ # makes the identity endpoint hostname fail to resolve \u2014 "no such host" \u2014 at plan time).
571
+ tenancy_ocid = trimspace(var.oci_tenancy_ocid)
572
+ user_ocid = trimspace(var.oci_user_ocid)
573
+ fingerprint = trimspace(var.oci_fingerprint)
566
574
  private_key = var.oci_private_key
567
- region = var.oci_region
575
+ region = trimspace(var.oci_region)
568
576
  }`);
569
577
  }
570
578
  const vars = ['variable "cloudflare_zone_id" { type = string }'];
@@ -792,7 +800,8 @@ async function gatherSecrets(name, repo, env, prefill) {
792
800
  for (const pack of packs) {
793
801
  console.log(`\u2500\u2500 ${pack.name}${pack.setupUrl ? ` \u2192 ${pack.setupUrl}` : ""}`);
794
802
  for (const tok of pack.tokens) {
795
- const key = tok.envVar.toUpperCase();
803
+ const suffix = `_${name.toUpperCase().replace(/-/g, "_")}`;
804
+ const key = tok.envVar.toUpperCase() + (tok.perTool ? suffix : "");
796
805
  if (key === "GITHUB_TOKEN") {
797
806
  console.log(" \xB7 GITHUB_TOKEN \u2014 provided automatically by Actions; skipping");
798
807
  continue;
@@ -1039,7 +1048,7 @@ async function addCommand(args) {
1039
1048
  const name = args[0];
1040
1049
  if (!name || name.startsWith("-")) {
1041
1050
  throw new Error(
1042
- "usage: greenlight add <name> --lane <lane> --target <target> [--data <d>] [--auth <a>] [--envs beta,prod]"
1051
+ "usage: greenlight add <name> --lane <lane> --target <target> [--data <d>] [--auth <a>] [--envs beta,prod] [--port 8000]"
1043
1052
  );
1044
1053
  }
1045
1054
  const lane = flag2(args, "--lane");
@@ -1049,13 +1058,15 @@ async function addCommand(args) {
1049
1058
  if (path.endsWith(".example.ts")) {
1050
1059
  throw new Error("no greenlight.config.ts \u2014 run `greenlight init` first");
1051
1060
  }
1061
+ const portFlag = flag2(args, "--port");
1052
1062
  const next = addTool(config, {
1053
1063
  name,
1054
1064
  lane,
1055
1065
  target,
1056
1066
  data: flag2(args, "--data"),
1057
1067
  auth: flag2(args, "--auth"),
1058
- envs: flag2(args, "--envs")?.split(",")
1068
+ envs: flag2(args, "--envs")?.split(","),
1069
+ port: portFlag ? Number(portFlag) : void 0
1059
1070
  });
1060
1071
  const entry = next.tools.find((t) => t.name === name);
1061
1072
  const data = entry?.data ?? "none";
@@ -1094,7 +1105,10 @@ async function addCommand(args) {
1094
1105
  if (existsSync6(toolTf)) {
1095
1106
  console.log(`\xB7 infra/${name}.tf exists \u2014 left as-is`);
1096
1107
  } else {
1097
- writeFileSync3(toolTf, emitToolTf({ name, domain: config.domain, lane, target, data, envs }));
1108
+ writeFileSync3(
1109
+ toolTf,
1110
+ emitToolTf({ name, domain: config.domain, lane, target, data, envs, port: entry?.port })
1111
+ );
1098
1112
  console.log(`\u2714 wrote infra/${name}.tf (modules: ${providers.join(", ")})`);
1099
1113
  }
1100
1114
  if (!args.includes("--no-tokens")) {
@@ -1307,13 +1321,13 @@ concurrency:
1307
1321
 
1308
1322
  jobs:
1309
1323
  build:
1310
- runs-on: ubuntu-latest
1324
+ # Native arm64 runner \u2014 builds the arm64 image directly (no QEMU emulation, much faster).
1325
+ runs-on: ubuntu-24.04-arm
1311
1326
  steps:
1312
1327
  - uses: actions/checkout@v4
1313
1328
  - name: Resolve image ref (GHCR namespaces are lowercase)
1314
1329
  id: img
1315
- run: echo "ref=ghcr.io/\${GITHUB_REPOSITORY_OWNER,,}/${name}:prod" >> "$GITHUB_OUTPUT"
1316
- - uses: docker/setup-qemu-action@v3
1330
+ run: echo "base=ghcr.io/\${GITHUB_REPOSITORY_OWNER,,}/${name}" >> "$GITHUB_OUTPUT"
1317
1331
  - uses: docker/setup-buildx-action@v3
1318
1332
  - uses: docker/login-action@v3
1319
1333
  with:
@@ -1325,7 +1339,12 @@ jobs:
1325
1339
  context: .
1326
1340
  platforms: linux/arm64
1327
1341
  push: true
1328
- tags: \${{ steps.img.outputs.ref }}
1342
+ # :prod is the moving deploy tag; :<sha> is immutable (rollback + deploy-identity).
1343
+ tags: |
1344
+ \${{ steps.img.outputs.base }}:prod
1345
+ \${{ steps.img.outputs.base }}:\${{ github.sha }}
1346
+ cache-from: type=gha
1347
+ cache-to: type=gha,mode=max
1329
1348
  - name: Notify wrapper to deploy
1330
1349
  env:
1331
1350
  GH_TOKEN: \${{ secrets.GREENLIGHT_DISPATCH_TOKEN }}
@@ -1356,7 +1375,7 @@ jobs:
1356
1375
  - uses: jdx/mise-action@v2
1357
1376
  - run: pnpm install --frozen-lockfile
1358
1377
  - run: pip install --quiet oci-cli
1359
- - name: Deploy (restart container instance -> re-pull GHCR image)
1378
+ - name: Deploy (resolve instance OCID by name -> restart -> re-pull GHCR image)
1360
1379
  env:
1361
1380
  # The OCI CLI reuses the SAME TF_VAR_OCI_* secrets the apply uses \u2014 one secret set.
1362
1381
  OCI_CLI_TENANCY: \${{ secrets.TF_VAR_OCI_TENANCY_OCID }}
@@ -1364,12 +1383,36 @@ jobs:
1364
1383
  OCI_CLI_FINGERPRINT: \${{ secrets.TF_VAR_OCI_FINGERPRINT }}
1365
1384
  OCI_CLI_KEY_CONTENT: \${{ secrets.TF_VAR_OCI_PRIVATE_KEY }}
1366
1385
  OCI_CLI_REGION: \${{ secrets.TF_VAR_OCI_REGION }}
1367
- OCI_CONTAINER_INSTANCE_OCID: \${{ secrets.OCI_CONTAINER_INSTANCE_OCID }}
1368
- run: pnpm exec greenlight deploy ${name} --env prod
1386
+ OCI_COMPARTMENT_ID: \${{ secrets.TF_VAR_OCI_COMPARTMENT_ID }}
1387
+ run: |
1388
+ # The instance OCID is abstracted from the developer: resolve it from OCI by the
1389
+ # instance's display name (= the tool name, set by the oci-container-instance module).
1390
+ # No manually-fetched/stored OCID secret. Compartment falls back to the tenancy (root).
1391
+ set -o pipefail
1392
+ COMPARTMENT="\${OCI_COMPARTMENT_ID:-$OCI_CLI_TENANCY}"
1393
+ echo "Resolving '${name}' (region=$OCI_CLI_REGION)\u2026"
1394
+ oci container-instances container-instance list \\
1395
+ --compartment-id "$COMPARTMENT" --display-name ${name} --all > list.json \\
1396
+ || { echo "::error::oci list failed (auth/region/compartment) \u2014 see output above"; exit 1; }
1397
+ # OCIDs are identifiers, not secrets (tenancy/compartment are masked by Actions).
1398
+ echo "Matches:"; jq -r '(.data.items // .data // [])[]? | " \\(.["lifecycle-state"] // "?") \\(.id)"' list.json
1399
+ OCID=$(jq -r '[(.data.items // .data // [])[]? | select((.["lifecycle-state"] // "")=="ACTIVE") | .id][0] // ""' list.json)
1400
+ if [ -z "$OCID" ]; then
1401
+ echo "::error::no ACTIVE container instance named '${name}' in compartment $COMPARTMENT"
1402
+ exit 1
1403
+ fi
1404
+ echo "Resolved ${name} instance: $OCID"
1405
+ OCI_CONTAINER_INSTANCE_OCID="$OCID" pnpm exec greenlight deploy ${name} --env prod
1406
+ - name: Verify prod (gate the signal on real health, not just the restart)
1407
+ # The deploy "succeeds" only if the NEW image is actually serving. verify has a built-in
1408
+ # readiness wait (re-pull + container start). A failure here fails the job \u2192 the status
1409
+ # posted back is red. oci is verify-gated direct-to-prod (no cheap standing beta on free A1).
1410
+ run: pnpm exec greenlight verify ${name} --env prod
1369
1411
  - name: Report status back to ${toolRepo}
1370
1412
  if: \${{ always() && github.event.client_payload.sha != '' }}
1371
1413
  env:
1372
- GH_TOKEN: \${{ secrets.GREENLIGHT_STATUS_TOKEN }}
1414
+ # Per-tool name: the status PAT lives on the shared wrapper, scoped to this tool's repo.
1415
+ GH_TOKEN: \${{ secrets.GREENLIGHT_STATUS_TOKEN_${name.toUpperCase().replace(/-/g, "_")} }}
1373
1416
  run: |
1374
1417
  [ -z "$GH_TOKEN" ] && exit 0
1375
1418
  gh api repos/${toolRepo}/statuses/\${{ github.event.client_payload.sha }} \\
@@ -1488,7 +1531,7 @@ Next:
1488
1531
  git commit && git push # CI (infra.yml) applies. Tool's CI builds; wrapper deploys.${target === "oci" ? `
1489
1532
  Secrets (guided): greenlight secrets gather ${name} --repo <wrapper> # TF_VAR_OCI_* + GREENLIGHT_STATUS_TOKEN
1490
1533
  greenlight secrets gather ${name} --repo ${slug} # GREENLIGHT_DISPATCH_TOKEN
1491
- After apply, set OCI_CONTAINER_INSTANCE_OCID (the TF output) in the wrapper.` : ""}`);
1534
+ The instance OCID is auto-resolved by the deploy workflow (by display name) \u2014 nothing to set.` : ""}`);
1492
1535
  }
1493
1536
  async function adoptStandalone(ctx) {
1494
1537
  const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
@@ -1710,7 +1753,7 @@ async function deployCommand(args) {
1710
1753
  }
1711
1754
  const { config } = await loadManifest();
1712
1755
  const entry = resolveEntry(config, name);
1713
- if (entry.external) {
1756
+ if (entry.external && entry.target !== "oci") {
1714
1757
  throw new Error(`"${name}" is external (registry pointer) \u2014 deploy it from its own repo`);
1715
1758
  }
1716
1759
  const adapter = createAdapter(entry.target, { domain: config.domain, name: entry.name });
@@ -9,7 +9,10 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
9
9
  async function verifyMcp(baseUrl, spec) {
10
10
  const checks = [];
11
11
  const client = new Client({ name: "greenlight-verify", version: "0.0.0" });
12
- const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
12
+ const transport = new StreamableHTTPClientTransport(
13
+ new URL(baseUrl),
14
+ spec.headers ? { requestInit: { headers: spec.headers } } : void 0
15
+ );
13
16
  try {
14
17
  await client.connect(transport);
15
18
  checks.push({ name: "initialize handshake", pass: true });
@@ -33,6 +33,10 @@ var ToolSchema = z.object({
33
33
  access: AccessEnum.default("public"),
34
34
  envs: z.array(EnvEnum).nonempty("a tool needs at least one env"),
35
35
  adopted: z.boolean().default(false),
36
+ // The port the container listens on (target: oci). The tunnel routes to localhost:<port>;
37
+ // defaults to 8000 (the mcp/FastMCP convention). Set it for a lane:docker tool on a different
38
+ // port so the oci modules stay generic. Ignored by non-oci targets.
39
+ port: z.number().int().positive().optional(),
36
40
  // Directory the tool builds/deploys from. Defaults to tools/<name>; a standalone
37
41
  // (poly-repo) tool sets '.' (the repo root).
38
42
  dir: z.string().optional(),
@@ -225,7 +229,7 @@ async function verify(baseUrl, spec, opts) {
225
229
  case "api":
226
230
  return verifyApi(baseUrl, spec);
227
231
  case "mcp": {
228
- const { verifyMcp: verifyMcp2 } = await import("./mcp-KU7WKB5K.js");
232
+ const { verifyMcp: verifyMcp2 } = await import("./mcp-3L6HJ6BH.js");
229
233
  return verifyMcp2(baseUrl, spec);
230
234
  }
231
235
  case "playwright": {
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  defineConfig,
3
3
  defineVerify,
4
4
  loadConfig
5
- } from "./chunk-KFKYLGFX.js";
6
- import "./chunk-XBDQJVAX.js";
5
+ } from "./chunk-VONSDNH4.js";
6
+ import "./chunk-ADS6BJJ5.js";
7
7
  import "./chunk-WFZTRXBF.js";
8
8
  import "./chunk-KP3Y6WRU.js";
9
9
  import "./chunk-UXHHLEYO.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  verifyMcp
3
- } from "./chunk-XBDQJVAX.js";
3
+ } from "./chunk-ADS6BJJ5.js";
4
4
  import "./chunk-QFKE5JKC.js";
5
5
  export {
6
6
  verifyMcp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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",