@rtrentjones/greenlight 0.2.4 → 0.2.6

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,19 @@ 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`** — on the **wrapper** repo, scoped **Commit statuses: write** on the
29
+ **tool** → the wrapper posts deploy/verify status back to the tool's commit.
30
+
31
+ Provider creds (OCI/Cloudflare/…) live **only in the wrapper**; the tool repo holds just the
32
+ dispatch PAT (its build pushes to GHCR with the built-in `github.token`).
33
+
21
34
  ## Secrets sync
22
35
 
23
36
  `greenlight secrets sync [--repo o/r] [--env <env>]` pushes `.greenlight/secrets.env` to the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: provider-oci
3
- description: How Oracle Cloud (OCI) works in a Greenlight setup — the `target: oci` runtime for stateful MCP servers (BAMCP) on a free-tier Ampere A1 Container Instance, the provider-agnostic build-via-GitHub→GHCR model, Greenlight-owned compute + tunnel Terraform, the OCI token CLI, deploy = restart, and the Always-Free idle-reclaim trap (PAYG, manual). Use when wiring/debugging an oci-target tool.
3
+ description: How Oracle Cloud (OCI) works in a Greenlight setup — the `target: oci` runtime for stateful MCP servers (BAMCP) on a free-tier Ampere A1 Container Instance, the provider-agnostic build-via-GitHub→GHCR model, Greenlight-owned compute + tunnel Terraform, the OCI token CLI, deploy = restart, and staying on the free tier (no PAYG; recover-on-alert). Use when wiring/debugging an oci-target tool.
4
4
  ---
5
5
 
6
6
  # provider-oci
@@ -34,11 +34,26 @@ The `oci` provider (auth below) is added to `infra/main.tf`.
34
34
 
35
35
  ## OCI token CLI
36
36
 
37
- `greenlight add`/`init` gather the OCI creds into `.greenlight/secrets.env` (+ GH secrets):
38
- **provider auth** `TF_VAR_oci_tenancy_ocid`, `TF_VAR_oci_user_ocid`, `TF_VAR_oci_fingerprint`,
39
- `TF_VAR_oci_private_key` (PEM), `TF_VAR_oci_region`; **placement** `TF_VAR_oci_compartment_id`,
40
- `TF_VAR_oci_availability_domain`, `TF_VAR_oci_subnet_id`; and `OCI_CONTAINER_INSTANCE_OCID`
41
- (the Terraform output, for deploy). Auth is API-key request signing — no bearer, so no fetch-verify.
37
+ `greenlight secrets gather <tool> --repo <o/r>` pushes the OCI creds straight to GitHub secrets
38
+ (hidden prompts, no disk/logs). **The only manual OCI inputs are the API-key auth values**
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`. `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.
44
+
45
+ **The VCN, subnet, and availability domain are NOT manual** — they're Terraform: the `oci-network`
46
+ module creates the VCN + a public (egress-only) subnet, and the container-instance module looks the
47
+ AD up via an `oci_identity_availability_domains` data source. So the bootstrap is just "create one
48
+ API key" — Terraform can't create the credential it needs to authenticate, but it owns everything
49
+ after that. (Out-of-A1-capacity in one AD? set `availability_domain` on the instance module to pin
50
+ another — the only time you touch it.)
51
+
52
+ **Shortcut — feed the API-key config preview directly.** After *Add API key*, OCI shows a
53
+ "Configuration file preview" (the `[DEFAULT]` block) and you download the `.pem`. Pass both:
54
+ `greenlight secrets gather <tool> --repo <o/r> --oci-config ~/path/config [--oci-key ~/path/key.pem]`
55
+ — it auto-fills the 5 auth secrets (incl. the multi-line PEM, read from the file so it's never
56
+ pasted) and only prompts for the 3 placement values + the option-B deploy PATs.
42
57
 
43
58
  ## Deploy = restart (re-pull)
44
59
 
@@ -46,11 +61,12 @@ The `oci` provider (auth below) is added to `infra/main.tf`.
46
61
  --container-instance-id <OCID>` — the instance re-pulls the latest GHCR image. The tool's CI
47
62
  builds; an event trigger (the chosen deploy option) fires the restart. The adapter does NOT build.
48
63
 
49
- ## The idle-reclaim trap fixed manually, NOT by code
64
+ ## Idle-reclaim — stay free, recover on alert
50
65
 
51
- OCI **Always-Free reclaims idle compute**; pings don't count, only account standing. Convert the
52
- tenancy to **Pay-As-You-Go** (+ a low billing budget alarm) a one-time manual change (see
53
- `docs/oci-payg-runbook.md`). Keepalive only **health-checks** + nags; it cannot stop reclaim.
66
+ OCI Always-Free can reclaim idle compute. We **stay on the free tier** and accept that: the
67
+ instance runs restart-policy ALWAYS, keepalive health-checks it, and if it's ever stopped/reclaimed
68
+ the alert fires and a **re-apply / redeploy** restores it. **PAYG is NOT used** — it's an optional
69
+ last resort (see `docs/oci-payg-runbook.md`) only if reclaim ever becomes a recurring problem.
54
70
 
