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 +18 -4
- package/dist/commands/login.js +50 -1
- package/dist/commands/logout.d.ts +4 -0
- package/dist/commands/logout.js +21 -0
- package/dist/commands/service.d.ts +13 -0
- package/dist/commands/service.js +71 -0
- package/dist/commands/start.js +7 -2
- package/dist/config.js +1 -0
- package/dist/localServer.js +1 -1
- package/dist/tunnel.js +16 -2
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
-
|
|
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,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>;
|
package/dist/commands/service.js
CHANGED
|
@@ -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');
|
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.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
|
-
|
|
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)
|
package/dist/localServer.js
CHANGED
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.
|
|
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: ${
|
|
121
|
+
console.error(` [Tunnel] Error: ${msg}`);
|
|
108
122
|
});
|
|
109
123
|
});
|
|
110
124
|
}
|
package/package.json
CHANGED