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.
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/bin/localpov-mcp.js +15 -0
- package/bin/localpov.js +599 -0
- package/dashboard/index.html +909 -0
- package/dist/collectors/browser-capture.d.ts +124 -0
- package/dist/collectors/browser-capture.js +327 -0
- package/dist/collectors/browser-capture.js.map +1 -0
- package/dist/collectors/build-parser.d.ts +18 -0
- package/dist/collectors/build-parser.js +192 -0
- package/dist/collectors/build-parser.js.map +1 -0
- package/dist/collectors/docker-watcher.d.ts +42 -0
- package/dist/collectors/docker-watcher.js +101 -0
- package/dist/collectors/docker-watcher.js.map +1 -0
- package/dist/collectors/terminal.d.ts +42 -0
- package/dist/collectors/terminal.js +128 -0
- package/dist/collectors/terminal.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +466 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/utils/inject.d.ts +11 -0
- package/dist/utils/inject.js +241 -0
- package/dist/utils/inject.js.map +1 -0
- package/dist/utils/network.d.ts +7 -0
- package/dist/utils/network.js +42 -0
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/proxy.d.ts +24 -0
- package/dist/utils/proxy.js +363 -0
- package/dist/utils/proxy.js.map +1 -0
- package/dist/utils/scanner.d.ts +9 -0
- package/dist/utils/scanner.js +96 -0
- package/dist/utils/scanner.js.map +1 -0
- package/dist/utils/session-manager.d.ts +109 -0
- package/dist/utils/session-manager.js +488 -0
- package/dist/utils/session-manager.js.map +1 -0
- package/dist/utils/shell-init.d.ts +26 -0
- package/dist/utils/shell-init.js +422 -0
- package/dist/utils/shell-init.js.map +1 -0
- package/dist/utils/system-info.d.ts +51 -0
- package/dist/utils/system-info.js +170 -0
- package/dist/utils/system-info.js.map +1 -0
- package/package.json +64 -0
package/bin/localpov.js
ADDED
|
@@ -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
|
+
}
|