@leverageaiapps/theseus-server 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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/dist/capture.d.ts +3 -0
  4. package/dist/capture.d.ts.map +1 -0
  5. package/dist/capture.js +134 -0
  6. package/dist/capture.js.map +1 -0
  7. package/dist/cloudflare-tunnel.d.ts +9 -0
  8. package/dist/cloudflare-tunnel.d.ts.map +1 -0
  9. package/dist/cloudflare-tunnel.js +218 -0
  10. package/dist/cloudflare-tunnel.js.map +1 -0
  11. package/dist/config.d.ts +7 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +84 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/context-extractor.d.ts +17 -0
  16. package/dist/context-extractor.d.ts.map +1 -0
  17. package/dist/context-extractor.js +118 -0
  18. package/dist/context-extractor.js.map +1 -0
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +45 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/pty.d.ts +20 -0
  24. package/dist/pty.d.ts.map +1 -0
  25. package/dist/pty.js +148 -0
  26. package/dist/pty.js.map +1 -0
  27. package/dist/relay.d.ts +5 -0
  28. package/dist/relay.d.ts.map +1 -0
  29. package/dist/relay.js +131 -0
  30. package/dist/relay.js.map +1 -0
  31. package/dist/session.d.ts +5 -0
  32. package/dist/session.d.ts.map +1 -0
  33. package/dist/session.js +257 -0
  34. package/dist/session.js.map +1 -0
  35. package/dist/voice-recognition-modelscope.d.ts +50 -0
  36. package/dist/voice-recognition-modelscope.d.ts.map +1 -0
  37. package/dist/voice-recognition-modelscope.js +171 -0
  38. package/dist/voice-recognition-modelscope.js.map +1 -0
  39. package/dist/web-server.d.ts +6 -0
  40. package/dist/web-server.d.ts.map +1 -0
  41. package/dist/web-server.js +1971 -0
  42. package/dist/web-server.js.map +1 -0
  43. package/package.json +66 -0
  44. package/public/index.html +639 -0
  45. package/public/js/terminal-asr.js +508 -0
  46. package/public/js/terminal.js +514 -0
  47. package/public/js/voice-input.js +422 -0
  48. package/scripts/postinstall.js +66 -0
  49. package/scripts/verify-install.js +124 -0
