@launchsecure/launch-kit 0.0.35 → 0.0.36
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/server/cli.js +240 -24
- package/dist/server/council-entry.js +0 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/init-entry.js +637 -234
- package/dist/server/orbit-entry.js +880 -144
- package/dist/server/radar-docker-init-entry.js +276 -34
- package/dist/server/radar-entrypoint-entry.js +0 -0
- package/dist/server/radar-teardown-entry.js +0 -0
- package/dist/server/rover-entry.js +84 -17
- package/package.json +23 -22
- 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/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +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,
|
|
@@ -664,7 +672,7 @@ async function fetchConnectorToken(input, tunnelId) {
|
|
|
664
672
|
}
|
|
665
673
|
async function setIngressConfig(input, tunnelId) {
|
|
666
674
|
const ingress = input.services.map((s) => ({
|
|
667
|
-
hostname: `${s
|
|
675
|
+
hostname: `${serviceLabel(s)}.${input.zone.name}`,
|
|
668
676
|
service: `http://localhost:${s.port}`
|
|
669
677
|
}));
|
|
670
678
|
ingress.push({ service: "http_status:404" });
|
|
@@ -679,7 +687,7 @@ async function setIngressConfig(input, tunnelId) {
|
|
|
679
687
|
}
|
|
680
688
|
}
|
|
681
689
|
async function ensureDnsRecord(input, tunnelId, service) {
|
|
682
|
-
const fqdn = `${service
|
|
690
|
+
const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
|
|
683
691
|
const target = `${tunnelId}.cfargotunnel.com`;
|
|
684
692
|
const existing = await cf({
|
|
685
693
|
apiToken: input.apiToken,
|
|
@@ -723,7 +731,7 @@ async function provisionIngress(input) {
|
|
|
723
731
|
await setIngressConfig(input, tunnelId);
|
|
724
732
|
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
725
733
|
const hostnames = {};
|
|
726
|
-
for (const s of input.services) hostnames[s.name] = `${s
|
|
734
|
+
for (const s of input.services) hostnames[s.name] = `${serviceLabel(s)}.${input.zone.name}`;
|
|
727
735
|
return { tunnelId, connectorToken, hostnames };
|
|
728
736
|
}
|
|
729
737
|
var import_node_fs2, import_node_path2, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
|
|
@@ -737,19 +745,197 @@ var init_cf_ingress = __esm({
|
|
|
737
745
|
}
|
|
738
746
|
});
|
|
739
747
|
|
|
748
|
+
// src/server/cf-access.ts
|
|
749
|
+
async function cf2(opts) {
|
|
750
|
+
const res = await fetch(`${CF_API_BASE2}${opts.path}`, {
|
|
751
|
+
method: opts.method,
|
|
752
|
+
headers: {
|
|
753
|
+
Authorization: `Bearer ${opts.apiToken}`,
|
|
754
|
+
"Content-Type": "application/json",
|
|
755
|
+
Accept: "application/json",
|
|
756
|
+
"User-Agent": "launch-kit/cf-access"
|
|
757
|
+
},
|
|
758
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
759
|
+
signal: AbortSignal.timeout(15e3)
|
|
760
|
+
});
|
|
761
|
+
const text = await res.text();
|
|
762
|
+
try {
|
|
763
|
+
return text ? JSON.parse(text) : { success: false };
|
|
764
|
+
} catch {
|
|
765
|
+
throw new Error(`[cf-access] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON: ${text.slice(0, 200)}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function fail(env, what) {
|
|
769
|
+
throw new Error(`[cf-access] ${what} failed: ${JSON.stringify(env.errors)}`);
|
|
770
|
+
}
|
|
771
|
+
function loadState2(path6) {
|
|
772
|
+
if (!(0, import_node_fs3.existsSync)(path6)) return null;
|
|
773
|
+
try {
|
|
774
|
+
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(path6, "utf8"));
|
|
775
|
+
return typeof parsed?.accountId === "string" ? parsed : null;
|
|
776
|
+
} catch {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function saveState2(path6, state) {
|
|
781
|
+
const dir = (0, import_node_path3.dirname)(path6);
|
|
782
|
+
if (!(0, import_node_fs3.existsSync)(dir)) (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
783
|
+
(0, import_node_fs3.writeFileSync)(path6, JSON.stringify(state, null, 2));
|
|
784
|
+
}
|
|
785
|
+
async function getAccessAuthDomain(apiToken, accountId) {
|
|
786
|
+
const res = await cf2({
|
|
787
|
+
apiToken,
|
|
788
|
+
method: "GET",
|
|
789
|
+
path: `/accounts/${accountId}/access/organizations`
|
|
790
|
+
});
|
|
791
|
+
if (!res.success || !res.result?.auth_domain) {
|
|
792
|
+
fail(res, "GET access/organizations (is Zero Trust enabled on this account?)");
|
|
793
|
+
}
|
|
794
|
+
return res.result.auth_domain;
|
|
795
|
+
}
|
|
796
|
+
async function ensureAccessIdp(input) {
|
|
797
|
+
const config = {
|
|
798
|
+
client_id: input.clientId,
|
|
799
|
+
client_secret: input.clientSecret,
|
|
800
|
+
auth_url: `${input.issuer}/api/oidc/authorize`,
|
|
801
|
+
token_url: `${input.issuer}/api/oidc/token`,
|
|
802
|
+
certs_url: `${input.issuer}/.well-known/jwks.json`,
|
|
803
|
+
scopes: ["openid", "email", "profile"],
|
|
804
|
+
claims: ["org", "project_access", "roles", "email"],
|
|
805
|
+
email_claim_name: "email",
|
|
806
|
+
pkce_enabled: true
|
|
807
|
+
};
|
|
808
|
+
const body = { name: IDP_NAME, type: "oidc", config };
|
|
809
|
+
let idpId = input.knownIdpId;
|
|
810
|
+
if (!idpId) {
|
|
811
|
+
const list = await cf2({
|
|
812
|
+
apiToken: input.apiToken,
|
|
813
|
+
method: "GET",
|
|
814
|
+
path: `/accounts/${input.accountId}/access/identity_providers`
|
|
815
|
+
});
|
|
816
|
+
if (!list.success) fail(list, "list identity_providers");
|
|
817
|
+
idpId = (list.result ?? []).find((p) => p.name === IDP_NAME)?.id ?? null;
|
|
818
|
+
}
|
|
819
|
+
if (idpId) {
|
|
820
|
+
const upd = await cf2({
|
|
821
|
+
apiToken: input.apiToken,
|
|
822
|
+
method: "PUT",
|
|
823
|
+
path: `/accounts/${input.accountId}/access/identity_providers/${idpId}`,
|
|
824
|
+
body
|
|
825
|
+
});
|
|
826
|
+
if (!upd.success || !upd.result) fail(upd, "update identity_provider");
|
|
827
|
+
return upd.result.id;
|
|
828
|
+
}
|
|
829
|
+
const created = await cf2({
|
|
830
|
+
apiToken: input.apiToken,
|
|
831
|
+
method: "POST",
|
|
832
|
+
path: `/accounts/${input.accountId}/access/identity_providers`,
|
|
833
|
+
body
|
|
834
|
+
});
|
|
835
|
+
if (!created.success || !created.result) fail(created, "create identity_provider");
|
|
836
|
+
return created.result.id;
|
|
837
|
+
}
|
|
838
|
+
async function ensureAccessApp(input) {
|
|
839
|
+
const policy = {
|
|
840
|
+
name: "launch-kit-org-allow",
|
|
841
|
+
decision: "allow",
|
|
842
|
+
include: [
|
|
843
|
+
{
|
|
844
|
+
oidc: {
|
|
845
|
+
identity_provider_id: input.idpId,
|
|
846
|
+
claim_name: "org",
|
|
847
|
+
claim_value: input.organizationId
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
]
|
|
851
|
+
};
|
|
852
|
+
const body = {
|
|
853
|
+
name: `launch-kit ${input.service.hostname}`,
|
|
854
|
+
domain: input.service.hostname,
|
|
855
|
+
type: "self_hosted",
|
|
856
|
+
// Bot terminal = RCE surface → short session. Read portals = a workday.
|
|
857
|
+
session_duration: input.service.strict ? "30m" : "24h",
|
|
858
|
+
allowed_idps: [input.idpId],
|
|
859
|
+
auto_redirect_to_identity: true,
|
|
860
|
+
policies: [policy]
|
|
861
|
+
};
|
|
862
|
+
const list = await cf2({
|
|
863
|
+
apiToken: input.apiToken,
|
|
864
|
+
method: "GET",
|
|
865
|
+
path: `/accounts/${input.accountId}/access/apps`
|
|
866
|
+
});
|
|
867
|
+
if (!list.success) fail(list, "list access apps");
|
|
868
|
+
const existing = (list.result ?? []).find((a) => a.domain === input.service.hostname);
|
|
869
|
+
if (existing) {
|
|
870
|
+
const upd = await cf2({
|
|
871
|
+
apiToken: input.apiToken,
|
|
872
|
+
method: "PUT",
|
|
873
|
+
path: `/accounts/${input.accountId}/access/apps/${existing.id}`,
|
|
874
|
+
body
|
|
875
|
+
});
|
|
876
|
+
if (!upd.success || !upd.result) fail(upd, `update access app ${input.service.hostname}`);
|
|
877
|
+
return upd.result.id;
|
|
878
|
+
}
|
|
879
|
+
const created = await cf2({
|
|
880
|
+
apiToken: input.apiToken,
|
|
881
|
+
method: "POST",
|
|
882
|
+
path: `/accounts/${input.accountId}/access/apps`,
|
|
883
|
+
body
|
|
884
|
+
});
|
|
885
|
+
if (!created.success || !created.result) fail(created, `create access app ${input.service.hostname}`);
|
|
886
|
+
return created.result.id;
|
|
887
|
+
}
|
|
888
|
+
async function provisionAccess(input) {
|
|
889
|
+
const authDomain = await getAccessAuthDomain(input.apiToken, input.accountId);
|
|
890
|
+
const callbackUrl = `https://${authDomain}/cdn-cgi/access/callback`;
|
|
891
|
+
const { clientId, clientSecret, organizationId } = await input.registerClient([callbackUrl]);
|
|
892
|
+
const prior = loadState2(input.stateFile);
|
|
893
|
+
const idpId = await ensureAccessIdp({
|
|
894
|
+
apiToken: input.apiToken,
|
|
895
|
+
accountId: input.accountId,
|
|
896
|
+
issuer: input.issuer,
|
|
897
|
+
clientId,
|
|
898
|
+
clientSecret,
|
|
899
|
+
knownIdpId: prior?.idpId ?? null
|
|
900
|
+
});
|
|
901
|
+
saveState2(input.stateFile, { idpId, accountId: input.accountId });
|
|
902
|
+
const appIds = {};
|
|
903
|
+
for (const service of input.services) {
|
|
904
|
+
appIds[service.hostname] = await ensureAccessApp({
|
|
905
|
+
apiToken: input.apiToken,
|
|
906
|
+
accountId: input.accountId,
|
|
907
|
+
idpId,
|
|
908
|
+
organizationId,
|
|
909
|
+
service
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
return { idpId, authDomain, appIds };
|
|
913
|
+
}
|
|
914
|
+
var import_node_fs3, import_node_path3, CF_API_BASE2, IDP_NAME;
|
|
915
|
+
var init_cf_access = __esm({
|
|
916
|
+
"src/server/cf-access.ts"() {
|
|
917
|
+
"use strict";
|
|
918
|
+
import_node_fs3 = require("node:fs");
|
|
919
|
+
import_node_path3 = require("node:path");
|
|
920
|
+
CF_API_BASE2 = "https://api.cloudflare.com/client/v4";
|
|
921
|
+
IDP_NAME = "launch-kit-oidc";
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
|
|
740
925
|
// src/server/radar-docker-init-entry.ts
|
|
741
926
|
var radar_docker_init_entry_exports = {};
|
|
742
927
|
__export(radar_docker_init_entry_exports, {
|
|
928
|
+
maybeProvisionAccess: () => maybeProvisionAccess,
|
|
743
929
|
maybeProvisionIngress: () => maybeProvisionIngress,
|
|
744
930
|
spawnServiceGroup: () => spawnServiceGroup
|
|
745
931
|
});
|
|
746
|
-
function
|
|
932
|
+
function fail2(message) {
|
|
747
933
|
console.error(message);
|
|
748
934
|
process.exit(1);
|
|
749
935
|
}
|
|
750
936
|
function requireEnv(name) {
|
|
751
937
|
const v = process.env[name];
|
|
752
|
-
if (!v)
|
|
938
|
+
if (!v) fail2(`ERROR: ${name} is required but not set`);
|
|
753
939
|
return v;
|
|
754
940
|
}
|
|
755
941
|
function run2(cmd, args, stdio = "inherit") {
|
|
@@ -766,13 +952,16 @@ async function setupFromCloud() {
|
|
|
766
952
|
try {
|
|
767
953
|
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
768
954
|
} catch (err) {
|
|
769
|
-
|
|
955
|
+
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
956
|
}
|
|
771
957
|
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
772
958
|
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
773
959
|
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
960
|
+
if (!process.env.RADAR_RULES && Array.isArray(bundle.radarRules) && bundle.radarRules.length > 0) {
|
|
961
|
+
process.env.RADAR_RULES = JSON.stringify(bundle.radarRules);
|
|
962
|
+
}
|
|
774
963
|
if (!process.env.GH_TOKEN) {
|
|
775
|
-
|
|
964
|
+
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
965
|
}
|
|
777
966
|
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
778
967
|
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 +969,17 @@ async function setupFromCloud() {
|
|
|
780
969
|
}
|
|
781
970
|
function setupClaudeCredentials() {
|
|
782
971
|
const home = process.env.HOME ?? "/home/launchpod";
|
|
783
|
-
const claudeDir = (0,
|
|
784
|
-
(0,
|
|
972
|
+
const claudeDir = (0, import_node_path4.join)(home, ".claude");
|
|
973
|
+
(0, import_node_fs4.mkdirSync)(claudeDir, { recursive: true });
|
|
785
974
|
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
786
|
-
const credsPath = (0,
|
|
787
|
-
(0,
|
|
788
|
-
(0,
|
|
789
|
-
const configPath = (0,
|
|
975
|
+
const credsPath = (0, import_node_path4.join)(claudeDir, ".credentials.json");
|
|
976
|
+
(0, import_node_fs4.writeFileSync)(credsPath, decoded);
|
|
977
|
+
(0, import_node_fs4.chmodSync)(credsPath, 384);
|
|
978
|
+
const configPath = (0, import_node_path4.join)(home, ".claude.json");
|
|
790
979
|
let cfg = {};
|
|
791
|
-
if ((0,
|
|
980
|
+
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
792
981
|
try {
|
|
793
|
-
cfg = JSON.parse((0,
|
|
982
|
+
cfg = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
|
|
794
983
|
} catch {
|
|
795
984
|
cfg = {};
|
|
796
985
|
}
|
|
@@ -816,21 +1005,21 @@ function setupClaudeCredentials() {
|
|
|
816
1005
|
wsProject.enabledMcpjsonServers = mergedEnabled;
|
|
817
1006
|
projects[wsKey] = wsProject;
|
|
818
1007
|
cfg.projects = projects;
|
|
819
|
-
(0,
|
|
820
|
-
(0,
|
|
1008
|
+
(0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
1009
|
+
(0, import_node_fs4.chmodSync)(configPath, 384);
|
|
821
1010
|
}
|
|
822
1011
|
function setupGitAndGh() {
|
|
823
1012
|
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
824
1013
|
const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
|
|
825
1014
|
const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
826
|
-
if (status !== 0)
|
|
1015
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
827
1016
|
}
|
|
828
1017
|
function detectAndSetPreviewPort() {
|
|
829
1018
|
if (process.env.PREVIEW_PORT) return;
|
|
830
1019
|
try {
|
|
831
1020
|
const pkgPath = "/workspace/package.json";
|
|
832
|
-
if (!(0,
|
|
833
|
-
const pkg = JSON.parse((0,
|
|
1021
|
+
if (!(0, import_node_fs4.existsSync)(pkgPath)) return;
|
|
1022
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
834
1023
|
const scripts = pkg.scripts ?? {};
|
|
835
1024
|
const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
|
|
836
1025
|
for (const name of ["dev", "start", "serve"]) {
|
|
@@ -848,7 +1037,7 @@ function detectAndSetPreviewPort() {
|
|
|
848
1037
|
}
|
|
849
1038
|
function initWorkspaceIfEmpty() {
|
|
850
1039
|
process.chdir("/workspace");
|
|
851
|
-
if ((0,
|
|
1040
|
+
if ((0, import_node_fs4.existsSync)(".git")) {
|
|
852
1041
|
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
853
1042
|
return;
|
|
854
1043
|
}
|
|
@@ -861,7 +1050,7 @@ function initWorkspaceIfEmpty() {
|
|
|
861
1050
|
`--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
|
|
862
1051
|
`--dir=/workspace`
|
|
863
1052
|
]);
|
|
864
|
-
if (status !== 0)
|
|
1053
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
865
1054
|
}
|
|
866
1055
|
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
867
1056
|
const token = bundle.cloudflareToken ?? null;
|
|
@@ -869,28 +1058,36 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
869
1058
|
const zones = bundle.cloudflareZones ?? [];
|
|
870
1059
|
if (!token && !accountId && zones.length === 0) return null;
|
|
871
1060
|
if (!token || !accountId) {
|
|
872
|
-
|
|
1061
|
+
fail2(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
873
1062
|
}
|
|
874
1063
|
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
875
1064
|
let chosen = null;
|
|
876
1065
|
if (baseDomain) {
|
|
877
1066
|
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
878
1067
|
if (!chosen) {
|
|
879
|
-
|
|
1068
|
+
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
1069
|
}
|
|
881
1070
|
} else if (zones.length === 1) {
|
|
882
1071
|
chosen = { id: zones[0].id, name: zones[0].name };
|
|
883
1072
|
} else {
|
|
884
|
-
|
|
1073
|
+
fail2(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
885
1074
|
}
|
|
886
1075
|
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
887
|
-
|
|
1076
|
+
const slugLabel = projectSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1077
|
+
const DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
1078
|
+
for (const s of services) {
|
|
1079
|
+
const label = `${slugLabel}-${s.name}`;
|
|
1080
|
+
if (!DNS_LABEL_RE.test(label)) {
|
|
1081
|
+
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}").`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => `${slugLabel}-${s.name}`).join(",")}`);
|
|
888
1085
|
const result = await provisionIngress({
|
|
889
1086
|
apiToken: token,
|
|
890
1087
|
accountId,
|
|
891
1088
|
zone: chosen,
|
|
892
1089
|
tunnelName: `launch-kit-${projectSlug}`,
|
|
893
|
-
services: services.map((s) => ({ name: s.name, port: s.port })),
|
|
1090
|
+
services: services.map((s) => ({ name: s.name, label: `${slugLabel}-${s.name}`, port: s.port })),
|
|
894
1091
|
stateFile
|
|
895
1092
|
});
|
|
896
1093
|
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
@@ -898,6 +1095,55 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
898
1095
|
}
|
|
899
1096
|
return result;
|
|
900
1097
|
}
|
|
1098
|
+
async function registerOidcClient(serverUrl, pat, redirectUris) {
|
|
1099
|
+
const res = await fetch(new URL("/api/rover/oidc-client", serverUrl), {
|
|
1100
|
+
method: "POST",
|
|
1101
|
+
headers: {
|
|
1102
|
+
Authorization: `Bearer ${pat}`,
|
|
1103
|
+
"Content-Type": "application/json",
|
|
1104
|
+
Accept: "application/json"
|
|
1105
|
+
},
|
|
1106
|
+
body: JSON.stringify({ redirectUris }),
|
|
1107
|
+
signal: AbortSignal.timeout(15e3)
|
|
1108
|
+
});
|
|
1109
|
+
const body = await res.json().catch(() => null);
|
|
1110
|
+
if (!res.ok || !body?.success || !body.data) {
|
|
1111
|
+
fail2(`[entrypoint] OIDC client provisioning failed (HTTP ${res.status}): ${body?.error ?? "unexpected response"}`);
|
|
1112
|
+
}
|
|
1113
|
+
return body.data;
|
|
1114
|
+
}
|
|
1115
|
+
async function maybeProvisionAccess(bundle, ingress) {
|
|
1116
|
+
const token = bundle.cloudflareToken ?? null;
|
|
1117
|
+
const accountId = bundle.cloudflareAccountId ?? null;
|
|
1118
|
+
if (!token || !accountId) return;
|
|
1119
|
+
const services = [];
|
|
1120
|
+
const skipped = [];
|
|
1121
|
+
for (const [name, hostname] of Object.entries(ingress.hostnames)) {
|
|
1122
|
+
const cfg = GATED_SERVICES[name];
|
|
1123
|
+
if (cfg) services.push({ hostname, strict: cfg.strict });
|
|
1124
|
+
else skipped.push(name);
|
|
1125
|
+
}
|
|
1126
|
+
if (skipped.length > 0) {
|
|
1127
|
+
console.log(`[entrypoint] CF Access: leaving machine surface(s) ungated: ${skipped.join(", ")}`);
|
|
1128
|
+
}
|
|
1129
|
+
if (services.length === 0) {
|
|
1130
|
+
console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview not provisioned)");
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
|
|
1134
|
+
const pat = requireEnv("LS_PAT");
|
|
1135
|
+
const stateFile = "/workspace/.launchpod/launch-kit-access.json";
|
|
1136
|
+
console.log(`[entrypoint] gating ${services.map((s) => s.hostname).join(", ")} behind CF Access (IdP: ${serverUrl})`);
|
|
1137
|
+
const result = await provisionAccess({
|
|
1138
|
+
apiToken: token,
|
|
1139
|
+
accountId,
|
|
1140
|
+
issuer: serverUrl,
|
|
1141
|
+
services,
|
|
1142
|
+
stateFile,
|
|
1143
|
+
registerClient: (redirectUris) => registerOidcClient(serverUrl, pat, redirectUris)
|
|
1144
|
+
});
|
|
1145
|
+
console.log(`[entrypoint] CF Access gate live \u2014 IdP ${result.idpId}, auth domain ${result.authDomain}`);
|
|
1146
|
+
}
|
|
901
1147
|
function spawnServiceGroup(services) {
|
|
902
1148
|
const children = [];
|
|
903
1149
|
let shuttingDown = false;
|
|
@@ -990,7 +1236,7 @@ async function main() {
|
|
|
990
1236
|
try {
|
|
991
1237
|
services = resolveServices();
|
|
992
1238
|
} catch (err) {
|
|
993
|
-
|
|
1239
|
+
fail2(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
994
1240
|
}
|
|
995
1241
|
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
996
1242
|
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
@@ -999,8 +1245,9 @@ async function main() {
|
|
|
999
1245
|
const radarFqdn = ingress.hostnames.radar;
|
|
1000
1246
|
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
1001
1247
|
else if (services.some((s) => s.name === "radar")) {
|
|
1002
|
-
|
|
1248
|
+
fail2(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
1003
1249
|
}
|
|
1250
|
+
await maybeProvisionAccess(bundle, ingress);
|
|
1004
1251
|
} else if (services.length > 1) {
|
|
1005
1252
|
const first = services[0];
|
|
1006
1253
|
console.warn(
|
|
@@ -1018,22 +1265,29 @@ async function main() {
|
|
|
1018
1265
|
process.exit(1);
|
|
1019
1266
|
}
|
|
1020
1267
|
}
|
|
1021
|
-
var import_node_child_process2,
|
|
1268
|
+
var import_node_child_process2, import_node_fs4, import_node_path4, REQUIRED_ENV, GATED_SERVICES;
|
|
1022
1269
|
var init_radar_docker_init_entry = __esm({
|
|
1023
1270
|
"src/server/radar-docker-init-entry.ts"() {
|
|
1024
1271
|
"use strict";
|
|
1025
1272
|
import_node_child_process2 = require("node:child_process");
|
|
1026
|
-
|
|
1027
|
-
|
|
1273
|
+
import_node_fs4 = require("node:fs");
|
|
1274
|
+
import_node_path4 = require("node:path");
|
|
1028
1275
|
init_mcp();
|
|
1029
1276
|
init_launch_kit_services();
|
|
1030
1277
|
init_cf_ingress();
|
|
1278
|
+
init_cf_access();
|
|
1031
1279
|
REQUIRED_ENV = [
|
|
1032
1280
|
"CLAUDE_CREDENTIALS_B64",
|
|
1033
1281
|
"LS_PAT",
|
|
1034
1282
|
"LS_ORG_SLUG",
|
|
1035
1283
|
"LS_PROJECT_SLUG"
|
|
1036
1284
|
];
|
|
1285
|
+
GATED_SERVICES = {
|
|
1286
|
+
// Claude web terminal — live drivable shell ⇒ RCE surface ⇒ short session.
|
|
1287
|
+
bot: { strict: true },
|
|
1288
|
+
// The user's own dev/preview server — a workday-length session is fine.
|
|
1289
|
+
preview: { strict: false }
|
|
1290
|
+
};
|
|
1037
1291
|
if (!process.env.VITEST) {
|
|
1038
1292
|
main().catch((err) => {
|
|
1039
1293
|
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1226,6 +1480,24 @@ function footer(msg, hints = []) {
|
|
|
1226
1480
|
function warn(msg) {
|
|
1227
1481
|
console.log(` ${c.yellow("\u26A0")} ${msg}`);
|
|
1228
1482
|
}
|
|
1483
|
+
var ALL_STEP_IDS = [
|
|
1484
|
+
"resolve",
|
|
1485
|
+
"clone",
|
|
1486
|
+
"cred",
|
|
1487
|
+
"mcp",
|
|
1488
|
+
"gitignore",
|
|
1489
|
+
"install",
|
|
1490
|
+
"onboard",
|
|
1491
|
+
"recall",
|
|
1492
|
+
"migrate-safety",
|
|
1493
|
+
"ls-marketplace",
|
|
1494
|
+
"recall-hook",
|
|
1495
|
+
"statusline"
|
|
1496
|
+
];
|
|
1497
|
+
var PRESETS = {
|
|
1498
|
+
init: [],
|
|
1499
|
+
refresh: ["resolve", "clone", "install", "onboard", "recall"]
|
|
1500
|
+
};
|
|
1229
1501
|
var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
|
|
1230
1502
|
var LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD = `
|
|
1231
1503
|
Wired in Claude Code (.mcp.json):
|
|
@@ -1305,7 +1577,16 @@ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
1305
1577
|
"--guide",
|
|
1306
1578
|
"--no-guide"
|
|
1307
1579
|
]);
|
|
1308
|
-
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity"]);
|
|
1580
|
+
var KNOWN_KV_KEYS = /* @__PURE__ */ new Set(["token", "org", "project", "url", "dir", "course", "git-identity", "preset", "skip", "only", "with"]);
|
|
1581
|
+
function parseStepList(val, flag) {
|
|
1582
|
+
const ids = val.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
1583
|
+
const bad = ids.filter((id) => !ALL_STEP_IDS.includes(id));
|
|
1584
|
+
if (bad.length > 0) {
|
|
1585
|
+
fail3(`${flag}: unknown step id(s): ${bad.join(", ")}
|
|
1586
|
+
Known steps: ${ALL_STEP_IDS.join(", ")}`);
|
|
1587
|
+
}
|
|
1588
|
+
return ids;
|
|
1589
|
+
}
|
|
1309
1590
|
function parseArgs(argv) {
|
|
1310
1591
|
const args = {
|
|
1311
1592
|
token: process.env.LS_PAT ?? null,
|
|
@@ -1327,6 +1608,10 @@ function parseArgs(argv) {
|
|
|
1327
1608
|
dryRun: false,
|
|
1328
1609
|
verbose: false,
|
|
1329
1610
|
guide: null,
|
|
1611
|
+
preset: null,
|
|
1612
|
+
only: null,
|
|
1613
|
+
skip: [],
|
|
1614
|
+
with: [],
|
|
1330
1615
|
help: false
|
|
1331
1616
|
};
|
|
1332
1617
|
const unknown = [];
|
|
@@ -1417,11 +1702,27 @@ function parseArgs(argv) {
|
|
|
1417
1702
|
args.course = val;
|
|
1418
1703
|
continue;
|
|
1419
1704
|
}
|
|
1705
|
+
if (key === "preset") {
|
|
1706
|
+
args.preset = val;
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
if (key === "skip") {
|
|
1710
|
+
args.skip.push(...parseStepList(val, "--skip"));
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
if (key === "with") {
|
|
1714
|
+
args.with.push(...parseStepList(val, "--with"));
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
if (key === "only") {
|
|
1718
|
+
args.only = [...args.only ?? [], ...parseStepList(val, "--only")];
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1420
1721
|
if (key === "git-identity") {
|
|
1421
1722
|
try {
|
|
1422
1723
|
args.gitIdentity = parseGitIdentityFlag(val);
|
|
1423
1724
|
} catch (err) {
|
|
1424
|
-
|
|
1725
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
1425
1726
|
}
|
|
1426
1727
|
continue;
|
|
1427
1728
|
}
|
|
@@ -1437,7 +1738,7 @@ function parseArgs(argv) {
|
|
|
1437
1738
|
if (unknown.length > 0) {
|
|
1438
1739
|
const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
|
|
1439
1740
|
const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
|
|
1440
|
-
|
|
1741
|
+
fail3(`Unknown argument(s): ${unknown.join(" ")}
|
|
1441
1742
|
Known boolean flags: ${knownBool}
|
|
1442
1743
|
Known key=value flags: ${knownKv}`);
|
|
1443
1744
|
}
|
|
@@ -1501,9 +1802,12 @@ function printHelp() {
|
|
|
1501
1802
|
console.log(`launch-kit \u2014 bootstrap and refresh a LaunchSecure project on this machine
|
|
1502
1803
|
|
|
1503
1804
|
Subcommands:
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1805
|
+
run One flow, step-selectable. \`--preset=init|refresh\` plus
|
|
1806
|
+
\`--skip\` / \`--only\` / \`--with\` choose which steps fire.
|
|
1807
|
+
\`init\` and \`refresh\` are presets over this same flow.
|
|
1808
|
+
init Preset of \`run\` \u2014 full bootstrap (clone, cred, MCP, scaffolds, install)
|
|
1809
|
+
refresh Preset of \`run\` \u2014 re-apply scaffolds + MCP entries only
|
|
1810
|
+
(skips resolve/clone/install/onboard/recall \u2014 see \`launch-kit refresh --help\`)
|
|
1507
1811
|
setup-git Configure git identity + gh credential helper in one
|
|
1508
1812
|
shot. Use in containers / CI where init isn't needed.
|
|
1509
1813
|
\`launch-kit setup-git --identity="Name <email>"\`.
|
|
@@ -1580,6 +1884,20 @@ Options:
|
|
|
1580
1884
|
launch-secure MCP entry) and runs refresh instead.
|
|
1581
1885
|
Pass --force to re-init from scratch even when the
|
|
1582
1886
|
target dir is already set up.
|
|
1887
|
+
|
|
1888
|
+
Step selection (one-command flow \u2014 compose over the preset):
|
|
1889
|
+
Steps: resolve, clone, cred, mcp, gitignore, install, onboard, recall,
|
|
1890
|
+
migrate-safety, ls-marketplace, recall-hook, statusline
|
|
1891
|
+
--preset=<name> init (everything) or refresh (skips resolve/clone/
|
|
1892
|
+
install/onboard/recall). Default: init. \`init\` and
|
|
1893
|
+
\`refresh\` subcommands just select this.
|
|
1894
|
+
--skip=<a,b> Remove steps from the preset (e.g. --skip=clone,install).
|
|
1895
|
+
Legacy --no-* flags are sugar for this.
|
|
1896
|
+
--with=<a,b> Add steps back onto the preset.
|
|
1897
|
+
--only=<a,b> Run exactly these steps, ignoring the preset.
|
|
1898
|
+
(Dependent steps auto-skip if their prerequisite isn't
|
|
1899
|
+
selected: clone needs resolve, onboard needs install,
|
|
1900
|
+
mcp needs cred.)
|
|
1583
1901
|
--dry-run Preview every file write, merge, clone, and install
|
|
1584
1902
|
command without making any changes. Useful before
|
|
1585
1903
|
re-running init against a customized project. The
|
|
@@ -1621,7 +1939,7 @@ async function prompt(question) {
|
|
|
1621
1939
|
resolve3(answer.trim());
|
|
1622
1940
|
}));
|
|
1623
1941
|
}
|
|
1624
|
-
function
|
|
1942
|
+
function fail3(msg) {
|
|
1625
1943
|
console.error(`[launch-kit] \u2717 ${msg}`);
|
|
1626
1944
|
process.exit(1);
|
|
1627
1945
|
}
|
|
@@ -1643,8 +1961,8 @@ function which(bin) {
|
|
|
1643
1961
|
}
|
|
1644
1962
|
function preflight() {
|
|
1645
1963
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
1646
|
-
if (nodeMajor < 18)
|
|
1647
|
-
if (!which("git"))
|
|
1964
|
+
if (nodeMajor < 18) fail3(`Node.js >= 18 required (current: ${process.versions.node}).`);
|
|
1965
|
+
if (!which("git")) fail3("git not found in PATH. Install git: https://git-scm.com/downloads");
|
|
1648
1966
|
const hasGh2 = which("gh") !== null;
|
|
1649
1967
|
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh2 ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
1650
1968
|
return { hasGh: hasGh2 };
|
|
@@ -1819,12 +2137,12 @@ function cloneRepo(repoUrl, targetDir, hasGh2) {
|
|
|
1819
2137
|
}
|
|
1820
2138
|
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
1821
2139
|
if (res.status !== 0) {
|
|
1822
|
-
|
|
2140
|
+
fail3(
|
|
1823
2141
|
`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
2142
|
);
|
|
1825
2143
|
}
|
|
1826
2144
|
if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
|
|
1827
|
-
|
|
2145
|
+
fail3(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
|
|
1828
2146
|
}
|
|
1829
2147
|
ok(`cloned to ${targetDir}`);
|
|
1830
2148
|
}
|
|
@@ -1915,7 +2233,7 @@ function mergeMcpFile(targetDir, launchKitEntries) {
|
|
|
1915
2233
|
try {
|
|
1916
2234
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1917
2235
|
} catch (err) {
|
|
1918
|
-
|
|
2236
|
+
fail3(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1919
2237
|
}
|
|
1920
2238
|
}
|
|
1921
2239
|
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
@@ -1975,7 +2293,7 @@ function detectPackageManager(repoDir) {
|
|
|
1975
2293
|
function runInstall(repoDir, detected) {
|
|
1976
2294
|
const { pm } = detected;
|
|
1977
2295
|
if (!which(pm.binary)) {
|
|
1978
|
-
|
|
2296
|
+
fail3(
|
|
1979
2297
|
`${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
2298
|
);
|
|
1981
2299
|
}
|
|
@@ -1986,7 +2304,7 @@ function runInstall(repoDir, detected) {
|
|
|
1986
2304
|
}
|
|
1987
2305
|
const res = (0, import_node_child_process3.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
1988
2306
|
if (res.status !== 0) {
|
|
1989
|
-
|
|
2307
|
+
fail3(
|
|
1990
2308
|
`${pm.name} install failed (exit ${res.status}).
|
|
1991
2309
|
|
|
1992
2310
|
Half-init state \u2014 install didn't complete, but these files ARE on disk:
|
|
@@ -2043,7 +2361,7 @@ function runOnboard(repoDir, pm) {
|
|
|
2043
2361
|
}
|
|
2044
2362
|
const res = (0, import_node_child_process3.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
2045
2363
|
if (res.status !== 0) {
|
|
2046
|
-
|
|
2364
|
+
fail3(
|
|
2047
2365
|
`${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
2366
|
);
|
|
2049
2367
|
}
|
|
@@ -2234,7 +2552,7 @@ function wireLsSettings(targetDir) {
|
|
|
2234
2552
|
try {
|
|
2235
2553
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
2236
2554
|
} catch (err) {
|
|
2237
|
-
|
|
2555
|
+
fail3(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
2238
2556
|
}
|
|
2239
2557
|
}
|
|
2240
2558
|
const merged = { ...existing };
|
|
@@ -2288,7 +2606,7 @@ function wireRecallHook(targetDir) {
|
|
|
2288
2606
|
try {
|
|
2289
2607
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
2290
2608
|
} catch (err) {
|
|
2291
|
-
|
|
2609
|
+
fail3(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
2292
2610
|
}
|
|
2293
2611
|
}
|
|
2294
2612
|
const hooks = existing.hooks ?? {};
|
|
@@ -2353,14 +2671,14 @@ async function main2() {
|
|
|
2353
2671
|
let identityVal = null;
|
|
2354
2672
|
for (const a of process.argv.slice(3)) {
|
|
2355
2673
|
if (a.startsWith("--identity=")) identityVal = a.slice("--identity=".length);
|
|
2356
|
-
else
|
|
2674
|
+
else fail3(`Unknown setup-git flag: "${a}". Supported: --identity="Name <email>".`);
|
|
2357
2675
|
}
|
|
2358
|
-
if (!identityVal)
|
|
2676
|
+
if (!identityVal) fail3(`launch-kit setup-git requires --identity="Name <email>".`);
|
|
2359
2677
|
try {
|
|
2360
2678
|
const identity = parseGitIdentityFlag(identityVal, "--identity");
|
|
2361
2679
|
configureGitForBot(identity);
|
|
2362
2680
|
} catch (err) {
|
|
2363
|
-
|
|
2681
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2364
2682
|
}
|
|
2365
2683
|
return;
|
|
2366
2684
|
}
|
|
@@ -2380,13 +2698,13 @@ async function main2() {
|
|
|
2380
2698
|
for (const a of process.argv.slice(4)) {
|
|
2381
2699
|
if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
|
|
2382
2700
|
else if (a === "--compact") compactArg = true;
|
|
2383
|
-
else
|
|
2701
|
+
else fail3(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
|
|
2384
2702
|
}
|
|
2385
2703
|
const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
|
|
2386
2704
|
let res;
|
|
2387
2705
|
if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
|
|
2388
2706
|
else if (action === "deactivate") res = deactivateStatusline2();
|
|
2389
|
-
else
|
|
2707
|
+
else fail3(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
|
|
2390
2708
|
if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
2391
2709
|
else info(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
2392
2710
|
return;
|
|
@@ -2404,7 +2722,7 @@ async function main2() {
|
|
|
2404
2722
|
return;
|
|
2405
2723
|
}
|
|
2406
2724
|
if (action !== "pull") {
|
|
2407
|
-
|
|
2725
|
+
fail3(`Unknown secrets action: "${action}". Supported: pull.`);
|
|
2408
2726
|
}
|
|
2409
2727
|
let envOverride = null;
|
|
2410
2728
|
let dirArg = null;
|
|
@@ -2413,14 +2731,14 @@ async function main2() {
|
|
|
2413
2731
|
if (a.startsWith("--env=")) envOverride = a.slice("--env=".length);
|
|
2414
2732
|
else if (a.startsWith("--dir=")) dirArg = a.slice("--dir=".length);
|
|
2415
2733
|
else if (a.startsWith("--file=")) fileArg = a.slice("--file=".length);
|
|
2416
|
-
else
|
|
2734
|
+
else fail3(`Unknown secrets pull flag: "${a}". Supported: --env, --dir, --file.`);
|
|
2417
2735
|
}
|
|
2418
2736
|
const targetDir = path5.resolve(dirArg ?? process.cwd());
|
|
2419
2737
|
const { runSecretsPull: runSecretsPull2 } = await Promise.resolve().then(() => (init_secrets_pull(), secrets_pull_exports));
|
|
2420
2738
|
try {
|
|
2421
2739
|
await runSecretsPull2({ targetDir, envOverride, fileName: fileArg });
|
|
2422
2740
|
} catch (err) {
|
|
2423
|
-
|
|
2741
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2424
2742
|
}
|
|
2425
2743
|
return;
|
|
2426
2744
|
}
|
|
@@ -2431,10 +2749,14 @@ async function main2() {
|
|
|
2431
2749
|
return;
|
|
2432
2750
|
}
|
|
2433
2751
|
if (!subcommand || subcommand.startsWith("--")) {
|
|
2434
|
-
|
|
2752
|
+
fail3(`missing subcommand. Usage: launch-kit <run|init|refresh|statusline|secrets> [options]. Run with --help.`);
|
|
2753
|
+
}
|
|
2754
|
+
if (subcommand !== "init" && subcommand !== "refresh" && subcommand !== "run") {
|
|
2755
|
+
fail3(`Unknown subcommand "${subcommand}". Supported: run, init, refresh, statusline, secrets. Run with --help for usage.`);
|
|
2435
2756
|
}
|
|
2436
|
-
if (subcommand
|
|
2437
|
-
|
|
2757
|
+
if (!args.preset) args.preset = subcommand === "refresh" ? "refresh" : "init";
|
|
2758
|
+
if (!PRESETS[args.preset]) {
|
|
2759
|
+
fail3(`Unknown --preset "${args.preset}". Known presets: ${Object.keys(PRESETS).join(", ")}.`);
|
|
2438
2760
|
}
|
|
2439
2761
|
DRY_RUN = args.dryRun;
|
|
2440
2762
|
VERBOSE = args.verbose || DRY_RUN;
|
|
@@ -2444,102 +2766,30 @@ async function main2() {
|
|
|
2444
2766
|
console.log(c.dim("Lines tagged (dry-run) show what would happen."));
|
|
2445
2767
|
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
2768
|
}
|
|
2447
|
-
|
|
2448
|
-
return mainInit(args);
|
|
2769
|
+
return runFlow(args);
|
|
2449
2770
|
}
|
|
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
|
-
}
|
|
2771
|
+
function resolveEnabledSteps(args) {
|
|
2772
|
+
let enabled;
|
|
2773
|
+
if (args.only) {
|
|
2774
|
+
enabled = new Set(args.only);
|
|
2775
|
+
} else {
|
|
2776
|
+
enabled = new Set(ALL_STEP_IDS);
|
|
2777
|
+
for (const id of PRESETS[args.preset ?? "init"] ?? []) enabled.delete(id);
|
|
2778
|
+
}
|
|
2779
|
+
for (const id of args.skip) enabled.delete(id);
|
|
2780
|
+
for (const id of args.with) enabled.add(id);
|
|
2781
|
+
if (args.noInstall) enabled.delete("install");
|
|
2782
|
+
if (args.noOnboard) enabled.delete("onboard");
|
|
2783
|
+
if (args.noRecall) enabled.delete("recall");
|
|
2784
|
+
if (args.noMigrateSafety) enabled.delete("migrate-safety");
|
|
2785
|
+
if (args.noLsMarketplace) enabled.delete("ls-marketplace");
|
|
2786
|
+
if (args.noRecallHook) enabled.delete("recall-hook");
|
|
2787
|
+
return enabled;
|
|
2788
|
+
}
|
|
2789
|
+
async function stepResolve(ctx) {
|
|
2790
|
+
const { args } = ctx;
|
|
2541
2791
|
if (!args.token || !args.orgSlug || !args.projectSlug) {
|
|
2542
|
-
const recoveryDir = path5.resolve(args.targetDir ??
|
|
2792
|
+
const recoveryDir = path5.resolve(args.targetDir ?? ctx.cwd);
|
|
2543
2793
|
if (fs5.existsSync(recoveryDir)) {
|
|
2544
2794
|
const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
|
|
2545
2795
|
const nested = cred ? toNested(cred) : null;
|
|
@@ -2567,16 +2817,18 @@ async function mainInit(args) {
|
|
|
2567
2817
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
2568
2818
|
args.token = t || null;
|
|
2569
2819
|
}
|
|
2570
|
-
if (!args.token)
|
|
2571
|
-
if (!/^ls_pat_/.test(args.token))
|
|
2572
|
-
if (!args.orgSlug)
|
|
2573
|
-
if (!args.projectSlug)
|
|
2820
|
+
if (!args.token) fail3("--token (or LS_PAT env) is required.");
|
|
2821
|
+
if (!/^ls_pat_/.test(args.token)) fail3("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
|
|
2822
|
+
if (!args.orgSlug) fail3("--org=<orgSlug> is required.");
|
|
2823
|
+
if (!args.projectSlug) fail3("--project=<projectSlug> is required.");
|
|
2574
2824
|
header("launch-kit init", [
|
|
2575
2825
|
["org", args.orgSlug],
|
|
2576
2826
|
["project", args.projectSlug],
|
|
2577
2827
|
["server", args.serverUrl]
|
|
2578
2828
|
]);
|
|
2829
|
+
ctx.headerPrinted = true;
|
|
2579
2830
|
const { hasGh: hasGh2 } = preflight();
|
|
2831
|
+
ctx.hasGh = hasGh2;
|
|
2580
2832
|
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
2833
|
if (args.gitIdentity) {
|
|
2582
2834
|
configureGitForBot(args.gitIdentity);
|
|
@@ -2589,120 +2841,271 @@ async function mainInit(args) {
|
|
|
2589
2841
|
try {
|
|
2590
2842
|
resolved = await callProjectInfo(args);
|
|
2591
2843
|
} catch (err) {
|
|
2592
|
-
|
|
2844
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2593
2845
|
}
|
|
2594
2846
|
ok(`resolved "${resolved.projectName}"`);
|
|
2595
2847
|
phase("project_info", { status: "ok", summary: `"${resolved.projectName}"` });
|
|
2596
2848
|
if (!resolved.repositoryUrl) {
|
|
2597
|
-
|
|
2849
|
+
fail3(
|
|
2598
2850
|
`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
2851
|
);
|
|
2600
2852
|
}
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2853
|
+
ctx.resolved = resolved;
|
|
2854
|
+
ctx.repoUrl = resolved.repositoryUrl;
|
|
2855
|
+
ctx.targetDir = path5.resolve(args.targetDir ?? path5.join(ctx.cwd, resolved.projectSlug));
|
|
2856
|
+
ctx.cfg = { pat: args.token, orgSlug: resolved.orgSlug, projectSlug: resolved.projectSlug, serverUrl: args.serverUrl };
|
|
2857
|
+
ctx.courseName = args.course ?? inferCourseName(ctx.cfg.serverUrl);
|
|
2858
|
+
}
|
|
2859
|
+
function stepClone(ctx) {
|
|
2860
|
+
const repoUrl = ctx.repoUrl;
|
|
2861
|
+
const { targetDir, cwd } = ctx;
|
|
2604
2862
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
2605
|
-
let skipClone = false;
|
|
2606
2863
|
if (fs5.existsSync(targetDir)) {
|
|
2607
2864
|
if (isGitRepo(targetDir)) {
|
|
2608
2865
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
2609
2866
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
2610
2867
|
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 wiring this existing repo (skipping clone, no GitHub auth needed)`);
|
|
2611
2868
|
info(`tip: once wired, use \`launch-kit refresh\` here to re-apply configs without re-running init`);
|
|
2612
|
-
skipClone = true;
|
|
2869
|
+
ctx.skipClone = true;
|
|
2613
2870
|
} else {
|
|
2614
|
-
|
|
2871
|
+
fail3(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
|
|
2615
2872
|
}
|
|
2616
2873
|
} else if (!dirIsEmpty(targetDir)) {
|
|
2617
|
-
|
|
2874
|
+
fail3(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
|
|
2618
2875
|
}
|
|
2619
2876
|
}
|
|
2620
2877
|
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
2621
|
-
if (!skipClone) {
|
|
2878
|
+
if (!ctx.skipClone) {
|
|
2622
2879
|
section(`Cloning ${repoUrl}`);
|
|
2623
|
-
cloneRepo(repoUrl, targetDir,
|
|
2880
|
+
cloneRepo(repoUrl, targetDir, ctx.hasGh);
|
|
2624
2881
|
phase("clone", { status: "ok", summary: `\u2192 ${relTarget}` });
|
|
2625
2882
|
} else {
|
|
2626
2883
|
phase("clone", { status: "in-sync", summary: `${relTarget} (already a clone of this repo)` });
|
|
2627
2884
|
}
|
|
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
|
-
}
|
|
2885
|
+
}
|
|
2886
|
+
function stepCred(ctx) {
|
|
2887
|
+
const { args, cwd } = ctx;
|
|
2888
|
+
if (ctx.cfg && ctx.resolved) {
|
|
2889
|
+
writeConfigFile(ctx.targetDir, ctx.cfg, ctx.courseName);
|
|
2890
|
+
phase("cred file", { status: "ok", summary: `course=${ctx.courseName}, active` });
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
const targetDir = ctx.targetDir;
|
|
2894
|
+
if (!fs5.existsSync(targetDir)) fail3(`target dir does not exist: ${targetDir}`);
|
|
2895
|
+
let cred;
|
|
2896
|
+
let source;
|
|
2897
|
+
try {
|
|
2898
|
+
const recovery = recoverCred(targetDir, getRecoveryOptions());
|
|
2899
|
+
cred = recovery.cred;
|
|
2900
|
+
source = recovery.source;
|
|
2901
|
+
} catch (err) {
|
|
2902
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2903
|
+
}
|
|
2904
|
+
if (cred && source === "mcp") {
|
|
2905
|
+
info(`recovered cred from .mcp.json launch-secure headers (PAT + org + project + url)`);
|
|
2906
|
+
const courseName = inferCourseName(cred.serverUrl);
|
|
2907
|
+
const nested2 = upsertProfile(null, courseName, cred);
|
|
2908
|
+
if (DRY_RUN) {
|
|
2909
|
+
dryNote(`would write ${CONFIG_FILENAME} from recovered .mcp.json cred (course: ${courseName})`);
|
|
2910
|
+
} else {
|
|
2911
|
+
writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
|
|
2912
|
+
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
2672
2913
|
}
|
|
2673
2914
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2915
|
+
if (!cred && args.token && args.orgSlug && args.projectSlug) {
|
|
2916
|
+
const courseName = args.course ?? inferCourseName(args.serverUrl);
|
|
2917
|
+
const seedCfg = {
|
|
2918
|
+
pat: args.token,
|
|
2919
|
+
orgSlug: args.orgSlug,
|
|
2920
|
+
projectSlug: args.projectSlug,
|
|
2921
|
+
serverUrl: args.serverUrl
|
|
2922
|
+
};
|
|
2923
|
+
info(`no ${CONFIG_FILENAME} found \u2014 seeding it from --token/--org/--project (course: ${courseName})`);
|
|
2924
|
+
writeConfigFile(targetDir, seedCfg, courseName);
|
|
2925
|
+
cred = { active: courseName, profiles: { [courseName]: seedCfg } };
|
|
2926
|
+
}
|
|
2927
|
+
if (!cred) {
|
|
2928
|
+
fail3(
|
|
2929
|
+
`no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json.
|
|
2930
|
+
Refresh re-applies configs to an already-wired checkout, so it needs a cred. Two ways forward:
|
|
2931
|
+
\u2022 Seed it inline (you already have the repo): re-run with credentials \u2014
|
|
2932
|
+
launch-kit refresh --dir=${path5.relative(cwd, targetDir) || "."} --token=<pat> --org=<org> --project=<project>
|
|
2933
|
+
\u2022 Or run a full init in place \u2014 it detects this existing repo and skips the clone:
|
|
2934
|
+
launch-kit init --token=<pat> --org=<org> --project=<project> --dir=${path5.relative(cwd, targetDir) || "."}`
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2937
|
+
const nested = toNested(cred);
|
|
2938
|
+
if (!nested) fail3(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
|
|
2939
|
+
const active = nested.profiles[nested.active];
|
|
2940
|
+
if (!active) fail3(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
|
|
2941
|
+
ctx.cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
|
|
2942
|
+
ctx.courseName = nested.active;
|
|
2943
|
+
info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
|
|
2944
|
+
if (!ctx.headerPrinted) {
|
|
2945
|
+
header("launch-kit refresh", [
|
|
2946
|
+
["course", nested.active],
|
|
2947
|
+
["project", `${active.orgSlug}/${active.projectSlug}`],
|
|
2948
|
+
["dir", path5.relative(cwd, targetDir) || "."]
|
|
2949
|
+
]);
|
|
2950
|
+
ctx.headerPrinted = true;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
function stepMcp(ctx) {
|
|
2954
|
+
phase(".mcp.json", mergeMcpFile(ctx.targetDir, buildLaunchKitMcpEntries(ctx.cfg)));
|
|
2955
|
+
}
|
|
2956
|
+
function stepGitignore(ctx) {
|
|
2957
|
+
ensureGitignoreLine(ctx.targetDir, CONFIG_FILENAME);
|
|
2958
|
+
}
|
|
2959
|
+
function stepInstall(ctx) {
|
|
2960
|
+
const detected = detectPackageManager(ctx.targetDir);
|
|
2961
|
+
ctx.detected = detected;
|
|
2962
|
+
if (detected) info(`detected package manager: ${detected.pm.name} (${detected.source})`);
|
|
2963
|
+
if (!detected) {
|
|
2964
|
+
ctx.installSkippedReason = "no package.json found";
|
|
2965
|
+
phase("install", { status: "skipped", summary: ctx.installSkippedReason });
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
section(`Installing dependencies (${detected.pm.name})`);
|
|
2969
|
+
runInstall(ctx.targetDir, detected);
|
|
2970
|
+
ctx.installRan = true;
|
|
2971
|
+
phase("install", { status: "ok", summary: `${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` });
|
|
2972
|
+
}
|
|
2973
|
+
async function stepOnboard(ctx) {
|
|
2974
|
+
if (!ctx.installRan || !ctx.detected) return;
|
|
2975
|
+
const { detected, targetDir, cwd } = ctx;
|
|
2976
|
+
if (hasOnboardScript(targetDir)) {
|
|
2977
|
+
section(`Running ${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}`);
|
|
2978
|
+
runOnboard(targetDir, detected.pm);
|
|
2979
|
+
phase("onboard", { status: "ok", summary: `${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME}` });
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
section("Pulling environment secrets");
|
|
2983
|
+
info("running launch-kit secrets pull \u2026");
|
|
2984
|
+
if (DRY_RUN) {
|
|
2985
|
+
dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
|
|
2986
|
+
phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
try {
|
|
2990
|
+
await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
|
|
2991
|
+
phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
|
|
2992
|
+
} catch (err) {
|
|
2993
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2994
|
+
warn(`secrets pull skipped \u2014 ${msg}`);
|
|
2995
|
+
phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
|
|
2680
2996
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2997
|
+
}
|
|
2998
|
+
function stepRecall(ctx) {
|
|
2999
|
+
section("Initializing launch-recall (shadow git backup)");
|
|
3000
|
+
runRecallInit(ctx.targetDir);
|
|
3001
|
+
phase("launch-recall", { status: "ok", summary: "shadow git ready" });
|
|
3002
|
+
}
|
|
3003
|
+
function stepMigrateSafety(ctx) {
|
|
3004
|
+
phase("migrate-safety", scaffoldMigrateSafety(ctx.targetDir, ctx.args.refreshScaffolds));
|
|
3005
|
+
}
|
|
3006
|
+
function stepLsMarketplace(ctx) {
|
|
3007
|
+
phase("ls-marketplace", scaffoldLsMarketplace(ctx.targetDir));
|
|
3008
|
+
}
|
|
3009
|
+
function stepRecallHook(ctx) {
|
|
3010
|
+
phase("recall-hook", scaffoldRecallHook(ctx.targetDir));
|
|
3011
|
+
}
|
|
3012
|
+
function stepStatusline(_ctx) {
|
|
2684
3013
|
const slR = tryActivateStatusline();
|
|
2685
3014
|
if (slR) phase("statusline", slR);
|
|
3015
|
+
}
|
|
3016
|
+
var STEPS = [
|
|
3017
|
+
{ id: "resolve", run: stepResolve },
|
|
3018
|
+
{ id: "clone", requires: ["resolve"], run: stepClone },
|
|
3019
|
+
{ id: "cred", run: stepCred },
|
|
3020
|
+
{ id: "mcp", requires: ["cred"], run: stepMcp },
|
|
3021
|
+
{ id: "gitignore", run: stepGitignore },
|
|
3022
|
+
{ id: "install", run: stepInstall },
|
|
3023
|
+
{ id: "onboard", requires: ["install"], run: stepOnboard },
|
|
3024
|
+
{ id: "recall", run: stepRecall },
|
|
3025
|
+
{ id: "migrate-safety", run: stepMigrateSafety },
|
|
3026
|
+
{ id: "ls-marketplace", run: stepLsMarketplace },
|
|
3027
|
+
{ id: "recall-hook", run: stepRecallHook },
|
|
3028
|
+
{ id: "statusline", run: stepStatusline }
|
|
3029
|
+
];
|
|
3030
|
+
function buildCtx(args, enabled) {
|
|
3031
|
+
const cwd = process.cwd();
|
|
3032
|
+
const targetDir = path5.resolve(args.targetDir ?? cwd);
|
|
3033
|
+
if (enabled.has("resolve") && !args.force && fs5.existsSync(targetDir)) {
|
|
3034
|
+
const detection = detectExistingBootstrap(targetDir);
|
|
3035
|
+
if (detection.bootstrapped) {
|
|
3036
|
+
info(`detected existing bootstrap at ${targetDir} (${detection.reason})`);
|
|
3037
|
+
info(`delegating to refresh. Pass --force to re-init from scratch (will re-prompt for PAT if needed).`);
|
|
3038
|
+
for (const id of PRESETS.refresh) enabled.delete(id);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
return {
|
|
3042
|
+
args,
|
|
3043
|
+
enabled,
|
|
3044
|
+
cwd,
|
|
3045
|
+
targetDir,
|
|
3046
|
+
hasGh: false,
|
|
3047
|
+
cfg: null,
|
|
3048
|
+
courseName: null,
|
|
3049
|
+
resolved: null,
|
|
3050
|
+
repoUrl: null,
|
|
3051
|
+
skipClone: false,
|
|
3052
|
+
headerPrinted: false,
|
|
3053
|
+
detected: null,
|
|
3054
|
+
installRan: false,
|
|
3055
|
+
installSkippedReason: null
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
async function runFlow(args) {
|
|
3059
|
+
const enabled = resolveEnabledSteps(args);
|
|
3060
|
+
const ctx = buildCtx(args, enabled);
|
|
3061
|
+
for (const step of STEPS) {
|
|
3062
|
+
if (!ctx.enabled.has(step.id)) continue;
|
|
3063
|
+
if (step.requires?.some((r) => !ctx.enabled.has(r))) continue;
|
|
3064
|
+
await step.run(ctx);
|
|
3065
|
+
}
|
|
3066
|
+
epilogue(ctx);
|
|
3067
|
+
}
|
|
3068
|
+
function epilogue(ctx) {
|
|
3069
|
+
const { args, cwd, targetDir } = ctx;
|
|
3070
|
+
const isRefreshLike = !ctx.enabled.has("resolve");
|
|
3071
|
+
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
2686
3072
|
if (DRY_RUN) {
|
|
2687
3073
|
console.log("");
|
|
2688
3074
|
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
|
-
|
|
3075
|
+
if (isRefreshLike) {
|
|
3076
|
+
console.log(c.bold("DRY RUN COMPLETE") + c.dim(" \u2014 refresh would have applied the above; no files modified."));
|
|
3077
|
+
} else {
|
|
3078
|
+
console.log(c.bold("DRY RUN COMPLETE") + c.dim(` \u2014 no files were modified, no commands ran.`));
|
|
3079
|
+
console.log(c.dim(`Target: ${targetDir}`));
|
|
3080
|
+
console.log(c.dim(`Re-run without --dry-run to apply the changes shown above.`));
|
|
3081
|
+
}
|
|
2692
3082
|
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
3083
|
return;
|
|
2694
3084
|
}
|
|
2695
|
-
|
|
3085
|
+
if (isRefreshLike) {
|
|
3086
|
+
ok(`refresh complete \u2014 restart Claude Code to pick up any new /kit:* commands`);
|
|
3087
|
+
const showGuide2 = args.quiet ? false : args.guide === true;
|
|
3088
|
+
const hints = ["Restart Claude Code to pick up any new /kit:* commands."];
|
|
3089
|
+
if (!showGuide2 && !args.quiet) hints.push("Run with --guide to print the full MCP + slash-command catalog. --verbose for per-file detail.");
|
|
3090
|
+
footer("Refresh complete.", hints);
|
|
3091
|
+
if (showGuide2) {
|
|
3092
|
+
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)`));
|
|
3093
|
+
console.log(getLaunchKitToolsGuide());
|
|
3094
|
+
}
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
const projectName = ctx.resolved?.projectName ?? relTarget;
|
|
3098
|
+
ok(`done \u2014 ${projectName} is ready at ${targetDir}`);
|
|
2696
3099
|
const showGuide = args.quiet ? false : args.guide ?? true;
|
|
2697
3100
|
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`);
|
|
3101
|
+
if (ctx.installSkippedReason) {
|
|
3102
|
+
nextSteps.push(ctx.detected ? `${ctx.detected.pm.binary} ${ctx.detected.pm.installArgs.join(" ")} # install skipped: ${ctx.installSkippedReason}` : `npm install # install skipped: ${ctx.installSkippedReason}`);
|
|
3103
|
+
if (hasOnboardScript(targetDir) && ctx.detected && ctx.enabled.has("onboard")) {
|
|
3104
|
+
nextSteps.push(`${ctx.detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook`);
|
|
2702
3105
|
}
|
|
2703
3106
|
}
|
|
2704
3107
|
nextSteps.push("claude # launch Claude Code (5 MCPs wired)");
|
|
2705
|
-
footer(`${
|
|
3108
|
+
footer(`${projectName} is ready at ${relTarget}.`, [
|
|
2706
3109
|
"Next:",
|
|
2707
3110
|
...nextSteps.map((s) => " " + s)
|
|
2708
3111
|
]);
|