create-op-node 0.10.5 → 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 +439 -311
- 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;
|
|
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,
|
|
@@ -1989,6 +2032,10 @@ async function locateOrCloneRepo(input) {
|
|
|
1989
2032
|
return { kind: "cloned", path: target };
|
|
1990
2033
|
}
|
|
1991
2034
|
|
|
2035
|
+
// src/lib/assert.ts
|
|
2036
|
+
function assertNever(_value) {
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1992
2039
|
// src/commands/bootstrap.ts
|
|
1993
2040
|
var PUBLIC_PROFILES = ["public"];
|
|
1994
2041
|
var LOCAL_PROFILES = [];
|
|
@@ -2036,6 +2083,33 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2036
2083
|
).default(false)
|
|
2037
2084
|
).addOption(new Option("-y, --yes", "Skip confirmation prompts").default(false)).action(async (opts) => {
|
|
2038
2085
|
p3.intro(pc2.bgCyan(pc2.black(" create-op-node bootstrap ")));
|
|
2086
|
+
const nodeType = await selectNodeType(opts);
|
|
2087
|
+
const region = await resolveRegion2(opts);
|
|
2088
|
+
const owner = opts.owner ?? "OpusPopuli";
|
|
2089
|
+
const repoName = `opuspopuli-node-${region}`;
|
|
2090
|
+
const composeFile = opts.composeFile ?? defaultComposeFiles(opts);
|
|
2091
|
+
const llmModelChoice = opts.llmModel ?? await selectLlmModel(opts);
|
|
2092
|
+
const [embeddingModel, llmModel] = resolveModels({
|
|
2093
|
+
llmModel: llmModelChoice,
|
|
2094
|
+
...opts.embeddingModel !== void 0 ? { embeddingModel: opts.embeddingModel } : {}
|
|
2095
|
+
});
|
|
2096
|
+
await runSystemChecksPhase();
|
|
2097
|
+
await runBrewPhase(opts);
|
|
2098
|
+
await ensureGhAuth();
|
|
2099
|
+
await promptTailscaleSignin();
|
|
2100
|
+
const repoPath = await locateRegionRepoPhase(opts, owner, repoName);
|
|
2101
|
+
await unlockKeychainPhase();
|
|
2102
|
+
const secrets = await collectSecretsPhase({ region, nodeType, opts });
|
|
2103
|
+
await runLaunchAgentPhase({ opts, secrets, llmModel, embeddingModel });
|
|
2104
|
+
await installWrapperPhase({ repoPath, region, promptServiceUrl: secrets.promptServiceUrl });
|
|
2105
|
+
await loginGhcrPhase();
|
|
2106
|
+
await runOllamaPhase({ opts, embeddingModel, llmModel });
|
|
2107
|
+
await runStackPhase({ opts, repoPath, region, composeFile, secrets, llmModel, embeddingModel });
|
|
2108
|
+
});
|
|
2109
|
+
function defaultComposeFiles(opts) {
|
|
2110
|
+
return opts.localOnly ? ["docker-compose-prod.yml"] : ["docker-compose-prod.yml", "docker-compose-backup.yml"];
|
|
2111
|
+
}
|
|
2112
|
+
async function selectNodeType(opts) {
|
|
2039
2113
|
const nodeType = opts.nodeType ? opts.nodeType : unwrap(
|
|
2040
2114
|
await p3.select({
|
|
2041
2115
|
message: "What kind of node are you provisioning?",
|
|
@@ -2062,22 +2136,18 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2062
2136
|
);
|
|
2063
2137
|
process.exit(1);
|
|
2064
2138
|
}
|
|
2065
|
-
|
|
2139
|
+
return nodeType;
|
|
2140
|
+
}
|
|
2141
|
+
async function resolveRegion2(opts) {
|
|
2142
|
+
return opts.region ? opts.region : unwrap(
|
|
2066
2143
|
await p3.text({
|
|
2067
2144
|
message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
|
|
2068
2145
|
placeholder: "us-ca",
|
|
2069
2146
|
validate: (v) => /^[a-z0-9-]{2,32}$/.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
|
|
2070
2147
|
})
|
|
2071
2148
|
);
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
const composeFileDefault = opts.localOnly ? ["docker-compose-prod.yml"] : ["docker-compose-prod.yml", "docker-compose-backup.yml"];
|
|
2075
|
-
const composeFile = opts.composeFile ?? composeFileDefault;
|
|
2076
|
-
const llmModelChoice = opts.llmModel ?? await selectLlmModel(opts);
|
|
2077
|
-
const [embeddingModel, llmModel] = resolveModels({
|
|
2078
|
-
llmModel: llmModelChoice,
|
|
2079
|
-
...opts.embeddingModel !== void 0 ? { embeddingModel: opts.embeddingModel } : {}
|
|
2080
|
-
});
|
|
2149
|
+
}
|
|
2150
|
+
async function runSystemChecksPhase() {
|
|
2081
2151
|
const sysSpin = p3.spinner();
|
|
2082
2152
|
sysSpin.start("Inspecting macOS\u2026");
|
|
2083
2153
|
const snap = await inspectSystem();
|
|
@@ -2122,54 +2192,55 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2122
2192
|
if (!r.ok) p3.note(`${pc2.red("\u2717")} ${r.reason}`, "pmset failed");
|
|
2123
2193
|
}
|
|
2124
2194
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
"
|
|
2137
|
-
)
|
|
2138
|
-
|
|
2139
|
-
}
|
|
2140
|
-
const brewSpin = p3.spinner();
|
|
2141
|
-
brewSpin.start("Installing Studio packages\u2026 (~5\u201315 min first run)");
|
|
2142
|
-
const report = await installPackages(STUDIO_PACKAGES, (pkg, status) => {
|
|
2143
|
-
brewSpin.message(`${status}: ${pkg.name}`);
|
|
2144
|
-
});
|
|
2145
|
-
brewSpin.stop(
|
|
2146
|
-
report.failed.length === 0 ? pc2.green(
|
|
2147
|
-
`\u2713 ${report.installed.length} installed, ${report.alreadyPresent.length} already present`
|
|
2148
|
-
) : pc2.yellow(
|
|
2149
|
-
`\u26A0 ${report.installed.length} installed, ${report.alreadyPresent.length} present, ${report.failed.length} failed`
|
|
2150
|
-
)
|
|
2195
|
+
}
|
|
2196
|
+
async function runBrewPhase(opts) {
|
|
2197
|
+
if (opts.skipBrew) return;
|
|
2198
|
+
const brewInfo = await detectBrew();
|
|
2199
|
+
if (!brewInfo.installed) {
|
|
2200
|
+
p3.note(
|
|
2201
|
+
[
|
|
2202
|
+
`Homebrew is not installed. Open another shell and run:`,
|
|
2203
|
+
"",
|
|
2204
|
+
pc2.cyan(HOMEBREW_INSTALL_COMMAND),
|
|
2205
|
+
"",
|
|
2206
|
+
pc2.dim("Then come back and press Enter.")
|
|
2207
|
+
].join("\n"),
|
|
2208
|
+
"Manual step"
|
|
2151
2209
|
);
|
|
2152
|
-
|
|
2153
|
-
p3.note(
|
|
2154
|
-
[
|
|
2155
|
-
report.failed.map((f) => `${pc2.red("\u2022")} ${f.pkg.name}: ${f.reason}`).join("\n"),
|
|
2156
|
-
"",
|
|
2157
|
-
pc2.dim("A partial install usually fails downstream (Docker / Ollama / compose)."),
|
|
2158
|
-
pc2.dim("Install the failed packages manually and re-run with --skip-brew.")
|
|
2159
|
-
].join("\n"),
|
|
2160
|
-
"Brew failures"
|
|
2161
|
-
);
|
|
2162
|
-
const cont = unwrap(
|
|
2163
|
-
await p3.confirm({
|
|
2164
|
-
message: "Continue anyway? (Default no \u2014 recommended to fix and re-run.)",
|
|
2165
|
-
initialValue: false
|
|
2166
|
-
})
|
|
2167
|
-
);
|
|
2168
|
-
if (!cont) process.exit(1);
|
|
2169
|
-
}
|
|
2210
|
+
unwrap(await p3.confirm({ message: "Homebrew installed?", initialValue: true }));
|
|
2170
2211
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2212
|
+
const brewSpin = p3.spinner();
|
|
2213
|
+
brewSpin.start("Installing Studio packages\u2026 (~5\u201315 min first run)");
|
|
2214
|
+
const report = await installPackages(STUDIO_PACKAGES, (pkg, status) => {
|
|
2215
|
+
brewSpin.message(`${status}: ${pkg.name}`);
|
|
2216
|
+
});
|
|
2217
|
+
brewSpin.stop(
|
|
2218
|
+
report.failed.length === 0 ? pc2.green(
|
|
2219
|
+
`\u2713 ${report.installed.length} installed, ${report.alreadyPresent.length} already present`
|
|
2220
|
+
) : pc2.yellow(
|
|
2221
|
+
`\u26A0 ${report.installed.length} installed, ${report.alreadyPresent.length} present, ${report.failed.length} failed`
|
|
2222
|
+
)
|
|
2223
|
+
);
|
|
2224
|
+
if (report.failed.length > 0) {
|
|
2225
|
+
p3.note(
|
|
2226
|
+
[
|
|
2227
|
+
report.failed.map((f) => `${pc2.red("\u2022")} ${f.pkg.name}: ${f.reason}`).join("\n"),
|
|
2228
|
+
"",
|
|
2229
|
+
pc2.dim("A partial install usually fails downstream (Docker / Ollama / compose)."),
|
|
2230
|
+
pc2.dim("Install the failed packages manually and re-run with --skip-brew.")
|
|
2231
|
+
].join("\n"),
|
|
2232
|
+
"Brew failures"
|
|
2233
|
+
);
|
|
2234
|
+
const cont = unwrap(
|
|
2235
|
+
await p3.confirm({
|
|
2236
|
+
message: "Continue anyway? (Default no \u2014 recommended to fix and re-run.)",
|
|
2237
|
+
initialValue: false
|
|
2238
|
+
})
|
|
2239
|
+
);
|
|
2240
|
+
if (!cont) process.exit(1);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
async function locateRegionRepoPhase(opts, owner, repoName) {
|
|
2173
2244
|
const repoSpin = p3.spinner();
|
|
2174
2245
|
repoSpin.start("Locating your region node repo\u2026");
|
|
2175
2246
|
const located = await locateOrCloneRepo({
|
|
@@ -2187,6 +2258,9 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2187
2258
|
repoSpin.stop(
|
|
2188
2259
|
located.kind === "cloned" ? pc2.green(`\u2713 Cloned ${owner}/${repoName} to ${repoPath}`) : pc2.green(`\u2713 Found region repo at ${repoPath}`)
|
|
2189
2260
|
);
|
|
2261
|
+
return repoPath;
|
|
2262
|
+
}
|
|
2263
|
+
async function unlockKeychainPhase() {
|
|
2190
2264
|
const keychain = await detectKeychain();
|
|
2191
2265
|
if (!keychain.available) {
|
|
2192
2266
|
p3.cancel(
|
|
@@ -2219,6 +2293,9 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2219
2293
|
}
|
|
2220
2294
|
p3.note(`${pc2.green("\u2713")} Keychain unlocked for this session.`, "Keychain");
|
|
2221
2295
|
}
|
|
2296
|
+
}
|
|
2297
|
+
async function collectCoreSecrets(args) {
|
|
2298
|
+
const { region, opts } = args;
|
|
2222
2299
|
const pgsodiumKey = await loadSecret({
|
|
2223
2300
|
region,
|
|
2224
2301
|
account: "pgsodium-root-key",
|
|
@@ -2272,6 +2349,18 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2272
2349
|
validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
|
|
2273
2350
|
generate: generateDashboardPassword
|
|
2274
2351
|
});
|
|
2352
|
+
return {
|
|
2353
|
+
pgsodiumKey,
|
|
2354
|
+
tunnelToken,
|
|
2355
|
+
postgresPassword,
|
|
2356
|
+
jwtSecret,
|
|
2357
|
+
supabaseAnonKey,
|
|
2358
|
+
supabaseServiceRoleKey,
|
|
2359
|
+
dashboardPassword
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
async function collectPromptServiceSecrets(args) {
|
|
2363
|
+
const { region, nodeType, opts } = args;
|
|
2275
2364
|
if (nodeType === "region-with-prompts") {
|
|
2276
2365
|
await loadSecret({
|
|
2277
2366
|
region,
|
|
@@ -2315,6 +2404,9 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2315
2404
|
);
|
|
2316
2405
|
process.exit(1);
|
|
2317
2406
|
}
|
|
2407
|
+
return promptServiceUrl;
|
|
2408
|
+
}
|
|
2409
|
+
async function resolveSupabaseUrl(opts) {
|
|
2318
2410
|
let supabaseUrl;
|
|
2319
2411
|
if (opts.supabaseUrl) {
|
|
2320
2412
|
supabaseUrl = opts.supabaseUrl;
|
|
@@ -2338,32 +2430,44 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2338
2430
|
);
|
|
2339
2431
|
process.exit(1);
|
|
2340
2432
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2433
|
+
return supabaseUrl;
|
|
2434
|
+
}
|
|
2435
|
+
async function collectSecretsPhase(args) {
|
|
2436
|
+
const core = await collectCoreSecrets({ region: args.region, opts: args.opts });
|
|
2437
|
+
const promptServiceUrl = await collectPromptServiceSecrets(args);
|
|
2438
|
+
const supabaseUrl = await resolveSupabaseUrl(args.opts);
|
|
2439
|
+
return { ...core, promptServiceUrl, supabaseUrl };
|
|
2440
|
+
}
|
|
2441
|
+
async function runLaunchAgentPhase(args) {
|
|
2442
|
+
const { opts, secrets, llmModel, embeddingModel } = args;
|
|
2443
|
+
if (opts.skipLaunchAgent) return;
|
|
2444
|
+
const laSpin = p3.spinner();
|
|
2445
|
+
laSpin.start("Writing pgsodium key file + LaunchAgent plist\u2026");
|
|
2446
|
+
const la = await setupLaunchAgent({
|
|
2447
|
+
pgsodiumKey: secrets.pgsodiumKey,
|
|
2448
|
+
...secrets.tunnelToken !== void 0 ? { tunnelToken: secrets.tunnelToken } : {},
|
|
2449
|
+
llmModel,
|
|
2450
|
+
embeddingModel,
|
|
2451
|
+
postgresPassword: secrets.postgresPassword,
|
|
2452
|
+
jwtSecret: secrets.jwtSecret,
|
|
2453
|
+
supabaseAnonKey: secrets.supabaseAnonKey,
|
|
2454
|
+
supabaseServiceRoleKey: secrets.supabaseServiceRoleKey,
|
|
2455
|
+
dashboardPassword: secrets.dashboardPassword,
|
|
2456
|
+
supabaseUrl: secrets.supabaseUrl
|
|
2457
|
+
});
|
|
2458
|
+
if (!la.ok) {
|
|
2459
|
+
laSpin.stop(pc2.red(`\u2717 LaunchAgent step ${la.step} failed.`));
|
|
2460
|
+
p3.cancel(la.reason ?? "LaunchAgent setup failed.");
|
|
2461
|
+
process.exit(1);
|
|
2366
2462
|
}
|
|
2463
|
+
laSpin.stop(
|
|
2464
|
+
pc2.green(
|
|
2465
|
+
`\u2713 LaunchAgent loaded (${la.paths.plistFile})${opts.localOnly ? " \u2014 local-only mode, no TUNNEL_TOKEN set" : ""}.`
|
|
2466
|
+
)
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
async function installWrapperPhase(args) {
|
|
2470
|
+
const { repoPath, region, promptServiceUrl } = args;
|
|
2367
2471
|
const wrapperSpin = p3.spinner();
|
|
2368
2472
|
wrapperSpin.start("Installing bin/op-compose wrapper\u2026");
|
|
2369
2473
|
const wrapper = await installOpComposeWrapper({ repoDir: repoPath, region, promptServiceUrl });
|
|
@@ -2373,6 +2477,8 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2373
2477
|
process.exit(1);
|
|
2374
2478
|
}
|
|
2375
2479
|
wrapperSpin.stop(pc2.green(`\u2713 Installed ${wrapper.path} (mode 0755).`));
|
|
2480
|
+
}
|
|
2481
|
+
async function loginGhcrPhase() {
|
|
2376
2482
|
const ghcrSpin = p3.spinner();
|
|
2377
2483
|
ghcrSpin.start("Authenticating Docker to ghcr.io\u2026");
|
|
2378
2484
|
const ghcr = await loginToGhcr();
|
|
@@ -2382,45 +2488,66 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2382
2488
|
process.exit(1);
|
|
2383
2489
|
}
|
|
2384
2490
|
ghcrSpin.stop(pc2.green("\u2713 Logged in to ghcr.io."));
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
if (!olHealth.reachable) {
|
|
2402
|
-
olSpin.stop(
|
|
2403
|
-
pc2.yellow("\u26A0 Ollama service started but daemon not yet answering on :11434.")
|
|
2404
|
-
);
|
|
2405
|
-
p3.cancel(
|
|
2406
|
-
"Give it another few seconds, then re-run with --skip-brew --skip-launch-agent."
|
|
2407
|
-
);
|
|
2408
|
-
process.exit(1);
|
|
2409
|
-
}
|
|
2491
|
+
}
|
|
2492
|
+
async function runOllamaPhase(args) {
|
|
2493
|
+
const { opts, embeddingModel, llmModel } = args;
|
|
2494
|
+
if (opts.skipOllama) return;
|
|
2495
|
+
const olSpin = p3.spinner();
|
|
2496
|
+
olSpin.start(`Pulling + warming Ollama models\u2026 (${estimatedPullTime(llmModel)})`);
|
|
2497
|
+
let olHealth = await checkOllamaHealth();
|
|
2498
|
+
if (!olHealth.reachable) {
|
|
2499
|
+
olSpin.message("Ollama not reachable \u2014 running `brew services start ollama`\u2026");
|
|
2500
|
+
const start = await startOllamaService();
|
|
2501
|
+
if (!start.ok) {
|
|
2502
|
+
olSpin.stop(pc2.red("\u2717 Couldn't start Ollama service."));
|
|
2503
|
+
p3.cancel(
|
|
2504
|
+
`${start.reason ?? "unknown"} \u2014 start it manually with \`brew services start ollama\` and re-run with --skip-brew --skip-launch-agent.`
|
|
2505
|
+
);
|
|
2506
|
+
process.exit(1);
|
|
2410
2507
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
p3.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
|
|
2508
|
+
await new Promise((res) => setTimeout(res, 3e3));
|
|
2509
|
+
olHealth = await checkOllamaHealth();
|
|
2510
|
+
if (!olHealth.reachable) {
|
|
2511
|
+
olSpin.stop(
|
|
2512
|
+
pc2.yellow("\u26A0 Ollama service started but daemon not yet answering on :11434.")
|
|
2513
|
+
);
|
|
2514
|
+
p3.cancel(
|
|
2515
|
+
"Give it another few seconds, then re-run with --skip-brew --skip-launch-agent."
|
|
2516
|
+
);
|
|
2517
|
+
process.exit(1);
|
|
2422
2518
|
}
|
|
2423
2519
|
}
|
|
2520
|
+
const modelReport = await setupModels([embeddingModel, llmModel], (model, status) => {
|
|
2521
|
+
olSpin.message(`${status}: ${model}`);
|
|
2522
|
+
});
|
|
2523
|
+
olSpin.stop(
|
|
2524
|
+
modelReport.failed.length === 0 ? pc2.green(
|
|
2525
|
+
`\u2713 ${modelReport.pulled.length} pulled, ${modelReport.alreadyPresent.length} present, ${modelReport.warmed.length} warmed`
|
|
2526
|
+
) : pc2.yellow(`\u26A0 ${modelReport.failed.length} model pull(s) failed`)
|
|
2527
|
+
);
|
|
2528
|
+
const probe = await probeHostDockerInternal();
|
|
2529
|
+
if (!probe.ok) {
|
|
2530
|
+
p3.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
function buildComposeEnv(args) {
|
|
2534
|
+
const { secrets, llmModel, embeddingModel } = args;
|
|
2535
|
+
return {
|
|
2536
|
+
PGSODIUM_ROOT_KEY: secrets.pgsodiumKey,
|
|
2537
|
+
POSTGRES_PASSWORD: secrets.postgresPassword,
|
|
2538
|
+
JWT_SECRET: secrets.jwtSecret,
|
|
2539
|
+
SUPABASE_ANON_KEY: secrets.supabaseAnonKey,
|
|
2540
|
+
SUPABASE_SERVICE_ROLE_KEY: secrets.supabaseServiceRoleKey,
|
|
2541
|
+
DASHBOARD_PASSWORD: secrets.dashboardPassword,
|
|
2542
|
+
SUPABASE_URL: secrets.supabaseUrl,
|
|
2543
|
+
AUTH_JWT_SECRET: process.env["AUTH_JWT_SECRET"] ?? secrets.jwtSecret,
|
|
2544
|
+
...secrets.tunnelToken !== void 0 ? { TUNNEL_TOKEN: secrets.tunnelToken } : {},
|
|
2545
|
+
...llmModel ? { LLM_MODEL: llmModel } : {},
|
|
2546
|
+
...embeddingModel ? { EMBEDDINGS_MODEL: embeddingModel } : {}
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
async function runStackPhase(args) {
|
|
2550
|
+
const { opts, repoPath, region, composeFile, secrets, llmModel, embeddingModel } = args;
|
|
2424
2551
|
if (opts.skipStack) {
|
|
2425
2552
|
const profileFlag = opts.localOnly ? "" : "--profile public ";
|
|
2426
2553
|
p3.outro(
|
|
@@ -2431,19 +2558,7 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2431
2558
|
return;
|
|
2432
2559
|
}
|
|
2433
2560
|
const composeFiles = resolveComposeFiles(repoPath, composeFile);
|
|
2434
|
-
const composeEnv = {
|
|
2435
|
-
PGSODIUM_ROOT_KEY: pgsodiumKey,
|
|
2436
|
-
POSTGRES_PASSWORD: postgresPassword,
|
|
2437
|
-
JWT_SECRET: jwtSecret,
|
|
2438
|
-
SUPABASE_ANON_KEY: supabaseAnonKey,
|
|
2439
|
-
SUPABASE_SERVICE_ROLE_KEY: supabaseServiceRoleKey,
|
|
2440
|
-
DASHBOARD_PASSWORD: dashboardPassword,
|
|
2441
|
-
SUPABASE_URL: supabaseUrl,
|
|
2442
|
-
AUTH_JWT_SECRET: process.env["AUTH_JWT_SECRET"] ?? jwtSecret,
|
|
2443
|
-
...tunnelToken !== void 0 ? { TUNNEL_TOKEN: tunnelToken } : {},
|
|
2444
|
-
...llmModel ? { LLM_MODEL: llmModel } : {},
|
|
2445
|
-
...embeddingModel ? { EMBEDDINGS_MODEL: embeddingModel } : {}
|
|
2446
|
-
};
|
|
2561
|
+
const composeEnv = buildComposeEnv({ secrets, llmModel, embeddingModel });
|
|
2447
2562
|
const composeOpts = {
|
|
2448
2563
|
files: composeFiles,
|
|
2449
2564
|
cwd: repoPath,
|
|
@@ -2489,9 +2604,25 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2489
2604
|
healthSpin.message(`${healthy}/${running} healthy`);
|
|
2490
2605
|
}
|
|
2491
2606
|
});
|
|
2607
|
+
healthSpin.stop(healthOutcomeHeadline(outcome));
|
|
2608
|
+
finishHealthOutcome(outcome, { region, opts, composeFiles });
|
|
2609
|
+
}
|
|
2610
|
+
function healthOutcomeHeadline(outcome) {
|
|
2611
|
+
switch (outcome.kind) {
|
|
2612
|
+
case "healthy":
|
|
2613
|
+
return pc2.green(`\u2713 All ${outcome.snapshots.length} containers healthy.`);
|
|
2614
|
+
case "unhealthy":
|
|
2615
|
+
return pc2.red(`\u2717 ${outcome.problem}`);
|
|
2616
|
+
case "timeout":
|
|
2617
|
+
return pc2.yellow("\u26A0 Timed out waiting for all containers to report healthy.");
|
|
2618
|
+
default:
|
|
2619
|
+
return assertNever();
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function finishHealthOutcome(outcome, args) {
|
|
2623
|
+
const { region, opts, composeFiles } = args;
|
|
2492
2624
|
switch (outcome.kind) {
|
|
2493
2625
|
case "healthy":
|
|
2494
|
-
healthSpin.stop(pc2.green(`\u2713 All ${outcome.snapshots.length} containers healthy.`));
|
|
2495
2626
|
if (opts.localOnly) {
|
|
2496
2627
|
p3.outro(
|
|
2497
2628
|
pc2.cyan(
|
|
@@ -2511,7 +2642,6 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2511
2642
|
}
|
|
2512
2643
|
return;
|
|
2513
2644
|
case "unhealthy":
|
|
2514
|
-
healthSpin.stop(pc2.red(`\u2717 ${outcome.problem}`));
|
|
2515
2645
|
p3.note(
|
|
2516
2646
|
outcome.snapshots.map((s) => ` ${kvHealth(s.health)} ${s.name} (${s.state})`).join("\n"),
|
|
2517
2647
|
"Container state"
|
|
@@ -2522,7 +2652,6 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2522
2652
|
process.exit(1);
|
|
2523
2653
|
// eslint-disable-next-line no-fallthrough
|
|
2524
2654
|
case "timeout":
|
|
2525
|
-
healthSpin.stop(pc2.yellow("\u26A0 Timed out waiting for all containers to report healthy."));
|
|
2526
2655
|
p3.note(
|
|
2527
2656
|
outcome.snapshots.map((s) => ` ${kvHealth(s.health)} ${s.name} (${s.state})`).join("\n"),
|
|
2528
2657
|
"Container state at timeout"
|
|
@@ -2532,11 +2661,10 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2532
2661
|
);
|
|
2533
2662
|
process.exit(1);
|
|
2534
2663
|
// eslint-disable-next-line no-fallthrough
|
|
2535
|
-
default:
|
|
2664
|
+
default:
|
|
2536
2665
|
return;
|
|
2537
|
-
}
|
|
2538
2666
|
}
|
|
2539
|
-
}
|
|
2667
|
+
}
|
|
2540
2668
|
function resolveComposeFiles(repoPath, composeFile) {
|
|
2541
2669
|
const inputs = composeFile ?? ["docker-compose-prod.yml"];
|
|
2542
2670
|
return inputs.map((f) => f.startsWith("/") ? f : join(repoPath, f));
|
|
@@ -4492,7 +4620,7 @@ async function looksLikeRegionsRepo(dir) {
|
|
|
4492
4620
|
}
|
|
4493
4621
|
|
|
4494
4622
|
// src/cli.ts
|
|
4495
|
-
var VERSION = "0.10.
|
|
4623
|
+
var VERSION = "0.10.7";
|
|
4496
4624
|
var program = new Command();
|
|
4497
4625
|
program.name("create-op-node").description(
|
|
4498
4626
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|