nstantpage-agent 0.5.0 → 0.5.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.js CHANGED
@@ -17,20 +17,25 @@
17
17
  import { Command } from 'commander';
18
18
  import chalk from 'chalk';
19
19
  import { loginCommand } from './commands/login.js';
20
+ import { logoutCommand } from './commands/logout.js';
20
21
  import { startCommand } from './commands/start.js';
21
22
  import { statusCommand } from './commands/status.js';
22
- import { serviceInstallCommand, serviceUninstallCommand, serviceStatusCommand } from './commands/service.js';
23
+ import { serviceInstallCommand, serviceUninstallCommand, serviceStatusCommand, serviceStartCommand, serviceStopCommand } from './commands/service.js';
23
24
  const program = new Command();
24
25
  program
25
26
  .name('nstantpage')
26
27
  .description('Local development agent for nstantpage.com — run projects on your machine, preview in the cloud')
27
- .version('0.5.0');
28
+ .version('0.5.2');
28
29
  program
29
30
  .command('login')
30
31
  .description('Authenticate with nstantpage.com')
31
32
  .option('--gateway <url>', 'Gateway URL (auto-detects local vs production)')
32
33
  .option('--force', 'Re-authenticate even if already logged in')
33
34
  .action(loginCommand);
35
+ program
36
+ .command('logout')
37
+ .description('Sign out and clear stored credentials')
38
+ .action(logoutCommand);
34
39
  program
35
40
  .command('start')
36
41
  .description('Start the agent for a project (replaces cloud containers)')
@@ -38,7 +43,7 @@ program
38
43
  .option('-p, --port <port>', 'Local dev server port', '3000')
39
44
  .option('-a, --api-port <port>', 'Local API server port (internal)', '18924')
40
45
  .option('--project-id <id>', 'Link to a specific project ID')
41
- .option('--gateway <url>', 'Gateway URL', 'wss://webprev.live')
46
+ .option('--gateway <url>', 'Gateway URL (default: from login)')
42
47
  .option('--backend <url>', 'Backend API URL (auto-detected from gateway)')
43
48
  .option('--token <token>', 'Auth token (skip login flow)')
44
49
  .option('--dir <path>', 'Project directory override')
@@ -75,9 +80,18 @@ program
75
80
  const service = program
76
81
  .command('service')
77
82
  .description('Manage the background service (keeps your machine connected to nstantpage.com)');
83
+ service
84
+ .command('start')
85
+ .description('Start the agent in standby mode (foreground — keeps your machine online)')
86
+ .option('--gateway <url>', 'Gateway URL', 'wss://webprev.live')
87
+ .action(serviceStartCommand);
88
+ service
89
+ .command('stop')
90
+ .description('Stop the running agent')
91
+ .action(serviceStopCommand);
78
92
  service
79
93
  .command('install')
80
- .description('Install & start the agent as a background service (starts on boot)')
94
+ .description('Install as a background service (auto-starts on boot)')
81
95
  .option('--gateway <url>', 'Gateway URL', 'wss://webprev.live')
82
96
  .action(serviceInstallCommand);
83
97
  service
@@ -16,13 +16,48 @@ function resolveFrontendUrl(gateway) {
16
16
  }
17
17
  return 'https://nstantpage.com';
18
18
  }
19
+ /**
20
+ * Decode a JWT token payload (without verification — just reading claims).
21
+ */
22
+ function decodeJwtPayload(token) {
23
+ try {
24
+ const parts = token.split('.');
25
+ if (parts.length !== 3)
26
+ return null;
27
+ const payload = Buffer.from(parts[1], 'base64').toString('utf-8');
28
+ return JSON.parse(payload);
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /**
35
+ * Extract email from JWT claims.
36
+ * .NET uses long claim type URIs by default.
37
+ */
38
+ function getEmailFromToken(token) {
39
+ const payload = decodeJwtPayload(token);
40
+ if (!payload)
41
+ return null;
42
+ return payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress']
43
+ || payload['email']
44
+ || payload['sub']
45
+ || null;
46
+ }
19
47
  export async function loginCommand(options = {}) {
20
48
  const conf = getConfig();
21
49
  // Check if already logged in (unless --force)
22
50
  const existingToken = conf.get('token');
23
51
  if (existingToken && !options.force) {
52
+ const email = conf.get('email') || getEmailFromToken(existingToken);
53
+ const storedGateway = conf.get('gatewayUrl') || 'wss://webprev.live';
54
+ const isStoredLocal = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(storedGateway);
24
55
  console.log(chalk.green('✓ Already authenticated'));
56
+ if (email)
57
+ console.log(chalk.gray(` Account: ${email}`));
58
+ console.log(chalk.gray(` Server: ${isStoredLocal ? 'localhost (dev)' : 'nstantpage.com'}`));
25
59
  console.log(chalk.gray(' Run "nstantpage login --force" to re-authenticate'));
60
+ console.log(chalk.gray(' Run "nstantpage logout" to sign out.'));
26
61
  return;
27
62
  }
28
63
  const frontendUrl = resolveFrontendUrl(options.gateway);
@@ -78,7 +113,21 @@ export async function loginCommand(options = {}) {
78
113
  }, 5 * 60 * 1000);
79
114
  });
