@launchsecure/launch-kit 0.0.1

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.
Files changed (64) hide show
  1. package/README.md +37 -0
  2. package/dist/client/assets/index-C8GAsRGO.css +32 -0
  3. package/dist/client/assets/index-CcHIoRl6.js +286 -0
  4. package/dist/client/index.html +22 -0
  5. package/dist/server/cli.js +8853 -0
  6. package/dist/server/fb-wizard.js +136 -0
  7. package/dist/server/graph-mcp-entry.js +1542 -0
  8. package/dist/server/public/app.js +1312 -0
  9. package/dist/server/public/icons.js +36 -0
  10. package/dist/server/public/index.html +159 -0
  11. package/dist/server/public/plan-detector.js +186 -0
  12. package/dist/server/public/session-manager.js +1129 -0
  13. package/dist/server/public/splits.js +569 -0
  14. package/dist/server/public/style.css +1620 -0
  15. package/package.json +73 -0
  16. package/prompts/analysis.md +992 -0
  17. package/prompts/architect-reconcile.md +931 -0
  18. package/prompts/architecture-sync.md +902 -0
  19. package/prompts/be-contract.md +709 -0
  20. package/prompts/be-impl.md +565 -0
  21. package/prompts/be-policy.md +551 -0
  22. package/prompts/be-test.md +591 -0
  23. package/prompts/bug-diagnosis.md +653 -0
  24. package/prompts/bug-intake.md +563 -0
  25. package/prompts/change-request-intake.md +593 -0
  26. package/prompts/db-contract.md +644 -0
  27. package/prompts/db-impl.md +522 -0
  28. package/prompts/db-interaction.md +569 -0
  29. package/prompts/db-test.md +630 -0
  30. package/prompts/decision-pack.md +654 -0
  31. package/prompts/fe-contract.md +992 -0
  32. package/prompts/fe-flow.md +537 -0
  33. package/prompts/fe-impl.md +597 -0
  34. package/prompts/fe-reconcile.md +506 -0
  35. package/prompts/fe-review.md +550 -0
  36. package/prompts/fe-test.md +705 -0
  37. package/prompts/fix-planner.md +1219 -0
  38. package/prompts/global-db-patterns.md +588 -0
  39. package/prompts/global-env-config.md +460 -0
  40. package/prompts/global-integrations.md +504 -0
  41. package/prompts/global-middleware.md +442 -0
  42. package/prompts/global-navigation.md +502 -0
  43. package/prompts/global-security.md +603 -0
  44. package/prompts/global-services.md +427 -0
  45. package/prompts/greenfield-classifier.md +590 -0
  46. package/prompts/llm-council.md +597 -0
  47. package/prompts/module-sequencer.md +529 -0
  48. package/prompts/normalize.md +611 -0
  49. package/prompts/optimization.md +633 -0
  50. package/prompts/prd-generation.md +544 -0
  51. package/prompts/prd-reconcile.md +584 -0
  52. package/prompts/prd-review.md +504 -0
  53. package/prompts/pre-code-analysis.md +565 -0
  54. package/prompts/pre-code-global-analysis.md +169 -0
  55. package/prompts/production-bootstrap.md +577 -0
  56. package/prompts/research.md +702 -0
  57. package/prompts/retrofit-analysis.md +845 -0
  58. package/prompts/spike.md +850 -0
  59. package/prompts/theming.md +835 -0
  60. package/prompts/triage.md +599 -0
  61. package/prompts/unified-reconcile.md +628 -0
  62. package/prompts/unified-review.md +592 -0
  63. package/prompts/user-stories.md +486 -0
  64. package/prompts/wireframe.md +576 -0
