@rtrentjones/greenlight 0.2.4 → 0.2.5

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.
@@ -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,25 @@ 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` 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 signing — no bearer, so no fetch-verify.
43
+
44
+ **The VCN, subnet, and availability domain are NOT manual** — they're Terraform: the `oci-network`
45
+ module creates the VCN + a public (egress-only) subnet, and the container-instance module looks the
46
+ AD up via an `oci_identity_availability_domains` data source. So the bootstrap is just "create one
47
+ API key" — Terraform can't create the credential it needs to authenticate, but it owns everything
48
+ after that. (Out-of-A1-capacity in one AD? set `availability_domain` on the instance module to pin
49
+ another — the only time you touch it.)
50
+
51
+ **Shortcut — feed the API-key config preview directly.** After *Add API key*, OCI shows a
52
+ "Configuration file preview" (the `[DEFAULT]` block) and you download the `.pem`. Pass both:
53
+ `greenlight secrets gather <tool> --repo <o/r> --oci-config ~/path/config [--oci-key ~/path/key.pem]`
54
+ — it auto-fills the 5 auth secrets (incl. the multi-line PEM, read from the file so it's never
55
+ pasted) and only prompts for the 3 placement values + the option-B deploy PATs.
42
56
 
43
57
  ## Deploy = restart (re-pull)
44
58
 
@@ -46,11 +60,12 @@ The `oci` provider (auth below) is added to `infra/main.tf`.
46
60
  --container-instance-id <OCID>` — the instance re-pulls the latest GHCR image. The tool's CI
47
61
  builds; an event trigger (the chosen deploy option) fires the restart. The adapter does NOT build.
48
62
 
49
- ## The idle-reclaim trap fixed manually, NOT by code
63
+ ## Idle-reclaim — stay free, recover on alert
50
64
 
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.
65
+ OCI Always-Free can reclaim idle compute. We **stay on the free tier** and accept that: the
66
+ instance runs restart-policy ALWAYS, keepalive health-checks it, and if it's ever stopped/reclaimed
67
+ the alert fires and a **re-apply / redeploy** restores it. **PAYG is NOT used** — it's an optional
68
+ last resort (see `docs/oci-payg-runbook.md`) only if reclaim ever becomes a recurring problem.
54
69
 
55
70
  ## Verify
56
71
  The tool is typically an **MCP server**: verify with `mode: mcp`, connect at `<name>.<domain>/mcp`
package/dist/bin.js CHANGED
@@ -132,6 +132,7 @@ function resolveEntry(config, name) {
132
132
  name: void 0,
133
133
  lane: config.blog.lane,
134
134
  target: config.blog.target,
135
+ data: config.blog.data,
135
136
  dir: "apps/blog",
136
137
  external: false
137
138
  };
@@ -145,6 +146,7 @@ function resolveEntry(config, name) {
145
146
  name: tool.name,
146
147
  lane: tool.lane,
147
148
  target: tool.target,
149
+ data: tool.data,
148
150
  dir: tool.dir ?? `tools/${tool.name}`,
149
151
  external: tool.external
150
152
  };