80
115
  conf.set('token', token);
116
+ const email = getEmailFromToken(token);
117
+ if (email)
118
+ conf.set('email', email);
119
+ // Store the gateway URL used for this login so other commands use the matching server
120
+ if (options.gateway) {
121
+ // Convert ws:// to wss:// format for storage (login receives gateway URL)
122
+ conf.set('gatewayUrl', options.gateway);
123
+ }
124
+ else {
125
+ conf.set('gatewayUrl', 'wss://webprev.live');
126
+ }
81
127
  console.log(chalk.green('\n✓ Successfully authenticated!'));
82
- console.log(chalk.gray(' Run "nstantpage start" to connect a project'));
128
+ if (email)
129
+ console.log(chalk.gray(` Account: ${email}`));
130
+ console.log(chalk.gray(` Server: ${isLocal ? 'localhost (dev)' : 'nstantpage.com'}`));
131
+ console.log(chalk.gray(' Run "nstantpage start" to connect your machine'));
83
132
  }
84
133
  //# sourceMappingURL=login.js.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Logout command — clear stored credentials
3
+ */
4
+ export declare function logoutCommand(): Promise<void>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Logout command — clear stored credentials
3
+ */
4
+ import chalk from 'chalk';
5
+ import { getConfig } from '../config.js';
6
+ export async function logoutCommand() {
7
+ const conf = getConfig();
8
+ const email = conf.get('email');
9
+ conf.delete('token');
10
+ conf.delete('email');
11
+ conf.delete('projectId');
12
+ conf.delete('gatewayUrl');
13
+ if (email) {
14
+ console.log(chalk.green(`✓ Logged out (was: ${email})`));
15
+ }
16
+ else {
17
+ console.log(chalk.green('✓ Logged out'));
18
+ }
19
+ console.log(chalk.gray(' Run "nstantpage login" to authenticate again.'));
20
+ }
21
+ //# sourceMappingURL=logout.js.map
@@ -18,6 +18,19 @@
18
18
  interface ServiceInstallOptions {
19
19
  gateway?: string;
20
20
  }
21
+ interface ServiceStartOptions {
22
+ gateway?: string;
23
+ }
24
+ /**
25
+ * `nstantpage service start` — run agent in standby mode (foreground).
26
+ * This keeps your machine online and visible from the web UI.
27
+ * Projects are started via the web when you click "Connect".
28
+ */
29
+ export declare function serviceStartCommand(options?: ServiceStartOptions): Promise<void>;
30
+ /**
31
+ * `nstantpage service stop` — stop any running agent (PID-based or OS service).
32
+ */
33
+ export declare function serviceStopCommand(): Promise<void>;
21
34
  export declare function serviceInstallCommand(options?: ServiceInstallOptions): Promise<void>;
22
35
  export declare function serviceUninstallCommand(): Promise<void>;
23
36
  export declare function serviceStatusCommand(): Promise<void>;
@@ -21,8 +21,79 @@ import path from 'path';
21
21
  import os from 'os';
22
22
  import { execSync } from 'child_process';
23
23
  import { getConfig } from '../config.js';
24
+ import { startCommand } from './start.js';
24
25
  const PLIST_LABEL = 'com.nstantpage.agent';
25
26
  const SYSTEMD_SERVICE = 'nstantpage-agent';
