create-op-node 0.1.0 → 0.3.0

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/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, Option } from 'commander';
3
3
  import pc2 from 'picocolors';
4
- import * as p2 from '@clack/prompts';
4
+ import * as p3 from '@clack/prompts';
5
5
  import { execa } from 'execa';
6
6
  import { Octokit } from '@octokit/rest';
7
7
  import { randomBytes } from 'crypto';
8
8
  import { join, resolve, dirname } from 'path';
9
- import { mkdir, writeFile, access, chmod } from 'fs/promises';
9
+ import { mkdir, writeFile, access, rm, chmod } from 'fs/promises';
10
10
  import { homedir } from 'os';
11
11
  import { readFileSync } from 'fs';
12
12
  import * as tls from 'tls';
@@ -225,85 +225,86 @@ async function safeExeca(cmd, args, options) {
225
225
  }
226
226
  }
227
227
 
228
- // src/lib/onepassword.ts
229
- async function detectOp() {
230
- const version = await safeExeca("op", ["--version"]);
231
- if (version === null) {
232
- return { installed: false, signedIn: false };
233
- }
234
- const installed = true;
235
- const whoami = await safeExeca("op", ["whoami", "--format=json"]);
236
- if (whoami === null || whoami.exitCode !== 0) {
237
- return { installed, signedIn: false };
238
- }
239
- let email;
240
- try {
241
- const parsed = JSON.parse(whoami.stdout);
242
- email = parsed.email;
243
- } catch {
244
- }
245
- return {
246
- installed,
247
- signedIn: true,
248
- ...email ? { email } : {}
249
- };
228
+ // src/lib/keychain.ts
229
+ var SERVICE_PREFIX = "org.opuspopuli";
230
+ function serviceFor(region) {
231
+ return `${SERVICE_PREFIX}.${region}`;
232
+ }
233
+ function labelFor(coords) {
234
+ const friendly = coords.account === "pgsodium-root-key" ? "pgsodium root key" : "Cloudflare Tunnel token";
235
+ return `Opus Populi (${coords.region}) \u2014 ${friendly}`;
250
236
  }
251
- async function saveSecretToOp(input) {
252
- const vaultArg = input.vault ? ["--vault", input.vault] : [];
253
- const existing = await safeExeca("op", ["item", "get", input.title, ...vaultArg, "--format=json"]);
254
- if (existing === null) {
237
+ async function detectKeychain() {
238
+ const res = await safeExeca("security", ["-h"]);
239
+ if (res === null) {
255
240
  return {
256
- written: false,
257
- alreadyExisted: false,
258
- reason: "`op` CLI not installed"
241
+ available: false,
242
+ reason: "`security` CLI not on PATH (Keychain requires macOS)"
259
243
  };
260
244
  }
261
- if (existing.exitCode === 0) {
262
- if (!input.overwrite) {
263
- return { written: false, alreadyExisted: true };
264
- }
265
- const edit = await safeExeca(
266
- "op",
267
- ["item", "edit", input.title, ...vaultArg, `notesPlain=${input.value}`]
268
- );
269
- if (edit === null || edit.exitCode !== 0) {
270
- return {
271
- written: false,
272
- alreadyExisted: true,
273
- reason: `op item edit failed: ${edit?.stderr || edit?.stdout || "`op` not installed"}`
274
- };
275
- }
276
- return { written: true, alreadyExisted: true };
277
- }
278
- const create = await safeExeca("op", [
279
- "item",
280
- "create",
281
- "--category",
282
- "Secure Note",
283
- "--title",
284
- input.title,
285
- ...vaultArg,
286
- `notesPlain=${input.value}`
245
+ return { available: true };
246
+ }
247
+ async function saveSecret(coords, value) {
248
+ const service = serviceFor(coords.region);
249
+ const label = labelFor(coords);
250
+ const existing = await safeExeca("security", [
251
+ "find-generic-password",
252
+ "-s",
253
+ service,
254
+ "-a",
255
+ coords.account
256
+ ]);
257
+ const updated = existing !== null && existing.exitCode === 0;
258
+ const res = await safeExeca("security", [
259
+ "add-generic-password",
260
+ "-U",
261
+ // upsert
262
+ "-s",
263
+ service,
264
+ "-a",
265
+ coords.account,
266
+ "-l",
267
+ label,
268
+ "-D",
269
+ "Opus Populi secret",
270
+ "-w",
271
+ value
272
+ // argv-exposure caveat, see file header
287
273
  ]);
288
- if (create === null || create.exitCode !== 0) {
274
+ if (res === null) {
275
+ return { written: false, updated: false, reason: "`security` CLI not on PATH" };
276
+ }
277
+ if (res.exitCode !== 0) {
289
278
  return {
290
279
  written: false,
291
- alreadyExisted: false,
292
- reason: `op item create failed: ${create?.stderr || create?.stdout || "`op` not installed"}`
280
+ updated,
281
+ reason: `security add-generic-password failed (${res.exitCode ?? "signal"}): ${res.stderr || res.stdout}`
293
282
  };
294
283
  }
295
- return { written: true, alreadyExisted: false };
296
- }
297
- async function readSecretFromOp(input) {
298
- const vaultArg = input.vault ? ["--vault", input.vault] : [];
299
- const res = await safeExeca(
300
- "op",
301
- ["item", "get", input.title, ...vaultArg, "--fields", "notesPlain", "--reveal"]
302
- );
284
+ return { written: true, updated };
285
+ }
286
+ async function readSecret(coords) {
287
+ const service = serviceFor(coords.region);
288
+ const res = await safeExeca("security", [
289
+ "find-generic-password",
290
+ "-s",
291
+ service,
292
+ "-a",
293
+ coords.account,
294
+ "-w"
295
+ // print the password to stdout (final flag = read mode)
296
+ ]);
303
297
  if (res === null || res.exitCode !== 0) return null;
304
298
  const value = res.stdout.trim();
305
299
  return value.length > 0 ? value : null;
306
300
  }
301
+ function unwrap(value) {
302
+ if (p3.isCancel(value)) {
303
+ p3.cancel("Cancelled.");
304
+ process.exit(0);
305
+ }
306
+ return value;
307
+ }
307
308
  var _client = null;
308
309
  var _clientToken = null;
309
310
  function client(token) {
@@ -513,13 +514,6 @@ async function waitForApply(input, budgets = DEFAULT_BUDGETS, deps = realDeps) {
513
514
  }
514
515
 
515
516
  // src/commands/init.ts
516
- function unwrap(value) {
517
- if (p2.isCancel(value)) {
518
- p2.cancel("Cancelled.");
519
- process.exit(0);
520
- }
521
- return value;
522
- }
523
517
  async function ghTokenFromCli() {
524
518
  const r = await safeExeca("gh", ["auth", "token"]);
525
519
  if (r === null || r.exitCode !== 0) return null;
@@ -532,10 +526,10 @@ var initCommand = new Command("init").description(
532
526
  new Option("--template <owner/repo>", "Template repo to clone from").default(
533
527
  "OpusPopuli/opuspopuli-node"
534
528
  )
535
- ).addOption(new Option("--project <name>", "tfvars project prefix").default("opuspopuli")).addOption(new Option("--cf-token <token>", "Cloudflare Account API token").env("CF_TOKEN")).addOption(new Option("--cf-account <id>", "Cloudflare account ID").env("CF_ACCOUNT")).addOption(new Option("--cf-zone <id>", "Cloudflare zone ID for your domain").env("CF_ZONE")).addOption(new Option("--tf-token <token>", "Terraform Cloud user/team token").env("TF_API_TOKEN")).addOption(new Option("--tf-org <org>", "Terraform Cloud organization").env("TF_CLOUD_ORGANIZATION")).addOption(new Option("--gh-token <token>", "GitHub Personal Access Token (else gh CLI)").env("GH_TOKEN")).addOption(new Option("--vault <vault>", "1Password vault for secrets").default("Private")).addOption(
529
+ ).addOption(new Option("--project <name>", "tfvars project prefix").default("opuspopuli")).addOption(new Option("--cf-token <token>", "Cloudflare Account API token").env("CF_TOKEN")).addOption(new Option("--cf-account <id>", "Cloudflare account ID").env("CF_ACCOUNT")).addOption(new Option("--cf-zone <id>", "Cloudflare zone ID for your domain").env("CF_ZONE")).addOption(new Option("--tf-token <token>", "Terraform Cloud user/team token").env("TF_API_TOKEN")).addOption(new Option("--tf-org <org>", "Terraform Cloud organization").env("TF_CLOUD_ORGANIZATION")).addOption(new Option("--gh-token <token>", "GitHub Personal Access Token (else gh CLI)").env("GH_TOKEN")).addOption(
536
530
  new Option(
537
531
  "--overwrite",
538
- "Overwrite existing 1Password items (pgsodium key, Tunnel token) on a re-run"
532
+ "Overwrite existing Keychain items (pgsodium key, Tunnel token) on a re-run"
539
533
  ).default(false)
540
534
  ).addOption(
541
535
  new Option(
@@ -543,8 +537,8 @@ var initCommand = new Command("init").description(
543
537
  "Continue using a previously-created node repo (don't fail on 'already exists')"
544
538
  ).default(false)
545
539
  ).addOption(new Option("--skip-wait", "Don't poll for Terraform apply; exit after PR open").default(false)).addOption(new Option("-y, --yes", "Skip the final confirmation").default(false)).action(async (opts) => {
546
- p2.intro(pc2.bgCyan(pc2.black(" create-op-node init ")));
547
- p2.note(
540
+ p3.intro(pc2.bgCyan(pc2.black(" create-op-node init ")));
541
+ p3.note(
548
542
  [
549
543
  "This walks you from a sealed Mac Studio + a Cloudflare account to a",
550
544
  "live federation node serving traffic at api.<your-domain>.",
@@ -555,14 +549,14 @@ var initCommand = new Command("init").description(
555
549
  "Welcome"
556
550
  );
557
551
  const region = opts.region ? opts.region : unwrap(
558
- await p2.text({
559
- message: "Short region label (used as a prefix in 1Password + R2)?",
552
+ await p3.text({
553
+ message: "Short region label (used as the Keychain service suffix + R2 prefix)?",
560
554
  placeholder: "us-ca",
561
555
  validate: (v) => /^[a-z0-9-]{2,32}$/.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
562
556
  })
563
557
  );
564
558
  const domain = opts.domain ? opts.domain : unwrap(
565
- await p2.text({
559
+ await p3.text({
566
560
  message: "Domain registered in Cloudflare?",
567
561
  placeholder: "example.org",
568
562
  validate: (v) => !v ? "Required" : v.includes(".") ? void 0 : "Missing a TLD?"
@@ -578,7 +572,7 @@ var initCommand = new Command("init").description(
578
572
  );
579
573
  const cfAccount = await collectId(opts.cfAccount, "Cloudflare account ID");
580
574
  const cfZone = await collectId(opts.cfZone, `Cloudflare zone ID for ${domain}`);
581
- const cfSpin = p2.spinner();
575
+ const cfSpin = p3.spinner();
582
576
  cfSpin.start("Verifying Cloudflare token + 5 scopes\u2026");
583
577
  const cfProbe = await probeCloudflareToken({
584
578
  token: cfToken,
@@ -588,7 +582,7 @@ var initCommand = new Command("init").description(
588
582
  if (!cfProbe.ok) {
589
583
  cfSpin.stop(pc2.red("\u2717 Cloudflare token check failed."));
590
584
  for (const issue of cfProbe.issues) console.error(` - ${issue}`);
591
- p2.cancel("Fix the token in the Cloudflare dashboard, then re-run.");
585
+ p3.cancel("Fix the token in the Cloudflare dashboard, then re-run.");
592
586
  process.exit(1);
593
587
  }
594
588
  cfSpin.stop(pc2.green("\u2713 Cloudflare token valid, 5 scopes present."));
@@ -598,18 +592,18 @@ var initCommand = new Command("init").description(
598
592
  "tfc"
599
593
  );
600
594
  const tfOrg = opts.tfOrg ? opts.tfOrg : unwrap(
601
- await p2.text({
595
+ await p3.text({
602
596
  message: "Terraform Cloud organization name?",
603
597
  placeholder: "op-region-ca"
604
598
  })
605
599
  );
606
- const tfSpin = p2.spinner();
600
+ const tfSpin = p3.spinner();
607
601
  tfSpin.start("Verifying Terraform Cloud token + organization\u2026");
608
602
  const tfProbe = await probeTfcToken({ token: tfToken, organization: tfOrg });
609
603
  if (!tfProbe.ok) {
610
604
  tfSpin.stop(pc2.red("\u2717 Terraform Cloud check failed."));
611
605
  for (const issue of tfProbe.issues) console.error(` - ${issue}`);
612
- p2.cancel("Fix the token / org, then re-run.");
606
+ p3.cancel("Fix the token / org, then re-run.");
613
607
  process.exit(1);
614
608
  }
615
609
  tfSpin.stop(
@@ -622,7 +616,7 @@ var initCommand = new Command("init").description(
622
616
  const fromCli = await ghTokenFromCli();
623
617
  if (fromCli) {
624
618
  const useCli = unwrap(
625
- await p2.confirm({
619
+ await p3.confirm({
626
620
  message: "Found a signed-in `gh` CLI \u2014 use its token?",
627
621
  initialValue: true
628
622
  })
@@ -637,10 +631,11 @@ var initCommand = new Command("init").description(
637
631
  "github"
638
632
  );
639
633
  }
640
- const op = await detectOp();
641
- const opNote = op.installed ? op.signedIn ? `\u2713 1Password CLI signed in${op.email ? ` (${op.email})` : ""} \u2014 pgsodium key + Tunnel token will be saved automatically.` : `\u26A0 1Password CLI installed but not signed in. Run \`op signin\` in another shell, or you'll be prompted to paste secrets.` : `\u26A0 1Password CLI not installed. Secrets will be printed for you to paste into 1Password by hand.`;
642
- p2.note(opNote, "1Password");
643
- p2.note(
634
+ const keychain = await detectKeychain();
635
+ const kcNote = keychain.available ? `\u2713 macOS Keychain available \u2014 pgsodium key + Tunnel token will be saved to your login keychain.
636
+ ` + pc2.dim("Note: items don't sync to iCloud Keychain via the security CLI.\n") + pc2.dim("On the Studio, bootstrap will read locally and prompt you to paste if missing.") : `\u26A0 ${keychain.reason ?? "Keychain unavailable"}. Secrets will be printed for manual stash.`;
637
+ p3.note(kcNote, "Secret store");
638
+ p3.note(
644
639
  [
645
640
  `Region label: ${pc2.cyan(region)}`,
646
641
  `Domain: ${pc2.cyan(domain)}`,
@@ -656,14 +651,14 @@ var initCommand = new Command("init").description(
656
651
  );
657
652
  if (!opts.yes) {
658
653
  const go = unwrap(
659
- await p2.confirm({ message: "Proceed?", initialValue: true })
654
+ await p3.confirm({ message: "Proceed?", initialValue: true })
660
655
  );
661
656
  if (!go) {
662
- p2.cancel("Cancelled.");
657
+ p3.cancel("Cancelled.");
663
658
  process.exit(0);
664
659
  }
665
660
  }
666
- const repoSpin = p2.spinner();
661
+ const repoSpin = p3.spinner();
667
662
  repoSpin.start(`Creating ${newRepoFull} from ${opts.template}\u2026`);
668
663
  let created;
669
664
  try {
@@ -680,18 +675,18 @@ var initCommand = new Command("init").description(
680
675
  const message = err.message ?? "unknown error";
681
676
  if (status !== 422) {
682
677
  repoSpin.stop(pc2.red(`\u2717 Couldn't create repo: ${message}`));
683
- p2.cancel("Fix the issue and re-run.");
678
+ p3.cancel("Fix the issue and re-run.");
684
679
  process.exit(1);
685
680
  }
686
681
  repoSpin.stop(pc2.yellow(`\u26A0 ${newRepoFull} already exists.`));
687
682
  const reuse = opts.useExistingRepo || unwrap(
688
- await p2.confirm({
683
+ await p3.confirm({
689
684
  message: `Continue with the existing ${newRepoFull}? (Re-seeds secrets + re-commits prod.tfvars on a fresh branch.)`,
690
685
  initialValue: true
691
686
  })
692
687
  );
693
688
  if (!reuse) {
694
- p2.cancel(
689
+ p3.cancel(
695
690
  `Delete ${newRepoFull} on GitHub and re-run, or pass --use-existing-repo to skip this prompt.`
696
691
  );
697
692
  process.exit(0);
@@ -709,7 +704,7 @@ var initCommand = new Command("init").description(
709
704
  { name: "TF_API_TOKEN", value: tfToken },
710
705
  { name: "TF_CLOUD_ORGANIZATION", value: tfOrg }
711
706
  ];
712
- const secSpin = p2.spinner();
707
+ const secSpin = p3.spinner();
713
708
  secSpin.start(`Seeding ${secrets.length} repo secrets\u2026`);
714
709
  const seeded = [];
715
710
  for (const s of secrets) {
@@ -717,7 +712,7 @@ var initCommand = new Command("init").description(
717
712
  if (!r.written) {
718
713
  secSpin.stop(pc2.red(`\u2717 Failed to set ${s.name}: ${r.reason ?? "unknown"}`));
719
714
  const remaining = secrets.slice(secrets.findIndex((x) => x.name === s.name)).map((x) => x.name);
720
- p2.cancel(
715
+ p3.cancel(
721
716
  [
722
717
  seeded.length > 0 ? `Already seeded on ${newRepoFull}: ${seeded.join(", ")}.` : `Nothing seeded yet on ${newRepoFull}.`,
723
718
  `Still pending: ${remaining.join(", ")}.`,
@@ -732,7 +727,7 @@ var initCommand = new Command("init").description(
732
727
  }
733
728
  secSpin.stop(pc2.green(`\u2713 Seeded ${secrets.length} secrets on ${newRepoFull}`));
734
729
  const branch = `init/region-${region}-${isoStampUtc(/* @__PURE__ */ new Date())}`;
735
- const setupSpin = p2.spinner();
730
+ const setupSpin = p3.spinner();
736
731
  setupSpin.start(`Writing prod.tfvars on branch ${branch}\u2026`);
737
732
  try {
738
733
  await createBranch({
@@ -755,11 +750,11 @@ var initCommand = new Command("init").description(
755
750
  });
756
751
  } catch (err) {
757
752
  setupSpin.stop(pc2.red(`\u2717 Couldn't write tfvars: ${err.message}`));
758
- p2.cancel("The repo exists but the branch / commit failed. Open it on GitHub and inspect.");
753
+ p3.cancel("The repo exists but the branch / commit failed. Open it on GitHub and inspect.");
759
754
  process.exit(1);
760
755
  }
761
756
  setupSpin.stop(pc2.green("\u2713 Wrote prod.tfvars"));
762
- const prSpin = p2.spinner();
757
+ const prSpin = p3.spinner();
763
758
  prSpin.start("Opening pull request\u2026");
764
759
  let pr;
765
760
  try {
@@ -782,54 +777,44 @@ var initCommand = new Command("init").description(
782
777
  });
783
778
  } catch (err) {
784
779
  prSpin.stop(pc2.red(`\u2717 Couldn't open PR: ${err.message}`));
785
- p2.cancel("Branch + tfvars are committed; open the PR by hand on GitHub.");
780
+ p3.cancel("Branch + tfvars are committed; open the PR by hand on GitHub.");
786
781
  process.exit(1);
787
782
  }
788
783
  prSpin.stop(pc2.green(`\u2713 Opened PR #${pr.number}`));
789
- const keyTitle = `opuspopuli-${region}-pgsodium-root-key`;
790
- const vaultArg = opts.vault ? { vault: opts.vault } : {};
791
- if (op.installed && op.signedIn) {
792
- const existing = opts.overwrite ? null : await readSecretFromOp({ title: keyTitle, ...vaultArg });
784
+ const pgsodiumCoords = { region, account: "pgsodium-root-key" };
785
+ const keyLabel = `org.opuspopuli.${region}/pgsodium-root-key`;
786
+ if (keychain.available) {
787
+ const existing = opts.overwrite ? null : await readSecret(pgsodiumCoords);
793
788
  if (existing && /^[a-f0-9]{64}$/.test(existing)) {
794
- p2.note(
795
- `${pc2.green("\u2713")} Re-using existing pgsodium master key from 1Password (${pc2.cyan(keyTitle)}). Pass --overwrite to rotate.`,
789
+ p3.note(
790
+ `${pc2.green("\u2713")} Re-using existing pgsodium master key from Keychain (${pc2.cyan(keyLabel)}). Pass --overwrite to rotate.`,
796
791
  "Secret"
797
792
  );
798
793
  } else {
799
794
  const fresh = generatePgsodiumRootKey();
800
- const r = await saveSecretToOp({
801
- title: keyTitle,
802
- value: fresh,
803
- overwrite: opts.overwrite ?? false,
804
- ...vaultArg
805
- });
795
+ const r = await saveSecret(pgsodiumCoords, fresh);
806
796
  if (r.written) {
807
- p2.note(
808
- `${pc2.green("\u2713")} pgsodium master key${r.alreadyExisted ? " rotated" : " saved"} to 1Password as ${pc2.cyan(keyTitle)}.`,
809
- "Secret"
810
- );
811
- } else if (r.alreadyExisted) {
812
- p2.note(
813
- `${pc2.yellow("!")} 1Password has an item titled ${pc2.cyan(keyTitle)} but its value is NOT a 64-hex pgsodium key. Inspect it in 1Password; re-run with --overwrite to replace.`,
797
+ p3.note(
798
+ `${pc2.green("\u2713")} pgsodium master key${r.updated ? " rotated" : " saved"} to Keychain as ${pc2.cyan(keyLabel)}.`,
814
799
  "Secret"
815
800
  );
816
801
  } else {
817
- await printKeyForManualSave(fresh, keyTitle, r.reason);
802
+ await printKeyForManualSave(fresh, keyLabel, r.reason);
818
803
  }
819
804
  }
820
805
  } else {
821
806
  const fresh = generatePgsodiumRootKey();
822
- await printKeyForManualSave(fresh, keyTitle);
807
+ await printKeyForManualSave(fresh, keyLabel);
823
808
  }
824
809
  if (opts.skipWait) {
825
- p2.outro(
810
+ p3.outro(
826
811
  pc2.cyan(
827
812
  `Review + merge ${pr.htmlUrl} when ready. Re-run with no --skip-wait or run \`npx create-op-node verify\` afterwards.`
828
813
  )
829
814
  );
830
815
  return;
831
816
  }
832
- p2.note(
817
+ p3.note(
833
818
  [
834
819
  `Open the PR: ${pc2.cyan(pr.htmlUrl)}`,
835
820
  "",
@@ -839,10 +824,10 @@ var initCommand = new Command("init").description(
839
824
  "Next step: review + merge"
840
825
  );
841
826
  const merged = unwrap(
842
- await p2.confirm({ message: "PR merged?", initialValue: false })
827
+ await p3.confirm({ message: "PR merged?", initialValue: false })
843
828
  );
844
829
  if (!merged) {
845
- p2.outro(
830
+ p3.outro(
846
831
  pc2.cyan(`No worries \u2014 re-run \`create-op-node init --skip-wait\` later to skip this step, or finish manually.`)
847
832
  );
848
833
  return;
@@ -853,7 +838,7 @@ var initCommand = new Command("init").description(
853
838
  tags: ["opuspopuli", "cloudflare"]
854
839
  });
855
840
  if (!ws) {
856
- p2.cancel(
841
+ p3.cancel(
857
842
  `Couldn't find a TFC workspace tagged opuspopuli + cloudflare in ${tfOrg}. Check the workflow's run log; the workspace is created on first apply.`
858
843
  );
859
844
  process.exit(1);
@@ -865,31 +850,27 @@ var initCommand = new Command("init").description(
865
850
  runId: ws.currentRunId
866
851
  });
867
852
  if (!tunnelToken) {
868
- p2.cancel(
853
+ p3.cancel(
869
854
  `Terraform apply didn't produce a tunnel_token output. Check the run on TFC; if it succeeded, the output may be named differently \u2014 patch and re-run, or fetch via 'terraform output -raw tunnel_token'.`
870
855
  );
871
856
  process.exit(1);
872
857
  }
873
- const tunnelTitle = `opuspopuli-${region}-tunnel-token`;
874
- if (op.installed && op.signedIn) {
875
- const r = await saveSecretToOp({
876
- title: tunnelTitle,
877
- value: tunnelToken,
878
- ...opts.vault ? { vault: opts.vault } : {},
879
- overwrite: true
880
- });
858
+ const tunnelCoords = { region, account: "tunnel-token" };
859
+ const tunnelLabel = `org.opuspopuli.${region}/tunnel-token`;
860
+ if (keychain.available) {
861
+ const r = await saveSecret(tunnelCoords, tunnelToken);
881
862
  if (r.written) {
882
- p2.note(
883
- `${pc2.green("\u2713")} Tunnel token saved to 1Password as ${pc2.cyan(tunnelTitle)}.`,
863
+ p3.note(
864
+ `${pc2.green("\u2713")} Tunnel token saved to Keychain as ${pc2.cyan(tunnelLabel)}.`,
884
865
  "Secret"
885
866
  );
886
867
  } else {
887
- await printKeyForManualSave(tunnelToken, tunnelTitle, r.reason);
868
+ await printKeyForManualSave(tunnelToken, tunnelLabel, r.reason);
888
869
  }
889
870
  } else {
890
- await printKeyForManualSave(tunnelToken, tunnelTitle);
871
+ await printKeyForManualSave(tunnelToken, tunnelLabel);
891
872
  }
892
- p2.outro(
873
+ p3.outro(
893
874
  pc2.cyan(
894
875
  `Region ${region} is provisioned. Next: \`npx create-op-node bootstrap\` on the Mac Studio.`
895
876
  )
@@ -905,7 +886,7 @@ async function collectSecret(preset, message, kind = "generic") {
905
886
  if (preset) return preset;
906
887
  const min = MIN_TOKEN_LENGTH[kind];
907
888
  const v = unwrap(
908
- await p2.password({
889
+ await p3.password({
909
890
  message,
910
891
  validate: (v2) => v2 && v2.length >= min ? void 0 : `That looks too short \u2014 expected at least ${min} characters for a ${kind} token`
911
892
  })
@@ -915,7 +896,7 @@ async function collectSecret(preset, message, kind = "generic") {
915
896
  async function collectId(preset, message) {
916
897
  if (preset) return preset;
917
898
  const v = unwrap(
918
- await p2.text({
899
+ await p3.text({
919
900
  message,
920
901
  placeholder: "0000000000000000000000000000000",
921
902
  validate: (v2) => /^[a-f0-9]{32}$/.test(v2 ?? "") ? void 0 : "Expected 32 hex characters"
@@ -923,21 +904,22 @@ async function collectId(preset, message) {
923
904
  );
924
905
  return v;
925
906
  }
926
- async function printKeyForManualSave(value, title, reason) {
927
- p2.note(
907
+ async function printKeyForManualSave(value, label, reason) {
908
+ p3.note(
928
909
  [
929
910
  reason ? `${pc2.red("\u2717")} ${reason}` : "",
930
- `Save this value to 1Password as ${pc2.cyan(title)}:`,
911
+ `Save this value to your secret store as ${pc2.cyan(label)}:`,
931
912
  "",
932
913
  pc2.yellow(value),
933
914
  "",
934
- pc2.dim("It will not be shown again.")
915
+ pc2.dim("It will not be shown again."),
916
+ pc2.dim("On the Studio, bootstrap will prompt you to paste it back.")
935
917
  ].filter((line) => line.length > 0).join("\n"),
936
918
  "Manual save"
937
919
  );
938
920
  unwrap(
939
- await p2.confirm({
940
- message: "Value stashed in 1Password?",
921
+ await p3.confirm({
922
+ message: "Value stashed?",
941
923
  initialValue: true
942
924
  })
943
925
  );
@@ -947,7 +929,7 @@ function isoStampUtc(d) {
947
929
  return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
948
930
  }
949
931
  async function waitForApplyAndFetchTunnelToken(input) {
950
- const spin = p2.spinner();
932
+ const spin = p3.spinner();
951
933
  spin.start("Waiting for terraform apply to finish (polling every 10s)\u2026");
952
934
  const outcome = await waitForApply({
953
935
  token: input.token,
@@ -1204,6 +1186,30 @@ async function setupLaunchAgent(input) {
1204
1186
  }
1205
1187
  return { ok: true, paths };
1206
1188
  }
1189
+ async function teardownLaunchAgent(paths, opts = {}) {
1190
+ const steps = [];
1191
+ const unload = await safeExeca("launchctl", ["unload", paths.plistFile]);
1192
+ steps.push({
1193
+ step: "unload",
1194
+ ok: true,
1195
+ ...unload === null ? { reason: "`launchctl` not on PATH" } : {}
1196
+ });
1197
+ try {
1198
+ await rm(paths.plistFile, { force: true });
1199
+ steps.push({ step: "rm-plist", ok: true });
1200
+ } catch (err) {
1201
+ steps.push({ step: "rm-plist", ok: false, reason: err.message });
1202
+ }
1203
+ if (!opts.keepKeyFile) {
1204
+ try {
1205
+ await rm(paths.keyFile, { force: true });
1206
+ steps.push({ step: "rm-key-file", ok: true });
1207
+ } catch (err) {
1208
+ steps.push({ step: "rm-key-file", ok: false, reason: err.message });
1209
+ }
1210
+ }
1211
+ return { ok: steps.every((s) => s.ok), steps };
1212
+ }
1207
1213
 
1208
1214
  // src/lib/docker.ts
1209
1215
  var GHCR_REGISTRY = "ghcr.io";
@@ -1258,6 +1264,17 @@ async function composeUp(opts) {
1258
1264
  const res = await safeExeca("docker", composeArgs(opts, ["up", "-d", "--remove-orphans"]));
1259
1265
  return result(res, "compose up");
1260
1266
  }
1267
+ async function composeDown(opts) {
1268
+ const flags = ["down"];
1269
+ if (opts.wipeVolumes) flags.push("-v");
1270
+ if (opts.removeOrphans) flags.push("--remove-orphans");
1271
+ const res = await safeExeca("docker", composeArgs(opts, flags));
1272
+ return result(res, opts.wipeVolumes ? "compose down -v" : "compose down");
1273
+ }
1274
+ async function dockerLogout(registry = GHCR_REGISTRY) {
1275
+ const res = await safeExeca("docker", ["logout", registry]);
1276
+ return result(res, `docker logout ${registry}`);
1277
+ }
1261
1278
  function result(res, label) {
1262
1279
  if (res === null) return { ok: false, reason: "`docker` not installed" };
1263
1280
  if (res.exitCode !== 0) {
@@ -1412,10 +1429,10 @@ async function warmModel(name, url = OLLAMA_URL) {
1412
1429
  body: JSON.stringify({ model: name, prompt: "hi", stream: false })
1413
1430
  });
1414
1431
  if (!res.ok) {
1415
- const text5 = await res.text().catch(() => "");
1432
+ const text6 = await res.text().catch(() => "");
1416
1433
  return {
1417
1434
  ok: false,
1418
- reason: `warm ${name} returned HTTP ${res.status}: ${text5 || "(no body)"}`
1435
+ reason: `warm ${name} returned HTTP ${res.status}: ${text6 || "(no body)"}`
1419
1436
  };
1420
1437
  }
1421
1438
  await res.text();
@@ -1526,16 +1543,9 @@ async function locateOrCloneRepo(input) {
1526
1543
  }
1527
1544
 
1528
1545
  // src/commands/bootstrap.ts
1529
- function unwrap2(value) {
1530
- if (p2.isCancel(value)) {
1531
- p2.cancel("Cancelled.");
1532
- process.exit(0);
1533
- }
1534
- return value;
1535
- }
1536
1546
  var bootstrapCommand = new Command("bootstrap").description(
1537
1547
  "Configure the Mac Studio and bring the stack up. Run this on the Studio itself, after `init` has finished on your laptop."
1538
- ).addOption(new Option("--region <slug>", "Region label set during init (e.g. us-ca)")).addOption(new Option("--owner <owner>", "GitHub owner for the node repo").default("OpusPopuli")).addOption(new Option("--vault <vault>", "1Password vault to read secrets from").default("Private")).addOption(new Option("--repo-dir <path>", "Explicit path to a checked-out node repo (overrides cwd + clone)")).addOption(
1548
+ ).addOption(new Option("--region <slug>", "Region label set during init (e.g. us-ca)")).addOption(new Option("--owner <owner>", "GitHub owner for the node repo").default("OpusPopuli")).addOption(new Option("--repo-dir <path>", "Explicit path to a checked-out node repo (overrides cwd + clone)")).addOption(
1539
1549
  new Option(
1540
1550
  "--compose-file <path>",
1541
1551
  "Repeatable. Compose file relative to repo root. Default: docker-compose-prod.yml + docker-compose-backup.yml"
@@ -1546,9 +1556,9 @@ var bootstrapCommand = new Command("bootstrap").description(
1546
1556
  "Skip the LaunchAgent setup (assumes one is already in place)"
1547
1557
  ).default(false)
1548
1558
  ).addOption(new Option("--skip-ollama", "Skip the Ollama model pull + warm").default(false)).addOption(new Option("--skip-stack", "Stop before `docker compose pull && up`").default(false)).addOption(new Option("-y, --yes", "Skip confirmation prompts").default(false)).action(async (opts) => {
1549
- p2.intro(pc2.bgCyan(pc2.black(" create-op-node bootstrap ")));
1550
- const region = opts.region ? opts.region : unwrap2(
1551
- await p2.text({
1559
+ p3.intro(pc2.bgCyan(pc2.black(" create-op-node bootstrap ")));
1560
+ const region = opts.region ? opts.region : unwrap(
1561
+ await p3.text({
1552
1562
  message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
1553
1563
  placeholder: "us-ca",
1554
1564
  validate: (v) => /^[a-z0-9-]{2,32}$/.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
@@ -1556,17 +1566,17 @@ var bootstrapCommand = new Command("bootstrap").description(
1556
1566
  );
1557
1567
  const owner = opts.owner ?? "OpusPopuli";
1558
1568
  const repoName = `opuspopuli-node-${region}`;
1559
- const sysSpin = p2.spinner();
1569
+ const sysSpin = p3.spinner();
1560
1570
  sysSpin.start("Inspecting macOS\u2026");
1561
1571
  const snap = await inspectSystem();
1562
1572
  sysSpin.stop(pc2.green("\u2713 macOS inspected."));
1563
1573
  if (!snap.isAppleSilicon) {
1564
- p2.cancel(
1574
+ p3.cancel(
1565
1575
  `Bootstrap requires an Apple Silicon Mac (the runbook targets M-series). Detected: ${snap.osVersion ?? "unknown"} (uname -m != arm64).`
1566
1576
  );
1567
1577
  process.exit(1);
1568
1578
  }
1569
- p2.note(
1579
+ p3.note(
1570
1580
  [
1571
1581
  `Hostname: ${pc2.cyan(snap.hostname)}`,
1572
1582
  `macOS: ${pc2.cyan(snap.osVersion ?? "(sw_vers failed)")}`,
@@ -1577,33 +1587,33 @@ var bootstrapCommand = new Command("bootstrap").description(
1577
1587
  "System snapshot"
1578
1588
  );
1579
1589
  if (!snap.autoRestartOnPowerFailure) {
1580
- const fix = unwrap2(
1581
- await p2.confirm({
1590
+ const fix = unwrap(
1591
+ await p3.confirm({
1582
1592
  message: "Auto-restart-on-power-failure is OFF. Enable now (requires sudo)?",
1583
1593
  initialValue: true
1584
1594
  })
1585
1595
  );
1586
1596
  if (fix) {
1587
1597
  const r = await enableAutoRestartOnPowerFailure();
1588
- if (!r.ok) p2.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
1598
+ if (!r.ok) p3.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
1589
1599
  }
1590
1600
  }
1591
1601
  if (!snap.diskSleepDisabled) {
1592
- const fix = unwrap2(
1593
- await p2.confirm({
1602
+ const fix = unwrap(
1603
+ await p3.confirm({
1594
1604
  message: "Disk sleep is enabled. Disable now (requires sudo)?",
1595
1605
  initialValue: true
1596
1606
  })
1597
1607
  );
1598
1608
  if (fix) {
1599
1609
  const r = await disableDiskSleep();
1600
- if (!r.ok) p2.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
1610
+ if (!r.ok) p3.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
1601
1611
  }
1602
1612
  }
1603
1613
  if (!opts.skipBrew) {
1604
1614
  const brewInfo = await detectBrew();
1605
1615
  if (!brewInfo.installed) {
1606
- p2.note(
1616
+ p3.note(
1607
1617
  [
1608
1618
  `Homebrew is not installed. Open another shell and run:`,
1609
1619
  "",
@@ -1613,9 +1623,9 @@ var bootstrapCommand = new Command("bootstrap").description(
1613
1623
  ].join("\n"),
1614
1624
  "Manual step"
1615
1625
  );
1616
- unwrap2(await p2.confirm({ message: "Homebrew installed?", initialValue: true }));
1626
+ unwrap(await p3.confirm({ message: "Homebrew installed?", initialValue: true }));
1617
1627
  }
1618
- const brewSpin = p2.spinner();
1628
+ const brewSpin = p3.spinner();
1619
1629
  brewSpin.start("Installing Studio packages\u2026");
1620
1630
  const report = await installPackages(STUDIO_PACKAGES, (pkg, status) => {
1621
1631
  brewSpin.message(`${status}: ${pkg.name}`);
@@ -1628,7 +1638,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1628
1638
  )
1629
1639
  );
1630
1640
  if (report.failed.length > 0) {
1631
- p2.note(
1641
+ p3.note(
1632
1642
  [
1633
1643
  report.failed.map((f) => `${pc2.red("\u2022")} ${f.pkg.name}: ${f.reason}`).join("\n"),
1634
1644
  "",
@@ -1637,8 +1647,8 @@ var bootstrapCommand = new Command("bootstrap").description(
1637
1647
  ].join("\n"),
1638
1648
  "Brew failures"
1639
1649
  );
1640
- const cont = unwrap2(
1641
- await p2.confirm({
1650
+ const cont = unwrap(
1651
+ await p3.confirm({
1642
1652
  message: "Continue anyway? (Default no \u2014 recommended to fix and re-run.)",
1643
1653
  initialValue: false
1644
1654
  })
@@ -1648,7 +1658,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1648
1658
  }
1649
1659
  await ensureGhAuth();
1650
1660
  await promptTailscaleSignin();
1651
- const repoSpin = p2.spinner();
1661
+ const repoSpin = p3.spinner();
1652
1662
  repoSpin.start("Locating your region node repo\u2026");
1653
1663
  const located = await locateOrCloneRepo({
1654
1664
  owner,
@@ -1665,59 +1675,49 @@ var bootstrapCommand = new Command("bootstrap").description(
1665
1675
  repoSpin.stop(
1666
1676
  located.kind === "cloned" ? pc2.green(`\u2713 Cloned ${owner}/${repoName} to ${repoPath}`) : pc2.green(`\u2713 Found region repo at ${repoPath}`)
1667
1677
  );
1668
- const op = await detectOp();
1669
- if (!op.installed || !op.signedIn) {
1670
- p2.cancel(
1671
- "1Password CLI not installed or not signed in. Run `op signin` and re-run bootstrap. (Or paste pgsodium key + Tunnel token by hand \u2014 manual path not in v0.1.)"
1672
- );
1673
- process.exit(1);
1674
- }
1675
- const keyTitle = `opuspopuli-${region}-pgsodium-root-key`;
1676
- const tunnelTitle = `opuspopuli-${region}-tunnel-token`;
1677
- const vaultArg = opts.vault ? { vault: opts.vault } : {};
1678
- const opSpin = p2.spinner();
1679
- opSpin.start("Reading pgsodium key + Tunnel token from 1Password\u2026");
1680
- const pgsodiumKey = await readSecretFromOp({ title: keyTitle, ...vaultArg });
1681
- const tunnelToken = await readSecretFromOp({ title: tunnelTitle, ...vaultArg });
1682
- if (!pgsodiumKey || !tunnelToken) {
1683
- opSpin.stop(pc2.red("\u2717 Required 1Password items missing."));
1684
- const missing = [
1685
- pgsodiumKey ? null : keyTitle,
1686
- tunnelToken ? null : tunnelTitle
1687
- ].filter(Boolean);
1688
- p2.cancel(
1689
- `Missing in 1Password (vault: ${opts.vault ?? "Private"}): ${missing.join(", ")}. Run \`create-op-node init\` first, or pass --vault if your items live elsewhere.`
1678
+ const keychain = await detectKeychain();
1679
+ if (!keychain.available) {
1680
+ p3.cancel(
1681
+ `${keychain.reason ?? "Keychain unavailable"}. Bootstrap requires the macOS Keychain.`
1690
1682
  );
1691
1683
  process.exit(1);
1692
1684
  }
1693
- if (!PGSODIUM_KEY_RE.test(pgsodiumKey)) {
1694
- opSpin.stop(pc2.red("\u2717 pgsodium item exists but isn't a 64-hex key."));
1695
- p2.cancel("Inspect the item in 1Password; it should be 64 lowercase hex characters.");
1696
- process.exit(1);
1697
- }
1698
- opSpin.stop(pc2.green("\u2713 Secrets read from 1Password."));
1685
+ const pgsodiumKey = await loadOrPromptSecret({
1686
+ region,
1687
+ account: "pgsodium-root-key",
1688
+ label: "pgsodium master key",
1689
+ placeholder: "64 lowercase hex characters",
1690
+ validate: (v) => PGSODIUM_KEY_RE.test(v) ? void 0 : "must be exactly 64 lowercase hex characters"
1691
+ });
1692
+ const tunnelToken = await loadOrPromptSecret({
1693
+ region,
1694
+ account: "tunnel-token",
1695
+ label: "Cloudflare Tunnel token",
1696
+ placeholder: "JWT-style base64url string from `terraform output tunnel_token`",
1697
+ validate: (v) => TUNNEL_TOKEN_RE.test(v) ? void 0 : "tunnel token must be a base64-url JWT"
1698
+ });
1699
1699
  if (!opts.skipLaunchAgent) {
1700
- const laSpin = p2.spinner();
1700
+ const laSpin = p3.spinner();
1701
1701
  laSpin.start("Writing pgsodium key file + LaunchAgent plist\u2026");
1702
1702
  const la = await setupLaunchAgent({ pgsodiumKey, tunnelToken });
1703
1703
  if (!la.ok) {
1704
1704
  laSpin.stop(pc2.red(`\u2717 LaunchAgent step ${la.step} failed.`));
1705
- p2.cancel(la.reason ?? "LaunchAgent setup failed.");
1705
+ p3.cancel(la.reason ?? "LaunchAgent setup failed.");
1706
1706
  process.exit(1);
1707
1707
  }
1708
1708
  laSpin.stop(pc2.green(`\u2713 LaunchAgent loaded (${la.paths.plistFile}).`));
1709
1709
  }
1710
- const ghcrSpin = p2.spinner();
1710
+ const ghcrSpin = p3.spinner();
1711
1711
  ghcrSpin.start("Authenticating Docker to ghcr.io\u2026");
1712
1712
  const ghcr = await loginToGhcr();
1713
1713
  if (!ghcr.ok) {
1714
1714
  ghcrSpin.stop(pc2.red("\u2717 ghcr.io login failed."));
1715
- p2.cancel(ghcr.reason ?? "ghcr.io login failed.");
1715
+ p3.cancel(ghcr.reason ?? "ghcr.io login failed.");
1716
1716
  process.exit(1);
1717
1717
  }
1718
1718
  ghcrSpin.stop(pc2.green("\u2713 Logged in to ghcr.io."));
1719
1719
  if (!opts.skipOllama) {
1720
- const olSpin = p2.spinner();
1720
+ const olSpin = p3.spinner();
1721
1721
  olSpin.start("Pulling + warming Ollama models\u2026");
1722
1722
  let olHealth = await checkOllamaHealth();
1723
1723
  if (!olHealth.reachable) {
@@ -1725,7 +1725,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1725
1725
  const start = await startOllamaService();
1726
1726
  if (!start.ok) {
1727
1727
  olSpin.stop(pc2.red("\u2717 Couldn't start Ollama service."));
1728
- p2.cancel(
1728
+ p3.cancel(
1729
1729
  `${start.reason ?? "unknown"} \u2014 start it manually with \`brew services start ollama\` and re-run with --skip-brew --skip-launch-agent.`
1730
1730
  );
1731
1731
  process.exit(1);
@@ -1736,7 +1736,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1736
1736
  olSpin.stop(
1737
1737
  pc2.yellow("\u26A0 Ollama service started but daemon not yet answering on :11434.")
1738
1738
  );
1739
- p2.cancel(
1739
+ p3.cancel(
1740
1740
  "Give it another few seconds, then re-run with --skip-brew --skip-launch-agent."
1741
1741
  );
1742
1742
  process.exit(1);
@@ -1752,11 +1752,11 @@ var bootstrapCommand = new Command("bootstrap").description(
1752
1752
  );
1753
1753
  const probe = await probeHostDockerInternal();
1754
1754
  if (!probe.ok) {
1755
- p2.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
1755
+ p3.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
1756
1756
  }
1757
1757
  }
1758
1758
  if (opts.skipStack) {
1759
- p2.outro(
1759
+ p3.outro(
1760
1760
  pc2.cyan(
1761
1761
  `Stack-up skipped. Run \`docker compose -f ${(opts.composeFile ?? ["docker-compose-prod.yml"]).join(" -f ")} pull && up -d\` from ${repoPath} when ready.`
1762
1762
  )
@@ -1769,25 +1769,25 @@ var bootstrapCommand = new Command("bootstrap").description(
1769
1769
  cwd: repoPath,
1770
1770
  ...opts.envFile ? { envFile: opts.envFile } : {}
1771
1771
  };
1772
- const pullSpin = p2.spinner();
1772
+ const pullSpin = p3.spinner();
1773
1773
  pullSpin.start("Pulling images from ghcr.io\u2026");
1774
1774
  const pull = await composePull(composeOpts);
1775
1775
  if (!pull.ok) {
1776
1776
  pullSpin.stop(pc2.red("\u2717 compose pull failed."));
1777
- p2.cancel(pull.reason ?? "compose pull failed.");
1777
+ p3.cancel(pull.reason ?? "compose pull failed.");
1778
1778
  process.exit(1);
1779
1779
  }
1780
1780
  pullSpin.stop(pc2.green("\u2713 Images pulled."));
1781
- const upSpin = p2.spinner();
1781
+ const upSpin = p3.spinner();
1782
1782
  upSpin.start("Starting the stack\u2026");
1783
1783
  const up = await composeUp(composeOpts);
1784
1784
  if (!up.ok) {
1785
1785
  upSpin.stop(pc2.red("\u2717 compose up failed."));
1786
- p2.cancel(up.reason ?? "compose up failed.");
1786
+ p3.cancel(up.reason ?? "compose up failed.");
1787
1787
  process.exit(1);
1788
1788
  }
1789
1789
  upSpin.stop(pc2.green("\u2713 Containers started \u2014 waiting for healthy\u2026"));
1790
- const healthSpin = p2.spinner();
1790
+ const healthSpin = p3.spinner();
1791
1791
  healthSpin.start("Polling for healthy containers (5s, up to 5 minutes)\u2026");
1792
1792
  const outcome = await waitForHealthy(composeOpts, {
1793
1793
  onPoll: (snaps) => {
@@ -1799,7 +1799,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1799
1799
  switch (outcome.kind) {
1800
1800
  case "healthy":
1801
1801
  healthSpin.stop(pc2.green(`\u2713 All ${outcome.snapshots.length} containers healthy.`));
1802
- p2.outro(
1802
+ p3.outro(
1803
1803
  pc2.cyan(
1804
1804
  `Region ${region} is up. Next: verify off-LAN with \`npx create-op-node verify --domain <your-domain>\`.`
1805
1805
  )
@@ -1807,22 +1807,22 @@ var bootstrapCommand = new Command("bootstrap").description(
1807
1807
  return;
1808
1808
  case "unhealthy":
1809
1809
  healthSpin.stop(pc2.red(`\u2717 ${outcome.problem}`));
1810
- p2.note(
1810
+ p3.note(
1811
1811
  outcome.snapshots.map((s) => ` ${kvHealth(s.health)} ${s.name} (${s.state})`).join("\n"),
1812
1812
  "Container state"
1813
1813
  );
1814
- p2.cancel(
1814
+ p3.cancel(
1815
1815
  `Inspect with \`docker compose -f ${composeFiles.join(" -f ")} logs\`. Fix and re-run.`
1816
1816
  );
1817
1817
  process.exit(1);
1818
1818
  // eslint-disable-next-line no-fallthrough
1819
1819
  case "timeout":
1820
1820
  healthSpin.stop(pc2.yellow("\u26A0 Timed out waiting for all containers to report healthy."));
1821
- p2.note(
1821
+ p3.note(
1822
1822
  outcome.snapshots.map((s) => ` ${kvHealth(s.health)} ${s.name} (${s.state})`).join("\n"),
1823
1823
  "Container state at timeout"
1824
1824
  );
1825
- p2.cancel(
1825
+ p3.cancel(
1826
1826
  `Re-run \`create-op-node bootstrap --skip-brew --skip-launch-agent --skip-ollama\` to skip the already-done phases and poll again once containers settle.`
1827
1827
  );
1828
1828
  process.exit(1);
@@ -1862,20 +1862,20 @@ function repoPathFromLocate(out) {
1862
1862
  function handleLocateError(out, owner, name) {
1863
1863
  switch (out.kind) {
1864
1864
  case "explicit-not-a-node-repo":
1865
- p2.cancel(
1865
+ p3.cancel(
1866
1866
  `${out.path} doesn't look like a node repo (missing one of docker-compose-prod.yml / supabase/init/pgsodium_getkey_env.sh). Point --repo-dir at the right place.`
1867
1867
  );
1868
1868
  return;
1869
1869
  case "gh-not-installed":
1870
- p2.cancel("`gh` not installed \u2014 install via `brew install gh` and re-run.");
1870
+ p3.cancel("`gh` not installed \u2014 install via `brew install gh` and re-run.");
1871
1871
  return;
1872
1872
  case "clone-failed":
1873
- p2.cancel(
1873
+ p3.cancel(
1874
1874
  `Couldn't clone ${owner}/${name}: ${out.reason}. Check repo exists + you have access.`
1875
1875
  );
1876
1876
  return;
1877
1877
  case "clone-disallowed":
1878
- p2.cancel(
1878
+ p3.cancel(
1879
1879
  "No checked-out node repo found and --no-clone (or similar) was set. Provide --repo-dir."
1880
1880
  );
1881
1881
  return;
@@ -1890,12 +1890,12 @@ function handleLocateError(out, owner, name) {
1890
1890
  async function ensureGhAuth() {
1891
1891
  const status = await safeExeca("gh", ["auth", "status"]);
1892
1892
  if (status === null) {
1893
- p2.cancel("`gh` not on PATH. Install via `brew install gh` and re-run.");
1893
+ p3.cancel("`gh` not on PATH. Install via `brew install gh` and re-run.");
1894
1894
  process.exit(1);
1895
1895
  }
1896
1896
  if (status.exitCode === 0) return;
1897
1897
  const detail = (status.stderr || status.stdout).trim();
1898
- p2.note(
1898
+ p3.note(
1899
1899
  [
1900
1900
  "`gh` is not signed in (or signed in to a different account / missing scope).",
1901
1901
  detail ? "" : void 0,
@@ -1909,12 +1909,12 @@ async function ensureGhAuth() {
1909
1909
  ].filter((l) => typeof l === "string").join("\n"),
1910
1910
  "Manual step"
1911
1911
  );
1912
- unwrap2(await p2.confirm({ message: "gh signed in?", initialValue: true }));
1912
+ unwrap(await p3.confirm({ message: "gh signed in?", initialValue: true }));
1913
1913
  }
1914
1914
  async function promptTailscaleSignin() {
1915
1915
  const status = await safeExeca("tailscale", ["status"]);
1916
1916
  if (status !== null && status.exitCode === 0) return;
1917
- p2.note(
1917
+ p3.note(
1918
1918
  [
1919
1919
  "Tailscale needs to be signed in (or you can skip \u2014 Tailscale is",
1920
1920
  "optional; it provides out-of-band SSH from your laptop. The rest",
@@ -1928,8 +1928,319 @@ async function promptTailscaleSignin() {
1928
1928
  ].join("\n"),
1929
1929
  "Manual step"
1930
1930
  );
1931
- unwrap2(await p2.confirm({ message: "Tailscale signed in (or skipped)?", initialValue: true }));
1931
+ unwrap(await p3.confirm({ message: "Tailscale signed in (or skipped)?", initialValue: true }));
1932
+ }
1933
+ async function loadOrPromptSecret(input) {
1934
+ const coords = { region: input.region, account: input.account };
1935
+ const spin = p3.spinner();
1936
+ spin.start(`Reading ${input.label} from Keychain\u2026`);
1937
+ const existing = await readSecret(coords);
1938
+ if (existing && input.validate(existing) === void 0) {
1939
+ spin.stop(pc2.green(`\u2713 ${input.label} read from Keychain.`));
1940
+ return existing;
1941
+ }
1942
+ spin.stop(
1943
+ existing ? pc2.yellow(`\u26A0 ${input.label} in Keychain failed validation \u2014 will re-prompt.`) : pc2.dim(`\xB7 ${input.label} not in Keychain \u2014 will prompt.`)
1944
+ );
1945
+ const pasted = unwrap(
1946
+ await p3.password({
1947
+ message: `Paste the ${input.label} for region ${input.region}:`,
1948
+ validate: (v) => input.validate(v ?? "")
1949
+ })
1950
+ );
1951
+ const save = await saveSecret(coords, pasted);
1952
+ if (!save.written) {
1953
+ p3.note(`${pc2.yellow("\u26A0")} Couldn't persist to Keychain: ${save.reason ?? "unknown"}. Continuing anyway.`, "Keychain");
1954
+ } else {
1955
+ p3.note(`${pc2.green("\u2713")} Stored ${input.label} in Keychain for re-runs.`, "Keychain");
1956
+ }
1957
+ return pasted;
1958
+ }
1959
+ var REGION_RE = /^[a-z0-9-]{2,32}$/;
1960
+ var RESET_PHASES = {
1961
+ STOP_STACK: "Stop stack",
1962
+ LAUNCH_AGENT: "LaunchAgent",
1963
+ DOCKER_LOGOUT: "docker logout"
1964
+ };
1965
+ var DEFAULT_DEPS = {
1966
+ composeDown,
1967
+ teardownLaunchAgent,
1968
+ dockerLogout
1969
+ };
1970
+ async function runReset(input, deps = DEFAULT_DEPS) {
1971
+ const phases = [];
1972
+ const push = (ph) => {
1973
+ phases.push(ph);
1974
+ deps.onPhase?.(ph);
1975
+ };
1976
+ if (!input.stack) {
1977
+ push({
1978
+ name: RESET_PHASES.STOP_STACK,
1979
+ status: "skipped",
1980
+ detail: "--skip-stack or no repo path resolved"
1981
+ });
1982
+ } else if (input.dryRun) {
1983
+ push({
1984
+ name: RESET_PHASES.STOP_STACK,
1985
+ status: "dry-run",
1986
+ detail: `would run: docker compose -f ${input.stack.composeFiles.join(" -f ")} down${input.stack.wipeVolumes ? " -v" : ""}${input.stack.removeOrphans ? " --remove-orphans" : ""}`
1987
+ });
1988
+ } else {
1989
+ const result2 = await deps.composeDown({
1990
+ files: input.stack.composeFiles,
1991
+ cwd: input.stack.repoPath,
1992
+ ...input.stack.envFile ? { envFile: input.stack.envFile } : {},
1993
+ wipeVolumes: input.stack.wipeVolumes,
1994
+ removeOrphans: input.stack.removeOrphans
1995
+ });
1996
+ if (result2.ok) {
1997
+ push({
1998
+ name: RESET_PHASES.STOP_STACK,
1999
+ status: "ok",
2000
+ detail: input.stack.wipeVolumes ? "volumes destroyed" : "containers stopped, volumes preserved"
2001
+ });
2002
+ } else {
2003
+ push({ name: RESET_PHASES.STOP_STACK, status: "fail", detail: result2.reason ?? "unknown failure" });
2004
+ }
2005
+ }
2006
+ if (!input.launchAgent) {
2007
+ push({ name: RESET_PHASES.LAUNCH_AGENT, status: "skipped", detail: "--skip-launch-agent or no plist found" });
2008
+ } else if (input.dryRun) {
2009
+ push({
2010
+ name: RESET_PHASES.LAUNCH_AGENT,
2011
+ status: "dry-run",
2012
+ detail: `would unload ${input.launchAgent.paths.plistFile}, rm plist${input.launchAgent.keepKeyFile ? "" : " + key file"}`
2013
+ });
2014
+ } else {
2015
+ const result2 = await deps.teardownLaunchAgent(
2016
+ input.launchAgent.paths,
2017
+ { keepKeyFile: input.launchAgent.keepKeyFile }
2018
+ );
2019
+ if (result2.ok) {
2020
+ const removed = result2.steps.filter((s) => s.step !== "unload").map((s) => s.step).join(", ");
2021
+ push({ name: RESET_PHASES.LAUNCH_AGENT, status: "ok", detail: `unloaded + ${removed}` });
2022
+ } else {
2023
+ const fail = result2.steps.find((s) => !s.ok);
2024
+ push({
2025
+ name: RESET_PHASES.LAUNCH_AGENT,
2026
+ status: "fail",
2027
+ detail: `${fail?.step}: ${fail?.reason ?? "unknown"}`
2028
+ });
2029
+ }
2030
+ }
2031
+ if (!input.dockerLogout) {
2032
+ push({ name: RESET_PHASES.DOCKER_LOGOUT, status: "skipped", detail: "--skip-docker-logout" });
2033
+ } else if (input.dryRun) {
2034
+ push({
2035
+ name: RESET_PHASES.DOCKER_LOGOUT,
2036
+ status: "dry-run",
2037
+ detail: `would run: docker logout ${input.dockerLogout.registry}`
2038
+ });
2039
+ } else {
2040
+ const result2 = await deps.dockerLogout(input.dockerLogout.registry);
2041
+ if (result2.ok) {
2042
+ push({
2043
+ name: RESET_PHASES.DOCKER_LOGOUT,
2044
+ status: "ok",
2045
+ detail: `credentials removed from ${input.dockerLogout.registry}`
2046
+ });
2047
+ } else {
2048
+ push({ name: RESET_PHASES.DOCKER_LOGOUT, status: "warn", detail: result2.reason ?? "unknown" });
2049
+ }
2050
+ }
2051
+ return { phases };
2052
+ }
2053
+ async function fileExists2(path) {
2054
+ try {
2055
+ await access(path);
2056
+ return true;
2057
+ } catch {
2058
+ return false;
2059
+ }
2060
+ }
2061
+ function phaseIcon(ph) {
2062
+ switch (ph.status) {
2063
+ case "ok":
2064
+ return pc2.green("\u2713");
2065
+ case "warn":
2066
+ return pc2.yellow("\u26A0");
2067
+ case "skipped":
2068
+ return pc2.dim("\xB7");
2069
+ case "dry-run":
2070
+ return pc2.cyan("?");
2071
+ case "fail":
2072
+ return pc2.red("\u2717");
2073
+ }
1932
2074
  }
2075
+ var resetCommand = new Command("reset").description(
2076
+ "Reverse the Mac Studio side of bootstrap: stop containers, unload the LaunchAgent, remove the pgsodium key file. Preserves docker volumes by default \u2014 pass --wipe-data for a full data nuke (requires retyping the region label)."
2077
+ ).addOption(new Option("--region <slug>", "Region label (used for --wipe-data confirmation + finding the repo)")).addOption(new Option("--owner <owner>", "GitHub owner for the node repo").default("OpusPopuli")).addOption(new Option("--repo-dir <path>", "Explicit path to a checked-out node repo")).addOption(
2078
+ new Option(
2079
+ "--compose-file <path>",
2080
+ "Repeatable. Compose file relative to repo root. Default: docker-compose-prod.yml"
2081
+ ).default(["docker-compose-prod.yml"])
2082
+ ).addOption(new Option("--env-file <path>", "Compose --env-file. Default: .env.production")).addOption(
2083
+ new Option(
2084
+ "--wipe-data",
2085
+ "DESTROYS docker volumes (adds -v to `compose down`). Requires retyping the region label as confirmation. Default off \u2014 volumes preserved."
2086
+ ).default(false)
2087
+ ).addOption(
2088
+ new Option(
2089
+ "--no-remove-orphans",
2090
+ "Do NOT pass --remove-orphans to compose down. Preserves containers from compose files no longer included."
2091
+ )
2092
+ ).addOption(new Option("--skip-stack", "Don't touch docker compose").default(false)).addOption(new Option("--skip-launch-agent", "Don't touch the LaunchAgent").default(false)).addOption(
2093
+ new Option(
2094
+ "--keep-key-file",
2095
+ "Preserve the pgsodium key file (only matters when LaunchAgent teardown runs). Useful as belt-and-suspenders backup before a wipe-data run."
2096
+ ).default(false)
2097
+ ).addOption(new Option("--skip-docker-logout", "Don't run `docker logout`").default(false)).addOption(new Option("--registry <registry>", "Registry to log out of").default(GHCR_REGISTRY)).addOption(new Option("--dry-run", "Show what would happen without acting").default(false)).addOption(new Option("-y, --yes", "Skip non-destructive confirmations (wipe still confirms)").default(false)).action(async (opts) => {
2098
+ p3.intro(pc2.bgCyan(pc2.black(" create-op-node reset ")));
2099
+ const wipeData = opts.wipeData ?? false;
2100
+ const removeOrphans = opts.removeOrphans ?? true;
2101
+ const region = opts.region ? opts.region : unwrap(
2102
+ await p3.text({
2103
+ message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
2104
+ placeholder: "us-ca",
2105
+ validate: (v) => REGION_RE.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
2106
+ })
2107
+ );
2108
+ if (!REGION_RE.test(region)) {
2109
+ p3.cancel(`--region ${JSON.stringify(region)} is not a valid region slug.`);
2110
+ process.exit(2);
2111
+ }
2112
+ const owner = opts.owner ?? "OpusPopuli";
2113
+ const repoName = `opuspopuli-node-${region}`;
2114
+ const launchAgentPaths = defaultPaths();
2115
+ const plistExists = await fileExists2(launchAgentPaths.plistFile);
2116
+ const keyFileExists = await fileExists2(launchAgentPaths.keyFile);
2117
+ let repoPath;
2118
+ let stackSkipReason;
2119
+ if (!opts.skipStack) {
2120
+ const located = await locateOrCloneRepo({
2121
+ owner,
2122
+ name: repoName,
2123
+ cwd: process.cwd(),
2124
+ allowClone: false,
2125
+ ...opts.repoDir ? { explicit: opts.repoDir } : {}
2126
+ });
2127
+ switch (located.kind) {
2128
+ case "found":
2129
+ repoPath = located.path;
2130
+ break;
2131
+ case "explicit-not-a-node-repo":
2132
+ p3.cancel(
2133
+ `--repo-dir ${JSON.stringify(located.path)} doesn't look like a node repo (missing one of: ${NODE_REPO_MARKERS.join(", ")}). Verify the path or drop --repo-dir to let reset search the cwd.`
2134
+ );
2135
+ process.exit(2);
2136
+ break;
2137
+ case "clone-disallowed":
2138
+ stackSkipReason = `no checkout found at ${process.cwd()}; pass --repo-dir to target one`;
2139
+ break;
2140
+ case "cloned":
2141
+ case "gh-not-installed":
2142
+ case "clone-failed":
2143
+ stackSkipReason = `unexpected outcome ${located.kind} from locateOrCloneRepo with allowClone=false`;
2144
+ break;
2145
+ }
2146
+ } else {
2147
+ stackSkipReason = "--skip-stack";
2148
+ }
2149
+ let runningContainers = null;
2150
+ if (repoPath) {
2151
+ runningContainers = await composePs({
2152
+ files: resolveComposeFiles(repoPath, opts.composeFile),
2153
+ ...opts.envFile ? { envFile: opts.envFile } : {}
2154
+ });
2155
+ }
2156
+ p3.note(
2157
+ [
2158
+ `Region: ${pc2.cyan(region)}`,
2159
+ `Repo path: ${repoPath ? pc2.cyan(repoPath) : pc2.dim(`not used (${stackSkipReason ?? "no repo path resolved"})`)}`,
2160
+ `Running containers: ${runningContainers === null ? pc2.dim("unknown") : runningContainers.length === 0 ? pc2.dim("none") : pc2.cyan(`${runningContainers.length} listed by compose ps`)}`,
2161
+ `LaunchAgent plist: ${plistExists ? pc2.cyan(launchAgentPaths.plistFile) : pc2.dim("not present")}`,
2162
+ `pgsodium key file: ${keyFileExists ? pc2.cyan(launchAgentPaths.keyFile) : pc2.dim("not present")}`,
2163
+ `Registry to log out: ${pc2.cyan(opts.registry ?? GHCR_REGISTRY)}`,
2164
+ `Volume policy: ${wipeData ? pc2.red("WIPE") : pc2.green("preserve (default)")}`,
2165
+ `Dry run: ${opts.dryRun ? pc2.yellow("yes") : pc2.dim("no")}`
2166
+ ].join("\n"),
2167
+ "Snapshot"
2168
+ );
2169
+ if (wipeData && !opts.dryRun) {
2170
+ unwrap(
2171
+ await p3.text({
2172
+ message: pc2.red(`Type the region label to confirm WIPING all volumes (the slug you'd use with --region):`),
2173
+ validate: (v) => v === region ? void 0 : `must match the region label exactly`
2174
+ })
2175
+ );
2176
+ } else if (!opts.dryRun && !opts.yes) {
2177
+ const cont = unwrap(
2178
+ await p3.confirm({
2179
+ message: "Proceed with reset? (volumes will be preserved)",
2180
+ initialValue: true
2181
+ })
2182
+ );
2183
+ if (!cont) {
2184
+ p3.cancel("Cancelled.");
2185
+ process.exit(0);
2186
+ }
2187
+ }
2188
+ const stack = repoPath ? {
2189
+ repoPath,
2190
+ composeFiles: resolveComposeFiles(repoPath, opts.composeFile),
2191
+ wipeVolumes: wipeData,
2192
+ removeOrphans,
2193
+ ...opts.envFile ? { envFile: opts.envFile } : {}
2194
+ } : void 0;
2195
+ const launchAgent = plistExists && !opts.skipLaunchAgent ? {
2196
+ paths: launchAgentPaths,
2197
+ keepKeyFile: opts.keepKeyFile ?? false
2198
+ } : void 0;
2199
+ const dockerLogoutInput = !opts.skipDockerLogout ? { registry: opts.registry ?? GHCR_REGISTRY } : void 0;
2200
+ const phaseSpins = /* @__PURE__ */ new Map();
2201
+ const actingPhases = [];
2202
+ if (stack && !opts.dryRun) actingPhases.push(RESET_PHASES.STOP_STACK);
2203
+ if (launchAgent && !opts.dryRun) actingPhases.push(RESET_PHASES.LAUNCH_AGENT);
2204
+ if (dockerLogoutInput && !opts.dryRun) actingPhases.push(RESET_PHASES.DOCKER_LOGOUT);
2205
+ for (const name of actingPhases) {
2206
+ const s = p3.spinner();
2207
+ s.start(`${name}\u2026`);
2208
+ phaseSpins.set(name, s);
2209
+ }
2210
+ const renderPhase = (ph) => {
2211
+ const spin = phaseSpins.get(ph.name);
2212
+ const line = `${phaseIcon(ph)} ${ph.name}: ${pc2.dim(ph.detail)}`;
2213
+ if (spin) {
2214
+ spin.stop(line);
2215
+ phaseSpins.delete(ph.name);
2216
+ } else {
2217
+ p3.log.info(line);
2218
+ }
2219
+ };
2220
+ const report = await runReset(
2221
+ {
2222
+ ...stack ? { stack } : {},
2223
+ ...launchAgent ? { launchAgent } : {},
2224
+ ...dockerLogoutInput ? { dockerLogout: dockerLogoutInput } : {},
2225
+ dryRun: opts.dryRun ?? false
2226
+ },
2227
+ { ...DEFAULT_DEPS, onPhase: renderPhase }
2228
+ );
2229
+ for (const [, spin] of phaseSpins) spin.stop(pc2.dim("\u2014 skipped"));
2230
+ const failed = report.phases.filter((ph) => ph.status === "fail").length;
2231
+ if (failed > 0) {
2232
+ p3.outro(pc2.red(`${failed} step${failed === 1 ? "" : "s"} failed.`));
2233
+ process.exit(1);
2234
+ } else if (opts.dryRun) {
2235
+ p3.outro(pc2.cyan("Dry run complete. Re-run without --dry-run to apply."));
2236
+ } else {
2237
+ p3.outro(
2238
+ pc2.green(
2239
+ wipeData ? `Reset complete \u2014 volumes wiped. Re-run \`create-op-node bootstrap --region ${region}\` to start fresh.` : `Reset complete \u2014 volumes preserved. Re-run \`create-op-node bootstrap --region ${region}\` to bring the stack back up.`
2240
+ )
2241
+ );
2242
+ }
2243
+ });
1933
2244
 
1934
2245
  // src/lib/cosign.ts
1935
2246
  var COSIGN_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
@@ -1965,8 +2276,8 @@ async function cosignVerifyImage(input) {
1965
2276
  // src/lib/http.ts
1966
2277
  var GRAPHQL_ACCEPT = "application/graphql-response+json, application/json";
1967
2278
  async function readBodyCapped(res) {
1968
- const text5 = await res.text();
1969
- return text5.slice(0, BODY_PREVIEW_MAX);
2279
+ const text6 = await res.text();
2280
+ return text6.slice(0, BODY_PREVIEW_MAX);
1970
2281
  }
1971
2282
  async function httpProbe(input) {
1972
2283
  const expected = input.expectedStatus ?? 200;
@@ -2004,22 +2315,22 @@ async function graphqlProbe(input) {
2004
2315
  body: JSON.stringify({ query: "{ __typename }" }),
2005
2316
  signal: ctrl.signal
2006
2317
  });
2007
- const text5 = await readBodyCapped(res);
2318
+ const text6 = await readBodyCapped(res);
2008
2319
  if (res.status !== 200) {
2009
2320
  return {
2010
2321
  ok: false,
2011
2322
  status: res.status,
2012
- reason: `expected HTTP 200, got ${res.status}: ${text5}`
2323
+ reason: `expected HTTP 200, got ${res.status}: ${text6}`
2013
2324
  };
2014
2325
  }
2015
2326
  let envelope;
2016
2327
  try {
2017
- envelope = JSON.parse(text5);
2328
+ envelope = JSON.parse(text6);
2018
2329
  } catch {
2019
2330
  return {
2020
2331
  ok: false,
2021
2332
  status: res.status,
2022
- reason: `response was not JSON: ${text5}`
2333
+ reason: `response was not JSON: ${text6}`
2023
2334
  };
2024
2335
  }
2025
2336
  const data = envelope.data;
@@ -2028,7 +2339,7 @@ async function graphqlProbe(input) {
2028
2339
  return {
2029
2340
  ok: false,
2030
2341
  status: res.status,
2031
- reason: `response missing { data: { __typename: string } }: ${text5}`
2342
+ reason: `response missing { data: { __typename: string } }: ${text6}`
2032
2343
  };
2033
2344
  }
2034
2345
  return { ok: true, typename };
@@ -2096,13 +2407,6 @@ function tlsHandshake(input) {
2096
2407
  }
2097
2408
 
2098
2409
  // src/commands/verify.ts
2099
- function unwrap3(value) {
2100
- if (p2.isCancel(value)) {
2101
- p2.cancel("Cancelled.");
2102
- process.exit(0);
2103
- }
2104
- return value;
2105
- }
2106
2410
  var DOMAIN_RE = /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i;
2107
2411
  function summarize(report) {
2108
2412
  let failed = 0;
@@ -2118,14 +2422,14 @@ function formatExpiry(days) {
2118
2422
  if (days === 0) return "expires today";
2119
2423
  return `${days}d to expiry`;
2120
2424
  }
2121
- var DEFAULT_DEPS = {
2425
+ var DEFAULT_DEPS2 = {
2122
2426
  tls: tlsHandshake,
2123
2427
  http: httpProbe,
2124
2428
  graphql: graphqlProbe,
2125
2429
  tunnel: tunnelStatus,
2126
2430
  cosign: cosignVerifyImage
2127
2431
  };
2128
- async function runVerify(input, deps = DEFAULT_DEPS) {
2432
+ async function runVerify(input, deps = DEFAULT_DEPS2) {
2129
2433
  const phases = [];
2130
2434
  const push = (ph) => {
2131
2435
  phases.push(ph);
@@ -2244,26 +2548,26 @@ var verifyCommand = new Command("verify").description(
2244
2548
  ).addOption(
2245
2549
  new Option("--show-skipped", "Include skipped phases in the summary").default(false)
2246
2550
  ).action(async (opts) => {
2247
- p2.intro(pc2.bgCyan(pc2.black(" create-op-node verify ")));
2248
- const domain = opts.domain ? opts.domain : unwrap3(
2249
- await p2.text({
2551
+ p3.intro(pc2.bgCyan(pc2.black(" create-op-node verify ")));
2552
+ const domain = opts.domain ? opts.domain : unwrap(
2553
+ await p3.text({
2250
2554
  message: "Public domain of the node?",
2251
2555
  placeholder: "yournode.example.org",
2252
2556
  validate: (v) => DOMAIN_RE.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens, dots; must contain a dot and a TLD"
2253
2557
  })
2254
2558
  );
2255
2559
  if (!DOMAIN_RE.test(domain)) {
2256
- p2.cancel(`--domain ${JSON.stringify(domain)} doesn't look like a domain.`);
2560
+ p3.cancel(`--domain ${JSON.stringify(domain)} doesn't look like a domain.`);
2257
2561
  process.exit(2);
2258
2562
  }
2259
2563
  const rawDays = opts.certWarnDays ?? "14";
2260
2564
  const certWarnDays = Number.parseInt(rawDays, 10);
2261
2565
  if (!Number.isFinite(certWarnDays) || certWarnDays < 0 || !/^\d+$/.test(rawDays)) {
2262
- p2.cancel(`--cert-warn-days must be a non-negative integer (got ${JSON.stringify(rawDays)}).`);
2566
+ p3.cancel(`--cert-warn-days must be a non-negative integer (got ${JSON.stringify(rawDays)}).`);
2263
2567
  process.exit(2);
2264
2568
  }
2265
2569
  if (opts.cfToken && opts.cfTokenFile) {
2266
- p2.cancel("Pass either --cf-token or --cf-token-file, not both.");
2570
+ p3.cancel("Pass either --cf-token or --cf-token-file, not both.");
2267
2571
  process.exit(2);
2268
2572
  }
2269
2573
  let cfToken = opts.cfToken;
@@ -2271,7 +2575,7 @@ var verifyCommand = new Command("verify").description(
2271
2575
  try {
2272
2576
  cfToken = readTokenFile(opts.cfTokenFile);
2273
2577
  } catch (err) {
2274
- p2.cancel(err.message);
2578
+ p3.cancel(err.message);
2275
2579
  process.exit(2);
2276
2580
  }
2277
2581
  }
@@ -2287,10 +2591,10 @@ var verifyCommand = new Command("verify").description(
2287
2591
  activeSpin = null;
2288
2592
  };
2289
2593
  const deps = {
2290
- ...DEFAULT_DEPS,
2594
+ ...DEFAULT_DEPS2,
2291
2595
  onPhase: renderPhase
2292
2596
  };
2293
- activeSpin = p2.spinner();
2597
+ activeSpin = p3.spinner();
2294
2598
  activeSpin.start("Running checks\u2026");
2295
2599
  const report = await runVerify(
2296
2600
  {
@@ -2309,24 +2613,24 @@ var verifyCommand = new Command("verify").description(
2309
2613
  const visible = opts.showSkipped ?? false ? report.phases : report.phases.filter((ph) => ph.status !== "skipped");
2310
2614
  const lines = visible.map((ph, i) => {
2311
2615
  const idx = report.phases.indexOf(ph) + 1;
2312
- return `${phaseIcon(ph)} [${idx}/${totalPhases}] ${ph.name}: ${pc2.dim(ph.detail)}`;
2616
+ return `${phaseIcon2(ph)} [${idx}/${totalPhases}] ${ph.name}: ${pc2.dim(ph.detail)}`;
2313
2617
  });
2314
2618
  const skippedCount = report.phases.length - visible.length;
2315
2619
  if (skippedCount > 0 && !opts.showSkipped) {
2316
2620
  lines.push(pc2.dim(`\xB7 ${skippedCount} phase${skippedCount === 1 ? "" : "s"} skipped (run with --show-skipped to see them)`));
2317
2621
  }
2318
- p2.note(lines.join("\n"), "Summary");
2622
+ p3.note(lines.join("\n"), "Summary");
2319
2623
  const summary = summarize(report);
2320
2624
  if (summary.ok) {
2321
- p2.outro(
2625
+ p3.outro(
2322
2626
  summary.warned === 0 ? pc2.green("All checks passed.") : pc2.yellow(`Passed with ${summary.warned} warning${summary.warned === 1 ? "" : "s"}.`)
2323
2627
  );
2324
2628
  } else {
2325
- p2.outro(pc2.red(`${summary.failed} check${summary.failed === 1 ? "" : "s"} failed.`));
2629
+ p3.outro(pc2.red(`${summary.failed} check${summary.failed === 1 ? "" : "s"} failed.`));
2326
2630
  process.exit(1);
2327
2631
  }
2328
2632
  });
2329
- function phaseIcon(ph) {
2633
+ function phaseIcon2(ph) {
2330
2634
  switch (ph.status) {
2331
2635
  case "ok":
2332
2636
  return pc2.green("\u2713");
@@ -2339,7 +2643,7 @@ function phaseIcon(ph) {
2339
2643
  }
2340
2644
  }
2341
2645
  function formatPhaseLine(prefix, ph) {
2342
- const icon = phaseIcon(ph);
2646
+ const icon = phaseIcon2(ph);
2343
2647
  const colorize = ph.status === "ok" ? pc2.green : ph.status === "warn" ? pc2.yellow : ph.status === "skipped" ? pc2.dim : pc2.red;
2344
2648
  return colorize(`${icon} ${prefix}: ${ph.detail}`);
2345
2649
  }
@@ -3122,13 +3426,6 @@ function validateRegionConfig(file) {
3122
3426
  }
3123
3427
 
3124
3428
  // src/commands/region.ts
3125
- function unwrap4(value) {
3126
- if (p2.isCancel(value)) {
3127
- p2.cancel("Cancelled.");
3128
- process.exit(0);
3129
- }
3130
- return value;
3131
- }
3132
3429
  var regionCommand = new Command("region").description(
3133
3430
  "Scaffold a schema-valid region config for the OpusPopuli/opuspopuli-regions repo. Run this from inside your checkout of that repo."
3134
3431
  ).addOption(
@@ -3141,8 +3438,8 @@ var regionCommand = new Command("region").description(
3141
3438
  "current directory"
3142
3439
  )
3143
3440
  ).addOption(new Option("-f, --force", "Overwrite an existing config file").default(false)).action(async (opts) => {
3144
- p2.intro(pc2.bgMagenta(pc2.black(" create-op-node region ")));
3145
- p2.note(
3441
+ p3.intro(pc2.bgMagenta(pc2.black(" create-op-node region ")));
3442
+ p3.note(
3146
3443
  [
3147
3444
  "Scaffolds a declarative region config JSON for opuspopuli-regions \u2014",
3148
3445
  "the file that defines WHAT civic data a region collects.",
@@ -3156,19 +3453,19 @@ var regionCommand = new Command("region").description(
3156
3453
  const targetDir = opts.outDir ?? process.cwd();
3157
3454
  const looksRight = await looksLikeRegionsRepo(targetDir);
3158
3455
  if (!looksRight) {
3159
- const proceed = unwrap4(
3160
- await p2.confirm({
3456
+ const proceed = unwrap(
3457
+ await p3.confirm({
3161
3458
  message: `${targetDir} doesn't look like an opuspopuli-regions checkout (no schema/region-plugin.schema.json). Continue anyway?`,
3162
3459
  initialValue: false
3163
3460
  })
3164
3461
  );
3165
3462
  if (!proceed) {
3166
- p2.cancel("Cancelled \u2014 run again from your opuspopuli-regions checkout, or pass --out-dir.");
3463
+ p3.cancel("Cancelled \u2014 run again from your opuspopuli-regions checkout, or pass --out-dir.");
3167
3464
  process.exit(0);
3168
3465
  }
3169
3466
  }
3170
- const level = opts.level === "state" || opts.level === "county" ? opts.level : unwrap4(
3171
- await p2.select({
3467
+ const level = opts.level === "state" || opts.level === "county" ? opts.level : unwrap(
3468
+ await p3.select({
3172
3469
  message: "What level is this region?",
3173
3470
  options: [
3174
3471
  { value: "state", label: "State", hint: "e.g. California" },
@@ -3176,8 +3473,8 @@ var regionCommand = new Command("region").description(
3176
3473
  ]
3177
3474
  })
3178
3475
  );
3179
- const rawName = opts.name ?? unwrap4(
3180
- await p2.text({
3476
+ const rawName = opts.name ?? unwrap(
3477
+ await p3.text({
3181
3478
  message: level === "county" ? "County name?" : "State name?",
3182
3479
  placeholder: level === "county" ? "Alameda" : "California",
3183
3480
  validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
@@ -3185,62 +3482,62 @@ var regionCommand = new Command("region").description(
3185
3482
  );
3186
3483
  let parentSlug;
3187
3484
  if (level === "county") {
3188
- parentSlug = opts.parent ?? unwrap4(
3189
- await p2.text({
3485
+ parentSlug = opts.parent ?? unwrap(
3486
+ await p3.text({
3190
3487
  message: "Parent state slug?",
3191
3488
  placeholder: "california",
3192
3489
  validate: (v) => isValidSlug(v ?? "") ? void 0 : "kebab-case (lowercase letters, digits, hyphens)"
3193
3490
  })
3194
3491
  );
3195
3492
  if (!isValidSlug(parentSlug)) {
3196
- p2.cancel(`Parent slug "${parentSlug}" must be kebab-case.`);
3493
+ p3.cancel(`Parent slug "${parentSlug}" must be kebab-case.`);
3197
3494
  process.exit(1);
3198
3495
  }
3199
3496
  }
3200
3497
  const ownSlug = slugify(rawName);
3201
3498
  const regionId = level === "county" ? `${parentSlug}-${ownSlug}` : ownSlug;
3202
3499
  if (!isValidSlug(regionId)) {
3203
- p2.cancel(`Derived regionId "${regionId}" is not valid kebab-case. Try a simpler name.`);
3500
+ p3.cancel(`Derived regionId "${regionId}" is not valid kebab-case. Try a simpler name.`);
3204
3501
  process.exit(1);
3205
3502
  }
3206
- const displayName = unwrap4(
3207
- await p2.text({
3503
+ const displayName = unwrap(
3504
+ await p3.text({
3208
3505
  message: "Display name?",
3209
3506
  defaultValue: rawName
3210
3507
  })
3211
3508
  );
3212
3509
  const regionName = displayName || rawName;
3213
- const description = unwrap4(
3214
- await p2.text({
3510
+ const description = unwrap(
3511
+ await p3.text({
3215
3512
  message: "One-line description of the data coverage?",
3216
3513
  placeholder: level === "county" ? `Civic data for ${rawName} County` : `Civic data for the state of ${rawName}`,
3217
3514
  validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
3218
3515
  })
3219
3516
  );
3220
- const stateCode = (opts.stateCode ?? unwrap4(
3221
- await p2.text({
3517
+ const stateCode = (opts.stateCode ?? unwrap(
3518
+ await p3.text({
3222
3519
  message: "Two-letter state code?",
3223
3520
  placeholder: "CA",
3224
3521
  validate: (v) => isValidStateCode((v ?? "").toUpperCase()) ? void 0 : "Two letters, e.g. CA"
3225
3522
  })
3226
3523
  )).toUpperCase();
3227
3524
  if (!isValidStateCode(stateCode)) {
3228
- p2.cancel(`State code "${stateCode}" must be two letters.`);
3525
+ p3.cancel(`State code "${stateCode}" must be two letters.`);
3229
3526
  process.exit(1);
3230
3527
  }
3231
- const fipsCode = opts.fips ?? unwrap4(
3232
- await p2.text({
3528
+ const fipsCode = opts.fips ?? unwrap(
3529
+ await p3.text({
3233
3530
  message: level === "county" ? "County FIPS (5 digits)?" : "State FIPS (2 digits)?",
3234
3531
  placeholder: level === "county" ? "06001" : "06",
3235
3532
  validate: (v) => isValidFips(v ?? "", level) ? void 0 : `Expected ${level === "county" ? "5" : "2"} digits`
3236
3533
  })
3237
3534
  );
3238
3535
  if (!isValidFips(fipsCode, level)) {
3239
- p2.cancel(`FIPS "${fipsCode}" is the wrong length for a ${level}.`);
3536
+ p3.cancel(`FIPS "${fipsCode}" is the wrong length for a ${level}.`);
3240
3537
  process.exit(1);
3241
3538
  }
3242
- const timezone = opts.timezone ?? unwrap4(
3243
- await p2.text({
3539
+ const timezone = opts.timezone ?? unwrap(
3540
+ await p3.text({
3244
3541
  message: "IANA timezone?",
3245
3542
  defaultValue: "America/Los_Angeles",
3246
3543
  placeholder: "America/Los_Angeles",
@@ -3249,35 +3546,35 @@ var regionCommand = new Command("region").description(
3249
3546
  );
3250
3547
  const dataSources = [];
3251
3548
  do {
3252
- const url = unwrap4(
3253
- await p2.text({
3549
+ const url = unwrap(
3550
+ await p3.text({
3254
3551
  message: `Data source #${dataSources.length + 1} \u2014 URL?`,
3255
3552
  placeholder: "https://example.gov/meetings",
3256
3553
  validate: (v) => /^https?:\/\//.test(v ?? "") ? void 0 : "Must start with http(s)://"
3257
3554
  })
3258
3555
  );
3259
- const dataType = unwrap4(
3260
- await p2.select({
3556
+ const dataType = unwrap(
3557
+ await p3.select({
3261
3558
  message: "Data type?",
3262
3559
  options: DATA_TYPES.map((t) => ({ value: t, label: t }))
3263
3560
  })
3264
3561
  );
3265
- const sourceType = unwrap4(
3266
- await p2.select({
3562
+ const sourceType = unwrap(
3563
+ await p3.select({
3267
3564
  message: "Source type?",
3268
3565
  initialValue: "html_scrape",
3269
3566
  options: SOURCE_TYPES.map((t) => ({ value: t, label: t }))
3270
3567
  })
3271
3568
  );
3272
- const contentGoal = unwrap4(
3273
- await p2.text({
3569
+ const contentGoal = unwrap(
3570
+ await p3.text({
3274
3571
  message: "Content goal (what should the scraper extract)?",
3275
3572
  placeholder: "Fetch upcoming board meeting agendas and minutes",
3276
3573
  validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
3277
3574
  })
3278
3575
  );
3279
- const category = unwrap4(
3280
- await p2.text({
3576
+ const category = unwrap(
3577
+ await p3.text({
3281
3578
  message: "Category label (optional)?",
3282
3579
  placeholder: "Board of Supervisors"
3283
3580
  })
@@ -3285,8 +3582,8 @@ var regionCommand = new Command("region").description(
3285
3582
  const src = { url, dataType, sourceType, contentGoal };
3286
3583
  if (category && category.trim().length > 0) src.category = category.trim();
3287
3584
  dataSources.push(src);
3288
- const again = unwrap4(
3289
- await p2.confirm({ message: "Add another data source?", initialValue: false })
3585
+ const again = unwrap(
3586
+ await p3.confirm({ message: "Add another data source?", initialValue: false })
3290
3587
  );
3291
3588
  if (!again) break;
3292
3589
  } while (true);
@@ -3309,8 +3606,8 @@ var regionCommand = new Command("region").description(
3309
3606
  const file = buildRegionConfig(input);
3310
3607
  const issues = validateRegionConfig(file);
3311
3608
  if (issues.length > 0) {
3312
- p2.note(issues.map((i) => `${pc2.red("\u2022")} ${i}`).join("\n"), "Validation failed");
3313
- p2.cancel("The generated config would not pass the regions repo CI. Re-run and adjust.");
3609
+ p3.note(issues.map((i) => `${pc2.red("\u2022")} ${i}`).join("\n"), "Validation failed");
3610
+ p3.cancel("The generated config would not pass the regions repo CI. Re-run and adjust.");
3314
3611
  process.exit(1);
3315
3612
  }
3316
3613
  const relPath = regionFilePath({
@@ -3322,21 +3619,21 @@ var regionCommand = new Command("region").description(
3322
3619
  const absPath = resolve(targetDir, relPath);
3323
3620
  const json = `${JSON.stringify(file, null, 2)}
3324
3621
  `;
3325
- p2.note(json, `${relPath} (preview)`);
3326
- if (!opts.force && await fileExists2(absPath)) {
3327
- p2.cancel(`${relPath} already exists. Re-run with --force to overwrite.`);
3622
+ p3.note(json, `${relPath} (preview)`);
3623
+ if (!opts.force && await fileExists3(absPath)) {
3624
+ p3.cancel(`${relPath} already exists. Re-run with --force to overwrite.`);
3328
3625
  process.exit(1);
3329
3626
  }
3330
- const write = unwrap4(
3331
- await p2.confirm({ message: `Write ${relPath}?`, initialValue: true })
3627
+ const write = unwrap(
3628
+ await p3.confirm({ message: `Write ${relPath}?`, initialValue: true })
3332
3629
  );
3333
3630
  if (!write) {
3334
- p2.cancel("Nothing written.");
3631
+ p3.cancel("Nothing written.");
3335
3632
  process.exit(0);
3336
3633
  }
3337
3634
  await mkdir(dirname(absPath), { recursive: true });
3338
3635
  await writeFile(absPath, json, "utf8");
3339
- p2.note(
3636
+ p3.note(
3340
3637
  [
3341
3638
  `${pc2.green("\u2713")} Wrote ${pc2.cyan(relPath)}`,
3342
3639
  "",
@@ -3347,9 +3644,9 @@ var regionCommand = new Command("region").description(
3347
3644
  ].join("\n"),
3348
3645
  "Done"
3349
3646
  );
3350
- p2.outro(pc2.magenta(`Region scaffolded: ${regionId}`));
3647
+ p3.outro(pc2.magenta(`Region scaffolded: ${regionId}`));
3351
3648
  });
3352
- async function fileExists2(path) {
3649
+ async function fileExists3(path) {
3353
3650
  try {
3354
3651
  await access(path);
3355
3652
  return true;
@@ -3359,20 +3656,21 @@ async function fileExists2(path) {
3359
3656
  }
3360
3657
  async function looksLikeRegionsRepo(dir) {
3361
3658
  const [hasSchema, hasRegions] = await Promise.all([
3362
- fileExists2(join(dir, "schema", "region-plugin.schema.json")),
3363
- fileExists2(join(dir, "regions"))
3659
+ fileExists3(join(dir, "schema", "region-plugin.schema.json")),
3660
+ fileExists3(join(dir, "regions"))
3364
3661
  ]);
3365
3662
  return hasSchema && hasRegions;
3366
3663
  }
3367
3664
 
3368
3665
  // src/cli.ts
3369
- var VERSION = "0.1.0";
3666
+ var VERSION = "0.3.0";
3370
3667
  var program = new Command();
3371
3668
  program.name("create-op-node").description(
3372
3669
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
3373
3670
  ).version(VERSION, "-v, --version", "show version");
3374
3671
  program.addCommand(initCommand);
3375
3672
  program.addCommand(bootstrapCommand);
3673
+ program.addCommand(resetCommand);
3376
3674
  program.addCommand(verifyCommand);
3377
3675
  program.addCommand(regionCommand);
3378
3676
  if (process.argv.length === 2) {