create-op-node 0.10.6 → 0.10.8
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 +402 -305
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -645,6 +645,37 @@ var initCommand = new Command("init").description(
|
|
|
645
645
|
).default(false)
|
|
646
646
|
).addOption(new Option("-y, --yes", "Skip the final confirmation").default(false)).action(async (opts) => {
|
|
647
647
|
p3.intro(pc2.bgCyan(pc2.black(" create-op-node init ")));
|
|
648
|
+
printWelcomeNote(opts);
|
|
649
|
+
const region = await resolveRegion(opts);
|
|
650
|
+
const owner = opts.owner ?? "OpusPopuli";
|
|
651
|
+
const newRepoName = `opuspopuli-node-${region}`;
|
|
652
|
+
const newRepoFull = `${owner}/${newRepoName}`;
|
|
653
|
+
warnIgnoredLocalOnlyFlags(opts);
|
|
654
|
+
const publicConfig = opts.localOnly ? null : await collectPublicConfig(opts);
|
|
655
|
+
const ghToken = await resolveGhToken(opts);
|
|
656
|
+
const keychain = await detectKeychain();
|
|
657
|
+
printSecretStoreNote(keychain);
|
|
658
|
+
await confirmPlan({ opts, region, owner, newRepoFull, publicConfig });
|
|
659
|
+
const created = await createRegionRepo({ opts, ghToken, owner, newRepoName, newRepoFull, region });
|
|
660
|
+
if (publicConfig) {
|
|
661
|
+
await seedRepoSecrets({ publicConfig, newRepoFull });
|
|
662
|
+
}
|
|
663
|
+
const pr = publicConfig ? await openInfraPr({ opts, ghToken, region, newRepoFull, publicConfig, created }) : null;
|
|
664
|
+
await persistPgsodiumKey({ keychain, opts, region });
|
|
665
|
+
if (!publicConfig) {
|
|
666
|
+
printLocalOnlyOutro({ region, newRepoFull });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (!pr) {
|
|
670
|
+
throw new Error("init: PR creation step did not run despite publicConfig present");
|
|
671
|
+
}
|
|
672
|
+
const proceed = await pauseForMerge({ pr, opts });
|
|
673
|
+
if (!proceed) return;
|
|
674
|
+
const tunnelToken = await fetchTunnelToken({ publicConfig });
|
|
675
|
+
await persistTunnelToken({ keychain, region, tunnelToken });
|
|
676
|
+
printDoneOutro(region);
|
|
677
|
+
});
|
|
678
|
+
function printWelcomeNote(opts) {
|
|
648
679
|
p3.note(
|
|
649
680
|
opts.localOnly ? [
|
|
650
681
|
"Local-only setup \u2014 creates the region repo from template + a pgsodium key.",
|
|
@@ -663,32 +694,30 @@ var initCommand = new Command("init").description(
|
|
|
663
694
|
].join("\n"),
|
|
664
695
|
"Welcome"
|
|
665
696
|
);
|
|
666
|
-
|
|
697
|
+
}
|
|
698
|
+
async function resolveRegion(opts) {
|
|
699
|
+
return opts.region ? opts.region : unwrap(
|
|
667
700
|
await p3.text({
|
|
668
701
|
message: "Short region label (used as the Keychain service suffix + R2 prefix)?",
|
|
669
702
|
placeholder: "us-ca",
|
|
670
703
|
validate: (v) => /^[a-z0-9-]{2,32}$/.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
|
|
671
704
|
})
|
|
672
705
|
);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
`\u26A0 The following ${ignored.length === 1 ? "flag is" : "flags are"} ignored with --local-only:
|
|
706
|
+
}
|
|
707
|
+
function warnIgnoredLocalOnlyFlags(opts) {
|
|
708
|
+
if (!opts.localOnly) return;
|
|
709
|
+
const ignored = listIgnoredLocalOnlyFlags(opts);
|
|
710
|
+
if (ignored.length === 0) return;
|
|
711
|
+
p3.note(
|
|
712
|
+
pc2.yellow(
|
|
713
|
+
`\u26A0 The following ${ignored.length === 1 ? "flag is" : "flags are"} ignored with --local-only:
|
|
682
714
|
${ignored.join(", ")}
|
|
683
|
-
` + pc2.dim(
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
const publicConfig = opts.localOnly ? null : await collectPublicConfig(opts);
|
|
715
|
+
` + pc2.dim("These configure Cloudflare / TFC / PR-merge flow, which --local-only skips.")
|
|
716
|
+
),
|
|
717
|
+
"Notice"
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
async function resolveGhToken(opts) {
|
|
692
721
|
let ghToken = opts.ghToken;
|
|
693
722
|
if (!ghToken) {
|
|
694
723
|
const fromCli = await ghTokenFromCli();
|
|
@@ -709,10 +738,15 @@ var initCommand = new Command("init").description(
|
|
|
709
738
|
"github"
|
|
710
739
|
);
|
|
711
740
|
}
|
|
712
|
-
|
|
741
|
+
return ghToken;
|
|
742
|
+
}
|
|
743
|
+
function printSecretStoreNote(keychain) {
|
|
713
744
|
const kcNote = keychain.available ? `\u2713 macOS Keychain available \u2014 pgsodium key + Tunnel token will be saved to your login keychain.
|
|
714
745
|
` + 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.`;
|
|
715
746
|
p3.note(kcNote, "Secret store");
|
|
747
|
+
}
|
|
748
|
+
async function confirmPlan(args) {
|
|
749
|
+
const { opts, region, owner, newRepoFull, publicConfig } = args;
|
|
716
750
|
const orgWriteAccess = pc2.dim(`required for ${owner}`);
|
|
717
751
|
p3.note(
|
|
718
752
|
opts.localOnly ? [
|
|
@@ -739,20 +773,19 @@ var initCommand = new Command("init").description(
|
|
|
739
773
|
].join("\n"),
|
|
740
774
|
"Plan"
|
|
741
775
|
);
|
|
742
|
-
if (
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
);
|
|
746
|
-
|
|
747
|
-
p3.cancel("Cancelled.");
|
|
748
|
-
process.exit(0);
|
|
749
|
-
}
|
|
776
|
+
if (opts.yes) return;
|
|
777
|
+
const go = unwrap(await p3.confirm({ message: "Proceed?", initialValue: true }));
|
|
778
|
+
if (!go) {
|
|
779
|
+
p3.cancel("Cancelled.");
|
|
780
|
+
process.exit(0);
|
|
750
781
|
}
|
|
782
|
+
}
|
|
783
|
+
async function createRegionRepo(args) {
|
|
784
|
+
const { opts, ghToken, owner, newRepoName, newRepoFull, region } = args;
|
|
751
785
|
const repoSpin = p3.spinner();
|
|
752
786
|
repoSpin.start(`Creating ${newRepoFull} from ${opts.template}\u2026`);
|
|
753
|
-
let created;
|
|
754
787
|
try {
|
|
755
|
-
created = await createRepoFromTemplate({
|
|
788
|
+
const created = await createRepoFromTemplate({
|
|
756
789
|
token: ghToken,
|
|
757
790
|
template: opts.template ?? "OpusPopuli/opuspopuli-node",
|
|
758
791
|
owner,
|
|
@@ -760,6 +793,7 @@ var initCommand = new Command("init").description(
|
|
|
760
793
|
description: `Opus Populi node deployment for ${region}`
|
|
761
794
|
});
|
|
762
795
|
repoSpin.stop(pc2.green(`\u2713 Created ${created.fullName}`));
|
|
796
|
+
return { fullName: created.fullName, defaultBranch: created.defaultBranch };
|
|
763
797
|
} catch (err) {
|
|
764
798
|
const status = typeof err === "object" && err !== null && "status" in err ? err.status : 0;
|
|
765
799
|
const message = err.message ?? "unknown error";
|
|
@@ -781,154 +815,155 @@ var initCommand = new Command("init").description(
|
|
|
781
815
|
);
|
|
782
816
|
process.exit(0);
|
|
783
817
|
}
|
|
784
|
-
|
|
785
|
-
fullName: newRepoFull,
|
|
786
|
-
htmlUrl: `https://github.com/${newRepoFull}`,
|
|
787
|
-
defaultBranch: "main"
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
if (publicConfig) {
|
|
791
|
-
const secrets = [
|
|
792
|
-
{ name: "CLOUDFLARE_API_TOKEN", value: publicConfig.cfToken },
|
|
793
|
-
{ name: "CLOUDFLARE_ACCOUNT_ID", value: publicConfig.cfAccount },
|
|
794
|
-
{ name: "CLOUDFLARE_ZONE_ID", value: publicConfig.cfZone },
|
|
795
|
-
{ name: "TF_API_TOKEN", value: publicConfig.tfToken },
|
|
796
|
-
{ name: "TF_CLOUD_ORGANIZATION", value: publicConfig.tfOrg }
|
|
797
|
-
];
|
|
798
|
-
const secSpin = p3.spinner();
|
|
799
|
-
secSpin.start(`Seeding ${secrets.length} repo secrets\u2026`);
|
|
800
|
-
const seeded = [];
|
|
801
|
-
for (const s of secrets) {
|
|
802
|
-
const r = await setRepoSecret({ repo: newRepoFull, name: s.name, value: s.value });
|
|
803
|
-
if (!r.written) {
|
|
804
|
-
secSpin.stop(pc2.red(`\u2717 Failed to set ${s.name}: ${r.reason ?? "unknown"}`));
|
|
805
|
-
const remaining = secrets.slice(secrets.findIndex((x) => x.name === s.name)).map((x) => x.name);
|
|
806
|
-
p3.cancel(
|
|
807
|
-
[
|
|
808
|
-
seeded.length > 0 ? `Already seeded on ${newRepoFull}: ${seeded.join(", ")}.` : `Nothing seeded yet on ${newRepoFull}.`,
|
|
809
|
-
`Still pending: ${remaining.join(", ")}.`,
|
|
810
|
-
"",
|
|
811
|
-
`Make sure \`gh\` is installed and signed in (\`gh auth login\`).`,
|
|
812
|
-
`Re-run with --use-existing-repo to retry (GitHub will overwrite the already-set secrets idempotently).`
|
|
813
|
-
].join("\n")
|
|
814
|
-
);
|
|
815
|
-
process.exit(1);
|
|
816
|
-
}
|
|
817
|
-
seeded.push(s.name);
|
|
818
|
-
}
|
|
819
|
-
secSpin.stop(pc2.green(`\u2713 Seeded ${secrets.length} secrets on ${newRepoFull}`));
|
|
818
|
+
return { fullName: newRepoFull, defaultBranch: "main" };
|
|
820
819
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
message: `init: prod.tfvars for ${region}`
|
|
844
|
-
});
|
|
845
|
-
} catch (err) {
|
|
846
|
-
setupSpin.stop(pc2.red(`\u2717 Couldn't write tfvars: ${err.message}`));
|
|
847
|
-
p3.cancel("The repo exists but the branch / commit failed. Open it on GitHub and inspect.");
|
|
848
|
-
process.exit(1);
|
|
849
|
-
}
|
|
850
|
-
setupSpin.stop(pc2.green("\u2713 Wrote prod.tfvars"));
|
|
851
|
-
const prSpin = p3.spinner();
|
|
852
|
-
prSpin.start("Opening pull request\u2026");
|
|
853
|
-
try {
|
|
854
|
-
pr = await openPullRequest({
|
|
855
|
-
token: ghToken,
|
|
856
|
-
repo: newRepoFull,
|
|
857
|
-
head: branch,
|
|
858
|
-
base: created.defaultBranch,
|
|
859
|
-
title: `init: bring up region ${region}`,
|
|
860
|
-
body: [
|
|
861
|
-
`Initial region deployment for **${region}** (${publicConfig.domain}).`,
|
|
862
|
-
"",
|
|
863
|
-
"Adds `infra/cloudflare/environments/prod.tfvars`. Merging this PR triggers",
|
|
864
|
-
"`cloudflare-infra.yml` which runs `terraform apply` against the",
|
|
865
|
-
`\`${publicConfig.tfOrg}\` Terraform Cloud organization, provisioning the Cloudflare`,
|
|
866
|
-
"Tunnel, DNS records, R2 buckets, and Pages project.",
|
|
820
|
+
}
|
|
821
|
+
async function seedRepoSecrets(args) {
|
|
822
|
+
const { publicConfig, newRepoFull } = args;
|
|
823
|
+
const secrets = [
|
|
824
|
+
{ name: "CLOUDFLARE_API_TOKEN", value: publicConfig.cfToken },
|
|
825
|
+
{ name: "CLOUDFLARE_ACCOUNT_ID", value: publicConfig.cfAccount },
|
|
826
|
+
{ name: "CLOUDFLARE_ZONE_ID", value: publicConfig.cfZone },
|
|
827
|
+
{ name: "TF_API_TOKEN", value: publicConfig.tfToken },
|
|
828
|
+
{ name: "TF_CLOUD_ORGANIZATION", value: publicConfig.tfOrg }
|
|
829
|
+
];
|
|
830
|
+
const secSpin = p3.spinner();
|
|
831
|
+
secSpin.start(`Seeding ${secrets.length} repo secrets\u2026`);
|
|
832
|
+
const seeded = [];
|
|
833
|
+
for (const s of secrets) {
|
|
834
|
+
const r = await setRepoSecret({ repo: newRepoFull, name: s.name, value: s.value });
|
|
835
|
+
if (!r.written) {
|
|
836
|
+
secSpin.stop(pc2.red(`\u2717 Failed to set ${s.name}: ${r.reason ?? "unknown"}`));
|
|
837
|
+
const remaining = secrets.slice(secrets.findIndex((x) => x.name === s.name)).map((x) => x.name);
|
|
838
|
+
p3.cancel(
|
|
839
|
+
[
|
|
840
|
+
seeded.length > 0 ? `Already seeded on ${newRepoFull}: ${seeded.join(", ")}.` : `Nothing seeded yet on ${newRepoFull}.`,
|
|
841
|
+
`Still pending: ${remaining.join(", ")}.`,
|
|
867
842
|
"",
|
|
868
|
-
`
|
|
843
|
+
`Make sure \`gh\` is installed and signed in (\`gh auth login\`).`,
|
|
844
|
+
`Re-run with --use-existing-repo to retry (GitHub will overwrite the already-set secrets idempotently).`
|
|
869
845
|
].join("\n")
|
|
870
|
-
|
|
871
|
-
} catch (err) {
|
|
872
|
-
prSpin.stop(pc2.red(`\u2717 Couldn't open PR: ${err.message}`));
|
|
873
|
-
p3.cancel("Branch + tfvars are committed; open the PR by hand on GitHub.");
|
|
846
|
+
);
|
|
874
847
|
process.exit(1);
|
|
875
848
|
}
|
|
849
|
+
seeded.push(s.name);
|
|
850
|
+
}
|
|
851
|
+
secSpin.stop(pc2.green(`\u2713 Seeded ${secrets.length} secrets on ${newRepoFull}`));
|
|
852
|
+
}
|
|
853
|
+
async function openInfraPr(args) {
|
|
854
|
+
const { opts, ghToken, region, newRepoFull, publicConfig, created } = args;
|
|
855
|
+
const branch = `init/region-${region}-${isoStampUtc(/* @__PURE__ */ new Date())}`;
|
|
856
|
+
const setupSpin = p3.spinner();
|
|
857
|
+
setupSpin.start(`Writing prod.tfvars on branch ${branch}\u2026`);
|
|
858
|
+
try {
|
|
859
|
+
await createBranch({
|
|
860
|
+
token: ghToken,
|
|
861
|
+
repo: newRepoFull,
|
|
862
|
+
branch,
|
|
863
|
+
fromBranch: created.defaultBranch
|
|
864
|
+
});
|
|
865
|
+
const tfvars = renderProdTfvars({
|
|
866
|
+
project: opts.project ?? "opuspopuli",
|
|
867
|
+
domain: publicConfig.domain
|
|
868
|
+
});
|
|
869
|
+
await commitFile({
|
|
870
|
+
token: ghToken,
|
|
871
|
+
repo: newRepoFull,
|
|
872
|
+
branch,
|
|
873
|
+
path: "infra/cloudflare/environments/prod.tfvars",
|
|
874
|
+
content: tfvars,
|
|
875
|
+
message: `init: prod.tfvars for ${region}`
|
|
876
|
+
});
|
|
877
|
+
} catch (err) {
|
|
878
|
+
setupSpin.stop(pc2.red(`\u2717 Couldn't write tfvars: ${err.message}`));
|
|
879
|
+
p3.cancel("The repo exists but the branch / commit failed. Open it on GitHub and inspect.");
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
setupSpin.stop(pc2.green("\u2713 Wrote prod.tfvars"));
|
|
883
|
+
const prSpin = p3.spinner();
|
|
884
|
+
prSpin.start("Opening pull request\u2026");
|
|
885
|
+
try {
|
|
886
|
+
const pr = await openPullRequest({
|
|
887
|
+
token: ghToken,
|
|
888
|
+
repo: newRepoFull,
|
|
889
|
+
head: branch,
|
|
890
|
+
base: created.defaultBranch,
|
|
891
|
+
title: `init: bring up region ${region}`,
|
|
892
|
+
body: [
|
|
893
|
+
`Initial region deployment for **${region}** (${publicConfig.domain}).`,
|
|
894
|
+
"",
|
|
895
|
+
"Adds `infra/cloudflare/environments/prod.tfvars`. Merging this PR triggers",
|
|
896
|
+
"`cloudflare-infra.yml` which runs `terraform apply` against the",
|
|
897
|
+
`\`${publicConfig.tfOrg}\` Terraform Cloud organization, provisioning the Cloudflare`,
|
|
898
|
+
"Tunnel, DNS records, R2 buckets, and Pages project.",
|
|
899
|
+
"",
|
|
900
|
+
`Generated by \`create-op-node init\`.`
|
|
901
|
+
].join("\n")
|
|
902
|
+
});
|
|
876
903
|
prSpin.stop(pc2.green(`\u2713 Opened PR #${pr.number}`));
|
|
904
|
+
return pr;
|
|
905
|
+
} catch (err) {
|
|
906
|
+
prSpin.stop(pc2.red(`\u2717 Couldn't open PR: ${err.message}`));
|
|
907
|
+
p3.cancel("Branch + tfvars are committed; open the PR by hand on GitHub.");
|
|
908
|
+
process.exit(1);
|
|
877
909
|
}
|
|
910
|
+
}
|
|
911
|
+
async function persistPgsodiumKey(args) {
|
|
912
|
+
const { keychain, opts, region } = args;
|
|
878
913
|
const pgsodiumCoords = { region, account: "pgsodium-root-key" };
|
|
879
914
|
const keyLabel = `org.opuspopuli.${region}/pgsodium-root-key`;
|
|
880
|
-
if (keychain.available) {
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
`${pc2.green("\u2713")} Re-using existing pgsodium master key from Keychain (${pc2.cyan(keyLabel)}). Pass --overwrite to rotate.`,
|
|
885
|
-
"Secret"
|
|
886
|
-
);
|
|
887
|
-
} else {
|
|
888
|
-
const fresh = generatePgsodiumRootKey();
|
|
889
|
-
const r = await saveSecret(pgsodiumCoords, fresh);
|
|
890
|
-
if (r.written) {
|
|
891
|
-
p3.note(
|
|
892
|
-
`${pc2.green("\u2713")} pgsodium master key${r.updated ? " rotated" : " saved"} to Keychain as ${pc2.cyan(keyLabel)}.`,
|
|
893
|
-
"Secret"
|
|
894
|
-
);
|
|
895
|
-
} else {
|
|
896
|
-
await printKeyForManualSave(fresh, keyLabel, r.reason);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
} else {
|
|
900
|
-
const fresh = generatePgsodiumRootKey();
|
|
901
|
-
await printKeyForManualSave(fresh, keyLabel);
|
|
915
|
+
if (!keychain.available) {
|
|
916
|
+
const fresh2 = generatePgsodiumRootKey();
|
|
917
|
+
await printKeyForManualSave(fresh2, keyLabel);
|
|
918
|
+
return;
|
|
902
919
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
`Repo: https://github.com/${newRepoFull}`,
|
|
909
|
-
``,
|
|
910
|
-
`Next: on the Studio (fresh or otherwise \u2014 bootstrap installs`,
|
|
911
|
-
`brew packages it needs), run:`,
|
|
912
|
-
` npx create-op-node bootstrap --region ${region} --local-only`,
|
|
913
|
-
``,
|
|
914
|
-
`When you're ready to expose publicly, re-run BOTH \`init\` and`,
|
|
915
|
-
`\`bootstrap\` without --local-only. Same region repo + pgsodium key`,
|
|
916
|
-
`promote cleanly to the production-shaped deploy.`
|
|
917
|
-
].join("\n")
|
|
918
|
-
)
|
|
920
|
+
const existing = opts.overwrite ? null : await readSecret(pgsodiumCoords);
|
|
921
|
+
if (existing && /^[a-f0-9]{64}$/.test(existing)) {
|
|
922
|
+
p3.note(
|
|
923
|
+
`${pc2.green("\u2713")} Re-using existing pgsodium master key from Keychain (${pc2.cyan(keyLabel)}). Pass --overwrite to rotate.`,
|
|
924
|
+
"Secret"
|
|
919
925
|
);
|
|
920
926
|
return;
|
|
921
927
|
}
|
|
922
|
-
|
|
923
|
-
|
|
928
|
+
const fresh = generatePgsodiumRootKey();
|
|
929
|
+
const r = await saveSecret(pgsodiumCoords, fresh);
|
|
930
|
+
if (r.written) {
|
|
931
|
+
p3.note(
|
|
932
|
+
`${pc2.green("\u2713")} pgsodium master key${r.updated ? " rotated" : " saved"} to Keychain as ${pc2.cyan(keyLabel)}.`,
|
|
933
|
+
"Secret"
|
|
934
|
+
);
|
|
935
|
+
} else {
|
|
936
|
+
await printKeyForManualSave(fresh, keyLabel, r.reason);
|
|
924
937
|
}
|
|
938
|
+
}
|
|
939
|
+
function printLocalOnlyOutro(args) {
|
|
940
|
+
const { region, newRepoFull } = args;
|
|
941
|
+
p3.outro(
|
|
942
|
+
pc2.cyan(
|
|
943
|
+
[
|
|
944
|
+
`Region ${region} provisioned for local-only mode.`,
|
|
945
|
+
`Repo: https://github.com/${newRepoFull}`,
|
|
946
|
+
``,
|
|
947
|
+
`Next: on the Studio (fresh or otherwise \u2014 bootstrap installs`,
|
|
948
|
+
`brew packages it needs), run:`,
|
|
949
|
+
` npx create-op-node bootstrap --region ${region} --local-only`,
|
|
950
|
+
``,
|
|
951
|
+
`When you're ready to expose publicly, re-run BOTH \`init\` and`,
|
|
952
|
+
`\`bootstrap\` without --local-only. Same region repo + pgsodium key`,
|
|
953
|
+
`promote cleanly to the production-shaped deploy.`
|
|
954
|
+
].join("\n")
|
|
955
|
+
)
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
async function pauseForMerge(args) {
|
|
959
|
+
const { pr, opts } = args;
|
|
925
960
|
if (opts.skipWait) {
|
|
926
961
|
p3.outro(
|
|
927
962
|
pc2.cyan(
|
|
928
963
|
`Review + merge ${pr.htmlUrl} when ready. Re-run with no --skip-wait or run \`npx create-op-node verify\` afterwards.`
|
|
929
964
|
)
|
|
930
965
|
);
|
|
931
|
-
return;
|
|
966
|
+
return false;
|
|
932
967
|
}
|
|
933
968
|
p3.note(
|
|
934
969
|
[
|
|
@@ -939,15 +974,17 @@ var initCommand = new Command("init").description(
|
|
|
939
974
|
].join("\n"),
|
|
940
975
|
"Next step: review + merge"
|
|
941
976
|
);
|
|
942
|
-
const merged = unwrap(
|
|
943
|
-
await p3.confirm({ message: "PR merged?", initialValue: false })
|
|
944
|
-
);
|
|
977
|
+
const merged = unwrap(await p3.confirm({ message: "PR merged?", initialValue: false }));
|
|
945
978
|
if (!merged) {
|
|
946
979
|
p3.outro(
|
|
947
980
|
pc2.cyan(`No worries \u2014 re-run \`create-op-node init --skip-wait\` later to skip this step, or finish manually.`)
|
|
948
981
|
);
|
|
949
|
-
return;
|
|
982
|
+
return false;
|
|
950
983
|
}
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
async function fetchTunnelToken(args) {
|
|
987
|
+
const { publicConfig } = args;
|
|
951
988
|
const ws = await findWorkspace({
|
|
952
989
|
token: publicConfig.tfToken,
|
|
953
990
|
organization: publicConfig.tfOrg,
|
|
@@ -971,27 +1008,33 @@ var initCommand = new Command("init").description(
|
|
|
971
1008
|
);
|
|
972
1009
|
process.exit(1);
|
|
973
1010
|
}
|
|
1011
|
+
return tunnelToken;
|
|
1012
|
+
}
|
|
1013
|
+
async function persistTunnelToken(args) {
|
|
1014
|
+
const { keychain, region, tunnelToken } = args;
|
|
974
1015
|
const tunnelCoords = { region, account: "tunnel-token" };
|
|
975
1016
|
const tunnelLabel = `org.opuspopuli.${region}/tunnel-token`;
|
|
976
|
-
if (keychain.available) {
|
|
977
|
-
const r = await saveSecret(tunnelCoords, tunnelToken);
|
|
978
|
-
if (r.written) {
|
|
979
|
-
p3.note(
|
|
980
|
-
`${pc2.green("\u2713")} Tunnel token saved to Keychain as ${pc2.cyan(tunnelLabel)}.`,
|
|
981
|
-
"Secret"
|
|
982
|
-
);
|
|
983
|
-
} else {
|
|
984
|
-
await printKeyForManualSave(tunnelToken, tunnelLabel, r.reason);
|
|
985
|
-
}
|
|
986
|
-
} else {
|
|
1017
|
+
if (!keychain.available) {
|
|
987
1018
|
await printKeyForManualSave(tunnelToken, tunnelLabel);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const r = await saveSecret(tunnelCoords, tunnelToken);
|
|
1022
|
+
if (r.written) {
|
|
1023
|
+
p3.note(
|
|
1024
|
+
`${pc2.green("\u2713")} Tunnel token saved to Keychain as ${pc2.cyan(tunnelLabel)}.`,
|
|
1025
|
+
"Secret"
|
|
1026
|
+
);
|
|
1027
|
+
} else {
|
|
1028
|
+
await printKeyForManualSave(tunnelToken, tunnelLabel, r.reason);
|
|
988
1029
|
}
|
|
1030
|
+
}
|
|
1031
|
+
function printDoneOutro(region) {
|
|
989
1032
|
p3.outro(
|
|
990
1033
|
pc2.cyan(
|
|
991
1034
|
`Region ${region} is provisioned. Next: \`npx create-op-node bootstrap\` on the Mac Studio.`
|
|
992
1035
|
)
|
|
993
1036
|
);
|
|
994
|
-
}
|
|
1037
|
+
}
|
|
995
1038
|
var MIN_TOKEN_LENGTH = {
|
|
996
1039
|
cloudflare: 40,
|
|
997
1040
|
tfc: 40,
|
|
@@ -2041,7 +2084,7 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2041
2084
|
).addOption(new Option("-y, --yes", "Skip confirmation prompts").default(false)).action(async (opts) => {
|
|
2042
2085
|
p3.intro(pc2.bgCyan(pc2.black(" create-op-node bootstrap ")));
|
|
2043
2086
|
const nodeType = await selectNodeType(opts);
|
|
2044
|
-
const region = await
|
|
2087
|
+
const region = await resolveRegion2(opts);
|
|
2045
2088
|
const owner = opts.owner ?? "OpusPopuli";
|
|
2046
2089
|
const repoName = `opuspopuli-node-${region}`;
|
|
2047
2090
|
const composeFile = opts.composeFile ?? defaultComposeFiles(opts);
|
|
@@ -2095,7 +2138,7 @@ async function selectNodeType(opts) {
|
|
|
2095
2138
|
}
|
|
2096
2139
|
return nodeType;
|
|
2097
2140
|
}
|
|
2098
|
-
async function
|
|
2141
|
+
async function resolveRegion2(opts) {
|
|
2099
2142
|
return opts.region ? opts.region : unwrap(
|
|
2100
2143
|
await p3.text({
|
|
2101
2144
|
message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
|
|
@@ -2847,87 +2890,89 @@ async function runReset(input, deps = DEFAULT_DEPS) {
|
|
|
2847
2890
|
phases.push(ph);
|
|
2848
2891
|
deps.onPhase?.(ph);
|
|
2849
2892
|
};
|
|
2893
|
+
push(await resetStopStackPhase(input, deps));
|
|
2894
|
+
push(await resetLaunchAgentPhase(input, deps));
|
|
2895
|
+
push(await resetDockerLogoutPhase(input, deps));
|
|
2896
|
+
return { phases };
|
|
2897
|
+
}
|
|
2898
|
+
async function resetStopStackPhase(input, deps) {
|
|
2850
2899
|
if (!input.stack) {
|
|
2851
|
-
|
|
2900
|
+
return {
|
|
2852
2901
|
name: RESET_PHASES.STOP_STACK,
|
|
2853
2902
|
status: "skipped",
|
|
2854
2903
|
detail: "--skip-stack or no repo path resolved"
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
if (input.dryRun) {
|
|
2907
|
+
return {
|
|
2858
2908
|
name: RESET_PHASES.STOP_STACK,
|
|
2859
2909
|
status: "dry-run",
|
|
2860
2910
|
detail: `would run: docker compose -f ${input.stack.composeFiles.join(" -f ")} down${input.stack.wipeVolumes ? " -v" : ""}${input.stack.removeOrphans ? " --remove-orphans" : ""}${input.stack.wipeImages ? " --rmi all" : ""}`
|
|
2861
|
-
}
|
|
2862
|
-
} else {
|
|
2863
|
-
const result2 = await deps.composeDown({
|
|
2864
|
-
files: input.stack.composeFiles,
|
|
2865
|
-
cwd: input.stack.repoPath,
|
|
2866
|
-
...input.stack.envFile ? { envFile: input.stack.envFile } : {},
|
|
2867
|
-
wipeVolumes: input.stack.wipeVolumes,
|
|
2868
|
-
removeOrphans: input.stack.removeOrphans,
|
|
2869
|
-
...input.stack.wipeImages ? { removeImages: "all" } : {}
|
|
2870
|
-
});
|
|
2871
|
-
if (result2.ok) {
|
|
2872
|
-
const bits = [
|
|
2873
|
-
input.stack.wipeVolumes ? "volumes destroyed" : "containers stopped, volumes preserved",
|
|
2874
|
-
input.stack.wipeImages ? "images removed (next bootstrap will re-pull)" : null
|
|
2875
|
-
].filter(Boolean);
|
|
2876
|
-
push({
|
|
2877
|
-
name: RESET_PHASES.STOP_STACK,
|
|
2878
|
-
status: "ok",
|
|
2879
|
-
detail: bits.join("; ")
|
|
2880
|
-
});
|
|
2881
|
-
} else {
|
|
2882
|
-
push({ name: RESET_PHASES.STOP_STACK, status: "fail", detail: result2.reason ?? "unknown failure" });
|
|
2883
|
-
}
|
|
2911
|
+
};
|
|
2884
2912
|
}
|
|
2913
|
+
const result2 = await deps.composeDown({
|
|
2914
|
+
files: input.stack.composeFiles,
|
|
2915
|
+
cwd: input.stack.repoPath,
|
|
2916
|
+
...input.stack.envFile ? { envFile: input.stack.envFile } : {},
|
|
2917
|
+
wipeVolumes: input.stack.wipeVolumes,
|
|
2918
|
+
removeOrphans: input.stack.removeOrphans,
|
|
2919
|
+
...input.stack.wipeImages ? { removeImages: "all" } : {}
|
|
2920
|
+
});
|
|
2921
|
+
if (!result2.ok) {
|
|
2922
|
+
return { name: RESET_PHASES.STOP_STACK, status: "fail", detail: result2.reason ?? "unknown failure" };
|
|
2923
|
+
}
|
|
2924
|
+
const bits = [
|
|
2925
|
+
input.stack.wipeVolumes ? "volumes destroyed" : "containers stopped, volumes preserved",
|
|
2926
|
+
input.stack.wipeImages ? "images removed (next bootstrap will re-pull)" : null
|
|
2927
|
+
].filter(Boolean);
|
|
2928
|
+
return { name: RESET_PHASES.STOP_STACK, status: "ok", detail: bits.join("; ") };
|
|
2929
|
+
}
|
|
2930
|
+
async function resetLaunchAgentPhase(input, deps) {
|
|
2885
2931
|
if (!input.launchAgent) {
|
|
2886
|
-
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2932
|
+
return { name: RESET_PHASES.LAUNCH_AGENT, status: "skipped", detail: "--skip-launch-agent or no plist found" };
|
|
2933
|
+
}
|
|
2934
|
+
if (input.dryRun) {
|
|
2935
|
+
return {
|
|
2889
2936
|
name: RESET_PHASES.LAUNCH_AGENT,
|
|
2890
2937
|
status: "dry-run",
|
|
2891
2938
|
detail: `would unload ${input.launchAgent.paths.plistFile}, rm plist${input.launchAgent.keepKeyFile ? "" : " + key file"}`
|
|
2892
|
-
}
|
|
2893
|
-
} else {
|
|
2894
|
-
const result2 = await deps.teardownLaunchAgent(
|
|
2895
|
-
input.launchAgent.paths,
|
|
2896
|
-
{ keepKeyFile: input.launchAgent.keepKeyFile }
|
|
2897
|
-
);
|
|
2898
|
-
if (result2.ok) {
|
|
2899
|
-
const removed = result2.steps.filter((s) => s.step !== "unload").map((s) => s.step).join(", ");
|
|
2900
|
-
push({ name: RESET_PHASES.LAUNCH_AGENT, status: "ok", detail: `unloaded + ${removed}` });
|
|
2901
|
-
} else {
|
|
2902
|
-
const fail = result2.steps.find((s) => !s.ok);
|
|
2903
|
-
push({
|
|
2904
|
-
name: RESET_PHASES.LAUNCH_AGENT,
|
|
2905
|
-
status: "fail",
|
|
2906
|
-
detail: `${fail?.step}: ${fail?.reason ?? "unknown"}`
|
|
2907
|
-
});
|
|
2908
|
-
}
|
|
2939
|
+
};
|
|
2909
2940
|
}
|
|
2941
|
+
const result2 = await deps.teardownLaunchAgent(
|
|
2942
|
+
input.launchAgent.paths,
|
|
2943
|
+
{ keepKeyFile: input.launchAgent.keepKeyFile }
|
|
2944
|
+
);
|
|
2945
|
+
if (!result2.ok) {
|
|
2946
|
+
const fail = result2.steps.find((s) => !s.ok);
|
|
2947
|
+
return {
|
|
2948
|
+
name: RESET_PHASES.LAUNCH_AGENT,
|
|
2949
|
+
status: "fail",
|
|
2950
|
+
detail: `${fail?.step}: ${fail?.reason ?? "unknown"}`
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
const removed = result2.steps.filter((s) => s.step !== "unload").map((s) => s.step).join(", ");
|
|
2954
|
+
return { name: RESET_PHASES.LAUNCH_AGENT, status: "ok", detail: `unloaded + ${removed}` };
|
|
2955
|
+
}
|
|
2956
|
+
async function resetDockerLogoutPhase(input, deps) {
|
|
2910
2957
|
if (!input.dockerLogout) {
|
|
2911
|
-
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2958
|
+
return { name: RESET_PHASES.DOCKER_LOGOUT, status: "skipped", detail: "--skip-docker-logout" };
|
|
2959
|
+
}
|
|
2960
|
+
if (input.dryRun) {
|
|
2961
|
+
return {
|
|
2914
2962
|
name: RESET_PHASES.DOCKER_LOGOUT,
|
|
2915
2963
|
status: "dry-run",
|
|
2916
2964
|
detail: `would run: docker logout ${input.dockerLogout.registry}`
|
|
2917
|
-
}
|
|
2918
|
-
} else {
|
|
2919
|
-
const result2 = await deps.dockerLogout(input.dockerLogout.registry);
|
|
2920
|
-
if (result2.ok) {
|
|
2921
|
-
push({
|
|
2922
|
-
name: RESET_PHASES.DOCKER_LOGOUT,
|
|
2923
|
-
status: "ok",
|
|
2924
|
-
detail: `credentials removed from ${input.dockerLogout.registry}`
|
|
2925
|
-
});
|
|
2926
|
-
} else {
|
|
2927
|
-
push({ name: RESET_PHASES.DOCKER_LOGOUT, status: "warn", detail: result2.reason ?? "unknown" });
|
|
2928
|
-
}
|
|
2965
|
+
};
|
|
2929
2966
|
}
|
|
2930
|
-
|
|
2967
|
+
const result2 = await deps.dockerLogout(input.dockerLogout.registry);
|
|
2968
|
+
if (result2.ok) {
|
|
2969
|
+
return {
|
|
2970
|
+
name: RESET_PHASES.DOCKER_LOGOUT,
|
|
2971
|
+
status: "ok",
|
|
2972
|
+
detail: `credentials removed from ${input.dockerLogout.registry}`
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
return { name: RESET_PHASES.DOCKER_LOGOUT, status: "warn", detail: result2.reason ?? "unknown" };
|
|
2931
2976
|
}
|
|
2932
2977
|
async function fileExists2(path) {
|
|
2933
2978
|
try {
|
|
@@ -2983,6 +3028,43 @@ var resetCommand = new Command("reset").description(
|
|
|
2983
3028
|
const wipeImages = opts.wipeImages ?? false;
|
|
2984
3029
|
const wipeData = (opts.wipeData ?? false) || wipeImages;
|
|
2985
3030
|
const removeOrphans = opts.removeOrphans ?? true;
|
|
3031
|
+
const region = await resolveResetRegion(opts);
|
|
3032
|
+
const owner = opts.owner ?? "OpusPopuli";
|
|
3033
|
+
const repoName = `opuspopuli-node-${region}`;
|
|
3034
|
+
const launchAgentPaths = defaultPaths();
|
|
3035
|
+
const plistExists = await fileExists2(launchAgentPaths.plistFile);
|
|
3036
|
+
const keyFileExists = await fileExists2(launchAgentPaths.keyFile);
|
|
3037
|
+
const { repoPath, stackSkipReason } = await locateResetRepo(opts, owner, repoName);
|
|
3038
|
+
const runningContainers = repoPath ? await composePs({
|
|
3039
|
+
files: resolveComposeFiles(repoPath, opts.composeFile),
|
|
3040
|
+
cwd: repoPath,
|
|
3041
|
+
...opts.envFile ? { envFile: opts.envFile } : {}
|
|
3042
|
+
}) : null;
|
|
3043
|
+
renderSnapshotNote({
|
|
3044
|
+
region,
|
|
3045
|
+
repoPath,
|
|
3046
|
+
stackSkipReason,
|
|
3047
|
+
runningContainers,
|
|
3048
|
+
plistExists,
|
|
3049
|
+
keyFileExists,
|
|
3050
|
+
launchAgentPaths,
|
|
3051
|
+
opts,
|
|
3052
|
+
wipeData,
|
|
3053
|
+
wipeImages
|
|
3054
|
+
});
|
|
3055
|
+
await confirmReset({ opts, region, wipeData });
|
|
3056
|
+
const input = buildResetInput({
|
|
3057
|
+
opts,
|
|
3058
|
+
repoPath,
|
|
3059
|
+
plistExists,
|
|
3060
|
+
launchAgentPaths,
|
|
3061
|
+
wipeData,
|
|
3062
|
+
wipeImages,
|
|
3063
|
+
removeOrphans
|
|
3064
|
+
});
|
|
3065
|
+
await runResetWithSpinners({ opts, region, wipeData, input });
|
|
3066
|
+
});
|
|
3067
|
+
async function resolveResetRegion(opts) {
|
|
2986
3068
|
const region = opts.region ? opts.region : unwrap(
|
|
2987
3069
|
await p3.text({
|
|
2988
3070
|
message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
|
|
@@ -2994,51 +3076,52 @@ var resetCommand = new Command("reset").description(
|
|
|
2994
3076
|
p3.cancel(`--region ${JSON.stringify(region)} is not a valid region slug.`);
|
|
2995
3077
|
process.exit(2);
|
|
2996
3078
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
stackSkipReason = `unexpected outcome ${located.kind} from locateOrCloneRepo with allowClone=false`;
|
|
3029
|
-
break;
|
|
3030
|
-
}
|
|
3031
|
-
} else {
|
|
3032
|
-
stackSkipReason = "--skip-stack";
|
|
3033
|
-
}
|
|
3034
|
-
let runningContainers = null;
|
|
3035
|
-
if (repoPath) {
|
|
3036
|
-
runningContainers = await composePs({
|
|
3037
|
-
files: resolveComposeFiles(repoPath, opts.composeFile),
|
|
3038
|
-
cwd: repoPath,
|
|
3039
|
-
...opts.envFile ? { envFile: opts.envFile } : {}
|
|
3040
|
-
});
|
|
3079
|
+
return region;
|
|
3080
|
+
}
|
|
3081
|
+
async function locateResetRepo(opts, owner, repoName) {
|
|
3082
|
+
if (opts.skipStack) {
|
|
3083
|
+
return { stackSkipReason: "--skip-stack" };
|
|
3084
|
+
}
|
|
3085
|
+
const located = await locateOrCloneRepo({
|
|
3086
|
+
owner,
|
|
3087
|
+
name: repoName,
|
|
3088
|
+
cwd: process.cwd(),
|
|
3089
|
+
allowClone: false,
|
|
3090
|
+
...opts.repoDir ? { explicit: opts.repoDir } : {}
|
|
3091
|
+
});
|
|
3092
|
+
switch (located.kind) {
|
|
3093
|
+
case "found":
|
|
3094
|
+
return { repoPath: located.path };
|
|
3095
|
+
case "explicit-not-a-node-repo":
|
|
3096
|
+
p3.cancel(
|
|
3097
|
+
`--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.`
|
|
3098
|
+
);
|
|
3099
|
+
return process.exit(2);
|
|
3100
|
+
case "clone-disallowed":
|
|
3101
|
+
return { stackSkipReason: `no checkout found at ${process.cwd()}; pass --repo-dir to target one` };
|
|
3102
|
+
case "cloned":
|
|
3103
|
+
case "gh-not-installed":
|
|
3104
|
+
case "clone-failed":
|
|
3105
|
+
return {
|
|
3106
|
+
stackSkipReason: `unexpected outcome ${located.kind} from locateOrCloneRepo with allowClone=false`
|
|
3107
|
+
};
|
|
3108
|
+
default:
|
|
3109
|
+
return assertNever();
|
|
3041
3110
|
}
|
|
3111
|
+
}
|
|
3112
|
+
function renderSnapshotNote(args) {
|
|
3113
|
+
const {
|
|
3114
|
+
region,
|
|
3115
|
+
repoPath,
|
|
3116
|
+
stackSkipReason,
|
|
3117
|
+
runningContainers,
|
|
3118
|
+
plistExists,
|
|
3119
|
+
keyFileExists,
|
|
3120
|
+
launchAgentPaths,
|
|
3121
|
+
opts,
|
|
3122
|
+
wipeData,
|
|
3123
|
+
wipeImages
|
|
3124
|
+
} = args;
|
|
3042
3125
|
let runningContainersLabel;
|
|
3043
3126
|
if (runningContainers === null) {
|
|
3044
3127
|
runningContainersLabel = pc2.dim("unknown");
|
|
@@ -3061,6 +3144,9 @@ var resetCommand = new Command("reset").description(
|
|
|
3061
3144
|
].join("\n"),
|
|
3062
3145
|
"Snapshot"
|
|
3063
3146
|
);
|
|
3147
|
+
}
|
|
3148
|
+
async function confirmReset(args) {
|
|
3149
|
+
const { opts, region, wipeData } = args;
|
|
3064
3150
|
if (wipeData && !opts.dryRun) {
|
|
3065
3151
|
unwrap(
|
|
3066
3152
|
await p3.text({
|
|
@@ -3068,7 +3154,9 @@ var resetCommand = new Command("reset").description(
|
|
|
3068
3154
|
validate: (v) => v === region ? void 0 : `must match the region label exactly`
|
|
3069
3155
|
})
|
|
3070
3156
|
);
|
|
3071
|
-
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (!opts.dryRun && !opts.yes) {
|
|
3072
3160
|
const cont = unwrap(
|
|
3073
3161
|
await p3.confirm({
|
|
3074
3162
|
message: "Proceed with reset? (volumes will be preserved)",
|
|
@@ -3080,6 +3168,9 @@ var resetCommand = new Command("reset").description(
|
|
|
3080
3168
|
process.exit(0);
|
|
3081
3169
|
}
|
|
3082
3170
|
}
|
|
3171
|
+
}
|
|
3172
|
+
function buildResetInput(args) {
|
|
3173
|
+
const { opts, repoPath, plistExists, launchAgentPaths, wipeData, wipeImages, removeOrphans } = args;
|
|
3083
3174
|
const stack = repoPath ? {
|
|
3084
3175
|
repoPath,
|
|
3085
3176
|
composeFiles: resolveComposeFiles(repoPath, opts.composeFile),
|
|
@@ -3093,11 +3184,20 @@ var resetCommand = new Command("reset").description(
|
|
|
3093
3184
|
keepKeyFile: opts.keepKeyFile ?? false
|
|
3094
3185
|
} : void 0;
|
|
3095
3186
|
const dockerLogoutInput = !opts.skipDockerLogout ? { registry: opts.registry ?? GHCR_REGISTRY } : void 0;
|
|
3187
|
+
return {
|
|
3188
|
+
...stack ? { stack } : {},
|
|
3189
|
+
...launchAgent ? { launchAgent } : {},
|
|
3190
|
+
...dockerLogoutInput ? { dockerLogout: dockerLogoutInput } : {},
|
|
3191
|
+
dryRun: opts.dryRun ?? false
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
async function runResetWithSpinners(args) {
|
|
3195
|
+
const { opts, region, wipeData, input } = args;
|
|
3096
3196
|
const phaseSpins = /* @__PURE__ */ new Map();
|
|
3097
3197
|
const actingPhases = [];
|
|
3098
|
-
if (stack && !
|
|
3099
|
-
if (launchAgent && !
|
|
3100
|
-
if (
|
|
3198
|
+
if (input.stack && !input.dryRun) actingPhases.push(RESET_PHASES.STOP_STACK);
|
|
3199
|
+
if (input.launchAgent && !input.dryRun) actingPhases.push(RESET_PHASES.LAUNCH_AGENT);
|
|
3200
|
+
if (input.dockerLogout && !input.dryRun) actingPhases.push(RESET_PHASES.DOCKER_LOGOUT);
|
|
3101
3201
|
for (const name of actingPhases) {
|
|
3102
3202
|
const s = p3.spinner();
|
|
3103
3203
|
s.start(`${name}\u2026`);
|
|
@@ -3113,30 +3213,27 @@ var resetCommand = new Command("reset").description(
|
|
|
3113
3213
|
p3.log.info(line);
|
|
3114
3214
|
}
|
|
3115
3215
|
};
|
|
3116
|
-
const report = await runReset(
|
|
3117
|
-
{
|
|
3118
|
-
...stack ? { stack } : {},
|
|
3119
|
-
...launchAgent ? { launchAgent } : {},
|
|
3120
|
-
...dockerLogoutInput ? { dockerLogout: dockerLogoutInput } : {},
|
|
3121
|
-
dryRun: opts.dryRun ?? false
|
|
3122
|
-
},
|
|
3123
|
-
{ ...DEFAULT_DEPS, onPhase: renderPhase }
|
|
3124
|
-
);
|
|
3216
|
+
const report = await runReset(input, { ...DEFAULT_DEPS, onPhase: renderPhase });
|
|
3125
3217
|
for (const [, spin] of phaseSpins) spin.stop(pc2.dim("\u2014 skipped"));
|
|
3218
|
+
reportResetOutcome({ report, opts, wipeData, region });
|
|
3219
|
+
}
|
|
3220
|
+
function reportResetOutcome(args) {
|
|
3221
|
+
const { report, opts, wipeData, region } = args;
|
|
3126
3222
|
const failed = report.phases.filter((ph) => ph.status === "fail").length;
|
|
3127
3223
|
if (failed > 0) {
|
|
3128
3224
|
p3.outro(pc2.red(`${failed} step${failed === 1 ? "" : "s"} failed.`));
|
|
3129
3225
|
process.exit(1);
|
|
3130
|
-
}
|
|
3226
|
+
}
|
|
3227
|
+
if (opts.dryRun) {
|
|
3131
3228
|
p3.outro(pc2.cyan("Dry run complete. Re-run without --dry-run to apply."));
|
|
3132
|
-
|
|
3133
|
-
p3.outro(
|
|
3134
|
-
pc2.green(
|
|
3135
|
-
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.`
|
|
3136
|
-
)
|
|
3137
|
-
);
|
|
3229
|
+
return;
|
|
3138
3230
|
}
|
|
3139
|
-
|
|
3231
|
+
p3.outro(
|
|
3232
|
+
pc2.green(
|
|
3233
|
+
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.`
|
|
3234
|
+
)
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3140
3237
|
|
|
3141
3238
|
// src/lib/cosign.ts
|
|
3142
3239
|
var COSIGN_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
|
|
@@ -4577,7 +4674,7 @@ async function looksLikeRegionsRepo(dir) {
|
|
|
4577
4674
|
}
|
|
4578
4675
|
|
|
4579
4676
|
// src/cli.ts
|
|
4580
|
-
var VERSION = "0.10.
|
|
4677
|
+
var VERSION = "0.10.8";
|
|
4581
4678
|
var program = new Command();
|
|
4582
4679
|
program.name("create-op-node").description(
|
|
4583
4680
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|