27
+ /**
28
+ * `nstantpage service start` — run agent in standby mode (foreground).
29
+ * This keeps your machine online and visible from the web UI.
30
+ * Projects are started via the web when you click "Connect".
31
+ */
32
+ export async function serviceStartCommand(options = {}) {
33
+ const conf = getConfig();
34
+ const token = conf.get('token');
35
+ if (!token) {
36
+ console.log(chalk.red('✗ Not authenticated. Run "nstantpage login" first.'));
37
+ process.exit(1);
38
+ }
39
+ const email = conf.get('email');
40
+ if (email) {
41
+ console.log(chalk.gray(` Account: ${email}`));
42
+ }
43
+ // Delegate to start command with no project-id → enters standby mode
44
+ // Use stored gateway from login, or explicit --gateway, or default
45
+ const gateway = options.gateway || conf.get('gatewayUrl') || 'wss://webprev.live';
46
+ await startCommand('.', {
47
+ port: '3000',
48
+ apiPort: '18924',
49
+ gateway,
50
+ token,
51
+ });
52
+ }
53
+ /**
54
+ * `nstantpage service stop` — stop any running agent (PID-based or OS service).
55
+ */
56
+ export async function serviceStopCommand() {
57
+ const platform = os.platform();
58
+ let stopped = false;
59
+ // Try stopping OS service first
60
+ if (platform === 'darwin') {
61
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
62
+ if (fs.existsSync(plistPath)) {
63
+ try {
64
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
65
+ console.log(chalk.green(' ✓ Background service stopped'));
66
+ stopped = true;
67
+ }
68
+ catch { }
69
+ }
70
+ }
71
+ else if (platform === 'linux') {
72
+ try {
73
+ execSync(`systemctl --user stop ${SYSTEMD_SERVICE} 2>/dev/null`, { encoding: 'utf-8' });
74
+ console.log(chalk.green(' ✓ Background service stopped'));
75
+ stopped = true;
76
+ }
77
+ catch { }
78
+ }
79
+ // Also try PID-based stop from global config
80
+ const conf = getConfig();
81
+ const pid = conf.get('agentPid');
82
+ if (pid) {
83
+ try {
84
+ process.kill(pid, 'SIGTERM');
85
+ conf.delete('agentPid');
86
+ console.log(chalk.green(' ✓ Agent process stopped'));
87
+ stopped = true;
88
+ }
89
+ catch {
90
+ conf.delete('agentPid');
91
+ }
92
+ }
93
+ if (!stopped) {
94
+ console.log(chalk.yellow(' No running agent found.'));
95
+ }
96
+ }
26
97
  export async function serviceInstallCommand(options = {}) {
27
98
  const conf = getConfig();
28
99
  const token = conf.get('token');
@@ -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.0';
27
+ const VERSION = '0.5.2';
28
28
  /**
29
29
  * Resolve the backend API base URL.
30
30
  * - If --backend is passed, use it
@@ -164,6 +164,10 @@ export async function startCommand(directory, options) {
164
164
  if (options.token) {
165
165
  conf.set('token', options.token);
166
166
  }
167
+ // Resolve gateway URL: explicit flag > stored from login > default
168
+ if (!options.gateway) {
169
+ options.gateway = conf.get('gatewayUrl') || 'wss://webprev.live';
170
+ }
167
171
  // Check authentication
168
172
  const isLocalGateway = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(options.gateway);
169
173
  let token = conf.get('token');
@@ -176,7 +180,8 @@ export async function startCommand(directory, options) {
176
180
  token = 'local-dev';
177
181
  }
178
182
  // Determine project ID (optional — without it, agent enters standby mode)
179
- let projectId = options.projectId || conf.get('projectId');
183
+ // Only use explicitly passed --project-id, never fall back to stored value
184
+ let projectId = options.projectId;
180
185
  const backendUrl = resolveBackendUrl(options);
181
186
  const deviceId = getDeviceId();
182
187
  if (!projectId) {
package/dist/config.js CHANGED
@@ -20,6 +20,7 @@ export function getConfig() {
20
20
  projectName: 'nstantpage-agent',
21
21
  schema: {
22
22
  token: { type: 'string', default: '' },
23
+ email: { type: 'string', default: '' },
23
24
  gatewayUrl: { type: 'string', default: 'wss://webprev.live' },
24
25
  projectId: { type: 'string', default: '' },
25
26
  // Legacy fields (kept for backward compat, but per-project config is preferred)
@@ -502,7 +502,7 @@ export class LocalServer {
502
502
  connected: true,
503
503
  projectId: this.options.projectId,
504
504
  agent: {
505
- version: '0.5.0',
505
+ version: '0.5.2',
506
506
  hostname: os.hostname(),
507
507
  platform: `${os.platform()} ${os.arch()}`,
508
508
  },
package/dist/tunnel.js CHANGED
@@ -18,6 +18,7 @@
18
18
  import WebSocket from 'ws';
19
19
  import http from 'http';
20
20
  import os from 'os';
21
+ import chalk from 'chalk';
21
22
  import { getDeviceId } from './config.js';
22
23
  import { getTerminalSession, attachTerminalClient } from './localServer.js';
23
24
  export class TunnelClient {
@@ -62,7 +63,7 @@ export class TunnelClient {
62
63
  // Send enhanced agent info with capabilities and deviceId
63
64
  this.send({
64
65
  type: 'agent-info',
65
- version: '0.5.0',
66
+ version: '0.5.2',
66
67
  hostname: os.hostname(),
67
68
  platform: `${os.platform()} ${os.arch()}`,
68
69
  deviceId: getDeviceId(),
@@ -101,10 +102,23 @@ export class TunnelClient {
101
102
  });
102
103
  this.ws.on('error', (err) => {
103
104
  clearTimeout(connectTimeout);
105
+ const msg = err.message || '';
106
+ // Detect 401 — token is invalid/expired/wrong-server. Stop retrying.
107
+ if (msg.includes('401')) {
108
+ this.shouldReconnect = false;
109
+ console.error(` [Tunnel] Authentication failed (401)`);
110
+ console.error(chalk.red(` ✗ Your token was rejected by the gateway.`));
111
+ console.error(chalk.gray(` This usually means you need to re-login:`));
112
+ console.error(chalk.gray(` nstantpage logout && nstantpage login`));
113
+ if (this.reconnectAttempts === 0) {
114
+ reject(err);
115
+ }
116
+ return;
117
+ }
104
118
  if (this.reconnectAttempts === 0) {
105
119
  reject(err);
106
120
  }
107
- console.error(` [Tunnel] Error: ${err.message}`);
121
+ console.error(` [Tunnel] Error: ${msg}`);
108
122
  });
109
123
  });
110
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.5.0",
3
+ "version": "0.5.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": {