nstantpage-agent 0.5.2 → 0.5.3
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/dist/cli.js +1 -1
- package/dist/commands/start.js +1 -1
- package/dist/localServer.d.ts +10 -1
- package/dist/localServer.js +102 -36
- package/dist/tunnel.js +4 -7
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ const program = new Command();
|
|
|
25
25
|
program
|
|
26
26
|
.name('nstantpage')
|
|
27
27
|
.description('Local development agent for nstantpage.com — run projects on your machine, preview in the cloud')
|
|
28
|
-
.version('0.5.
|
|
28
|
+
.version('0.5.3');
|
|
29
29
|
program
|
|
30
30
|
.command('login')
|
|
31
31
|
.description('Authenticate with nstantpage.com')
|
package/dist/commands/start.js
CHANGED
|
@@ -24,7 +24,7 @@ import { getConfig, getProjectConfig, setProjectConfig, clearProjectConfig, getD
|
|
|
24
24
|
import { TunnelClient } from '../tunnel.js';
|
|
25
25
|
import { LocalServer } from '../localServer.js';
|
|
26
26
|
import { PackageInstaller } from '../packageInstaller.js';
|
|
27
|
-
const VERSION = '0.5.
|
|
27
|
+
const VERSION = '0.5.3';
|
|
28
28
|
/**
|
|
29
29
|
* Resolve the backend API base URL.
|
|
30
30
|
* - If --backend is passed, use it
|
package/dist/localServer.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ import { DevServer } from './devServer.js';
|
|
|
17
17
|
interface TerminalSession {
|
|
18
18
|
id: string;
|
|
19
19
|
projectId: string;
|
|
20
|
-
shell: ChildProcess;
|
|
20
|
+
shell: ChildProcess | null;
|
|
21
|
+
ptyProcess: any | null;
|
|
21
22
|
outputBuffer: string[];
|
|
22
23
|
createdAt: number;
|
|
23
24
|
lastActivity: number;
|
|
@@ -35,6 +36,14 @@ interface TerminalSession {
|
|
|
35
36
|
* Get a terminal session by ID (used by WS relay in tunnel).
|
|
36
37
|
*/
|
|
37
38
|
export declare function getTerminalSession(sessionId: string): TerminalSession | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Write data to a terminal session (handles both node-pty and child_process).
|
|
41
|
+
*/
|
|
42
|
+
export declare function writeToTerminalSession(session: TerminalSession, data: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Resize a terminal session (only works with node-pty).
|
|
45
|
+
*/
|
|
46
|
+
export declare function resizeTerminalSession(session: TerminalSession, cols: number, rows: number): void;
|
|
38
47
|
/**
|
|
39
48
|
* Attach a real-time listener to a terminal session (for WebSocket relay).
|
|
40
49
|
* Returns a cleanup function to detach.
|
package/dist/localServer.js
CHANGED
|
@@ -14,12 +14,22 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import http from 'http';
|
|
16
16
|
import os from 'os';
|
|
17
|
+
import { createRequire } from 'module';
|
|
17
18
|
import { spawn } from 'child_process';
|
|
18
19
|
import { DevServer } from './devServer.js';
|
|
19
20
|
import { FileManager } from './fileManager.js';
|
|
20
21
|
import { Checker } from './checker.js';
|
|
21
22
|
import { ErrorStore, structuredErrorToString } from './errorStore.js';
|
|
22
23
|
import { PackageInstaller } from './packageInstaller.js';
|
|
24
|
+
// ─── Try to load node-pty for real PTY support ─────────────────
|
|
25
|
+
let ptyModule = null;
|
|
26
|
+
try {
|
|
27
|
+
const _require = createRequire(import.meta.url);
|
|
28
|
+
ptyModule = _require('node-pty');
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// node-pty not available — will fall back to 'script' PTY wrapper
|
|
32
|
+
}
|
|
23
33
|
const terminalSessions = new Map();
|
|
24
34
|
let sessionCounter = 0;
|
|
25
35
|
function generateSessionId() {
|
|
@@ -31,6 +41,30 @@ function generateSessionId() {
|
|
|
31
41
|
export function getTerminalSession(sessionId) {
|
|
32
42
|
return terminalSessions.get(sessionId);
|
|
33
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Write data to a terminal session (handles both node-pty and child_process).
|
|
46
|
+
*/
|
|
47
|
+
export function writeToTerminalSession(session, data) {
|
|
48
|
+
if (session.ptyProcess) {
|
|
49
|
+
session.ptyProcess.write(data);
|
|
50
|
+
}
|
|
51
|
+
else if (session.shell) {
|
|
52
|
+
session.shell.stdin?.write(data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resize a terminal session (only works with node-pty).
|
|
57
|
+
*/
|
|
58
|
+
export function resizeTerminalSession(session, cols, rows) {
|
|
59
|
+
session.cols = cols;
|
|
60
|
+
session.rows = rows;
|
|
61
|
+
if (session.ptyProcess) {
|
|
62
|
+
try {
|
|
63
|
+
session.ptyProcess.resize(cols, rows);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
34
68
|
/**
|
|
35
69
|
* Attach a real-time listener to a terminal session (for WebSocket relay).
|
|
36
70
|
* Returns a cleanup function to detach.
|
|
@@ -366,17 +400,50 @@ export class LocalServer {
|
|
|
366
400
|
const isAiSession = parsed.isAiSession || false;
|
|
367
401
|
// Determine shell
|
|
368
402
|
const shellCmd = process.platform === 'win32' ? 'cmd.exe' : (process.env.SHELL || '/bin/bash');
|
|
369
|
-
const
|
|
370
|
-
cwd: this.options.projectDir,
|
|
371
|
-
env: { ...process.env, TERM: 'xterm-256color', COLUMNS: String(cols), LINES: String(rows) },
|
|
372
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
373
|
-
});
|
|
403
|
+
const shellEnv = { ...process.env, TERM: 'xterm-256color', COLUMNS: String(cols), LINES: String(rows) };
|
|
374
404
|
sessionCounter++;
|
|
375
405
|
const label = parsed.label || `Terminal ${sessionCounter}`;
|
|
406
|
+
let shell = null;
|
|
407
|
+
let ptyProcess = null;
|
|
408
|
+
// Prefer node-pty for real PTY (interactive shell, echo, prompt, resize)
|
|
409
|
+
if (ptyModule) {
|
|
410
|
+
ptyProcess = ptyModule.spawn(shellCmd, [], {
|
|
411
|
+
name: 'xterm-256color',
|
|
412
|
+
cols,
|
|
413
|
+
rows,
|
|
414
|
+
cwd: this.options.projectDir,
|
|
415
|
+
env: shellEnv,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
else if (process.platform === 'darwin') {
|
|
419
|
+
// macOS fallback: use 'script' to allocate a PTY
|
|
420
|
+
shell = spawn('script', ['-q', '/dev/null', shellCmd], {
|
|
421
|
+
cwd: this.options.projectDir,
|
|
422
|
+
env: shellEnv,
|
|
423
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
else if (process.platform === 'linux') {
|
|
427
|
+
// Linux fallback: 'script' with -c flag
|
|
428
|
+
shell = spawn('script', ['-qc', shellCmd, '/dev/null'], {
|
|
429
|
+
cwd: this.options.projectDir,
|
|
430
|
+
env: shellEnv,
|
|
431
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
// Windows / other: raw spawn (limited interactivity)
|
|
436
|
+
shell = spawn(shellCmd, [], {
|
|
437
|
+
cwd: this.options.projectDir,
|
|
438
|
+
env: shellEnv,
|
|
439
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
440
|
+
});
|
|
441
|
+
}
|
|
376
442
|
const session = {
|
|
377
443
|
id: sessionId,
|
|
378
444
|
projectId: this.options.projectId,
|
|
379
445
|
shell,
|
|
446
|
+
ptyProcess,
|
|
380
447
|
outputBuffer: [],
|
|
381
448
|
createdAt: Date.now(),
|
|
382
449
|
lastActivity: Date.now(),
|
|
@@ -389,39 +456,22 @@ export class LocalServer {
|
|
|
389
456
|
dataListeners: new Set(),
|
|
390
457
|
exitListeners: new Set(),
|
|
391
458
|
};
|
|
392
|
-
|
|
393
|
-
|
|
459
|
+
// Helper to push output to buffer and notify listeners
|
|
460
|
+
const pushOutput = (str) => {
|
|
394
461
|
session.outputBuffer.push(str);
|
|
395
462
|
session.lastActivity = Date.now();
|
|
396
|
-
// Keep buffer capped at ~100KB
|
|
397
463
|
while (session.outputBuffer.length > 500)
|
|
398
464
|
session.outputBuffer.shift();
|
|
399
|
-
// Notify real-time listeners (WebSocket relay)
|
|
400
465
|
for (const listener of session.dataListeners) {
|
|
401
466
|
try {
|
|
402
467
|
listener(str);
|
|
403
468
|
}
|
|
404
469
|
catch { }
|
|
405
470
|
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const str = data.toString('utf-8');
|
|
409
|
-
session.outputBuffer.push(str);
|
|
410
|
-
session.lastActivity = Date.now();
|
|
411
|
-
while (session.outputBuffer.length > 500)
|
|
412
|
-
session.outputBuffer.shift();
|
|
413
|
-
// Notify real-time listeners
|
|
414
|
-
for (const listener of session.dataListeners) {
|
|
415
|
-
try {
|
|
416
|
-
listener(str);
|
|
417
|
-
}
|
|
418
|
-
catch { }
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
shell.on('exit', (code) => {
|
|
471
|
+
};
|
|
472
|
+
const handleExit = (code) => {
|
|
422
473
|
session.exited = true;
|
|
423
474
|
session.exitCode = code;
|
|
424
|
-
// Notify exit listeners
|
|
425
475
|
for (const listener of session.exitListeners) {
|
|
426
476
|
try {
|
|
427
477
|
listener(code);
|
|
@@ -429,7 +479,17 @@ export class LocalServer {
|
|
|
429
479
|
catch { }
|
|
430
480
|
}
|
|
431
481
|
terminalSessions.delete(sessionId);
|
|
432
|
-
}
|
|
482
|
+
};
|
|
483
|
+
if (ptyProcess) {
|
|
484
|
+
// node-pty: single onData for combined stdout+stderr
|
|
485
|
+
ptyProcess.onData((data) => pushOutput(data));
|
|
486
|
+
ptyProcess.onExit(({ exitCode }) => handleExit(exitCode));
|
|
487
|
+
}
|
|
488
|
+
else if (shell) {
|
|
489
|
+
shell.stdout?.on('data', (data) => pushOutput(data.toString('utf-8')));
|
|
490
|
+
shell.stderr?.on('data', (data) => pushOutput(data.toString('utf-8')));
|
|
491
|
+
shell.on('exit', (code) => handleExit(code));
|
|
492
|
+
}
|
|
433
493
|
terminalSessions.set(sessionId, session);
|
|
434
494
|
// Return format matching frontend TerminalSessionInfo
|
|
435
495
|
this.json(res, {
|
|
@@ -467,7 +527,7 @@ export class LocalServer {
|
|
|
467
527
|
this.json(res, { success: false, error: 'Session not found' });
|
|
468
528
|
return;
|
|
469
529
|
}
|
|
470
|
-
session
|
|
530
|
+
writeToTerminalSession(session, inputData);
|
|
471
531
|
session.lastActivity = Date.now();
|
|
472
532
|
this.json(res, { success: true });
|
|
473
533
|
}
|
|
@@ -490,10 +550,7 @@ export class LocalServer {
|
|
|
490
550
|
this.json(res, { success: false, error: 'Session not found' });
|
|
491
551
|
return;
|
|
492
552
|
}
|
|
493
|
-
session
|
|
494
|
-
session.rows = rows || session.rows;
|
|
495
|
-
// For real pty we'd call pty.resize(cols, rows), but for child_process
|
|
496
|
-
// we just update the env (takes effect on next data)
|
|
553
|
+
resizeTerminalSession(session, cols || session.cols, rows || session.rows);
|
|
497
554
|
this.json(res, { success: true, cols: session.cols, rows: session.rows });
|
|
498
555
|
}
|
|
499
556
|
// ─── /live/agent-status ──────────────────────────────────────
|
|
@@ -502,7 +559,7 @@ export class LocalServer {
|
|
|
502
559
|
connected: true,
|
|
503
560
|
projectId: this.options.projectId,
|
|
504
561
|
agent: {
|
|
505
|
-
version: '0.5.
|
|
562
|
+
version: '0.5.3',
|
|
506
563
|
hostname: os.hostname(),
|
|
507
564
|
platform: `${os.platform()} ${os.arch()}`,
|
|
508
565
|
},
|
|
@@ -625,9 +682,18 @@ export class LocalServer {
|
|
|
625
682
|
}
|
|
626
683
|
// ─── /live/close ─────────────────────────────────────────────
|
|
627
684
|
async handleClose(_req, res) {
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
|
|
685
|
+
// Stop the dev server when user explicitly clicks Stop in the UI.
|
|
686
|
+
// The frontend only calls /live/close from the Stop button, not on navigation.
|
|
687
|
+
try {
|
|
688
|
+
if (this.devServer.isRunning) {
|
|
689
|
+
await this.devServer.stop();
|
|
690
|
+
}
|
|
691
|
+
this.json(res, { success: true, message: 'Dev server stopped' });
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
res.statusCode = 500;
|
|
695
|
+
this.json(res, { success: false, error: err.message });
|
|
696
|
+
}
|
|
631
697
|
}
|
|
632
698
|
// ─── /live/heartbeat ────────────────────────────────────────
|
|
633
699
|
async handleHeartbeat(_req, res) {
|
package/dist/tunnel.js
CHANGED
|
@@ -20,7 +20,7 @@ import http from 'http';
|
|
|
20
20
|
import os from 'os';
|
|
21
21
|
import chalk from 'chalk';
|
|
22
22
|
import { getDeviceId } from './config.js';
|
|
23
|
-
import { getTerminalSession, attachTerminalClient } from './localServer.js';
|
|
23
|
+
import { getTerminalSession, attachTerminalClient, writeToTerminalSession, resizeTerminalSession } from './localServer.js';
|
|
24
24
|
export class TunnelClient {
|
|
25
25
|
ws = null;
|
|
26
26
|
options;
|
|
@@ -63,7 +63,7 @@ export class TunnelClient {
|
|
|
63
63
|
// Send enhanced agent info with capabilities and deviceId
|
|
64
64
|
this.send({
|
|
65
65
|
type: 'agent-info',
|
|
66
|
-
version: '0.5.
|
|
66
|
+
version: '0.5.3',
|
|
67
67
|
hostname: os.hostname(),
|
|
68
68
|
platform: `${os.platform()} ${os.arch()}`,
|
|
69
69
|
deviceId: getDeviceId(),
|
|
@@ -215,16 +215,13 @@ export class TunnelClient {
|
|
|
215
215
|
switch (msg.type) {
|
|
216
216
|
case 'input':
|
|
217
217
|
if (typeof msg.data === 'string') {
|
|
218
|
-
session
|
|
218
|
+
writeToTerminalSession(session, msg.data);
|
|
219
219
|
session.lastActivity = Date.now();
|
|
220
220
|
}
|
|
221
221
|
break;
|
|
222
222
|
case 'resize':
|
|
223
223
|
if (typeof msg.cols === 'number' && typeof msg.rows === 'number') {
|
|
224
|
-
session.cols
|
|
225
|
-
session.rows = msg.rows;
|
|
226
|
-
// Note: child_process.spawn doesn't support resize natively
|
|
227
|
-
// (only node-pty does). We update the stored size for future use.
|
|
224
|
+
resizeTerminalSession(session, msg.cols, msg.rows);
|
|
228
225
|
}
|
|
229
226
|
break;
|
|
230
227
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nstantpage-agent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Local development agent for nstantpage.com — run your projects locally, preview in the cloud. Replaces cloud containers for faster builds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -58,5 +58,8 @@
|
|
|
58
58
|
"@types/ws": "^8.5.10",
|
|
59
59
|
"tsx": "^4.19.0",
|
|
60
60
|
"typescript": "^5.5.0"
|
|
61
|
+
},
|
|
62
|
+
"optionalDependencies": {
|
|
63
|
+
"node-pty": "^1.1.0"
|
|
61
64
|
}
|
|
62
65
|
}
|