nstantpage-agent 0.4.0 → 0.4.2

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.d.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  * nstantpage start [dir] — Start agent for a project directory
8
8
  * nstantpage stop — Stop running agent
9
9
  * nstantpage status — Show agent status
10
+ * nstantpage service — Install as background service (auto-start on boot)
11
+ * nstantpage service --uninstall — Remove background service
10
12
  *
11
13
  * The agent replaces Docker containers — your project runs natively on your
12
14
  * machine with full performance, and the cloud editor connects through a tunnel.
package/dist/cli.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * nstantpage start [dir] — Start agent for a project directory
8
8
  * nstantpage stop — Stop running agent
9
9
  * nstantpage status — Show agent status
10
+ * nstantpage service — Install as background service (auto-start on boot)
11
+ * nstantpage service --uninstall — Remove background service
10
12
  *
11
13
  * The agent replaces Docker containers — your project runs natively on your
12
14
  * machine with full performance, and the cloud editor connects through a tunnel.
@@ -16,6 +18,7 @@ import chalk from 'chalk';
16
18
  import { loginCommand } from './commands/login.js';
17
19
  import { startCommand } from './commands/start.js';
18
20
  import { statusCommand } from './commands/status.js';
21
+ import { serviceCommand } from './commands/service.js';
19
22
  const program = new Command();
20
23
  program
21
24
  .name('nstantpage')
@@ -66,5 +69,12 @@ program
66
69
  .command('status')
67
70
  .description('Show agent status')
68
71
  .action(statusCommand);
72
+ program
73
+ .command('service')
74
+ .description('Install/uninstall agent as a background service (starts on boot)')
75
+ .option('--project-id <id>', 'Project ID to serve')
76
+ .option('--gateway <url>', 'Gateway URL', 'wss://webprev.live')
77
+ .option('--uninstall', 'Remove the background service')
78
+ .action(serviceCommand);
69
79
  program.parse();
70
80
  //# sourceMappingURL=cli.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Service command — install/uninstall the nstantpage agent as a background service.