@@ -184,6 +186,7 @@ var PACKS = [
184
186
  // the zone/DNS provider + Workers (keepalive) for every Greenlight setup
185
187
  appliesTo: () => true,
186
188
  guide: "docs/provider-tokens.md \u2014 CLOUDFLARE_API_TOKEN (Workers Scripts:Edit + Zone DNS:Edit)",
189
+ setupUrl: "https://dash.cloudflare.com/profile/api-tokens",
187
190
  tokens: [
188
191
  {
189
192
  envVar: "CLOUDFLARE_API_TOKEN",
@@ -214,6 +217,7 @@ var PACKS = [
214
217
  name: "Vercel",
215
218
  appliesTo: (t) => t.target === "vercel",
216
219
  guide: "docs/provider-tokens.md \u2014 VERCEL_API_TOKEN (team-scoped)",
220
+ setupUrl: "https://vercel.com/account/settings/tokens",
217
221
  tokens: [
218
222
  {
219
223
  envVar: "VERCEL_API_TOKEN",
@@ -235,6 +239,7 @@ var PACKS = [
235
239
  name: "Supabase",
236
240
  appliesTo: (t) => t.data === "supabase",
237
241
  guide: "docs/provider-tokens.md \u2014 SUPABASE_ACCESS_TOKEN (Management API)",
242
+ setupUrl: "https://supabase.com/dashboard/account/tokens",
238
243
  tokens: [
239
244
  {
240
245
  envVar: "SUPABASE_ACCESS_TOKEN",
@@ -269,6 +274,7 @@ var PACKS = [
269
274
  // remote state backs every wrapper's infra
270
275
  appliesTo: () => true,
271
276
  guide: "docs/terraform-state-r2.md \u2014 HCP Terraform free tier (no credit card)",
277
+ setupUrl: "https://app.terraform.io/app/settings/tokens",
272
278
  tokens: [
273
279
  {
274
280
  envVar: "TF_API_TOKEN",
@@ -290,6 +296,7 @@ var PACKS = [
290
296
  // secrets sync + repo/branch infra
291
297
  appliesTo: () => true,
292
298
  guide: "docs/provider-tokens.md \u2014 GitHub (gh auth, or a fine-grained PAT)",
299
+ setupUrl: "https://github.com/settings/personal-access-tokens/new",
293
300
  tokens: [
294
301
  {
295
302
  envVar: "GITHUB_TOKEN",
@@ -305,7 +312,8 @@ var PACKS = [
305
312
  id: "oci",
306
313
  name: "Oracle Cloud (OCI)",
307
314
  appliesTo: (t) => t.target === "oci",
308
- guide: "docs/oci-payg-runbook.md \u2014 Always-Free A1 Container Instance + tunnel (PAYG to stop reclaim)",
315
+ guide: "docs/oci-payg-runbook.md \u2014 Always-Free A1 Container Instance + tunnel (no PAYG)",
316
+ setupUrl: "https://cloud.oracle.com \u2014 Profile \u2192 User settings \u2192 Tokens and keys \u2192 Add API key",
309
317
  tokens: [
310
318
  // OCI provider auth = API-key request signing (no bearer → no cheap fetch verify). These
311
319
  // flow to the `oci` Terraform provider as TF_VAR_oci_* (the wrapper apply uses them).
@@ -318,16 +326,12 @@ var PACKS = [
318
326
  optional: true
319
327
  },
320
328
  { 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 },
329
+ // Compartment is the ONLY placement input, and it's optional blank the tenancy (root)
330
+ // compartment. The VCN/subnet are created by the oci-network module and the availability
331
+ // domain is looked up by a data source, so neither is a manual secret anymore.
323
332
  {
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)",
333
+ envVar: "TF_VAR_oci_compartment_id",
334
+ label: "OCI compartment OCID (optional \u2014 blank uses the tenancy/root compartment)",
331
335
  optional: true
332
336
  },
333
337
  // Deploy (restart the instance → re-pull). Set from the Terraform output.
@@ -335,11 +339,25 @@ var PACKS = [
335
339
  envVar: "OCI_CONTAINER_INSTANCE_OCID",
336
340
  label: "container instance OCID (TF output) \u2014 `greenlight deploy` restarts it",
337
341
  optional: true
342
+ },
343
+ // Option-B event-driven deploy (GitHub PATs). dispatch → set on the TOOL repo;
344
+ // status → set on the WRAPPER repo. Skip the one that doesn't match `--repo`.
345
+ {
346
+ envVar: "GREENLIGHT_DISPATCH_TOKEN",
347
+ label: "GitHub PAT, Contents:write on the WRAPPER (TOOL repo fires the deploy dispatch)",
348
+ optional: true,
349
+ setupUrl: "https://github.com/settings/personal-access-tokens/new"
350
+ },
351
+ {
352
+ envVar: "GREENLIGHT_STATUS_TOKEN",
353
+ label: "GitHub PAT, Commits:write on the TOOL (WRAPPER posts deploy status back)",
354
+ optional: true,
355
+ setupUrl: "https://github.com/settings/personal-access-tokens/new"
338
356
  }
339
357
  ],
340
358
  skill: "provider-oci",
341
- tfModules: ["tool", "tunnel", "oci-container-instance"]
342
- // DNS + tunnel + compute; deploy = restart
359
+ // DNS + tunnel + network (VCN/subnet) + compute; deploy = restart.
360
+ tfModules: ["tool", "tunnel", "oci-network", "oci-container-instance"]
343
361
  }
344
362
  ];
345
363
  function packsForTool(tool) {
@@ -367,7 +385,7 @@ function tokensForTool(tool) {
367
385
  }
368
386
 
369
387
  // src/version.ts
370
- var MODULE_REF = "v0.2.4";
388
+ var MODULE_REF = "v0.2.5";
371
389
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
372
390
  function moduleSource(module, ref = MODULE_REF) {
373
391
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -384,13 +402,7 @@ function emitToolTf(opts) {
384
402
  const envList = envs.map((e) => `"${e}"`).join(", ");
385
403
  const blocks = [];
386
404
  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
- );
405
+ if (useOci) assumes.push("var.cloudflare_account_id", "local.oci_compartment_id");
394
406
  if (useSupabase) assumes.push("var.supabase_organization_id", "var.supabase_database_password");
395
407
  const ghcrOwner = (slug.split("/")[0] ?? "owner").toLowerCase();
396
408
  blocks.push(
@@ -468,15 +480,23 @@ module "${name}_tunnel" {
468
480
  ]
469
481
  }
470
482
 
483
+ # Network is IaC too \u2014 VCN + public subnet (egress only). No hand-clicking in the OCI console.
484
+ module "${name}_network" {
485
+ source = "${moduleSource("oci-network", ref)}"
486
+
487
+ name = "${name}"
488
+ compartment_id = local.oci_compartment_id
489
+ }
490
+
471
491
  module "${name}_instance" {
472
492
  source = "${moduleSource("oci-container-instance", ref)}"
473
493
 
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
494
+ name = "${name}"
495
+ compartment_id = local.oci_compartment_id
496
+ subnet_id = module.${name}_network.subnet_id
497
+ image_url = var.${name}_image
498
+ tunnel_token = module.${name}_tunnel.token
499
+ # availability_domain is auto-picked (first AD in the compartment); set it to pin a specific AD.
480
500
 
481
501
  # Tool runtime env \u2014 fill in (e.g. PORT/listen settings, auth). The container must listen on 8000.
482
502
  environment = {}
@@ -561,10 +581,16 @@ function emitWrapperMainTf(opts) {
561
581
  vars.push('variable "oci_fingerprint" { type = string }');
562
582
  vars.push('variable "oci_private_key" {\n type = string\n sensitive = true\n}');
563
583
  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 }');
584
+ vars.push(
585
+ 'variable "oci_compartment_id" {\n type = string\n default = "" # blank \u2192 tenancy (root) compartment\n}'
586
+ );
567
587
  }
588
+ const localsBlock = need.has("oci") ? `
589
+ locals {
590
+ # Compartment for all OCI tools \u2014 blank var.oci_compartment_id falls back to the tenancy (root).
591
+ oci_compartment_id = var.oci_compartment_id != "" ? var.oci_compartment_id : var.oci_tenancy_ocid
592
+ }
593
+ ` : "";
568
594
  return `# Wrapper infra (singleton): providers + remote-state backend + shared variables.
569
595
  # \`greenlight add\` appends per-tool module blocks as infra/<name>.tf. Apply is CI/CD's job
570
596
  # (infra.yml). Fill in the HCP backend below before the first apply (docs/terraform-state-r2.md).
@@ -585,7 +611,7 @@ ${req.join("\n")}
585
611
  ${providerBlocks.join("\n")}
586
612
 
587
613
  ${vars.join("\n")}
588
- `;
614
+ ${localsBlock}`;
589
615
  }
590
616
  function providersForTool(tool) {
591
617
  const ids = new Set(packsForTool(tool).map((p) => p.id));
@@ -599,12 +625,13 @@ function providersForTool(tool) {
599
625
  // src/tokens.ts
600
626
  import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
601
627
  import { resolve as resolve4 } from "path";
602
- import { createInterface } from "readline/promises";
628
+ import { createInterface as createInterface2 } from "readline/promises";
603
629
 
604
630
  // src/commands/secrets.ts
605
631
  import { execFileSync } from "child_process";
606
632
  import { existsSync as existsSync3, readFileSync } from "fs";
607
633
  import { resolve as resolve3 } from "path";
634
+ import { createInterface } from "readline";
608
635
  function parseSecretsEnv(text) {
609
636
  const out = [];
610
637
  for (const raw of text.split("\n")) {
@@ -616,6 +643,36 @@ function parseSecretsEnv(text) {
616
643
  }
617
644
  return out;
618
645
  }
646
+ function parseOciConfig(text) {
647
+ const out = {};
648
+ for (const raw of text.split("\n")) {
649
+ const line = raw.trim();
650
+ if (line === "" || line.startsWith("#") || line.startsWith("[")) continue;
651
+ const eq = line.indexOf("=");
652
+ if (eq <= 0) continue;
653
+ const key = line.slice(0, eq).trim().toLowerCase();
654
+ if (!(key in out)) out[key] = line.slice(eq + 1).trim();
655
+ }
656
+ return out;
657
+ }
658
+ function ociPrefill(configPath, keyPath) {
659
+ const cfg = parseOciConfig(readFileSync(configPath, "utf8"));
660
+ const map = /* @__PURE__ */ new Map();
661
+ const set = (k, v) => {
662
+ if (v) map.set(k, v);
663
+ };
664
+ set("TF_VAR_OCI_USER_OCID", cfg.user);
665
+ set("TF_VAR_OCI_FINGERPRINT", cfg.fingerprint);
666
+ set("TF_VAR_OCI_TENANCY_OCID", cfg.tenancy);
667
+ set("TF_VAR_OCI_REGION", cfg.region);
668
+ const pem = keyPath ?? cfg.key_file;
669
+ if (pem && existsSync3(pem)) {
670
+ map.set("TF_VAR_OCI_PRIVATE_KEY", readFileSync(pem, "utf8"));
671
+ } else if (pem) {
672
+ console.log(` ! PEM not found at ${pem} \u2014 set TF_VAR_OCI_PRIVATE_KEY manually (--oci-key)`);
673
+ }
674
+ return map;
675
+ }
619
676
  function parseRepo(remoteUrl) {
620
677
  const m = remoteUrl.trim().match(/github\.com[/:]([^/]+)\/(.+?)(?:\.git)?$/);
621
678
  return m ? `${m[1]}/${m[2]}` : null;
@@ -669,12 +726,135 @@ function syncSecrets(opts) {
669
726
  }
670
727
  return { repo, count: entries.length };
671
728
  }
729
+ function hiddenPrompter() {
730
+ const tty = Boolean(process.stdin.isTTY);
731
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: tty });
732
+ if (tty) rl._writeToOutput = () => {
733
+ };
734
+ return {
735
+ ask: (query) => new Promise((resolve11) => {
736
+ process.stdout.write(query);
737
+ rl.question("", (val) => {
738
+ process.stdout.write("\n");
739
+ resolve11(val.trim());
740
+ });
741
+ }),
742
+ close: () => rl.close()
743
+ };
744
+ }
745
+ function listGitHubSecrets(repo, env) {
746
+ const ghArgs = ["secret", "list", "--repo", repo, "--json", "name"];
747
+ if (env) ghArgs.push("--env", env);
748
+ try {
749
+ const out = execFileSync("gh", ghArgs, {
750
+ encoding: "utf8",
751
+ stdio: ["ignore", "pipe", "ignore"]
752
+ // don't leak gh's stderr into the guided flow
753
+ });
754
+ const parsed = JSON.parse(out);
755
+ return new Set(parsed.map((s) => s.name));
756
+ } catch {
757
+ return null;
758
+ }
759
+ }
760
+ function setGitHubSecret(repo, env, key, value) {
761
+ const ghArgs = ["secret", "set", key, "--repo", repo];
762
+ if (env) ghArgs.push("--env", env);
763
+ try {
764
+ execFileSync("gh", ghArgs, { input: value });
765
+ } catch (e) {
766
+ const err = e;
767
+ if (err.code === "ENOENT") {
768
+ throw new Error("the GitHub CLI `gh` is required \u2014 install it and run `gh auth login`");
769
+ }
770
+ const detail = err.stderr?.toString().trim();
771
+ throw new Error(`failed to set ${key}${detail ? `: ${detail}` : " (check `gh auth status`)"}`);
772
+ }
773
+ }
774
+ async function gatherSecrets(name, repo, env, prefill) {
775
+ const { config } = await loadManifest();
776
+ const entry = resolveEntry(config, name);
777
+ const packs = packsForTool({ target: entry.target, data: entry.data });
778
+ const dest = env ? `env "${env}" of ${repo}` : repo;
779
+ const existing = listGitHubSecrets(repo, env);
780
+ console.log(`Gathering secrets for "${name}" \u2192 GitHub ${dest}`);
781
+ console.log(
782
+ "Paste each value (hidden); Enter to skip. Values go straight to GitHub \u2014 never to disk."
783
+ );
784
+ console.log(
785
+ `[already set] = a value exists (paste to override, Enter to keep) \xB7 [not set] = new.${existing ? "" : " (could not read existing secrets \u2014 annotations omitted)"}`
786
+ );
787
+ if (prefill?.size) console.log(`Auto-filling ${prefill.size} value(s) from the OCI config.`);
788
+ console.log("");
789
+ const prompt = hiddenPrompter();
790
+ let pushed = 0;
791
+ try {
792
+ for (const pack of packs) {
793
+ console.log(`\u2500\u2500 ${pack.name}${pack.setupUrl ? ` \u2192 ${pack.setupUrl}` : ""}`);
794
+ for (const tok of pack.tokens) {
795
+ const key = tok.envVar.toUpperCase();
796
+ if (key === "GITHUB_TOKEN") {
797
+ console.log(" \xB7 GITHUB_TOKEN \u2014 provided automatically by Actions; skipping");
798
+ continue;
799
+ }
800
+ const pre = prefill?.get(key);
801
+ if (pre) {
802
+ setGitHubSecret(repo, env, key, pre);
803
+ console.log(` \u2714 ${existing?.has(key) ? "overrode" : "pushed"} ${key} \u2190 OCI config`);
804
+ pushed++;
805
+ continue;
806
+ }
807
+ if (tok.scopes?.length) console.log(` scopes: ${tok.scopes.join(", ")}`);
808
+ if (tok.setupUrl) console.log(` link: ${tok.setupUrl}`);
809
+ const state = existing ? existing.has(key) ? " [already set]" : " [not set]" : "";
810
+ const value = await prompt.ask(` ${key} \u2014 ${tok.label}${state}
811
+ value: `);
812
+ if (!value) {
813
+ console.log(existing?.has(key) ? " \xB7 kept existing" : " \xB7 skipped");
814
+ continue;
815
+ }
816
+ if (tok.verify) {
817
+ const check = await tok.verify(value, {}).catch((e) => ({ ok: false, detail: e instanceof Error ? e.message : String(e) }));
818
+ if (!check.ok) {
819
+ console.log(
820
+ ` \u2716 verify failed${check.detail ? ` (${check.detail})` : ""} \u2014 not pushed`
821
+ );
822
+ continue;
823
+ }
824
+ console.log(" \u2714 verified");
825
+ }
826
+ setGitHubSecret(repo, env, key, value);
827
+ const verb = existing?.has(key) ? "overrode" : "pushed";
828
+ console.log(` \u2714 ${verb} ${key} \u2192 ${repo}`);
829
+ pushed++;
830
+ }
831
+ }
832
+ } finally {
833
+ prompt.close();
834
+ }
835
+ console.log(`
836
+ ${pushed} secret(s) pushed to ${repo}. (None written to disk.)`);
837
+ }
672
838
  async function secretsCommand(args) {
673
- if (args[0] !== "sync") {
839
+ const sub = args[0];
840
+ if (sub === "gather") {
841
+ const name = args[1];
842
+ if (!name || name.startsWith("-")) {
843
+ throw new Error("usage: greenlight secrets gather <name> [--repo owner/repo] [--env <env>]");
844
+ }
845
+ const repo = flag(args, "--repo") ?? detectRepo(process.cwd());
846
+ if (!repo) throw new Error("could not determine the repo \u2014 pass --repo owner/repo");
847
+ const ociConfig2 = flag(args, "--oci-config");
848
+ const ociKey = flag(args, "--oci-key");
849
+ const prefill = ociConfig2 ? ociPrefill(resolve3(process.cwd(), ociConfig2), ociKey && resolve3(process.cwd(), ociKey)) : void 0;
850
+ await gatherSecrets(name, repo, flag(args, "--env"), prefill);
851
+ return;
852
+ }
853
+ if (sub !== "sync") {
674
854
  console.log(
675
- "usage: greenlight secrets sync [--repo owner/repo] [--env <env>] # push .greenlight/secrets.env to GitHub Actions secrets"
855
+ "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
856
  );
677
- process.exit(args[0] ? 1 : 0);
857
+ process.exit(sub ? 1 : 0);
678
858
  }
679
859
  const { count } = syncSecrets({
680
860
  cwd: process.cwd(),
@@ -724,7 +904,7 @@ async function ensureTokensForTool(cwd, tool, opts = {}) {
724
904
  const interactive = Boolean(process.stdin.isTTY);
725
905
  const env = presentEnv(cwd);
726
906
  const results = [];
727
- const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
907
+ const rl = interactive ? createInterface2({ input: process.stdin, output: process.stdout }) : null;
728
908
  try {
729
909
  for (const spec of tokensForTool(tool)) {
730
910
  let value = env[spec.envVar];
@@ -1157,7 +1337,6 @@ jobs:
1157
1337
  `;
1158
1338
  }
1159
1339
  function deployListenerYml(name, toolRepo) {
1160
- const SECRET = `${name.toUpperCase().replace(/-/g, "_")}_OCI_CONTAINER_INSTANCE_OCID`;
1161
1340
  return `name: greenlight-deploy-${name}
1162
1341
 
1163
1342
  # Option B: ${toolRepo} fires repository_dispatch(deploy-${name}) after pushing a new image.
@@ -1179,12 +1358,13 @@ jobs:
1179
1358
  - run: pip install --quiet oci-cli
1180
1359
  - name: Deploy (restart container instance -> re-pull GHCR image)
1181
1360
  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} }}
1361
+ # The OCI CLI reuses the SAME TF_VAR_OCI_* secrets the apply uses \u2014 one secret set.
1362
+ OCI_CLI_TENANCY: \${{ secrets.TF_VAR_OCI_TENANCY_OCID }}
1363
+ OCI_CLI_USER: \${{ secrets.TF_VAR_OCI_USER_OCID }}
1364
+ OCI_CLI_FINGERPRINT: \${{ secrets.TF_VAR_OCI_FINGERPRINT }}
1365
+ OCI_CLI_KEY_CONTENT: \${{ secrets.TF_VAR_OCI_PRIVATE_KEY }}
1366
+ OCI_CLI_REGION: \${{ secrets.TF_VAR_OCI_REGION }}
1367
+ OCI_CONTAINER_INSTANCE_OCID: \${{ secrets.OCI_CONTAINER_INSTANCE_OCID }}
1188
1368
  run: pnpm exec greenlight deploy ${name} --env prod
1189
1369
  - name: Report status back to ${toolRepo}
1190
1370
  if: \${{ always() && github.event.client_payload.sha != '' }}
@@ -1306,8 +1486,9 @@ Next:
1306
1486
  (in the wrapper) review infra/${name}.tf, then commit the submodule + infra + listener:
1307
1487
  git add .gitmodules ${toolRel} infra/${name}.tf verify/${name}.config.ts greenlight.config.ts .github
1308
1488
  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.` : ""}`);
1489
+ Secrets (guided): greenlight secrets gather ${name} --repo <wrapper> # TF_VAR_OCI_* + GREENLIGHT_STATUS_TOKEN
1490
+ greenlight secrets gather ${name} --repo ${slug} # GREENLIGHT_DISPATCH_TOKEN
1491
+ After apply, set OCI_CONTAINER_INSTANCE_OCID (the TF output) in the wrapper.` : ""}`);
1311
1492
  }
1312
1493
  async function adoptStandalone(ctx) {
1313
1494
  const { name, repoArg, lane, target, data, auth, envs, domain, reg, regPath } = ctx;
@@ -1613,7 +1794,7 @@ ${failed === 0 ? "\u2714 no failures" : `\u2718 ${failed} failure(s)`}`);
1613
1794
  // src/commands/init.ts
1614
1795
  import { existsSync as existsSync9, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
1615
1796
  import { resolve as resolve8 } from "path";
1616
- import { createInterface as createInterface2 } from "readline/promises";
1797
+ import { createInterface as createInterface3 } from "readline/promises";
1617
1798
  function flag5(args, name) {
1618
1799
  const i = args.indexOf(name);
1619
1800
  return i >= 0 ? args[i + 1] : void 0;
@@ -1630,7 +1811,7 @@ async function initCommand(args) {
1630
1811
  let domain = flag5(args, "--domain");
1631
1812
  if (!domain) {
1632
1813
  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 });
1814
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
1634
1815
  domain = (await rl.question("Domain (e.g. example.dev): ")).trim();
1635
1816
  rl.close();
1636
1817
  }
@@ -1901,6 +2082,7 @@ var HELP = `greenlight <command>
1901
2082
  preview <name> [--port <n>] build + serve locally + verify (one command)
1902
2083
  verify <name> [--env <env> | --url <url>] run the verify harness against the URL
1903
2084
  promote <name> [--perform] [--push] gated develop -> main fast-forward
2085
+ secrets gather <name> [--repo o/r] [--env e] guided, link-first token prompts -> GitHub secrets (no disk/logs)
1904
2086
  secrets sync [--repo o/r] [--env <env>] push .greenlight/secrets.env -> GitHub Actions secrets
1905
2087
  agent sync write the loop skill + CLAUDE.md block into this repo
1906
2088
  adopt <name> --repo <path> --lane --target onboard an existing tool repo as a thin consumer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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",
36
35
  "@rtrentjones/greenlight-shared": "0.2.4",
37
- "@rtrentjones/greenlight-verify": "0.2.4"
36
+ "@rtrentjones/greenlight-verify": "0.2.4",
37
+ "@rtrentjones/greenlight-loop": "0.2.4"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-assets.mjs && tsup",