localpov 0.1.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +309 -0
  3. package/bin/localpov-mcp.js +15 -0
  4. package/bin/localpov.js +599 -0
  5. package/dashboard/index.html +909 -0
  6. package/dist/collectors/browser-capture.d.ts +124 -0
  7. package/dist/collectors/browser-capture.js +327 -0
  8. package/dist/collectors/browser-capture.js.map +1 -0
  9. package/dist/collectors/build-parser.d.ts +18 -0
  10. package/dist/collectors/build-parser.js +192 -0
  11. package/dist/collectors/build-parser.js.map +1 -0
  12. package/dist/collectors/docker-watcher.d.ts +42 -0
  13. package/dist/collectors/docker-watcher.js +101 -0
  14. package/dist/collectors/docker-watcher.js.map +1 -0
  15. package/dist/collectors/terminal.d.ts +42 -0
  16. package/dist/collectors/terminal.js +128 -0
  17. package/dist/collectors/terminal.js.map +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +6 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/mcp-server.d.ts +1 -0
  22. package/dist/mcp-server.js +466 -0
  23. package/dist/mcp-server.js.map +1 -0
  24. package/dist/utils/inject.d.ts +11 -0
  25. package/dist/utils/inject.js +241 -0
  26. package/dist/utils/inject.js.map +1 -0
  27. package/dist/utils/network.d.ts +7 -0
  28. package/dist/utils/network.js +42 -0
  29. package/dist/utils/network.js.map +1 -0
  30. package/dist/utils/proxy.d.ts +24 -0
  31. package/dist/utils/proxy.js +363 -0
  32. package/dist/utils/proxy.js.map +1 -0
  33. package/dist/utils/scanner.d.ts +9 -0
  34. package/dist/utils/scanner.js +96 -0
  35. package/dist/utils/scanner.js.map +1 -0
  36. package/dist/utils/session-manager.d.ts +109 -0
  37. package/dist/utils/session-manager.js +488 -0
  38. package/dist/utils/session-manager.js.map +1 -0
  39. package/dist/utils/shell-init.d.ts +26 -0
  40. package/dist/utils/shell-init.js +422 -0
  41. package/dist/utils/shell-init.js.map +1 -0
  42. package/dist/utils/system-info.d.ts +51 -0
  43. package/dist/utils/system-info.js +170 -0
  44. package/dist/utils/system-info.js.map +1 -0
  45. package/package.json +64 -0
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Suppress http-proxy's util._extend deprecation warning (their code, not ours)
4
+ const _origEmit = process.emit;
5
+ process.emit = function (name, data) {
6
+ if (name === 'warning' && typeof data === 'object' && data.name === 'DeprecationWarning' && data.code === 'DEP0060') return false;
7
+ return _origEmit.apply(process, arguments);
8
+ };
9
+
10
+ const pkg = require('../package.json');
11
+ const { scanPorts, checkPort } = require('../dist/utils/scanner');
12
+ const { createServer } = require('../dist/utils/proxy');
13
+ const { getLocalIP, getAllIPs } = require('../dist/utils/network');
14
+ const { TerminalCapture } = require('../dist/collectors/terminal');
15
+ const { getInitScript, setup, unsetup, detectShell, cleanSessions } = require('../dist/utils/shell-init');
16
+ const { SessionManager } = require('../dist/utils/session-manager');
17
+ const { BrowserCapture } = require('../dist/collectors/browser-capture');
18
+
19
+ // Terminal color helpers (no-op when piped)
20
+ const noColor = !process.stdout.isTTY;
21
+ const c = {
22
+ g: (s) => noColor ? s : `\x1b[32m${s}\x1b[0m`,
23
+ r: (s) => noColor ? s : `\x1b[31m${s}\x1b[0m`,
24
+ y: (s) => noColor ? s : `\x1b[33m${s}\x1b[0m`,
25
+ b: (s) => noColor ? s : `\x1b[1m${s}\x1b[0m`,
26
+ d: (s) => noColor ? s : `\x1b[2m${s}\x1b[0m`,
27
+ c: (s) => noColor ? s : `\x1b[36m${s}\x1b[0m`,
28
+ };
29
+
30
+ // Parse args
31
+ const args = process.argv.slice(2);
32
+ const flags = {};
33
+
34
+ // ─── Subcommands that exit immediately ───
35
+
36
+ // localpov --mcp | localpov mcp — start MCP server (stdio transport for AI agents)
37
+ if (args[0] === '--mcp' || args[0] === 'mcp') {
38
+ flags.__mcp = true;
39
+ require('../dist/mcp-server');
40
+ }
41
+
42
+ // localpov mcp-config — print MCP config JSON for AI agents
43
+ if (args[0] === 'mcp-config') {
44
+ const which = args[1] || 'claude';
45
+ const config = {
46
+ localpov: {
47
+ command: 'npx',
48
+ args: ['-y', 'localpov', '--mcp'],
49
+ },
50
+ };
51
+
52
+ console.log('');
53
+ if (which === 'cursor') {
54
+ console.log(` ${c.b('Add to .cursor/mcp.json:')}`);
55
+ console.log('');
56
+ console.log(JSON.stringify({ mcpServers: config }, null, 2));
57
+ } else {
58
+ console.log(` ${c.b('Add to .mcp.json (project root):')}`);
59
+ console.log('');
60
+ console.log(JSON.stringify({ mcpServers: config }, null, 2));
61
+ }
62
+ console.log('');
63
+ console.log(` ${c.d('Or if installed globally (npm i -g localpov):')}`);
64
+ console.log(JSON.stringify({ mcpServers: { localpov: { command: 'localpov-mcp' } } }, null, 2));
65
+ console.log('');
66
+ process.exit(0);
67
+ }
68
+
69
+ // localpov init <shell> — print shell init script to stdout
70
+ if (args[0] === 'init') {
71
+ const shell = args[1] || detectShell();
72
+ const script = getInitScript(shell);
73
+ if (!script) {
74
+ console.error(` ${c.r('Error:')} Unsupported shell: ${shell}`);
75
+ console.error(` Supported: bash, zsh, fish, powershell`);
76
+ process.exit(1);
77
+ }
78
+ process.stdout.write(script);
79
+ process.exit(0);
80
+ }
81
+
82
+ // localpov setup [--shell <shell>] — install shell integration
83
+ if (args[0] === 'setup') {
84
+ let shell = null;
85
+ if (args[1] === '--shell' && args[2]) shell = args[2];
86
+ else if (args[1] && !args[1].startsWith('-')) shell = args[1];
87
+
88
+ console.log('');
89
+ console.log(` ${c.b('localpov setup')}`);
90
+ console.log('');
91
+
92
+ if (!shell) shell = detectShell();
93
+ console.log(` Detected shell: ${c.c(shell)}`);
94
+
95
+ const result = setup(shell);
96
+
97
+ if (!result.success) {
98
+ console.log(` ${c.r('✗')} ${result.error}`);
99
+ process.exit(1);
100
+ }
101
+
102
+ if (result.already) {
103
+ console.log(` ${c.g('✓')} Already installed in ${c.d(result.profilePath)}`);
104
+ } else {
105
+ console.log(` ${c.g('✓')} Init script written to ${c.d(result.initPath)}`);
106
+ console.log(` ${c.g('✓')} Source line added to ${c.d(result.profilePath)}`);
107
+ }
108
+
109
+ // Warn Git Bash users about tee fallback
110
+ if (shell === 'bash' && process.platform === 'win32' && process.env.MSYSTEM) {
111
+ console.log(` ${c.y('⚠')} Git Bash detected — using tee-based capture fallback`);
112
+ console.log(` ${c.d(' Output capture works, but minor readline display glitches are possible.')}`);
113
+ console.log(` ${c.d(' For full capture fidelity, use PowerShell: localpov setup powershell')}`);
114
+ }
115
+
116
+ console.log('');
117
+ console.log(` ${c.b('What happens now:')}`);
118
+ console.log(` Every new terminal session will be automatically captured.`);
119
+ console.log(` Output is saved to ${c.d('~/.localpov/sessions/')}`);
120
+ console.log('');
121
+ console.log(` ${c.b('To activate now:')}`);
122
+ if (shell === 'powershell') {
123
+ console.log(` Restart PowerShell, or run:`);
124
+ console.log(` ${c.c(`. "${result.initPath}"`)}`);
125
+ } else {
126
+ console.log(` Restart your terminal, or run:`);
127
+ console.log(` ${c.c(`source "${result.initPath}"`)}`);
128
+ }
129
+ console.log('');
130
+ console.log(` ${c.b('To remove:')}`);
131
+ console.log(` ${c.c('localpov unsetup')}`);
132
+ console.log('');
133
+ process.exit(0);
134
+ }
135
+
136
+ // localpov unsetup — remove shell integration
137
+ if (args[0] === 'unsetup') {
138
+ let shell = null;
139
+ if (args[1] === '--shell' && args[2]) shell = args[2];
140
+ else if (args[1] && !args[1].startsWith('-')) shell = args[1];
141
+
142
+ const result = unsetup(shell);
143
+
144
+ console.log('');
145
+ if (result.success) {
146
+ console.log(` ${c.g('✓')} Removed localpov from ${c.d(result.profilePath)}`);
147
+ console.log(` ${c.d('Restart your terminal for changes to take effect.')}`);
148
+ } else {
149
+ console.log(` ${c.r('✗')} ${result.error}`);
150
+ }
151
+ console.log('');
152
+ process.exit(0);
153
+ }
154
+
155
+ // localpov sessions — list captured terminal sessions
156
+ if (args[0] === 'sessions') {
157
+ const mgr = new SessionManager();
158
+
159
+ if (args[1] === 'clean') {
160
+ const cleaned = mgr.cleanup();
161
+ console.log('');
162
+ console.log(` ${c.g('✓')} Cleaned ${cleaned} stale session file(s)`);
163
+ console.log('');
164
+ process.exit(0);
165
+ }
166
+
167
+ if (args[1] === 'read' && args[2]) {
168
+ const pid = parseInt(args[2], 10);
169
+ const lines = parseInt(args[3] || '50', 10);
170
+ const result = mgr.readSession(pid, { lines });
171
+
172
+ if (result.error) {
173
+ console.error(` ${c.r('Error:')} ${result.error}`);
174
+ process.exit(1);
175
+ }
176
+
177
+ console.log('');
178
+ console.log(` ${c.b(`Session ${pid}`)} ${c.d(`(last ${result.lineCount} lines)`)}`);
179
+ console.log(` ${'─'.repeat(60)}`);
180
+ for (const line of result.lines) {
181
+ console.log(` ${line}`);
182
+ }
183
+
184
+ if (result.commands.length > 0) {
185
+ console.log('');
186
+ console.log(` ${c.b('Recent commands:')}`);
187
+ for (const cmd of result.commands.slice(-5)) {
188
+ const status = cmd.exitCode === null ? c.y('running')
189
+ : cmd.exitCode === 0 ? c.g('ok')
190
+ : c.r(`exit ${cmd.exitCode}`);
191
+ console.log(` ${status} ${c.c(cmd.command)}`);
192
+ }
193
+ }
194
+ console.log('');
195
+ process.exit(0);
196
+ }
197
+
198
+ if (args[1] === 'errors') {
199
+ const errors = mgr.getErrors();
200
+ console.log('');
201
+ if (errors.length === 0) {
202
+ console.log(` ${c.g('✓')} No errors detected across sessions`);
203
+ } else {
204
+ console.log(` ${c.r(`${errors.length} error(s) found:`)}`);
205
+ console.log('');
206
+ for (const err of errors.slice(0, 10)) {
207
+ const status = err.alive ? c.g('●') : c.r('●');
208
+ console.log(` ${status} ${c.d(`[PID ${err.pid}]`)} ${err.text}`);
209
+ }
210
+ }
211
+ console.log('');
212
+ process.exit(0);
213
+ }
214
+
215
+ // Default: list sessions
216
+ const sessions = mgr.listSessions();
217
+ console.log('');
218
+
219
+ if (sessions.length === 0) {
220
+ console.log(` ${c.y('⚠')} No captured sessions found`);
221
+ console.log(` ${c.d('Run `localpov setup` to start capturing terminal sessions')}`);
222
+ } else {
223
+ console.log(` ${c.b('Captured terminal sessions:')}`);
224
+ console.log('');
225
+ for (const s of sessions) {
226
+ const status = s.alive ? c.g('● alive') : c.d('● dead ');
227
+ const size = s.logSize > 1024 * 1024
228
+ ? `${(s.logSize / 1024 / 1024).toFixed(1)}MB`
229
+ : `${(s.logSize / 1024).toFixed(0)}KB`;
230
+ console.log(` ${status} PID ${c.b(String(s.pid).padEnd(7))} ${c.c(s.shell.padEnd(12))} ${c.d(size.padStart(8))} ${s.cwd}`);
231
+ }
232
+ console.log('');
233
+ console.log(` ${c.d('localpov sessions read <pid> [lines] — view session output')}`);
234
+ console.log(` ${c.d('localpov sessions errors — find errors across all')}`);
235
+ console.log(` ${c.d('localpov sessions clean — remove stale sessions')}`);
236
+ }
237
+ console.log('');
238
+ process.exit(0);
239
+ }
240
+
241
+ // Check for 'run' subcommand: localpov run [flags] -- <command>
242
+ if (args[0] === 'run') {
243
+ flags.run = true;
244
+ const sepIdx = args.indexOf('--');
245
+ const flagArgs = sepIdx >= 0 ? args.slice(1, sepIdx) : args.slice(1);
246
+ const cmdArgs = sepIdx >= 0 ? args.slice(sepIdx + 1) : [];
247
+
248
+ if (cmdArgs.length === 0) {
249
+ console.error(' \x1b[31mError:\x1b[0m Usage: localpov run [flags] -- <command>');
250
+ console.error(' Example: localpov run -- npm run dev');
251
+ process.exit(1);
252
+ }
253
+ flags.runCommand = cmdArgs.join(' ');
254
+
255
+ for (let i = 0; i < flagArgs.length; i++) {
256
+ if (flagArgs[i] === '--port' || flagArgs[i] === '-p') { flags.port = parseInt(flagArgs[++i], 10); }
257
+ else if (flagArgs[i] === '--listen' || flagArgs[i] === '-l') { flags.listen = parseInt(flagArgs[++i], 10); }
258
+ else if (flagArgs[i] === '--terminal-rw') { flags.terminalRw = true; }
259
+ else if (flagArgs[i] === '--help' || flagArgs[i] === '-h') { flags.help = true; }
260
+ }
261
+ } else {
262
+ for (let i = 0; i < args.length; i++) {
263
+ if (args[i] === '--port' || args[i] === '-p') { flags.port = parseInt(args[++i], 10); }
264
+ else if (args[i] === '--listen' || args[i] === '-l') { flags.listen = parseInt(args[++i], 10); }
265
+ else if (args[i] === '--help' || args[i] === '-h') { flags.help = true; }
266
+ else if (args[i] === '--version' || args[i] === '-v') { flags.version = true; }
267
+ else if (args[i] === 'doctor') { flags.doctor = true; }
268
+ }
269
+ }
270
+
271
+ // Load .localpovrc config file
272
+ const configPath = require('path').join(process.cwd(), '.localpovrc');
273
+ try {
274
+ const configRaw = require('fs').readFileSync(configPath, 'utf8');
275
+ const config = JSON.parse(configRaw);
276
+ if (config.port && flags.port === undefined) flags.port = parseInt(config.port, 10);
277
+ if (config.listen && flags.listen === undefined) flags.listen = parseInt(config.listen, 10);
278
+ if (config.ports && !flags.port) flags.customPorts = config.ports;
279
+ } catch {
280
+ // No config file or invalid JSON — that's fine
281
+ }
282
+
283
+ if (flags.version) { console.log(`localpov v${pkg.version}`); process.exit(0); }
284
+
285
+ // Validate arguments
286
+ if (flags.port !== undefined && (isNaN(flags.port) || flags.port < 1 || flags.port > 65535)) {
287
+ console.error(` ${c.r('Error:')} --port must be a number between 1 and 65535`);
288
+ process.exit(1);
289
+ }
290
+ if (flags.listen !== undefined && (isNaN(flags.listen) || flags.listen < 1 || flags.listen > 65535)) {
291
+ console.error(` ${c.r('Error:')} --listen must be a number between 1 and 65535`);
292
+ process.exit(1);
293
+ }
294
+
295
+ if (flags.help) {
296
+ console.log(`
297
+ ${c.b('localpov')} — development context bridge for AI coding agents
298
+
299
+ ${c.b('Usage:')}
300
+ localpov Start proxy and auto-detect dev servers
301
+ localpov --port 3000 Proxy a specific port
302
+ localpov run -- npm run dev Run command + capture terminal output
303
+ localpov setup Install shell integration (auto-capture all terminals)
304
+ localpov unsetup Remove shell integration
305
+ localpov sessions List captured terminal sessions
306
+ localpov sessions read <pid> View a session's output
307
+ localpov sessions errors Find errors across all sessions
308
+ localpov mcp-config Print MCP config for AI agents
309
+ localpov doctor Check your system setup
310
+
311
+ ${c.b('Run command:')}
312
+ localpov run [flags] -- <command>
313
+ Starts the command, captures stdout/stderr, and streams it
314
+ to the Terminal tab in the dashboard (read-only by default).
315
+
316
+ --terminal-rw Allow remote input (interactive mode, use with caution)
317
+
318
+ ${c.b('Shell integration:')}
319
+ localpov setup Auto-capture every terminal session
320
+ localpov sessions List captured sessions + find errors
321
+ Sessions are saved to ~/.localpov/sessions/ and can be read by
322
+ AI coding agents via the LocalPOV MCP server.
323
+
324
+ ${c.b('MCP (AI agent integration):')}
325
+ localpov mcp-config Print config JSON for Claude Code / Cursor
326
+ localpov --mcp Start MCP server directly (stdio transport)
327
+
328
+ ${c.b('Config file:')}
329
+ Create .localpovrc in your project root:
330
+ { "port": 3000 }
331
+
332
+ ${c.b('Options:')}
333
+ -p, --port <port> Target a specific localhost port
334
+ -l, --listen <port> Port for LocalPOV to listen on (default: 4000)
335
+ -h, --help Show this help
336
+ -v, --version Show version
337
+ `);
338
+ process.exit(0);
339
+ }
340
+
341
+ if (flags.doctor) {
342
+ console.log(`\n ${c.b('localpov doctor')}\n`);
343
+ const ips = getAllIPs();
344
+ console.log(` Node.js: ${c.g('✓')} ${process.version}`);
345
+ console.log(` Platform: ${process.platform} ${process.arch}`);
346
+ console.log(` IPs:`);
347
+ for (const ip of ips) console.log(` ${ip.name}: ${c.c(ip.address)}`);
348
+ if (ips.length === 0) console.log(` ${c.y('⚠ No network interfaces found')}`);
349
+ console.log('');
350
+ process.exit(0);
351
+ }
352
+
353
+ const LISTEN_PORT = flags.listen || parseInt(process.env.LOCALPOV_PORT || '4000', 10);
354
+ const SCAN_INTERVAL = 3000;
355
+ let detectedApps = [];
356
+ let srv = null;
357
+ let terminal = null;
358
+ const browserCapture = new BrowserCapture();
359
+
360
+ // Clean stale sessions on startup
361
+ try { cleanSessions(); } catch {}
362
+
363
+ // ─── First-run detection ───
364
+ function isFirstRun() {
365
+ const fs = require('fs');
366
+ const { LOCALPOV_DIR } = require('../dist/utils/shell-init');
367
+ const markerPath = require('path').join(LOCALPOV_DIR, '.setup-done');
368
+ return !fs.existsSync(markerPath);
369
+ }
370
+
371
+ function markSetupDone() {
372
+ const fs = require('fs');
373
+ const { LOCALPOV_DIR } = require('../dist/utils/shell-init');
374
+ const markerPath = require('path').join(LOCALPOV_DIR, '.setup-done');
375
+ try {
376
+ fs.mkdirSync(LOCALPOV_DIR, { recursive: true });
377
+ fs.writeFileSync(markerPath, new Date().toISOString(), 'utf8');
378
+ } catch {}
379
+ }
380
+
381
+ function ask(question) {
382
+ return new Promise((resolve) => {
383
+ const readline = require('readline');
384
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
385
+ rl.question(question, (answer) => {
386
+ rl.close();
387
+ resolve(answer.trim().toLowerCase());
388
+ });
389
+ });
390
+ }
391
+
392
+ async function firstRunSetup() {
393
+ const fs = require('fs');
394
+ const { setup, detectShell } = require('../dist/utils/shell-init');
395
+
396
+ console.log('');
397
+ console.log(` ${c.b('Welcome to LocalPOV')} ${c.d(`v${pkg.version}`)}`);
398
+ console.log(` ${c.d('First-time setup — takes 30 seconds')}`);
399
+ console.log('');
400
+
401
+ // ─── Step 1: Shell integration ───
402
+ const shell = detectShell();
403
+ console.log(` ${c.b('1.')} Shell integration ${c.d('(auto-capture all terminals for AI agents)')}`);
404
+ console.log(` Detected: ${c.c(shell)}`);
405
+
406
+ const shellAnswer = await ask(` Add auto-capture to ${shell} profile? [Y/n] `);
407
+ if (shellAnswer !== 'n' && shellAnswer !== 'no') {
408
+ const result = setup(shell);
409
+ if (result.success) {
410
+ if (result.already) {
411
+ console.log(` ${c.g('✓')} Already installed`);
412
+ } else {
413
+ console.log(` ${c.g('✓')} Added to ${c.d(result.profilePath)}`);
414
+ console.log(` ${c.d('New terminals will be auto-captured after restart')}`);
415
+ }
416
+ } else {
417
+ console.log(` ${c.y('⚠')} ${result.error}`);
418
+ console.log(` ${c.d('You can run `localpov setup` later to retry')}`);
419
+ }
420
+ } else {
421
+ console.log(` ${c.d('Skipped. Run `localpov setup` anytime to enable.')}`);
422
+ }
423
+ console.log('');
424
+
425
+ // ─── Step 2: MCP config for AI agents ───
426
+ console.log(` ${c.b('2.')} AI agent integration ${c.d('(MCP server for Claude Code, Cursor, etc.)')}`);
427
+
428
+ const mcpPath = require('path').join(process.cwd(), '.mcp.json');
429
+ let mcpExists = false;
430
+ try {
431
+ const content = fs.readFileSync(mcpPath, 'utf8');
432
+ mcpExists = content.includes('localpov');
433
+ } catch {}
434
+
435
+ if (mcpExists) {
436
+ console.log(` ${c.g('✓')} Already configured in ${c.d('.mcp.json')}`);
437
+ } else {
438
+ const mcpAnswer = await ask(` Create .mcp.json in this project? [Y/n] `);
439
+ if (mcpAnswer !== 'n' && mcpAnswer !== 'no') {
440
+ const mcpConfig = {
441
+ mcpServers: {
442
+ localpov: {
443
+ command: 'npx',
444
+ args: ['-y', 'localpov', '--mcp'],
445
+ },
446
+ },
447
+ };
448
+ try {
449
+ const existing = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
450
+ existing.mcpServers = existing.mcpServers || {};
451
+ existing.mcpServers.localpov = mcpConfig.mcpServers.localpov;
452
+ fs.writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
453
+ } catch {
454
+ fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
455
+ }
456
+ console.log(` ${c.g('✓')} Created ${c.d('.mcp.json')}`);
457
+ console.log(` ${c.d('Restart your AI agent to activate the MCP server')}`);
458
+ } else {
459
+ console.log(` ${c.d('Skipped. Run `localpov mcp-config` to see the config.')}`);
460
+ }
461
+ }
462
+ console.log('');
463
+
464
+ markSetupDone();
465
+ console.log(` ${c.g('✓')} Setup complete. Starting LocalPOV...\n`);
466
+ }
467
+
468
+ async function main() {
469
+ console.log('');
470
+ console.log(` ${c.b('localpov')} ${c.d(`v${pkg.version}`)}`);
471
+ console.log('');
472
+
473
+ // ─── First-run: guided setup ───
474
+ if (isFirstRun() && !flags.run && !process.env.LOCALPOV_SKIP_SETUP) {
475
+ if (process.stdin.isTTY) {
476
+ await firstRunSetup();
477
+ } else {
478
+ console.log(` ${c.d('First time? Run `localpov setup` for shell integration + MCP config.')}`);
479
+ console.log('');
480
+ markSetupDone();
481
+ }
482
+ }
483
+
484
+ // ─── Run mode: spawn command + capture terminal ───
485
+ if (flags.run) {
486
+ console.log(` ${c.b('Running:')} ${c.c(flags.runCommand)}`);
487
+ console.log(` ${c.d(flags.terminalRw ? 'Interactive mode (stdin enabled)' : 'Read-only terminal capture')}`);
488
+ console.log('');
489
+
490
+ terminal = new TerminalCapture(flags.runCommand, {
491
+ interactive: !!flags.terminalRw,
492
+ });
493
+
494
+ terminal.start();
495
+
496
+ terminal.on('data', (data) => {
497
+ if (data.type === 'stdout') process.stdout.write(data.text);
498
+ else if (data.type === 'stderr') process.stderr.write(data.text);
499
+ });
500
+
501
+ terminal.on('exit', ({ code, signal }) => {
502
+ console.log('');
503
+ console.log(` ${code === 0 ? c.g('✓') : c.r('✗')} Process exited with code ${code}${signal ? ` (${signal})` : ''}`);
504
+ console.log(` ${c.d('Dashboard still running — terminal output preserved')}`);
505
+ });
506
+
507
+ await new Promise(r => setTimeout(r, 2000));
508
+ }
509
+
510
+ // Scan or use explicit port
511
+ if (flags.port) {
512
+ const alive = await checkPort(flags.port);
513
+ if (alive) {
514
+ const { detectFramework } = require('../dist/utils/scanner');
515
+ const fw = await detectFramework(flags.port);
516
+ detectedApps = [{ port: flags.port, framework: fw }];
517
+ console.log(` ${c.g('✓')} localhost:${c.b(flags.port)} ${c.d(`(${fw})`)}`);
518
+ } else {
519
+ console.log(` ${c.y('⚠')} localhost:${flags.port} not responding — will keep checking`);
520
+ detectedApps = [{ port: flags.port, framework: 'Not started' }];
521
+ }
522
+ } else {
523
+ process.stdout.write(` ${c.d('Scanning ports...')}`);
524
+ detectedApps = await scanPorts(flags.customPorts);
525
+ process.stdout.write('\r\x1b[K');
526
+
527
+ if (detectedApps.length === 0) {
528
+ console.log(` ${c.y('⚠')} No dev servers found`);
529
+ if (flags.run) {
530
+ console.log(` ${c.d('Waiting for the command to start a server...')}`);
531
+ } else {
532
+ console.log(` ${c.d("Start one (e.g. npm run dev) — we'll detect it.")}`);
533
+ }
534
+ } else {
535
+ for (const app of detectedApps) {
536
+ console.log(` ${c.g('✓')} localhost:${c.b(app.port)} ${c.d(`(${app.framework})`)}`);
537
+ }
538
+ }
539
+ }
540
+
541
+ console.log('');
542
+ const defaultPort = detectedApps.length > 0 ? detectedApps[0].port : 3000;
543
+
544
+ srv = createServer({
545
+ targetPort: defaultPort,
546
+ listenPort: LISTEN_PORT,
547
+ getApps: () => detectedApps,
548
+ terminal: terminal,
549
+ browserCapture: browserCapture,
550
+ onLog: (type, msg) => {
551
+ if (type === 'switch') console.log(` ${c.g('→')} Preview: localhost:${c.b(msg)}`);
552
+ else if (type === 'error') console.log(` ${c.r('✗')} ${msg}`);
553
+ },
554
+ onReady: () => {
555
+ const localURL = `http://localhost:${LISTEN_PORT}/__localpov__/`;
556
+
557
+ console.log(` ${c.g('✓')} Running on :${LISTEN_PORT}`);
558
+ if (terminal) {
559
+ console.log(` ${c.g('✓')} Terminal capture active`);
560
+ }
561
+ console.log('');
562
+ console.log(` ${c.b('Dashboard:')} ${c.c(localURL)}`);
563
+ console.log('');
564
+ console.log(` ${c.d('Ctrl+C to stop')}`);
565
+ console.log('');
566
+ },
567
+ });
568
+
569
+ // Background scan (skip if explicit port)
570
+ if (!flags.port) {
571
+ setInterval(async () => {
572
+ const fresh = await scanPorts(flags.customPorts);
573
+ for (const app of fresh) {
574
+ if (!detectedApps.find(a => a.port === app.port)) {
575
+ console.log(` ${c.g('+')} localhost:${c.b(app.port)} ${c.d(`(${app.framework})`)}`);
576
+ }
577
+ }
578
+ for (const app of detectedApps) {
579
+ if (!fresh.find(a => a.port === app.port)) {
580
+ console.log(` ${c.r('−')} localhost:${app.port} stopped`);
581
+ }
582
+ }
583
+ detectedApps = fresh;
584
+ }, SCAN_INTERVAL);
585
+ }
586
+ }
587
+
588
+ function cleanup() {
589
+ if (terminal) terminal.stop();
590
+ if (srv) srv.close();
591
+ }
592
+
593
+ process.on('SIGINT', () => { console.log(''); cleanup(); process.exit(0); });
594
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
595
+
596
+ // MCP server handles its own lifecycle — skip main()
597
+ if (!flags.__mcp) {
598
+ main().catch((err) => { console.error(` ${c.r('Error:')} ${err.message}`); process.exit(1); });
599
+ }