@@ -0,0 +1,422 @@
1
+ class VoiceInput {
2
+ constructor() {
3
+ this.voiceBtn = document.getElementById('voice-btn');
4
+ this.input = document.getElementById('input');
5
+ this.transcriptionDiv = document.getElementById('transcription');
6
+ this.contextOverlay = document.getElementById('context-overlay');
7
+ this.contextOverlayContent = document.getElementById('context-overlay-content');
8
+
9
+ this.isRecording = false;
10
+ this.interimTranscript = '';
11
+ this.finalTranscript = '';
12
+ this.collectedTranscript = ''; // Store all collected transcripts
13
+ this.interimTimer = null; // Timer to auto-hide interim transcript after 3 seconds
14
+ this.autoSubmitPending = false; // Flag to auto-submit after Claude processing
15
+
16
+ // Claude API is now handled by the gateway service
17
+ // No need for API keys in the client
18
+
19
+ // Debug flag (will be updated from server)
20
+ this.debugMode = false;
21
+ this.checkDebugMode();
22
+
23
+ this.init();
24
+ }
25
+
26
+ // Helper function for conditional debug logging
27
+ debugLog(...args) {
28
+ if (this.debugMode) {
29
+ console.log(...args);
30
+ }
31
+ }
32
+
33
+ // Check if debug mode is enabled on server
34
+ async checkDebugMode() {
35
+ try {
36
+ const response = await fetch('/api/terminal-context');
37
+ if (response.ok) {
38
+ const data = await response.json();
39
+ this.debugMode = data.debugAsr || false;
40
+ if (this.debugMode) {
41
+ console.log('[Voice Input] Debug mode enabled');
42
+ }
43
+ }
44
+ } catch (e) {
45
+ // Ignore errors
46
+ }
47
+ }
48
+
49
+ init() {
50
+ this.voiceBtn.addEventListener('click', () => this.toggleRecording());
51
+
52
+ // Check for browser support
53
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
54
+ console.warn('Voice input not supported in this browser');
55
+ this.voiceBtn.style.display = 'none';
56
+ return;
57
+ }
58
+
59
+ // Check Terminal ASR configuration (uses terminal WebSocket -> local server -> gateway)
60
+ if (!window.terminalASR) {
61
+ console.error('Terminal ASR not loaded');
62
+ this.voiceBtn.style.display = 'none';
63
+ return;
64
+ }
65
+ }
66
+
67
+ async toggleRecording() {
68
+ if (this.isRecording) {
69
+ await this.stopRecording();
70
+ } else {
71
+ await this.startRecording();
72
+ }
73
+ }
74
+
75
+ async startRecording() {
76
+ try {
77
+ // Terminal ASR uses local server which connects to gateway
78
+ // API keys are configured on the gateway server
79
+
80
+ // Update context from terminal output
81
+ const terminalLines = this.getTerminalContext();
82
+ const context = window.terminalASR.updateContext(terminalLines);
83
+
84
+ // Show loading indicator immediately
85
+ this.showLoadingIndicator();
86
+
87
+ // Clear previous transcripts
88
+ this.interimTranscript = '';
89
+ this.finalTranscript = '';
90
+ this.collectedTranscript = ''; // Reset collected transcript
91
+ this.updateTranscriptionDisplay();
92
+
93
+ // Start real-time recording with Terminal ASR
94
+ await window.terminalASR.startRecording(
95
+ // onPartialResult
96
+ (text) => {
97
+ this.interimTranscript = text;
98
+ this.updateTranscriptionDisplay();
99
+ },
100
+ // onFinalResult
101
+ (text) => {
102
+ // Always collect the transcript first (before any checks)
103
+ // This ensures we don't lose the text when "go go go" is detected
104
+ if (this.finalTranscript) {
105
+ this.finalTranscript += ' ' + text;
106
+ } else {
107
+ this.finalTranscript = text;
108
+ }
109
+ if (this.collectedTranscript) {
110
+ this.collectedTranscript += ' ' + text;
111
+ } else {
112
+ this.collectedTranscript = text;
113
+ }
114
+
115
+ // Clear interim transcript - final result replaces it
116
+ // This matches iOS behavior: interimTranscription = ""
117
+ this.interimTranscript = '';
118
+
119
+ this.updateTranscriptionDisplay();
120
+
121
+ // Check for "go go go" command (case insensitive, with variations)
122
+ const goPattern = /go\s*go\s*go|gogogo/i;
123
+ if (goPattern.test(text)) {
124
+ console.log('[Voice Input] "go go go" command detected!');
125
+ // Set flag to auto-submit after Claude processing
126
+ this.autoSubmitPending = true;
127
+ // Stop recording immediately
128
+ this.stopRecording();
129
+ return;
130
+ }
131
+
132
+ // DON'T add to input immediately - wait for stop
133
+ // this.addToInput(text); // REMOVED
134
+ },
135
+ // onError - called for errors during recording (not startup errors)
136
+ (error) => {
137
+ console.error('Voice error:', error);
138
+ this.showError(error.message);
139
+ this.stopRecording();
140
+ },
141
+ // onReady - called when ASR session is ready to receive audio
142
+ () => {
143
+ // Update loading indicator to show recording state
144
+ this.transcriptionDiv.innerHTML = `<span style="color: #ef4444">● Recording...</span>`;
145
+ this.transcriptionDiv.style.display = 'block';
146
+ this.transcriptionDiv.classList.add('visible');
147
+ // Show terminal context overlay
148
+ this.showContextOverlay(context);
149
+ }
150
+ );
151
+
152
+ // Update UI - only reached if startRecording succeeded
153
+ this.isRecording = true;
154
+ this.voiceBtn.classList.add('active');
155
+ this.voiceBtn.title = 'Click to stop recording';
156
+
157
+ } catch (error) {
158
+ // Startup error - hide loading indicator and show error
159
+ console.error('Failed to start recording:', error);
160
+ this.hideLoadingIndicator();
161
+ this.showError(error.message);
162
+ }
163
+ }
164
+
165
+ async stopRecording() {
166
+ if (!this.isRecording) return;
167
+
168
+ console.log('[Voice Input] Stopping recording...');
169
+
170
+ // Stop recording with Terminal ASR
171
+ await window.terminalASR.stopRecording();
172
+
173
+ // Update UI
174
+ this.isRecording = false;
175
+ this.voiceBtn.classList.remove('active');
176
+ this.voiceBtn.title = 'Voice input (Click to start/stop)';
177
+
178
+ // Hide context overlay
179
+ this.hideContextOverlay();
180
+
181
+ // Clear any pending interim timer
182
+ if (this.interimTimer) {
183
+ clearTimeout(this.interimTimer);
184
+ this.interimTimer = null;
185
+ }
186
+
187
+ // Debug: Check collected transcript
188
+ console.log('[Voice Input] Collected transcript:', this.collectedTranscript);
189
+ console.log('[Voice Input] Final transcript:', this.finalTranscript);
190
+
191
+ // Get the raw transcription
192
+ const rawText = this.collectedTranscript || this.finalTranscript;
193
+
194
+ if (rawText && rawText.trim()) {
195
+ // First, INSERT (not replace) the raw text into input
196
+ const currentValue = this.input.value;
197
+ if (currentValue) {
198
+ // Insert at cursor position or at end
199
+ const cursorPos = this.input.selectionStart || currentValue.length;
200
+ this.input.value = currentValue.slice(0, cursorPos) + rawText.trim() + ' ' + currentValue.slice(cursorPos);
201
+ } else {
202
+ this.input.value = rawText.trim();
203
+ }
204
+
205
+ console.log('[Voice Input] Inserted raw text:', rawText.trim());
206
+
207
+ // Show loading indicator for Claude correction
208
+ this.transcriptionDiv.innerHTML = `<div class="interim-text">AI optimizing...</div>`;
209
+ this.transcriptionDiv.style.display = 'block';
210
+
211
+ // Request Claude correction via terminal WebSocket -> gateway
212
+ window.terminalASR.requestCorrection(rawText.trim(), (original, corrected) => {
213
+ console.log('[Voice Input] Claude correction received:', corrected);
214
+
215
+ // Replace the inserted raw text with corrected text
216
+ if (this.input.value.includes(original)) {
217
+ this.input.value = this.input.value.replace(original, corrected);
218
+ console.log('[Voice Input] Replaced with corrected text');
219
+ }
220
+
221
+ // Hide loading indicator immediately
222
+ this.transcriptionDiv.classList.remove('visible');
223
+ this.transcriptionDiv.innerHTML = '';
224
+ this.transcriptionDiv.style.display = 'none';
225
+
226
+ // Check for auto-submit (go go go command)
227
+ if (this.autoSubmitPending) {
228
+ console.log('[Voice Input] Auto-submit triggered by go go go command');
229
+ this.autoSubmitPending = false;
230
+
231
+ // Remove "go go go" variants from the input text
232
+ const goPattern = /\s*(go\s*go\s*go|gogogo)[。.!!]?\s*/gi;
233
+ this.input.value = this.input.value.replace(goPattern, '').trim();
234
+
235
+ // Trigger enter key press to submit
236
+ if (this.input.value) {
237
+ const enterEvent = new KeyboardEvent('keydown', {
238
+ key: 'Enter',
239
+ code: 'Enter',
240
+ keyCode: 13,
241
+ which: 13,
242
+ bubbles: true
243
+ });
244
+ this.input.dispatchEvent(enterEvent);
245
+ }
246
+ }
247
+ });
248
+ }
249
+
250
+ // Clear transcription after Claude correction or timeout
251
+ // Don't clear immediately as we need them for correction callback
252
+ setTimeout(() => {
253
+ this.interimTranscript = '';
254
+ this.finalTranscript = '';
255
+ this.collectedTranscript = '';
256
+ // Only update display if not still waiting for correction
257
+ if (!this.transcriptionDiv.innerHTML.includes('AI optimizing')) {
258
+ this.updateTranscriptionDisplay();
259
+ }
260
+ }, 5000);
261
+ }
262
+
263
+ // Claude processing is now handled by the gateway service
264
+ // The processWithClaude method is no longer needed
265
+ async processWithClaude_DEPRECATED(transcript) {
266
+ this.debugLog('[Claude] Starting to process transcript:', transcript);
267
+
268
+ try {
269
+ // Get terminal context
270
+ const terminalContext = window.terminalASR ? window.terminalASR.terminalContext : '';
271
+ this.debugLog('[Claude] Terminal context length:', terminalContext.length);
272
+
273
+ // Store existing input content to append to later
274
+ this.existingInputContent = this.input.value;
275
+ // Clear input only for the streaming content (will be restored)
276
+ this.input.value = '';
277
+
278
+ // Send to server via WebSocket to process with Claude
279
+ if (window.terminalWs && window.terminalWs.readyState === WebSocket.OPEN) {
280
+ this.debugLog('[Claude] Sending to server via WebSocket');
281
+ console.log('[Claude] Sending to server via WebSocket');
282
+
283
+ // Send Claude processing request
284
+ window.terminalWs.send(JSON.stringify({
285
+ type: 'claude_process',
286
+ transcript: transcript,
287
+ context: terminalContext,
288
+ api_key: this.claudeApiKey,
289
+ model: this.claudeModel
290
+ }));
291
+
292
+ console.log('[Claude] Request sent to server');
293
+ // The server will send back claude_response messages with the streamed text
294
+ // These will be handled in the existing WebSocket message handler
295
+ } else {
296
+ console.error('[Claude] WebSocket not connected:', window.terminalWs ? 'exists but not open' : 'does not exist');
297
+ // Fallback: append the original transcript to existing content
298
+ const existingContent = this.existingInputContent || '';
299
+ const needSpace = existingContent && !existingContent.endsWith(' ');
300
+ this.input.value = existingContent + (needSpace ? ' ' : '') + transcript;
301
+ this.input.style.height = 'auto';
302
+ this.input.style.height = this.input.scrollHeight + 'px';
303
+ }
304
+ } catch (error) {
305
+ console.error('[Claude] Error:', error);
306
+ // Fallback: append the original transcript to existing content
307
+ const existingContent = this.existingInputContent || this.input.value || '';
308
+ const needSpace = existingContent && !existingContent.endsWith(' ');
309
+ this.input.value = existingContent + (needSpace ? ' ' : '') + transcript;
310
+ this.input.style.height = 'auto';
311
+ this.input.style.height = this.input.scrollHeight + 'px';
312
+ }
313
+ }
314
+
315
+ getTerminalContext() {
316
+ // Get terminal lines from xterm.js
317
+ if (window.term) {
318
+ const buffer = window.term.buffer.active;
319
+ const lines = [];
320
+ for (let i = Math.max(0, buffer.length - 50); i < buffer.length; i++) {
321
+ const line = buffer.getLine(i);
322
+ if (line) {
323
+ lines.push(line.translateToString(true));
324
+ }
325
+ }
326
+ return lines;
327
+ }
328
+ return [];
329
+ }
330
+
331
+ updateTranscriptionDisplay() {
332
+ // Use transcriptionDiv (blue floating dialog) for transcription display
333
+ if (!this.transcriptionDiv) return;
334
+
335
+ // Use collectedTranscript which accumulates ALL final results
336
+ // interimTranscript contains only the current partial (not yet confirmed)
337
+ const hasText = this.interimTranscript || this.collectedTranscript;
338
+
339
+ if (hasText) {
340
+ this.transcriptionDiv.style.display = 'block';
341
+ this.transcriptionDiv.classList.add('visible');
342
+ // Display: [all previous final results] + [current interim]
343
+ // This matches iOS behavior: transcription + interimTranscription
344
+ this.transcriptionDiv.innerHTML = `
345
+ ${this.collectedTranscript ? `<strong>${this.collectedTranscript}</strong>` : ''}
346
+ ${this.interimTranscript ? `<span style="opacity: 0.7"> ${this.interimTranscript}</span>` : ''}
347
+ `;
348
+
349
+ // Don't auto-hide during recording - let stop handle cleanup
350
+ } else if (!this.isRecording) {
351
+ // Only hide if not recording
352
+ this.transcriptionDiv.classList.remove('visible');
353
+ this.transcriptionDiv.innerHTML = '';
354
+ this.transcriptionDiv.style.display = 'none';
355
+ }
356
+ }
357
+
358
+ addToInput(text) {
359
+ if (this.input && text) {
360
+ // Get current input value
361
+ const currentValue = this.input.value;
362
+
363
+ // Add space if needed
364
+ if (currentValue && !currentValue.endsWith(' ')) {
365
+ this.input.value = currentValue + ' ' + text;
366
+ } else {
367
+ this.input.value = currentValue + text;
368
+ }
369
+
370
+ // Trigger input event for any listeners
371
+ this.input.dispatchEvent(new Event('input', { bubbles: true }));
372
+
373
+ // Focus input
374
+ this.input.focus();
375
+ }
376
+ }
377
+
378
+ showError(message) {
379
+ if (this.transcriptionDiv) {
380
+ this.transcriptionDiv.classList.add('visible');
381
+ this.transcriptionDiv.innerHTML = `<span style="color: #ef4444">❌ ${message}</span>`;
382
+
383
+ setTimeout(() => {
384
+ this.transcriptionDiv.classList.remove('visible');
385
+ }, 3000);
386
+ }
387
+ }
388
+
389
+ showLoadingIndicator() {
390
+ // Use transcriptionDiv (blue floating dialog) for loading indicator
391
+ if (this.transcriptionDiv) {
392
+ this.transcriptionDiv.classList.add('visible');
393
+ this.transcriptionDiv.innerHTML = `<span style="color: #60a5fa">🔄 Please wait, initializing voice input...</span>`;
394
+ }
395
+ }
396
+
397
+ hideLoadingIndicator() {
398
+ // Hide the loading indicator
399
+ if (this.transcriptionDiv) {
400
+ this.transcriptionDiv.classList.remove('visible');
401
+ this.transcriptionDiv.innerHTML = '';
402
+ }
403
+ }
404
+
405
+ showContextOverlay(context) {
406
+ if (this.contextOverlay && this.contextOverlayContent) {
407
+ this.contextOverlayContent.textContent = context || 'No context available yet...';
408
+ this.contextOverlay.classList.add('visible');
409
+ }
410
+ }
411
+
412
+ hideContextOverlay() {
413
+ if (this.contextOverlay) {
414
+ this.contextOverlay.classList.remove('visible');
415
+ }
416
+ }
417
+ }
418
+
419
+ // Initialize voice input when page loads
420
+ document.addEventListener('DOMContentLoaded', () => {
421
+ window.voiceInput = new VoiceInput();
422
+ });
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install script to fix node-pty permissions on macOS/Linux
5
+ * This ensures the spawn-helper binary has execute permissions
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const platform = os.platform();
13
+
14
+ // Only run on macOS and Linux
15
+ if (platform !== 'darwin' && platform !== 'linux') {
16
+ console.log('Skipping node-pty permission fix (not macOS/Linux)');
17
+ process.exit(0);
18
+ }
19
+
20
+ // Determine the architecture
21
+ const arch = os.arch();
22
+ let prebuildDir;
23
+
24
+ if (platform === 'darwin') {
25
+ prebuildDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
26
+ } else if (platform === 'linux') {
27
+ prebuildDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
28
+ }
29
+
30
+ const spawnHelperPath = path.join(
31
+ __dirname,
32
+ '..',
33
+ 'node_modules',
34
+ 'node-pty',
35
+ 'prebuilds',
36
+ prebuildDir,
37
+ 'spawn-helper'
38
+ );
39
+
40
+ // Check if spawn-helper exists
41
+ if (!fs.existsSync(spawnHelperPath)) {
42
+ console.log(`spawn-helper not found at: ${spawnHelperPath}`);
43
+ console.log('This is normal if node-pty uses a different installation method');
44
+ process.exit(0);
45
+ }
46
+
47
+ try {
48
+ // Get current permissions
49
+ const stats = fs.statSync(spawnHelperPath);
50
+ const currentMode = stats.mode;
51
+
52
+ // Add execute permission (chmod +x)
53
+ const newMode = currentMode | fs.constants.S_IXUSR | fs.constants.S_IXGRP | fs.constants.S_IXOTH;
54
+
55
+ if (currentMode !== newMode) {
56
+ fs.chmodSync(spawnHelperPath, newMode);
57
+ console.log('✓ Fixed node-pty spawn-helper permissions');
58
+ } else {
59
+ console.log('✓ node-pty spawn-helper permissions already correct');
60
+ }
61
+ } catch (error) {
62
+ console.error('Warning: Failed to fix node-pty permissions:', error.message);
63
+ console.error('You may need to run manually: chmod +x node_modules/node-pty/prebuilds/*/spawn-helper');
64
+ // Don't fail the installation
65
+ process.exit(0);
66
+ }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Installation verification script
5
+ * Checks if all dependencies and permissions are correctly set up
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const { execSync } = require('child_process');
12
+
13
+ console.log('🔍 Verifying Theseus installation...\n');
14
+
15
+ let hasErrors = false;
16
+ let hasWarnings = false;
17
+
18
+ // Check 1: Node.js version
19
+ console.log('1. Checking Node.js version...');
20
+ const nodeVersion = process.version;
21
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
22
+ if (majorVersion >= 18) {
23
+ console.log(` ✓ Node.js ${nodeVersion} (>= 18.0.0)\n`);
24
+ } else {
25
+ console.log(` ✗ Node.js ${nodeVersion} is too old. Required: >= 18.0.0\n`);
26
+ hasErrors = true;
27
+ }
28
+
29
+ // Check 2: cloudflared
30
+ console.log('2. Checking cloudflared...');
31
+ try {
32
+ const cloudflaredVersion = execSync('cloudflared --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
33
+ console.log(` ✓ cloudflared installed: ${cloudflaredVersion.trim()}\n`);
34
+ } catch (error) {
35
+ console.log(' ✗ cloudflared not found');
36
+ console.log(' Install it from: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/\n');
37
+ hasErrors = true;
38
+ }
39
+
40
+ // Check 3: node-pty installation
41
+ console.log('3. Checking node-pty...');
42
+ const nodePtyPath = path.join(__dirname, '..', 'node_modules', 'node-pty');
43
+ if (fs.existsSync(nodePtyPath)) {
44
+ console.log(' ✓ node-pty installed\n');
45
+ } else {
46
+ console.log(' ✗ node-pty not found. Run: npm install\n');
47
+ hasErrors = true;
48
+ }
49
+
50
+ // Check 4: spawn-helper permissions (macOS/Linux only)
51
+ if (os.platform() === 'darwin' || os.platform() === 'linux') {
52
+ console.log('4. Checking spawn-helper permissions...');
53
+
54
+ const arch = os.arch();
55
+ const platform = os.platform();
56
+ let prebuildDir;
57
+
58
+ if (platform === 'darwin') {
59
+ prebuildDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
60
+ } else {
61
+ prebuildDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
62
+ }
63
+
64
+ const spawnHelperPath = path.join(nodePtyPath, 'prebuilds', prebuildDir, 'spawn-helper');
65
+
66
+ if (fs.existsSync(spawnHelperPath)) {
67
+ try {
68
+ const stats = fs.statSync(spawnHelperPath);
69
+ const hasExecute = (stats.mode & fs.constants.S_IXUSR) !== 0;
70
+
71
+ if (hasExecute) {
72
+ console.log(' ✓ spawn-helper has execute permissions\n');
73
+ } else {
74
+ console.log(' ✗ spawn-helper missing execute permissions');
75
+ console.log(` Fix: chmod +x ${spawnHelperPath}\n`);
76
+ hasErrors = true;
77
+ }
78
+ } catch (error) {
79
+ console.log(` ⚠ Could not check permissions: ${error.message}\n`);
80
+ hasWarnings = true;
81
+ }
82
+ } else {
83
+ console.log(` ⚠ spawn-helper not found at expected location`);
84
+ console.log(` This may be normal if using a different node-pty version\n`);
85
+ hasWarnings = true;
86
+ }
87
+ } else {
88
+ console.log('4. Skipping spawn-helper check (Windows)\n');
89
+ }
90
+
91
+ // Check 5: Built files
92
+ console.log('5. Checking built files...');
93
+ const distPath = path.join(__dirname, '..', 'dist', 'index.js');
94
+ if (fs.existsSync(distPath)) {
95
+ console.log(' ✓ Project built successfully\n');
96
+ } else {
97
+ console.log(' ⚠ Project not built yet. Run: npm run build\n');
98
+ hasWarnings = true;
99
+ }
100
+
101
+ // Check 6: Shell availability
102
+ console.log('6. Checking shell...');
103
+ const shell = process.env.SHELL || (os.platform() === 'win32' ? 'cmd.exe' : '/bin/sh');
104
+ if (fs.existsSync(shell)) {
105
+ console.log(` ✓ Shell available: ${shell}\n`);
106
+ } else {
107
+ console.log(` ⚠ Default shell not found: ${shell}\n`);
108
+ hasWarnings = true;
109
+ }
110
+
111
+ // Summary
112
+ console.log('═'.repeat(50));
113
+ if (hasErrors) {
114
+ console.log('❌ Installation has errors. Please fix the issues above.');
115
+ process.exit(1);
116
+ } else if (hasWarnings) {
117
+ console.log('⚠️ Installation complete with warnings.');
118
+ console.log(' You may want to address the warnings above.');
119
+ process.exit(0);
120
+ } else {
121
+ console.log('✅ Installation verified successfully!');
122
+ console.log('\nYou can now run: theseus start');
123
+ process.exit(0);
124
+ }