create-op-node 0.1.0 → 0.2.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';
@@ -304,6 +304,13 @@ async function readSecretFromOp(input) {
304
304
  const value = res.stdout.trim();
305
305
  return value.length > 0 ? value : null;
306
306
  }
307
+ function unwrap(value) {
308
+ if (p3.isCancel(value)) {
309
+ p3.cancel("Cancelled.");
310
+ process.exit(0);
311
+ }
312
+ return value;
313
+ }
307
314
  var _client = null;
308
315
  var _clientToken = null;
309
316
  function client(token) {
@@ -513,13 +520,6 @@ async function waitForApply(input, budgets = DEFAULT_BUDGETS, deps = realDeps) {
513
520
  }
514
521
 
515
522
  // 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
523
  async function ghTokenFromCli() {
524
524
  const r = await safeExeca("gh", ["auth", "token"]);
525
525
  if (r === null || r.exitCode !== 0) return null;
@@ -543,8 +543,8 @@ var initCommand = new Command("init").description(
543
543
  "Continue using a previously-created node repo (don't fail on 'already exists')"
544
544
  ).default(false)
545
545
  ).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(
546
+ p3.intro(pc2.bgCyan(pc2.black(" create-op-node init ")));
547
+ p3.note(
548
548
  [
549
549
  "This walks you from a sealed Mac Studio + a Cloudflare account to a",
550
550
  "live federation node serving traffic at api.<your-domain>.",
@@ -555,14 +555,14 @@ var initCommand = new Command("init").description(
555
555
  "Welcome"
556
556
  );
557
557
  const region = opts.region ? opts.region : unwrap(
558
- await p2.text({
558
+ await p3.text({
559
559
  message: "Short region label (used as a prefix in 1Password + R2)?",
560
560
  placeholder: "us-ca",
561
561
  validate: (v) => /^[a-z0-9-]{2,32}$/.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
562
562
  })
563
563
  );
564
564
  const domain = opts.domain ? opts.domain : unwrap(
565
- await p2.text({
565
+ await p3.text({
566
566
  message: "Domain registered in Cloudflare?",
567
567
  placeholder: "example.org",
568
568
  validate: (v) => !v ? "Required" : v.includes(".") ? void 0 : "Missing a TLD?"
@@ -578,7 +578,7 @@ var initCommand = new Command("init").description(
578
578
  );
579
579
  const cfAccount = await collectId(opts.cfAccount, "Cloudflare account ID");
580
580
  const cfZone = await collectId(opts.cfZone, `Cloudflare zone ID for ${domain}`);
581
- const cfSpin = p2.spinner();
581
+ const cfSpin = p3.spinner();
582
582
  cfSpin.start("Verifying Cloudflare token + 5 scopes\u2026");
583
583
  const cfProbe = await probeCloudflareToken({
584
584
  token: cfToken,
@@ -588,7 +588,7 @@ var initCommand = new Command("init").description(
588
588
  if (!cfProbe.ok) {
589
589
  cfSpin.stop(pc2.red("\u2717 Cloudflare token check failed."));
590
590
  for (const issue of cfProbe.issues) console.error(` - ${issue}`);
591
- p2.cancel("Fix the token in the Cloudflare dashboard, then re-run.");
591
+ p3.cancel("Fix the token in the Cloudflare dashboard, then re-run.");
592
592
  process.exit(1);
593
593
  }
594
594
  cfSpin.stop(pc2.green("\u2713 Cloudflare token valid, 5 scopes present."));
@@ -598,18 +598,18 @@ var initCommand = new Command("init").description(
598
598
  "tfc"
599
599
  );
600
600
  const tfOrg = opts.tfOrg ? opts.tfOrg : unwrap(
601
- await p2.text({
601
+ await p3.text({
602
602
  message: "Terraform Cloud organization name?",
603
603
  placeholder: "op-region-ca"
604
604
  })
605
605
  );
606
- const tfSpin = p2.spinner();
606
+ const tfSpin = p3.spinner();
607
607
  tfSpin.start("Verifying Terraform Cloud token + organization\u2026");
608
608
  const tfProbe = await probeTfcToken({ token: tfToken, organization: tfOrg });
609
609
  if (!tfProbe.ok) {
610
610
  tfSpin.stop(pc2.red("\u2717 Terraform Cloud check failed."));
611
611
  for (const issue of tfProbe.issues) console.error(` - ${issue}`);
612
- p2.cancel("Fix the token / org, then re-run.");
612
+ p3.cancel("Fix the token / org, then re-run.");
613
613
  process.exit(1);
614
614
  }
615
615
  tfSpin.stop(
@@ -622,7 +622,7 @@ var initCommand = new Command("init").description(
622
622
  const fromCli = await ghTokenFromCli();
623
623
  if (fromCli) {
624
624
  const useCli = unwrap(
625
- await p2.confirm({
625
+ await p3.confirm({
626
626
  message: "Found a signed-in `gh` CLI \u2014 use its token?",
627
627
  initialValue: true
628
628
  })
@@ -639,8 +639,8 @@ var initCommand = new Command("init").description(
639
639
  }
640
640
  const op = await detectOp();
641
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(
642
+ p3.note(opNote, "1Password");
643
+ p3.note(
644
644
  [
645
645
  `Region label: ${pc2.cyan(region)}`,
646
646
  `Domain: ${pc2.cyan(domain)}`,
@@ -656,14 +656,14 @@ var initCommand = new Command("init").description(
656
656
  );
657
657
  if (!opts.yes) {
658
658
  const go = unwrap(
659
- await p2.confirm({ message: "Proceed?", initialValue: true })
659
+ await p3.confirm({ message: "Proceed?", initialValue: true })
660
660
  );
661
661
  if (!go) {
662
- p2.cancel("Cancelled.");
662
+ p3.cancel("Cancelled.");
663
663
  process.exit(0);
664
664
  }
665
665
  }
666
- const repoSpin = p2.spinner();
666
+ const repoSpin = p3.spinner();
667
667
  repoSpin.start(`Creating ${newRepoFull} from ${opts.template}\u2026`);
668
668
  let created;
669
669
  try {
@@ -680,18 +680,18 @@ var initCommand = new Command("init").description(
680
680
  const message = err.message ?? "unknown error";
681
681
  if (status !== 422) {
682
682
  repoSpin.stop(pc2.red(`\u2717 Couldn't create repo: ${message}`));
683
- p2.cancel("Fix the issue and re-run.");
683
+ p3.cancel("Fix the issue and re-run.");
684
684
  process.exit(1);
685
685
  }
686
686
  repoSpin.stop(pc2.yellow(`\u26A0 ${newRepoFull} already exists.`));
687
687
  const reuse = opts.useExistingRepo || unwrap(
688
- await p2.confirm({
688
+ await p3.confirm({
689
689
  message: `Continue with the existing ${newRepoFull}? (Re-seeds secrets + re-commits prod.tfvars on a fresh branch.)`,
690
690
  initialValue: true
691
691
  })
692
692
  );
693
693
  if (!reuse) {
694
- p2.cancel(
694
+ p3.cancel(
695
695
  `Delete ${newRepoFull} on GitHub and re-run, or pass --use-existing-repo to skip this prompt.`
696
696
  );
697
697
  process.exit(0);
@@ -709,7 +709,7 @@ var initCommand = new Command("init").description(
709
709
  { name: "TF_API_TOKEN", value: tfToken },
710
710
  { name: "TF_CLOUD_ORGANIZATION", value: tfOrg }
711
711
  ];
712
- const secSpin = p2.spinner();
712
+ const secSpin = p3.spinner();
713
713
  secSpin.start(`Seeding ${secrets.length} repo secrets\u2026`);
714
714
  const seeded = [];
715
715
  for (const s of secrets) {
@@ -717,7 +717,7 @@ var initCommand = new Command("init").description(
717
717
  if (!r.written) {
718
718
  secSpin.stop(pc2.red(`\u2717 Failed to set ${s.name}: ${r.reason ?? "unknown"}`));
719
719
  const remaining = secrets.slice(secrets.findIndex((x) => x.name === s.name)).map((x) => x.name);
720
- p2.cancel(
720
+ p3.cancel(
721
721
  [
722
722
  seeded.length > 0 ? `Already seeded on ${newRepoFull}: ${seeded.join(", ")}.` : `Nothing seeded yet on ${newRepoFull}.`,
723
723
  `Still pending: ${remaining.join(", ")}.`,
@@ -732,7 +732,7 @@ var initCommand = new Command("init").description(
732
732
  }
733
733
  secSpin.stop(pc2.green(`\u2713 Seeded ${secrets.length} secrets on ${newRepoFull}`));
734
734
  const branch = `init/region-${region}-${isoStampUtc(/* @__PURE__ */ new Date())}`;
735
- const setupSpin = p2.spinner();
735
+ const setupSpin = p3.spinner();
736
736
  setupSpin.start(`Writing prod.tfvars on branch ${branch}\u2026`);
737
737
  try {
738
738
  await createBranch({
@@ -755,11 +755,11 @@ var initCommand = new Command("init").description(
755
755
  });
756
756
  } catch (err) {
757
757
  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.");
758
+ p3.cancel("The repo exists but the branch / commit failed. Open it on GitHub and inspect.");
759
759
  process.exit(1);
760
760
  }
761
761
  setupSpin.stop(pc2.green("\u2713 Wrote prod.tfvars"));
762
- const prSpin = p2.spinner();
762
+ const prSpin = p3.spinner();
763
763
  prSpin.start("Opening pull request\u2026");
764
764
  let pr;
765
765
  try {
@@ -782,7 +782,7 @@ var initCommand = new Command("init").description(
782
782
  });
783
783
  } catch (err) {
784
784
  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.");
785
+ p3.cancel("Branch + tfvars are committed; open the PR by hand on GitHub.");
786
786
  process.exit(1);
787
787
  }
788
788
  prSpin.stop(pc2.green(`\u2713 Opened PR #${pr.number}`));
@@ -791,7 +791,7 @@ var initCommand = new Command("init").description(
791
791
  if (op.installed && op.signedIn) {
792
792
  const existing = opts.overwrite ? null : await readSecretFromOp({ title: keyTitle, ...vaultArg });
793
793
  if (existing && /^[a-f0-9]{64}$/.test(existing)) {
794
- p2.note(
794
+ p3.note(
795
795
  `${pc2.green("\u2713")} Re-using existing pgsodium master key from 1Password (${pc2.cyan(keyTitle)}). Pass --overwrite to rotate.`,
796
796
  "Secret"
797
797
  );
@@ -804,12 +804,12 @@ var initCommand = new Command("init").description(
804
804
  ...vaultArg
805
805
  });
806
806
  if (r.written) {
807
- p2.note(
807
+ p3.note(
808
808
  `${pc2.green("\u2713")} pgsodium master key${r.alreadyExisted ? " rotated" : " saved"} to 1Password as ${pc2.cyan(keyTitle)}.`,
809
809
  "Secret"
810
810
  );
811
811
  } else if (r.alreadyExisted) {
812
- p2.note(
812
+ p3.note(
813
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.`,
814
814
  "Secret"
815
815
  );
@@ -822,14 +822,14 @@ var initCommand = new Command("init").description(
822
822
  await printKeyForManualSave(fresh, keyTitle);
823
823
  }
824
824
  if (opts.skipWait) {
825
- p2.outro(
825
+ p3.outro(
826
826
  pc2.cyan(
827
827
  `Review + merge ${pr.htmlUrl} when ready. Re-run with no --skip-wait or run \`npx create-op-node verify\` afterwards.`
828
828
  )
829
829
  );
830
830
  return;
831
831
  }
832
- p2.note(
832
+ p3.note(
833
833
  [
834
834
  `Open the PR: ${pc2.cyan(pr.htmlUrl)}`,
835
835
  "",
@@ -839,10 +839,10 @@ var initCommand = new Command("init").description(
839
839
  "Next step: review + merge"
840
840
  );
841
841
  const merged = unwrap(
842
- await p2.confirm({ message: "PR merged?", initialValue: false })
842
+ await p3.confirm({ message: "PR merged?", initialValue: false })
843
843
  );
844
844
  if (!merged) {
845
- p2.outro(
845
+ p3.outro(
846
846
  pc2.cyan(`No worries \u2014 re-run \`create-op-node init --skip-wait\` later to skip this step, or finish manually.`)
847
847
  );
848
848
  return;
@@ -853,7 +853,7 @@ var initCommand = new Command("init").description(
853
853
  tags: ["opuspopuli", "cloudflare"]
854
854
  });
855
855
  if (!ws) {
856
- p2.cancel(
856
+ p3.cancel(
857
857
  `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
858
  );
859
859
  process.exit(1);
@@ -865,7 +865,7 @@ var initCommand = new Command("init").description(
865
865
  runId: ws.currentRunId
866
866
  });
867
867
  if (!tunnelToken) {
868
- p2.cancel(
868
+ p3.cancel(
869
869
  `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
870
  );
871
871
  process.exit(1);
@@ -879,7 +879,7 @@ var initCommand = new Command("init").description(
879
879
  overwrite: true
880
880
  });
881
881
  if (r.written) {
882
- p2.note(
882
+ p3.note(
883
883
  `${pc2.green("\u2713")} Tunnel token saved to 1Password as ${pc2.cyan(tunnelTitle)}.`,
884
884
  "Secret"
885
885
  );
@@ -889,7 +889,7 @@ var initCommand = new Command("init").description(
889
889
  } else {
890
890
  await printKeyForManualSave(tunnelToken, tunnelTitle);
891
891
  }
892
- p2.outro(
892
+ p3.outro(
893
893
  pc2.cyan(
894
894
  `Region ${region} is provisioned. Next: \`npx create-op-node bootstrap\` on the Mac Studio.`
895
895
  )
@@ -905,7 +905,7 @@ async function collectSecret(preset, message, kind = "generic") {
905
905
  if (preset) return preset;
906
906
  const min = MIN_TOKEN_LENGTH[kind];
907
907
  const v = unwrap(
908
- await p2.password({
908
+ await p3.password({
909
909
  message,
910
910
  validate: (v2) => v2 && v2.length >= min ? void 0 : `That looks too short \u2014 expected at least ${min} characters for a ${kind} token`
911
911
  })
@@ -915,7 +915,7 @@ async function collectSecret(preset, message, kind = "generic") {
915
915
  async function collectId(preset, message) {
916
916
  if (preset) return preset;
917
917
  const v = unwrap(
918
- await p2.text({
918
+ await p3.text({
919
919
  message,
920
920
  placeholder: "0000000000000000000000000000000",
921
921
  validate: (v2) => /^[a-f0-9]{32}$/.test(v2 ?? "") ? void 0 : "Expected 32 hex characters"
@@ -924,7 +924,7 @@ async function collectId(preset, message) {
924
924
  return v;
925
925
  }
926
926
  async function printKeyForManualSave(value, title, reason) {
927
- p2.note(
927
+ p3.note(
928
928
  [
929
929
  reason ? `${pc2.red("\u2717")} ${reason}` : "",
930
930
  `Save this value to 1Password as ${pc2.cyan(title)}:`,
@@ -936,7 +936,7 @@ async function printKeyForManualSave(value, title, reason) {
936
936
  "Manual save"
937
937
  );
938
938
  unwrap(
939
- await p2.confirm({
939
+ await p3.confirm({
940
940
  message: "Value stashed in 1Password?",
941
941
  initialValue: true
942
942
  })
@@ -947,7 +947,7 @@ function isoStampUtc(d) {
947
947
  return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
948
948
  }
949
949
  async function waitForApplyAndFetchTunnelToken(input) {
950
- const spin = p2.spinner();
950
+ const spin = p3.spinner();
951
951
  spin.start("Waiting for terraform apply to finish (polling every 10s)\u2026");
952
952
  const outcome = await waitForApply({
953
953
  token: input.token,
@@ -1204,6 +1204,30 @@ async function setupLaunchAgent(input) {
1204
1204
  }
1205
1205
  return { ok: true, paths };
1206
1206
  }
1207
+ async function teardownLaunchAgent(paths, opts = {}) {
1208
+ const steps = [];
1209
+ const unload = await safeExeca("launchctl", ["unload", paths.plistFile]);
1210
+ steps.push({
1211
+ step: "unload",
1212
+ ok: true,
1213
+ ...unload === null ? { reason: "`launchctl` not on PATH" } : {}
1214
+ });
1215
+ try {
1216
+ await rm(paths.plistFile, { force: true });
1217
+ steps.push({ step: "rm-plist", ok: true });
1218
+ } catch (err) {
1219
+ steps.push({ step: "rm-plist", ok: false, reason: err.message });
1220
+ }
1221
+ if (!opts.keepKeyFile) {
1222
+ try {
1223
+ await rm(paths.keyFile, { force: true });
1224
+ steps.push({ step: "rm-key-file", ok: true });
1225
+ } catch (err) {
1226
+ steps.push({ step: "rm-key-file", ok: false, reason: err.message });
1227
+ }
1228
+ }
1229
+ return { ok: steps.every((s) => s.ok), steps };
1230
+ }
1207
1231
 
1208
1232
  // src/lib/docker.ts
1209
1233
  var GHCR_REGISTRY = "ghcr.io";
@@ -1258,6 +1282,17 @@ async function composeUp(opts) {
1258
1282
  const res = await safeExeca("docker", composeArgs(opts, ["up", "-d", "--remove-orphans"]));
1259
1283
  return result(res, "compose up");
1260
1284
  }
1285
+ async function composeDown(opts) {
1286
+ const flags = ["down"];
1287
+ if (opts.wipeVolumes) flags.push("-v");
1288
+ if (opts.removeOrphans) flags.push("--remove-orphans");
1289
+ const res = await safeExeca("docker", composeArgs(opts, flags));
1290
+ return result(res, opts.wipeVolumes ? "compose down -v" : "compose down");
1291
+ }
1292
+ async function dockerLogout(registry = GHCR_REGISTRY) {
1293
+ const res = await safeExeca("docker", ["logout", registry]);
1294
+ return result(res, `docker logout ${registry}`);
1295
+ }
1261
1296
  function result(res, label) {
1262
1297
  if (res === null) return { ok: false, reason: "`docker` not installed" };
1263
1298
  if (res.exitCode !== 0) {
@@ -1412,10 +1447,10 @@ async function warmModel(name, url = OLLAMA_URL) {
1412
1447
  body: JSON.stringify({ model: name, prompt: "hi", stream: false })
1413
1448
  });
1414
1449
  if (!res.ok) {
1415
- const text5 = await res.text().catch(() => "");
1450
+ const text6 = await res.text().catch(() => "");
1416
1451
  return {
1417
1452
  ok: false,
1418
- reason: `warm ${name} returned HTTP ${res.status}: ${text5 || "(no body)"}`
1453
+ reason: `warm ${name} returned HTTP ${res.status}: ${text6 || "(no body)"}`
1419
1454
  };
1420
1455
  }
1421
1456
  await res.text();
@@ -1526,13 +1561,6 @@ async function locateOrCloneRepo(input) {
1526
1561
  }
1527
1562
 
1528
1563
  // 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
1564
  var bootstrapCommand = new Command("bootstrap").description(
1537
1565
  "Configure the Mac Studio and bring the stack up. Run this on the Studio itself, after `init` has finished on your laptop."
1538
1566
  ).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(
@@ -1546,9 +1574,9 @@ var bootstrapCommand = new Command("bootstrap").description(
1546
1574
  "Skip the LaunchAgent setup (assumes one is already in place)"
1547
1575
  ).default(false)
1548
1576
  ).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({
1577
+ p3.intro(pc2.bgCyan(pc2.black(" create-op-node bootstrap ")));
1578
+ const region = opts.region ? opts.region : unwrap(
1579
+ await p3.text({
1552
1580
  message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
1553
1581
  placeholder: "us-ca",
1554
1582
  validate: (v) => /^[a-z0-9-]{2,32}$/.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
@@ -1556,17 +1584,17 @@ var bootstrapCommand = new Command("bootstrap").description(
1556
1584
  );
1557
1585
  const owner = opts.owner ?? "OpusPopuli";
1558
1586
  const repoName = `opuspopuli-node-${region}`;
1559
- const sysSpin = p2.spinner();
1587
+ const sysSpin = p3.spinner();
1560
1588
  sysSpin.start("Inspecting macOS\u2026");
1561
1589
  const snap = await inspectSystem();
1562
1590
  sysSpin.stop(pc2.green("\u2713 macOS inspected."));
1563
1591
  if (!snap.isAppleSilicon) {
1564
- p2.cancel(
1592
+ p3.cancel(
1565
1593
  `Bootstrap requires an Apple Silicon Mac (the runbook targets M-series). Detected: ${snap.osVersion ?? "unknown"} (uname -m != arm64).`
1566
1594
  );
1567
1595
  process.exit(1);
1568
1596
  }
1569
- p2.note(
1597
+ p3.note(
1570
1598
  [
1571
1599
  `Hostname: ${pc2.cyan(snap.hostname)}`,
1572
1600
  `macOS: ${pc2.cyan(snap.osVersion ?? "(sw_vers failed)")}`,
@@ -1577,33 +1605,33 @@ var bootstrapCommand = new Command("bootstrap").description(
1577
1605
  "System snapshot"
1578
1606
  );
1579
1607
  if (!snap.autoRestartOnPowerFailure) {
1580
- const fix = unwrap2(
1581
- await p2.confirm({
1608
+ const fix = unwrap(
1609
+ await p3.confirm({
1582
1610
  message: "Auto-restart-on-power-failure is OFF. Enable now (requires sudo)?",
1583
1611
  initialValue: true
1584
1612
  })
1585
1613
  );
1586
1614
  if (fix) {
1587
1615
  const r = await enableAutoRestartOnPowerFailure();
1588
- if (!r.ok) p2.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
1616
+ if (!r.ok) p3.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
1589
1617
  }
1590
1618
  }
1591
1619
  if (!snap.diskSleepDisabled) {
1592
- const fix = unwrap2(
1593
- await p2.confirm({
1620
+ const fix = unwrap(
1621
+ await p3.confirm({
1594
1622
  message: "Disk sleep is enabled. Disable now (requires sudo)?",
1595
1623
  initialValue: true
1596
1624
  })
1597
1625
  );
1598
1626
  if (fix) {
1599
1627
  const r = await disableDiskSleep();
1600
- if (!r.ok) p2.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
1628
+ if (!r.ok) p3.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
1601
1629
  }
1602
1630
  }
1603
1631
  if (!opts.skipBrew) {
1604
1632
  const brewInfo = await detectBrew();
1605
1633
  if (!brewInfo.installed) {
1606
- p2.note(
1634
+ p3.note(
1607
1635
  [
1608
1636
  `Homebrew is not installed. Open another shell and run:`,
1609
1637
  "",
@@ -1613,9 +1641,9 @@ var bootstrapCommand = new Command("bootstrap").description(
1613
1641
  ].join("\n"),
1614
1642
  "Manual step"
1615
1643
  );
1616
- unwrap2(await p2.confirm({ message: "Homebrew installed?", initialValue: true }));
1644
+ unwrap(await p3.confirm({ message: "Homebrew installed?", initialValue: true }));
1617
1645
  }
1618
- const brewSpin = p2.spinner();
1646
+ const brewSpin = p3.spinner();
1619
1647
  brewSpin.start("Installing Studio packages\u2026");
1620
1648
  const report = await installPackages(STUDIO_PACKAGES, (pkg, status) => {
1621
1649
  brewSpin.message(`${status}: ${pkg.name}`);
@@ -1628,7 +1656,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1628
1656
  )
1629
1657
  );
1630
1658
  if (report.failed.length > 0) {
1631
- p2.note(
1659
+ p3.note(
1632
1660
  [
1633
1661
  report.failed.map((f) => `${pc2.red("\u2022")} ${f.pkg.name}: ${f.reason}`).join("\n"),
1634
1662
  "",
@@ -1637,8 +1665,8 @@ var bootstrapCommand = new Command("bootstrap").description(
1637
1665
  ].join("\n"),
1638
1666
  "Brew failures"
1639
1667
  );
1640
- const cont = unwrap2(
1641
- await p2.confirm({
1668
+ const cont = unwrap(
1669
+ await p3.confirm({
1642
1670
  message: "Continue anyway? (Default no \u2014 recommended to fix and re-run.)",
1643
1671
  initialValue: false
1644
1672
  })
@@ -1648,7 +1676,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1648
1676
  }
1649
1677
  await ensureGhAuth();
1650
1678
  await promptTailscaleSignin();
1651
- const repoSpin = p2.spinner();
1679
+ const repoSpin = p3.spinner();
1652
1680
  repoSpin.start("Locating your region node repo\u2026");
1653
1681
  const located = await locateOrCloneRepo({
1654
1682
  owner,
@@ -1667,7 +1695,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1667
1695
  );
1668
1696
  const op = await detectOp();
1669
1697
  if (!op.installed || !op.signedIn) {
1670
- p2.cancel(
1698
+ p3.cancel(
1671
1699
  "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
1700
  );
1673
1701
  process.exit(1);
@@ -1675,7 +1703,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1675
1703
  const keyTitle = `opuspopuli-${region}-pgsodium-root-key`;
1676
1704
  const tunnelTitle = `opuspopuli-${region}-tunnel-token`;
1677
1705
  const vaultArg = opts.vault ? { vault: opts.vault } : {};
1678
- const opSpin = p2.spinner();
1706
+ const opSpin = p3.spinner();
1679
1707
  opSpin.start("Reading pgsodium key + Tunnel token from 1Password\u2026");
1680
1708
  const pgsodiumKey = await readSecretFromOp({ title: keyTitle, ...vaultArg });
1681
1709
  const tunnelToken = await readSecretFromOp({ title: tunnelTitle, ...vaultArg });
@@ -1685,39 +1713,39 @@ var bootstrapCommand = new Command("bootstrap").description(
1685
1713
  pgsodiumKey ? null : keyTitle,
1686
1714
  tunnelToken ? null : tunnelTitle
1687
1715
  ].filter(Boolean);
1688
- p2.cancel(
1716
+ p3.cancel(
1689
1717
  `Missing in 1Password (vault: ${opts.vault ?? "Private"}): ${missing.join(", ")}. Run \`create-op-node init\` first, or pass --vault if your items live elsewhere.`
1690
1718
  );
1691
1719
  process.exit(1);
1692
1720
  }
1693
1721
  if (!PGSODIUM_KEY_RE.test(pgsodiumKey)) {
1694
1722
  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.");
1723
+ p3.cancel("Inspect the item in 1Password; it should be 64 lowercase hex characters.");
1696
1724
  process.exit(1);
1697
1725
  }
1698
1726
  opSpin.stop(pc2.green("\u2713 Secrets read from 1Password."));
1699
1727
  if (!opts.skipLaunchAgent) {
1700
- const laSpin = p2.spinner();
1728
+ const laSpin = p3.spinner();
1701
1729
  laSpin.start("Writing pgsodium key file + LaunchAgent plist\u2026");
1702
1730
  const la = await setupLaunchAgent({ pgsodiumKey, tunnelToken });
1703
1731
  if (!la.ok) {
1704
1732
  laSpin.stop(pc2.red(`\u2717 LaunchAgent step ${la.step} failed.`));
1705
- p2.cancel(la.reason ?? "LaunchAgent setup failed.");
1733
+ p3.cancel(la.reason ?? "LaunchAgent setup failed.");
1706
1734
  process.exit(1);
1707
1735
  }
1708
1736
  laSpin.stop(pc2.green(`\u2713 LaunchAgent loaded (${la.paths.plistFile}).`));
1709
1737
  }
1710
- const ghcrSpin = p2.spinner();
1738
+ const ghcrSpin = p3.spinner();
1711
1739
  ghcrSpin.start("Authenticating Docker to ghcr.io\u2026");
1712
1740
  const ghcr = await loginToGhcr();
1713
1741
  if (!ghcr.ok) {
1714
1742
  ghcrSpin.stop(pc2.red("\u2717 ghcr.io login failed."));
1715
- p2.cancel(ghcr.reason ?? "ghcr.io login failed.");
1743
+ p3.cancel(ghcr.reason ?? "ghcr.io login failed.");
1716
1744
  process.exit(1);
1717
1745
  }
1718
1746
  ghcrSpin.stop(pc2.green("\u2713 Logged in to ghcr.io."));
1719
1747
  if (!opts.skipOllama) {
1720
- const olSpin = p2.spinner();
1748
+ const olSpin = p3.spinner();
1721
1749
  olSpin.start("Pulling + warming Ollama models\u2026");
1722
1750
  let olHealth = await checkOllamaHealth();
1723
1751
  if (!olHealth.reachable) {
@@ -1725,7 +1753,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1725
1753
  const start = await startOllamaService();
1726
1754
  if (!start.ok) {
1727
1755
  olSpin.stop(pc2.red("\u2717 Couldn't start Ollama service."));
1728
- p2.cancel(
1756
+ p3.cancel(
1729
1757
  `${start.reason ?? "unknown"} \u2014 start it manually with \`brew services start ollama\` and re-run with --skip-brew --skip-launch-agent.`
1730
1758
  );
1731
1759
  process.exit(1);
@@ -1736,7 +1764,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1736
1764
  olSpin.stop(
1737
1765
  pc2.yellow("\u26A0 Ollama service started but daemon not yet answering on :11434.")
1738
1766
  );
1739
- p2.cancel(
1767
+ p3.cancel(
1740
1768
  "Give it another few seconds, then re-run with --skip-brew --skip-launch-agent."
1741
1769
  );
1742
1770
  process.exit(1);
@@ -1752,11 +1780,11 @@ var bootstrapCommand = new Command("bootstrap").description(
1752
1780
  );
1753
1781
  const probe = await probeHostDockerInternal();
1754
1782
  if (!probe.ok) {
1755
- p2.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
1783
+ p3.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
1756
1784
  }
1757
1785
  }
1758
1786
  if (opts.skipStack) {
1759
- p2.outro(
1787
+ p3.outro(
1760
1788
  pc2.cyan(
1761
1789
  `Stack-up skipped. Run \`docker compose -f ${(opts.composeFile ?? ["docker-compose-prod.yml"]).join(" -f ")} pull && up -d\` from ${repoPath} when ready.`
1762
1790
  )
@@ -1769,25 +1797,25 @@ var bootstrapCommand = new Command("bootstrap").description(
1769
1797
  cwd: repoPath,
1770
1798
  ...opts.envFile ? { envFile: opts.envFile } : {}
1771
1799
  };
1772
- const pullSpin = p2.spinner();
1800
+ const pullSpin = p3.spinner();
1773
1801
  pullSpin.start("Pulling images from ghcr.io\u2026");
1774
1802
  const pull = await composePull(composeOpts);
1775
1803
  if (!pull.ok) {
1776
1804
  pullSpin.stop(pc2.red("\u2717 compose pull failed."));
1777
- p2.cancel(pull.reason ?? "compose pull failed.");
1805
+ p3.cancel(pull.reason ?? "compose pull failed.");
1778
1806
  process.exit(1);
1779
1807
  }
1780
1808
  pullSpin.stop(pc2.green("\u2713 Images pulled."));
1781
- const upSpin = p2.spinner();
1809
+ const upSpin = p3.spinner();
1782
1810
  upSpin.start("Starting the stack\u2026");
1783
1811
  const up = await composeUp(composeOpts);
1784
1812
  if (!up.ok) {
1785
1813
  upSpin.stop(pc2.red("\u2717 compose up failed."));
1786
- p2.cancel(up.reason ?? "compose up failed.");
1814
+ p3.cancel(up.reason ?? "compose up failed.");
1787
1815
  process.exit(1);
1788
1816
  }
1789
1817
  upSpin.stop(pc2.green("\u2713 Containers started \u2014 waiting for healthy\u2026"));
1790
- const healthSpin = p2.spinner();
1818
+ const healthSpin = p3.spinner();
1791
1819
  healthSpin.start("Polling for healthy containers (5s, up to 5 minutes)\u2026");
1792
1820
  const outcome = await waitForHealthy(composeOpts, {
1793
1821
  onPoll: (snaps) => {
@@ -1799,7 +1827,7 @@ var bootstrapCommand = new Command("bootstrap").description(
1799
1827
  switch (outcome.kind) {
1800
1828
  case "healthy":
1801
1829
  healthSpin.stop(pc2.green(`\u2713 All ${outcome.snapshots.length} containers healthy.`));
1802
- p2.outro(
1830
+ p3.outro(
1803
1831
  pc2.cyan(
1804
1832
  `Region ${region} is up. Next: verify off-LAN with \`npx create-op-node verify --domain <your-domain>\`.`
1805
1833
  )
@@ -1807,22 +1835,22 @@ var bootstrapCommand = new Command("bootstrap").description(
1807
1835
  return;
1808
1836
  case "unhealthy":
1809
1837
  healthSpin.stop(pc2.red(`\u2717 ${outcome.problem}`));
1810
- p2.note(
1838
+ p3.note(
1811
1839
  outcome.snapshots.map((s) => ` ${kvHealth(s.health)} ${s.name} (${s.state})`).join("\n"),
1812
1840
  "Container state"
1813
1841
  );
1814
- p2.cancel(
1842
+ p3.cancel(
1815
1843
  `Inspect with \`docker compose -f ${composeFiles.join(" -f ")} logs\`. Fix and re-run.`
1816
1844
  );
1817
1845
  process.exit(1);
1818
1846
  // eslint-disable-next-line no-fallthrough
1819
1847
  case "timeout":
1820
1848
  healthSpin.stop(pc2.yellow("\u26A0 Timed out waiting for all containers to report healthy."));
1821
- p2.note(
1849
+ p3.note(
1822
1850
  outcome.snapshots.map((s) => ` ${kvHealth(s.health)} ${s.name} (${s.state})`).join("\n"),
1823
1851
  "Container state at timeout"
1824
1852
  );
1825
- p2.cancel(
1853
+ p3.cancel(
1826
1854
  `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
1855
  );
1828
1856
  process.exit(1);
@@ -1862,20 +1890,20 @@ function repoPathFromLocate(out) {
1862
1890
  function handleLocateError(out, owner, name) {
1863
1891
  switch (out.kind) {
1864
1892
  case "explicit-not-a-node-repo":
1865
- p2.cancel(
1893
+ p3.cancel(
1866
1894
  `${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
1895
  );
1868
1896
  return;
1869
1897
  case "gh-not-installed":
1870
- p2.cancel("`gh` not installed \u2014 install via `brew install gh` and re-run.");
1898
+ p3.cancel("`gh` not installed \u2014 install via `brew install gh` and re-run.");
1871
1899
  return;
1872
1900
  case "clone-failed":
1873
- p2.cancel(
1901
+ p3.cancel(
1874
1902
  `Couldn't clone ${owner}/${name}: ${out.reason}. Check repo exists + you have access.`
1875
1903
  );
1876
1904
  return;
1877
1905
  case "clone-disallowed":
1878
- p2.cancel(
1906
+ p3.cancel(
1879
1907
  "No checked-out node repo found and --no-clone (or similar) was set. Provide --repo-dir."
1880
1908
  );
1881
1909
  return;
@@ -1890,12 +1918,12 @@ function handleLocateError(out, owner, name) {
1890
1918
  async function ensureGhAuth() {
1891
1919
  const status = await safeExeca("gh", ["auth", "status"]);
1892
1920
  if (status === null) {
1893
- p2.cancel("`gh` not on PATH. Install via `brew install gh` and re-run.");
1921
+ p3.cancel("`gh` not on PATH. Install via `brew install gh` and re-run.");
1894
1922
  process.exit(1);
1895
1923
  }
1896
1924
  if (status.exitCode === 0) return;
1897
1925
  const detail = (status.stderr || status.stdout).trim();
1898
- p2.note(
1926
+ p3.note(
1899
1927
  [
1900
1928
  "`gh` is not signed in (or signed in to a different account / missing scope).",
1901
1929
  detail ? "" : void 0,
@@ -1909,12 +1937,12 @@ async function ensureGhAuth() {
1909
1937
  ].filter((l) => typeof l === "string").join("\n"),
1910
1938
  "Manual step"
1911
1939
  );
1912
- unwrap2(await p2.confirm({ message: "gh signed in?", initialValue: true }));
1940
+ unwrap(await p3.confirm({ message: "gh signed in?", initialValue: true }));
1913
1941
  }
1914
1942
  async function promptTailscaleSignin() {
1915
1943
  const status = await safeExeca("tailscale", ["status"]);
1916
1944
  if (status !== null && status.exitCode === 0) return;
1917
- p2.note(
1945
+ p3.note(
1918
1946
  [
1919
1947
  "Tailscale needs to be signed in (or you can skip \u2014 Tailscale is",
1920
1948
  "optional; it provides out-of-band SSH from your laptop. The rest",
@@ -1928,8 +1956,293 @@ async function promptTailscaleSignin() {
1928
1956
  ].join("\n"),
1929
1957
  "Manual step"
1930
1958
  );
1931
- unwrap2(await p2.confirm({ message: "Tailscale signed in (or skipped)?", initialValue: true }));
1959
+ unwrap(await p3.confirm({ message: "Tailscale signed in (or skipped)?", initialValue: true }));
1960
+ }
1961
+ var REGION_RE = /^[a-z0-9-]{2,32}$/;
1962
+ var RESET_PHASES = {
1963
+ STOP_STACK: "Stop stack",
1964
+ LAUNCH_AGENT: "LaunchAgent",
1965
+ DOCKER_LOGOUT: "docker logout"
1966
+ };
1967
+ var DEFAULT_DEPS = {
1968
+ composeDown,
1969
+ teardownLaunchAgent,
1970
+ dockerLogout
1971
+ };
1972
+ async function runReset(input, deps = DEFAULT_DEPS) {
1973
+ const phases = [];
1974
+ const push = (ph) => {
1975
+ phases.push(ph);
1976
+ deps.onPhase?.(ph);
1977
+ };
1978
+ if (!input.stack) {
1979
+ push({
1980
+ name: RESET_PHASES.STOP_STACK,
1981
+ status: "skipped",
1982
+ detail: "--skip-stack or no repo path resolved"
1983
+ });
1984
+ } else if (input.dryRun) {
1985
+ push({
1986
+ name: RESET_PHASES.STOP_STACK,
1987
+ status: "dry-run",
1988
+ detail: `would run: docker compose -f ${input.stack.composeFiles.join(" -f ")} down${input.stack.wipeVolumes ? " -v" : ""}${input.stack.removeOrphans ? " --remove-orphans" : ""}`
1989
+ });
1990
+ } else {
1991
+ const result2 = await deps.composeDown({
1992
+ files: input.stack.composeFiles,
1993
+ cwd: input.stack.repoPath,
1994
+ ...input.stack.envFile ? { envFile: input.stack.envFile } : {},
1995
+ wipeVolumes: input.stack.wipeVolumes,
1996
+ removeOrphans: input.stack.removeOrphans
1997
+ });
1998
+ if (result2.ok) {
1999
+ push({
2000
+ name: RESET_PHASES.STOP_STACK,
2001
+ status: "ok",
2002
+ detail: input.stack.wipeVolumes ? "volumes destroyed" : "containers stopped, volumes preserved"
2003
+ });
2004
+ } else {
2005
+ push({ name: RESET_PHASES.STOP_STACK, status: "fail", detail: result2.reason ?? "unknown failure" });
2006
+ }
2007
+ }
2008
+ if (!input.launchAgent) {
2009
+ push({ name: RESET_PHASES.LAUNCH_AGENT, status: "skipped", detail: "--skip-launch-agent or no plist found" });
2010
+ } else if (input.dryRun) {
2011
+ push({
2012
+ name: RESET_PHASES.LAUNCH_AGENT,
2013
+ status: "dry-run",
2014
+ detail: `would unload ${input.launchAgent.paths.plistFile}, rm plist${input.launchAgent.keepKeyFile ? "" : " + key file"}`
2015
+ });
2016
+ } else {
2017
+ const result2 = await deps.teardownLaunchAgent(
2018
+ input.launchAgent.paths,
2019
+ { keepKeyFile: input.launchAgent.keepKeyFile }
2020
+ );
2021
+ if (result2.ok) {
2022
+ const removed = result2.steps.filter((s) => s.step !== "unload").map((s) => s.step).join(", ");
2023
+ push({ name: RESET_PHASES.LAUNCH_AGENT, status: "ok", detail: `unloaded + ${removed}` });
2024
+ } else {
2025
+ const fail = result2.steps.find((s) => !s.ok);
2026
+ push({
2027
+ name: RESET_PHASES.LAUNCH_AGENT,
2028
+ status: "fail",
2029
+ detail: `${fail?.step}: ${fail?.reason ?? "unknown"}`
2030
+ });
2031
+ }
2032
+ }
2033
+ if (!input.dockerLogout) {
2034
+ push({ name: RESET_PHASES.DOCKER_LOGOUT, status: "skipped", detail: "--skip-docker-logout" });
2035
+ } else if (input.dryRun) {
2036
+ push({
2037
+ name: RESET_PHASES.DOCKER_LOGOUT,
2038
+ status: "dry-run",
2039
+ detail: `would run: docker logout ${input.dockerLogout.registry}`
2040
+ });
2041
+ } else {
2042
+ const result2 = await deps.dockerLogout(input.dockerLogout.registry);
2043
+ if (result2.ok) {
2044
+ push({
2045
+ name: RESET_PHASES.DOCKER_LOGOUT,
2046
+ status: "ok",
2047
+ detail: `credentials removed from ${input.dockerLogout.registry}`
2048
+ });
2049
+ } else {
2050
+ push({ name: RESET_PHASES.DOCKER_LOGOUT, status: "warn", detail: result2.reason ?? "unknown" });
2051
+ }
2052
+ }
2053
+ return { phases };
1932
2054
  }
2055
+ async function fileExists2(path) {
2056
+ try {
2057
+ await access(path);
2058
+ return true;
2059
+ } catch {
2060
+ return false;
2061
+ }
2062
+ }
2063
+ function phaseIcon(ph) {
2064
+ switch (ph.status) {
2065
+ case "ok":
2066
+ return pc2.green("\u2713");
2067
+ case "warn":
2068
+ return pc2.yellow("\u26A0");
2069
+ case "skipped":
2070
+ return pc2.dim("\xB7");
2071
+ case "dry-run":
2072
+ return pc2.cyan("?");
2073
+ case "fail":
2074
+ return pc2.red("\u2717");
2075
+ }
2076
+ }
2077
+ var resetCommand = new Command("reset").description(
2078
+ "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)."
2079
+ ).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(
2080
+ new Option(
2081
+ "--compose-file <path>",
2082
+ "Repeatable. Compose file relative to repo root. Default: docker-compose-prod.yml"
2083
+ ).default(["docker-compose-prod.yml"])
2084
+ ).addOption(new Option("--env-file <path>", "Compose --env-file. Default: .env.production")).addOption(
2085
+ new Option(
2086
+ "--wipe-data",
2087
+ "DESTROYS docker volumes (adds -v to `compose down`). Requires retyping the region label as confirmation. Default off \u2014 volumes preserved."
2088
+ ).default(false)
2089
+ ).addOption(
2090
+ new Option(
2091
+ "--no-remove-orphans",
2092
+ "Do NOT pass --remove-orphans to compose down. Preserves containers from compose files no longer included."
2093
+ )
2094
+ ).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(
2095
+ new Option(
2096
+ "--keep-key-file",
2097
+ "Preserve the pgsodium key file (only matters when LaunchAgent teardown runs). Useful as belt-and-suspenders backup before a wipe-data run."
2098
+ ).default(false)
2099
+ ).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) => {
2100
+ p3.intro(pc2.bgCyan(pc2.black(" create-op-node reset ")));
2101
+ const wipeData = opts.wipeData ?? false;
2102
+ const removeOrphans = opts.removeOrphans ?? true;
2103
+ const region = opts.region ? opts.region : unwrap(
2104
+ await p3.text({
2105
+ message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
2106
+ placeholder: "us-ca",
2107
+ validate: (v) => REGION_RE.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
2108
+ })
2109
+ );
2110
+ if (!REGION_RE.test(region)) {
2111
+ p3.cancel(`--region ${JSON.stringify(region)} is not a valid region slug.`);
2112
+ process.exit(2);
2113
+ }
2114
+ const owner = opts.owner ?? "OpusPopuli";
2115
+ const repoName = `opuspopuli-node-${region}`;
2116
+ const launchAgentPaths = defaultPaths();
2117
+ const plistExists = await fileExists2(launchAgentPaths.plistFile);
2118
+ const keyFileExists = await fileExists2(launchAgentPaths.keyFile);
2119
+ let repoPath;
2120
+ let stackSkipReason;
2121
+ if (!opts.skipStack) {
2122
+ const located = await locateOrCloneRepo({
2123
+ owner,
2124
+ name: repoName,
2125
+ cwd: process.cwd(),
2126
+ allowClone: false,
2127
+ ...opts.repoDir ? { explicit: opts.repoDir } : {}
2128
+ });
2129
+ switch (located.kind) {
2130
+ case "found":
2131
+ repoPath = located.path;
2132
+ break;
2133
+ case "explicit-not-a-node-repo":
2134
+ p3.cancel(
2135
+ `--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.`
2136
+ );
2137
+ process.exit(2);
2138
+ break;
2139
+ case "clone-disallowed":
2140
+ stackSkipReason = `no checkout found at ${process.cwd()}; pass --repo-dir to target one`;
2141
+ break;
2142
+ case "cloned":
2143
+ case "gh-not-installed":
2144
+ case "clone-failed":
2145
+ stackSkipReason = `unexpected outcome ${located.kind} from locateOrCloneRepo with allowClone=false`;
2146
+ break;
2147
+ }
2148
+ } else {
2149
+ stackSkipReason = "--skip-stack";
2150
+ }
2151
+ let runningContainers = null;
2152
+ if (repoPath) {
2153
+ runningContainers = await composePs({
2154
+ files: resolveComposeFiles(repoPath, opts.composeFile),
2155
+ ...opts.envFile ? { envFile: opts.envFile } : {}
2156
+ });
2157
+ }
2158
+ p3.note(
2159
+ [
2160
+ `Region: ${pc2.cyan(region)}`,
2161
+ `Repo path: ${repoPath ? pc2.cyan(repoPath) : pc2.dim(`not used (${stackSkipReason ?? "no repo path resolved"})`)}`,
2162
+ `Running containers: ${runningContainers === null ? pc2.dim("unknown") : runningContainers.length === 0 ? pc2.dim("none") : pc2.cyan(`${runningContainers.length} listed by compose ps`)}`,
2163
+ `LaunchAgent plist: ${plistExists ? pc2.cyan(launchAgentPaths.plistFile) : pc2.dim("not present")}`,
2164
+ `pgsodium key file: ${keyFileExists ? pc2.cyan(launchAgentPaths.keyFile) : pc2.dim("not present")}`,
2165
+ `Registry to log out: ${pc2.cyan(opts.registry ?? GHCR_REGISTRY)}`,
2166
+ `Volume policy: ${wipeData ? pc2.red("WIPE") : pc2.green("preserve (default)")}`,
2167
+ `Dry run: ${opts.dryRun ? pc2.yellow("yes") : pc2.dim("no")}`
2168
+ ].join("\n"),
2169
+ "Snapshot"
2170
+ );
2171
+ if (wipeData && !opts.dryRun) {
2172
+ unwrap(
2173
+ await p3.text({
2174
+ message: pc2.red(`Type the region label to confirm WIPING all volumes (the slug you'd use with --region):`),
2175
+ validate: (v) => v === region ? void 0 : `must match the region label exactly`
2176
+ })
2177
+ );
2178
+ } else if (!opts.dryRun && !opts.yes) {
2179
+ const cont = unwrap(
2180
+ await p3.confirm({
2181
+ message: "Proceed with reset? (volumes will be preserved)",
2182
+ initialValue: true
2183
+ })
2184
+ );
2185
+ if (!cont) {
2186
+ p3.cancel("Cancelled.");
2187
+ process.exit(0);
2188
+ }
2189
+ }
2190
+ const stack = repoPath ? {
2191
+ repoPath,
2192
+ composeFiles: resolveComposeFiles(repoPath, opts.composeFile),
2193
+ wipeVolumes: wipeData,
2194
+ removeOrphans,
2195
+ ...opts.envFile ? { envFile: opts.envFile } : {}
2196
+ } : void 0;
2197
+ const launchAgent = plistExists && !opts.skipLaunchAgent ? {
2198
+ paths: launchAgentPaths,
2199
+ keepKeyFile: opts.keepKeyFile ?? false
2200
+ } : void 0;
2201
+ const dockerLogoutInput = !opts.skipDockerLogout ? { registry: opts.registry ?? GHCR_REGISTRY } : void 0;
2202
+ const phaseSpins = /* @__PURE__ */ new Map();
2203
+ const actingPhases = [];
2204
+ if (stack && !opts.dryRun) actingPhases.push(RESET_PHASES.STOP_STACK);
2205
+ if (launchAgent && !opts.dryRun) actingPhases.push(RESET_PHASES.LAUNCH_AGENT);
2206
+ if (dockerLogoutInput && !opts.dryRun) actingPhases.push(RESET_PHASES.DOCKER_LOGOUT);
2207
+ for (const name of actingPhases) {
2208
+ const s = p3.spinner();
2209
+ s.start(`${name}\u2026`);
2210
+ phaseSpins.set(name, s);
2211
+ }
2212
+ const renderPhase = (ph) => {
2213
+ const spin = phaseSpins.get(ph.name);
2214
+ const line = `${phaseIcon(ph)} ${ph.name}: ${pc2.dim(ph.detail)}`;
2215
+ if (spin) {
2216
+ spin.stop(line);
2217
+ phaseSpins.delete(ph.name);
2218
+ } else {
2219
+ p3.log.info(line);
2220
+ }
2221
+ };
2222
+ const report = await runReset(
2223
+ {
2224
+ ...stack ? { stack } : {},
2225
+ ...launchAgent ? { launchAgent } : {},
2226
+ ...dockerLogoutInput ? { dockerLogout: dockerLogoutInput } : {},
2227
+ dryRun: opts.dryRun ?? false
2228
+ },
2229
+ { ...DEFAULT_DEPS, onPhase: renderPhase }
2230
+ );
2231
+ for (const [, spin] of phaseSpins) spin.stop(pc2.dim("\u2014 skipped"));
2232
+ const failed = report.phases.filter((ph) => ph.status === "fail").length;
2233
+ if (failed > 0) {
2234
+ p3.outro(pc2.red(`${failed} step${failed === 1 ? "" : "s"} failed.`));
2235
+ process.exit(1);
2236
+ } else if (opts.dryRun) {
2237
+ p3.outro(pc2.cyan("Dry run complete. Re-run without --dry-run to apply."));
2238
+ } else {
2239
+ p3.outro(
2240
+ pc2.green(
2241
+ 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.`
2242
+ )
2243
+ );
2244
+ }
2245
+ });
1933
2246
 
1934
2247
  // src/lib/cosign.ts
1935
2248
  var COSIGN_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
@@ -1965,8 +2278,8 @@ async function cosignVerifyImage(input) {
1965
2278
  // src/lib/http.ts
1966
2279
  var GRAPHQL_ACCEPT = "application/graphql-response+json, application/json";
1967
2280
  async function readBodyCapped(res) {
1968
- const text5 = await res.text();
1969
- return text5.slice(0, BODY_PREVIEW_MAX);
2281
+ const text6 = await res.text();
2282
+ return text6.slice(0, BODY_PREVIEW_MAX);
1970
2283
  }
1971
2284
  async function httpProbe(input) {
1972
2285
  const expected = input.expectedStatus ?? 200;
@@ -2004,22 +2317,22 @@ async function graphqlProbe(input) {
2004
2317
  body: JSON.stringify({ query: "{ __typename }" }),
2005
2318
  signal: ctrl.signal
2006
2319
  });
2007
- const text5 = await readBodyCapped(res);
2320
+ const text6 = await readBodyCapped(res);
2008
2321
  if (res.status !== 200) {
2009
2322
  return {
2010
2323
  ok: false,
2011
2324
  status: res.status,
2012
- reason: `expected HTTP 200, got ${res.status}: ${text5}`
2325
+ reason: `expected HTTP 200, got ${res.status}: ${text6}`
2013
2326
  };
2014
2327
  }
2015
2328
  let envelope;
2016
2329
  try {
2017
- envelope = JSON.parse(text5);
2330
+ envelope = JSON.parse(text6);
2018
2331
  } catch {
2019
2332
  return {
2020
2333
  ok: false,
2021
2334
  status: res.status,
2022
- reason: `response was not JSON: ${text5}`
2335
+ reason: `response was not JSON: ${text6}`
2023
2336
  };
2024
2337
  }
2025
2338
  const data = envelope.data;
@@ -2028,7 +2341,7 @@ async function graphqlProbe(input) {
2028
2341
  return {
2029
2342
  ok: false,
2030
2343
  status: res.status,
2031
- reason: `response missing { data: { __typename: string } }: ${text5}`
2344
+ reason: `response missing { data: { __typename: string } }: ${text6}`
2032
2345
  };
2033
2346
  }
2034
2347
  return { ok: true, typename };
@@ -2096,13 +2409,6 @@ function tlsHandshake(input) {
2096
2409
  }
2097
2410
 
2098
2411
  // 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
2412
  var DOMAIN_RE = /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i;
2107
2413
  function summarize(report) {
2108
2414
  let failed = 0;
@@ -2118,14 +2424,14 @@ function formatExpiry(days) {
2118
2424
  if (days === 0) return "expires today";
2119
2425
  return `${days}d to expiry`;
2120
2426
  }
2121
- var DEFAULT_DEPS = {
2427
+ var DEFAULT_DEPS2 = {
2122
2428
  tls: tlsHandshake,
2123
2429
  http: httpProbe,
2124
2430
  graphql: graphqlProbe,
2125
2431
  tunnel: tunnelStatus,
2126
2432
  cosign: cosignVerifyImage
2127
2433
  };
2128
- async function runVerify(input, deps = DEFAULT_DEPS) {
2434
+ async function runVerify(input, deps = DEFAULT_DEPS2) {
2129
2435
  const phases = [];
2130
2436
  const push = (ph) => {
2131
2437
  phases.push(ph);
@@ -2244,26 +2550,26 @@ var verifyCommand = new Command("verify").description(
2244
2550
  ).addOption(
2245
2551
  new Option("--show-skipped", "Include skipped phases in the summary").default(false)
2246
2552
  ).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({
2553
+ p3.intro(pc2.bgCyan(pc2.black(" create-op-node verify ")));
2554
+ const domain = opts.domain ? opts.domain : unwrap(
2555
+ await p3.text({
2250
2556
  message: "Public domain of the node?",
2251
2557
  placeholder: "yournode.example.org",
2252
2558
  validate: (v) => DOMAIN_RE.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens, dots; must contain a dot and a TLD"
2253
2559
  })
2254
2560
  );
2255
2561
  if (!DOMAIN_RE.test(domain)) {
2256
- p2.cancel(`--domain ${JSON.stringify(domain)} doesn't look like a domain.`);
2562
+ p3.cancel(`--domain ${JSON.stringify(domain)} doesn't look like a domain.`);
2257
2563
  process.exit(2);
2258
2564
  }
2259
2565
  const rawDays = opts.certWarnDays ?? "14";
2260
2566
  const certWarnDays = Number.parseInt(rawDays, 10);
2261
2567
  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)}).`);
2568
+ p3.cancel(`--cert-warn-days must be a non-negative integer (got ${JSON.stringify(rawDays)}).`);
2263
2569
  process.exit(2);
2264
2570
  }
2265
2571
  if (opts.cfToken && opts.cfTokenFile) {
2266
- p2.cancel("Pass either --cf-token or --cf-token-file, not both.");
2572
+ p3.cancel("Pass either --cf-token or --cf-token-file, not both.");
2267
2573
  process.exit(2);
2268
2574
  }
2269
2575
  let cfToken = opts.cfToken;
@@ -2271,7 +2577,7 @@ var verifyCommand = new Command("verify").description(
2271
2577
  try {
2272
2578
  cfToken = readTokenFile(opts.cfTokenFile);
2273
2579
  } catch (err) {
2274
- p2.cancel(err.message);
2580
+ p3.cancel(err.message);
2275
2581
  process.exit(2);
2276
2582
  }
2277
2583
  }
@@ -2287,10 +2593,10 @@ var verifyCommand = new Command("verify").description(
2287
2593
  activeSpin = null;
2288
2594
  };
2289
2595
  const deps = {
2290
- ...DEFAULT_DEPS,
2596
+ ...DEFAULT_DEPS2,
2291
2597
  onPhase: renderPhase
2292
2598
  };
2293
- activeSpin = p2.spinner();
2599
+ activeSpin = p3.spinner();
2294
2600
  activeSpin.start("Running checks\u2026");
2295
2601
  const report = await runVerify(
2296
2602
  {
@@ -2309,24 +2615,24 @@ var verifyCommand = new Command("verify").description(
2309
2615
  const visible = opts.showSkipped ?? false ? report.phases : report.phases.filter((ph) => ph.status !== "skipped");
2310
2616
  const lines = visible.map((ph, i) => {
2311
2617
  const idx = report.phases.indexOf(ph) + 1;
2312
- return `${phaseIcon(ph)} [${idx}/${totalPhases}] ${ph.name}: ${pc2.dim(ph.detail)}`;
2618
+ return `${phaseIcon2(ph)} [${idx}/${totalPhases}] ${ph.name}: ${pc2.dim(ph.detail)}`;
2313
2619
  });
2314
2620
  const skippedCount = report.phases.length - visible.length;
2315
2621
  if (skippedCount > 0 && !opts.showSkipped) {
2316
2622
  lines.push(pc2.dim(`\xB7 ${skippedCount} phase${skippedCount === 1 ? "" : "s"} skipped (run with --show-skipped to see them)`));
2317
2623
  }
2318
- p2.note(lines.join("\n"), "Summary");
2624
+ p3.note(lines.join("\n"), "Summary");
2319
2625
  const summary = summarize(report);
2320
2626
  if (summary.ok) {
2321
- p2.outro(
2627
+ p3.outro(
2322
2628
  summary.warned === 0 ? pc2.green("All checks passed.") : pc2.yellow(`Passed with ${summary.warned} warning${summary.warned === 1 ? "" : "s"}.`)
2323
2629
  );
2324
2630
  } else {
2325
- p2.outro(pc2.red(`${summary.failed} check${summary.failed === 1 ? "" : "s"} failed.`));
2631
+ p3.outro(pc2.red(`${summary.failed} check${summary.failed === 1 ? "" : "s"} failed.`));
2326
2632
  process.exit(1);
2327
2633
  }
2328
2634
  });
2329
- function phaseIcon(ph) {
2635
+ function phaseIcon2(ph) {
2330
2636
  switch (ph.status) {
2331
2637
  case "ok":
2332
2638
  return pc2.green("\u2713");
@@ -2339,7 +2645,7 @@ function phaseIcon(ph) {
2339
2645
  }
2340
2646
  }
2341
2647
  function formatPhaseLine(prefix, ph) {
2342
- const icon = phaseIcon(ph);
2648
+ const icon = phaseIcon2(ph);
2343
2649
  const colorize = ph.status === "ok" ? pc2.green : ph.status === "warn" ? pc2.yellow : ph.status === "skipped" ? pc2.dim : pc2.red;
2344
2650
  return colorize(`${icon} ${prefix}: ${ph.detail}`);
2345
2651
  }
@@ -3122,13 +3428,6 @@ function validateRegionConfig(file) {
3122
3428
  }
3123
3429
 
3124
3430
  // 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
3431
  var regionCommand = new Command("region").description(
3133
3432
  "Scaffold a schema-valid region config for the OpusPopuli/opuspopuli-regions repo. Run this from inside your checkout of that repo."
3134
3433
  ).addOption(
@@ -3141,8 +3440,8 @@ var regionCommand = new Command("region").description(
3141
3440
  "current directory"
3142
3441
  )
3143
3442
  ).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(
3443
+ p3.intro(pc2.bgMagenta(pc2.black(" create-op-node region ")));
3444
+ p3.note(
3146
3445
  [
3147
3446
  "Scaffolds a declarative region config JSON for opuspopuli-regions \u2014",
3148
3447
  "the file that defines WHAT civic data a region collects.",
@@ -3156,19 +3455,19 @@ var regionCommand = new Command("region").description(
3156
3455
  const targetDir = opts.outDir ?? process.cwd();
3157
3456
  const looksRight = await looksLikeRegionsRepo(targetDir);
3158
3457
  if (!looksRight) {
3159
- const proceed = unwrap4(
3160
- await p2.confirm({
3458
+ const proceed = unwrap(
3459
+ await p3.confirm({
3161
3460
  message: `${targetDir} doesn't look like an opuspopuli-regions checkout (no schema/region-plugin.schema.json). Continue anyway?`,
3162
3461
  initialValue: false
3163
3462
  })
3164
3463
  );
3165
3464
  if (!proceed) {
3166
- p2.cancel("Cancelled \u2014 run again from your opuspopuli-regions checkout, or pass --out-dir.");
3465
+ p3.cancel("Cancelled \u2014 run again from your opuspopuli-regions checkout, or pass --out-dir.");
3167
3466
  process.exit(0);
3168
3467
  }
3169
3468
  }
3170
- const level = opts.level === "state" || opts.level === "county" ? opts.level : unwrap4(
3171
- await p2.select({
3469
+ const level = opts.level === "state" || opts.level === "county" ? opts.level : unwrap(
3470
+ await p3.select({
3172
3471
  message: "What level is this region?",
3173
3472
  options: [
3174
3473
  { value: "state", label: "State", hint: "e.g. California" },
@@ -3176,8 +3475,8 @@ var regionCommand = new Command("region").description(
3176
3475
  ]
3177
3476
  })
3178
3477
  );
3179
- const rawName = opts.name ?? unwrap4(
3180
- await p2.text({
3478
+ const rawName = opts.name ?? unwrap(
3479
+ await p3.text({
3181
3480
  message: level === "county" ? "County name?" : "State name?",
3182
3481
  placeholder: level === "county" ? "Alameda" : "California",
3183
3482
  validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
@@ -3185,62 +3484,62 @@ var regionCommand = new Command("region").description(
3185
3484
  );
3186
3485
  let parentSlug;
3187
3486
  if (level === "county") {
3188
- parentSlug = opts.parent ?? unwrap4(
3189
- await p2.text({
3487
+ parentSlug = opts.parent ?? unwrap(
3488
+ await p3.text({
3190
3489
  message: "Parent state slug?",
3191
3490
  placeholder: "california",
3192
3491
  validate: (v) => isValidSlug(v ?? "") ? void 0 : "kebab-case (lowercase letters, digits, hyphens)"
3193
3492
  })
3194
3493
  );
3195
3494
  if (!isValidSlug(parentSlug)) {
3196
- p2.cancel(`Parent slug "${parentSlug}" must be kebab-case.`);
3495
+ p3.cancel(`Parent slug "${parentSlug}" must be kebab-case.`);
3197
3496
  process.exit(1);
3198
3497
  }
3199
3498
  }
3200
3499
  const ownSlug = slugify(rawName);
3201
3500
  const regionId = level === "county" ? `${parentSlug}-${ownSlug}` : ownSlug;
3202
3501
  if (!isValidSlug(regionId)) {
3203
- p2.cancel(`Derived regionId "${regionId}" is not valid kebab-case. Try a simpler name.`);
3502
+ p3.cancel(`Derived regionId "${regionId}" is not valid kebab-case. Try a simpler name.`);
3204
3503
  process.exit(1);
3205
3504
  }
3206
- const displayName = unwrap4(
3207
- await p2.text({
3505
+ const displayName = unwrap(
3506
+ await p3.text({
3208
3507
  message: "Display name?",
3209
3508
  defaultValue: rawName
3210
3509
  })
3211
3510
  );
3212
3511
  const regionName = displayName || rawName;
3213
- const description = unwrap4(
3214
- await p2.text({
3512
+ const description = unwrap(
3513
+ await p3.text({
3215
3514
  message: "One-line description of the data coverage?",
3216
3515
  placeholder: level === "county" ? `Civic data for ${rawName} County` : `Civic data for the state of ${rawName}`,
3217
3516
  validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
3218
3517
  })
3219
3518
  );
3220
- const stateCode = (opts.stateCode ?? unwrap4(
3221
- await p2.text({
3519
+ const stateCode = (opts.stateCode ?? unwrap(
3520
+ await p3.text({
3222
3521
  message: "Two-letter state code?",
3223
3522
  placeholder: "CA",
3224
3523
  validate: (v) => isValidStateCode((v ?? "").toUpperCase()) ? void 0 : "Two letters, e.g. CA"
3225
3524
  })
3226
3525
  )).toUpperCase();
3227
3526
  if (!isValidStateCode(stateCode)) {
3228
- p2.cancel(`State code "${stateCode}" must be two letters.`);
3527
+ p3.cancel(`State code "${stateCode}" must be two letters.`);
3229
3528
  process.exit(1);
3230
3529
  }
3231
- const fipsCode = opts.fips ?? unwrap4(
3232
- await p2.text({
3530
+ const fipsCode = opts.fips ?? unwrap(
3531
+ await p3.text({
3233
3532
  message: level === "county" ? "County FIPS (5 digits)?" : "State FIPS (2 digits)?",
3234
3533
  placeholder: level === "county" ? "06001" : "06",
3235
3534
  validate: (v) => isValidFips(v ?? "", level) ? void 0 : `Expected ${level === "county" ? "5" : "2"} digits`
3236
3535
  })
3237
3536
  );
3238
3537
  if (!isValidFips(fipsCode, level)) {
3239
- p2.cancel(`FIPS "${fipsCode}" is the wrong length for a ${level}.`);
3538
+ p3.cancel(`FIPS "${fipsCode}" is the wrong length for a ${level}.`);
3240
3539
  process.exit(1);
3241
3540
  }
3242
- const timezone = opts.timezone ?? unwrap4(
3243
- await p2.text({
3541
+ const timezone = opts.timezone ?? unwrap(
3542
+ await p3.text({
3244
3543
  message: "IANA timezone?",
3245
3544
  defaultValue: "America/Los_Angeles",
3246
3545
  placeholder: "America/Los_Angeles",
@@ -3249,35 +3548,35 @@ var regionCommand = new Command("region").description(
3249
3548
  );
3250
3549
  const dataSources = [];
3251
3550
  do {
3252
- const url = unwrap4(
3253
- await p2.text({
3551
+ const url = unwrap(
3552
+ await p3.text({
3254
3553
  message: `Data source #${dataSources.length + 1} \u2014 URL?`,
3255
3554
  placeholder: "https://example.gov/meetings",
3256
3555
  validate: (v) => /^https?:\/\//.test(v ?? "") ? void 0 : "Must start with http(s)://"
3257
3556
  })
3258
3557
  );
3259
- const dataType = unwrap4(
3260
- await p2.select({
3558
+ const dataType = unwrap(
3559
+ await p3.select({
3261
3560
  message: "Data type?",
3262
3561
  options: DATA_TYPES.map((t) => ({ value: t, label: t }))
3263
3562
  })
3264
3563
  );
3265
- const sourceType = unwrap4(
3266
- await p2.select({
3564
+ const sourceType = unwrap(
3565
+ await p3.select({
3267
3566
  message: "Source type?",
3268
3567
  initialValue: "html_scrape",
3269
3568
  options: SOURCE_TYPES.map((t) => ({ value: t, label: t }))
3270
3569
  })
3271
3570
  );
3272
- const contentGoal = unwrap4(
3273
- await p2.text({
3571
+ const contentGoal = unwrap(
3572
+ await p3.text({
3274
3573
  message: "Content goal (what should the scraper extract)?",
3275
3574
  placeholder: "Fetch upcoming board meeting agendas and minutes",
3276
3575
  validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
3277
3576
  })
3278
3577
  );
3279
- const category = unwrap4(
3280
- await p2.text({
3578
+ const category = unwrap(
3579
+ await p3.text({
3281
3580
  message: "Category label (optional)?",
3282
3581
  placeholder: "Board of Supervisors"
3283
3582
  })
@@ -3285,8 +3584,8 @@ var regionCommand = new Command("region").description(
3285
3584
  const src = { url, dataType, sourceType, contentGoal };
3286
3585
  if (category && category.trim().length > 0) src.category = category.trim();
3287
3586
  dataSources.push(src);
3288
- const again = unwrap4(
3289
- await p2.confirm({ message: "Add another data source?", initialValue: false })
3587
+ const again = unwrap(
3588
+ await p3.confirm({ message: "Add another data source?", initialValue: false })
3290
3589
  );
3291
3590
  if (!again) break;
3292
3591
  } while (true);
@@ -3309,8 +3608,8 @@ var regionCommand = new Command("region").description(
3309
3608
  const file = buildRegionConfig(input);
3310
3609
  const issues = validateRegionConfig(file);
3311
3610
  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.");
3611
+ p3.note(issues.map((i) => `${pc2.red("\u2022")} ${i}`).join("\n"), "Validation failed");
3612
+ p3.cancel("The generated config would not pass the regions repo CI. Re-run and adjust.");
3314
3613
  process.exit(1);
3315
3614
  }
3316
3615
  const relPath = regionFilePath({
@@ -3322,21 +3621,21 @@ var regionCommand = new Command("region").description(
3322
3621
  const absPath = resolve(targetDir, relPath);
3323
3622
  const json = `${JSON.stringify(file, null, 2)}
3324
3623
  `;
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.`);
3624
+ p3.note(json, `${relPath} (preview)`);
3625
+ if (!opts.force && await fileExists3(absPath)) {
3626
+ p3.cancel(`${relPath} already exists. Re-run with --force to overwrite.`);
3328
3627
  process.exit(1);
3329
3628
  }
3330
- const write = unwrap4(
3331
- await p2.confirm({ message: `Write ${relPath}?`, initialValue: true })
3629
+ const write = unwrap(
3630
+ await p3.confirm({ message: `Write ${relPath}?`, initialValue: true })
3332
3631
  );
3333
3632
  if (!write) {
3334
- p2.cancel("Nothing written.");
3633
+ p3.cancel("Nothing written.");
3335
3634
  process.exit(0);
3336
3635
  }
3337
3636
  await mkdir(dirname(absPath), { recursive: true });
3338
3637
  await writeFile(absPath, json, "utf8");
3339
- p2.note(
3638
+ p3.note(
3340
3639
  [
3341
3640
  `${pc2.green("\u2713")} Wrote ${pc2.cyan(relPath)}`,
3342
3641
  "",
@@ -3347,9 +3646,9 @@ var regionCommand = new Command("region").description(
3347
3646
  ].join("\n"),
3348
3647
  "Done"
3349
3648
  );
3350
- p2.outro(pc2.magenta(`Region scaffolded: ${regionId}`));
3649
+ p3.outro(pc2.magenta(`Region scaffolded: ${regionId}`));
3351
3650
  });
3352
- async function fileExists2(path) {
3651
+ async function fileExists3(path) {
3353
3652
  try {
3354
3653
  await access(path);
3355
3654
  return true;
@@ -3359,20 +3658,21 @@ async function fileExists2(path) {
3359
3658
  }
3360
3659
  async function looksLikeRegionsRepo(dir) {
3361
3660
  const [hasSchema, hasRegions] = await Promise.all([
3362
- fileExists2(join(dir, "schema", "region-plugin.schema.json")),
3363
- fileExists2(join(dir, "regions"))
3661
+ fileExists3(join(dir, "schema", "region-plugin.schema.json")),
3662
+ fileExists3(join(dir, "regions"))
3364
3663
  ]);
3365
3664
  return hasSchema && hasRegions;
3366
3665
  }
3367
3666
 
3368
3667
  // src/cli.ts
3369
- var VERSION = "0.1.0";
3668
+ var VERSION = "0.2.0";
3370
3669
  var program = new Command();
3371
3670
  program.name("create-op-node").description(
3372
3671
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
3373
3672
  ).version(VERSION, "-v, --version", "show version");
3374
3673
  program.addCommand(initCommand);
3375
3674
  program.addCommand(bootstrapCommand);
3675
+ program.addCommand(resetCommand);
3376
3676
  program.addCommand(verifyCommand);
3377
3677
  program.addCommand(regionCommand);
3378
3678
  if (process.argv.length === 2) {