3
+ *
4
+ * macOS: Uses launchd (~/Library/LaunchAgents/com.nstantpage.agent.plist)
5
+ * Linux: Uses systemd (--user mode: ~/.config/systemd/user/nstantpage-agent.service)
6
+ * Windows: Uses node-windows / Task Scheduler (future)
7
+ *
8
+ * This allows the agent to run on boot, always ready to accept connections
9
+ * from the web editor without manually starting it each time.
10
+ */
11
+ interface ServiceOptions {
12
+ projectId?: string;
13
+ gateway?: string;
14
+ uninstall?: boolean;
15
+ }
16
+ export declare function serviceCommand(options: ServiceOptions): Promise<void>;
17
+ export {};
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Service command — install/uninstall the nstantpage agent as a background service.
3
+ *
4
+ * macOS: Uses launchd (~/Library/LaunchAgents/com.nstantpage.agent.plist)
5
+ * Linux: Uses systemd (--user mode: ~/.config/systemd/user/nstantpage-agent.service)
6
+ * Windows: Uses node-windows / Task Scheduler (future)
7
+ *
8
+ * This allows the agent to run on boot, always ready to accept connections
9
+ * from the web editor without manually starting it each time.
10
+ */
11
+ import chalk from 'chalk';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import os from 'os';
15
+ import { execSync } from 'child_process';
16
+ import { getConfig } from '../config.js';
17
+ const PLIST_LABEL = 'com.nstantpage.agent';
18
+ const SYSTEMD_SERVICE = 'nstantpage-agent';
19
+ export async function serviceCommand(options) {
20
+ if (options.uninstall) {
21
+ return uninstallService();
22
+ }
23
+ return installService(options);
24
+ }
25
+ function getAgentBinPath() {
26
+ // Find the globally installed nstantpage binary
27
+ try {
28
+ const resolved = execSync('which nstantpage 2>/dev/null || which nstantpage-agent 2>/dev/null', { encoding: 'utf-8' }).trim();
29
+ if (resolved)
30
+ return resolved;
31
+ }
32
+ catch { }
33
+ // Fallback: try npm root -g
34
+ try {
35
+ const npmRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
36
+ const bin = path.join(npmRoot, '.bin', 'nstantpage');
37
+ if (fs.existsSync(bin))
38
+ return bin;
39
+ }
40
+ catch { }
41
+ throw new Error('Could not find nstantpage binary. Install globally first: npm install -g nstantpage-agent');
42
+ }
43
+ function getNodePath() {
44
+ try {
45
+ return execSync('which node', { encoding: 'utf-8' }).trim();
46
+ }
47
+ catch {
48
+ return '/usr/local/bin/node';
49
+ }
50
+ }
51
+ async function installService(options) {
52
+ const conf = getConfig();
53
+ const token = conf.get('token');
54
+ if (!token) {
55
+ console.log(chalk.red('✗ Not authenticated. Run "nstantpage login" first.'));
56
+ process.exit(1);
57
+ }
58
+ const projectId = options.projectId;
59
+ if (!projectId) {
60
+ console.log(chalk.red('✗ --project-id is required for service mode.'));
61
+ console.log(chalk.gray(' Example: nstantpage service --project-id 1234'));
62
+ process.exit(1);
63
+ }
64
+ const gateway = options.gateway || 'wss://webprev.live';
65
+ const platform = os.platform();
66
+ if (platform === 'darwin') {
67
+ await installLaunchd(projectId, gateway, token);
68
+ }
69
+ else if (platform === 'linux') {
70
+ await installSystemd(projectId, gateway, token);
71
+ }
72
+ else {
73
+ console.log(chalk.yellow('⚠ Background service not yet supported on this platform.'));
74
+ console.log(chalk.gray(' Use "nstantpage start" to run manually.'));
75
+ console.log(chalk.gray(' Windows support coming soon.'));
76
+ process.exit(1);
77
+ }
78
+ }
79
+ async function installLaunchd(projectId, gateway, token) {
80
+ const binPath = getAgentBinPath();
81
+ const nodePath = getNodePath();
82
+ const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
83
+ const plistPath = path.join(plistDir, `${PLIST_LABEL}.plist`);
84
+ const logPath = path.join(os.homedir(), '.nstantpage', 'agent.log');
85
+ const errPath = path.join(os.homedir(), '.nstantpage', 'agent.err.log');
86
+ // Ensure dirs exist
87
+ fs.mkdirSync(plistDir, { recursive: true });
88
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
89
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
90
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
91
+ <plist version="1.0">
92
+ <dict>
93
+ <key>Label</key>
94
+ <string>${PLIST_LABEL}</string>
95
+ <key>ProgramArguments</key>
96
+ <array>
97
+ <string>${nodePath}</string>
98
+ <string>${binPath}</string>
99
+ <string>start</string>
100
+ <string>--project-id</string>
101
+ <string>${projectId}</string>
102
+ <string>--gateway</string>
103
+ <string>${gateway}</string>
104
+ <string>--token</string>
105
+ <string>${token}</string>
106
+ </array>
107
+ <key>RunAtLoad</key>
108
+ <true/>
109
+ <key>KeepAlive</key>
110
+ <dict>
111
+ <key>SuccessfulExit</key>
112
+ <false/>
113
+ </dict>
114
+ <key>StandardOutPath</key>
115
+ <string>${logPath}</string>
116
+ <key>StandardErrorPath</key>
117
+ <string>${errPath}</string>
118
+ <key>EnvironmentVariables</key>
119
+ <dict>
120
+ <key>PATH</key>
121
+ <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
122
+ </dict>
123
+ <key>ThrottleInterval</key>
124
+ <integer>10</integer>
125
+ </dict>
126
+ </plist>`;
127
+ fs.writeFileSync(plistPath, plist, 'utf-8');
128
+ // Unload if already running
129
+ try {
130
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
131
+ }
132
+ catch { }
133
+ // Load
134
+ execSync(`launchctl load "${plistPath}"`, { encoding: 'utf-8' });
135
+ console.log(chalk.green('\n ✓ Agent installed as background service (launchd)\n'));
136
+ console.log(chalk.gray(` Plist: ${plistPath}`));
137
+ console.log(chalk.gray(` Project: ${projectId}`));
138
+ console.log(chalk.gray(` Log: ${logPath}`));
139
+ console.log(chalk.gray(` Status: launchctl list | grep nstantpage`));
140
+ console.log(chalk.gray(` Stop: nstantpage service --uninstall\n`));
141
+ console.log(chalk.blue(' The agent will now start automatically on login.'));
142
+ }
143
+ async function installSystemd(projectId, gateway, token) {
144
+ const binPath = getAgentBinPath();
145
+ const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
146
+ const servicePath = path.join(serviceDir, `${SYSTEMD_SERVICE}.service`);
147
+ fs.mkdirSync(serviceDir, { recursive: true });
148
+ const unit = `[Unit]
149
+ Description=nstantpage Local Development Agent
150
+ After=network-online.target
151
+ Wants=network-online.target
152
+
153
+ [Service]
154
+ Type=simple
155
+ ExecStart=${binPath} start --project-id ${projectId} --gateway ${gateway} --token ${token}
156
+ Restart=on-failure
157
+ RestartSec=10
158
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
159
+ Environment=HOME=${os.homedir()}
160
+
161
+ [Install]
162
+ WantedBy=default.target
163
+ `;
164
+ fs.writeFileSync(servicePath, unit, 'utf-8');
165
+ try {
166
+ execSync('systemctl --user daemon-reload', { encoding: 'utf-8' });
167
+ execSync(`systemctl --user enable ${SYSTEMD_SERVICE}`, { encoding: 'utf-8' });
168
+ execSync(`systemctl --user start ${SYSTEMD_SERVICE}`, { encoding: 'utf-8' });
169
+ }
170
+ catch (err) {
171
+ console.log(chalk.yellow(` ⚠ systemctl commands failed: ${err.message}`));
172
+ console.log(chalk.gray(' You may need to reload manually: systemctl --user daemon-reload'));
173
+ }
174
+ console.log(chalk.green('\n ✓ Agent installed as background service (systemd --user)\n'));
175
+ console.log(chalk.gray(` Unit: ${servicePath}`));
176
+ console.log(chalk.gray(` Project: ${projectId}`));
177
+ console.log(chalk.gray(` Status: systemctl --user status ${SYSTEMD_SERVICE}`));
178
+ console.log(chalk.gray(` Logs: journalctl --user -u ${SYSTEMD_SERVICE} -f`));
179
+ console.log(chalk.gray(` Stop: nstantpage service --uninstall\n`));
180
+ console.log(chalk.blue(' The agent will now start automatically on login.'));
181
+ }
182
+ async function uninstallService() {
183
+ const platform = os.platform();
184
+ if (platform === 'darwin') {
185
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
186
+ try {
187
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
188
+ }
189
+ catch { }
190
+ if (fs.existsSync(plistPath)) {
191
+ fs.unlinkSync(plistPath);
192
+ }
193
+ console.log(chalk.green(' ✓ Agent service uninstalled (launchd)'));
194
+ }
195
+ else if (platform === 'linux') {
196
+ try {
197
+ execSync(`systemctl --user stop ${SYSTEMD_SERVICE} 2>/dev/null`, { encoding: 'utf-8' });
198
+ execSync(`systemctl --user disable ${SYSTEMD_SERVICE} 2>/dev/null`, { encoding: 'utf-8' });
199
+ }
200
+ catch { }
201
+ const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', `${SYSTEMD_SERVICE}.service`);
202
+ if (fs.existsSync(servicePath)) {
203
+ fs.unlinkSync(servicePath);
204
+ try {
205
+ execSync('systemctl --user daemon-reload', { encoding: 'utf-8' });
206
+ }
207
+ catch { }
208
+ }
209
+ console.log(chalk.green(' ✓ Agent service uninstalled (systemd)'));
210
+ }
211
+ else {
212
+ console.log(chalk.yellow(' ⚠ Service uninstall not supported on this platform'));
213
+ }
214
+ }
215
+ //# sourceMappingURL=service.js.map
@@ -24,18 +24,18 @@ 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.4.0';
27
+ const VERSION = '0.4.1';
28
28
  /**
29
29
  * Resolve the backend API base URL.
30
30
  * - If --backend is passed, use it
31
31
  * - If gateway is local (ws://localhost), assume backend is localhost:5001
32
- * - Otherwise, default to https://ntstantpage.com
32
+ * - Otherwise, default to https://nstantpage.com
33
33
  */
34
34
  function resolveBackendUrl(options) {
35
35
  if (options.backend)
36
36
  return options.backend.replace(/\/$/, '');
37
37
  const isLocal = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(options.gateway);
38
- return isLocal ? 'http://localhost:5001' : 'https://ntstantpage.com';
38
+ return isLocal ? 'http://localhost:5001' : 'https://nstantpage.com';
39
39
  }
40
40
  /**
41
41
  * Resolve project directory:
@@ -46,6 +46,10 @@ export declare class LocalServer {
46
46
  private handleInstall;
47
47
  private handleExec;
48
48
  private handleTerminal;
49
+ private handleTerminalSessions;
50
+ private handleTerminalWrite;
51
+ private handleTerminalResize;
52
+ private handleAgentStatus;
49
53
  private handleContainerStatus;
50
54
  private handleContainerStats;
51
55
  private handleLogs;
@@ -14,11 +14,16 @@
14
14
  */
15
15
  import http from 'http';
16
16
  import os from 'os';
17
+ import { spawn } from 'child_process';
17
18
  import { DevServer } from './devServer.js';
18
19
  import { FileManager } from './fileManager.js';
19
20
  import { Checker } from './checker.js';
20
21
  import { ErrorStore, structuredErrorToString } from './errorStore.js';
21
22
  import { PackageInstaller } from './packageInstaller.js';
23
+ const terminalSessions = new Map();
24
+ function generateSessionId() {
25
+ return Math.random().toString(16).slice(2, 10);
26
+ }
22
27
  export class LocalServer {
23
28
  server = null;
24
29
  options;
@@ -113,6 +118,10 @@ export class LocalServer {
113
118
  '/live/install': this.handleInstall,
114
119
  '/live/exec': this.handleExec,
115
120
  '/live/terminal': this.handleTerminal,
121
+ '/live/terminal/sessions': this.handleTerminalSessions,
122
+ '/live/terminal/write': this.handleTerminalWrite,
123
+ '/live/terminal/resize': this.handleTerminalResize,
124
+ '/live/agent-status': this.handleAgentStatus,
116
125
  '/live/container-status': this.handleContainerStatus,
117
126
  '/live/container-stats': this.handleContainerStats,
118
127
  '/live/logs': this.handleLogs,
@@ -286,6 +295,144 @@ export class LocalServer {
286
295
  stderr: result.stderr,
287
296
  });
288
297
  }
298
+ // ─── /live/terminal/sessions ─────────────────────────────────
299
+ async handleTerminalSessions(req, res, body, url) {
300
+ const method = req.method?.toUpperCase() || 'GET';
301
+ // GET → list sessions for this project
302
+ if (method === 'GET') {
303
+ const sessions = Array.from(terminalSessions.values())
304
+ .filter(s => s.projectId === this.options.projectId)
305
+ .map(s => ({
306
+ sessionId: s.id,
307
+ projectId: s.projectId,
308
+ createdAt: s.createdAt,
309
+ lastActivity: s.lastActivity,
310
+ cols: s.cols,
311
+ rows: s.rows,
312
+ isAiSession: s.isAiSession,
313
+ }));
314
+ this.json(res, { success: true, sessions });
315
+ return;
316
+ }
317
+ // POST → create a new terminal session
318
+ if (method !== 'POST') {
319
+ res.statusCode = 405;
320
+ this.json(res, { error: 'Method not allowed' });
321
+ return;
322
+ }
323
+ let parsed = {};
324
+ try {
325
+ parsed = body ? JSON.parse(body) : {};
326
+ }
327
+ catch { }
328
+ const sessionId = generateSessionId();
329
+ const cols = parsed.cols || 120;
330
+ const rows = parsed.rows || 30;
331
+ const isAiSession = parsed.isAiSession || false;
332
+ // Determine shell
333
+ const shellCmd = process.platform === 'win32' ? 'cmd.exe' : (process.env.SHELL || '/bin/bash');
334
+ const shell = spawn(shellCmd, [], {
335
+ cwd: this.options.projectDir,
336
+ env: { ...process.env, TERM: 'xterm-256color', COLUMNS: String(cols), LINES: String(rows) },
337
+ stdio: ['pipe', 'pipe', 'pipe'],
338
+ });
339
+ const session = {
340
+ id: sessionId,
341
+ projectId: this.options.projectId,
342
+ shell,
343
+ outputBuffer: [],
344
+ createdAt: Date.now(),
345
+ lastActivity: Date.now(),
346
+ cols,
347
+ rows,
348
+ isAiSession,
349
+ };
350
+ shell.stdout?.on('data', (data) => {
351
+ session.outputBuffer.push(data.toString('utf-8'));
352
+ session.lastActivity = Date.now();
353
+ // Keep buffer capped at ~100KB
354
+ while (session.outputBuffer.length > 500)
355
+ session.outputBuffer.shift();
356
+ });
357
+ shell.stderr?.on('data', (data) => {
358
+ session.outputBuffer.push(data.toString('utf-8'));
359
+ session.lastActivity = Date.now();
360
+ while (session.outputBuffer.length > 500)
361
+ session.outputBuffer.shift();
362
+ });
363
+ shell.on('exit', () => {
364
+ terminalSessions.delete(sessionId);
365
+ });
366
+ terminalSessions.set(sessionId, session);
367
+ this.json(res, {
368
+ success: true,
369
+ sessionId,
370
+ projectId: this.options.projectId,
371
+ cols,
372
+ rows,
373
+ isAiSession,
374
+ });
375
+ }
376
+ // ─── /live/terminal/write ────────────────────────────────────
377
+ async handleTerminalWrite(_req, res, body) {
378
+ let parsed = {};
379
+ try {
380
+ parsed = JSON.parse(body);
381
+ }
382
+ catch { }
383
+ const { sessionId, data: inputData } = parsed;
384
+ if (!sessionId || !inputData) {
385
+ res.statusCode = 400;
386
+ this.json(res, { success: false, error: 'sessionId and data required' });
387
+ return;
388
+ }
389
+ const session = terminalSessions.get(sessionId);
390
+ if (!session) {
391
+ res.statusCode = 404;
392
+ this.json(res, { success: false, error: 'Session not found' });
393
+ return;
394
+ }
395
+ session.shell.stdin?.write(inputData);
396
+ session.lastActivity = Date.now();
397
+ this.json(res, { success: true });
398
+ }
399
+ // ─── /live/terminal/resize ───────────────────────────────────
400
+ async handleTerminalResize(_req, res, body) {
401
+ let parsed = {};
402
+ try {
403
+ parsed = JSON.parse(body);
404
+ }
405
+ catch { }
406
+ const { sessionId, cols, rows } = parsed;
407
+ if (!sessionId) {
408
+ res.statusCode = 400;
409
+ this.json(res, { success: false, error: 'sessionId required' });
410
+ return;
411
+ }
412
+ const session = terminalSessions.get(sessionId);
413
+ if (!session) {
414
+ res.statusCode = 404;
415
+ this.json(res, { success: false, error: 'Session not found' });
416
+ return;
417
+ }
418
+ session.cols = cols || session.cols;
419
+ session.rows = rows || session.rows;
420
+ // For real pty we'd call pty.resize(cols, rows), but for child_process
421
+ // we just update the env (takes effect on next data)
422
+ this.json(res, { success: true, cols: session.cols, rows: session.rows });
423
+ }
424
+ // ─── /live/agent-status ──────────────────────────────────────
425
+ async handleAgentStatus(_req, res) {
426
+ this.json(res, {
427
+ connected: true,
428
+ projectId: this.options.projectId,
429
+ agent: {
430
+ version: '0.4.1',
431
+ hostname: os.hostname(),
432
+ platform: `${os.platform()} ${os.arch()}`,
433
+ },
434
+ });
435
+ }
289
436
  // ─── /live/container-status ──────────────────────────────────
290
437
  async handleContainerStatus(_req, res, _body, url) {
291
438
  this.json(res, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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": {