@launchsecure/launch-kit 0.0.26 → 0.0.28
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/beacon/beacon.mjs +1003 -440
- package/dist/beacon/beacon.mjs.map +1 -1
- package/dist/beacon/beacon.umd.js +45 -24
- package/dist/beacon/beacon.umd.js.map +1 -1
- package/dist/beacon/types/capture/events.d.ts +20 -0
- package/dist/beacon/types/capture/events.d.ts.map +1 -0
- package/dist/beacon/types/element.d.ts +1 -0
- package/dist/beacon/types/element.d.ts.map +1 -1
- package/dist/beacon/types/index.d.ts +2 -1
- package/dist/beacon/types/index.d.ts.map +1 -1
- package/dist/beacon/types/monitor/dom.d.ts +13 -0
- package/dist/beacon/types/monitor/dom.d.ts.map +1 -0
- package/dist/beacon/types/monitor/index.d.ts +19 -0
- package/dist/beacon/types/monitor/index.d.ts.map +1 -0
- package/dist/beacon/types/monitor/network.d.ts +12 -0
- package/dist/beacon/types/monitor/network.d.ts.map +1 -0
- package/dist/beacon/types/monitor/transport.d.ts +27 -0
- package/dist/beacon/types/monitor/transport.d.ts.map +1 -0
- package/dist/beacon/types/monitor/types.d.ts +117 -0
- package/dist/beacon/types/monitor/types.d.ts.map +1 -0
- package/dist/beacon/types/types.d.ts +10 -0
- package/dist/beacon/types/types.d.ts.map +1 -1
- package/dist/beacon/types/ui/drawer.d.ts +3 -1
- package/dist/beacon/types/ui/drawer.d.ts.map +1 -1
- package/dist/beacon/types/ui/monitor-panel.d.ts +19 -0
- package/dist/beacon/types/ui/monitor-panel.d.ts.map +1 -0
- package/dist/chart-client/assets/index-CJ4mgRRF.css +1 -0
- package/dist/chart-client/assets/{index-Bk1hawjD.js → index-Ccy-DpI-.js} +46 -42
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-DI5qSR_w.css +32 -0
- package/dist/client/assets/index-Dp0_okva.js +294 -0
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/index-C_-vAM9L.css +1 -0
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-C2xT_eYu.js → _baseUniq-W2JQDmje.js} +1 -1
- package/dist/deck-client/assets/{arc-CmVL9pGd.js → arc-DIBWAId9.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-BSFgdjve.js → architectureDiagram-Q4EWVU46-CAIRMvJK.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-DuLzscvP.js → blockDiagram-DXYQGD6D-BeNaNiOi.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CfCJB8eY.js → c4Diagram-AHTNJAMY-B9Ozi62h.js} +1 -1
- package/dist/deck-client/assets/channel-CRdozqbp.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-DxmLYTWZ.js → chunk-4BX2VUAB-D7AZ47dt.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-CCnf7GFE.js → chunk-4TB4RGXK-DnVnNPcI.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-Db9DApcj.js → chunk-55IACEB6-UKYs-YNd.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-DmYDq8ZI.js → chunk-EDXVE4YY-D43b-SKn.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-BGhUlF20.js → chunk-FMBD7UC4-QzBAoyyW.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-CpEnicQZ.js → chunk-OYMX7WX6-Cjif4r6W.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-Doa7LKwf.js → chunk-QZHKN3VN-CqLDirEI.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-CpkIlH6V.js → chunk-YZCP3GAM-_FQvmMs4.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-lIZMp57W.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-lIZMp57W.js +1 -0
- package/dist/deck-client/assets/clone-BtWeSTyJ.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bkh8Bfcb.js → cose-bilkent-S5V4N54A-rfrocesE.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-Bp0XpTgH.js → dagre-KV5264BT-Bv_7DJat.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-ZHiyGYPQ.js → diagram-5BDNPKRD-4F1414G5.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-BW-Q8_H5.js → diagram-G4DWMVQ6-C4-Pszqm.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-6I3LTafu.js → diagram-MMDJMWI5-B647TIx9.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-CyM5YK28.js → diagram-TYMM5635-BFAqpezd.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-CjNxVJHk.js → erDiagram-SMLLAGMA-BfBfrJOC.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-BDQHuAJR.js → flowDiagram-DWJPFMVM-DX9YAYes.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-B7MnkpbP.js → ganttDiagram-T4ZO3ILL-DCuiy7wF.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-C9dZAcYD.js → gitGraphDiagram-UUTBAWPF-CGp1IXUh.js} +1 -1
- package/dist/deck-client/assets/{graph-CjdBnzUy.js → graph-B7g8aoxv.js} +1 -1
- package/dist/deck-client/assets/{index-DeIVPW63.js → index-Dg1r-WSN.js} +3 -3
- package/dist/deck-client/assets/index-DsIZ3LqL.css +1 -0
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-C7d3iRC3.js → infoDiagram-42DDH7IO-L3fahMkF.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-BcYGKj09.js → ishikawaDiagram-UXIWVN3A-aS_EjWBZ.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-DqFlRrOL.js → journeyDiagram-VCZTEJTY-djTSQZF9.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BJhPp1NR.js → kanban-definition-6JOO6SKY-CcTHo4CM.js} +1 -1
- package/dist/deck-client/assets/{layout-DIeS6GvK.js → layout-mEJiadb7.js} +1 -1
- package/dist/deck-client/assets/{linear-He_yJy5H.js → linear-XgTKqyRu.js} +1 -1
- package/dist/deck-client/assets/{min-DQ6Kx06t.js → min-Ct9jZdpd.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-sQ62L8T2.js → mindmap-definition-QFDTVHPH-BaFxCGNU.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-BqCWmU2K.js → pieDiagram-DEJITSTG-CIbYYjtw.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-rQ1TJOoe.js → quadrantDiagram-34T5L4WZ-D9EtCOvh.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-BO2MPBOM.js → requirementDiagram-MS252O5E-xeni9eVG.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BgsHEVex.js → sankeyDiagram-XADWPNL6-LYeknz9h.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-B3j1yMLU.js → sequenceDiagram-FGHM5R23-RDbsKFZf.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-C8jFlZou.js → stateDiagram-FHFEXIEX-BH1Zjglk.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BrV78NDR.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-tM-qo4Zk.js → timeline-definition-GMOUNBTQ-IFXxKptt.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-B0-6kOEu.js → vennDiagram-DHZGUBPP-D-sLkQs9.js} +1 -1
- package/dist/deck-client/assets/{wardley-RL74JXVD-HpBk07P-.js → wardley-RL74JXVD-C010F8l4.js} +1 -1
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-BkA1NLDE.js → wardleyDiagram-NUSXRM2D-BTjjuDU3.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CEKGSuI-.js → xychartDiagram-5P7HB3ND-AYbv92n-.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/beacon-monitor-entry.js +353 -0
- package/dist/server/chart-serve.js +3836 -3750
- package/dist/server/cli.js +8789 -8219
- package/dist/server/council-entry.js +17 -5
- package/dist/server/council-serve.js +8 -3
- package/dist/server/course-entry.js +246 -0
- package/dist/server/deck-mcp-entry.js +24 -12
- package/dist/server/deck-serve.js +11 -8
- package/dist/server/graph-mcp-entry.js +5005 -4865
- package/dist/server/init-entry.js +939 -0
- package/dist/server/orbit-entry.js +2435 -0
- package/dist/server/parse-worker-entry.js +4721 -0
- package/dist/server/recall-entry.js +356 -18
- package/package.json +11 -4
- package/scaffolds/ls-marketplace/.claude-plugin/marketplace.json +15 -0
- package/scaffolds/ls-marketplace/plugins/ls/.claude-plugin/plugin.json +28 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/activate-beacon.md +216 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-array.md +92 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-clear.md +68 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-pulse.md +80 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/beacon-scan.md +62 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/show-mcp-status.md +109 -0
- package/scaffolds/ls-marketplace/plugins/ls/commands/standup.md +177 -0
- package/scaffolds/migrate-safety/.github/workflows/backup-on-migration.yml +72 -0
- package/scaffolds/migrate-safety/docs/migrations-runbook.md +172 -0
- package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +294 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +69 -0
- package/dist/chart-client/assets/index-DpaGa3bY.css +0 -1
- package/dist/client/assets/index-Bfel4OQ5.css +0 -32
- package/dist/client/assets/index-eC-WuUWB.js +0 -291
- package/dist/council-client/assets/index-P5kMsT5a.css +0 -1
- package/dist/deck-client/assets/channel-B4aNO8ZB.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-BHTI0yWz.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-BHTI0yWz.js +0 -1
- package/dist/deck-client/assets/clone-HduFm7qU.js +0 -1
- package/dist/deck-client/assets/index-LKZDAS9S.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BoqepHW0.js +0 -1
- /package/dist/council-client/assets/{index-Cs_MVXHf.js → index-Dt4zWKSj.js} +0 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/server/init-entry.ts
|
|
27
|
+
var import_node_child_process = require("node:child_process");
|
|
28
|
+
var fs2 = __toESM(require("node:fs"));
|
|
29
|
+
var import_node_http = require("node:http");
|
|
30
|
+
var import_node_https = require("node:https");
|
|
31
|
+
var path2 = __toESM(require("node:path"));
|
|
32
|
+
var readline = __toESM(require("node:readline"));
|
|
33
|
+
var import_node_url = require("node:url");
|
|
34
|
+
|
|
35
|
+
// src/server/cred-shape.ts
|
|
36
|
+
var fs = __toESM(require("node:fs"));
|
|
37
|
+
var path = __toESM(require("node:path"));
|
|
38
|
+
var CONFIG_FILENAME = ".launch-secure.cred.config";
|
|
39
|
+
function inferCourseName(serverUrl) {
|
|
40
|
+
try {
|
|
41
|
+
const host = new URL(serverUrl).hostname.toLowerCase();
|
|
42
|
+
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "local";
|
|
43
|
+
if (host.includes("staging")) return "staging";
|
|
44
|
+
if (host.endsWith(".vercel.app")) return "prod";
|
|
45
|
+
return host.split(".")[0] || "default";
|
|
46
|
+
} catch {
|
|
47
|
+
return "default";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function toNested(cred) {
|
|
51
|
+
if (cred.profiles && cred.active && cred.profiles[cred.active]) {
|
|
52
|
+
return { active: cred.active, profiles: cred.profiles };
|
|
53
|
+
}
|
|
54
|
+
if (!cred.pat || !cred.orgSlug || !cred.projectSlug || !cred.serverUrl) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const name = inferCourseName(cred.serverUrl);
|
|
58
|
+
return {
|
|
59
|
+
active: name,
|
|
60
|
+
profiles: {
|
|
61
|
+
[name]: {
|
|
62
|
+
pat: cred.pat,
|
|
63
|
+
orgSlug: cred.orgSlug,
|
|
64
|
+
projectSlug: cred.projectSlug,
|
|
65
|
+
serverUrl: cred.serverUrl
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function upsertProfile(existing, name, profile) {
|
|
71
|
+
const base = existing ? toNested(existing) ?? { active: name, profiles: {} } : { active: name, profiles: {} };
|
|
72
|
+
return {
|
|
73
|
+
active: name,
|
|
74
|
+
profiles: { ...base.profiles, [name]: profile }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function readCredFile(repoRoot) {
|
|
78
|
+
const p = path.join(repoRoot, CONFIG_FILENAME);
|
|
79
|
+
if (!fs.existsSync(p)) return null;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
throw new Error(`could not parse ${CONFIG_FILENAME}: ${err instanceof Error ? err.message : String(err)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function writeJsonAtomic(absPath, value, mode) {
|
|
87
|
+
const tmp = `${absPath}.tmp`;
|
|
88
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
89
|
+
if (mode !== void 0) {
|
|
90
|
+
try {
|
|
91
|
+
fs.chmodSync(tmp, mode);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
fs.renameSync(tmp, absPath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/server/init-entry.ts
|
|
99
|
+
var DEFAULT_SERVER_URL = "https://launchsecure-v2.vercel.app";
|
|
100
|
+
var LEGACY_CONFIG_FILENAME = ".launch-secure.config";
|
|
101
|
+
var ONBOARD_SCRIPT_NAME = "onboard";
|
|
102
|
+
var LAUNCH_KIT_PKG = "@launchsecure/launch-kit";
|
|
103
|
+
var LAUNCH_KIT_TOOLS_GUIDE = `
|
|
104
|
+
Wired in Claude Code (.mcp.json):
|
|
105
|
+
launch-secure \u2014 LS API: work items, comms, secrets, members, board
|
|
106
|
+
launch-chart \u2014 code search + project graph (use instead of grep/glob)
|
|
107
|
+
launch-deck \u2014 visual playground / blast-radius diagrams
|
|
108
|
+
launch-orbit \u2014 git worktree orchestration (branch / merge gates)
|
|
109
|
+
launch-recall \u2014 restore deleted/modified files from shadow git
|
|
110
|
+
|
|
111
|
+
Other tools (run on demand via npx):
|
|
112
|
+
npx launch-pod radar \u2014 webhook listener (LS pings \u2192 terminal/UI)
|
|
113
|
+
npx launch-pod \u2014 full pipeline UI (separate launch-pod login)
|
|
114
|
+
npx launch-beacon monitor \u2014 local HTTP receiver for the launch-kit-beacon
|
|
115
|
+
in-browser monitor. Paste the printed URL into
|
|
116
|
+
the beacon debug panel; events stream to
|
|
117
|
+
.launchsecure/beacon-<token>.ndjson for the
|
|
118
|
+
/ls:beacon-* commands below to read.
|
|
119
|
+
|
|
120
|
+
LS slash commands (run inside Claude Code in this project):
|
|
121
|
+
/ls:activate-beacon \u2014 wire the launch-kit-beacon in-app feedback
|
|
122
|
+
widget into this app (mounts the <launch-kit-
|
|
123
|
+
beacon> Web Component + scaffolds /api/feedback
|
|
124
|
+
forwarding to LaunchSecure Comm Hub)
|
|
125
|
+
/ls:standup \u2014 draft a daily standup from work since the last
|
|
126
|
+
push (chart-grouped themes, work-item linkage,
|
|
127
|
+
release detection) and post to LS Comm Hub as
|
|
128
|
+
a daily_update after you confirm
|
|
129
|
+
/ls:show-mcp-status \u2014 show recall watcher health + last snapshot.
|
|
130
|
+
Add 'full' for expanded report (PID, shadow
|
|
131
|
+
repo size, recent snaps)
|
|
132
|
+
/ls:beacon-scan \u2014 scan recent events from the active
|
|
133
|
+
launch-beacon monitor session. Pass a kind
|
|
134
|
+
(error/click/fetch/route/dialog/probe) and/or
|
|
135
|
+
a limit to filter.
|
|
136
|
+
/ls:beacon-pulse \u2014 most recent error + the N events that preceded
|
|
137
|
+
it. "What was happening just before it broke."
|
|
138
|
+
/ls:beacon-array \u2014 list monitor sessions in .launchsecure/ with
|
|
139
|
+
event counts, last activity, liveness glyph.
|
|
140
|
+
Add 'full' for a per-session expanded report.
|
|
141
|
+
/ls:beacon-clear \u2014 wipe the latest monitor session NDJSON (or
|
|
142
|
+
'all'). Confirms before deleting.
|
|
143
|
+
|
|
144
|
+
Open this repo in Claude Code; on first open you'll be prompted to install
|
|
145
|
+
the "launchsecure" marketplace \u2014 accept to enable the commands above.
|
|
146
|
+
`;
|
|
147
|
+
var PACKAGE_MANAGERS = [
|
|
148
|
+
{ name: "pnpm", binary: "pnpm", lockfiles: ["pnpm-lock.yaml"], workspaceFiles: ["pnpm-workspace.yaml"], installArgs: ["install"] },
|
|
149
|
+
{ name: "yarn", binary: "yarn", lockfiles: ["yarn.lock"], installArgs: ["install"] },
|
|
150
|
+
{ name: "bun", binary: "bun", lockfiles: ["bun.lockb", "bun.lock"], installArgs: ["install"] },
|
|
151
|
+
{ name: "npm", binary: "npm", lockfiles: ["package-lock.json"], installArgs: ["install"] }
|
|
152
|
+
];
|
|
153
|
+
function parseArgs(argv) {
|
|
154
|
+
const args = {
|
|
155
|
+
token: process.env.LS_PAT ?? null,
|
|
156
|
+
orgSlug: null,
|
|
157
|
+
projectSlug: null,
|
|
158
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
159
|
+
targetDir: null,
|
|
160
|
+
course: null,
|
|
161
|
+
noInstall: false,
|
|
162
|
+
noRecall: false,
|
|
163
|
+
noMigrateSafety: false,
|
|
164
|
+
noLsMarketplace: false,
|
|
165
|
+
noRecallHook: false,
|
|
166
|
+
dryRun: false,
|
|
167
|
+
help: false
|
|
168
|
+
};
|
|
169
|
+
for (const raw of argv) {
|
|
170
|
+
if (raw === "--help" || raw === "-h") {
|
|
171
|
+
args.help = true;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (raw === "--no-install") {
|
|
175
|
+
args.noInstall = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (raw === "--no-recall") {
|
|
179
|
+
args.noRecall = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (raw === "--no-migrate-safety") {
|
|
183
|
+
args.noMigrateSafety = true;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (raw === "--no-ls-marketplace") {
|
|
187
|
+
args.noLsMarketplace = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (raw === "--no-recall-hook") {
|
|
191
|
+
args.noRecallHook = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (raw === "--dry-run") {
|
|
195
|
+
args.dryRun = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const eq = raw.indexOf("=");
|
|
199
|
+
if (!raw.startsWith("--") || eq < 0) continue;
|
|
200
|
+
const key = raw.slice(2, eq);
|
|
201
|
+
const val = raw.slice(eq + 1);
|
|
202
|
+
if (key === "token") args.token = val;
|
|
203
|
+
else if (key === "org") args.orgSlug = val;
|
|
204
|
+
else if (key === "project") args.projectSlug = val;
|
|
205
|
+
else if (key === "url") args.serverUrl = val.replace(/\/+$/, "");
|
|
206
|
+
else if (key === "dir") args.targetDir = val;
|
|
207
|
+
else if (key === "course") args.course = val;
|
|
208
|
+
}
|
|
209
|
+
return args;
|
|
210
|
+
}
|
|
211
|
+
function printHelp() {
|
|
212
|
+
console.log(`launch-kit init \u2014 bootstrap a LaunchSecure project on this machine
|
|
213
|
+
|
|
214
|
+
Usage:
|
|
215
|
+
npx launch-kit init --token=<pat> --org=<orgSlug> --project=<projectSlug> [options]
|
|
216
|
+
|
|
217
|
+
Required:
|
|
218
|
+
--token=<pat> LaunchSecure PAT (ls_pat_...). Or set LS_PAT env var.
|
|
219
|
+
--org=<orgSlug> Organization slug.
|
|
220
|
+
--project=<projectSlug> Project slug.
|
|
221
|
+
|
|
222
|
+
Options:
|
|
223
|
+
--url=<serverUrl> LaunchSecure base URL (default: ${DEFAULT_SERVER_URL}).
|
|
224
|
+
--dir=<path> Target directory (default: ./<projectSlug>).
|
|
225
|
+
--course=<name> Name for the course (profile) being added to
|
|
226
|
+
.launch-secure.cred.config. When omitted, inferred
|
|
227
|
+
from --url: localhost\u2192"local", *staging*\u2192"staging",
|
|
228
|
+
*.vercel.app\u2192"prod", else hostname. The course
|
|
229
|
+
becomes active; re-run with a different --course
|
|
230
|
+
and --url to add another (e.g. local + staging).
|
|
231
|
+
Use \`launch-course set <name>\` to switch later.
|
|
232
|
+
--no-install Skip dependency install step.
|
|
233
|
+
--no-recall Skip launch-recall (shadow git backup) scaffold.
|
|
234
|
+
--no-migrate-safety Skip migrate-safety scaffold (pg_dump-before-migrate
|
|
235
|
+
wrapper + GitHub Action + runbook).
|
|
236
|
+
--no-ls-marketplace Skip the Claude Code "launchsecure" marketplace
|
|
237
|
+
scaffold (.claude/marketplace/ + .claude/settings.json
|
|
238
|
+
wiring \u2014 exposes /ls:activate-beacon and future
|
|
239
|
+
ls-namespaced slash commands).
|
|
240
|
+
--no-recall-hook Skip the SessionStart hook scaffold (Claude Code
|
|
241
|
+
hook + scripts/ensure-recall.sh that auto-restarts
|
|
242
|
+
the launch-recall watcher if it died between
|
|
243
|
+
sessions). The hook is the surfacing layer for
|
|
244
|
+
watcher-died-silently scenarios.
|
|
245
|
+
--dry-run Preview every file write, merge, clone, and install
|
|
246
|
+
command without making any changes. Useful before
|
|
247
|
+
re-running init against a customized project. The
|
|
248
|
+
project_info HTTP call still runs (it's read-only).
|
|
249
|
+
--help Show this help.
|
|
250
|
+
|
|
251
|
+
What it does:
|
|
252
|
+
1. Preflight: checks git + node + (optional) gh.
|
|
253
|
+
2. Calls LaunchSecure to resolve the project + its git remote.
|
|
254
|
+
3. Clones the repo (prefers gh, falls back to git) \u2014 skipped if dir already
|
|
255
|
+
contains the matching clone.
|
|
256
|
+
4. Writes .launch-secure.cred.config (gitignored). Auto-migrates any
|
|
257
|
+
legacy .launch-secure.config that contains a PAT.
|
|
258
|
+
5. Merges .mcp.json with 5 entries (launch-secure / -chart / -deck / -orbit /
|
|
259
|
+
-recall). Preserves your other MCP entries. Auth via headersHelper \u2014
|
|
260
|
+
no secrets written into .mcp.json.
|
|
261
|
+
6. Detects package manager (packageManager field > lockfile > npm fallback)
|
|
262
|
+
and runs install. Skip with --no-install.
|
|
263
|
+
7. If package.json declares a "${ONBOARD_SCRIPT_NAME}" script, runs it
|
|
264
|
+
(your repo's hook for codegen / env pull / db setup / etc.).
|
|
265
|
+
8. Scaffolds launch-recall (shadow git backup). Skip with --no-recall.
|
|
266
|
+
9. Scaffolds migrate-safety (pg_dump wrapper + GHA backup workflow +
|
|
267
|
+
runbook + .backups/ gitignore line). Skip with --no-migrate-safety.
|
|
268
|
+
10. Scaffolds the Claude Code "launchsecure" marketplace at
|
|
269
|
+
.claude/marketplace/ and wires .claude/settings.json so Claude Code
|
|
270
|
+
auto-discovers it and enables the "ls" plugin (exposes
|
|
271
|
+
/ls:activate-beacon for wiring the launch-kit-beacon in-app feedback
|
|
272
|
+
widget). Skip with --no-ls-marketplace.
|
|
273
|
+
11. Scaffolds scripts/ensure-recall.sh and appends a SessionStart hook to
|
|
274
|
+
.claude/settings.json that respawns the launch-recall watcher if it
|
|
275
|
+
died between sessions. Idempotent (dedupes by hook command-match).
|
|
276
|
+
Skip with --no-recall-hook.
|
|
277
|
+
`);
|
|
278
|
+
}
|
|
279
|
+
async function prompt(question) {
|
|
280
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
281
|
+
return new Promise((resolve2) => rl.question(question, (answer) => {
|
|
282
|
+
rl.close();
|
|
283
|
+
resolve2(answer.trim());
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
function fail(msg) {
|
|
287
|
+
console.error(`[launch-kit] \u2717 ${msg}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
function info(msg) {
|
|
291
|
+
console.log(`[launch-kit] ${msg}`);
|
|
292
|
+
}
|
|
293
|
+
function ok(msg) {
|
|
294
|
+
console.log(`[launch-kit] \u2713 ${msg}`);
|
|
295
|
+
}
|
|
296
|
+
var DRY_RUN = false;
|
|
297
|
+
function dryNote(msg) {
|
|
298
|
+
console.log(`[launch-kit] (dry-run) ${msg}`);
|
|
299
|
+
}
|
|
300
|
+
function which(bin) {
|
|
301
|
+
const res = (0, import_node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" });
|
|
302
|
+
if (res.status !== 0) return null;
|
|
303
|
+
return res.stdout.split(/\r?\n/)[0]?.trim() || null;
|
|
304
|
+
}
|
|
305
|
+
function preflight() {
|
|
306
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
307
|
+
if (nodeMajor < 18) fail(`Node.js >= 18 required (current: ${process.versions.node}).`);
|
|
308
|
+
if (!which("git")) fail("git not found in PATH. Install git: https://git-scm.com/downloads");
|
|
309
|
+
const hasGh = which("gh") !== null;
|
|
310
|
+
ok(`preflight ok \u2014 node ${process.versions.node}, git present${hasGh ? ", gh present" : ", gh not found (will use git for clone)"}`);
|
|
311
|
+
return { hasGh };
|
|
312
|
+
}
|
|
313
|
+
function callProjectInfo(args) {
|
|
314
|
+
return new Promise((resolve2, reject) => {
|
|
315
|
+
const mcpUrl = new import_node_url.URL("/api/mcp/project", args.serverUrl);
|
|
316
|
+
const body = JSON.stringify({
|
|
317
|
+
jsonrpc: "2.0",
|
|
318
|
+
id: 1,
|
|
319
|
+
method: "tools/call",
|
|
320
|
+
params: {
|
|
321
|
+
name: "project_info",
|
|
322
|
+
arguments: { org_slug: args.orgSlug, project_slug: args.projectSlug }
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
const requester = mcpUrl.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
326
|
+
const req = requester(
|
|
327
|
+
{
|
|
328
|
+
host: mcpUrl.hostname,
|
|
329
|
+
port: mcpUrl.port || (mcpUrl.protocol === "https:" ? 443 : 80),
|
|
330
|
+
path: mcpUrl.pathname,
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: {
|
|
333
|
+
"Content-Type": "application/json",
|
|
334
|
+
"Accept": "application/json, text/event-stream",
|
|
335
|
+
"Content-Length": String(Buffer.byteLength(body)),
|
|
336
|
+
"Authorization": `Bearer ${args.token}`,
|
|
337
|
+
"X-Org-Slug": args.orgSlug,
|
|
338
|
+
"X-Project-Slug": args.projectSlug
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
(res) => {
|
|
342
|
+
const chunks = [];
|
|
343
|
+
res.on("data", (c) => chunks.push(c));
|
|
344
|
+
res.on("end", () => {
|
|
345
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
346
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
347
|
+
reject(new Error(`PAT rejected (${res.statusCode}). Check token + that it has access to ${args.orgSlug}/${args.projectSlug}.`));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
351
|
+
reject(new Error(`LaunchSecure responded ${res.statusCode}: ${text.slice(0, 300)}`));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let json = text;
|
|
355
|
+
if (text.startsWith("event:") || text.includes("\ndata: ")) {
|
|
356
|
+
for (const line of text.split("\n")) {
|
|
357
|
+
if (line.startsWith("data: ")) {
|
|
358
|
+
json = line.slice(6);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(json);
|
|
365
|
+
if (parsed.error) {
|
|
366
|
+
reject(new Error(`project_info error: ${parsed.error.message ?? "unknown"}`));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const inner = parsed.result?.content?.[0]?.text;
|
|
370
|
+
if (!inner) {
|
|
371
|
+
reject(new Error("project_info returned no content"));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (inner.startsWith("\u2500\u2500 Error")) {
|
|
375
|
+
const firstLine = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("\u2500\u2500"))[0];
|
|
376
|
+
reject(new Error(`project_info: ${firstLine ?? inner}`));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const payload = JSON.parse(inner);
|
|
380
|
+
resolve2({
|
|
381
|
+
orgSlug: payload.org.slug,
|
|
382
|
+
projectSlug: payload.project.slug,
|
|
383
|
+
projectName: payload.project.name,
|
|
384
|
+
repositoryUrl: payload.project.repositoryUrl
|
|
385
|
+
});
|
|
386
|
+
} catch (err) {
|
|
387
|
+
reject(new Error(`Could not parse project_info response: ${err instanceof Error ? err.message : String(err)}`));
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
req.on("error", reject);
|
|
393
|
+
req.write(body);
|
|
394
|
+
req.end();
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function gitRemoteUrl(dir) {
|
|
398
|
+
const res = (0, import_node_child_process.spawnSync)("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
|
|
399
|
+
if (res.status !== 0) return null;
|
|
400
|
+
return res.stdout.trim() || null;
|
|
401
|
+
}
|
|
402
|
+
function normalizeRepoUrl(url) {
|
|
403
|
+
let u = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
|
404
|
+
const sshMatch = u.match(/^git@([^:]+):(.+)$/);
|
|
405
|
+
if (sshMatch) u = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
406
|
+
try {
|
|
407
|
+
const parsed = new import_node_url.URL(u);
|
|
408
|
+
return `${parsed.protocol}//${parsed.host.toLowerCase()}${parsed.pathname}`;
|
|
409
|
+
} catch {
|
|
410
|
+
return u;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function isGitRepo(dir) {
|
|
414
|
+
return fs2.existsSync(path2.join(dir, ".git"));
|
|
415
|
+
}
|
|
416
|
+
function dirIsEmpty(dir) {
|
|
417
|
+
if (!fs2.existsSync(dir)) return true;
|
|
418
|
+
return fs2.readdirSync(dir).length === 0;
|
|
419
|
+
}
|
|
420
|
+
function cloneRepo(repoUrl, targetDir, hasGh) {
|
|
421
|
+
const isGithub = /github\.com/i.test(repoUrl);
|
|
422
|
+
let cmd;
|
|
423
|
+
let args;
|
|
424
|
+
if (hasGh && isGithub) {
|
|
425
|
+
cmd = "gh";
|
|
426
|
+
args = ["repo", "clone", repoUrl, targetDir];
|
|
427
|
+
info(`cloning via gh: ${repoUrl} \u2192 ${targetDir}`);
|
|
428
|
+
} else {
|
|
429
|
+
cmd = "git";
|
|
430
|
+
args = ["clone", repoUrl, targetDir];
|
|
431
|
+
info(`cloning via git: ${repoUrl} \u2192 ${targetDir}`);
|
|
432
|
+
}
|
|
433
|
+
if (DRY_RUN) {
|
|
434
|
+
dryNote(`would run: ${cmd} ${args.join(" ")}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const res = (0, import_node_child_process.spawnSync)(cmd, args, { stdio: "inherit" });
|
|
438
|
+
if (res.status !== 0) {
|
|
439
|
+
fail(
|
|
440
|
+
`Clone failed (${cmd} exited ${res.status}). For private repos make sure your GitHub auth is set up: \`gh auth login\` or an SSH key on your GitHub account.`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
ok(`cloned to ${targetDir}`);
|
|
444
|
+
}
|
|
445
|
+
function writeConfigFile(targetDir, cfg, courseName) {
|
|
446
|
+
migrateLegacyCredFile(targetDir);
|
|
447
|
+
const p = path2.join(targetDir, CONFIG_FILENAME);
|
|
448
|
+
const existing = readCredFile(targetDir);
|
|
449
|
+
const isNew = existing === null;
|
|
450
|
+
const isUpdate = !isNew && Boolean(existing?.profiles?.[courseName]);
|
|
451
|
+
if (DRY_RUN) {
|
|
452
|
+
const verb = isNew ? "write" : isUpdate ? "update course" : "add course";
|
|
453
|
+
dryNote(`would ${verb} "${courseName}" in ${CONFIG_FILENAME} (org=${cfg.orgSlug}, project=${cfg.projectSlug}, url=${cfg.serverUrl})`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const nested = upsertProfile(existing, courseName, cfg);
|
|
457
|
+
writeJsonAtomic(p, nested, 384);
|
|
458
|
+
const action = isNew ? "wrote" : isUpdate ? `updated course "${courseName}" in` : `added course "${courseName}" to`;
|
|
459
|
+
ok(`${action} ${CONFIG_FILENAME} (active: ${courseName})`);
|
|
460
|
+
}
|
|
461
|
+
function migrateLegacyCredFile(targetDir) {
|
|
462
|
+
const legacy = path2.join(targetDir, LEGACY_CONFIG_FILENAME);
|
|
463
|
+
const dest = path2.join(targetDir, CONFIG_FILENAME);
|
|
464
|
+
if (!fs2.existsSync(legacy) || fs2.existsSync(dest)) return;
|
|
465
|
+
let parsed;
|
|
466
|
+
try {
|
|
467
|
+
parsed = JSON.parse(fs2.readFileSync(legacy, "utf-8"));
|
|
468
|
+
} catch {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const pat = parsed?.pat;
|
|
472
|
+
if (typeof pat !== "string" || !pat.startsWith("ls_pat_")) return;
|
|
473
|
+
if (DRY_RUN) {
|
|
474
|
+
dryNote(`would migrate legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} and strip the legacy gitignore line`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
fs2.renameSync(legacy, dest);
|
|
478
|
+
removeGitignoreLine(targetDir, LEGACY_CONFIG_FILENAME);
|
|
479
|
+
ok(`migrated legacy ${LEGACY_CONFIG_FILENAME} \u2192 ${CONFIG_FILENAME} (the old name is now reserved for file-backed-config)`);
|
|
480
|
+
}
|
|
481
|
+
function removeGitignoreLine(targetDir, line) {
|
|
482
|
+
const p = path2.join(targetDir, ".gitignore");
|
|
483
|
+
if (!fs2.existsSync(p)) return;
|
|
484
|
+
const before = fs2.readFileSync(p, "utf-8");
|
|
485
|
+
const after = before.split(/\r?\n/).filter((l) => l.trim() !== line).join("\n");
|
|
486
|
+
if (after === before) return;
|
|
487
|
+
if (DRY_RUN) {
|
|
488
|
+
dryNote(`would remove "${line}" from .gitignore`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
fs2.writeFileSync(p, after, "utf-8");
|
|
492
|
+
ok(`removed ${line} from .gitignore (now reserved for file-backed-config)`);
|
|
493
|
+
}
|
|
494
|
+
var LAUNCH_SECURE_HEADERS_HELPER = `node -e 'const j=JSON.parse(require("fs").readFileSync(".launch-secure.cred.config","utf-8"));const c=j.profiles&&j.active?j.profiles[j.active]:j;process.stdout.write(JSON.stringify({Authorization:"Bearer "+c.pat,"X-Org-Slug":c.orgSlug,"X-Project-Slug":c.projectSlug}))'`;
|
|
495
|
+
function buildLaunchKitMcpEntries(cfg) {
|
|
496
|
+
return {
|
|
497
|
+
"launch-secure": {
|
|
498
|
+
type: "http",
|
|
499
|
+
url: `${cfg.serverUrl}/api/mcp/project`,
|
|
500
|
+
headersHelper: LAUNCH_SECURE_HEADERS_HELPER
|
|
501
|
+
},
|
|
502
|
+
"launch-chart": {
|
|
503
|
+
command: "npx",
|
|
504
|
+
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-chart"],
|
|
505
|
+
env: { LAUNCH_CHART_AUTOSERVE: "1" }
|
|
506
|
+
},
|
|
507
|
+
"launch-deck": {
|
|
508
|
+
command: "npx",
|
|
509
|
+
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-deck"]
|
|
510
|
+
},
|
|
511
|
+
"launch-orbit": {
|
|
512
|
+
command: "npx",
|
|
513
|
+
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-orbit", "mcp"]
|
|
514
|
+
},
|
|
515
|
+
"launch-recall": {
|
|
516
|
+
command: "npx",
|
|
517
|
+
args: ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "mcp"]
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function mergeMcpFile(targetDir, launchKitEntries) {
|
|
522
|
+
const p = path2.join(targetDir, ".mcp.json");
|
|
523
|
+
const hadExisting = fs2.existsSync(p);
|
|
524
|
+
let existing = {};
|
|
525
|
+
if (hadExisting) {
|
|
526
|
+
try {
|
|
527
|
+
existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
528
|
+
} catch (err) {
|
|
529
|
+
fail(`Could not parse existing .mcp.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const existingServerCount = Object.keys(existing.mcpServers ?? {}).length;
|
|
533
|
+
const merged = { ...existing, mcpServers: { ...existing.mcpServers ?? {} } };
|
|
534
|
+
const overwrites = [];
|
|
535
|
+
const additions = [];
|
|
536
|
+
for (const [name, entry] of Object.entries(launchKitEntries)) {
|
|
537
|
+
if (merged.mcpServers[name]) overwrites.push(name);
|
|
538
|
+
else additions.push(name);
|
|
539
|
+
merged.mcpServers[name] = entry;
|
|
540
|
+
}
|
|
541
|
+
if (DRY_RUN) {
|
|
542
|
+
const action2 = hadExisting && existingServerCount > 0 ? "would merge into" : "would write";
|
|
543
|
+
dryNote(`${action2} .mcp.json \u2014 overwriting [${overwrites.join(", ") || "none"}], adding [${additions.join(", ") || "none"}]`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
547
|
+
const action = hadExisting && existingServerCount > 0 ? "merged into" : "wrote";
|
|
548
|
+
ok(`${action} .mcp.json (${Object.keys(launchKitEntries).length} launch-kit entries)`);
|
|
549
|
+
}
|
|
550
|
+
function detectPackageManager(repoDir) {
|
|
551
|
+
const pkgPath = path2.join(repoDir, "package.json");
|
|
552
|
+
if (!fs2.existsSync(pkgPath)) return null;
|
|
553
|
+
try {
|
|
554
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
555
|
+
if (typeof pkg.packageManager === "string") {
|
|
556
|
+
const name = pkg.packageManager.split("@")[0];
|
|
557
|
+
const match = PACKAGE_MANAGERS.find((p) => p.name === name);
|
|
558
|
+
if (match) return { pm: match, source: `package.json packageManager (${pkg.packageManager})` };
|
|
559
|
+
info(`packageManager field "${pkg.packageManager}" is not recognized \u2014 falling back to lockfile detection`);
|
|
560
|
+
}
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
const matches = PACKAGE_MANAGERS.map((pm) => ({ pm, lockfile: pm.lockfiles.find((lf) => fs2.existsSync(path2.join(repoDir, lf))) ?? null })).filter((m) => m.lockfile !== null);
|
|
564
|
+
if (matches.length === 1) {
|
|
565
|
+
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile}` };
|
|
566
|
+
}
|
|
567
|
+
if (matches.length > 1) {
|
|
568
|
+
info(`multiple lockfiles found (${matches.map((m) => m.lockfile).join(", ")}) \u2014 picking ${matches[0].pm.name} by precedence`);
|
|
569
|
+
return { pm: matches[0].pm, source: `lockfile ${matches[0].lockfile} (multiple present)` };
|
|
570
|
+
}
|
|
571
|
+
for (const pm of PACKAGE_MANAGERS) {
|
|
572
|
+
if (pm.workspaceFiles?.some((wf) => fs2.existsSync(path2.join(repoDir, wf)))) {
|
|
573
|
+
return { pm, source: `workspace file (${pm.workspaceFiles.find((wf) => fs2.existsSync(path2.join(repoDir, wf)))})` };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const npm = PACKAGE_MANAGERS.find((p) => p.name === "npm");
|
|
577
|
+
return { pm: npm, source: "default (no signal found)" };
|
|
578
|
+
}
|
|
579
|
+
function runInstall(repoDir, detected) {
|
|
580
|
+
const { pm } = detected;
|
|
581
|
+
if (!which(pm.binary)) {
|
|
582
|
+
fail(
|
|
583
|
+
`${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 ${path2.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
info(`running ${pm.binary} ${pm.installArgs.join(" ")} \u2026`);
|
|
587
|
+
if (DRY_RUN) {
|
|
588
|
+
dryNote(`would run: ${pm.binary} ${pm.installArgs.join(" ")} (cwd: ${repoDir})`);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const res = (0, import_node_child_process.spawnSync)(pm.binary, pm.installArgs, { cwd: repoDir, stdio: "inherit" });
|
|
592
|
+
if (res.status !== 0) {
|
|
593
|
+
fail(
|
|
594
|
+
`${pm.name} install failed (exit ${res.status}). Configs and clone are intact \u2014 fix the underlying error and retry: cd ${path2.basename(repoDir)} && ${pm.binary} ${pm.installArgs.join(" ")}`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
ok(`${pm.name} install complete`);
|
|
598
|
+
}
|
|
599
|
+
function hasOnboardScript(repoDir) {
|
|
600
|
+
const pkgPath = path2.join(repoDir, "package.json");
|
|
601
|
+
if (!fs2.existsSync(pkgPath)) return false;
|
|
602
|
+
try {
|
|
603
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
604
|
+
return typeof pkg.scripts?.[ONBOARD_SCRIPT_NAME] === "string";
|
|
605
|
+
} catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function runRecallInit(repoDir) {
|
|
610
|
+
info(`scaffolding launch-recall (shadow git backup) \u2026`);
|
|
611
|
+
const recallEntry = path2.resolve(__dirname, "recall-entry.js");
|
|
612
|
+
const useSibling = fs2.existsSync(recallEntry);
|
|
613
|
+
const cmd = useSibling ? process.execPath : "npx";
|
|
614
|
+
const args = useSibling ? [recallEntry, "init"] : ["-y", "-p", LAUNCH_KIT_PKG, "launch-recall", "init"];
|
|
615
|
+
if (DRY_RUN) {
|
|
616
|
+
dryNote(`would run launch-recall init: ${cmd} ${args.join(" ")} (cwd: ${repoDir})`);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const res = (0, import_node_child_process.spawnSync)(cmd, args, { cwd: repoDir, stdio: "inherit" });
|
|
620
|
+
if (res.status !== 0) {
|
|
621
|
+
info(`\u26A0 launch-recall init failed (exit ${res.status}). Main onboarding is complete \u2014 you can retry later: cd ${path2.basename(repoDir)} && npx -y -p ${LAUNCH_KIT_PKG} launch-recall init`);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
ok(`launch-recall ready (shadow git initialized)`);
|
|
625
|
+
}
|
|
626
|
+
function runOnboard(repoDir, pm) {
|
|
627
|
+
info(`running ${pm.binary} run ${ONBOARD_SCRIPT_NAME} \u2026`);
|
|
628
|
+
if (DRY_RUN) {
|
|
629
|
+
dryNote(`would run: ${pm.binary} run ${ONBOARD_SCRIPT_NAME} (cwd: ${repoDir})`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const res = (0, import_node_child_process.spawnSync)(pm.binary, ["run", ONBOARD_SCRIPT_NAME], { cwd: repoDir, stdio: "inherit" });
|
|
633
|
+
if (res.status !== 0) {
|
|
634
|
+
fail(
|
|
635
|
+
`${pm.name} run ${ONBOARD_SCRIPT_NAME} failed (exit ${res.status}). Install completed but the onboard script errored. Fix and retry: cd ${path2.basename(repoDir)} && ${pm.binary} run ${ONBOARD_SCRIPT_NAME}`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
ok(`${ONBOARD_SCRIPT_NAME} script complete`);
|
|
639
|
+
}
|
|
640
|
+
function ensureGitignoreLine(targetDir, line) {
|
|
641
|
+
const p = path2.join(targetDir, ".gitignore");
|
|
642
|
+
let content = fs2.existsSync(p) ? fs2.readFileSync(p, "utf-8") : "";
|
|
643
|
+
const lines = content.split(/\r?\n/);
|
|
644
|
+
if (lines.some((l) => l.trim() === line)) return;
|
|
645
|
+
if (content.length && !content.endsWith("\n")) content += "\n";
|
|
646
|
+
content += `${line}
|
|
647
|
+
`;
|
|
648
|
+
if (DRY_RUN) {
|
|
649
|
+
dryNote(`would append "${line}" to .gitignore`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
fs2.writeFileSync(p, content, "utf-8");
|
|
653
|
+
ok(`appended ${line} to .gitignore`);
|
|
654
|
+
}
|
|
655
|
+
function copyScaffoldIfMissing(srcPath, destPath, label) {
|
|
656
|
+
if (!fs2.existsSync(srcPath)) return "missing-src";
|
|
657
|
+
if (fs2.existsSync(destPath)) {
|
|
658
|
+
info(`${label} already present \u2014 leaving alone`);
|
|
659
|
+
return "existed";
|
|
660
|
+
}
|
|
661
|
+
if (DRY_RUN) {
|
|
662
|
+
dryNote(`would write ${label}`);
|
|
663
|
+
return "wrote";
|
|
664
|
+
}
|
|
665
|
+
fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
|
|
666
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
667
|
+
try {
|
|
668
|
+
const srcMode = fs2.statSync(srcPath).mode;
|
|
669
|
+
fs2.chmodSync(destPath, srcMode);
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
ok(`wrote ${label}`);
|
|
673
|
+
return "wrote";
|
|
674
|
+
}
|
|
675
|
+
function copyScaffoldDirAlways(srcDir, destDir, labelPrefix) {
|
|
676
|
+
if (!fs2.existsSync(srcDir)) return;
|
|
677
|
+
for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
|
|
678
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
679
|
+
const destPath = path2.join(destDir, entry.name);
|
|
680
|
+
const label = labelPrefix ? `${labelPrefix}/${entry.name}` : entry.name;
|
|
681
|
+
if (entry.isDirectory()) {
|
|
682
|
+
copyScaffoldDirAlways(srcPath, destPath, label);
|
|
683
|
+
} else if (entry.isFile()) {
|
|
684
|
+
const existed = fs2.existsSync(destPath);
|
|
685
|
+
if (DRY_RUN) {
|
|
686
|
+
dryNote(`would ${existed ? "refresh" : "write"} ${label}`);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
|
|
690
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
691
|
+
try {
|
|
692
|
+
const srcMode = fs2.statSync(srcPath).mode;
|
|
693
|
+
fs2.chmodSync(destPath, srcMode);
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
ok(`${existed ? "refreshed" : "wrote"} ${label}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function scaffoldMigrateSafety(targetDir) {
|
|
701
|
+
const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "migrate-safety");
|
|
702
|
+
if (!fs2.existsSync(scaffoldsRoot)) {
|
|
703
|
+
info(`\u26A0 migrate-safety scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const files = [
|
|
707
|
+
{
|
|
708
|
+
src: path2.join(scaffoldsRoot, ".github", "workflows", "backup-on-migration.yml"),
|
|
709
|
+
dest: path2.join(targetDir, ".github", "workflows", "backup-on-migration.yml"),
|
|
710
|
+
label: ".github/workflows/backup-on-migration.yml"
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
src: path2.join(scaffoldsRoot, "scripts", "migrate-with-backup.sh"),
|
|
714
|
+
dest: path2.join(targetDir, "scripts", "migrate-with-backup.sh"),
|
|
715
|
+
label: "scripts/migrate-with-backup.sh"
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
src: path2.join(scaffoldsRoot, "docs", "migrations-runbook.md"),
|
|
719
|
+
dest: path2.join(targetDir, "docs", "migrations-runbook.md"),
|
|
720
|
+
label: "docs/migrations-runbook.md"
|
|
721
|
+
}
|
|
722
|
+
];
|
|
723
|
+
info("scaffolding migrate-safety (pg_dump wrapper + GHA backup workflow + runbook) \u2026");
|
|
724
|
+
for (const f of files) copyScaffoldIfMissing(f.src, f.dest, f.label);
|
|
725
|
+
ensureGitignoreLine(targetDir, ".backups/");
|
|
726
|
+
ok("migrate-safety ready \u2014 see docs/migrations-runbook.md for db:migrate wiring + PROD_DATABASE_URL secret setup");
|
|
727
|
+
}
|
|
728
|
+
var MARKETPLACE_ID = "launchsecure";
|
|
729
|
+
var LS_PLUGIN_ID = "ls";
|
|
730
|
+
function scaffoldLsMarketplace(targetDir) {
|
|
731
|
+
const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "ls-marketplace");
|
|
732
|
+
if (!fs2.existsSync(scaffoldsRoot)) {
|
|
733
|
+
info(`\u26A0 ls-marketplace scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const marketplaceRoot = path2.join(targetDir, ".claude", "marketplace");
|
|
737
|
+
info("scaffolding ls marketplace (Claude Code /ls: namespace \u2014 refreshes every /ls:* command found in the scaffold) \u2026");
|
|
738
|
+
copyScaffoldDirAlways(scaffoldsRoot, marketplaceRoot, ".claude/marketplace");
|
|
739
|
+
wireLsSettings(targetDir);
|
|
740
|
+
ok(`ls marketplace ready \u2014 open this repo in Claude Code, approve the "${MARKETPLACE_ID}" marketplace prompt, then try /ls:activate-beacon, /ls:standup, or /ls:show-mcp-status`);
|
|
741
|
+
}
|
|
742
|
+
function wireLsSettings(targetDir) {
|
|
743
|
+
const p = path2.join(targetDir, ".claude", "settings.json");
|
|
744
|
+
const hadExisting = fs2.existsSync(p);
|
|
745
|
+
let existing = {};
|
|
746
|
+
if (hadExisting) {
|
|
747
|
+
try {
|
|
748
|
+
existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
749
|
+
} catch (err) {
|
|
750
|
+
fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const merged = { ...existing };
|
|
754
|
+
merged.extraKnownMarketplaces = {
|
|
755
|
+
...existing.extraKnownMarketplaces ?? {},
|
|
756
|
+
[MARKETPLACE_ID]: {
|
|
757
|
+
source: { source: "directory", path: "./.claude/marketplace" }
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
merged.enabledPlugins = {
|
|
761
|
+
...existing.enabledPlugins ?? {},
|
|
762
|
+
[`${LS_PLUGIN_ID}@${MARKETPLACE_ID}`]: true
|
|
763
|
+
};
|
|
764
|
+
if (DRY_RUN) {
|
|
765
|
+
dryNote(`would ${hadExisting ? "merge into" : "write"} .claude/settings.json (set extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${LS_PLUGIN_ID}@${MARKETPLACE_ID}; preserves every other key)`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
fs2.mkdirSync(path2.dirname(p), { recursive: true });
|
|
769
|
+
fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
770
|
+
ok(`${hadExisting ? "merged into" : "wrote"} .claude/settings.json (extraKnownMarketplaces.${MARKETPLACE_ID} + enabledPlugins.${LS_PLUGIN_ID}@${MARKETPLACE_ID})`);
|
|
771
|
+
}
|
|
772
|
+
var RECALL_HOOK_SIGNATURE = "ensure-recall.sh";
|
|
773
|
+
var RECALL_HOOK_COMMAND = 'bash "${CLAUDE_PROJECT_DIR:-$PWD}/scripts/ensure-recall.sh"';
|
|
774
|
+
function scaffoldRecallHook(targetDir) {
|
|
775
|
+
const scaffoldsRoot = path2.resolve(__dirname, "..", "..", "scaffolds", "recall-hook");
|
|
776
|
+
if (!fs2.existsSync(scaffoldsRoot)) {
|
|
777
|
+
info(`\u26A0 recall-hook scaffolds not found at ${scaffoldsRoot} \u2014 skipping (this is a packaging bug; main onboarding is unaffected)`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
info("scaffolding recall-hook (SessionStart watcher-respawn hook + ensure-recall.sh) \u2026");
|
|
781
|
+
copyScaffoldDirAlways(scaffoldsRoot, targetDir, "");
|
|
782
|
+
wireRecallHook(targetDir);
|
|
783
|
+
ok("recall-hook ready \u2014 opens with Claude Code will respawn the launch-recall watcher if it died between sessions");
|
|
784
|
+
}
|
|
785
|
+
function wireRecallHook(targetDir) {
|
|
786
|
+
const p = path2.join(targetDir, ".claude", "settings.json");
|
|
787
|
+
const hadExisting = fs2.existsSync(p);
|
|
788
|
+
let existing = {};
|
|
789
|
+
if (hadExisting) {
|
|
790
|
+
try {
|
|
791
|
+
existing = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
792
|
+
} catch (err) {
|
|
793
|
+
fail(`Could not parse existing .claude/settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const hooks = existing.hooks ?? {};
|
|
797
|
+
const sessionStart = hooks.SessionStart ?? [];
|
|
798
|
+
const alreadyWired = sessionStart.some(
|
|
799
|
+
(group) => (group.hooks ?? []).some((h) => typeof h?.command === "string" && h.command.includes(RECALL_HOOK_SIGNATURE))
|
|
800
|
+
);
|
|
801
|
+
if (alreadyWired) {
|
|
802
|
+
info(".claude/settings.json SessionStart hook already references ensure-recall.sh \u2014 leaving alone");
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const newGroup = {
|
|
806
|
+
hooks: [{ type: "command", command: RECALL_HOOK_COMMAND }]
|
|
807
|
+
};
|
|
808
|
+
const merged = {
|
|
809
|
+
...existing,
|
|
810
|
+
hooks: {
|
|
811
|
+
...hooks,
|
|
812
|
+
SessionStart: [...sessionStart, newGroup]
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
if (DRY_RUN) {
|
|
816
|
+
dryNote(`would append SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh; preserves every other key + existing hooks)`);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
fs2.mkdirSync(path2.dirname(p), { recursive: true });
|
|
820
|
+
fs2.writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
821
|
+
ok(`appended SessionStart hook to .claude/settings.json (bash scripts/ensure-recall.sh)`);
|
|
822
|
+
}
|
|
823
|
+
async function main() {
|
|
824
|
+
const args = parseArgs(process.argv.slice(2));
|
|
825
|
+
if (args.help) {
|
|
826
|
+
printHelp();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const subcommand = process.argv[2];
|
|
830
|
+
if (subcommand && subcommand !== "init" && !subcommand.startsWith("--")) {
|
|
831
|
+
fail(`Unknown subcommand "${subcommand}". Only "init" is supported. Run with --help for usage.`);
|
|
832
|
+
}
|
|
833
|
+
DRY_RUN = args.dryRun;
|
|
834
|
+
if (DRY_RUN) {
|
|
835
|
+
info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
836
|
+
info("DRY RUN \u2014 no files will be written, no commands will run.");
|
|
837
|
+
info("Lines tagged (dry-run) show what would happen.");
|
|
838
|
+
info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
839
|
+
}
|
|
840
|
+
if (!args.token) {
|
|
841
|
+
const t = await prompt("LaunchSecure PAT (ls_pat_\u2026): ");
|
|
842
|
+
args.token = t || null;
|
|
843
|
+
}
|
|
844
|
+
if (!args.token) fail("--token (or LS_PAT env) is required.");
|
|
845
|
+
if (!/^ls_pat_/.test(args.token)) fail("Token does not look like a LaunchSecure PAT (expected prefix ls_pat_).");
|
|
846
|
+
if (!args.orgSlug) fail("--org=<orgSlug> is required.");
|
|
847
|
+
if (!args.projectSlug) fail("--project=<projectSlug> is required.");
|
|
848
|
+
const { hasGh } = preflight();
|
|
849
|
+
info(`resolving project ${args.orgSlug}/${args.projectSlug} on ${args.serverUrl} \u2026`);
|
|
850
|
+
let resolved;
|
|
851
|
+
try {
|
|
852
|
+
resolved = await callProjectInfo(args);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
855
|
+
}
|
|
856
|
+
ok(`resolved "${resolved.projectName}"`);
|
|
857
|
+
if (!resolved.repositoryUrl) {
|
|
858
|
+
fail(
|
|
859
|
+
`Project "${resolved.projectSlug}" has no GitHub repository configured. Connect GitHub at ${args.serverUrl}/${resolved.orgSlug}/projects/${resolved.projectSlug}/settings/integrations, then re-run init.`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
const repoUrl = resolved.repositoryUrl;
|
|
863
|
+
const cwd = process.cwd();
|
|
864
|
+
const targetDir = path2.resolve(args.targetDir ?? path2.join(cwd, resolved.projectSlug));
|
|
865
|
+
const normalizedRemote = normalizeRepoUrl(repoUrl);
|
|
866
|
+
let skipClone = false;
|
|
867
|
+
if (fs2.existsSync(targetDir)) {
|
|
868
|
+
if (isGitRepo(targetDir)) {
|
|
869
|
+
const existingRemote = gitRemoteUrl(targetDir);
|
|
870
|
+
if (existingRemote && normalizeRepoUrl(existingRemote) === normalizedRemote) {
|
|
871
|
+
ok(`${targetDir} is already a clone of ${repoUrl} \u2014 skipping clone, refreshing configs only`);
|
|
872
|
+
skipClone = true;
|
|
873
|
+
} else {
|
|
874
|
+
fail(`${targetDir} is a git repo but its remote (${existingRemote ?? "unknown"}) does not match ${repoUrl}. Refusing to overwrite. Pass --dir=<other-path>.`);
|
|
875
|
+
}
|
|
876
|
+
} else if (!dirIsEmpty(targetDir)) {
|
|
877
|
+
fail(`${targetDir} exists and is not empty (and not a matching git repo). Refusing to clone into it. Pass --dir=<other-path>.`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (!skipClone) cloneRepo(repoUrl, targetDir, hasGh);
|
|
881
|
+
const cfg = {
|
|
882
|
+
pat: args.token,
|
|
883
|
+
orgSlug: resolved.orgSlug,
|
|
884
|
+
projectSlug: resolved.projectSlug,
|
|
885
|
+
serverUrl: args.serverUrl
|
|
886
|
+
};
|
|
887
|
+
const courseName = args.course ?? inferCourseName(cfg.serverUrl);
|
|
888
|
+
writeConfigFile(targetDir, cfg, courseName);
|
|
889
|
+
mergeMcpFile(targetDir, buildLaunchKitMcpEntries(cfg));
|
|
890
|
+
ensureGitignoreLine(targetDir, CONFIG_FILENAME);
|
|
891
|
+
let installSkippedReason = null;
|
|
892
|
+
const detected = detectPackageManager(targetDir);
|
|
893
|
+
if (detected) info(`detected package manager: ${detected.pm.name} (${detected.source})`);
|
|
894
|
+
if (args.noInstall) {
|
|
895
|
+
installSkippedReason = "--no-install passed";
|
|
896
|
+
} else if (!detected) {
|
|
897
|
+
installSkippedReason = "no package.json found";
|
|
898
|
+
} else {
|
|
899
|
+
runInstall(targetDir, detected);
|
|
900
|
+
if (hasOnboardScript(targetDir)) runOnboard(targetDir, detected.pm);
|
|
901
|
+
}
|
|
902
|
+
const hasOnboard = hasOnboardScript(targetDir);
|
|
903
|
+
if (!args.noRecall) runRecallInit(targetDir);
|
|
904
|
+
if (!args.noMigrateSafety) scaffoldMigrateSafety(targetDir);
|
|
905
|
+
if (!args.noLsMarketplace) scaffoldLsMarketplace(targetDir);
|
|
906
|
+
if (!args.noRecallHook) scaffoldRecallHook(targetDir);
|
|
907
|
+
const relTarget = path2.relative(cwd, targetDir) || ".";
|
|
908
|
+
console.log("");
|
|
909
|
+
if (DRY_RUN) {
|
|
910
|
+
info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
911
|
+
info(`DRY RUN COMPLETE \u2014 no files were modified, no commands ran.`);
|
|
912
|
+
info(`Target: ${targetDir}`);
|
|
913
|
+
info(`Re-run without --dry-run to apply the changes shown above.`);
|
|
914
|
+
info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
ok(`done \u2014 ${resolved.projectName} is ready at ${targetDir}`);
|
|
918
|
+
if (installSkippedReason) {
|
|
919
|
+
const installLine = detected ? ` ${detected.pm.binary} ${detected.pm.installArgs.join(" ")}` : ` npm install # or your package manager of choice`;
|
|
920
|
+
const onboardLine = hasOnboard && detected ? `
|
|
921
|
+
${detected.pm.binary} run ${ONBOARD_SCRIPT_NAME} # project setup hook` : "";
|
|
922
|
+
console.log(`
|
|
923
|
+
Next steps (install skipped: ${installSkippedReason}):
|
|
924
|
+
cd ${relTarget}
|
|
925
|
+
${installLine}${onboardLine}
|
|
926
|
+
claude # launch Claude Code (5 MCPs wired)
|
|
927
|
+
${LAUNCH_KIT_TOOLS_GUIDE}`);
|
|
928
|
+
} else {
|
|
929
|
+
console.log(`
|
|
930
|
+
Next steps:
|
|
931
|
+
cd ${relTarget}
|
|
932
|
+
claude # launch Claude Code (5 MCPs wired)
|
|
933
|
+
${LAUNCH_KIT_TOOLS_GUIDE}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
main().catch((err) => {
|
|
937
|
+
console.error(`[launch-kit] unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
938
|
+
process.exit(1);
|
|
939
|
+
});
|