@launchsecure/launch-kit 0.0.34 → 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 +277 -33
- package/dist/server/init-entry.js +741 -230
- package/dist/server/launch-bot-entry.js +4078 -0
- package/dist/server/orbit-entry.js +969 -136
- package/dist/server/radar-docker-init-entry.js +326 -32
- package/dist/server/rover-entry.js +624 -124
- package/package.json +4 -3
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/SKILL.md +53 -22
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/briefs.mjs +152 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/kickoff/SKILL.md +167 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/orbit/SKILL.md +41 -9
|
@@ -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)";
|
|
@@ -473,10 +479,15 @@ function defaultServices() {
|
|
|
473
479
|
return [expandShorthand("radar")];
|
|
474
480
|
}
|
|
475
481
|
function expandShorthand(name) {
|
|
482
|
+
if (name === "preview") {
|
|
483
|
+
const raw = process.env.PREVIEW_PORT;
|
|
484
|
+
const port = raw && Number.isFinite(Number.parseInt(raw, 10)) ? Number.parseInt(raw, 10) : 3e3;
|
|
485
|
+
return { name: "preview", port, bin: "", args: [], skipSpawn: true };
|
|
486
|
+
}
|
|
476
487
|
const def = SHORTHANDS[name];
|
|
477
488
|
if (!def) {
|
|
478
489
|
throw new Error(
|
|
479
|
-
`[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${Object.keys(SHORTHANDS).join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
|
|
490
|
+
`[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${[...Object.keys(SHORTHANDS), "preview"].join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
|
|
480
491
|
);
|
|
481
492
|
}
|
|
482
493
|
return { name, port: def.port, bin: def.bin, args: [...def.args] };
|
|
@@ -568,14 +579,20 @@ var init_launch_kit_services = __esm({
|
|
|
568
579
|
sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
|
|
569
580
|
chart: { port: 52819, bin: "launch-chart", args: ["serve"] },
|
|
570
581
|
deck: { port: 52829, bin: "launch-deck", args: ["serve"] },
|
|
571
|
-
|
|
582
|
+
// Claude web terminal — exposes a viewable/drivable `claude` session at
|
|
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).
|
|
585
|
+
bot: { port: 52849, bin: "launch-bot", args: ["serve"] }
|
|
572
586
|
};
|
|
573
587
|
DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
574
|
-
SHORTHAND_NAMES = Object.keys(SHORTHANDS);
|
|
588
|
+
SHORTHAND_NAMES = [...Object.keys(SHORTHANDS), "preview"];
|
|
575
589
|
}
|
|
576
590
|
});
|
|
577
591
|
|
|
578
592
|
// src/server/cf-ingress.ts
|
|
593
|
+
function serviceLabel(s) {
|
|
594
|
+
return s.label ?? s.name;
|
|
595
|
+
}
|
|
579
596
|
async function cf(opts) {
|
|
580
597
|
const res = await fetch(`${CF_API_BASE}${opts.path}`, {
|
|
581
598
|
method: opts.method,
|
|
@@ -655,7 +672,7 @@ async function fetchConnectorToken(input, tunnelId) {
|
|
|
655
672
|
}
|
|
656
673
|
async function setIngressConfig(input, tunnelId) {
|
|
657
674
|
const ingress = input.services.map((s) => ({
|
|
658
|
-
hostname: `${s
|
|
675
|
+
hostname: `${serviceLabel(s)}.${input.zone.name}`,
|
|
659
676
|
service: `http://localhost:${s.port}`
|
|
660
677
|
}));
|
|
661
678
|
ingress.push({ service: "http_status:404" });
|
|
@@ -670,7 +687,7 @@ async function setIngressConfig(input, tunnelId) {
|
|
|
670
687
|
}
|
|
671
688
|
}
|
|
672
689
|
async function ensureDnsRecord(input, tunnelId, service) {
|
|
673
|
-
const fqdn = `${service
|
|
690
|
+
const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
|
|
674
691
|
const target = `${tunnelId}.cfargotunnel.com`;
|
|
675
692
|
const existing = await cf({
|
|
676
693
|
apiToken: input.apiToken,
|
|
@@ -714,7 +731,7 @@ async function provisionIngress(input) {
|
|
|
714
731
|
await setIngressConfig(input, tunnelId);
|
|
715
732
|
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
716
733
|
const hostnames = {};
|
|
717
|
-
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}`;
|
|
718
735
|
return { tunnelId, connectorToken, hostnames };
|
|
719
736
|
}
|
|
720
737
|
var import_node_fs2, import_node_path2, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
|
|
@@ -728,19 +745,197 @@ var init_cf_ingress = __esm({
|
|
|
728
745
|
}
|
|
729
746
|
});
|
|
730
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
|
+
|
|
731
925
|
// src/server/radar-docker-init-entry.ts
|
|
732
926
|
var radar_docker_init_entry_exports = {};
|
|
733
927
|
__export(radar_docker_init_entry_exports, {
|
|
928
|
+
maybeProvisionAccess: () => maybeProvisionAccess,
|
|
734
929
|
maybeProvisionIngress: () => maybeProvisionIngress,
|
|
735
930
|
spawnServiceGroup: () => spawnServiceGroup
|
|
736
931
|
});
|
|
737
|
-
function
|
|
932
|
+
function fail2(message) {
|
|
738
933
|
console.error(message);
|
|
739
934
|
process.exit(1);
|
|
740
935
|
}
|
|
741
936
|
function requireEnv(name) {
|
|
742
937
|
const v = process.env[name];
|
|
743
|
-
if (!v)
|
|
938
|
+
if (!v) fail2(`ERROR: ${name} is required but not set`);
|
|
744
939
|
return v;
|
|
745
940
|
}
|
|
746
941
|
function run2(cmd, args, stdio = "inherit") {
|
|
@@ -757,13 +952,16 @@ async function setupFromCloud() {
|
|
|
757
952
|
try {
|
|
758
953
|
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
759
954
|
} catch (err) {
|
|
760
|
-
|
|
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.`);
|
|
761
956
|
}
|
|
762
957
|
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
763
958
|
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
764
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
|
+
}
|
|
765
963
|
if (!process.env.GH_TOKEN) {
|
|
766
|
-
|
|
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.`);
|
|
767
965
|
}
|
|
768
966
|
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
769
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}`);
|
|
@@ -771,17 +969,17 @@ async function setupFromCloud() {
|
|
|
771
969
|
}
|
|
772
970
|
function setupClaudeCredentials() {
|
|
773
971
|
const home = process.env.HOME ?? "/home/launchpod";
|
|
774
|
-
const claudeDir = (0,
|
|
775
|
-
(0,
|
|
972
|
+
const claudeDir = (0, import_node_path4.join)(home, ".claude");
|
|
973
|
+
(0, import_node_fs4.mkdirSync)(claudeDir, { recursive: true });
|
|
776
974
|
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
777
|
-
const credsPath = (0,
|
|
778
|
-
(0,
|
|
779
|
-
(0,
|
|
780
|
-
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");
|
|
781
979
|
let cfg = {};
|
|
782
|
-
if ((0,
|
|
980
|
+
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
783
981
|
try {
|
|
784
|
-
cfg = JSON.parse((0,
|
|
982
|
+
cfg = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
|
|
785
983
|
} catch {
|
|
786
984
|
cfg = {};
|
|
787
985
|
}
|
|
@@ -790,18 +988,56 @@ function setupClaudeCredentials() {
|
|
|
790
988
|
cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
|
|
791
989
|
cfg.numStartups = (cfg.numStartups ?? 0) + 1;
|
|
792
990
|
cfg.installMethod = cfg.installMethod ?? "global";
|
|
793
|
-
|
|
794
|
-
|
|
991
|
+
const PREAPPROVED_MCPS = [
|
|
992
|
+
"launch-secure",
|
|
993
|
+
"launch-chart",
|
|
994
|
+
"launch-deck",
|
|
995
|
+
"launch-orbit",
|
|
996
|
+
"launch-recall",
|
|
997
|
+
"launch-beacon",
|
|
998
|
+
"launch-sequencer"
|
|
999
|
+
];
|
|
1000
|
+
const projects = cfg.projects ?? {};
|
|
1001
|
+
const wsKey = "/workspace";
|
|
1002
|
+
const wsProject = projects[wsKey] ?? {};
|
|
1003
|
+
const existingEnabled = Array.isArray(wsProject.enabledMcpjsonServers) ? wsProject.enabledMcpjsonServers : [];
|
|
1004
|
+
const mergedEnabled = Array.from(/* @__PURE__ */ new Set([...existingEnabled, ...PREAPPROVED_MCPS]));
|
|
1005
|
+
wsProject.enabledMcpjsonServers = mergedEnabled;
|
|
1006
|
+
projects[wsKey] = wsProject;
|
|
1007
|
+
cfg.projects = projects;
|
|
1008
|
+
(0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
1009
|
+
(0, import_node_fs4.chmodSync)(configPath, 384);
|
|
795
1010
|
}
|
|
796
1011
|
function setupGitAndGh() {
|
|
797
1012
|
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
798
1013
|
const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
|
|
799
1014
|
const status = run2("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
800
|
-
if (status !== 0)
|
|
1015
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
1016
|
+
}
|
|
1017
|
+
function detectAndSetPreviewPort() {
|
|
1018
|
+
if (process.env.PREVIEW_PORT) return;
|
|
1019
|
+
try {
|
|
1020
|
+
const pkgPath = "/workspace/package.json";
|
|
1021
|
+
if (!(0, import_node_fs4.existsSync)(pkgPath)) return;
|
|
1022
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
1023
|
+
const scripts = pkg.scripts ?? {};
|
|
1024
|
+
const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
|
|
1025
|
+
for (const name of ["dev", "start", "serve"]) {
|
|
1026
|
+
const script = scripts[name];
|
|
1027
|
+
if (typeof script !== "string") continue;
|
|
1028
|
+
const m = script.match(portRe);
|
|
1029
|
+
if (m) {
|
|
1030
|
+
process.env.PREVIEW_PORT = m[1];
|
|
1031
|
+
console.log(`[entrypoint] preview port detected from package.json scripts.${name}: ${m[1]}`);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} catch {
|
|
1036
|
+
}
|
|
801
1037
|
}
|
|
802
1038
|
function initWorkspaceIfEmpty() {
|
|
803
1039
|
process.chdir("/workspace");
|
|
804
|
-
if ((0,
|
|
1040
|
+
if ((0, import_node_fs4.existsSync)(".git")) {
|
|
805
1041
|
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
806
1042
|
return;
|
|
807
1043
|
}
|
|
@@ -814,7 +1050,7 @@ function initWorkspaceIfEmpty() {
|
|
|
814
1050
|
`--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
|
|
815
1051
|
`--dir=/workspace`
|
|
816
1052
|
]);
|
|
817
|
-
if (status !== 0)
|
|
1053
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
818
1054
|
}
|
|
819
1055
|
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
820
1056
|
const token = bundle.cloudflareToken ?? null;
|
|
@@ -822,28 +1058,36 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
822
1058
|
const zones = bundle.cloudflareZones ?? [];
|
|
823
1059
|
if (!token && !accountId && zones.length === 0) return null;
|
|
824
1060
|
if (!token || !accountId) {
|
|
825
|
-
|
|
1061
|
+
fail2(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
826
1062
|
}
|
|
827
1063
|
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
828
1064
|
let chosen = null;
|
|
829
1065
|
if (baseDomain) {
|
|
830
1066
|
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
831
1067
|
if (!chosen) {
|
|
832
|
-
|
|
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.`);
|
|
833
1069
|
}
|
|
834
1070
|
} else if (zones.length === 1) {
|
|
835
1071
|
chosen = { id: zones[0].id, name: zones[0].name };
|
|
836
1072
|
} else {
|
|
837
|
-
|
|
1073
|
+
fail2(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
838
1074
|
}
|
|
839
1075
|
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
840
|
-
|
|
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(",")}`);
|
|
841
1085
|
const result = await provisionIngress({
|
|
842
1086
|
apiToken: token,
|
|
843
1087
|
accountId,
|
|
844
1088
|
zone: chosen,
|
|
845
1089
|
tunnelName: `launch-kit-${projectSlug}`,
|
|
846
|
-
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 })),
|
|
847
1091
|
stateFile
|
|
848
1092
|
});
|
|
849
1093
|
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
@@ -851,6 +1095,55 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
851
1095
|
}
|
|
852
1096
|
return result;
|
|
853
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
|
+
}
|
|
854
1147
|
function spawnServiceGroup(services) {
|
|
855
1148
|
const children = [];
|
|
856
1149
|
let shuttingDown = false;
|
|
@@ -898,6 +1191,10 @@ function spawnServiceGroup(services) {
|
|
|
898
1191
|
let exitedCount = 0;
|
|
899
1192
|
let firstFailure = null;
|
|
900
1193
|
for (const spec of services) {
|
|
1194
|
+
if (spec.skipSpawn) {
|
|
1195
|
+
console.log(`[entrypoint] ${spec.name} \u2192 ingress-only on port ${spec.port} (no spawn; user starts dev server here)`);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
901
1198
|
const args = [...spec.args, "--port", String(spec.port)];
|
|
902
1199
|
console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
|
|
903
1200
|
const proc = (0, import_node_child_process2.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -934,11 +1231,12 @@ async function main() {
|
|
|
934
1231
|
setupClaudeCredentials();
|
|
935
1232
|
setupGitAndGh();
|
|
936
1233
|
initWorkspaceIfEmpty();
|
|
1234
|
+
detectAndSetPreviewPort();
|
|
937
1235
|
let services;
|
|
938
1236
|
try {
|
|
939
1237
|
services = resolveServices();
|
|
940
1238
|
} catch (err) {
|
|
941
|
-
|
|
1239
|
+
fail2(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
942
1240
|
}
|
|
943
1241
|
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
944
1242
|
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
@@ -947,8 +1245,9 @@ async function main() {
|
|
|
947
1245
|
const radarFqdn = ingress.hostnames.radar;
|
|
948
1246
|
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
949
1247
|
else if (services.some((s) => s.name === "radar")) {
|
|
950
|
-
|
|
1248
|
+
fail2(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
951
1249
|
}
|
|
1250
|
+
await maybeProvisionAccess(bundle, ingress);
|
|
952
1251
|
} else if (services.length > 1) {
|
|
953
1252
|
const first = services[0];
|
|
954
1253
|
console.warn(
|
|
@@ -966,22 +1265,29 @@ async function main() {
|
|
|
966
1265
|
process.exit(1);
|
|
967
1266
|
}
|
|
968
1267
|
}
|
|
969
|
-
var import_node_child_process2,
|
|
1268
|
+
var import_node_child_process2, import_node_fs4, import_node_path4, REQUIRED_ENV, GATED_SERVICES;
|
|
970
1269
|
var init_radar_docker_init_entry = __esm({
|
|
971
1270
|
"src/server/radar-docker-init-entry.ts"() {
|
|
972
1271
|
"use strict";
|
|
973
1272
|
import_node_child_process2 = require("node:child_process");
|
|
974
|
-
|
|
975
|
-
|
|
1273
|
+
import_node_fs4 = require("node:fs");
|
|
1274
|
+
import_node_path4 = require("node:path");
|
|
976
1275
|
init_mcp();
|
|
977
1276
|
init_launch_kit_services();
|
|
978
1277
|
init_cf_ingress();
|
|
1278
|
+
init_cf_access();
|
|
979
1279
|
REQUIRED_ENV = [
|
|
980
1280
|
"CLAUDE_CREDENTIALS_B64",
|
|
981
1281
|
"LS_PAT",
|
|
982
1282
|
"LS_ORG_SLUG",
|
|
983
1283
|
"LS_PROJECT_SLUG"
|
|
984
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
|
+
};
|
|
985
1291
|
if (!process.env.VITEST) {
|
|
986
1292
|
main().catch((err) => {
|
|
987
1293
|
console.error(`[entrypoint] fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1007,10 +1313,28 @@ var import_node_child_process = require("node:child_process");
|
|
|
1007
1313
|
function run(cmd, args, stdio = "inherit") {
|
|
1008
1314
|
return (0, import_node_child_process.spawnSync)(cmd, args, { stdio }).status ?? 1;
|
|
1009
1315
|
}
|
|
1010
|
-
function
|
|
1011
|
-
|
|
1316
|
+
function hasGh() {
|
|
1317
|
+
return (0, import_node_child_process.spawnSync)("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
1318
|
+
}
|
|
1319
|
+
function wireGitHubAuth() {
|
|
1320
|
+
if (!process.env.GH_TOKEN) return false;
|
|
1321
|
+
if (hasGh()) {
|
|
1012
1322
|
run("gh", ["auth", "setup-git"]);
|
|
1323
|
+
return true;
|
|
1013
1324
|
}
|
|
1325
|
+
const key = "credential.https://github.com.helper";
|
|
1326
|
+
run("git", ["config", "--global", "--replace-all", key, ""]);
|
|
1327
|
+
run("git", [
|
|
1328
|
+
"config",
|
|
1329
|
+
"--global",
|
|
1330
|
+
"--add",
|
|
1331
|
+
key,
|
|
1332
|
+
`!f() { echo username=x-access-token; echo "password=$GH_TOKEN"; }; f`
|
|
1333
|
+
]);
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
function configureGitForBot(identity) {
|
|
1337
|
+
wireGitHubAuth();
|
|
1014
1338
|
run("git", ["config", "--global", "user.name", identity.name]);
|
|
1015
1339
|
run("git", ["config", "--global", "user.email", identity.email]);
|
|
1016
1340
|
run("git", ["config", "--global", "init.defaultBranch", "main"]);
|
|
@@ -1156,6 +1480,24 @@ function footer(msg, hints = []) {
|
|
|
1156
1480
|
function warn(msg) {
|
|
1157
1481
|
console.log(` ${c.yellow("\u26A0")} ${msg}`);
|
|
1158
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
|
+
};
|
|
1159
1501
|
var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
|
|
1160
1502
|
var LAUNCH_KIT_TOOLS_GUIDE_STATIC_HEAD = `
|
|
1161
1503
|
Wired in Claude Code (.mcp.json):
|
|
@@ -1235,7 +1577,16 @@ var KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
|
1235
1577
|
"--guide",
|
|
1236
1578
|
"--no-guide"
|
|
1237
1579
|
]);
|
|
1238
|
-
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
|
+
}
|
|
1239
1590
|
function parseArgs(argv) {
|
|
1240
1591
|
const args = {
|
|
1241
1592
|
token: process.env.LS_PAT ?? null,
|
|
@@ -1257,6 +1608,10 @@ function parseArgs(argv) {
|
|
|
1257
1608
|
dryRun: false,
|
|
1258
1609
|
verbose: false,
|
|
1259
1610
|
guide: null,
|
|
1611
|
+
preset: null,
|
|
1612
|
+
only: null,
|
|
1613
|
+
skip: [],
|
|
1614
|
+
with: [],
|
|
1260
1615
|
help: false
|
|
1261
1616
|
};
|
|
1262
1617
|
const unknown = [];
|
|
@@ -1347,11 +1702,27 @@ function parseArgs(argv) {
|
|
|
1347
1702
|
args.course = val;
|
|
1348
1703
|
continue;
|
|
1349
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
|
+
}
|
|
1350
1721
|
if (key === "git-identity") {
|
|
1351
1722
|
try {
|
|
1352
1723
|
args.gitIdentity = parseGitIdentityFlag(val);
|
|
1353
1724
|
} catch (err) {
|
|
1354
|
-
|
|
1725
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
1355
1726
|
}
|
|
1356
1727
|
continue;
|
|
1357
1728
|
}
|
|
@@ -1367,7 +1738,7 @@ function parseArgs(argv) {
|
|
|
1367
1738
|
if (unknown.length > 0) {
|
|
1368
1739
|
const knownBool = [...KNOWN_BOOL_FLAGS].join(", ");
|
|
1369
1740
|
const knownKv = [...KNOWN_KV_KEYS].map((k) => `--${k}=<value>`).join(", ");
|
|
1370
|
-
|
|
1741
|
+
fail3(`Unknown argument(s): ${unknown.join(" ")}
|
|
1371
1742
|
Known boolean flags: ${knownBool}
|
|
1372
1743
|
Known key=value flags: ${knownKv}`);
|
|
1373
1744
|
}
|
|
@@ -1389,9 +1760,23 @@ What it does:
|
|
|
1389
1760
|
Does NOT clone, re-install deps, re-prompt for PAT, or re-run the onboard
|
|
1390
1761
|
script. Use \`init\` for those.
|
|
1391
1762
|
|
|
1763
|
+
No cred yet? If the repo already exists here but was never init'd (e.g. the
|
|
1764
|
+
repo was created by hand and connected in LS afterward), pass
|
|
1765
|
+
--token/--org/--project and refresh will SEED the cred file, then proceed \u2014
|
|
1766
|
+
no clone, no init required.
|
|
1767
|
+
|
|
1392
1768
|
Options:
|
|
1393
1769
|
--dir=<path> Target directory (default: cwd). Must contain a
|
|
1394
|
-
valid .launch-secure.cred.config
|
|
1770
|
+
valid .launch-secure.cred.config, OR pass the
|
|
1771
|
+
--token/--org/--project flags below to seed one.
|
|
1772
|
+
--token=<ls_pat_\u2026> LaunchSecure PAT. Only needed to seed a cred when
|
|
1773
|
+
none exists yet (otherwise read from the cred file).
|
|
1774
|
+
--org=<orgSlug> Org slug for the seeded cred (with --token).
|
|
1775
|
+
--project=<projectSlug> Project slug for the seeded cred (with --token).
|
|
1776
|
+
--url=<serverUrl> LaunchSecure base URL for the seeded cred
|
|
1777
|
+
(default: ${DEFAULT_SERVER_URL}).
|
|
1778
|
+
--course=<name> Course/profile name for the seeded cred
|
|
1779
|
+
(default: inferred from --url).
|
|
1395
1780
|
--no-migrate-safety Skip refreshing the migrate-safety scaffold.
|
|
1396
1781
|
--no-ls-marketplace Skip refreshing the launch-secure marketplace.
|
|
1397
1782
|
--no-recall-hook Skip refreshing the recall-hook scaffold.
|
|
@@ -1417,9 +1802,12 @@ function printHelp() {
|
|
|
1417
1802
|
console.log(`launch-kit \u2014 bootstrap and refresh a LaunchSecure project on this machine
|
|
1418
1803
|
|
|
1419
1804
|
Subcommands:
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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\`)
|
|
1423
1811
|
setup-git Configure git identity + gh credential helper in one
|
|
1424
1812
|
shot. Use in containers / CI where init isn't needed.
|
|
1425
1813
|
\`launch-kit setup-git --identity="Name <email>"\`.
|
|
@@ -1453,9 +1841,13 @@ Options:
|
|
|
1453
1841
|
--git-identity="N <e>" Non-interactive git identity for service-account /
|
|
1454
1842
|
CI / Docker runs. Configures git user.name, user.email,
|
|
1455
1843
|
init.defaultBranch=main, pull.rebase=false; also
|
|
1456
|
-
wires GH_TOKEN into git's credential helper via
|
|
1457
|
-
\`gh auth setup-git
|
|
1844
|
+
wires GH_TOKEN into git's credential helper (via
|
|
1845
|
+
\`gh auth setup-git\`, or a gh-less helper when the
|
|
1846
|
+
GitHub CLI is absent) when GH_TOKEN is set. Example:
|
|
1458
1847
|
--git-identity="Radar Bot <radar@launchpod.local>".
|
|
1848
|
+
Note: GH_TOKEN is wired for the clone even without
|
|
1849
|
+
--git-identity, so private-repo clones work on hosts
|
|
1850
|
+
with no \`gh\` as long as GH_TOKEN is set.
|
|
1459
1851
|
--no-install Skip dependency install step (also skips the onboard
|
|
1460
1852
|
script \u2014 install is its prerequisite).
|
|
1461
1853
|
--no-onboard Skip the onboard script even when install runs.
|
|
@@ -1492,6 +1884,20 @@ Options:
|
|
|
1492
1884
|
launch-secure MCP entry) and runs refresh instead.
|
|
1493
1885
|
Pass --force to re-init from scratch even when the
|
|
1494
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.)
|
|
1495
1901
|
--dry-run Preview every file write, merge, clone, and install
|
|
1496
1902
|
command without making any changes. Useful before
|
|
1497
1903
|
re-running init against a customized project. The
|
|
@@ -1533,7 +1939,7 @@ async function prompt(question) {
|
|
|
1533
1939
|
resolve3(answer.trim());
|
|
1534
1940
|
}));
|
|
1535
1941
|
}
|
|
1536
|
-
function
|
|
1942
|
+
function fail3(msg) {
|
|
1537
1943
|
console.error(`[launch-kit] \u2717 ${msg}`);
|
|
1538
1944
|
process.exit(1);
|
|
1539
1945
|
}
|
|
@@ -1555,11 +1961,11 @@ function which(bin) {
|
|
|
1555
1961
|
}
|
|
1556
1962
|
function preflight() {
|
|
1557
1963
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
1558
|
-
if (nodeMajor < 18)
|
|
1559
|
-
if (!which("git"))
|
|
1560
|
-
const
|
|
1561
|
-
ok(`preflight ok \u2014 node ${process.versions.node}, git present${
|
|
1562
|
-
return { hasGh };
|
|
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");
|
|
1966
|
+
const hasGh2 = which("gh") !== null;
|
|
1967
|
+
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh2 ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
1968
|
+
return { hasGh: hasGh2 };
|
|
1563
1969
|
}
|
|
1564
1970
|
var PROJECT_INFO_TIMEOUT_MS = 3e4;
|
|
1565
1971
|
var PROJECT_INFO_MAX_ATTEMPTS = 3;
|
|
@@ -1712,11 +2118,11 @@ function dirIsEmpty(dir) {
|
|
|
1712
2118
|
if (!fs5.existsSync(dir)) return true;
|
|
1713
2119
|
return fs5.readdirSync(dir).length === 0;
|
|
1714
2120
|
}
|
|
1715
|
-
function cloneRepo(repoUrl, targetDir,
|
|
2121
|
+
function cloneRepo(repoUrl, targetDir, hasGh2) {
|
|
1716
2122
|
const isGithub = /github\.com/i.test(repoUrl);
|
|
1717
2123
|
let cmd;
|
|
1718
2124
|
let args;
|
|
1719
|
-
if (
|
|
2125
|
+
if (hasGh2 && isGithub) {
|
|
1720
2126
|
cmd = "gh";
|
|
1721
2127
|
args = ["repo", "clone", repoUrl, targetDir];
|
|
1722
2128
|
info(`cloning via gh: ${repoUrl} \u2192 ${targetDir}`);
|
|
@@ -1731,12 +2137,12 @@ function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
|
1731
2137
|
}
|
|
1732
2138
|
const res = (0, import_node_child_process3.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
1733
2139
|
if (res.status !== 0) {
|
|
1734
|
-
|
|
1735
|
-
`Clone failed (${cmd} exited ${res.status}). For private repos make sure your GitHub auth is set up: \`gh auth login
|
|
2140
|
+
fail3(
|
|
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.`
|
|
1736
2142
|
);
|
|
1737
2143
|
}
|
|
1738
2144
|
if (!fs5.existsSync(path5.join(targetDir, ".git"))) {
|
|
1739
|
-
|
|
2145
|
+
fail3(`Clone reported success but .git is missing at ${targetDir}. Possible partial clone, filesystem issue, or sandboxing \u2014 investigate manually.`);
|
|
1740
2146
|
}
|
|
1741
2147
|
ok(`cloned to ${targetDir}`);
|
|
1742
2148
|
}
|
|
@@ -1827,7 +2233,7 @@ function mergeMcpFile(targetDir, launchKitEntries) {
|
|
|
1827
2233
|
try {
|
|
1828
2234
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
1829
2235
|
} catch (err) {
|
|
1830
|
-
|
|
2236
|
+
fail3(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
1831
2237
|
}
|
|
1832
2238
|
}
|
|
1833
2239
|
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
@@ -1887,7 +2293,7 @@ function detectPackageManager(repoDir) {
|
|
|
1887
2293
|
function runInstall(repoDir, detected) {
|
|
1888
2294
|
const { pm } = detected;
|
|
1889
2295
|
if (!which(pm.binary)) {
|
|
1890
|
-
|
|
2296
|
+
fail3(
|
|
1891
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(" ")}`
|
|
1892
2298
|
);
|
|
1893
2299
|
}
|
|
@@ -1898,7 +2304,7 @@ function runInstall(repoDir, detected) {
|
|
|
1898
2304
|
}
|
|
1899
2305
|
const res = (0, import_node_child_process3.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
1900
2306
|
if (res.status !== 0) {
|
|
1901
|
-
|
|
2307
|
+
fail3(
|
|
1902
2308
|
`${pm.name} install failed (exit ${res.status}).
|
|
1903
2309
|
|
|
1904
2310
|
Half-init state \u2014 install didn't complete, but these files ARE on disk:
|
|
@@ -1955,7 +2361,7 @@ function runOnboard(repoDir, pm) {
|
|
|
1955
2361
|
}
|
|
1956
2362
|
const res = (0, import_node_child_process3.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
1957
2363
|
if (res.status !== 0) {
|
|
1958
|
-
|
|
2364
|
+
fail3(
|
|
1959
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}`
|
|
1960
2366
|
);
|
|
1961
2367
|
}
|
|
@@ -2146,7 +2552,7 @@ function wireLsSettings(targetDir) {
|
|
|
2146
2552
|
try {
|
|
2147
2553
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
2148
2554
|
} catch (err) {
|
|
2149
|
-
|
|
2555
|
+
fail3(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
2150
2556
|
}
|
|
2151
2557
|
}
|
|
2152
2558
|
const merged = { ...existing };
|
|
@@ -2200,7 +2606,7 @@ function wireRecallHook(targetDir) {
|
|
|
2200
2606
|
try {
|
|
2201
2607
|
existing = JSON.parse(fs5.readFileSync(p, "utf-8"));
|
|
2202
2608
|
} catch (err) {
|
|
2203
|
-
|
|
2609
|
+
fail3(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
2204
2610
|
}
|
|
2205
2611
|
}
|
|
2206
2612
|
const hooks = existing.hooks ?? {};
|
|
@@ -2265,14 +2671,14 @@ async function main2() {
|
|
|
2265
2671
|
let identityVal = null;
|
|
2266
2672
|
for (const a of process.argv.slice(3)) {
|
|
2267
2673
|
if (a.startsWith("--identity=")) identityVal = a.slice("--identity=".length);
|
|
2268
|
-
else
|
|
2674
|
+
else fail3(`Unknown setup-git flag: "${a}". Supported: --identity="Name <email>".`);
|
|
2269
2675
|
}
|
|
2270
|
-
if (!identityVal)
|
|
2676
|
+
if (!identityVal) fail3(`launch-kit setup-git requires --identity="Name <email>".`);
|
|
2271
2677
|
try {
|
|
2272
2678
|
const identity = parseGitIdentityFlag(identityVal, "--identity");
|
|
2273
2679
|
configureGitForBot(identity);
|
|
2274
2680
|
} catch (err) {
|
|
2275
|
-
|
|
2681
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2276
2682
|
}
|
|
2277
2683
|
return;
|
|
2278
2684
|
}
|
|
@@ -2292,13 +2698,13 @@ async function main2() {
|
|
|
2292
2698
|
for (const a of process.argv.slice(4)) {
|
|
2293
2699
|
if (a.startsWith("--show=")) showArg = a.slice("--show=".length);
|
|
2294
2700
|
else if (a === "--compact") compactArg = true;
|
|
2295
|
-
else
|
|
2701
|
+
else fail3(`Unknown statusline flag: "${a}". Supported: --show=<csv>, --compact.`);
|
|
2296
2702
|
}
|
|
2297
2703
|
const { activateStatusline: activateStatusline2, deactivateStatusline: deactivateStatusline2 } = await Promise.resolve().then(() => (init_statusline_install(), statusline_install_exports));
|
|
2298
2704
|
let res;
|
|
2299
2705
|
if (action === "activate") res = activateStatusline2({ show: showArg, compact: compactArg });
|
|
2300
2706
|
else if (action === "deactivate") res = deactivateStatusline2();
|
|
2301
|
-
else
|
|
2707
|
+
else fail3(`Unknown statusline action: "${action}". Supported: activate, deactivate.`);
|
|
2302
2708
|
if (res.ok) ok(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
2303
2709
|
else info(`statusline ${res.outcome} \u2014 ${res.message}`);
|
|
2304
2710
|
return;
|
|
@@ -2316,7 +2722,7 @@ async function main2() {
|
|
|
2316
2722
|
return;
|
|
2317
2723
|
}
|
|
2318
2724
|
if (action !== "pull") {
|
|
2319
|
-
|
|
2725
|
+
fail3(`Unknown secrets action: "${action}". Supported: pull.`);
|
|
2320
2726
|
}
|
|
2321
2727
|
let envOverride = null;
|
|
2322
2728
|
let dirArg = null;
|
|
@@ -2325,14 +2731,14 @@ async function main2() {
|
|
|
2325
2731
|
if (a.startsWith("--env=")) envOverride = a.slice("--env=".length);
|
|
2326
2732
|
else if (a.startsWith("--dir=")) dirArg = a.slice("--dir=".length);
|
|
2327
2733
|
else if (a.startsWith("--file=")) fileArg = a.slice("--file=".length);
|
|
2328
|
-
else
|
|
2734
|
+
else fail3(`Unknown secrets pull flag: "${a}". Supported: --env, --dir, --file.`);
|
|
2329
2735
|
}
|
|
2330
2736
|
const targetDir = path5.resolve(dirArg ?? process.cwd());
|
|
2331
2737
|
const { runSecretsPull: runSecretsPull2 } = await Promise.resolve().then(() => (init_secrets_pull(), secrets_pull_exports));
|
|
2332
2738
|
try {
|
|
2333
2739
|
await runSecretsPull2({ targetDir, envOverride, fileName: fileArg });
|
|
2334
2740
|
} catch (err) {
|
|
2335
|
-
|
|
2741
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2336
2742
|
}
|
|
2337
2743
|
return;
|
|
2338
2744
|
}
|
|
@@ -2343,10 +2749,14 @@ async function main2() {
|
|
|
2343
2749
|
return;
|
|
2344
2750
|
}
|
|
2345
2751
|
if (!subcommand || subcommand.startsWith("--")) {
|
|
2346
|
-
|
|
2752
|
+
fail3(`missing subcommand. Usage: launch-kit <run|init|refresh|statusline|secrets> [options]. Run with --help.`);
|
|
2347
2753
|
}
|
|
2348
|
-
if (subcommand !== "init" && subcommand !== "refresh") {
|
|
2349
|
-
|
|
2754
|
+
if (subcommand !== "init" && subcommand !== "refresh" && subcommand !== "run") {
|
|
2755
|
+
fail3(`Unknown subcommand "${subcommand}". Supported: run, init, refresh, statusline, secrets. Run with --help for usage.`);
|
|
2756
|
+
}
|
|
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(", ")}.`);
|
|
2350
2760
|
}
|
|
2351
2761
|
DRY_RUN = args.dryRun;
|
|
2352
2762
|
VERBOSE = args.verbose || DRY_RUN;
|
|
@@ -2356,85 +2766,30 @@ async function main2() {
|
|
|
2356
2766
|
console.log(c.dim("Lines tagged (dry-run) show what would happen."));
|
|
2357
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"));
|
|
2358
2768
|
}
|
|
2359
|
-
|
|
2360
|
-
return mainInit(args);
|
|
2769
|
+
return runFlow(args);
|
|
2361
2770
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
if (
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
writeJsonAtomic(path5.join(targetDir, CONFIG_FILENAME), nested2, 384);
|
|
2383
|
-
ok(`wrote ${CONFIG_FILENAME} (course: ${courseName})`);
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
if (!cred) {
|
|
2387
|
-
fail2(
|
|
2388
|
-
`no ${CONFIG_FILENAME} found at ${targetDir}, and could not recover from .mcp.json. Refresh requires an existing cred or a hardcoded launch-secure MCP entry \u2014 run \`npx @launchsecure/launch-kit init --token=<pat> --org=<org> --project=<project> --dir=${path5.relative(cwd, targetDir) || "."}\` first.`
|
|
2389
|
-
);
|
|
2390
|
-
}
|
|
2391
|
-
const nested = toNested(cred);
|
|
2392
|
-
if (!nested) fail2(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl).`);
|
|
2393
|
-
const active = nested.profiles[nested.active];
|
|
2394
|
-
if (!active) fail2(`${CONFIG_FILENAME} active profile "${nested.active}" is not present in profiles.`);
|
|
2395
|
-
info(`refreshing launch-kit in ${targetDir} (course: ${nested.active}, project: ${active.orgSlug}/${active.projectSlug}) \u2026`);
|
|
2396
|
-
header("launch-kit refresh", [
|
|
2397
|
-
["course", nested.active],
|
|
2398
|
-
["project", `${active.orgSlug}/${active.projectSlug}`],
|
|
2399
|
-
["dir", path5.relative(cwd, targetDir) || "."]
|
|
2400
|
-
]);
|
|
2401
|
-
const cfg = { pat: active.pat, orgSlug: active.orgSlug, projectSlug: active.projectSlug, serverUrl: active.serverUrl };
|
|
2402
|
-
phase(".mcp.json", mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg)));
|
|
2403
|
-
ensureGitignoreLine(targetDir, CONFIG_FILENAME);
|
|
2404
|
-
if (!args.noMigrateSafety) phase("migrate-safety", scaffoldMigrateSafety(targetDir, args.refreshScaffolds));
|
|
2405
|
-
if (!args.noLsMarketplace) phase("ls-marketplace", scaffoldLsMarketplace(targetDir));
|
|
2406
|
-
if (!args.noRecallHook) phase("recall-hook", scaffoldRecallHook(targetDir));
|
|
2407
|
-
const slR = tryActivateStatusline();
|
|
2408
|
-
if (slR) phase("statusline", slR);
|
|
2409
|
-
if (DRY_RUN) {
|
|
2410
|
-
console.log("");
|
|
2411
|
-
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"));
|
|
2412
|
-
console.log(c.bold("DRY RUN COMPLETE") + c.dim(" \u2014 refresh would have applied the above; no files modified."));
|
|
2413
|
-
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"));
|
|
2414
|
-
return;
|
|
2415
|
-
}
|
|
2416
|
-
ok(`refresh complete \u2014 restart Claude Code to pick up any new /kit:* commands`);
|
|
2417
|
-
const showGuide = args.quiet ? false : args.guide === true;
|
|
2418
|
-
const hints = ["Restart Claude Code to pick up any new /kit:* commands."];
|
|
2419
|
-
if (!showGuide && !args.quiet) hints.push("Run with --guide to print the full MCP + slash-command catalog. --verbose for per-file detail.");
|
|
2420
|
-
footer("Refresh complete.", hints);
|
|
2421
|
-
if (showGuide) {
|
|
2422
|
-
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)`));
|
|
2423
|
-
console.log(getLaunchKitToolsGuide());
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
async function mainInit(args) {
|
|
2427
|
-
const probeDir = path5.resolve(args.targetDir ?? process.cwd());
|
|
2428
|
-
if (!args.force && fs5.existsSync(probeDir)) {
|
|
2429
|
-
const detection = detectExistingBootstrap(probeDir);
|
|
2430
|
-
if (detection.bootstrapped) {
|
|
2431
|
-
info(`detected existing bootstrap at ${probeDir} (${detection.reason})`);
|
|
2432
|
-
info(`delegating to refresh. Pass --force to re-init from scratch (will re-prompt for PAT if needed).`);
|
|
2433
|
-
return mainRefresh({ ...args, targetDir: probeDir });
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
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;
|
|
2436
2791
|
if (!args.token || !args.orgSlug || !args.projectSlug) {
|
|
2437
|
-
const recoveryDir = path5.resolve(args.targetDir ??
|
|
2792
|
+
const recoveryDir = path5.resolve(args.targetDir ?? ctx.cwd);
|
|
2438
2793
|
if (fs5.existsSync(recoveryDir)) {
|
|
2439
2794
|
const { cred } = recoverCred(recoveryDir, getRecoveryOptions());
|
|
2440
2795
|
const nested = cred ? toNested(cred) : null;
|
|
@@ -2462,139 +2817,295 @@ async function mainInit(args) {
|
|
|
2462
2817
|
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
2463
2818
|
args.token = t || null;
|
|
2464
2819
|
}
|
|
2465
|
-
if (!args.token)
|
|
2466
|
-
if (!/^ls_pat_/.test(args.token))
|
|
2467
|
-
if (!args.orgSlug)
|
|
2468
|
-
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.");
|
|
2469
2824
|
header("launch-kit init", [
|
|
2470
2825
|
["org", args.orgSlug],
|
|
2471
2826
|
["project", args.projectSlug],
|
|
2472
2827
|
["server", args.serverUrl]
|
|
2473
2828
|
]);
|
|
2474
|
-
|
|
2475
|
-
|
|
2829
|
+
ctx.headerPrinted = true;
|
|
2830
|
+
const { hasGh: hasGh2 } = preflight();
|
|
2831
|
+
ctx.hasGh = hasGh2;
|
|
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)"}` });
|
|
2476
2833
|
if (args.gitIdentity) {
|
|
2477
2834
|
configureGitForBot(args.gitIdentity);
|
|
2478
|
-
phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7
|
|
2835
|
+
phase("git identity", { status: "ok", summary: `${args.gitIdentity.name} <${args.gitIdentity.email}>${process.env.GH_TOKEN ? " \xB7 git credential helper wired" : ""}` });
|
|
2836
|
+
} else if (wireGitHubAuth()) {
|
|
2837
|
+
phase("git auth", { status: "ok", summary: hasGh2 ? "GH_TOKEN \u2192 gh credential helper" : "GH_TOKEN \u2192 git credential helper (gh not found)" });
|
|
2479
2838
|
}
|
|
2480
2839
|
info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
|
|
2481
2840
|
let resolved;
|
|
2482
2841
|
try {
|
|
2483
2842
|
resolved = await callProjectInfo(args);
|
|
2484
2843
|
} catch (err) {
|
|
2485
|
-
|
|
2844
|
+
fail3(err instanceof Error ? err.message : String(err));
|
|
2486
2845
|
}
|
|
2487
2846
|
ok(`resolved "${resolved.projectName}"`);
|
|
2488
2847
|
phase("project_info", { status: "ok", summary: `"${resolved.projectName}"` });
|
|
2489
2848
|
if (!resolved.repositoryUrl) {
|
|
2490
|
-
|
|
2849
|
+
fail3(
|
|
2491
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.`
|
|
2492
2851
|
);
|
|
2493
2852
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
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;
|
|
2497
2862
|
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
2498
|
-
let skipClone = false;
|
|
2499
2863
|
if (fs5.existsSync(targetDir)) {
|
|
2500
2864
|
if (isGitRepo(targetDir)) {
|
|
2501
2865
|
const existingRemote = gitRemoteUrl(targetDir);
|
|
2502
2866
|
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
2503
|
-
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone,
|
|
2504
|
-
|
|
2867
|
+
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 wiring this existing repo (skipping clone, no GitHub auth needed)`);
|
|
2868
|
+
info(`tip: once wired, use \`launch-kit refresh\` here to re-apply configs without re-running init`);
|
|
2869
|
+
ctx.skipClone = true;
|
|
2505
2870
|
} else {
|
|
2506
|
-
|
|
2871
|
+
fail3(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
|
|
2507
2872
|
}
|
|
2508
2873
|
} else if (!dirIsEmpty(targetDir)) {
|
|
2509
|
-
|
|
2874
|
+
fail3(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
|
|
2510
2875
|
}
|
|
2511
2876
|
}
|
|
2512
2877
|
const relTarget = path5.relative(cwd, targetDir) || ".";
|
|
2513
|
-
if (!skipClone) {
|
|
2878
|
+
if (!ctx.skipClone) {
|
|
2514
2879
|
section(`Cloning ${repoUrl}`);
|
|
2515
|
-
cloneRepo(repoUrl, targetDir, hasGh);
|
|
2880
|
+
cloneRepo(repoUrl, targetDir, ctx.hasGh);
|
|
2516
2881
|
phase("clone", { status: "ok", summary: `\u2192 ${relTarget}` });
|
|
2517
2882
|
} else {
|
|
2518
2883
|
phase("clone", { status: "in-sync", summary: `${relTarget} (already a clone of this repo)` });
|
|
2519
2884
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
let
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
}
|
|
2548
|
-
section("Pulling environment secrets");
|
|
2549
|
-
info("running launch-kit secrets pull \u2026");
|
|
2550
|
-
if (DRY_RUN) {
|
|
2551
|
-
dryNote(`would run: launch-kit secrets pull --dir=${path5.relative(cwd, targetDir) || "."}`);
|
|
2552
|
-
phase("secrets pull", { status: "skipped", summary: "(dry-run)" });
|
|
2553
|
-
} else {
|
|
2554
|
-
try {
|
|
2555
|
-
await runSecretsPull({ targetDir, envOverride: null, fileName: ".env.local" });
|
|
2556
|
-
phase("secrets pull", { status: "ok", summary: ".env.local from cloud LS" });
|
|
2557
|
-
} catch (err) {
|
|
2558
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2559
|
-
warn(`secrets pull skipped \u2014 ${msg}`);
|
|
2560
|
-
phase("secrets pull", { status: "warn", summary: "pull manually with `launch-kit secrets pull`" });
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
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})`);
|
|
2564
2913
|
}
|
|
2565
2914
|
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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;
|
|
2572
2967
|
}
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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`" });
|
|
2996
|
+
}
|
|
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) {
|
|
2576
3013
|
const slR = tryActivateStatusline();
|
|
2577
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) || ".";
|
|
2578
3072
|
if (DRY_RUN) {
|
|
2579
3073
|
console.log("");
|
|
2580
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"));
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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
|
+
}
|
|
2584
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"));
|
|
2585
3083
|
return;
|
|
2586
3084
|
}
|
|
2587
|
-
|
|
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}`);
|
|
2588
3099
|
const showGuide = args.quiet ? false : args.guide ?? true;
|
|
2589
3100
|
const nextSteps = [`cd ${relTarget}`];
|
|
2590
|
-
if (installSkippedReason) {
|
|
2591
|
-
nextSteps.push(detected ? `${detected.pm.binary} ${detected.pm.installArgs.join(" ")} # install skipped: ${installSkippedReason}` : `npm install # install skipped: ${installSkippedReason}`);
|
|
2592
|
-
if (
|
|
2593
|
-
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`);
|
|
2594
3105
|
}
|
|
2595
3106
|
}
|
|
2596
3107
|
nextSteps.push("claude # launch Claude Code (5 MCPs wired)");
|
|
2597
|
-
footer(`${
|
|
3108
|
+
footer(`${projectName} is ready at ${relTarget}.`, [
|
|
2598
3109
|
"Next:",
|
|
2599
3110
|
...nextSteps.map((s) => " " + s)
|
|
2600
3111
|
]);
|