@launchsecure/launch-kit 0.0.32 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chart-client/assets/{index-B__ARB8k.js → index-DFu2xIrM.js} +2 -2
- package/dist/chart-client/assets/index-DpKO9p0s.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/{index-h8kMzVtG.js → index-Cbw6bVdx.js} +2 -2
- package/dist/client/assets/index-Dv6dD2zY.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/index-AqQ9Sei6.css +1 -0
- package/dist/council-client/assets/{index-CWaDcsFR.js → index-CAsmGTzg.js} +2 -2
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-C7GsHvgg.js → _baseUniq-BiVx0WO_.js} +1 -1
- package/dist/deck-client/assets/{arc-CSrZRINY.js → arc-DGMkiEzS.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-zoB-G17J.js → architectureDiagram-Q4EWVU46-Y2WRmHtk.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-BRjjtYH6.js → blockDiagram-DXYQGD6D-_Lbfu5BQ.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-C3D3sd2U.js → c4Diagram-AHTNJAMY-CTqpYTBX.js} +1 -1
- package/dist/deck-client/assets/channel-DB6LxW_l.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-DhpDMOPO.js → chunk-4BX2VUAB-liEIbPHs.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-BIRgPXRl.js → chunk-4TB4RGXK-CCc6lYvL.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-BF24dwDZ.js → chunk-55IACEB6-D02jJUR2.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-CW75Y61B.js → chunk-EDXVE4YY-BFmGMbLD.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-B5-oyL79.js → chunk-FMBD7UC4-6wFLOVcJ.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-BB2bHe_Q.js → chunk-OYMX7WX6-Bnr8RiBf.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-D80eZO4B.js → chunk-QZHKN3VN-Ct82MksJ.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-Dz9787p_.js → chunk-YZCP3GAM-BXmN1diQ.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-g944ZyG8.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-g944ZyG8.js +1 -0
- package/dist/deck-client/assets/clone-DiIRH1pI.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-MQjiZLcL.js → cose-bilkent-S5V4N54A-CmQCT-mH.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-DG4EcLpJ.js → dagre-KV5264BT-DDdSa9EX.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-1n7hM3Gc.js → diagram-5BDNPKRD-Bccks2xJ.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-CYMarncV.js → diagram-G4DWMVQ6-CPPNgxmQ.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-DSisoipe.js → diagram-MMDJMWI5-KrD300pS.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-Btnq49OJ.js → diagram-TYMM5635-DefnLuQf.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-Cu2Hb_Tz.js → erDiagram-SMLLAGMA-DI9FfnFP.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-CGJzUzsO.js → flowDiagram-DWJPFMVM-twKyd3Fx.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-D9sqGUBT.js → ganttDiagram-T4ZO3ILL-Wau3jhBr.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-C0QwX2od.js → gitGraphDiagram-UUTBAWPF-D9GgYXwb.js} +1 -1
- package/dist/deck-client/assets/{graph-CcBjOQCl.js → graph-BhNLzyXS.js} +1 -1
- package/dist/deck-client/assets/index-B-YQq5b5.css +1 -0
- package/dist/deck-client/assets/{index-0arwoc0z.js → index-BtQBaQ7s.js} +3 -3
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-DTimhhhS.js → infoDiagram-42DDH7IO-TylGlSG-.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DxOxg_B4.js → ishikawaDiagram-UXIWVN3A-DAT8icpg.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-Bpq0qa4j.js → journeyDiagram-VCZTEJTY-D3v_XL72.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-aTIrpcVO.js → kanban-definition-6JOO6SKY-DNUOBiNr.js} +1 -1
- package/dist/deck-client/assets/{layout-DqglLR2E.js → layout-COfodgwF.js} +1 -1
- package/dist/deck-client/assets/{linear-D5GxehPc.js → linear-DmTsuIvK.js} +1 -1
- package/dist/deck-client/assets/{min-DXLfSREq.js → min-BW1F7i1D.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-mO5Vys7I.js → mindmap-definition-QFDTVHPH-CErFzKWl.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-Dm0gzdAr.js → pieDiagram-DEJITSTG-DW5F757o.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-Daq7j3qD.js → quadrantDiagram-34T5L4WZ-B1S2-TfI.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CmwV95um.js → requirementDiagram-MS252O5E-BY5BAR-5.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BOYl3Nkf.js → sankeyDiagram-XADWPNL6-CE1Cp9HS.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-BuUjhIcW.js → sequenceDiagram-FGHM5R23-IaHnbKye.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-LUZ_uwio.js → stateDiagram-FHFEXIEX-CwPJm9hU.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-DQYa2M1q.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-CDUxCCAW.js → timeline-definition-GMOUNBTQ-DVFGGSgN.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-BRb24Tf7.js → vennDiagram-DHZGUBPP-C1194MJi.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-CHZiUbBa.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-BLGlYrQz.js → wardleyDiagram-NUSXRM2D-hpwdFfGj.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-De31MSnk.js → xychartDiagram-5P7HB3ND-DYkotwy8.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/cli.js +91 -13
- package/dist/server/council-entry.js +0 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/init-entry.js +740 -221
- package/dist/server/radar-docker-init-entry.js +239 -0
- package/dist/server/radar-entrypoint-entry.js +99 -0
- package/dist/server/radar-teardown-entry.js +477 -0
- package/dist/server/recall-entry.js +4 -1
- package/package.json +22 -23
- package/scaffolds/ls-marketplace/plugins/kit/commands/activate-statusline.md +5 -5
- package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +274 -0
- package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
- package/scaffolds/statusline/statusline-mcp.sh +82 -2
- package/scaffolds/statusline/statusline-wrapper.sh +8 -1
- package/dist/chart-client/assets/index-CDIhdgWg.css +0 -1
- package/dist/client/assets/index-CfW4n40I.css +0 -32
- package/dist/council-client/assets/index-CZim6x1u.css +0 -1
- package/dist/deck-client/assets/channel-8ReQnQfH.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-cRxTeGkK.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-cRxTeGkK.js +0 -1
- package/dist/deck-client/assets/clone-LSHZ3K6R.js +0 -1
- package/dist/deck-client/assets/index-BlTlhxFW.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CnnRwE5D.js +0 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-B0BYyVBY.js +0 -162
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/server/radar-teardown-entry.ts
|
|
5
|
+
var import_node_child_process = require("node:child_process");
|
|
6
|
+
var import_node_fs = require("node:fs");
|
|
7
|
+
var import_node_path = require("node:path");
|
|
8
|
+
|
|
9
|
+
// src/server/recall/config.ts
|
|
10
|
+
var DEFAULT_CONFIG = {
|
|
11
|
+
debounce: 3e3,
|
|
12
|
+
ignore: [
|
|
13
|
+
"node_modules",
|
|
14
|
+
".next",
|
|
15
|
+
"dist",
|
|
16
|
+
"build",
|
|
17
|
+
".turbo",
|
|
18
|
+
".git",
|
|
19
|
+
".recall",
|
|
20
|
+
"coverage",
|
|
21
|
+
".pnpm-store",
|
|
22
|
+
".launchsecure/graphs",
|
|
23
|
+
".claude/settings.local.json"
|
|
24
|
+
],
|
|
25
|
+
retention: {
|
|
26
|
+
keepLast: 5e3,
|
|
27
|
+
maxAgeDays: 30
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/server/radar/mcp.ts
|
|
32
|
+
var import_node_https = require("node:https");
|
|
33
|
+
var import_node_http = require("node:http");
|
|
34
|
+
var ProjectMcpClient = class {
|
|
35
|
+
constructor(opts) {
|
|
36
|
+
this.initialized = false;
|
|
37
|
+
this.callId = 1;
|
|
38
|
+
this.mcpUrl = new URL("/api/mcp/project", opts.serverUrl);
|
|
39
|
+
this.headers = {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
"Accept": "application/json, text/event-stream",
|
|
42
|
+
"Authorization": `Bearer ${opts.pat}`
|
|
43
|
+
};
|
|
44
|
+
if (opts.orgSlug) this.headers["X-Org-Slug"] = opts.orgSlug;
|
|
45
|
+
if (opts.projectSlug) this.headers["X-Project-Slug"] = opts.projectSlug;
|
|
46
|
+
}
|
|
47
|
+
async call(toolName, args) {
|
|
48
|
+
await this.ensureInitialized();
|
|
49
|
+
const body = JSON.stringify({
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
id: this.callId++,
|
|
52
|
+
method: "tools/call",
|
|
53
|
+
params: { name: toolName, arguments: args }
|
|
54
|
+
});
|
|
55
|
+
const resp = await this.send(body);
|
|
56
|
+
const parsed = parseBody(resp.body);
|
|
57
|
+
if (parsed.error) {
|
|
58
|
+
throw new Error(`MCP ${toolName} failed: ${parsed.error.message ?? JSON.stringify(parsed.error)}`);
|
|
59
|
+
}
|
|
60
|
+
const text = parsed.result?.content?.[0]?.text;
|
|
61
|
+
if (!text) return parsed.result;
|
|
62
|
+
if (text.startsWith("\u2500\u2500 Error")) {
|
|
63
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
64
|
+
const reason = lines.find((l) => !l.startsWith("\u2500\u2500")) ?? text;
|
|
65
|
+
throw new Error(`MCP ${toolName} failed: ${reason}`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(text);
|
|
69
|
+
} catch {
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async ensureInitialized() {
|
|
74
|
+
if (this.initialized) return;
|
|
75
|
+
const initBody = JSON.stringify({
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
id: this.callId++,
|
|
78
|
+
method: "initialize",
|
|
79
|
+
params: {
|
|
80
|
+
protocolVersion: "2025-03-26",
|
|
81
|
+
capabilities: {},
|
|
82
|
+
clientInfo: { name: "launchpod-feedback-agent", version: "0.0.1" }
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const initResp = await this.send(initBody);
|
|
86
|
+
if (initResp.sessionId) this.sessionId = initResp.sessionId;
|
|
87
|
+
const notifBody = JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
88
|
+
await this.send(notifBody);
|
|
89
|
+
this.initialized = true;
|
|
90
|
+
}
|
|
91
|
+
send(body) {
|
|
92
|
+
return new Promise((resolve2, reject) => {
|
|
93
|
+
const headers = {
|
|
94
|
+
...this.headers,
|
|
95
|
+
"Content-Length": String(Buffer.byteLength(body))
|
|
96
|
+
};
|
|
97
|
+
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
98
|
+
const requester = this.mcpUrl.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
99
|
+
const req = requester(
|
|
100
|
+
{
|
|
101
|
+
host: this.mcpUrl.hostname,
|
|
102
|
+
port: this.mcpUrl.port || (this.mcpUrl.protocol === "https:" ? 443 : 80),
|
|
103
|
+
path: this.mcpUrl.pathname + this.mcpUrl.search,
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers
|
|
106
|
+
},
|
|
107
|
+
(res) => {
|
|
108
|
+
const chunks = [];
|
|
109
|
+
res.on("data", (c) => chunks.push(c));
|
|
110
|
+
res.on("end", () => {
|
|
111
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
112
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
113
|
+
reject(new Error(`MCP HTTP ${res.statusCode}: ${text}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const sid = res.headers["mcp-session-id"];
|
|
117
|
+
resolve2({ body: text, sessionId: typeof sid === "string" ? sid : void 0 });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
req.on("error", reject);
|
|
122
|
+
req.write(body);
|
|
123
|
+
req.end();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
function parseBody(text) {
|
|
128
|
+
if (!text) return {};
|
|
129
|
+
if (text.startsWith("event:") || text.includes("\ndata: ")) {
|
|
130
|
+
for (const line of text.split("\n")) {
|
|
131
|
+
if (line.startsWith("data: ")) {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(line.slice(6));
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(text);
|
|
141
|
+
} catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/server/radar-teardown-entry.ts
|
|
147
|
+
var COMPOSE_FILE = (0, import_node_path.resolve)(process.cwd(), "docker-compose.yml");
|
|
148
|
+
var RADAR_DIR = (0, import_node_path.dirname)(COMPOSE_FILE);
|
|
149
|
+
var COMPOSE_BASE = ["compose", "-f", COMPOSE_FILE];
|
|
150
|
+
var ENV_PATH = (0, import_node_path.join)(RADAR_DIR, ".env");
|
|
151
|
+
function ensureRadarCompose() {
|
|
152
|
+
if (!(0, import_node_fs.existsSync)(COMPOSE_FILE)) {
|
|
153
|
+
console.error(`[teardown] aborting \u2014 no docker-compose.yml at ${COMPOSE_FILE}`);
|
|
154
|
+
console.error(`[teardown] run from packages/cli/docker/radar/ (the directory holding the radar compose).`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const text = (0, import_node_fs.readFileSync)(COMPOSE_FILE, "utf-8");
|
|
158
|
+
const hasServices = /(^|\n)services:\s*(\n|$)/.test(text);
|
|
159
|
+
const hasRadar = /\n[ \t]+radar:/.test(text);
|
|
160
|
+
if (!hasServices || !hasRadar) {
|
|
161
|
+
console.error(`[teardown] aborting \u2014 ${COMPOSE_FILE} does not define a 'radar' service.`);
|
|
162
|
+
console.error(`[teardown] this command is only for the launch-pod radar container; refusing to touch this compose project.`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function parseArgs(argv) {
|
|
167
|
+
const out = { force: false, removeEnv: false, removeImage: false, help: false };
|
|
168
|
+
for (const a of argv) {
|
|
169
|
+
switch (a) {
|
|
170
|
+
case "--force":
|
|
171
|
+
out.force = true;
|
|
172
|
+
break;
|
|
173
|
+
case "--remove-env":
|
|
174
|
+
out.removeEnv = true;
|
|
175
|
+
break;
|
|
176
|
+
case "--remove-image":
|
|
177
|
+
out.removeImage = true;
|
|
178
|
+
break;
|
|
179
|
+
case "-h":
|
|
180
|
+
case "--help":
|
|
181
|
+
out.help = true;
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
console.error(`unknown flag: ${a}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function printHelp() {
|
|
191
|
+
console.log("usage: launch-pod radar:teardown [--remove-env] [--remove-image] [--force]");
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log(" --remove-env also delete .env (default: keep \u2014 contains live creds)");
|
|
194
|
+
console.log(" --remove-image also remove launchpod-radar:local image");
|
|
195
|
+
console.log(" --force skip the workspace-safety preflight (discards");
|
|
196
|
+
console.log(" uncommitted changes / unpushed commits / in-flight");
|
|
197
|
+
console.log(" analyzer sessions without warning)");
|
|
198
|
+
}
|
|
199
|
+
function sh(cmd, args, opts = {}) {
|
|
200
|
+
const r = (0, import_node_child_process.spawnSync)(cmd, args, { encoding: "utf8", ...opts });
|
|
201
|
+
return {
|
|
202
|
+
status: r.status ?? 1,
|
|
203
|
+
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
204
|
+
stderr: typeof r.stderr === "string" ? r.stderr : ""
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function workspaceSh(command) {
|
|
208
|
+
const psQ = sh("docker", [...COMPOSE_BASE, "ps", "-q", "radar"]);
|
|
209
|
+
if (psQ.status === 0 && psQ.stdout.trim()) {
|
|
210
|
+
return sh("docker", [...COMPOSE_BASE, "exec", "-T", "radar", "sh", "-c", command]).stdout;
|
|
211
|
+
}
|
|
212
|
+
return sh("docker", [...COMPOSE_BASE, "run", "--rm", "--no-deps", "--entrypoint", "sh", "radar", "-c", command]).stdout;
|
|
213
|
+
}
|
|
214
|
+
function loadIgnoreSegs() {
|
|
215
|
+
const json = workspaceSh("cat /workspace/.recall/config.json 2>/dev/null");
|
|
216
|
+
if (json.trim()) {
|
|
217
|
+
try {
|
|
218
|
+
const j = JSON.parse(json);
|
|
219
|
+
if (Array.isArray(j.ignore) && j.ignore.every((s) => typeof s === "string")) {
|
|
220
|
+
return j.ignore;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return [...DEFAULT_CONFIG.ignore];
|
|
226
|
+
}
|
|
227
|
+
function pathIsNoise(p, segs) {
|
|
228
|
+
for (const seg of segs) {
|
|
229
|
+
if (p === seg) return true;
|
|
230
|
+
if (p.startsWith(seg + "/")) return true;
|
|
231
|
+
if (p.endsWith("/" + seg)) return true;
|
|
232
|
+
if (p.includes("/" + seg + "/")) return true;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
var SCAFFOLDED_MCP_PATHS = [
|
|
237
|
+
"mcpServers.launch-secure.url"
|
|
238
|
+
];
|
|
239
|
+
function collectDiffPaths(a, b, prefix, out) {
|
|
240
|
+
if (a === b) return;
|
|
241
|
+
const aIsPrim = a === null || typeof a !== "object";
|
|
242
|
+
const bIsPrim = b === null || typeof b !== "object";
|
|
243
|
+
if (aIsPrim || bIsPrim) {
|
|
244
|
+
if (a !== b) out.push(prefix || "$");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const aIsArr = Array.isArray(a);
|
|
248
|
+
const bIsArr = Array.isArray(b);
|
|
249
|
+
if (aIsArr || bIsArr) {
|
|
250
|
+
if (!aIsArr || !bIsArr || a.length !== b.length) {
|
|
251
|
+
out.push(prefix || "$");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const aa = a;
|
|
255
|
+
const bb = b;
|
|
256
|
+
for (let i = 0; i < aa.length; i++) {
|
|
257
|
+
collectDiffPaths(aa[i], bb[i], prefix ? `${prefix}[${i}]` : `[${i}]`, out);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const ao = a;
|
|
262
|
+
const bo = b;
|
|
263
|
+
for (const k of /* @__PURE__ */ new Set([...Object.keys(ao), ...Object.keys(bo)])) {
|
|
264
|
+
const child = prefix ? `${prefix}.${k}` : k;
|
|
265
|
+
if (!(k in ao) || !(k in bo)) {
|
|
266
|
+
out.push(child);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
collectDiffPaths(ao[k], bo[k], child, out);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function mcpJsonOnlyScaffoldedDiff() {
|
|
273
|
+
const headJson = workspaceSh("git -C /workspace show HEAD:.mcp.json 2>/dev/null");
|
|
274
|
+
const wtJson = workspaceSh("cat /workspace/.mcp.json 2>/dev/null");
|
|
275
|
+
if (!headJson.trim() || !wtJson.trim()) return false;
|
|
276
|
+
let head, wt;
|
|
277
|
+
try {
|
|
278
|
+
head = JSON.parse(headJson);
|
|
279
|
+
wt = JSON.parse(wtJson);
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
const diffs = [];
|
|
284
|
+
collectDiffPaths(head, wt, "", diffs);
|
|
285
|
+
if (diffs.length === 0) return false;
|
|
286
|
+
return diffs.every((d) => SCAFFOLDED_MCP_PATHS.some((s) => d === s || d.startsWith(s + ".")));
|
|
287
|
+
}
|
|
288
|
+
function filterStatusSignal(raw, modeOnly, segs, scaffoldClean) {
|
|
289
|
+
const out = [];
|
|
290
|
+
for (const rawLine of raw.split("\n")) {
|
|
291
|
+
const line = rawLine.replace(/\r$/, "");
|
|
292
|
+
if (line.length < 3) continue;
|
|
293
|
+
let p = line.slice(3);
|
|
294
|
+
if (line.startsWith("R") || line.startsWith("C")) {
|
|
295
|
+
const arrow = p.indexOf(" -> ");
|
|
296
|
+
if (arrow >= 0) p = p.slice(arrow + 4);
|
|
297
|
+
}
|
|
298
|
+
if (pathIsNoise(p, segs)) continue;
|
|
299
|
+
if (modeOnly.has(p)) continue;
|
|
300
|
+
if (scaffoldClean.has(p)) continue;
|
|
301
|
+
out.push(line);
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
function probeDocker() {
|
|
306
|
+
const containerRunning = sh("docker", [...COMPOSE_BASE, "ps", "-q", "radar"]).stdout.trim().length > 0;
|
|
307
|
+
const projectName = (sh("docker", [...COMPOSE_BASE, "config", "--format", "json"]).stdout.match(/"name"\s*:\s*"([^"]+)"/) ?? [])[1] ?? "";
|
|
308
|
+
let workspaceVolumeExists = false;
|
|
309
|
+
if (projectName) {
|
|
310
|
+
const vols = sh("docker", ["volume", "ls", "-q", "--filter", `name=^${projectName}_workspace$`]);
|
|
311
|
+
workspaceVolumeExists = vols.status === 0 && vols.stdout.trim().length > 0;
|
|
312
|
+
}
|
|
313
|
+
return { containerRunning, workspaceVolumeExists };
|
|
314
|
+
}
|
|
315
|
+
function preflightWorkspace(args) {
|
|
316
|
+
if (args.force) {
|
|
317
|
+
console.log("[teardown] --force given \u2014 skipping workspace-safety preflight");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const state = probeDocker();
|
|
321
|
+
if (!state.containerRunning && !state.workspaceVolumeExists) {
|
|
322
|
+
console.log("[teardown] no radar container or workspace volume present \u2014 nothing to check");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
console.log("[teardown] checking workspace for unsaved work\u2026");
|
|
326
|
+
const segs = loadIgnoreSegs();
|
|
327
|
+
const numstatRaw = workspaceSh("git -C /workspace diff --numstat 2>/dev/null");
|
|
328
|
+
const modeOnly = /* @__PURE__ */ new Set();
|
|
329
|
+
for (const line of numstatRaw.split("\n")) {
|
|
330
|
+
const parts = line.split(" ");
|
|
331
|
+
if (parts.length >= 3 && parts[0] === "0" && parts[1] === "0") {
|
|
332
|
+
modeOnly.add(parts[2]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const rawDirty = workspaceSh("git -C /workspace status --porcelain 2>/dev/null | head -200");
|
|
336
|
+
const scaffoldClean = /* @__PURE__ */ new Set();
|
|
337
|
+
if (mcpJsonOnlyScaffoldedDiff()) scaffoldClean.add(".mcp.json");
|
|
338
|
+
const dirty = filterStatusSignal(rawDirty, modeOnly, segs, scaffoldClean);
|
|
339
|
+
const unpushedRaw = workspaceSh("git -C /workspace rev-list --count @{u}..HEAD 2>/dev/null || echo 0").trim();
|
|
340
|
+
const unpushed = parseInt(unpushedRaw || "0", 10);
|
|
341
|
+
const inflightRaw = workspaceSh(`grep -oE '"state":[[:space:]]*"(analyzing|queued)"' /workspace/.launchpod/radar-pings.json 2>/dev/null | wc -l | tr -d ' '`).trim();
|
|
342
|
+
const inflight = parseInt(inflightRaw || "0", 10);
|
|
343
|
+
let blocked = false;
|
|
344
|
+
if (dirty.length > 0) {
|
|
345
|
+
console.log("[teardown] \u2717 uncommitted changes in /workspace (after recall-ignore filter):");
|
|
346
|
+
for (const line of dirty) console.log(" " + line);
|
|
347
|
+
blocked = true;
|
|
348
|
+
}
|
|
349
|
+
if (unpushed > 0) {
|
|
350
|
+
console.log(`[teardown] \u2717 ${unpushed} unpushed commit(s) ahead of upstream`);
|
|
351
|
+
blocked = true;
|
|
352
|
+
}
|
|
353
|
+
if (inflight > 0) {
|
|
354
|
+
console.log(`[teardown] \u2717 ${inflight} in-flight or queued analyzer session(s) \u2014 output will be lost`);
|
|
355
|
+
blocked = true;
|
|
356
|
+
}
|
|
357
|
+
if (blocked) {
|
|
358
|
+
console.log("");
|
|
359
|
+
console.log("[teardown] aborting \u2014 workspace has unsaved work.");
|
|
360
|
+
console.log(" \u2022 commit + push from inside the container (docker compose exec radar sh), then re-run, OR");
|
|
361
|
+
console.log(" \u2022 re-run with --force to discard the work and tear down anyway.");
|
|
362
|
+
process.exit(2);
|
|
363
|
+
}
|
|
364
|
+
console.log("[teardown] \u2713 workspace is clean (only recall-ignored noise present) \u2014 safe to tear down");
|
|
365
|
+
}
|
|
366
|
+
function parseEnvFile(path) {
|
|
367
|
+
const env = {};
|
|
368
|
+
for (const raw of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
|
|
369
|
+
const line = raw.trim();
|
|
370
|
+
if (!line || line.startsWith("#")) continue;
|
|
371
|
+
const eq = line.indexOf("=");
|
|
372
|
+
if (eq < 0) continue;
|
|
373
|
+
const key = line.slice(0, eq).trim();
|
|
374
|
+
let value = line.slice(eq + 1).trim();
|
|
375
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
376
|
+
value = value.slice(1, -1);
|
|
377
|
+
}
|
|
378
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) env[key] = value;
|
|
379
|
+
}
|
|
380
|
+
return env;
|
|
381
|
+
}
|
|
382
|
+
async function releaseWebhook() {
|
|
383
|
+
const psQ = sh("docker", [...COMPOSE_BASE, "ps", "-q", "radar"]);
|
|
384
|
+
if (psQ.status !== 0 || !psQ.stdout.trim()) {
|
|
385
|
+
console.log("[teardown] container not running \u2014 skipping webhook release");
|
|
386
|
+
console.log("[teardown] (any prior registration is now orphaned \u2014 clean up via cloud LS settings UI)");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const radarJson = workspaceSh("cat /workspace/.launchpod/radar.json 2>/dev/null");
|
|
390
|
+
let endpointId = "";
|
|
391
|
+
try {
|
|
392
|
+
const j = JSON.parse(radarJson);
|
|
393
|
+
endpointId = j.endpointId ?? j.registration?.endpointId ?? j.webhook?.endpointId ?? "";
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
if (!endpointId) {
|
|
397
|
+
console.log("[teardown] no endpoint_id found in /workspace/.launchpod/radar.json \u2014 skipping release");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!(0, import_node_fs.existsSync)(ENV_PATH)) {
|
|
401
|
+
console.log(`[teardown] no .env found at ${ENV_PATH} \u2014 can't release webhook (manual cleanup needed)`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const env = parseEnvFile(ENV_PATH);
|
|
405
|
+
const pat = env.LS_PAT;
|
|
406
|
+
const orgSlug = env.LS_ORG_SLUG;
|
|
407
|
+
const projectSlug = env.LS_PROJECT_SLUG;
|
|
408
|
+
const serverUrl = env.LS_SERVER_URL || "https://launchsecure-v2.vercel.app";
|
|
409
|
+
if (!pat || !orgSlug || !projectSlug) {
|
|
410
|
+
console.log("[teardown] .env is missing LS_PAT / LS_ORG_SLUG / LS_PROJECT_SLUG \u2014 skipping release");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
console.log(`[teardown] releasing endpoint ${endpointId} from ${serverUrl}`);
|
|
414
|
+
try {
|
|
415
|
+
const mcp = new ProjectMcpClient({ serverUrl, pat, orgSlug, projectSlug });
|
|
416
|
+
await mcp.call("launchpod_webhook_release", { endpoint_id: endpointId });
|
|
417
|
+
console.log("[teardown] \u2713 webhook released");
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
420
|
+
console.log(`[teardown] \u26A0 webhook release failed (${msg}) \u2014 delete manually at:`);
|
|
421
|
+
console.log(` ${serverUrl}/${orgSlug}/projects/${projectSlug}/settings`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function dockerComposeDown() {
|
|
425
|
+
console.log(`[teardown] docker compose -f ${COMPOSE_FILE} down -v`);
|
|
426
|
+
const down = (0, import_node_child_process.spawnSync)("docker", [...COMPOSE_BASE, "down", "-v"], { encoding: "utf8" });
|
|
427
|
+
const combined = (down.stdout || "") + (down.stderr || "");
|
|
428
|
+
for (const line of combined.split("\n")) {
|
|
429
|
+
if (/Removing|Removed|Stopping|Stopped/.test(line)) console.log(" " + line.trim());
|
|
430
|
+
}
|
|
431
|
+
if (down.status !== 0) {
|
|
432
|
+
console.error(`[teardown] docker compose down -v failed (status ${down.status})`);
|
|
433
|
+
process.exit(down.status ?? 1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function cleanupLocalArtifacts(args) {
|
|
437
|
+
if (args.removeImage) {
|
|
438
|
+
console.log("[teardown] removing image launchpod-radar:local");
|
|
439
|
+
const rmi = sh("docker", ["rmi", "launchpod-radar:local"]);
|
|
440
|
+
console.log(rmi.status === 0 ? " removed" : " not present");
|
|
441
|
+
}
|
|
442
|
+
for (const f of (0, import_node_fs.readdirSync)(RADAR_DIR)) {
|
|
443
|
+
if (f.startsWith("launchsecure-launch-kit-") && f.endsWith(".tgz")) {
|
|
444
|
+
console.log("[teardown] removing launch-kit tarball(s)");
|
|
445
|
+
try {
|
|
446
|
+
(0, import_node_fs.unlinkSync)((0, import_node_path.join)(RADAR_DIR, f));
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (args.removeEnv && (0, import_node_fs.existsSync)(ENV_PATH)) {
|
|
452
|
+
console.log("[teardown] removing .env (--remove-env)");
|
|
453
|
+
try {
|
|
454
|
+
(0, import_node_fs.unlinkSync)(ENV_PATH);
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
} else if ((0, import_node_fs.existsSync)(ENV_PATH)) {
|
|
458
|
+
console.log("[teardown] keeping .env (use --remove-env to delete it next time)");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function main() {
|
|
462
|
+
const args = parseArgs(process.argv.slice(2));
|
|
463
|
+
if (args.help) {
|
|
464
|
+
printHelp();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
ensureRadarCompose();
|
|
468
|
+
preflightWorkspace(args);
|
|
469
|
+
await releaseWebhook();
|
|
470
|
+
dockerComposeDown();
|
|
471
|
+
cleanupLocalArtifacts(args);
|
|
472
|
+
console.log("[teardown] done.");
|
|
473
|
+
}
|
|
474
|
+
main().catch((err) => {
|
|
475
|
+
console.error("[teardown] fatal:", err);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchsecure/launch-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"description": "LaunchSecure toolkit — launch-pod (pipeline), launch-chart (project graph MCP), launch-deck (visual playground MCP), launch-kit-beacon (feedback Web Component), launch-recall (file-watcher backup).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "LaunchSecure - AutomateWithUs",
|
|
@@ -56,24 +56,6 @@
|
|
|
56
56
|
"launch-course": "./dist/server/course-entry.js",
|
|
57
57
|
"launch-beacon": "./dist/server/beacon-monitor-entry.js"
|
|
58
58
|
},
|
|
59
|
-
"scripts": {
|
|
60
|
-
"build": "pnpm build:client && pnpm build:chart-client && pnpm build:deck-client && pnpm build:council-client && pnpm build:beacon && pnpm build:server",
|
|
61
|
-
"build:beacon": "vite build --config vite.beacon.config.ts && tsc -p tsconfig.beacon.json --emitDeclarationOnly --outDir dist/beacon/types",
|
|
62
|
-
"test:beacon": "vitest run --config vite.beacon.config.ts",
|
|
63
|
-
"test:radar": "vitest run --config vite.radar.config.ts",
|
|
64
|
-
"test:chart": "vitest run --config vite.chart.test.config.ts",
|
|
65
|
-
"build:deck-client": "vite build --config vite.deck.config.ts",
|
|
66
|
-
"build:council-client": "vite build --config vite.council.config.ts",
|
|
67
|
-
"build:client": "vite build",
|
|
68
|
-
"build:chart-client": "vite build --config vite.chart.config.ts",
|
|
69
|
-
"build:server": "esbuild src/server/cli.ts src/server/fb-wizard.ts src/server/graph-mcp-entry.ts src/server/chart-serve.ts src/server/deck-mcp-entry.ts src/server/deck-serve.ts src/server/council-entry.ts src/server/council-serve.ts src/server/recall-entry.ts src/server/init-entry.ts src/server/orbit-entry.ts src/server/course-entry.ts src/server/beacon-monitor-entry.ts src/server/parse-worker-entry.ts --bundle --platform=node --target=node18 --outdir=dist/server --external:node-pty --external:ws --external:typescript --external:web-tree-sitter --external:tree-sitter-typescript --external:cloudflared --external:pg --external:pg-native --external:pgsql-parser --external:libpg-query && rm -rf dist/server/public && cp -r ../claude-code-web/src/public dist/server/public && rm -rf dist/server/graph/queries && mkdir -p dist/server/graph && cp -r src/server/graph/queries dist/server/graph/queries",
|
|
70
|
-
"dev:client": "vite",
|
|
71
|
-
"dev:deck-serve": "cd ../.. && tsx watch packages/cli/src/server/deck-mcp-entry.ts serve",
|
|
72
|
-
"dev:chart": "pnpm build:server && pnpm build:chart-client && node dist/server/graph-mcp-entry.js serve",
|
|
73
|
-
"dev:server": "pnpm build:server && node dist/server/cli.js",
|
|
74
|
-
"dev": "pnpm build:server && concurrently -k -n client,server -c cyan,magenta \"vite\" \"node dist/server/cli.js\"",
|
|
75
|
-
"prepublishOnly": "pnpm build"
|
|
76
|
-
},
|
|
77
59
|
"files": [
|
|
78
60
|
"dist",
|
|
79
61
|
"prompts",
|
|
@@ -94,8 +76,6 @@
|
|
|
94
76
|
"ws": "^8.18.0"
|
|
95
77
|
},
|
|
96
78
|
"devDependencies": {
|
|
97
|
-
"@launchsecure/claude-code-web": "workspace:*",
|
|
98
|
-
"@launchsecure/ui": "workspace:*",
|
|
99
79
|
"@types/node": "^20.0.0",
|
|
100
80
|
"@types/pg": "^8.11.10",
|
|
101
81
|
"@types/react": "^18.3.12",
|
|
@@ -119,6 +99,25 @@
|
|
|
119
99
|
"react-router-dom": "^6.28.0",
|
|
120
100
|
"tailwindcss": "^3.4.19",
|
|
121
101
|
"vite": "^5.4.11",
|
|
122
|
-
"vitest": "^1.6.0"
|
|
102
|
+
"vitest": "^1.6.0",
|
|
103
|
+
"@launchsecure/claude-code-web": "0.0.1",
|
|
104
|
+
"@launchsecure/ui": "0.0.1"
|
|
105
|
+
},
|
|
106
|
+
"scripts": {
|
|
107
|
+
"build": "pnpm build:client && pnpm build:chart-client && pnpm build:deck-client && pnpm build:council-client && pnpm build:beacon && pnpm build:server",
|
|
108
|
+
"build:beacon": "vite build --config vite.beacon.config.ts && tsc -p tsconfig.beacon.json --emitDeclarationOnly --outDir dist/beacon/types",
|
|
109
|
+
"test:beacon": "vitest run --config vite.beacon.config.ts",
|
|
110
|
+
"test:radar": "vitest run --config vite.radar.config.ts",
|
|
111
|
+
"test:chart": "vitest run --config vite.chart.test.config.ts",
|
|
112
|
+
"build:deck-client": "vite build --config vite.deck.config.ts",
|
|
113
|
+
"build:council-client": "vite build --config vite.council.config.ts",
|
|
114
|
+
"build:client": "vite build",
|
|
115
|
+
"build:chart-client": "vite build --config vite.chart.config.ts",
|
|
116
|
+
"build:server": "esbuild src/server/cli.ts src/server/fb-wizard.ts src/server/graph-mcp-entry.ts src/server/chart-serve.ts src/server/deck-mcp-entry.ts src/server/deck-serve.ts src/server/council-entry.ts src/server/council-serve.ts src/server/recall-entry.ts src/server/init-entry.ts src/server/orbit-entry.ts src/server/course-entry.ts src/server/beacon-monitor-entry.ts src/server/parse-worker-entry.ts src/server/radar-teardown-entry.ts src/server/radar-docker-init-entry.ts --bundle --platform=node --target=node18 --outdir=dist/server --external:node-pty --external:ws --external:typescript --external:web-tree-sitter --external:tree-sitter-typescript --external:cloudflared --external:pg --external:pg-native --external:pgsql-parser --external:libpg-query && rm -rf dist/server/public && cp -r ../claude-code-web/src/public dist/server/public && rm -rf dist/server/graph/queries && mkdir -p dist/server/graph && cp -r src/server/graph/queries dist/server/graph/queries",
|
|
117
|
+
"dev:client": "vite",
|
|
118
|
+
"dev:deck-serve": "cd ../.. && tsx watch packages/cli/src/server/deck-mcp-entry.ts serve",
|
|
119
|
+
"dev:chart": "pnpm build:server && pnpm build:chart-client && node dist/server/graph-mcp-entry.js serve",
|
|
120
|
+
"dev:server": "pnpm build:server && node dist/server/cli.js",
|
|
121
|
+
"dev": "pnpm build:server && concurrently -k -n client,server -c cyan,magenta \"vite\" \"node dist/server/cli.js\""
|
|
123
122
|
}
|
|
124
|
-
}
|
|
123
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Wire launch-kit's MCP
|
|
2
|
+
description: Wire launch-kit's MCP chips into the user's existing Claude Code statusline. Additive — does not create or replace an existing statusline, only appends colored chips (recall/chart/deck/council/secure) showing each daemon's liveness + last-activity age, plus a reachability probe for the hosted launch-secure MCP. Idempotent. Run /kit:deactivate-statusline to undo.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# /kit:activate-statusline
|
|
@@ -21,12 +21,12 @@ Extends the user's existing `~/.claude/settings.json` statusline so MCP daemon h
|
|
|
21
21
|
Shell out to the kit binary:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
launch-kit statusline activate
|
|
25
|
-
launch-kit statusline activate --show=recall,chart
|
|
26
|
-
launch-kit statusline activate --show=recall,chart,deck,council
|
|
24
|
+
launch-kit statusline activate # all chips
|
|
25
|
+
launch-kit statusline activate --show=recall,chart # only these
|
|
26
|
+
launch-kit statusline activate --show=recall,chart,deck,council,secure # explicit all
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
The `--show` flag accepts any comma-separated subset of `recall,chart,deck,council`. Omitting it shows all
|
|
29
|
+
The `--show` flag accepts any comma-separated subset of `recall,chart,deck,council,secure`. Omitting it shows all five. `secure` is a reachability probe (curl, cached ~30s) against the active LaunchSecure profile's `serverUrl` — green when the host responds, red on transport failure, orange when the cred config or curl is missing. The chip label shows the active profile (e.g. `secure(prod)`). Re-running activate with a different `--show` updates the chip set in place — no need to deactivate first.
|
|
30
30
|
|
|
31
31
|
If the published bin isn't on PATH (dev repo, monorepo), fall back to:
|
|
32
32
|
|