@@ -0,0 +1,569 @@
1
+ /**
2
+ * SplitContainer - Simple VS Code-style split view
3
+ * Manages up to 2 terminal panes side-by-side with independent terminals
4
+ */
5
+
6
+ class Split {
7
+ constructor(container, index, app) {
8
+ this.container = container;
9
+ this.index = index;
10
+ this.app = app;
11
+ this.sessionId = null;
12
+ this.isActive = false;
13
+
14
+ // Create independent terminal instance for this split
15
+ this.terminal = null;
16
+ this.fitAddon = null;
17
+ this.webLinksAddon = null;
18
+ this.socket = null;
19
+
20
+ this.createTerminal();
21
+ }
22
+
23
+ createTerminal() {
24
+ // Create terminal wrapper
25
+ const wrapper = document.createElement('div');
26
+ wrapper.className = 'split-terminal-wrapper';
27
+
28
+ const terminalDiv = document.createElement('div');
29
+ terminalDiv.id = `split-terminal-${this.index}`;
30
+ wrapper.appendChild(terminalDiv);
31
+
32
+ this.container.appendChild(wrapper);
33
+
34
+ // Initialize xterm.js terminal
35
+ this.terminal = new Terminal({
36
+ fontFamily: this.app?.terminal?.options?.fontFamily || 'JetBrains Mono, monospace',
37
+ fontSize: this.app?.terminal?.options?.fontSize || 14,
38
+ cursorBlink: true,
39
+ convertEol: true,
40
+ allowProposedApi: true,
41
+ theme: this.app?.terminal?.options?.theme || {
42
+ background: '#0d1117',
43
+ foreground: '#c9d1d9',
44
+ cursor: '#58a6ff'
45
+ }
46
+ });
47
+
48
+ this.fitAddon = new FitAddon.FitAddon();
49
+ this.webLinksAddon = new WebLinksAddon.WebLinksAddon();
50
+
51
+ this.terminal.loadAddon(this.fitAddon);
52
+ this.terminal.loadAddon(this.webLinksAddon);
53
+ this.terminal.open(terminalDiv);
54
+
55
+ // Setup terminal input handler
56
+ this.terminal.onData((data) => {
57
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
58
+ this.socket.send(JSON.stringify({ type: 'input', data }));
59
+ }
60
+ });
61
+
62
+ // Setup resize handler
63
+ this.terminal.onResize(({ cols, rows }) => {
64
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
65
+ this.socket.send(JSON.stringify({ type: 'resize', cols, rows }));
66
+ }
67
+ });
68
+
69
+ this.fit();
70
+ }
71
+
72
+ async setSession(sessionId) {
73
+ if (this.sessionId === sessionId) return;
74
+
75
+ // Disconnect from old session
76
+ if (this.socket) {
77
+ this.disconnect();
78
+ }
79
+
80
+ this.sessionId = sessionId;
81
+
82
+ // Connect to new session
83
+ if (sessionId) {
84
+ await this.connect(sessionId);
85
+ }
86
+
87
+ // Update active state
88
+ this.updateActiveState();
89
+ }
90
+
91
+ async connect(sessionId) {
92
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
93
+ const wsUrl = `${protocol}//${location.host}/terminal/ws?sessionId=${encodeURIComponent(sessionId)}`;
94
+
95
+ this.socket = new WebSocket(wsUrl);
96
+
97
+ this.socket.onopen = () => {
98
+ console.log(`[Split ${this.index}] Connected to session ${sessionId}`);
99
+ // Send initial resize
100
+ const { cols, rows } = this.terminal;
101
+ this.socket.send(JSON.stringify({ type: 'resize', cols, rows }));
102
+ };
103
+
104
+ this.socket.onmessage = (event) => {
105
+ try {
106
+ const msg = JSON.parse(event.data);
107
+ this.handleMessage(msg);
108
+ } catch (error) {
109
+ console.error(`[Split ${this.index}] Error handling message:`, error);
110
+ }
111
+ };
112
+
113
+ this.socket.onclose = () => {
114
+ console.log(`[Split ${this.index}] Disconnected from session ${sessionId}`);
115
+ };
116
+
117
+ this.socket.onerror = (error) => {
118
+ console.error(`[Split ${this.index}] WebSocket error:`, error);
119
+ };
120
+ }
121
+
122
+ handleMessage(msg) {
123
+ switch (msg.type) {
124
+ case 'output':
125
+ this.terminal.write(msg.data);
126
+ break;
127
+
128
+ case 'session_joined':
129
+ // Replay output buffer
130
+ if (msg.outputBuffer && msg.outputBuffer.length > 0) {
131
+ const joined = msg.outputBuffer.join('');
132
+ this.terminal.write(joined);
133
+ }
134
+ break;
135
+
136
+ case 'claude_started':
137
+ case 'codex_started':
138
+ case 'agent_started':
139
+ console.log(`[Split ${this.index}] Agent started`);
140
+ break;
141
+
142
+ case 'exit':
143
+ this.terminal.write('\r\n[Process exited]\r\n');
144
+ break;
145
+
146
+ case 'error':
147
+ this.terminal.write(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`);
148
+ break;
149
+ }
150
+ }
151
+
152
+ disconnect() {
153
+ if (this.socket) {
154
+ try {
155
+ this.socket.close();
156
+ } catch (e) {
157
+ // Ignore errors
158
+ }
159
+ this.socket = null;
160
+ }
161
+ }
162
+
163
+ fit() {
164
+ try {
165
+ if (this.fitAddon) {
166
+ this.fitAddon.fit();
167
+ }
168
+ } catch (error) {
169
+ // Ignore fit errors
170
+ }
171
+ }
172
+
173
+ updateActiveState() {
174
+ if (this.container) {
175
+ if (this.isActive) {
176
+ this.container.classList.add('split-active');
177
+ } else {
178
+ this.container.classList.remove('split-active');
179
+ }
180
+ }
181
+ }
182
+
183
+ clear() {
184
+ this.disconnect();
185
+ this.sessionId = null;
186
+ this.isActive = false;
187
+ if (this.terminal) {
188
+ this.terminal.clear();
189
+ }
190
+ this.updateActiveState();
191
+ }
192
+
193
+ destroy() {
194
+ this.disconnect();
195
+ if (this.terminal) {
196
+ this.terminal.dispose();
197
+ }
198
+ }
199
+ }
200
+
201
+ class SplitContainer {
202
+ constructor(app) {
203
+ this.app = app;
204
+ this.enabled = false;
205
+ this.splits = [];
206
+ this.activeSplitIndex = 0;
207
+ this.dividerPosition = 50; // percentage
208
+
209
+ // Create split container elements
210
+ this.createSplitElements();
211
+
212
+ // Restore state from localStorage
213
+ this.restoreState();
214
+
215
+ // Setup keyboard shortcuts
216
+ this.setupKeyboardShortcuts();
217
+ }
218
+
219
+ createSplitElements() {
220
+ const main = document.querySelector('.main');
221
+ if (!main) return;
222
+
223
+ // Create split container (initially hidden)
224
+ this.splitContainerEl = document.createElement('div');
225
+ this.splitContainerEl.className = 'split-container';
226
+ this.splitContainerEl.style.display = 'none';
227
+
228
+ // Create left split
229
+ const leftSplit = document.createElement('div');
230
+ leftSplit.className = 'split-pane split-left';
231
+ leftSplit.dataset.splitIndex = '0';
232
+
233
+ // Create divider
234
+ this.divider = document.createElement('div');
235
+ this.divider.className = 'split-divider';
236
+ this.setupDividerDrag();
237
+
238
+ // Create right split
239
+ const rightSplit = document.createElement('div');
240
+ rightSplit.className = 'split-pane split-right';
241
+ rightSplit.dataset.splitIndex = '1';
242
+
243
+ // Add close button to right split
244
+ const closeBtn = document.createElement('button');
245
+ closeBtn.className = 'split-close';
246
+ closeBtn.title = 'Close Split (Ctrl+\\)';
247
+ closeBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
248
+ <line x1="18" y1="6" x2="6" y2="18"/>
249
+ <line x1="6" y1="6" x2="18" y2="18"/>
250
+ </svg>`;
251
+ closeBtn.addEventListener('click', () => this.closeSplit());
252
+ rightSplit.appendChild(closeBtn);
253
+
254
+ this.splitContainerEl.appendChild(leftSplit);
255
+ this.splitContainerEl.appendChild(this.divider);
256
+ this.splitContainerEl.appendChild(rightSplit);
257
+
258
+ main.appendChild(this.splitContainerEl);
259
+
260
+ // Create Split instances with their own terminals
261
+ this.splits.push(new Split(leftSplit, 0, this.app));
262
+ this.splits.push(new Split(rightSplit, 1, this.app));
263
+
264
+ // Mark left as active by default
265
+ this.splits[0].isActive = true;
266
+ this.splits[0].updateActiveState();
267
+
268
+ // Click handlers to focus splits
269
+ leftSplit.addEventListener('click', () => this.focusSplit(0));
270
+ rightSplit.addEventListener('click', () => this.focusSplit(1));
271
+ }
272
+
273
+ setupDividerDrag() {
274
+ let isDragging = false;
275
+ let startX = 0;
276
+ let startPosition = 50;
277
+
278
+ this.divider.addEventListener('mousedown', (e) => {
279
+ isDragging = true;
280
+ startX = e.clientX;
281
+ startPosition = this.dividerPosition;
282
+ document.body.style.cursor = 'col-resize';
283
+ e.preventDefault();
284
+ });
285
+
286
+ document.addEventListener('mousemove', (e) => {
287
+ if (!isDragging) return;
288
+
289
+ const container = this.splitContainerEl.getBoundingClientRect();
290
+ const delta = e.clientX - startX;
291
+ const deltaPercent = (delta / container.width) * 100;
292
+
293
+ this.dividerPosition = Math.max(20, Math.min(80, startPosition + deltaPercent));
294
+ this.updateDividerPosition();
295
+ });
296
+
297
+ document.addEventListener('mouseup', () => {
298
+ if (isDragging) {
299
+ isDragging = false;
300
+ document.body.style.cursor = '';
301
+ this.saveState();
302
+ }
303
+ });
304
+ }
305
+
306
+ updateDividerPosition() {
307
+ const leftSplit = this.splitContainerEl.querySelector('.split-left');
308
+ const rightSplit = this.splitContainerEl.querySelector('.split-right');
309
+
310
+ if (leftSplit && rightSplit) {
311
+ leftSplit.style.width = `${this.dividerPosition}%`;
312
+ rightSplit.style.width = `${100 - this.dividerPosition}%`;
313
+
314
+ // Fit both terminals
315
+ this.splits.forEach(split => split.fit());
316
+ }
317
+ }
318
+
319
+ async createSplit(sessionId) {
320
+ if (this.enabled) return; // Already split
321
+
322
+ this.enabled = true;
323
+
324
+ // Hide single terminal container
325
+ const terminalContainer = document.getElementById('terminalContainer');
326
+ if (terminalContainer) {
327
+ terminalContainer.style.display = 'none';
328
+ }
329
+
330
+ // Show split container
331
+ this.splitContainerEl.style.display = 'flex';
332
+
333
+ // Update divider position
334
+ this.updateDividerPosition();
335
+
336
+ // Set sessions - left gets current session, right gets the dragged session
337
+ const currentSessionId = this.app.currentClaudeSessionId;
338
+ await this.splits[0].setSession(currentSessionId);
339
+ await this.splits[1].setSession(sessionId);
340
+
341
+ // Focus right split (newly created)
342
+ this.focusSplit(1);
343
+
344
+ // Save state
345
+ this.saveState();
346
+
347
+ console.log(`[SplitContainer] Created split with sessions: ${currentSessionId} | ${sessionId}`);
348
+ }
349
+
350
+ closeSplit() {
351
+ if (!this.enabled) return;
352
+
353
+ this.enabled = false;
354
+
355
+ // Disconnect both splits
356
+ this.splits.forEach(split => split.disconnect());
357
+
358
+ // Show single terminal container
359
+ const terminalContainer = document.getElementById('terminalContainer');
360
+ if (terminalContainer) {
361
+ terminalContainer.style.display = 'flex';
362
+ }
363
+
364
+ // Hide split container
365
+ this.splitContainerEl.style.display = 'none';
366
+
367
+ // Clear splits but don't destroy terminals (we'll reuse them)
368
+ this.splits.forEach((split, i) => {
369
+ split.sessionId = null;
370
+ split.isActive = (i === 0);
371
+ split.updateActiveState();
372
+ if (split.terminal) {
373
+ split.terminal.clear();
374
+ }
375
+ });
376
+
377
+ this.activeSplitIndex = 0;
378
+
379
+ // Reconnect main terminal to current session if we have one
380
+ if (this.app.currentClaudeSessionId) {
381
+ setTimeout(() => {
382
+ this.app.connect();
383
+ }, 100);
384
+ }
385
+
386
+ // Save state
387
+ this.saveState();
388
+
389
+ console.log('[SplitContainer] Closed split, back to single pane');
390
+ }
391
+
392
+ focusSplit(index) {
393
+ if (index < 0 || index >= this.splits.length) return;
394
+ if (this.activeSplitIndex === index) return;
395
+
396
+ // Update active state
397
+ this.splits.forEach((split, i) => {
398
+ split.isActive = (i === index);
399
+ split.updateActiveState();
400
+ });
401
+
402
+ this.activeSplitIndex = index;
403
+
404
+ // Focus the terminal in this split
405
+ const split = this.splits[index];
406
+ if (split.terminal) {
407
+ split.terminal.focus();
408
+ }
409
+
410
+ // Update app's current session to match this split
411
+ if (split.sessionId && this.app) {
412
+ this.app.currentClaudeSessionId = split.sessionId;
413
+
414
+ // Update tab selection
415
+ if (this.app.sessionTabManager) {
416
+ const tab = this.app.sessionTabManager.tabs.get(split.sessionId);
417
+ if (tab) {
418
+ // Update visual state of tabs
419
+ this.app.sessionTabManager.tabs.forEach((t, id) => {
420
+ if (id === split.sessionId) {
421
+ t.classList.add('active');
422
+ } else {
423
+ t.classList.remove('active');
424
+ }
425
+ });
426
+ this.app.sessionTabManager.activeTabId = split.sessionId;
427
+ }
428
+ }
429
+ }
430
+
431
+ console.log(`[SplitContainer] Focused split ${index}, session: ${split.sessionId}`);
432
+ }
433
+
434
+ // Called when a tab is switched - update the active split's session
435
+ async onTabSwitch(sessionId) {
436
+ if (!this.enabled) return;
437
+
438
+ const activeSplit = this.splits[this.activeSplitIndex];
439
+ if (activeSplit) {
440
+ await activeSplit.setSession(sessionId);
441
+ }
442
+ }
443
+
444
+ setupKeyboardShortcuts() {
445
+ document.addEventListener('keydown', (e) => {
446
+ // Cmd/Ctrl + \ to toggle split
447
+ if ((e.metaKey || e.ctrlKey) && e.key === '\\') {
448
+ e.preventDefault();
449
+ if (this.enabled) {
450
+ this.closeSplit();
451
+ } else {
452
+ // Create split - need to pick a session to split with
453
+ // For now, just show a message
454
+ console.log('[SplitContainer] To create a split, drag a tab to the right edge of the terminal');
455
+ }
456
+ }
457
+
458
+ // Cmd/Ctrl + 1/2 to focus splits
459
+ if ((e.metaKey || e.ctrlKey) && this.enabled) {
460
+ if (e.key === '1') {
461
+ e.preventDefault();
462
+ this.focusSplit(0);
463
+ } else if (e.key === '2') {
464
+ e.preventDefault();
465
+ this.focusSplit(1);
466
+ }
467
+ }
468
+ });
469
+ }
470
+
471
+ saveState() {
472
+ try {
473
+ const state = {
474
+ enabled: this.enabled,
475
+ dividerPosition: this.dividerPosition,
476
+ activeSplitIndex: this.activeSplitIndex,
477
+ sessions: this.splits.map(s => s.sessionId)
478
+ };
479
+ localStorage.setItem('cc-web-splits', JSON.stringify(state));
480
+ } catch (error) {
481
+ console.error('Failed to save split state:', error);
482
+ }
483
+ }
484
+
485
+ restoreState() {
486
+ try {
487
+ const saved = localStorage.getItem('cc-web-splits');
488
+ if (!saved) return;
489
+
490
+ const state = JSON.parse(saved);
491
+
492
+ // Restore divider position
493
+ if (state.dividerPosition) {
494
+ this.dividerPosition = state.dividerPosition;
495
+ }
496
+
497
+ // Note: Don't auto-restore enabled state on page load
498
+ // User needs to manually create splits
499
+ // This prevents issues with stale session IDs
500
+ } catch (error) {
501
+ console.error('Failed to restore split state:', error);
502
+ }
503
+ }
504
+
505
+ // Setup drop zones for drag-to-split
506
+ setupDropZones() {
507
+ const terminalContainer = document.getElementById('terminalContainer');
508
+ if (!terminalContainer) return;
509
+
510
+ // Create drop zone indicator
511
+ const dropZone = document.createElement('div');
512
+ dropZone.className = 'split-drop-zone';
513
+ dropZone.style.display = 'none';
514
+ terminalContainer.appendChild(dropZone);
515
+
516
+ // Listen for drag events on terminal container
517
+ terminalContainer.addEventListener('dragover', (e) => {
518
+ // Only show drop zone if we're not already in split mode
519
+ if (this.enabled) return;
520
+
521
+ const sessionId = e.dataTransfer?.getData('application/x-session-id');
522
+ if (!sessionId) return;
523
+
524
+ // Don't allow splitting with the current session
525
+ if (sessionId === this.app.currentClaudeSessionId) return;
526
+
527
+ e.preventDefault();
528
+ e.dataTransfer.dropEffect = 'move';
529
+
530
+ // Show drop zone if near right edge
531
+ const rect = terminalContainer.getBoundingClientRect();
532
+ const isNearRightEdge = (e.clientX > rect.right - 100);
533
+
534
+ if (isNearRightEdge) {
535
+ dropZone.style.display = 'block';
536
+ } else {
537
+ dropZone.style.display = 'none';
538
+ }
539
+ });
540
+
541
+ terminalContainer.addEventListener('dragleave', () => {
542
+ dropZone.style.display = 'none';
543
+ });
544
+
545
+ terminalContainer.addEventListener('drop', async (e) => {
546
+ const sessionId = e.dataTransfer?.getData('application/x-session-id');
547
+ if (!sessionId) return;
548
+
549
+ // Don't allow splitting with the current session
550
+ if (sessionId === this.app.currentClaudeSessionId) {
551
+ dropZone.style.display = 'none';
552
+ return;
553
+ }
554
+
555
+ const rect = terminalContainer.getBoundingClientRect();
556
+ const isNearRightEdge = (e.clientX > rect.right - 100);
557
+
558
+ if (isNearRightEdge && !this.enabled) {
559
+ e.preventDefault();
560
+ await this.createSplit(sessionId);
561
+ }
562
+
563
+ dropZone.style.display = 'none';
564
+ });
565
+ }
566
+ }
567
+
568
+ // Export for use in app.js
569
+ window.SplitContainer = SplitContainer;