brainstorm-companion 1.0.0
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/README.md +66 -0
- package/bin/brainstorm.js +2 -0
- package/package.json +39 -0
- package/skill/SKILL.md +71 -0
- package/skill/visual-companion.md +331 -0
- package/src/cli.js +441 -0
- package/src/content-detect.js +52 -0
- package/src/mcp.js +331 -0
- package/src/server.js +624 -0
- package/src/session.js +215 -0
- package/src/templates/comparison-helper.js +277 -0
- package/src/templates/comparison.html +277 -0
- package/src/templates/frame.html +283 -0
- package/src/templates/helper.js +78 -0
- package/src/templates/waiting.html +8 -0
- package/src/ws-protocol.js +69 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseArgs } = require('node:util');
|
|
4
|
+
const { exec, spawn } = require('node:child_process');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { SessionManager } = require('./session');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Usage
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function printUsage() {
|
|
15
|
+
console.log(`Usage: brainstorm-companion <command> [options]
|
|
16
|
+
|
|
17
|
+
Commands:
|
|
18
|
+
start Start the brainstorm server
|
|
19
|
+
push Push HTML content to the browser
|
|
20
|
+
events Read user interaction events
|
|
21
|
+
clear Clear content or events
|
|
22
|
+
stop Stop the server
|
|
23
|
+
status Show server status
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--mcp Run as MCP server
|
|
27
|
+
--help Show this help`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function pollForServerInfo(sessionDir, timeoutMs = 5000, intervalMs = 100) {
|
|
39
|
+
const serverInfoPath = path.join(sessionDir, '.server-info');
|
|
40
|
+
const deadline = Date.now() + timeoutMs;
|
|
41
|
+
while (Date.now() < deadline) {
|
|
42
|
+
if (fs.existsSync(serverInfoPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(serverInfoPath, 'utf8');
|
|
45
|
+
const info = JSON.parse(raw);
|
|
46
|
+
if (info.url) return info;
|
|
47
|
+
} catch {
|
|
48
|
+
// file may be partially written, retry
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await sleep(intervalMs);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function openBrowser(url) {
|
|
57
|
+
const platform = process.platform;
|
|
58
|
+
let cmd;
|
|
59
|
+
if (platform === 'darwin') {
|
|
60
|
+
cmd = `open "${url}"`;
|
|
61
|
+
} else if (platform === 'linux') {
|
|
62
|
+
cmd = `xdg-open "${url}"`;
|
|
63
|
+
} else {
|
|
64
|
+
cmd = `start "${url}"`;
|
|
65
|
+
}
|
|
66
|
+
exec(cmd, (err) => {
|
|
67
|
+
if (err) {
|
|
68
|
+
console.error(`Warning: could not open browser: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getActiveOrExit(projectDir) {
|
|
74
|
+
const session = new SessionManager(projectDir);
|
|
75
|
+
const active = session.getActive();
|
|
76
|
+
if (!active) {
|
|
77
|
+
console.error('No active session found.');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
return { session, active };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Command: start
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
async function start(argv) {
|
|
88
|
+
const { values } = parseArgs({
|
|
89
|
+
args: argv,
|
|
90
|
+
options: {
|
|
91
|
+
'project-dir': { type: 'string' },
|
|
92
|
+
'port': { type: 'string', default: '0' },
|
|
93
|
+
'host': { type: 'string', default: '127.0.0.1' },
|
|
94
|
+
'foreground': { type: 'boolean', default: false },
|
|
95
|
+
'no-open': { type: 'boolean', default: false },
|
|
96
|
+
},
|
|
97
|
+
strict: false,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const projectDir = values['project-dir'] || null;
|
|
101
|
+
const host = values['host'];
|
|
102
|
+
const port = parseInt(values['port'], 10) || 0;
|
|
103
|
+
const foreground = values['foreground'];
|
|
104
|
+
const noOpen = values['no-open'];
|
|
105
|
+
|
|
106
|
+
const session = new SessionManager(projectDir);
|
|
107
|
+
const { sessionDir } = session.create();
|
|
108
|
+
|
|
109
|
+
if (foreground) {
|
|
110
|
+
// Run server in-process
|
|
111
|
+
const { startServer } = require('./server');
|
|
112
|
+
const instance = startServer({
|
|
113
|
+
screenDir: sessionDir,
|
|
114
|
+
host,
|
|
115
|
+
port,
|
|
116
|
+
ownerPid: process.pid,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
instance.server.once('listening', () => {
|
|
120
|
+
console.log(`Server started: ${instance.url}`);
|
|
121
|
+
if (!noOpen) {
|
|
122
|
+
openBrowser(instance.url);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
instance.server.once('error', (err) => {
|
|
127
|
+
console.error(`Server error: ${err.message}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Keep process alive
|
|
132
|
+
process.on('SIGINT', () => {
|
|
133
|
+
instance.shutdown('sigint');
|
|
134
|
+
process.exit(0);
|
|
135
|
+
});
|
|
136
|
+
process.on('SIGTERM', () => {
|
|
137
|
+
instance.shutdown('sigterm');
|
|
138
|
+
process.exit(0);
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
// Background the server
|
|
142
|
+
const serverScript = path.join(__dirname, 'server.js');
|
|
143
|
+
const child = spawn(process.execPath, [serverScript], {
|
|
144
|
+
detached: true,
|
|
145
|
+
stdio: 'ignore',
|
|
146
|
+
env: {
|
|
147
|
+
...process.env,
|
|
148
|
+
BRAINSTORM_DIR: sessionDir,
|
|
149
|
+
BRAINSTORM_HOST: host,
|
|
150
|
+
BRAINSTORM_PORT: String(port),
|
|
151
|
+
BRAINSTORM_OWNER_PID: String(process.pid),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
child.unref();
|
|
155
|
+
|
|
156
|
+
// Poll for .server-info
|
|
157
|
+
const serverInfo = await pollForServerInfo(sessionDir, 5000, 100);
|
|
158
|
+
if (!serverInfo) {
|
|
159
|
+
console.error('Timed out waiting for server to start.');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`Server started: ${serverInfo.url}`);
|
|
164
|
+
|
|
165
|
+
if (!noOpen) {
|
|
166
|
+
openBrowser(serverInfo.url);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Command: push
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
async function push(argv) {
|
|
176
|
+
const { values, positionals } = parseArgs({
|
|
177
|
+
args: argv,
|
|
178
|
+
options: {
|
|
179
|
+
'project-dir': { type: 'string' },
|
|
180
|
+
'slot': { type: 'string' },
|
|
181
|
+
'label': { type: 'string' },
|
|
182
|
+
'html': { type: 'string' },
|
|
183
|
+
},
|
|
184
|
+
allowPositionals: true,
|
|
185
|
+
strict: false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const projectDir = values['project-dir'] || null;
|
|
189
|
+
const slot = values['slot'];
|
|
190
|
+
const label = values['label'];
|
|
191
|
+
let html = values['html'];
|
|
192
|
+
|
|
193
|
+
if (!html) {
|
|
194
|
+
const fileArg = positionals[0];
|
|
195
|
+
if (!fileArg) {
|
|
196
|
+
console.error('Usage: brainstorm-companion push <file|-> [--slot a|b|c] [--label "name"] [--html "<content>"]');
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
if (fileArg === '-') {
|
|
200
|
+
// Read from stdin
|
|
201
|
+
html = fs.readFileSync('/dev/stdin', 'utf8');
|
|
202
|
+
} else {
|
|
203
|
+
if (!fs.existsSync(fileArg)) {
|
|
204
|
+
console.error(`File not found: ${fileArg}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
html = fs.readFileSync(fileArg, 'utf8');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const session = new SessionManager(projectDir);
|
|
212
|
+
const active = session.getActive();
|
|
213
|
+
if (!active) {
|
|
214
|
+
console.error('No active session found.');
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const result = session.pushScreen(html, { slot, label });
|
|
219
|
+
console.log(`Content pushed to ${result.path}`);
|
|
220
|
+
if (slot) {
|
|
221
|
+
console.log(`Slot: ${slot}${label ? ` (${label})` : ''}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Command: events
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
function events(argv) {
|
|
230
|
+
const { values } = parseArgs({
|
|
231
|
+
args: argv,
|
|
232
|
+
options: {
|
|
233
|
+
'project-dir': { type: 'string' },
|
|
234
|
+
'format': { type: 'string', default: 'json' },
|
|
235
|
+
'clear': { type: 'boolean', default: false },
|
|
236
|
+
},
|
|
237
|
+
strict: false,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const projectDir = values['project-dir'] || null;
|
|
241
|
+
const format = values['format'];
|
|
242
|
+
const doClear = values['clear'];
|
|
243
|
+
|
|
244
|
+
const session = new SessionManager(projectDir);
|
|
245
|
+
const eventList = session.readEvents();
|
|
246
|
+
|
|
247
|
+
if (format === 'text') {
|
|
248
|
+
if (eventList.length === 0) {
|
|
249
|
+
console.log('No events.');
|
|
250
|
+
} else {
|
|
251
|
+
for (const ev of eventList) {
|
|
252
|
+
const ts = ev.timestamp ? new Date(ev.timestamp).toISOString() : '';
|
|
253
|
+
console.log(`[${ts}] ${ev.type || 'event'}: ${ev.choice !== undefined ? ev.choice : JSON.stringify(ev)}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
// Default: json
|
|
258
|
+
console.log(JSON.stringify(eventList, null, 2));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (doClear) {
|
|
262
|
+
session.clearEvents();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Command: clear
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
function clear(argv) {
|
|
271
|
+
const { values } = parseArgs({
|
|
272
|
+
args: argv,
|
|
273
|
+
options: {
|
|
274
|
+
'project-dir': { type: 'string' },
|
|
275
|
+
'slot': { type: 'string' },
|
|
276
|
+
'all': { type: 'boolean', default: false },
|
|
277
|
+
'events': { type: 'boolean', default: false },
|
|
278
|
+
},
|
|
279
|
+
strict: false,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const projectDir = values['project-dir'] || null;
|
|
283
|
+
const slot = values['slot'];
|
|
284
|
+
const all = values['all'];
|
|
285
|
+
const eventsOnly = values['events'];
|
|
286
|
+
|
|
287
|
+
const session = new SessionManager(projectDir);
|
|
288
|
+
const active = session.getActive();
|
|
289
|
+
if (!active) {
|
|
290
|
+
console.error('No active session found.');
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (eventsOnly) {
|
|
295
|
+
session.clearEvents();
|
|
296
|
+
console.log('Events cleared.');
|
|
297
|
+
} else if (slot) {
|
|
298
|
+
session.clearSlot(slot);
|
|
299
|
+
console.log(`Slot ${slot} cleared.`);
|
|
300
|
+
} else if (all) {
|
|
301
|
+
session.clearAll();
|
|
302
|
+
console.log('All content cleared.');
|
|
303
|
+
} else {
|
|
304
|
+
console.error('Specify --slot <a|b|c>, --all, or --events');
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Command: stop
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
function stop(argv) {
|
|
314
|
+
const { values } = parseArgs({
|
|
315
|
+
args: argv,
|
|
316
|
+
options: {
|
|
317
|
+
'project-dir': { type: 'string' },
|
|
318
|
+
},
|
|
319
|
+
strict: false,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const projectDir = values['project-dir'] || null;
|
|
323
|
+
const session = new SessionManager(projectDir);
|
|
324
|
+
const active = session.getActive();
|
|
325
|
+
if (!active) {
|
|
326
|
+
console.error('No active session found.');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const { serverInfo } = active;
|
|
331
|
+
const pid = serverInfo.pid || serverInfo.serverPid;
|
|
332
|
+
if (!pid) {
|
|
333
|
+
console.error('No PID found in server info.');
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
process.kill(pid, 'SIGTERM');
|
|
339
|
+
console.log(`Server (PID ${pid}) stopped.`);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
console.error(`Failed to stop server: ${err.message}`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Command: status
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
function status(argv) {
|
|
351
|
+
const { values } = parseArgs({
|
|
352
|
+
args: argv,
|
|
353
|
+
options: {
|
|
354
|
+
'project-dir': { type: 'string' },
|
|
355
|
+
},
|
|
356
|
+
strict: false,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const projectDir = values['project-dir'] || null;
|
|
360
|
+
const session = new SessionManager(projectDir);
|
|
361
|
+
const statusInfo = session.getStatus();
|
|
362
|
+
|
|
363
|
+
if (!statusInfo) {
|
|
364
|
+
console.error('No active session found.');
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const { sessionId, sessionDir, slots, eventCount, uptime, url } = statusInfo;
|
|
369
|
+
|
|
370
|
+
console.log(`Session ID : ${sessionId}`);
|
|
371
|
+
console.log(`Session Dir: ${sessionDir}`);
|
|
372
|
+
console.log(`URL : ${url || '(unknown)'}`);
|
|
373
|
+
|
|
374
|
+
if (uptime !== null) {
|
|
375
|
+
const seconds = Math.floor(uptime / 1000);
|
|
376
|
+
const minutes = Math.floor(seconds / 60);
|
|
377
|
+
const hours = Math.floor(minutes / 60);
|
|
378
|
+
let uptimeStr;
|
|
379
|
+
if (hours > 0) {
|
|
380
|
+
uptimeStr = `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
381
|
+
} else if (minutes > 0) {
|
|
382
|
+
uptimeStr = `${minutes}m ${seconds % 60}s`;
|
|
383
|
+
} else {
|
|
384
|
+
uptimeStr = `${seconds}s`;
|
|
385
|
+
}
|
|
386
|
+
console.log(`Uptime : ${uptimeStr}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
console.log(`Events : ${eventCount}`);
|
|
390
|
+
|
|
391
|
+
if (slots.length > 0) {
|
|
392
|
+
console.log('Slots:');
|
|
393
|
+
for (const s of slots) {
|
|
394
|
+
const label = s.label ? ` (${s.label})` : '';
|
|
395
|
+
const content = s.hasContent ? 'has content' : 'empty';
|
|
396
|
+
console.log(` [${s.slot}]${label}: ${content}`);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
console.log('Slots : none');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Command dispatch
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
const COMMANDS = { start, push, events, clear, stop, status };
|
|
408
|
+
|
|
409
|
+
function main() {
|
|
410
|
+
const args = process.argv.slice(2);
|
|
411
|
+
const command = args[0];
|
|
412
|
+
|
|
413
|
+
if (!command || command === '--help' || command === '-h') {
|
|
414
|
+
printUsage();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const handler = COMMANDS[command];
|
|
419
|
+
if (!handler) {
|
|
420
|
+
console.error(`Unknown command: ${command}`);
|
|
421
|
+
printUsage();
|
|
422
|
+
process.exitCode = 1;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Wrap async handlers
|
|
427
|
+
const result = handler(args.slice(1));
|
|
428
|
+
if (result && typeof result.then === 'function') {
|
|
429
|
+
result.catch((err) => {
|
|
430
|
+
console.error(`Error: ${err.message}`);
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!process.argv.includes('--mcp')) {
|
|
437
|
+
main();
|
|
438
|
+
} else {
|
|
439
|
+
const { McpServer } = require('./mcp');
|
|
440
|
+
new McpServer().start();
|
|
441
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// detectLibraries — scan HTML for library usage signals
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
function detectLibraries(html) {
|
|
8
|
+
const mermaid = /class=["']mermaid["']/.test(html);
|
|
9
|
+
const prism = /class=["']language-/.test(html);
|
|
10
|
+
const katex = /\$\$/.test(html) || /class=["']math["']/.test(html);
|
|
11
|
+
return { mermaid, prism, katex };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// buildInjections — produce <script>/<link> tags for detected libraries
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function buildInjections(needs, cdnBase) {
|
|
19
|
+
if (!cdnBase) {
|
|
20
|
+
cdnBase = process.env.BRAINSTORM_CDN_BASE || 'https://cdn.jsdelivr.net';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parts = [];
|
|
24
|
+
|
|
25
|
+
if (needs.mermaid) {
|
|
26
|
+
parts.push(
|
|
27
|
+
`<script src="${cdnBase}/npm/mermaid/dist/mermaid.min.js"></script>`,
|
|
28
|
+
`<script>mermaid.initialize({startOnLoad:true,theme:window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'default'});</script>`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (needs.prism) {
|
|
33
|
+
parts.push(
|
|
34
|
+
`<link rel="stylesheet" href="${cdnBase}/npm/prismjs/themes/prism-tomorrow.min.css">`,
|
|
35
|
+
`<script src="${cdnBase}/npm/prismjs/prism.min.js"></script>`,
|
|
36
|
+
`<script src="${cdnBase}/npm/prismjs/plugins/autoloader/prism-autoloader.min.js"></script>`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (needs.katex) {
|
|
41
|
+
parts.push(
|
|
42
|
+
`<link rel="stylesheet" href="${cdnBase}/npm/katex/dist/katex.min.css">`,
|
|
43
|
+
`<script src="${cdnBase}/npm/katex/dist/katex.min.js"></script>`,
|
|
44
|
+
`<script src="${cdnBase}/npm/katex/dist/contrib/auto-render.min.js"></script>`,
|
|
45
|
+
`<script>document.addEventListener('DOMContentLoaded',function(){renderMathInElement(document.body)});</script>`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parts.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { detectLibraries, buildInjections };
|