create-op-node 0.10.6 → 0.10.7
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 +220 -177
- 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
|
-
};
|
|
818
|
+
return { fullName: newRepoFull, defaultBranch: "main" };
|
|
789
819
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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}`));
|
|
820
|
-
}
|
|
821
|
-
let pr = null;
|
|
822
|
-
if (publicConfig) {
|
|
823
|
-
const branch = `init/region-${region}-${isoStampUtc(/* @__PURE__ */ new Date())}`;
|
|
824
|
-
const setupSpin = p3.spinner();
|
|
825
|
-
setupSpin.start(`Writing prod.tfvars on branch ${branch}\u2026`);
|
|
826
|
-
try {
|
|
827
|
-
await createBranch({
|
|
828
|
-
token: ghToken,
|
|
829
|
-
repo: newRepoFull,
|
|
830
|
-
branch,
|
|
831
|
-
fromBranch: created.defaultBranch
|
|
832
|
-
});
|
|
833
|
-
const tfvars = renderProdTfvars({
|
|
834
|
-
project: opts.project ?? "opuspopuli",
|
|
835
|
-
domain: publicConfig.domain
|
|
836
|
-
});
|
|
837
|
-
await commitFile({
|
|
838
|
-
token: ghToken,
|
|
839
|
-
repo: newRepoFull,
|
|
840
|
-
branch,
|
|
841
|
-
path: "infra/cloudflare/environments/prod.tfvars",
|
|
842
|
-
content: tfvars,
|
|
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;
|
|
988
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);
|
|
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)?",
|
|
@@ -4577,7 +4620,7 @@ async function looksLikeRegionsRepo(dir) {
|
|
|
4577
4620
|
}
|
|
4578
4621
|
|
|
4579
4622
|
// src/cli.ts
|
|
4580
|
-
var VERSION = "0.10.
|
|
4623
|
+
var VERSION = "0.10.7";
|
|
4581
4624
|
var program = new Command();
|
|
4582
4625
|
program.name("create-op-node").description(
|
|
4583
4626
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|