@launchsecure/launch-kit 0.0.35 → 0.0.37
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/chart-client/assets/index-DJrjyXbN.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-8eSXr3Ez.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/index-4K0t2WrZ.css +1 -0
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-BiVx0WO_.js → _baseUniq-Cn5TyL9s.js} +1 -1
- package/dist/deck-client/assets/{arc-DGMkiEzS.js → arc-D61amKYu.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-Y2WRmHtk.js → architectureDiagram-Q4EWVU46-CpKrvC2W.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-_Lbfu5BQ.js → blockDiagram-DXYQGD6D-Yj5OjxvG.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CTqpYTBX.js → c4Diagram-AHTNJAMY-BIR810Tv.js} +1 -1
- package/dist/deck-client/assets/channel-DrJz2x-n.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-liEIbPHs.js → chunk-4BX2VUAB-BeSHwGvx.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-CCc6lYvL.js → chunk-4TB4RGXK-CCqzsLpg.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-D02jJUR2.js → chunk-55IACEB6-CuW_aq4-.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-BFmGMbLD.js → chunk-EDXVE4YY-Dl35ixYh.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-6wFLOVcJ.js → chunk-FMBD7UC4-TwreZQTv.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-Bnr8RiBf.js → chunk-OYMX7WX6-Ahfw8EUo.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-Ct82MksJ.js → chunk-QZHKN3VN-DlE_zlU-.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-BXmN1diQ.js → chunk-YZCP3GAM-Dj6QWzSg.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-a3tg9w7z.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-a3tg9w7z.js +1 -0
- package/dist/deck-client/assets/clone-Dd7JBCL5.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-CmQCT-mH.js → cose-bilkent-S5V4N54A-BO1z5aOM.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-DDdSa9EX.js → dagre-KV5264BT-DVsw17fE.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-Bccks2xJ.js → diagram-5BDNPKRD-6jYs7oZk.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-CPPNgxmQ.js → diagram-G4DWMVQ6-6DbggeGE.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-KrD300pS.js → diagram-MMDJMWI5-CQtk1cSU.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-DefnLuQf.js → diagram-TYMM5635-BR-gt75b.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-DI9FfnFP.js → erDiagram-SMLLAGMA-C9qMtjdY.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-twKyd3Fx.js → flowDiagram-DWJPFMVM-CdaPhPYb.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-Wau3jhBr.js → ganttDiagram-T4ZO3ILL-BRsZWUy4.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-D9GgYXwb.js → gitGraphDiagram-UUTBAWPF-B8Z90jCj.js} +1 -1
- package/dist/deck-client/assets/{graph-BhNLzyXS.js → graph-my2Zphm4.js} +1 -1
- package/dist/deck-client/assets/index-ByqxPEgU.css +1 -0
- package/dist/deck-client/assets/{index-BtQBaQ7s.js → index-DqAoYZwV.js} +43 -42
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-TylGlSG-.js → infoDiagram-42DDH7IO-Csr9loin.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DAT8icpg.js → ishikawaDiagram-UXIWVN3A-HWdvUNFi.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-D3v_XL72.js → journeyDiagram-VCZTEJTY-CjYHG6EM.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-DNUOBiNr.js → kanban-definition-6JOO6SKY-CX3JdUu7.js} +1 -1
- package/dist/deck-client/assets/{layout-COfodgwF.js → layout-Bcucv5Gi.js} +1 -1
- package/dist/deck-client/assets/{linear-DmTsuIvK.js → linear-CUGM5FJZ.js} +1 -1
- package/dist/deck-client/assets/{min-BW1F7i1D.js → min-Dw4g5w9z.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-CErFzKWl.js → mindmap-definition-QFDTVHPH-C8oo61fg.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DW5F757o.js → pieDiagram-DEJITSTG-D2WYGkq8.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-B1S2-TfI.js → quadrantDiagram-34T5L4WZ-Vh00GISt.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-BY5BAR-5.js → requirementDiagram-MS252O5E-DxI-DFrN.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-CE1Cp9HS.js → sankeyDiagram-XADWPNL6-QgwyjasI.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-IaHnbKye.js → sequenceDiagram-FGHM5R23-DmOmD5Ni.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-CwPJm9hU.js → stateDiagram-FHFEXIEX-CRwglGg_.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BvZLEWAA.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-DVFGGSgN.js → timeline-definition-GMOUNBTQ-Dj9YGKOh.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-C1194MJi.js → vennDiagram-DHZGUBPP-xzIaOzEU.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-CEAay09T.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-hpwdFfGj.js → wardleyDiagram-NUSXRM2D-BIYYh-JZ.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-DYkotwy8.js → xychartDiagram-5P7HB3ND-Cy9EoJCh.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/cli.js +261 -26
- package/dist/server/council-entry.js +86 -2
- package/dist/server/council-serve.js +81 -2
- package/dist/server/deck-mcp-entry.js +449 -68
- package/dist/server/deck-serve.js +411 -42
- package/dist/server/init-entry.js +732 -237
- package/dist/server/orbit-entry.js +880 -144
- package/dist/server/radar-docker-init-entry.js +371 -37
- package/dist/server/rover-entry.js +108 -20
- package/package.json +1 -1
- package/scaffolds/ls-marketplace/plugins/kit/skills/deploy-check/SKILL.md +5 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/kickoff/SKILL.md +20 -4
- package/scaffolds/ls-marketplace/plugins/kit/skills/orbit/SKILL.md +27 -7
- package/dist/chart-client/assets/index-DpKO9p0s.css +0 -1
- package/dist/client/assets/index-Dv6dD2zY.css +0 -32
- package/dist/council-client/assets/index-AqQ9Sei6.css +0 -1
- package/dist/deck-client/assets/channel-DB6LxW_l.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-g944ZyG8.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-g944ZyG8.js +0 -1
- package/dist/deck-client/assets/clone-DiIRH1pI.js +0 -1
- package/dist/deck-client/assets/index-B-YQq5b5.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-DQYa2M1q.js +0 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-CHZiUbBa.js +0 -162
- /package/dist/chart-client/assets/{index-DFu2xIrM.js → index-BgUxHxwE.js} +0 -0
- /package/dist/client/assets/{index-Cbw6bVdx.js → index-CUivaQnN.js} +0 -0
- /package/dist/council-client/assets/{index-CAsmGTzg.js → index-DN8HN_5K.js} +0 -0
|
@@ -121,6 +121,12 @@ async function runSecretsPull(opts) {
|
|
|
121
121
|
}
|
|
122
122
|
const envsRaw = await callMcp(profile, "environments_list", {});
|
|
123
123
|
const envsResult = Array.isArray(envsRaw) ? { environments: envsRaw, defaultPullEnvSlug: null } : envsRaw;
|
|
124
|
+
if (!opts.envOverride && !process.env.LS_ENV && !envsResult.defaultPullEnvSlug && envsResult.environments.length !== 1) {
|
|
125
|
+
const list = envsResult.environments.map((e) => e.slug).join(", ") || "(none)";
|
|
126
|
+
throw new Error(
|
|
127
|
+
`No project-level default pull env set (defaultPullEnvSlug is unset) and no --env / $LS_ENV provided. Available: ${list}. Set the Default pull env at ${profile.serverUrl}/${profile.orgSlug}/projects/${profile.projectSlug}/settings.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
124
130
|
const envSlug = resolveEnvSlug(opts.envOverride, envsResult);
|
|
125
131
|
if (!envSlug) {
|
|
126
132
|
const list = envsResult.environments.map((e) => e.slug).join(", ") || "(none)";
|
|
@@ -573,10 +579,9 @@ var init_launch_kit_services = __esm({
|
|
|
573
579
|
sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
|
|
574
580
|
chart: { port: 52819, bin: "launch-chart", args: ["serve"] },
|
|
575
581
|
deck: { port: 52829, bin: "launch-deck", args: ["serve"] },
|
|
576
|
-
council: { port: 52839, bin: "launch-council", args: ["serve"] },
|
|
577
582
|
// Claude web terminal — exposes a viewable/drivable `claude` session at
|
|
578
|
-
// `bot.<baseDomain>`.
|
|
579
|
-
//
|
|
583
|
+
// `bot.<baseDomain>`. Gated at the edge by CF Access → our OIDC IdP (#183);
|
|
584
|
+
// see GATED_SERVICES in radar-docker-init-entry.ts (bot = strict session).
|
|
580
585
|
bot: { port: 52849, bin: "launch-bot", args: ["serve"] }
|
|
581
586
|
};
|
|
582
587
|
DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -585,6 +590,9 @@ var init_launch_kit_services = __esm({
|
|
|
585
590
|
});
|
|
586
591
|
|
|
587
592
|
// src/server/cf-ingress.ts
|
|
593
|
+
function serviceLabel(s) {
|
|
594
|
+
return s.label ?? s.name;
|
|
595
|
+
}
|
|
588
596
|
async function cf(opts) {
|
|
589
597
|
const res = await fetch(`${CF_API_BASE}${opts.path}`, {
|
|
590
598
|
method: opts.method,
|
|
@@ -609,6 +617,17 @@ async function cf(opts) {
|
|
|
609
617
|
function isNotFound(env) {
|
|
610
618
|
return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
|
|
611
619
|
}
|
|
620
|
+
async function findTunnelByName(input) {
|
|
621
|
+
const q = new URLSearchParams({ name: input.tunnelName, is_deleted: "false" }).toString();
|
|
622
|
+
const res = await cf({
|
|
623
|
+
apiToken: input.apiToken,
|
|
624
|
+
method: "GET",
|
|
625
|
+
path: `/accounts/${input.accountId}/cfd_tunnel?${q}`
|
|
626
|
+
});
|
|
627
|
+
if (!res.success || !Array.isArray(res.result)) return null;
|
|
628
|
+
const live = res.result.find((t) => t.name === input.tunnelName && !t.deleted_at);
|
|
629
|
+
return live?.id ?? null;
|
|
630
|
+
}
|
|
612
631
|
function loadState(path6) {
|
|
613
632
|
if (!(0, import_node_fs2.existsSync)(path6)) return null;
|
|
614
633
|
try {
|
|
@@ -640,16 +659,26 @@ async function ensureTunnel(input, knownTunnelId) {
|
|
|
640
659
|
throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
|
|
641
660
|
}
|
|
642
661
|
}
|
|
662
|
+
const existing = await findTunnelByName(input);
|
|
663
|
+
if (existing) {
|
|
664
|
+
console.log(`[cf] adopted existing tunnel "${input.tunnelName}" (${existing}) \u2014 local state was missing`);
|
|
665
|
+
return existing;
|
|
666
|
+
}
|
|
643
667
|
const created = await cf({
|
|
644
668
|
apiToken: input.apiToken,
|
|
645
669
|
method: "POST",
|
|
646
670
|
path: `/accounts/${input.accountId}/cfd_tunnel`,
|
|
647
671
|
body: { name: input.tunnelName, config_src: "cloudflare" }
|
|
648
672
|
});
|
|
649
|
-
if (
|
|
650
|
-
|
|
673
|
+
if (created.success && created.result) return created.result.id;
|
|
674
|
+
if ((created.errors ?? []).some((e) => e.code === 1013)) {
|
|
675
|
+
const adopted = await findTunnelByName(input);
|
|
676
|
+
if (adopted) {
|
|
677
|
+
console.log(`[cf] tunnel "${input.tunnelName}" already existed (1013) \u2014 adopted ${adopted}`);
|
|
678
|
+
return adopted;
|
|
679
|
+
}
|
|
651
680
|
}
|
|
652
|
-
|
|
681
|
+
throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
|
|
653
682
|
}
|
|
654
683
|
async function fetchConnectorToken(input, tunnelId) {
|
|
655
684
|
const res = await cf({
|
|
@@ -664,7 +693,7 @@ async function fetchConnectorToken(input, tunnelId) {
|
|
|
664
693
|
}
|
|
665
694
|
async function setIngressConfig(input, tunnelId) {
|
|
666
695
|
const ingress = input.services.map((s) => ({
|
|
667
|
-
hostname: `${s
|
|
696
|
+
hostname: `${serviceLabel(s)}.${input.zone.name}`,
|
|
668
697
|
service: `http://localhost:${s.port}`
|
|
669
698
|
}));
|
|
670
699
|
ingress.push({ service: "http_status:404" });
|
|
@@ -679,7 +708,7 @@ async function setIngressConfig(input, tunnelId) {
|
|
|
679
708
|
}
|
|
680
709
|
}
|
|
681
710
|
async function ensureDnsRecord(input, tunnelId, service) {
|
|
682
|
-
const fqdn = `${service
|
|
711
|
+
const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
|
|
683
712
|
const target = `${tunnelId}.cfargotunnel.com`;
|
|
684
713
|
const existing = await cf({
|
|
685
714
|
apiToken: input.apiToken,
|
|
@@ -723,7 +752,7 @@ async function provisionIngress(input) {
|
|
|
723
752
|
await setIngressConfig(input, tunnelId);
|
|
724
753
|
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
725
754
|
const hostnames = {};
|
|
726
|
-
for (const s of input.services) hostnames[s.name] = `${s
|
|
755
|
+
for (const s of input.services) hostnames[s.name] = `${serviceLabel(s)}.${input.zone.name}`;
|
|
727
756
|
return { tunnelId, connectorToken, hostnames };
|
|
728
757
|
}
|
|
729
758
|
var import_node_fs2, import_node_path2, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
|
|
@@ -737,25 +766,257 @@ var init_cf_ingress = __esm({
|
|
|
737
766
|
}
|
|
738
767
|
});
|
|
739
768
|
|
|
769
|
+
// src/server/cf-access.ts
|
|
770
|
+
async function cf2(opts) {
|
|
771
|
+
const res = await fetch(`${CF_API_BASE2}${opts.path}`, {
|
|
772
|
+
method: opts.method,
|
|
773
|
+
headers: {
|
|
774
|
+
Authorization: `Bearer ${opts.apiToken}`,
|
|
775
|
+
"Content-Type": "application/json",
|
|
776
|
+
Accept: "application/json",
|
|
777
|
+
"User-Agent": "launch-kit/cf-access"
|
|
778
|
+
},
|
|
779
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
780
|
+
signal: AbortSignal.timeout(15e3)
|
|
781
|
+
});
|
|
782
|
+
const text = await res.text();
|
|
783
|
+
try {
|
|
784
|
+
return text ? JSON.parse(text) : { success: false };
|
|
785
|
+
} catch {
|
|
786
|
+
throw new Error(`[cf-access] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON: ${text.slice(0, 200)}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function fail(env, what) {
|
|
790
|
+
throw new Error(`[cf-access] ${what} failed: ${JSON.stringify(env.errors)}`);
|
|
791
|
+
}
|
|
792
|
+
function loadState2(path6) {
|
|
793
|
+
if (!(0, import_node_fs3.existsSync)(path6)) return null;
|
|
794
|
+
try {
|
|
795
|
+
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(path6, "utf8"));
|
|
796
|
+
return typeof parsed?.accountId === "string" ? parsed : null;
|
|
797
|
+
} catch {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function saveState2(path6, state) {
|
|
802
|
+
const dir = (0, import_node_path3.dirname)(path6);
|
|
803
|
+
if (!(0, import_node_fs3.existsSync)(dir)) (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
804
|
+
(0, import_node_fs3.writeFileSync)(path6, JSON.stringify(state, null, 2));
|
|
805
|
+
}
|
|
806
|
+
async function getAccessAuthDomain(apiToken, accountId) {
|
|
807
|
+
const res = await cf2({
|
|
808
|
+
apiToken,
|
|
809
|
+
method: "GET",
|
|
810
|
+
path: `/accounts/${accountId}/access/organizations`
|
|
811
|
+
});
|
|
812
|
+
if (!res.success || !res.result?.auth_domain) {
|
|
813
|
+
fail(res, "GET access/organizations (is Zero Trust enabled on this account?)");
|
|
814
|
+
}
|
|
815
|
+
return res.result.auth_domain;
|
|
816
|
+
}
|
|
817
|
+
async function ensureAccessIdp(input) {
|
|
818
|
+
const config = {
|
|
819
|
+
client_id: input.clientId,
|
|
820
|
+
client_secret: input.clientSecret,
|
|
821
|
+
auth_url: `${input.issuer}/api/oidc/authorize`,
|
|
822
|
+
token_url: `${input.issuer}/api/oidc/token`,
|
|
823
|
+
certs_url: `${input.issuer}/.well-known/jwks.json`,
|
|
824
|
+
scopes: ["openid", "email", "profile"],
|
|
825
|
+
claims: ["org", "project_access", "roles", "email"],
|
|
826
|
+
email_claim_name: "email",
|
|
827
|
+
pkce_enabled: true
|
|
828
|
+
};
|
|
829
|
+
const body = { name: IDP_NAME, type: "oidc", config };
|
|
830
|
+
let idpId = input.knownIdpId;
|
|
831
|
+
if (!idpId) {
|
|
832
|
+
const list = await cf2({
|
|
833
|
+
apiToken: input.apiToken,
|
|
834
|
+
method: "GET",
|
|
835
|
+
path: `/accounts/${input.accountId}/access/identity_providers`
|
|
836
|
+
});
|
|
837
|
+
if (!list.success) fail(list, "list identity_providers");
|
|
838
|
+
idpId = (list.result ?? []).find((p) => p.name === IDP_NAME)?.id ?? null;
|
|
839
|
+
}
|
|
840
|
+
if (idpId) {
|
|
841
|
+
const upd = await cf2({
|
|
842
|
+
apiToken: input.apiToken,
|
|
843
|
+
method: "PUT",
|
|
844
|
+
path: `/accounts/${input.accountId}/access/identity_providers/${idpId}`,
|
|
845
|
+
body
|
|
846
|
+
});
|
|
847
|
+
if (!upd.success || !upd.result) fail(upd, "update identity_provider");
|
|
848
|
+
return upd.result.id;
|
|
849
|
+
}
|
|
850
|
+
const created = await cf2({
|
|
851
|
+
apiToken: input.apiToken,
|
|
852
|
+
method: "POST",
|
|
853
|
+
path: `/accounts/${input.accountId}/access/identity_providers`,
|
|
854
|
+
body
|
|
855
|
+
});
|
|
856
|
+
if (!created.success || !created.result) fail(created, "create identity_provider");
|
|
857
|
+
return created.result.id;
|
|
858
|
+
}
|
|
859
|
+
async function ensureAccessApp(input) {
|
|
860
|
+
const policy = {
|
|
861
|
+
name: "launch-kit-org-allow",
|
|
862
|
+
decision: "allow",
|
|
863
|
+
include: [
|
|
864
|
+
{
|
|
865
|
+
oidc: {
|
|
866
|
+
identity_provider_id: input.idpId,
|
|
867
|
+
claim_name: "org",
|
|
868
|
+
claim_value: input.organizationId
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
]
|
|
872
|
+
};
|
|
873
|
+
const body = {
|
|
874
|
+
name: `launch-kit ${input.service.hostname}`,
|
|
875
|
+
domain: input.service.hostname,
|
|
876
|
+
type: "self_hosted",
|
|
877
|
+
// Bot terminal = RCE surface → short session. Read portals = a workday.
|
|
878
|
+
session_duration: input.service.strict ? "30m" : "24h",
|
|
879
|
+
allowed_idps: [input.idpId],
|
|
880
|
+
auto_redirect_to_identity: true,
|
|
881
|
+
policies: [policy]
|
|
882
|
+
};
|
|
883
|
+
const list = await cf2({
|
|
884
|
+
apiToken: input.apiToken,
|
|
885
|
+
method: "GET",
|
|
886
|
+
path: `/accounts/${input.accountId}/access/apps`
|
|
887
|
+
});
|
|
888
|
+
if (!list.success) fail(list, "list access apps");
|
|
889
|
+
const existing = (list.result ?? []).find((a) => a.domain === input.service.hostname);
|
|
890
|
+
if (existing) {
|
|
891
|
+
const upd = await cf2({
|
|
892
|
+
apiToken: input.apiToken,
|
|
893
|
+
method: "PUT",
|
|
894
|
+
path: `/accounts/${input.accountId}/access/apps/${existing.id}`,
|
|
895
|
+
body
|
|
896
|
+
});
|
|
897
|
+
if (!upd.success || !upd.result) fail(upd, `update access app ${input.service.hostname}`);
|
|
898
|
+
return upd.result.id;
|
|
899
|
+
}
|
|
900
|
+
const created = await cf2({
|
|
901
|
+
apiToken: input.apiToken,
|
|
902
|
+
method: "POST",
|
|
903
|
+
path: `/accounts/${input.accountId}/access/apps`,
|
|
904
|
+
body
|
|
905
|
+
});
|
|
906
|
+
if (!created.success || !created.result) fail(created, `create access app ${input.service.hostname}`);
|
|
907
|
+
return created.result.id;
|
|
908
|
+
}
|
|
909
|
+
async function provisionAccess(input) {
|
|
910
|
+
const authDomain = await getAccessAuthDomain(input.apiToken, input.accountId);
|
|
911
|
+
const callbackUrl = `https://${authDomain}/cdn-cgi/access/callback`;
|
|
912
|
+
const { clientId, clientSecret, organizationId } = await input.registerClient([callbackUrl]);
|
|
913
|
+
const prior = loadState2(input.stateFile);
|
|
914
|
+
const idpId = await ensureAccessIdp({
|
|
915
|
+
apiToken: input.apiToken,
|
|
916
|
+
accountId: input.accountId,
|
|
917
|
+
issuer: input.issuer,
|
|
918
|
+
clientId,
|
|
919
|
+
clientSecret,
|
|
920
|
+
knownIdpId: prior?.idpId ?? null
|
|
921
|
+
});
|
|
922
|
+
saveState2(input.stateFile, { idpId, accountId: input.accountId });
|
|
923
|
+
const appIds = {};
|
|
924
|
+
for (const service of input.services) {
|
|
925
|
+
appIds[service.hostname] = await ensureAccessApp({
|
|
926
|
+
apiToken: input.apiToken,
|
|
927
|
+
accountId: input.accountId,
|
|
928
|
+
idpId,
|
|
929
|
+
organizationId,
|
|
930
|
+
service
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return { idpId, authDomain, appIds };
|
|
934
|
+
}
|
|
935
|
+
var import_node_fs3, import_node_path3, CF_API_BASE2, IDP_NAME;
|
|
936
|
+
var init_cf_access = __esm({
|
|
937
|
+
"src/server/cf-access.ts"() {
|
|
938
|
+
"use strict";
|
|
939
|
+
import_node_fs3 = require("node:fs");
|
|
940
|
+
import_node_path3 = require("node:path");
|
|
941
|
+
CF_API_BASE2 = "https://api.cloudflare.com/client/v4";
|
|
942
|
+
IDP_NAME = "launch-kit-oidc";
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
740
946
|
// src/server/radar-docker-init-entry.ts
|
|
741
947
|
var radar_docker_init_entry_exports = {};
|
|
742
948
|
__export(radar_docker_init_entry_exports, {
|
|
949
|
+
maybeProvisionAccess: () => maybeProvisionAccess,
|
|
743
950
|
maybeProvisionIngress: () => maybeProvisionIngress,
|
|
744
951
|
spawnServiceGroup: () => spawnServiceGroup
|
|
745
952
|
});
|
|
746
|
-
function
|
|
953
|
+
function fail2(message) {
|
|
747
954
|
console.error(message);
|
|
748
955
|
process.exit(1);
|
|
749
956
|
}
|
|
750
957
|
function requireEnv(name) {
|
|
751
958
|
const v = process.env[name];
|
|
752
|
-
if (!v)
|
|
959
|
+
if (!v) fail2(`ERROR: ${name} is required but not set`);
|
|
753
960
|
return v;
|
|
754
961
|
}
|
|
755
962
|
function run2(cmd, args, stdio = "inherit") {
|
|
756
963
|
const r = (0, import_node_child_process2.spawnSync)(cmd, args, { stdio });
|
|
757
964
|
return r.status ?? 1;
|
|
758
965
|
}
|
|
966
|
+
function readCrashState() {
|
|
967
|
+
try {
|
|
968
|
+
const s = JSON.parse((0, import_node_fs4.readFileSync)(CRASH_STATE_FILE, "utf8"));
|
|
969
|
+
return typeof s?.count === "number" && s.count >= 0 ? s : null;
|
|
970
|
+
} catch {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function bumpCrashCount() {
|
|
975
|
+
const prev = readCrashState();
|
|
976
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
977
|
+
const next = {
|
|
978
|
+
count: (prev?.count ?? 0) + 1,
|
|
979
|
+
firstAt: prev?.firstAt ?? now,
|
|
980
|
+
lastAt: now
|
|
981
|
+
};
|
|
982
|
+
try {
|
|
983
|
+
(0, import_node_fs4.mkdirSync)(LAUNCHPOD_DIR, { recursive: true });
|
|
984
|
+
(0, import_node_fs4.writeFileSync)(CRASH_STATE_FILE, JSON.stringify(next, null, 2));
|
|
985
|
+
} catch (err) {
|
|
986
|
+
console.warn(`[entrypoint] could not persist boot-crash counter (continuing unprotected): ${err instanceof Error ? err.message : String(err)}`);
|
|
987
|
+
}
|
|
988
|
+
return next.count;
|
|
989
|
+
}
|
|
990
|
+
function clearCrashCount() {
|
|
991
|
+
try {
|
|
992
|
+
if ((0, import_node_fs4.existsSync)(CRASH_STATE_FILE)) (0, import_node_fs4.writeFileSync)(CRASH_STATE_FILE, JSON.stringify({ count: 0, firstAt: "", lastAt: "" }));
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async function parkAfterCrashLoop(count) {
|
|
997
|
+
const lines = [
|
|
998
|
+
"==================================================================",
|
|
999
|
+
`[entrypoint] CRASH-LOOP HALT \u2014 ${count} consecutive failed boots (cap ${MAX_BOOT_CRASHES}).`,
|
|
1000
|
+
"[entrypoint] Refusing to restart again. Container is now PARKED (idle, not",
|
|
1001
|
+
"[entrypoint] exiting) so it stops thrashing CF APIs and logs. Fix the root",
|
|
1002
|
+
"[entrypoint] cause, clear the counter, then restart the container:",
|
|
1003
|
+
`[entrypoint] rm ${CRASH_STATE_FILE} && docker restart <container>`,
|
|
1004
|
+
"=================================================================="
|
|
1005
|
+
];
|
|
1006
|
+
for (const l of lines) console.error(l);
|
|
1007
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
1008
|
+
process.on(sig, () => {
|
|
1009
|
+
console.log(`[entrypoint] received ${sig} while parked \u2014 exiting`);
|
|
1010
|
+
process.exit(0);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
setInterval(() => {
|
|
1014
|
+
console.error(`[entrypoint] still parked after crash-loop halt \u2014 clear ${CRASH_STATE_FILE} and restart to retry`);
|
|
1015
|
+
}, 15 * 6e4);
|
|
1016
|
+
await new Promise(() => {
|
|
1017
|
+
});
|
|
1018
|
+
throw new Error("unreachable");
|
|
1019
|
+
}
|
|
759
1020
|
async function setupFromCloud() {
|
|
760
1021
|
const pat = requireEnv("LS_PAT");
|
|
761
1022
|
const orgSlug = requireEnv("LS_ORG_SLUG");
|
|
@@ -766,13 +1027,16 @@ async function setupFromCloud() {
|
|
|
766
1027
|
try {
|
|
767
1028
|
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
768
1029
|
} catch (err) {
|
|
769
|
-
|
|
1030
|
+
fail2(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and LS_ORG_SLUG/LS_PROJECT_SLUG point at a project the user has access to.`);
|
|
770
1031
|
}
|
|
771
1032
|
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
772
1033
|
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
773
1034
|
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
1035
|
+
if (!process.env.RADAR_RULES && Array.isArray(bundle.radarRules) && bundle.radarRules.length > 0) {
|
|
1036
|
+
process.env.RADAR_RULES = JSON.stringify(bundle.radarRules);
|
|
1037
|
+
}
|
|
774
1038
|
if (!process.env.GH_TOKEN) {
|
|
775
|
-
|
|
1039
|
+
fail2(`[entrypoint] no GH_TOKEN available \u2014 user has not connected GitHub (githubTokenStatus=${bundle.githubTokenStatus}). Connect GitHub in LS or pre-set GH_TOKEN in the container env.`);
|
|
776
1040
|
}
|
|
777
1041
|
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
778
1042
|
console.log(`[entrypoint] bundle from cloud: org=${orgSlug} project=${projectSlug} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()} ${cfNote}`);
|
|
@@ -780,17 +1044,17 @@ async function setupFromCloud() {
|
|
|
780
1044
|
}
|
|
781
1045
|
function setupClaudeCredentials() {
|
|
782
1046
|
const home = process.env.HOME ?? "/home/launchpod";
|
|
783
|
-
const claudeDir = (0,
|
|
784
|
-
(0,
|
|
1047
|
+
const claudeDir = (0, import_node_path4.join)(home, ".claude");
|
|
1048
|
+
(0, import_node_fs4.mkdirSync)(claudeDir, { recursive: true });
|
|
785
1049
|
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
786
|
-
const credsPath = (0,
|
|
787
|
-
(0,
|
|
788
|
-
(0,
|
|
789
|
-
const configPath = (0,
|
|
1050
|
+
const credsPath = (0, import_node_path4.join)(claudeDir, ".credentials.json");
|
|
1051
|
+
(0, import_node_fs4.writeFileSync)(credsPath, decoded);
|
|
1052
|
+
(0, import_node_fs4.chmodSync)(credsPath, 384);
|
|
1053
|
+
const configPath = (0, import_node_path4.join)(home, ".claude.json");
|
|
790
1054
|
let cfg = {};
|
|
791
|
-
if ((0,
|
|
1055
|
+
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
792
1056
|
try {
|
|
793
|
-
cfg = JSON.parse((0,
|
|
1057
|
+
cfg = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
|
|
794
1058
|
} catch {
|
|
795
1059
|
cfg = {};
|
|
796
1060
|
}
|
|
@@ -816,21 +1080,21 @@ function setupClaudeCredentials() {
|
|
|
816
1080
|
wsProject.enabledMcpjsonServers = mergedEnabled;
|
|
817
1081
|
projects[wsKey] = wsProject;
|
|
818
1082
|
cfg.projects = projects;
|
|
819
|
-
(0,
|
|
820
|
-
(0,
|
|
1083
|
+
(0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
1084
|
+
(0, import_node_fs4.chmodSync)(configPath, 384);
|
|
821
1085
|
}
|
|
822
1086
|
function setupGitAndGh() {
|
|
823
1087
|
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
824
1088
|
const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
|
|
825
1089
|
const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
826
|
-
if (status !== 0)
|
|
1090
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
827
1091
|
}
|
|
828
1092
|
function detectAndSetPreviewPort() {
|
|
829
1093
|
if (process.env.PREVIEW_PORT) return;
|
|
830
1094
|
try {
|
|
831
1095
|
const pkgPath = "/workspace/package.json";
|
|
832
|
-
if (!(0,
|
|
833
|
-
const pkg = JSON.parse((0,
|
|
1096
|
+
if (!(0, import_node_fs4.existsSync)(pkgPath)) return;
|
|
1097
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
834
1098
|
const scripts = pkg.scripts ?? {};
|
|
835
1099
|
const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
|
|
836
1100
|
for (const name of ["dev", "start", "serve"]) {
|
|
@@ -848,7 +1112,7 @@ function detectAndSetPreviewPort() {
|
|
|
848
1112
|
}
|
|
849
1113
|
function initWorkspaceIfEmpty() {
|
|
850
1114
|
process.chdir("/workspace");
|
|
851
|
-
if ((0,
|
|
1115
|
+
if ((0, import_node_fs4.existsSync)(".git")) {
|
|
852
1116
|
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
853
1117
|
return;
|
|
854
1118
|
}
|
|
@@ -861,7 +1125,7 @@ function initWorkspaceIfEmpty() {
|
|
|
861
1125
|
`--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
|
|
862
1126
|
`--dir=/workspace`
|
|
863
1127
|
]);
|
|
864
|
-
if (status !== 0)
|
|
1128
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
865
1129
|
}
|
|
866
1130
|
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
867
1131
|
const token = bundle.cloudflareToken ?? null;
|
|
@@ -869,28 +1133,36 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
869
1133
|
const zones = bundle.cloudflareZones ?? [];
|
|
870
1134
|
if (!token && !accountId && zones.length === 0) return null;
|
|
871
1135
|
if (!token || !accountId) {
|
|
872
|
-
|
|
1136
|
+
fail2(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
873
1137
|
}
|
|
874
1138
|
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
875
1139
|
let chosen = null;
|
|
876
1140
|
if (baseDomain) {
|
|
877
1141
|
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
878
1142
|
if (!chosen) {
|
|
879
|
-
|
|
1143
|
+
fail2(`[entrypoint] LAUNCHKIT_CF_BASE_DOMAIN="${baseDomain}" is not among the connected CF token's zones (${zones.map((z) => z.name).join(", ") || "none"}). Either change the env or grant Zone:Read on that zone in the CF token.`);
|
|
880
1144
|
}
|
|
881
1145
|
} else if (zones.length === 1) {
|
|
882
1146
|
chosen = { id: zones[0].id, name: zones[0].name };
|
|
883
1147
|
} else {
|
|
884
|
-
|
|
1148
|
+
fail2(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
885
1149
|
}
|
|
886
1150
|
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
887
|
-
|
|
1151
|
+
const slugLabel = projectSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1152
|
+
const DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
1153
|
+
for (const s of services) {
|
|
1154
|
+
const label = `${slugLabel}-${s.name}`;
|
|
1155
|
+
if (!DNS_LABEL_RE.test(label)) {
|
|
1156
|
+
fail2(`[entrypoint] hostname label "${label}" (${label.length} chars) is not a valid DNS label \u2014 must be 1\u201363 chars of [a-z0-9-] with no leading/trailing hyphen. Shorten the project slug ("${projectSlug}") or the service name ("${s.name}").`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => `${slugLabel}-${s.name}`).join(",")}`);
|
|
888
1160
|
const result = await provisionIngress({
|
|
889
1161
|
apiToken: token,
|
|
890
1162
|
accountId,
|
|
891
1163
|
zone: chosen,
|
|
892
1164
|
tunnelName: `launch-kit-${projectSlug}`,
|
|
893
|
-
services: services.map((s) => ({ name: s.name, port: s.port })),
|
|
1165
|
+
services: services.map((s) => ({ name: s.name, label: `${slugLabel}-${s.name}`, port: s.port })),
|
|
894
1166
|
stateFile
|
|
895
1167
|
});
|
|
896
1168
|
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
@@ -898,6 +1170,55 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
898
1170
|
}
|
|
899
1171
|
return result;
|
|
900
1172
|
}
|
|
1173
|
+
async function registerOidcClient(serverUrl, pat, redirectUris) {
|
|
1174
|
+
const res = await fetch(new URL("/api/rover/oidc-client", serverUrl), {
|
|
1175
|
+
method: "POST",
|
|
1176
|
+
headers: {
|
|
1177
|
+
Authorization: `Bearer ${pat}`,
|
|
1178
|
+
"Content-Type": "application/json",
|
|
1179
|
+
Accept: "application/json"
|
|
1180
|
+
},
|
|
1181
|
+
body: JSON.stringify({ redirectUris }),
|
|
1182
|
+
signal: AbortSignal.timeout(15e3)
|
|
1183
|
+
});
|
|
1184
|
+
const body = await res.json().catch(() => null);
|
|
1185
|
+
if (!res.ok || !body?.success || !body.data) {
|
|
1186
|
+
fail2(`[entrypoint] OIDC client provisioning failed (HTTP ${res.status}): ${body?.error ?? "unexpected response"}`);
|
|
1187
|
+
}
|
|
1188
|
+
return body.data;
|
|
1189
|
+
}
|
|
1190
|
+
async function maybeProvisionAccess(bundle, ingress) {
|
|
1191
|
+
const token = bundle.cloudflareToken ?? null;
|
|
1192
|
+
const accountId = bundle.cloudflareAccountId ?? null;
|
|
1193
|
+
if (!token || !accountId) return;
|
|
1194
|
+
const services = [];
|
|
1195
|
+
const skipped = [];
|
|
1196
|
+
for (const [name, hostname] of Object.entries(ingress.hostnames)) {
|
|
1197
|
+
const cfg = GATED_SERVICES[name];
|
|
1198
|
+
if (cfg) services.push({ hostname, strict: cfg.strict });
|
|
1199
|
+
else skipped.push(name);
|
|
1200
|
+
}
|
|
1201
|
+
if (skipped.length > 0) {
|
|
1202
|
+
console.log(`[entrypoint] CF Access: leaving machine surface(s) ungated: ${skipped.join(", ")}`);
|
|
1203
|
+
}
|
|
1204
|
+
if (services.length === 0) {
|
|
1205
|
+
console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview not provisioned)");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
|
|
1209
|
+
const pat = requireEnv("LS_PAT");
|
|
1210
|
+
const stateFile = "/workspace/.launchpod/launch-kit-access.json";
|
|
1211
|
+
console.log(`[entrypoint] gating ${services.map((s) => s.hostname).join(", ")} behind CF Access (IdP: ${serverUrl})`);
|
|
1212
|
+
const result = await provisionAccess({
|
|
1213
|
+
apiToken: token,
|
|
1214
|
+
accountId,
|
|
1215
|
+
issuer: serverUrl,
|
|
1216
|
+
services,
|
|
1217
|
+
stateFile,
|
|
1218
|
+
registerClient: (redirectUris) => registerOidcClient(serverUrl, pat, redirectUris)
|
|
1219
|
+
});
|
|
1220
|
+
console.log(`[entrypoint] CF Access gate live \u2014 IdP ${result.idpId}, auth domain ${result.authDomain}`);
|
|
1221
|
+
}
|
|
901
1222
|
function spawnServiceGroup(services) {
|
|
902
1223
|
const children = [];
|
|
903
1224
|
let shuttingDown = false;
|
|
@@ -980,6 +1301,12 @@ function spawnServiceGroup(services) {
|
|
|
980
1301
|
}).finally(removeSignals);
|
|
981
1302
|
}
|
|
982
1303
|
async function main() {
|
|
1304
|
+
const priorCrashes = readCrashState()?.count ?? 0;
|
|
1305
|
+
if (priorCrashes >= MAX_BOOT_CRASHES) await parkAfterCrashLoop(priorCrashes);
|
|
1306
|
+
const bootAttempt = bumpCrashCount();
|
|
1307
|
+
if (bootAttempt > 1) {
|
|
1308
|
+
console.warn(`[entrypoint] boot attempt ${bootAttempt}/${MAX_BOOT_CRASHES} \u2014 prior boot(s) crashed before becoming stable`);
|
|
1309
|
+
}
|
|
983
1310
|
for (const k of REQUIRED_ENV) requireEnv(k);
|
|
984
1311
|
const bundle = await setupFromCloud();
|
|
985
1312
|
setupClaudeCredentials();
|
|
@@ -990,7 +1317,7 @@ async function main() {
|
|
|
990
1317
|
try {
|
|
991
1318
|
services = resolveServices();
|
|
992
1319
|
} catch (err) {
|
|
993
|
-
|
|
1320
|
+
fail2(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
994
1321
|
}
|
|
995
1322
|
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
996
1323
|
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
@@ -999,8 +1326,9 @@ async function main() {
|
|
|
999
1326
|
const radarFqdn = ingress.hostnames.radar;
|
|
1000
1327
|
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
1001
1328
|
else if (services.some((s) => s.name === "radar")) {
|
|
1002
|
-
|
|
1329
|
+
fail2(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
1003
1330
|
}
|
|
1331
|
+
await maybeProvisionAccess(bundle, ingress);
|
|
1004
1332
|
} else if (services.length > 1) {
|
|
1005
1333
|
const first = services[0];
|
|
1006
1334
|
console.warn(
|
|
@@ -1010,30 +1338,48 @@ async function main() {
|
|
|
1010
1338
|
console.warn(`[entrypoint] \u26A0 first service is "${first.name}", not "radar" \u2014 quick tunneling is owned by the radar agent today, so NO external URL will be available.`);
|
|
1011
1339
|
}
|
|
1012
1340
|
}
|
|
1341
|
+
const stableTimer = setTimeout(() => {
|
|
1342
|
+
clearCrashCount();
|
|
1343
|
+
console.log(`[entrypoint] services stable for ${Math.round(STABLE_AFTER_MS / 1e3)}s \u2014 boot-crash counter cleared`);
|
|
1344
|
+
}, STABLE_AFTER_MS);
|
|
1345
|
+
stableTimer.unref?.();
|
|
1013
1346
|
try {
|
|
1014
1347
|
await spawnServiceGroup(services);
|
|
1348
|
+
clearTimeout(stableTimer);
|
|
1015
1349
|
process.exit(0);
|
|
1016
1350
|
} catch (err) {
|
|
1351
|
+
clearTimeout(stableTimer);
|
|
1017
1352
|
console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
1018
1353
|
process.exit(1);
|
|
1019
1354
|
}
|
|
1020
1355
|
}
|
|
1021
|
-
var import_node_child_process2,
|
|
1356
|
+
var import_node_child_process2, import_node_fs4, import_node_path4, REQUIRED_ENV, LAUNCHPOD_DIR, CRASH_STATE_FILE, MAX_BOOT_CRASHES, STABLE_AFTER_MS, GATED_SERVICES;
|
|
1022
1357
|
var init_radar_docker_init_entry = __esm({
|
|
1023
1358
|
"src/server/radar-docker-init-entry.ts"() {
|
|
1024
1359
|
"use strict";
|
|
1025
1360
|
import_node_child_process2 = require("node:child_process");
|
|
1026
|
-
|
|
1027
|
-
|
|
1361
|
+
import_node_fs4 = require("node:fs");
|
|
1362
|
+
import_node_path4 = require("node:path");
|
|
1028
1363
|
init_mcp();
|
|
1029
1364
|
init_launch_kit_services();
|
|
1030
1365
|
init_cf_ingress();
|
|
1366
|
+
init_cf_access();
|
|
1031
1367
|
REQUIRED_ENV = [
|
|
1032
1368
|
"CLAUDE_CREDENTIALS_B64",
|
|
1033
1369
|
"LS_PAT",
|
|
1034
1370
|
"LS_ORG_SLUG",
|
|
1035
1371
|
"LS_PROJECT_SLUG"
|
|
1036
1372
|
];
|
|
1373
|
+
LAUNCHPOD_DIR = "/workspace/.launchpod";
|
|
1374
|
+
CRASH_STATE_FILE = (0, import_node_path4.join)(LAUNCHPOD_DIR, ".boot-crash.json");
|
|
1375
|
+
MAX_BOOT_CRASHES = 5;
|
|
1376
|
+
STABLE_AFTER_MS = 3e4;
|
|
1377
|
+
GATED_SERVICES = {
|
|
1378
|
+
// Claude web terminal — live drivable shell ⇒ RCE surface ⇒ short session.
|
|
1379
|
+
bot: { strict: true },
|
|
1380
|
+
// The user's own dev/preview server — a workday-length session is fine.
|
|
1381
|
+
preview: { strict: false }
|
|
1382
|
+
};
|
|
1037
1383
|
if (!process.env.VITEST) {
|
|
1038
1384
|
main().catch((err) => {
|
|
1039
1385
|
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1226,6 +1572,24 @@ function footer(msg, hints = []) {
|
|
|
1226
1572
|
function warn(msg) {
|
|
1227
1573
|
console.log(` ${c.yellow("\u26A0")} ${msg}`);
|
|
1228
1574
|
}
|
|
1575
|
+
var ALL_STEP_IDS = [
|
|
1576
|
+
"resolve",
|
|
1577
|
+
"clone",
|
|
1578
|
+
"cred",
|
|
1579
|
+
"mcp",
|
|
1580
|
+
"gitignore",
|
|
1581
|
+
"install",
|
|
1582
|
+
"onboard",
|
|
1583
|
+
"recall",
|
|
1584
|
+
"migrate-safety",
|
|
1585
|
+
"ls-marketplace",
|
|
1586
|
+
"recall-hook",
|
|
1587
|
+
"statusline"
|
|
1588
|
+
];
|
|
1589
|
+
var PRESETS = {
|
|
1590
|
+
init: [],
|
|
1591
|
+
refresh: ["resolve", "clone", "install", "onboard", "recall"]
|
|
1592
|
+
};
|
|
1229
1593
|
var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
|
|
1230
1594
|
var LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD = `
|
|
1231
1595
|
Wired in Claude Code (.mcp.json):
|
|
@@ -1305,7 +1669,16 @@ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
1305
1669
|
"--guide",
|
|
1306
1670
|
"--no-guide"
|
|
1307
1671
|
]);
|
|
1308
|
-
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity"]);
|
|
1672
|
+
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity", "preset", "skip", "only", "with"]);
|
|
1673
|
+
function parseStepList(val, flag) {
|
|
1674
|
+
const ids = val.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
1675
|
+
const bad = ids.filter((id) => !ALL_STEP_IDS.includes(id));
|
|
1676
|
+
if (bad.length > 0) {
|
|
1677
|
+
fail3(`${flag}: unknown step id(s): ${bad.join(", ")}
|
|
1678
|
+
Known steps: ${ALL_STEP_IDS.join(", ")}`);
|
|
1679
|
+
}
|
|
1680
|
+
return ids;
|
|
1681
|
+
}
|
|
1309
1682
|
function parseArgs(argv) {
|
|
1310
1683
|
const args = {
|
|
1311
1684
|
token: process.env.LS_PAT ?? null,
|
|
@@ -1327,6 +1700,10 @@ function parseArgs(argv) {
|
|
|
1327
1700
|
dryRun: false,
|
|
1328
1701
|
verbose: false,
|
|
1329
1702
|
guide: null,
|
|
1703
|
+
preset: null,
|
|
1704
|
+
only: null,
|
|
1705
|
+
skip: [],
|
|
1706
|
+
with: [],
|
|
1330
1707
|
help: false
|
|
1331
1708
|
};
|
|
1332
1709
|
const unknown = [];
|
|
@@ -1417,11 +1794,27 @@ function parseArgs(argv) {
|
|
|
1417
1794
|
args.course = val;
|
|
1418
1795
|
continue;
|
|
1419
1796
|
}
|
|
1797
|
+
if (key === "preset") {
|
|
1798
|
+
args.preset = val;
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
if (key === "skip") {
|
|
1802
|
+
args.skip.push(...parseStepList(val, "--skip"));
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
if (key === "with") {
|
|
1806
|
+
args.with.push(...parseStepList(val, "--with"));
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (key === "only") {
|
|
1810
|
+
args.only = [...args.only ?? [], ...parseStepList(val, "--only")];
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1420
1813
|
if (key === "git-identity") {
|
|
1421
1814
|
try {
|
|
1422
1815
|
args.gitIdentity = parseGitIdentityFlag(val);
|
|
1423
1816
|
} catch (err) {
|
|
1424
|
-
|
|
1817
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
1425
1818
|
}
|
|
1426
1819
|
continue;
|
|
1427
1820
|
}
|
|
@@ -1437,7 +1830,7 @@ function parseArgs(argv) {
|
|
|
1437
1830
|
if (unknown.length > 0) {
|
|
1438
1831
|
const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
|
|
1439
1832
|
const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
|
|
1440
|
-
|
|
1833
|
+
fail3(`Unknown argument(s): ${unknown.join(" ")}
|
|
1441
1834
|
Known boolean flags: ${knownBool}
|
|
1442
1835
|
Known key=value flags: ${knownKv}`);
|
|
1443
1836
|
}
|
|
@@ -1501,9 +1894,12 @@ function printHelp() {
|
|
|
1501
1894
|
console.log(`launch-kit \u2014 bootstrap and refresh a LaunchSecure project on this machine
|
|
1502
1895
|
|
|
1503
1896
|
Subcommands:
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1897
|
+
run One flow, step-selectable. \`--preset=init|refresh\` plus
|
|
1898
|
+
\`--skip\` / \`--only\` / \`--with\` choose which steps fire.
|
|
1899
|
+
\`init\` and \`refresh\` are presets over this same flow.
|
|
1900
|
+
init Preset of \`run\` \u2014 full bootstrap (clone, cred, MCP, scaffolds, install)
|
|
1901
|
+
refresh Preset of \`run\` \u2014 re-apply scaffolds + MCP entries only
|
|
1902
|
+
(skips resolve/clone/install/onboard/recall \u2014 see \`launch-kit refresh --help\`)
|
|
1507
1903
|
setup-git Configure git identity + gh credential helper in one
|
|
1508
1904
|
shot. Use in containers / CI where init isn't needed.
|
|
1509
1905
|
\`launch-kit setup-git --identity="Name <email>"\`.
|
|
@@ -1580,6 +1976,20 @@ Options:
|
|
|
1580
1976
|
launch-secure MCP entry) and runs refresh instead.
|
|
1581
1977
|
Pass --force to re-init from scratch even when the
|
|
1582
1978
|
target dir is already set up.
|
|
1979
|
+
|
|
1980
|
+
Step selection (one-command flow \u2014 compose over the preset):
|
|
1981
|
+
Steps: resolve, clone, cred, mcp, gitignore, install, onboard, recall,
|
|
1982
|
+
migrate-safety, ls-marketplace, recall-hook, statusline
|
|
1983
|
+
--preset=<name> init (everything) or refresh (skips resolve/clone/
|
|
1984
|
+
install/onboard/recall). Default: init. \`init\` and
|
|
1985
|
+
\`refresh\` subcommands just select this.
|
|
1986
|
+
--skip=<a,b> Remove steps from the preset (e.g. --skip=clone,install).
|
|
1987
|
+
Legacy --no-* flags are sugar for this.
|
|
1988
|
+
--with=<a,b> Add steps back onto the preset.
|
|
1989
|
+
--only=<a,b> Run exactly these steps, ignoring the preset.
|
|
1990
|
+
(Dependent steps auto-skip if their prerequisite isn't
|
|
1991
|
+
selected: clone needs resolve, onboard needs install,
|
|
1992
|
+
mcp needs cred.)
|
|
1583
1993
|
--dry-run Preview every file write, merge, clone, and install
|
|
1584
1994
|
command without making any changes. Useful before
|
|
1585
1995
|
re-running init against a customized project. The
|
|
@@ -1621,7 +2031,7 @@ async function prompt(question) {
|
|
|
1621
2031
|
resolve3(answer.trim());
|
|
1622
2032
|
}));
|
|
1623
2033
|
}
|
|
1624
|
-
function
|
|
2034
|
+
function fail3(msg) {
|
|
1625
2035
|
console.error(`[launch-kit] \u2717 ${msg}`);
|
|
1626
2036
|
process.exit(1);
|
|
1627
2037
|
}
|
|
@@ -1643,8 +2053,8 @@ function which(bin) {
|
|
|
1643
2053
|
}
|
|
1644
2054
|
function preflight() {
|
|
1645
2055
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
1646
|
-
if (nodeMajor < 18)
|
|
1647
|
-
if (!which("git"))
|
|
2056
|
+
if (nodeMajor < 18) fail3(`Node.js >= 18 required (current: ${process.versions.node}).`);
|
|
2057
|
+
if (!which("git")) fail3("git not found in PATH. Install git: https://git-scm.com/downloads");
|
|
1648
2058
|
const hasGh2 = which("gh") !== null;
|
|
1649
2059
|
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh2 ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
1650
2060
|
return { hasGh: hasGh2 };
|
|
@@ -1819,12 +2229,12 @@ function cloneRepo(repoUrl, targetDir, hasGh2) {
|
|
|
1819
2229
|
}
|
|
1820
2230
|
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
1821
2231
|
if (res.status !== 0) {
|
|
1822
|
-
|
|
2232
|
+
fail3(
|
|
1823
2233
|
`Clone failed (${cmd} exited ${res.status}). For private repos make sure your GitHub auth is set up: \`gh auth login\`, set GH_TOKEN=<a PAT with repo scope> before re-running (works without the GitHub CLI), or add an SSH key to your GitHub account.`
|
|
1824
2234
|
);
|
|
1825
2235
|
}
|
|
1826
2236
|
if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
|
|
1827
|
-
|
|
2237
|
+
fail3(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
|
|
1828
2238
|
}
|
|
1829
2239
|
ok(`cloned to ${targetDir}`);
|
|
1830
2240
|
}
|
|
@@ -1915,7 +2325,7 @@ function mergeMcpFile(targetDir, launchKitEntries) {
|
|
|
1915
2325
|
try {
|
|
1916
2326
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1917
2327
|
} catch (err) {
|
|
1918
|
-
|
|
2328
|
+
fail3(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1919
2329
|
}
|
|
1920
2330
|
}
|
|
1921
2331
|
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
@@ -1975,7 +2385,7 @@ function detectPackageManager(repoDir) {
|
|
|
1975
2385
|
function runInstall(repoDir, detected) {
|
|
1976
2386
|
const { pm } = detected;
|
|
1977
2387
|
if (!which(pm.binary)) {
|
|
1978
|
-
|
|
2388
|
+
fail3(
|
|
1979
2389
|
`${pm.name} not found on PATH. Configs and clone are intact. Install ${pm.name} (try \`corepack enable\` if you have Node \u226516), then run: cd ${path5.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
|
|
1980
2390
|
);
|
|
1981
2391
|
}
|
|
@@ -1986,7 +2396,7 @@ function runInstall(repoDir, detected) {
|
|
|
1986
2396
|
}
|
|
1987
2397
|
const res = (0, import_node_child_process3.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
1988
2398
|
if (res.status !== 0) {
|
|
1989
|
-
|
|
2399
|
+
fail3(
|
|
1990
2400
|
`${pm.name} install failed (exit ${res.status}).
|
|
1991
2401
|
|
|
1992
2402
|
Half-init state \u2014 install didn't complete, but these files ARE on disk:
|
|
@@ -2043,7 +2453,7 @@ function runOnboard(repoDir, pm) {
|
|
|
2043
2453
|
}
|
|
2044
2454
|
const res = (0, import_node_child_process3.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
2045
2455
|
if (res.status !== 0) {
|
|
2046
|
-
|
|
2456
|
+
fail3(
|
|
2047
2457
|
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path5.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
|
|
2048
2458
|
);
|
|
2049
2459
|
}
|
|
@@ -2234,7 +2644,7 @@ function wireLsSettings(targetDir) {
|
|
|
2234
2644
|
try {
|
|
2235
2645
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
2236
2646
|
} catch (err) {
|
|
2237
|
-
|
|
2647
|
+
fail3(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
2238
2648
|
}
|
|
2239
2649
|
}
|
|
2240
2650
|
const merged = { ...existing };
|
|
@@ -2288,7 +2698,7 @@ function wireRecallHook(targetDir) {
|
|
|
2288
2698
|
try {
|
|
2289
2699
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
2290
2700
|
} catch (err) {
|
|
2291
|
-
|
|
2701
|
+
fail3(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
2292
2702
|
}
|
|
2293
2703
|
}
|
|
2294
2704
|
const hooks = existing.hooks ?? {};
|
|
@@ -2353,14 +2763,14 @@ async function main2() {
|
|
|
2353
2763
|
let identityVal = null;
|
|
2354
2764
|
for (const a of process.argv.slice(3)) {
|
|
2355
2765
|
if (a.startsWith("--identity=")) identityVal = a.slice("--identity=".length);
|
|
2356
|
-
else
|
|
2766
|
+
else fail3(`Unknown setup-git flag: "${a}". Supported: --identity="Name <email>".`);
|
|
2357
2767
|
}
|
|
2358
|
-
if (!identityVal)
|
|
2768
|
+
if (!identityVal) fail3(`launch-kit setup-git requires --identity="Name <email>".`);
|
|
2359
2769
|
try {
|
|
2360
2770
|
const identity = parseGitIdentityFlag(identityVal, "--identity");
|
|
2361
2771
|
configureGitForBot(identity);
|
|
2362
2772
|
} catch (err) {
|
|
2363
|
-
|
|
2773
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2364
2774
|
}
|
|
2365
2775
|
return;
|
|
2366
2776
|
}
|
|
@@ -2380,13 +2790,13 @@ async function main2() {
|
|
|
2380
2790
|
for (const a of process.argv.slice(4)) {
|
|
2381
2791
|
if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
|
|
2382
2792
|
else if (a === "--compact") compactArg = true;
|
|
2383
|
-
else
|
|
2793
|
+
else fail3(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
|
|
2384
2794
|
}
|
|
2385
2795
|
const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
|
|
2386
2796
|
let res;
|
|
2387
2797
|
if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
|
|
2388
2798
|
else if (action === "deactivate") res = deactivateStatusline2();
|
|
2389
|
-
else
|
|
2799
|
+
else fail3(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
|
|
2390
2800
|
if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
2391
2801
|
else info(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
2392
2802
|
return;
|
|
@@ -2404,7 +2814,7 @@ async function main2() {
|
|
|
2404
2814
|
return;
|
|
2405
2815
|
}
|
|
2406
2816
|
if (action !== "pull") {
|
|
2407
|
-
|
|
2817
|
+
fail3(`Unknown secrets action: "${action}". Supported: pull.`);
|
|
2408
2818
|
}
|
|
2409
2819
|
let envOverride = null;
|
|
2410
2820
|
let dirArg = null;
|
|
@@ -2413,14 +2823,14 @@ async function main2() {
|
|
|
2413
2823
|
if (a.startsWith("--env=")) envOverride = a.slice("--env=".length);
|
|
2414
2824
|
else if (a.startsWith("--dir=")) dirArg = a.slice("--dir=".length);
|
|
2415
2825
|
else if (a.startsWith("--file=")) fileArg = a.slice("--file=".length);
|
|
2416
|
-
else
|
|
2826
|
+
else fail3(`Unknown secrets pull flag: "${a}". Supported: --env, --dir, --file.`);
|
|
2417
2827
|
}
|
|
2418
2828
|
const targetDir = path5.resolve(dirArg ?? process.cwd());
|
|
2419
2829
|
const { runSecretsPull: runSecretsPull2 } = await Promise.resolve().then(() => (init_secrets_pull(), secrets_pull_exports));
|
|
2420
2830
|
try {
|
|
2421
2831
|
await runSecretsPull2({ targetDir, envOverride, fileName: fileArg });
|
|
2422
2832
|
} catch (err) {
|
|
2423
|
-
|
|
2833
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2424
2834
|
}
|
|
2425
2835
|
return;
|
|
2426
2836
|
}
|
|
@@ -2431,10 +2841,14 @@ async function main2() {
|
|
|
2431
2841
|
return;
|
|
2432
2842
|
}
|
|
2433
2843
|
if (!subcommand || subcommand.startsWith("--")) {
|
|
2434
|
-
|
|
2844
|
+
fail3(`missing subcommand. Usage: launch-kit <run|init|refresh|statusline|secrets> [options]. Run with --help.`);
|
|
2845
|
+
}
|
|
2846
|
+
if (subcommand !== "init" && subcommand !== "refresh" && subcommand !== "run") {
|
|
2847
|
+
fail3(`Unknown subcommand "${subcommand}". Supported: run, init, refresh, statusline, secrets. Run with --help for usage.`);
|
|
2435
2848
|
}
|
|
2436
|
-
if (subcommand
|
|
2437
|
-
|
|
2849
|
+
if (!args.preset) args.preset = subcommand === "refresh" ? "refresh" : "init";
|
|
2850
|
+
if (!PRESETS[args.preset]) {
|
|
2851
|
+
fail3(`Unknown --preset "${args.preset}". Known presets: ${Object.keys(PRESETS).join(", ")}.`);
|
|
2438
2852
|
}
|
|
2439
2853
|
DRY_RUN = args.dryRun;
|
|
2440
2854
|
VERBOSE = args.verbose || DRY_RUN;
|
|
@@ -2444,102 +2858,30 @@ async function main2() {
|
|
|
2444
2858
|
console.log(c.dim("Lines tagged (dry-run) show what would happen."));
|
|
2445
2859
|
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2446
2860
|
}
|
|
2447
|
-
|
|
2448
|
-
return mainInit(args);
|
|
2861
|
+
return runFlow(args);
|
|
2449
2862
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
if (
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
|
|
2471
|
-
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
if (!cred && args.token && args.orgSlug && args.projectSlug) {
|
|
2475
|
-
const courseName = args.course ?? inferCourseName(args.serverUrl);
|
|
2476
|
-
const seedCfg = {
|
|
2477
|
-
pat: args.token,
|
|
2478
|
-
orgSlug: args.orgSlug,
|
|
2479
|
-
projectSlug: args.projectSlug,
|
|
2480
|
-
serverUrl: args.serverUrl
|
|
2481
|
-
};
|
|
2482
|
-
info(`no ${CONFIG_FILENAME} found \u2014 seeding it from --token/--org/--project (course: ${courseName})`);
|
|
2483
|
-
writeConfigFile(targetDir, seedCfg, courseName);
|
|
2484
|
-
cred = { active: courseName, profiles: { [courseName]: seedCfg } };
|
|
2485
|
-
}
|
|
2486
|
-
if (!cred) {
|
|
2487
|
-
fail2(
|
|
2488
|
-
`no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json.
|
|
2489
|
-
Refresh re-applies configs to an already-wired checkout, so it needs a cred. Two ways forward:
|
|
2490
|
-
\u2022 Seed it inline (you already have the repo): re-run refresh with credentials \u2014
|
|
2491
|
-
launch-kit refresh --dir=${path5.relative(cwd, targetDir) || "."} --token=<pat> --org=<org> --project=<project>
|
|
2492
|
-
\u2022 Or run a full init in place \u2014 it detects this existing repo and skips the clone:
|
|
2493
|
-
launch-kit init --token=<pat> --org=<org> --project=<project> --dir=${path5.relative(cwd, targetDir) || "."}`
|
|
2494
|
-
);
|
|
2495
|
-
}
|
|
2496
|
-
const nested = toNested(cred);
|
|
2497
|
-
if (!nested) fail2(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
|
|
2498
|
-
const active = nested.profiles[nested.active];
|
|
2499
|
-
if (!active) fail2(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
|
|
2500
|
-
info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
|
|
2501
|
-
header("launch-kit refresh", [
|
|
2502
|
-
["course", nested.active],
|
|
2503
|
-
["project", `${active.orgSlug}/${active.projectSlug}`],
|
|
2504
|
-
["dir", path5.relative(cwd, targetDir) || "."]
|
|
2505
|
-
]);
|
|
2506
|
-
const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
|
|
2507
|
-
phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
|
|
2508
|
-
ensureGitignoreLine(targetDir, CONFIG_FILENAME);
|
|
2509
|
-
if (!args.noMigrateSafety) phase("migrate-safety", scaffoldMigrateSafety(targetDir, args.refreshScaffolds));
|
|
2510
|
-
if (!args.noLsMarketplace) phase("ls-marketplace", scaffoldLsMarketplace(targetDir));
|
|
2511
|
-
if (!args.noRecallHook) phase("recall-hook", scaffoldRecallHook(targetDir));
|
|
2512
|
-
const slR = tryActivateStatusline();
|
|
2513
|
-
if (slR) phase("statusline", slR);
|
|
2514
|
-
if (DRY_RUN) {
|
|
2515
|
-
console.log("");
|
|
2516
|
-
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2517
|
-
console.log(c.bold("DRY RUN COMPLETE") + c.dim(" \u2014 refresh would have applied the above; no files modified."));
|
|
2518
|
-
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
ok(`refresh complete \u2014 restart Claude Code to pick up any new /kit:* commands`);
|
|
2522
|
-
const showGuide = args.quiet ? false : args.guide === true;
|
|
2523
|
-
const hints = ["Restart Claude Code to pick up any new /kit:* commands."];
|
|
2524
|
-
if (!showGuide && !args.quiet) hints.push("Run with --guide to print the full MCP + slash-command catalog. --verbose for per-file detail.");
|
|
2525
|
-
footer("Refresh complete.", hints);
|
|
2526
|
-
if (showGuide) {
|
|
2527
|
-
console.log(c.dim(`(refresh never runs these: clone, dependency install, onboard script, launch-recall init \u2014 use \`launch-kit init\` for a full bootstrap)`));
|
|
2528
|
-
console.log(getLaunchKitToolsGuide());
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
async function mainInit(args) {
|
|
2532
|
-
const probeDir = path5.resolve(args.targetDir ?? process.cwd());
|
|
2533
|
-
if (!args.force && fs5.existsSync(probeDir)) {
|
|
2534
|
-
const detection = detectExistingBootstrap(probeDir);
|
|
2535
|
-
if (detection.bootstrapped) {
|
|
2536
|
-
info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
|
|
2537
|
-
info(`delegating to refresh. Pass --force to re-init from scratch (will re-prompt for PAT if needed).`);
|
|
2538
|
-
return mainRefresh({ ...args, targetDir: probeDir });
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2863
|
+
function resolveEnabledSteps(args) {
|
|
2864
|
+
let enabled;
|
|
2865
|
+
if (args.only) {
|
|
2866
|
+
enabled = new Set(args.only);
|
|
2867
|
+
} else {
|
|
2868
|
+
enabled = new Set(ALL_STEP_IDS);
|
|
2869
|
+
for (const id of PRESETS[args.preset ?? "init"] ?? []) enabled.delete(id);
|
|
2870
|
+
}
|
|
2871
|
+
for (const id of args.skip) enabled.delete(id);
|
|
2872
|
+
for (const id of args.with) enabled.add(id);
|
|
2873
|
+
if (args.noInstall) enabled.delete("install");
|
|
2874
|
+
if (args.noOnboard) enabled.delete("onboard");
|
|
2875
|
+
if (args.noRecall) enabled.delete("recall");
|
|
2876
|
+
if (args.noMigrateSafety) enabled.delete("migrate-safety");
|
|
2877
|
+
if (args.noLsMarketplace) enabled.delete("ls-marketplace");
|
|
2878
|
+
if (args.noRecallHook) enabled.delete("recall-hook");
|
|
2879
|
+
return enabled;
|
|
2880
|
+
}
|
|
2881
|
+
async function stepResolve(ctx) {
|
|
2882
|
+
const { args } = ctx;
|
|
2541
2883
|
if (!args.token || !args.orgSlug || !args.projectSlug) {
|
|
2542
|
-
const recoveryDir = path5.resolve(args.targetDir ??
|
|
2884
|
+
const recoveryDir = path5.resolve(args.targetDir ?? ctx.cwd);
|
|
2543
2885
|
if (fs5.existsSync(recoveryDir)) {
|
|
2544
2886
|
const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
|
|
2545
2887
|
const nested = cred ? toNested(cred) : null;
|
|
@@ -2567,16 +2909,18 @@ async function mainInit(args) {
|
|
|
2567
2909
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
2568
2910
|
args.token = t || null;
|
|
2569
2911
|
}
|
|
2570
|
-
if (!args.token)
|
|
2571
|
-
if (!/^ls_pat_/.test(args.token))
|
|
2572
|
-
if (!args.orgSlug)
|
|
2573
|
-
if (!args.projectSlug)
|
|
2912
|
+
if (!args.token) fail3("--token (or LS_PAT env) is required.");
|
|
2913
|
+
if (!/^ls_pat_/.test(args.token)) fail3("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
|
|
2914
|
+
if (!args.orgSlug) fail3("--org=<orgSlug> is required.");
|
|
2915
|
+
if (!args.projectSlug) fail3("--project=<projectSlug> is required.");
|
|
2574
2916
|
header("launch-kit init", [
|
|
2575
2917
|
["org", args.orgSlug],
|
|
2576
2918
|
["project", args.projectSlug],
|
|
2577
2919
|
["server", args.serverUrl]
|
|
2578
2920
|
]);
|
|
2921
|
+
ctx.headerPrinted = true;
|
|
2579
2922
|
const { hasGh: hasGh2 } = preflight();
|
|
2923
|
+
ctx.hasGh = hasGh2;
|
|
2580
2924
|
phase("preflight", { status: "ok", summary: `node ${process.versions.node}${hasGh2 ? " \xB7 git \xB7 gh" : " \xB7 git (gh not found \u2014 will use git for clone)"}` });
|
|
2581
2925
|
if (args.gitIdentity) {
|
|
2582
2926
|
configureGitForBot(args.gitIdentity);
|
|
@@ -2589,120 +2933,271 @@ async function mainInit(args) {
|
|
|
2589
2933
|
try {
|
|
2590
2934
|
resolved = await callProjectInfo(args);
|
|
2591
2935
|
} catch (err) {
|
|
2592
|
-
|
|
2936
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2593
2937
|
}
|
|
2594
2938
|
ok(`resolved "${resolved.projectName}"`);
|
|
2595
2939
|
phase("project_info", { status: "ok", summary: `"${resolved.projectName}"` });
|
|
2596
2940
|
if (!resolved.repositoryUrl) {
|
|
2597
|
-
|
|
2941
|
+
fail3(
|
|
2598
2942
|
`Project "${resolved.projectSlug}" has no GitHub repository configured. Connect GitHub at ${args.serverUrl}/${resolved.orgSlug}/projects/${resolved.projectSlug}/settings/integrations, then re-run init.`
|
|
2599
2943
|
);
|
|
2600
2944
|
}
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2945
|
+
ctx.resolved = resolved;
|
|
2946
|
+
ctx.repoUrl = resolved.repositoryUrl;
|
|
2947
|
+
ctx.targetDir = path5.resolve(args.targetDir ?? path5.join(ctx.cwd, resolved.projectSlug));
|
|
2948
|
+
ctx.cfg = { pat: args.token, orgSlug: resolved.orgSlug, projectSlug: resolved.projectSlug, serverUrl: args.serverUrl };
|
|
2949
|
+
ctx.courseName = args.course ?? inferCourseName(ctx.cfg.serverUrl);
|
|
2950
|
+
}
|
|
2951
|
+
function stepClone(ctx) {
|
|
2952
|
+
const repoUrl = ctx.repoUrl;
|
|
2953
|
+
const { targetDir, cwd } = ctx;
|
|
2604
2954
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
2605
|
-
let skipClone = false;
|
|
2606
2955
|
if (fs5.existsSync(targetDir)) {
|
|
2607
2956
|
if (isGitRepo(targetDir)) {
|
|
2608
2957
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
2609
2958
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
2610
2959
|
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 wiring this existing repo (skipping clone, no GitHub auth needed)`);
|
|
2611
2960
|
info(`tip: once wired, use \`launch-kit refresh\` here to re-apply configs without re-running init`);
|
|
2612
|
-
skipClone = true;
|
|
2961
|
+
ctx.skipClone = true;
|
|
2613
2962
|
} else {
|
|
2614
|
-
|
|
2963
|
+
fail3(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
|
|
2615
2964
|
}
|
|
2616
2965
|
} else if (!dirIsEmpty(targetDir)) {
|
|
2617
|
-
|
|
2966
|
+
fail3(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
|
|
2618
2967
|
}
|
|
2619
2968
|
}
|
|
2620
2969
|
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
2621
|
-
if (!skipClone) {
|
|
2970
|
+
if (!ctx.skipClone) {
|
|
2622
2971
|
section(`Cloning ${repoUrl}`);
|
|
2623
|
-
cloneRepo(repoUrl, targetDir,
|
|
2972
|
+
cloneRepo(repoUrl, targetDir, ctx.hasGh);
|
|
2624
2973
|
phase("clone", { status: "ok", summary: `\u2192 ${relTarget}` });
|
|
2625
2974
|
} else {
|
|
2626
2975
|
phase("clone", { status: "in-sync", summary: `${relTarget} (already a clone of this repo)` });
|
|
2627
2976
|
}
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
let
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
}
|
|
2656
|
-
section("Pulling environment secrets");
|
|
2657
|
-
info("running launch-kit secrets pull \u2026");
|
|
2658
|
-
if (DRY_RUN) {
|
|
2659
|
-
dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
|
|
2660
|
-
phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
|
|
2661
|
-
} else {
|
|
2662
|
-
try {
|
|
2663
|
-
await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
|
|
2664
|
-
phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
|
|
2665
|
-
} catch (err) {
|
|
2666
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2667
|
-
warn(`secrets pull skipped \u2014 ${msg}`);
|
|
2668
|
-
phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2977
|
+
}
|
|
2978
|
+
function stepCred(ctx) {
|
|
2979
|
+
const { args, cwd } = ctx;
|
|
2980
|
+
if (ctx.cfg && ctx.resolved) {
|
|
2981
|
+
writeConfigFile(ctx.targetDir, ctx.cfg, ctx.courseName);
|
|
2982
|
+
phase("cred file", { status: "ok", summary: `course=${ctx.courseName}, active` });
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const targetDir = ctx.targetDir;
|
|
2986
|
+
if (!fs5.existsSync(targetDir)) fail3(`target dir does not exist: ${targetDir}`);
|
|
2987
|
+
let cred;
|
|
2988
|
+
let source;
|
|
2989
|
+
try {
|
|
2990
|
+
const recovery = recoverCred(targetDir, getRecoveryOptions());
|
|
2991
|
+
cred = recovery.cred;
|
|
2992
|
+
source = recovery.source;
|
|
2993
|
+
} catch (err) {
|
|
2994
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2995
|
+
}
|
|
2996
|
+
if (cred && source === "mcp") {
|
|
2997
|
+
info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
|
|
2998
|
+
const courseName = inferCourseName(cred.serverUrl);
|
|
2999
|
+
const nested2 = upsertProfile(null, courseName, cred);
|
|
3000
|
+
if (DRY_RUN) {
|
|
3001
|
+
dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
|
|
3002
|
+
} else {
|
|
3003
|
+
writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
|
|
3004
|
+
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
2672
3005
|
}
|
|
2673
3006
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
3007
|
+
if (!cred && args.token && args.orgSlug && args.projectSlug) {
|
|
3008
|
+
const courseName = args.course ?? inferCourseName(args.serverUrl);
|
|
3009
|
+
const seedCfg = {
|
|
3010
|
+
pat: args.token,
|
|
3011
|
+
orgSlug: args.orgSlug,
|
|
3012
|
+
projectSlug: args.projectSlug,
|
|
3013
|
+
serverUrl: args.serverUrl
|
|
3014
|
+
};
|
|
3015
|
+
info(`no ${CONFIG_FILENAME} found \u2014 seeding it from --token/--org/--project (course: ${courseName})`);
|
|
3016
|
+
writeConfigFile(targetDir, seedCfg, courseName);
|
|
3017
|
+
cred = { active: courseName, profiles: { [courseName]: seedCfg } };
|
|
3018
|
+
}
|
|
3019
|
+
if (!cred) {
|
|
3020
|
+
fail3(
|
|
3021
|
+
`no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json.
|
|
3022
|
+
Refresh re-applies configs to an already-wired checkout, so it needs a cred. Two ways forward:
|
|
3023
|
+
\u2022 Seed it inline (you already have the repo): re-run with credentials \u2014
|
|
3024
|
+
launch-kit refresh --dir=${path5.relative(cwd, targetDir) || "."} --token=<pat> --org=<org> --project=<project>
|
|
3025
|
+
\u2022 Or run a full init in place \u2014 it detects this existing repo and skips the clone:
|
|
3026
|
+
launch-kit init --token=<pat> --org=<org> --project=<project> --dir=${path5.relative(cwd, targetDir) || "."}`
|
|
3027
|
+
);
|
|
2680
3028
|
}
|
|
2681
|
-
|
|
2682
|
-
if (!
|
|
2683
|
-
|
|
3029
|
+
const nested = toNested(cred);
|
|
3030
|
+
if (!nested) fail3(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
|
|
3031
|
+
const active = nested.profiles[nested.active];
|
|
3032
|
+
if (!active) fail3(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
|
|
3033
|
+
ctx.cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
|
|
3034
|
+
ctx.courseName = nested.active;
|
|
3035
|
+
info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
|
|
3036
|
+
if (!ctx.headerPrinted) {
|
|
3037
|
+
header("launch-kit refresh", [
|
|
3038
|
+
["course", nested.active],
|
|
3039
|
+
["project", `${active.orgSlug}/${active.projectSlug}`],
|
|
3040
|
+
["dir", path5.relative(cwd, targetDir) || "."]
|
|
3041
|
+
]);
|
|
3042
|
+
ctx.headerPrinted = true;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
function stepMcp(ctx) {
|
|
3046
|
+
phase(".mcp.json", mergeMcpFile(ctx.targetDir, buildLaunchKitMcpEntries(ctx.cfg)));
|
|
3047
|
+
}
|
|
3048
|
+
function stepGitignore(ctx) {
|
|
3049
|
+
ensureGitignoreLine(ctx.targetDir, CONFIG_FILENAME);
|
|
3050
|
+
}
|
|
3051
|
+
function stepInstall(ctx) {
|
|
3052
|
+
const detected = detectPackageManager(ctx.targetDir);
|
|
3053
|
+
ctx.detected = detected;
|
|
3054
|
+
if (detected) info(`detected package manager: ${detected.pm.name} (${detected.source})`);
|
|
3055
|
+
if (!detected) {
|
|
3056
|
+
ctx.installSkippedReason = "no package.json found";
|
|
3057
|
+
phase("install", { status: "skipped", summary: ctx.installSkippedReason });
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
section(`Installing dependencies (${detected.pm.name})`);
|
|
3061
|
+
runInstall(ctx.targetDir, detected);
|
|
3062
|
+
ctx.installRan = true;
|
|
3063
|
+
phase("install", { status: "ok", summary: `${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` });
|
|
3064
|
+
}
|
|
3065
|
+
async function stepOnboard(ctx) {
|
|
3066
|
+
if (!ctx.installRan || !ctx.detected) return;
|
|
3067
|
+
const { detected, targetDir, cwd } = ctx;
|
|
3068
|
+
if (hasOnboardScript(targetDir)) {
|
|
3069
|
+
section(`Running ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}`);
|
|
3070
|
+
runOnboard(targetDir, detected.pm);
|
|
3071
|
+
phase("onboard", { status: "ok", summary: `${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}` });
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
section("Pulling environment secrets");
|
|
3075
|
+
info("running launch-kit secrets pull \u2026");
|
|
3076
|
+
if (DRY_RUN) {
|
|
3077
|
+
dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
|
|
3078
|
+
phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
try {
|
|
3082
|
+
await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
|
|
3083
|
+
phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
|
|
3084
|
+
} catch (err) {
|
|
3085
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3086
|
+
warn(`secrets pull skipped \u2014 ${msg}`);
|
|
3087
|
+
phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
function stepRecall(ctx) {
|
|
3091
|
+
section("Initializing launch-recall (shadow git backup)");
|
|
3092
|
+
runRecallInit(ctx.targetDir);
|
|
3093
|
+
phase("launch-recall", { status: "ok", summary: "shadow git ready" });
|
|
3094
|
+
}
|
|
3095
|
+
function stepMigrateSafety(ctx) {
|
|
3096
|
+
phase("migrate-safety", scaffoldMigrateSafety(ctx.targetDir, ctx.args.refreshScaffolds));
|
|
3097
|
+
}
|
|
3098
|
+
function stepLsMarketplace(ctx) {
|
|
3099
|
+
phase("ls-marketplace", scaffoldLsMarketplace(ctx.targetDir));
|
|
3100
|
+
}
|
|
3101
|
+
function stepRecallHook(ctx) {
|
|
3102
|
+
phase("recall-hook", scaffoldRecallHook(ctx.targetDir));
|
|
3103
|
+
}
|
|
3104
|
+
function stepStatusline(_ctx) {
|
|
2684
3105
|
const slR = tryActivateStatusline();
|
|
2685
3106
|
if (slR) phase("statusline", slR);
|
|
3107
|
+
}
|
|
3108
|
+
var STEPS = [
|
|
3109
|
+
{ id: "resolve", run: stepResolve },
|
|
3110
|
+
{ id: "clone", requires: ["resolve"], run: stepClone },
|
|
3111
|
+
{ id: "cred", run: stepCred },
|
|
3112
|
+
{ id: "mcp", requires: ["cred"], run: stepMcp },
|
|
3113
|
+
{ id: "gitignore", run: stepGitignore },
|
|
3114
|
+
{ id: "install", run: stepInstall },
|
|
3115
|
+
{ id: "onboard", requires: ["install"], run: stepOnboard },
|
|
3116
|
+
{ id: "recall", run: stepRecall },
|
|
3117
|
+
{ id: "migrate-safety", run: stepMigrateSafety },
|
|
3118
|
+
{ id: "ls-marketplace", run: stepLsMarketplace },
|
|
3119
|
+
{ id: "recall-hook", run: stepRecallHook },
|
|
3120
|
+
{ id: "statusline", run: stepStatusline }
|
|
3121
|
+
];
|
|
3122
|
+
function buildCtx(args, enabled) {
|
|
3123
|
+
const cwd = process.cwd();
|
|
3124
|
+
const targetDir = path5.resolve(args.targetDir ?? cwd);
|
|
3125
|
+
if (enabled.has("resolve") && !args.force && fs5.existsSync(targetDir)) {
|
|
3126
|
+
const detection = detectExistingBootstrap(targetDir);
|
|
3127
|
+
if (detection.bootstrapped) {
|
|
3128
|
+
info(`detected existing bootstrap at ${targetDir} (${detection.reason})`);
|
|
3129
|
+
info(`delegating to refresh. Pass --force to re-init from scratch (will re-prompt for PAT if needed).`);
|
|
3130
|
+
for (const id of PRESETS.refresh) enabled.delete(id);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return {
|
|
3134
|
+
args,
|
|
3135
|
+
enabled,
|
|
3136
|
+
cwd,
|
|
3137
|
+
targetDir,
|
|
3138
|
+
hasGh: false,
|
|
3139
|
+
cfg: null,
|
|
3140
|
+
courseName: null,
|
|
3141
|
+
resolved: null,
|
|
3142
|
+
repoUrl: null,
|
|
3143
|
+
skipClone: false,
|
|
3144
|
+
headerPrinted: false,
|
|
3145
|
+
detected: null,
|
|
3146
|
+
installRan: false,
|
|
3147
|
+
installSkippedReason: null
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
async function runFlow(args) {
|
|
3151
|
+
const enabled = resolveEnabledSteps(args);
|
|
3152
|
+
const ctx = buildCtx(args, enabled);
|
|
3153
|
+
for (const step of STEPS) {
|
|
3154
|
+
if (!ctx.enabled.has(step.id)) continue;
|
|
3155
|
+
if (step.requires?.some((r) => !ctx.enabled.has(r))) continue;
|
|
3156
|
+
await step.run(ctx);
|
|
3157
|
+
}
|
|
3158
|
+
epilogue(ctx);
|
|
3159
|
+
}
|
|
3160
|
+
function epilogue(ctx) {
|
|
3161
|
+
const { args, cwd, targetDir } = ctx;
|
|
3162
|
+
const isRefreshLike = !ctx.enabled.has("resolve");
|
|
3163
|
+
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
2686
3164
|
if (DRY_RUN) {
|
|
2687
3165
|
console.log("");
|
|
2688
3166
|
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
3167
|
+
if (isRefreshLike) {
|
|
3168
|
+
console.log(c.bold("DRY RUN COMPLETE") + c.dim(" \u2014 refresh would have applied the above; no files modified."));
|
|
3169
|
+
} else {
|
|
3170
|
+
console.log(c.bold("DRY RUN COMPLETE") + c.dim(` \u2014 no files were modified, no commands ran.`));
|
|
3171
|
+
console.log(c.dim(`Target: ${targetDir}`));
|
|
3172
|
+
console.log(c.dim(`Re-run without --dry-run to apply the changes shown above.`));
|
|
3173
|
+
}
|
|
2692
3174
|
console.log(c.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2693
3175
|
return;
|
|
2694
3176
|
}
|
|
2695
|
-
|
|
3177
|
+
if (isRefreshLike) {
|
|
3178
|
+
ok(`refresh complete \u2014 restart Claude Code to pick up any new /kit:* commands`);
|
|
3179
|
+
const showGuide2 = args.quiet ? false : args.guide === true;
|
|
3180
|
+
const hints = ["Restart Claude Code to pick up any new /kit:* commands."];
|
|
3181
|
+
if (!showGuide2 && !args.quiet) hints.push("Run with --guide to print the full MCP + slash-command catalog. --verbose for per-file detail.");
|
|
3182
|
+
footer("Refresh complete.", hints);
|
|
3183
|
+
if (showGuide2) {
|
|
3184
|
+
console.log(c.dim(`(refresh never runs these: clone, dependency install, onboard script, launch-recall init \u2014 use \`launch-kit init\` for a full bootstrap)`));
|
|
3185
|
+
console.log(getLaunchKitToolsGuide());
|
|
3186
|
+
}
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
const projectName = ctx.resolved?.projectName ?? relTarget;
|
|
3190
|
+
ok(`done \u2014 ${projectName} is ready at ${targetDir}`);
|
|
2696
3191
|
const showGuide = args.quiet ? false : args.guide ?? true;
|
|
2697
3192
|
const nextSteps = [`cd ${relTarget}`];
|
|
2698
|
-
if (installSkippedReason) {
|
|
2699
|
-
nextSteps.push(detected ? `${detected.pm.binary} ${detected.pm.installArgs.join(" ")} # install skipped: ${installSkippedReason}` : `npm install # install skipped: ${installSkippedReason}`);
|
|
2700
|
-
if (
|
|
2701
|
-
nextSteps.push(`${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook`);
|
|
3193
|
+
if (ctx.installSkippedReason) {
|
|
3194
|
+
nextSteps.push(ctx.detected ? `${ctx.detected.pm.binary} ${ctx.detected.pm.installArgs.join(" ")} # install skipped: ${ctx.installSkippedReason}` : `npm install # install skipped: ${ctx.installSkippedReason}`);
|
|
3195
|
+
if (hasOnboardScript(targetDir) && ctx.detected && ctx.enabled.has("onboard")) {
|
|
3196
|
+
nextSteps.push(`${ctx.detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook`);
|
|
2702
3197
|
}
|
|
2703
3198
|
}
|
|
2704
3199
|
nextSteps.push("claude # launch Claude Code (5 MCPs wired)");
|
|
2705
|
-
footer(`${
|
|
3200
|
+
footer(`${projectName} is ready at ${relTarget}.`, [
|
|
2706
3201
|
"Next:",
|
|
2707
3202
|
...nextSteps.map((s) => " " + s)
|
|
2708
3203
|
]);
|