55
71
  ## Verify
56
72
  The tool is typically an **MCP server**: verify with `mode: mcp`, connect at `<name>.<domain>/mcp`
package/dist/bin.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  resolveUrl,
7
7
  verifyAll
8
- } from "./chunk-KFKYLGFX.js";
8
+ } from "./chunk-LM6M3DIV.js";
9
9
  import "./chunk-XBDQJVAX.js";
10
10
  import "./chunk-WFZTRXBF.js";
11
11
  import "./chunk-KP3Y6WRU.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
  };
@@ -132,6 +133,7 @@ function resolveEntry(config, name) {
132
133
  name: void 0,
133
134
  lane: config.blog.lane,
134
135
  target: config.blog.target,
136
+ data: config.blog.data,
135
137
  dir: "apps/blog",
136
138
  external: false
137
139
  };
@@ -145,6 +147,7 @@ function resolveEntry(config, name) {
145
147
  name: tool.name,
146
148
  lane: tool.lane,
147
149
  target: tool.target,
150
+ data: tool.data,
148
151
  dir: tool.dir ?? `tools/${tool.name}`,
149
152
  external: tool.external
150
153
  };
@@ -183,15 +186,17 @@ var PACKS = [
183
186
  always: true,
184
187
  // the zone/DNS provider + Workers (keepalive) for every Greenlight setup
185
188
  appliesTo: () => true,
186
- 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)",
190
+ setupUrl: "https://dash.cloudflare.com/profile/api-tokens",
187
191
  tokens: [
188
192
  {
189
193
  envVar: "CLOUDFLARE_API_TOKEN",
190
- 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)",
191
195
  scopes: [
192
196
  "Account \xB7 Workers Scripts \xB7 Edit",
193
197
  "Zone \xB7 DNS \xB7 Edit",
194
- "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)"
195
200
  ],
196
201
  verify: async (t) => {
197
202
  const r = await fetch("https://api.cloudflare.com/client/v4/user/tokens/verify", {
@@ -214,6 +219,7 @@ var PACKS = [
214
219
  name: "Vercel",
215
220
  appliesTo: (t) => t.target === "vercel",
216
221
  guide: "docs/provider-tokens.md \u2014 VERCEL_API_TOKEN (team-scoped)",
222
+ setupUrl: "https://vercel.com/account/settings/tokens",
217
223
  tokens: [
218
224
  {
219
225
  envVar: "VERCEL_API_TOKEN",
@@ -235,6 +241,7 @@ var PACKS = [
235
241
  name: "Supabase",
236
242
  appliesTo: (t) => t.data === "supabase",
237
243
  guide: "docs/provider-tokens.md \u2014 SUPABASE_ACCESS_TOKEN (Management API)",
244
+ setupUrl: "https://supabase.com/dashboard/account/tokens",
238
245
  tokens: [
239
246
  {
240
247
  envVar: "SUPABASE_ACCESS_TOKEN",
@@ -269,6 +276,7 @@ var PACKS = [
269
276
  // remote state backs every wrapper's infra
270
277
  appliesTo: () => true,
271
278
  guide: "docs/terraform-state-r2.md \u2014 HCP Terraform free tier (no credit card)",
279
+ setupUrl: "https://app.terraform.io/app/settings/tokens",
272
280
  tokens: [
273
281
  {
274
282
  envVar: "TF_API_TOKEN",
@@ -290,6 +298,7 @@ var PACKS = [
290
298
  // secrets sync + repo/branch infra
291
299
  appliesTo: () => true,
292
300
  guide: "docs/provider-tokens.md \u2014 GitHub (gh auth, or a fine-grained PAT)",
301
+ setupUrl: "https://github.com/settings/personal-access-tokens/new",
293
302
  tokens: [
294
303
  {
295
304
  envVar: "GITHUB_TOKEN",
@@ -305,7 +314,8 @@ var PACKS = [
305
314
  id: "oci",
306
315
  name: "Oracle Cloud (OCI)",
307
316
  appliesTo: (t) => t.target === "oci",
308
- guide: "docs/oci-payg-runbook.md \u2014 Always-Free A1 Container Instance + tunnel (PAYG to stop reclaim)",
317
+ guide: "docs/oci-payg-runbook.md \u2014 Always-Free A1 Container Instance + tunnel (no PAYG)",
318
+ setupUrl: "https://cloud.oracle.com \u2014 Profile \u2192 User settings \u2192 Tokens and keys \u2192 Add API key",
309
319
  tokens: [
310
320
  // OCI provider auth = API-key request signing (no bearer → no cheap fetch verify). These
311
321
  // flow to the `oci` Terraform provider as TF_VAR_oci_* (the wrapper apply uses them).
@@ -318,16 +328,12 @@ var PACKS = [
318
328
  optional: true
319
329
  },
320
330
  { envVar: "TF_VAR_oci_region", label: "OCI region, e.g. us-ashburn-1", optional: true },
321
- // Container Instance placement (your Always-Free compartment / AD / a public subnet).
322
- { envVar: "TF_VAR_oci_compartment_id", label: "OCI compartment OCID", optional: true },
331
+ // Compartment is the ONLY placement input, and it's optional blank the tenancy (root)
332
+ // compartment. The VCN/subnet are created by the oci-network module and the availability
333
+ // domain is looked up by a data source, so neither is a manual secret anymore.
323
334
  {
324
- envVar: "TF_VAR_oci_availability_domain",
325
- label: "OCI availability domain",
326
- optional: true
327
- },
328
- {
329
- envVar: "TF_VAR_oci_subnet_id",
330
- label: "OCI subnet OCID (public, for egress)",
335
+ envVar: "TF_VAR_oci_compartment_id",
336
+ label: "OCI compartment OCID (optional \u2014 blank uses the tenancy/root compartment)",
331
337
  optional: true
332
338
  },
333
339
  // Deploy (restart the instance → re-pull). Set from the Terraform output.
@@ -335,11 +341,25 @@ var PACKS = [
335
341
  envVar: "OCI_CONTAINER_INSTANCE_OCID",
336
342
  label: "container instance OCID (TF output) \u2014 `greenlight deploy` restarts it",
337
343
  optional: true
344
+ },
345
+ // Option-B event-driven deploy (GitHub PATs). dispatch → set on the TOOL repo;
346
+ // status → set on the WRAPPER repo. Skip the one that doesn't match `--repo`.
347
+ {
348
+ envVar: "GREENLIGHT_DISPATCH_TOKEN",
349
+ label: "GitHub PAT, Contents:write on the WRAPPER (TOOL repo fires the deploy dispatch)",
350
+ optional: true,
351
+ setupUrl: "https://github.com/settings/personal-access-tokens/new"
352
+ },
353
+ {
354
+ envVar: "GREENLIGHT_STATUS_TOKEN",
355
+ label: "GitHub PAT, Commits:write on the TOOL (WRAPPER posts deploy status back)",
356
+ optional: true,
357
+ setupUrl: "https://github.com/settings/personal-access-tokens/new"
338
358
  }
339
359
  ],
340
360
  skill: "provider-oci",
341
- tfModules: ["tool", "tunnel", "oci-container-instance"]
342
- // DNS + tunnel + compute; deploy = restart
361
+ // DNS + tunnel + network (VCN/subnet) + compute; deploy = restart.
362
+ tfModules: ["tool", "tunnel", "oci-network", "oci-container-instance"]
343
363
  }
344
364
  ];
345
365
  function packsForTool(tool) {
@@ -367,7 +387,7 @@ function tokensForTool(tool) {
367
387
  }
368
388
 
369
389
  // src/version.ts
370
- var MODULE_REF = "v0.2.4";
390
+ var MODULE_REF = "v0.2.6";
371
391
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
372
392
  function moduleSource(module, ref = MODULE_REF) {
373
393
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -377,6 +397,7 @@ function moduleSource(module, ref = MODULE_REF) {
377
397
  var hcl = (s) => s.replace(/\n{3,}/g, "\n\n").trimEnd();
378
398
  function emitToolTf(opts) {
379
399
  const { name, domain, lane, target, data, envs, ref = MODULE_REF } = opts;
400
+ const port = opts.port ?? 8e3;
380
401
  const slug = opts.slug ?? `OWNER/${name}`;
381
402
  const useSupabase = data === "supabase";
382
403
  const useVercel = target === "vercel";
@@ -384,13 +405,7 @@ function emitToolTf(opts) {
384
405
  const envList = envs.map((e) => `"${e}"`).join(", ");
385
406
  const blocks = [];
386
407
  const assumes = ["var.cloudflare_zone_id"];
387
- if (useOci)
388
- assumes.push(
389
- "var.cloudflare_account_id",
390
- "var.oci_compartment_id",
391
- "var.oci_availability_domain",
392
- "var.oci_subnet_id"
393
- );
408
+ if (useOci) assumes.push("var.cloudflare_account_id", "local.oci_compartment_id");
394
409
  if (useSupabase) assumes.push("var.supabase_organization_id", "var.supabase_database_password");
395
410
  const ghcrOwner = (slug.split("/")[0] ?? "owner").toLowerCase();
396
411
  blocks.push(
@@ -455,7 +470,7 @@ variable "${name}_vercel_project_id" {
455
470
  }
456
471
  if (useOci) {
457
472
  blocks.push(`# OCI Container Instance (Always-Free Ampere A1) running the tool's GHCR image + a cloudflared
458
- # sidecar; the tunnel routes ${name}.${domain} \u2192 the container at localhost:8000. The tool's OWN
473
+ # sidecar; the tunnel routes ${name}.${domain} \u2192 the container at localhost:${port}. The tool's OWN
459
474
  # CI builds + pushes the image (provider-agnostic); deploy = restart the instance (re-pull).
460
475
  # beta would be a second instance + tunnel route \u2014 mind the free 2-OCPU / 12-GB A1 cap.
461
476
  module "${name}_tunnel" {
@@ -464,19 +479,27 @@ module "${name}_tunnel" {
464
479
  account_id = var.cloudflare_account_id
465
480
  name = "${name}-tunnel"
466
481
  ingress = [
467
- { hostname = "${name}.${domain}", service = "http://localhost:8000" },
482
+ { hostname = "${name}.${domain}", service = "http://localhost:${port}" },
468
483
  ]
469
484
  }
470
485
 
486
+ # Network is IaC too \u2014 VCN + public subnet (egress only). No hand-clicking in the OCI console.
487
+ module "${name}_network" {
488
+ source = "${moduleSource("oci-network", ref)}"
489
+
490
+ name = "${name}"
491
+ compartment_id = local.oci_compartment_id
492
+ }
493
+
471
494
  module "${name}_instance" {
472
495
  source = "${moduleSource("oci-container-instance", ref)}"
473
496
 
474
- name = "${name}"
475
- compartment_id = var.oci_compartment_id
476
- availability_domain = var.oci_availability_domain
477
- subnet_id = var.oci_subnet_id
478
- image_url = var.${name}_image
479
- tunnel_token = module.${name}_tunnel.token
497
+ name = "${name}"
498
+ compartment_id = local.oci_compartment_id
499
+ subnet_id = module.${name}_network.subnet_id
500
+ image_url = var.${name}_image
501
+ tunnel_token = module.${name}_tunnel.token
502
+ # availability_domain is auto-picked (first AD in the compartment); set it to pin a specific AD.
480
503
 
481
504
  # Tool runtime env \u2014 fill in (e.g. PORT/listen settings, auth). The container must listen on 8000.
482
505
  environment = {}
@@ -540,11 +563,13 @@ function emitWrapperMainTf(opts) {
540
563
  if (need.has("supabase")) providerBlocks.push('provider "supabase" {}');
541
564
  if (need.has("oci")) {
542
565
  providerBlocks.push(`provider "oci" {
543
- tenancy_ocid = var.oci_tenancy_ocid
544
- user_ocid = var.oci_user_ocid
545
- fingerprint = var.oci_fingerprint
566
+ # trimspace guards against a trailing newline/space in a pasted secret (a malformed region
567
+ # makes the identity endpoint hostname fail to resolve \u2014 "no such host" \u2014 at plan time).
568
+ tenancy_ocid = trimspace(var.oci_tenancy_ocid)
569
+ user_ocid = trimspace(var.oci_user_ocid)
570
+ fingerprint = trimspace(var.oci_fingerprint)
546
571
  private_key = var.oci_private_key
547
- region = var.oci_region
572
+ region = trimspace(var.oci_region)
548
573
  }`);
549
574
  }
550
575
  const vars = ['variable "cloudflare_zone_id" { type = string }'];
@@ -561,10 +586,16 @@ function emitWrapperMainTf(opts) {
561
586
  vars.push('variable "oci_fingerprint" { type = string }');
562
587
  vars.push('variable "oci_private_key" {\n type = string\n sensitive = true\n}');
563
588
  vars.push('variable "oci_region" { type = string }');
564
- vars.push('variable "oci_compartment_id" { type = string }');
565
- vars.push('variable "oci_availability_domain" { type = string }');
566
- vars.push('variable "oci_subnet_id" { type = string }');
589
+ vars.push(
590
+ 'variable "oci_compartment_id" {\n type = string\n default = "" # blank \u2192 tenancy (root) compartment\n}'
591
+ );
567
592
  }
593
+ const localsBlock = need.has("oci") ? `
594
+ locals {
595
+ # Compartment for all OCI tools \u2014 blank var.oci_compartment_id falls back to the tenancy (root).
596
+ oci_compartment_id = var.oci_compartment_id != "" ? var.oci_compartment_id : var.oci_tenancy_ocid
597
+ }
598
+ ` : "";
568
599
  return `# Wrapper infra (singleton): providers + remote-state backend + shared variables.
569
600
  # \`greenlight add\` appends per-tool module blocks as infra/<name>.tf. Apply is CI/CD's job
570
601
  # (infra.yml). Fill in the HCP backend below before the first apply (docs/terraform-state-r2.md).
@@ -585,7 +616,7 @@ ${req.join("\n")}
585
616
  ${providerBlocks.join("\n")}
586
617
 
587
618
  ${vars.join("\n")}
588
- `;
619
+ ${localsBlock}`;
589
620
  }
590
621
  function providersForTool(tool) {
591
622
  const ids = new Set(packsForTool(tool).map((p) => p.id));
@@ -599,12 +630,13 @@ function providersForTool(tool) {
599
630
  // src/tokens.ts
600
631
  import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
601
632
  import { resolve as resolve4 } from "path";
602
- import { createInterface } from "readline/promises";
633
+ import { createInterface as createInterface2 } from "readline/promises";
603
634
 
604
635
  // src/commands/secrets.ts
605
636
  import { execFileSync } from "child_process";
606
637
  import { existsSync as existsSync3, readFileSync } from "fs";
607
638
  import { resolve as resolve3 } from "path";
639
+ import { createInterface } from "readline";
608
640
  function parseSecretsEnv(text) {
609
641
  const out = [];
610
642
  for (const raw of text.split("\n")) {
@@ -616,6 +648,36 @@ function parseSecretsEnv(text) {
616
648
  }
617
649
  return out;
618
650
  }
651
+ function parseOciConfig(text) {
652
+ const out = {};
653
+ for (const raw of text.split("\n")) {
654
+ const line = raw.trim();
655
+ if (line === "" || line.startsWith("#") || line.startsWith("[")) continue;
656
+ const eq = line.indexOf("=");
657
+ if (eq <= 0) continue;
658
+ const key = line.slice(0, eq).trim().toLowerCase();
659
+ if (!(key in out)) out[key] = line.slice(eq + 1).trim();
660
+ }
661
+ return out;
662
+ }
663
+ function ociPrefill(configPath, keyPath) {
664
+ const cfg = parseOciConfig(readFileSync(configPath, "utf8"));
665
+ const map = /* @__PURE__ */ new Map();
666
+ const set = (k, v) => {
667
+ if (v) map.set(k, v);
668
+ };
669
+ set("TF_VAR_OCI_USER_OCID", cfg.user);
670
+ set("TF_VAR_OCI_FINGERPRINT", cfg.fingerprint);
671
+ set("TF_VAR_OCI_TENANCY_OCID", cfg.tenancy);
672
+ set("TF_VAR_OCI_REGION", cfg.region);
673
+ const pem = keyPath ?? cfg.key_file;
674
+ if (pem && existsSync3(pem)) {
675
+ map.set("TF_VAR_OCI_PRIVATE_KEY", readFileSync(pem, "utf8"));
676
+ } else if (pem) {
677
+ console.log(` ! PEM not found at ${pem} \u2014 set TF_VAR_OCI_PRIVATE_KEY manually (--oci-key)`);
678
+ }
679
+ return map;
680
+ }
619
681
  function parseRepo(remoteUrl) {
620
682
  const m = remoteUrl.trim().match(/github\.com[/:]([^/]+)\/(.+?)(?:\.git)?$/);
621
683
  return m ? `${m[1]}/${m[2]}` : null;
@@ -669,12 +731,135 @@ function syncSecrets(opts) {
669
731
  }
670
732
  return { repo, count: entries.length };
671
733
  }
734
+ function hiddenPrompter() {
735
+ const tty = Boolean(process.stdin.isTTY);
736
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: tty });
737
+ if (tty) rl._writeToOutput = () => {
738
+ };
739
+ return {
740
+ ask: (query) => new Promise((resolve11) => {
741
+ process.stdout.write(query);
742
+ rl.question("", (val) => {
743
+ process.stdout.write("\n");
744
+ resolve11(val.trim());
745
+ });
746
+ }),
747
+ close: () => rl.close()
748
+ };
749
+ }
750
+ function listGitHubSecrets(repo, env) {
751
+ const ghArgs = ["secret", "list", "--repo", repo, "--json", "name"];
752
+ if (env) ghArgs.push("--env", env);
753
+ try {
754
+ const out = execFileSync("gh", ghArgs, {
755
+ encoding: "utf8",
756
+ stdio: ["ignore", "pipe", "ignore"]
757
+ // don't leak gh's stderr into the guided flow
758
+ });
759
+ const parsed = JSON.parse(out);
760
+ return new Set(parsed.map((s) => s.name));
761
+ } catch {
762
+ return null;
763
+ }
764
+ }
765
+ function setGitHubSecret(repo, env, key, value) {
766
+ const ghArgs = ["secret", "set", key, "--repo", repo];
767
+ if (env) ghArgs.push("--env", env);
768
+ try {
769
+ execFileSync("gh", ghArgs, { input: value });
770
+ } catch (e) {
771
+ const err = e;
772
+ if (err.code === "ENOENT") {
773
+ throw new Error("the GitHub CLI `gh` is required \u2014 install it and run `gh auth login`");
774
+ }
775
+ const detail = err.stderr?.toString().trim();
776
+ throw new Error(`failed to set ${key}${detail ? `: ${detail}` : " (check `gh auth status`)"}`);
777
+ }
778
+ }
779
+ async function gatherSecrets(name, repo, env, prefill) {
780
+ const { config } = await loadManifest();
781
+ const entry = resolveEntry(config, name);
782
+ const packs = packsForTool({ target: entry.target, data: entry.data });
783
+ const dest = env ? `env "${env}" of ${repo}` : repo;
784
+ const existing = listGitHubSecrets(repo, env);
785
+ console.log(`Gathering secrets for "${name}" \u2192 GitHub ${dest}`);
786
+ console.log(
787
+ "Paste each value (hidden); Enter to skip. Values go straight to GitHub \u2014 never to disk."
788
+ );
789
+ console.log(
790
+ `[already set] = a value exists (paste to override, Enter to keep) \xB7 [not set] = new.${existing ? "" : " (could not read existing secrets \u2014 annotations omitted)"}`
791
+ );
792
+ if (prefill?.size) console.log(`Auto-filling ${prefill.size} value(s) from the OCI config.`);
793
+ console.log("");
794
+ const prompt = hiddenPrompter();
795
+ let pushed = 0;
796
+ try {
797
+ for (const pack of packs) {
798
+ console.log(`\u2500\u2500 ${pack.name}${pack.setupUrl ? ` \u2192 ${pack.setupUrl}` : ""}`);
799
+ for (const tok of pack.tokens) {
800
+ const key = tok.envVar.toUpperCase();
801
+ if (key === "GITHUB_TOKEN") {
802
+ console.log(" \xB7 GITHUB_TOKEN \u2014 provided automatically by Actions; skipping");
803
+ continue;
804
+ }
805
+ const pre = prefill?.get(key);
806
+ if (pre) {
807
+ setGitHubSecret(repo, env, key, pre);
808
+ console.log(` \u2714 ${existing?.has(key) ? "overrode" : "pushed"} ${key} \u2190 OCI config`);
809
+ pushed++;
810
+ continue;
811
+ }
812
+ if (tok.scopes?.length) console.log(` scopes: ${tok.scopes.join(", ")}`);
813
+ if (tok.setupUrl) console.log(` link: ${tok.setupUrl}`);
814
+ const state = existing ? existing.has(key) ? " [already set]" : " [not set]" : "";
815
+ const value = await prompt.ask(` ${key} \u2014 ${tok.label}${state}
816
+ value: `);
817
+ if (!value) {
818
+ console.log(existing?.has(key) ? " \xB7 kept existing" : " \xB7 skipped");
819
+ continue;
820
+ }
821
+ if (tok.verify) {
822
+ const check = await tok.verify(value, {}).catch((e) => ({ ok: false, detail: e instanceof Error ? e.message : String(e) }));
823
+ if (!check.ok) {
824
+ console.log(
825
+ ` \u2716 verify failed${check.detail ? ` (${check.detail})` : ""} \u2014 not pushed`
826
+ );
827
+ continue;
828
+ }
829
+ console.log(" \u2714 verified");
830
+ }
831
+ setGitHubSecret(repo, env, key, value);
832
+ const verb = existing?.has(key) ? "overrode" : "pushed";
833
+ console.log(` \u2714 ${verb} ${key} \u2192 ${repo}`);
834
+ pushed++;
835
+ }
836
+ }
837
+ } finally {
838
+ prompt.close();
839
+ }
840
+ console.log(`
841
+ ${pushed} secret(s) pushed to ${repo}. (None written to disk.)`);
842
+ }
672
843
  async function secretsCommand(args) {
673
- if (args[0] !== "sync") {
844
+ const sub = args[0];
845
+ if (sub === "gather") {
846
+ const name = args[1];
847
+ if (!name || name.startsWith("-")) {
848
+ throw new Error("usage: greenlight secrets gather <name> [--repo owner/repo] [--env <env>]");
849
+ }
850
+ const repo = flag(args, "--repo") ?? detectRepo(process.cwd());
851
+ if (!repo) throw new Error("could not determine the repo \u2014 pass --repo owner/repo");
852
+ const ociConfig2 = flag(args, "--oci-config");
853
+ const ociKey = flag(args, "--oci-key");
854
+ const prefill = ociConfig2 ? ociPrefill(resolve3(process.cwd(), ociConfig2), ociKey && resolve3(process.cwd(), ociKey)) : void 0;
855
+ await gatherSecrets(name, repo, flag(args, "--env"), prefill);
856
+ return;
857
+ }
858
+ if (sub !== "sync") {
674
859
  console.log(
675
- "usage: greenlight secrets sync [--repo owner/repo] [--env <env>] # push .greenlight/secrets.env to GitHub Actions secrets"
860
+ "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"
676
861
  );
677
- process.exit(args[0] ? 1 : 0);
862
+ process.exit(sub ? 1 : 0);
678
863
  }
679
864
  const { count } = syncSecrets({
680
865
  cwd: process.cwd(),
@@ -724,7 +909,7 @@ async function ensureTokensForTool(cwd, tool, opts = {}) {
724
909
  const interactive = Boolean(process.stdin.isTTY);
725
910
  const env = presentEnv(cwd);
726
911
  const results = [];
727
- const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
912
+ const rl = interactive ? createInterface2({ input: process.stdin, output: process.stdout }) : null;
728
913
  try {
729
914
  for (const spec of tokensForTool(tool)) {
730
915
  let value = env[spec.envVar];
@@ -859,7 +1044,7 @@ async function addCommand(args) {
859
1044
  const name = args[0];
860
1045
  if (!name || name.startsWith("-")) {
861
1046
  throw new Error(
862
- "usage: greenlight add <name> --lane <lane> --target <target> [--data <d>] [--auth <a>] [--envs beta,prod]"
1047
+ "usage: greenlight add <name> --lane <lane> --target <target> [--data <d>] [--auth <a>] [--envs beta,prod] [--port 8000]"
863
1048
  );
864
1049
  }
865
1050
  const lane = flag2(args, "--lane");
@@ -869,13 +1054,15 @@ async function addCommand(args) {
869
1054
  if (path.endsWith(".example.ts")) {
870
1055
  throw new Error("no greenlight.config.ts \u2014 run `greenlight init` first");
871
1056
  }
1057
+ const portFlag = flag2(args, "--port");
872
1058
  const next = addTool(config, {
873
1059
  name,
874
1060
  lane,
875
1061
  target,
876
1062
  data: flag2(args, "--data"),
877
1063
  auth: flag2(args, "--auth"),
878
- envs: flag2(args, "--envs")?.split(",")
1064
+ envs: flag2(args, "--envs")?.split(","),
1065
+ port: portFlag ? Number(portFlag) : void 0
879
1066
  });
880
1067
  const entry = next.tools.find((t) => t.name === name);
881
1068
  const data = entry?.data ?? "none";
@@ -914,7 +1101,10 @@ async function addCommand(args) {
914
1101
  if (existsSync6(toolTf)) {
915
1102
  console.log(`\xB7 infra/${name}.tf exists \u2014 left as-is`);
916
1103
  } else {
917
- writeFileSync3(toolTf, emitToolTf({ name, domain: config.domain, lane, target, data, envs }));
1104
+ writeFileSync3(
1105
+ toolTf,
1106
+ emitToolTf({ name, domain: config.domain, lane, target, data, envs, port: entry?.port })
1107
+ );
918
1108
  console.log(`\u2714 wrote infra/${name}.tf (modules: ${providers.join(", ")})`);
919
1109
  }
920
1110
  if (!args.includes("--no-tokens")) {
@@ -1157,7 +1347,6 @@ jobs:
1157
1347
  `;
1158
1348
  }
1159
1349
  function deployListenerYml(name, toolRepo) {
1160
- const SECRET = `${name.toUpperCase().replace(/-/g, "_")}_OCI_CONTAINER_INSTANCE_OCID`;
1161
1350
  return `name: greenlight-deploy-${name}
1162
1351
 
1163
1352
  # Option B: ${toolRepo} fires repository_dispatch(deploy-${name}) after pushing a new image.
@@ -1177,15 +1366,39 @@ jobs:
1177
1366
  - uses: jdx/mise-action@v2
1178
1367
  - run: pnpm install --frozen-lockfile
1179
1368
  - run: pip install --quiet oci-cli
1180
- - name: Deploy (restart container instance -> re-pull GHCR image)
1369
+ - name: Deploy (resolve instance OCID by name -> restart -> re-pull GHCR image)
1181
1370
  env:
1182
- OCI_CLI_USER: \${{ secrets.OCI_CLI_USER }}
1183
- OCI_CLI_TENANCY: \${{ secrets.OCI_CLI_TENANCY }}
1184
- OCI_CLI_FINGERPRINT: \${{ secrets.OCI_CLI_FINGERPRINT }}
1185
- OCI_CLI_KEY_CONTENT: \${{ secrets.OCI_CLI_KEY_CONTENT }}
1186
- OCI_CLI_REGION: \${{ secrets.OCI_CLI_REGION }}
1187
- OCI_CONTAINER_INSTANCE_OCID: \${{ secrets.${SECRET} }}
1188
- run: pnpm exec greenlight deploy ${name} --env prod
1371
+ # The OCI CLI reuses the SAME TF_VAR_OCI_* secrets the apply uses \u2014 one secret set.
1372
+ OCI_CLI_TENANCY: \${{ secrets.TF_VAR_OCI_TENANCY_OCID }}
1373
+ OCI_CLI_USER: \${{ secrets.TF_VAR_OCI_USER_OCID }}
1374
+ OCI_CLI_FINGERPRINT: \${{ secrets.TF_VAR_OCI_FINGERPRINT }}
1375
+ OCI_CLI_KEY_CONTENT: \${{ secrets.TF_VAR_OCI_PRIVATE_KEY }}
1376
+ OCI_CLI_REGION: \${{ secrets.TF_VAR_OCI_REGION }}
1377
+ OCI_COMPARTMENT_ID: \${{ secrets.TF_VAR_OCI_COMPARTMENT_ID }}
1378
+ run: |
1379
+ # The instance OCID is abstracted from the developer: resolve it from OCI by the
1380
+ # instance's display name (= the tool name, set by the oci-container-instance module).
1381
+ # No manually-fetched/stored OCID secret. Compartment falls back to the tenancy (root).
1382
+ set -o pipefail
1383
+ COMPARTMENT="\${OCI_COMPARTMENT_ID:-$OCI_CLI_TENANCY}"
1384
+ echo "Resolving '${name}' (region=$OCI_CLI_REGION)\u2026"
1385
+ oci container-instances container-instance list \\
1386
+ --compartment-id "$COMPARTMENT" --display-name ${name} --all > list.json \\
1387
+ || { echo "::error::oci list failed (auth/region/compartment) \u2014 see output above"; exit 1; }
1388
+ # OCIDs are identifiers, not secrets (tenancy/compartment are masked by Actions).
1389
+ echo "Matches:"; jq -r '(.data.items // .data // [])[]? | " \\(.["lifecycle-state"] // "?") \\(.id)"' list.json
1390
+ OCID=$(jq -r '[(.data.items // .data // [])[]? | select((.["lifecycle-state"] // "")=="ACTIVE") | .id][0] // ""' list.json)
1391
+ if [ -z "$OCID" ]; then
1392
+ echo "::error::no ACTIVE container instance named '${name}' in compartment $COMPARTMENT"
1393
+ exit 1
1394
+ fi
1395
+ echo "Resolved ${name} instance: $OCID"
1396
+ OCI_CONTAINER_INSTANCE_OCID="$OCID" pnpm exec greenlight deploy ${name} --env prod
1397
+ - name: Verify prod (gate the signal on real health, not just the restart)
1398
+ # The deploy "succeeds" only if the NEW image is actually serving. verify has a built-in
1399
+ # readiness wait (re-pull + container start). A failure here fails the job \u2192 the status
1400
+ # posted back is red. oci is verify-gated direct-to-prod (no cheap standing beta on free A1).
1401
+ run: pnpm exec greenlight verify ${name} --env prod
1189
1402
  - name: Report status back to ${toolRepo}
1190
1403
  if: \${{ always() && github.event.client_payload.sha != '' }}
1191
1404
  env:
@@ -1306,8 +1519,9 @@ Next:
1306
1519
  (in the wrapper) review infra/${name}.tf, then commit the submodule + infra + listener:
1307
1520
  git add .gitmodules ${toolRel} infra/${name}.tf verify/${name}.config.ts greenlight.config.ts .github
1308
1521
  git commit && git push # CI (infra.yml) applies. Tool's CI builds; wrapper deploys.${target === "oci" ? `
1309
- Secrets: in ${slug} set GREENLIGHT_DISPATCH_TOKEN; in this wrapper set the OCI_CLI_* creds +
1310
- ${name.toUpperCase().replace(/-/g, "_")}_OCI_CONTAINER_INSTANCE_OCID (from the TF output) + GREENLIGHT_STATUS_TOKEN.` : ""}`);
1522
+ Secrets (guided): greenlight secrets gather ${name} --repo <wrapper> # TF_VAR_OCI_* + GREENLIGHT_STATUS_TOKEN
1523
+ greenlight secrets gather ${name} --repo ${slug} # GREENLIGHT_DISPATCH_TOKEN
1524
+ The instance OCID is auto-resolved by the deploy workflow (by display name) \u2014 nothing to set.` : ""}`);
1311
1525
  }
1312
1526
  async function adoptStandalone(ctx) {
1313
1527
  const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
@@ -1529,7 +1743,7 @@ async function deployCommand(args) {
1529
1743
  }
1530
1744
  const { config } = await loadManifest();
1531
1745
  const entry = resolveEntry(config, name);
1532
- if (entry.external) {
1746
+ if (entry.external && entry.target !== "oci") {
1533
1747
  throw new Error(`"${name}" is external (registry pointer) \u2014 deploy it from its own repo`);
1534
1748
  }
1535
1749
  const adapter = createAdapter(entry.target, { domain: config.domain, name: entry.name });
@@ -1613,7 +1827,7 @@ ${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}`);
1613
1827
  // src/commands/init.ts
1614
1828
  import { existsSync as existsSync9, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
1615
1829
  import { resolve as resolve8 } from "path";
1616
- import { createInterface as createInterface2 } from "readline/promises";
1830
+ import { createInterface as createInterface3 } from "readline/promises";
1617
1831
  function flag5(args, name) {
1618
1832
  const i = args.indexOf(name);
1619
1833
  return i >= 0 ? args[i + 1] : void 0;
@@ -1630,7 +1844,7 @@ async function initCommand(args) {
1630
1844
  let domain = flag5(args, "--domain");
1631
1845
  if (!domain) {
1632
1846
  if (!process.stdin.isTTY) throw new Error("init needs --domain <domain> (no TTY for prompts)");
1633
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
1847
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
1634
1848
  domain = (await rl.question("Domain (e.g. example.dev): ")).trim();
1635
1849
  rl.close();
1636
1850
  }
@@ -1901,6 +2115,7 @@ var HELP = `greenlight <command>
1901
2115
  preview <name> [--port <n>] build + serve locally + verify (one command)
1902
2116
  verify <name> [--env <env> | --url <url>] run the verify harness against the URL
1903
2117
  promote <name> [--perform] [--push] gated develop -> main fast-forward
2118
+ secrets gather <name> [--repo o/r] [--env e] guided, link-first token prompts -> GitHub secrets (no disk/logs)
1904
2119
  secrets sync [--repo o/r] [--env <env>] push .greenlight/secrets.env -> GitHub Actions secrets
1905
2120
  agent sync write the loop skill + CLAUDE.md block into this repo
1906
2121
  adopt <name> --repo <path> --lane --target onboard an existing tool repo as a thin consumer
@@ -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(),
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  defineConfig,
3
3
  defineVerify,
4
4
  loadConfig
5
- } from "./chunk-KFKYLGFX.js";
5
+ } from "./chunk-LM6M3DIV.js";
6
6
  import "./chunk-XBDQJVAX.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.4",
3
+ "version": "0.2.6",
4
4
  "description": "Greenlight CLI — setup and lifecycle for the harness.",
5
5
  "license": "MIT",
6
6
  "repository": {