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 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.2');
28
+ .version('0.5.3');
29
29
  program
30
30
  .command('login')
31
31
  .description('Authenticate with nstantpage.com')
@@ -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.2';
27
+ const VERSION = '0.5.3';
28
28
  /**
29
29
  * Resolve the backend API base URL.
30
30
  * - If --backend is passed, use it
@@ -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.
@@ -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 shell = spawn(shellCmd, [], {
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
- shell.stdout?.on('data', (data) => {
393
- const str = data.toString('utf-8');
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
- shell.stderr?.on('data', (data) => {
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.shell.stdin?.write(inputData);
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.cols = cols || session.cols;
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.2',
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
- // Don't stop the dev server on close user is still working locally.
629
- // Just acknowledge.
630
- this.json(res, { success: true, message: 'Acknowledged (agent keeps running)' });
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.2',
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.shell.stdin?.write(msg.data);
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 = msg.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.2",
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
  }