nstantpage-agent 0.2.0

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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Package Installer — handles npm/pnpm package installation locally.
3
+ * Replaces the container-based /live/install endpoint.
4
+ */
5
+ import { spawn } from 'child_process';
6
+ import path from 'path';
7
+ import { existsSync } from 'fs';
8
+ export class PackageInstaller {
9
+ projectDir;
10
+ constructor(options) {
11
+ this.projectDir = options.projectDir;
12
+ }
13
+ /**
14
+ * Install packages into the project.
15
+ */
16
+ async install(packages, dev = false) {
17
+ const pm = this.detectPackageManager();
18
+ const args = this.buildInstallArgs(pm, packages, dev);
19
+ console.log(` [Installer] ${pm} ${args.join(' ')}`);
20
+ try {
21
+ const output = await this.runCommand(pm, args, 60_000);
22
+ return {
23
+ success: true,
24
+ output,
25
+ installedPackages: packages,
26
+ };
27
+ }
28
+ catch (err) {
29
+ return {
30
+ success: false,
31
+ output: err.message || 'Installation failed',
32
+ installedPackages: [],
33
+ };
34
+ }
35
+ }
36
+ /**
37
+ * Ensure all project dependencies are installed.
38
+ */
39
+ async ensureDependencies() {
40
+ const nodeModules = path.join(this.projectDir, 'node_modules');
41
+ if (existsSync(nodeModules))
42
+ return;
43
+ console.log(` [Installer] Installing project dependencies...`);
44
+ const pm = this.detectPackageManager();
45
+ const args = pm === 'pnpm' ? ['install'] : ['install'];
46
+ await this.runCommand(pm, args, 120_000);
47
+ console.log(` [Installer] Dependencies installed`);
48
+ }
49
+ /**
50
+ * Detect which package manager is used (pnpm, yarn, npm).
51
+ */
52
+ detectPackageManager() {
53
+ if (existsSync(path.join(this.projectDir, 'pnpm-lock.yaml')))
54
+ return 'pnpm';
55
+ if (existsSync(path.join(this.projectDir, 'yarn.lock')))
56
+ return 'yarn';
57
+ return 'npm';
58
+ }
59
+ buildInstallArgs(pm, packages, dev) {
60
+ const devFlag = dev
61
+ ? (pm === 'npm' ? '--save-dev' : '-D')
62
+ : '';
63
+ switch (pm) {
64
+ case 'pnpm':
65
+ return ['add', ...packages, ...(devFlag ? [devFlag] : [])];
66
+ case 'yarn':
67
+ return ['add', ...packages, ...(devFlag ? [devFlag] : [])];
68
+ default:
69
+ return ['install', ...packages, ...(devFlag ? [devFlag] : [])];
70
+ }
71
+ }
72
+ runCommand(cmd, args, timeoutMs) {
73
+ return new Promise((resolve, reject) => {
74
+ const proc = spawn(cmd, args, {
75
+ cwd: this.projectDir,
76
+ env: process.env,
77
+ shell: true,
78
+ stdio: ['pipe', 'pipe', 'pipe'],
79
+ });
80
+ let stdout = '';
81
+ let stderr = '';
82
+ proc.stdout?.on('data', (d) => {
83
+ stdout += d.toString();
84
+ process.stdout.write(` ${d.toString().trimEnd()}\n`);
85
+ });
86
+ proc.stderr?.on('data', (d) => {
87
+ stderr += d.toString();
88
+ // Only show actual errors, not progress bars
89
+ const line = d.toString().trim();
90
+ if (line && !line.includes('progress') && !line.startsWith('⸩')) {
91
+ process.stderr.write(` ${line}\n`);
92
+ }
93
+ });
94
+ proc.on('close', (code) => {
95
+ if (code === 0)
96
+ resolve(stdout);
97
+ else
98
+ reject(new Error(stderr || stdout || `${cmd} exited with code ${code}`));
99
+ });
100
+ proc.on('error', reject);
101
+ setTimeout(() => {
102
+ try {
103
+ proc.kill();
104
+ }
105
+ catch { }
106
+ reject(new Error(`${cmd} timed out after ${timeoutMs / 1000}s`));
107
+ }, timeoutMs);
108
+ });
109
+ }
110
+ }
111
+ //# sourceMappingURL=packageInstaller.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tunnel Client v2 — WebSocket connection to gateway
3
+ *
4
+ * Enhanced to route requests to the correct local handler:
5
+ * - /live/* API requests → Local API server (handles file sync, checks, etc.)
6
+ * - Everything else → Local dev server (Vite, Next.js, etc.)
7
+ *
8
+ * Protocol (JSON messages over WebSocket):
9
+ * Gateway → Agent:
10
+ * { type: "http-request", id, method, url, headers, body? }
11
+ * { type: "ping" }
12
+ * Agent → Gateway:
13
+ * { type: "http-response", id, statusCode, headers, body? }
14
+ * { type: "pong" }
15
+ * { type: "agent-info", version, hostname, platform, capabilities }
16
+ * { type: "error-update", errors }
17
+ */
18
+ interface TunnelClientOptions {
19
+ gatewayUrl: string;
20
+ token: string;
21
+ projectId: string;
22
+ /** Port where the local API server (/live/* handlers) runs */
23
+ apiPort: number;
24
+ /** Port where the dev server (Vite/Next.js) runs */
25
+ devPort: number;
26
+ }
27
+ export declare class TunnelClient {
28
+ private ws;
29
+ private options;
30
+ private reconnectTimer;
31
+ private shouldReconnect;
32
+ private reconnectAttempts;
33
+ private maxReconnectAttempts;
34
+ private pingInterval;
35
+ private requestsForwarded;
36
+ private connectedAt;
37
+ constructor(options: TunnelClientOptions);
38
+ get isConnected(): boolean;
39
+ get stats(): {
40
+ requestsForwarded: number;
41
+ connectedAt: number;
42
+ uptime: number;
43
+ };
44
+ connect(): Promise<void>;
45
+ disconnect(): void;
46
+ /**
47
+ * Push an error update to the gateway (for WebSocket error notifications).
48
+ */
49
+ pushErrorUpdate(errors: {
50
+ buildErrors: string[];
51
+ typeErrors: string[];
52
+ lintErrors: string[];
53
+ runtimeErrors: string[];
54
+ }): void;
55
+ private send;
56
+ private handleMessage;
57
+ /**
58
+ * Route incoming HTTP requests to the correct local server:
59
+ * - /live/* paths → Local API server (file sync, checks, etc.)
60
+ * - /health → Local API server
61
+ * - Everything else → Dev server (Vite HMR, page loads, etc.)
62
+ */
63
+ private handleHttpRequest;
64
+ /**
65
+ * Forward an HTTP request to a local server port and return the response.
66
+ */
67
+ private forwardToLocal;
68
+ private cleanup;
69
+ private scheduleReconnect;
70
+ }
71
+ export {};
package/dist/tunnel.js ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Tunnel Client v2 — WebSocket connection to gateway
3
+ *
4
+ * Enhanced to route requests to the correct local handler:
5
+ * - /live/* API requests → Local API server (handles file sync, checks, etc.)
6
+ * - Everything else → Local dev server (Vite, Next.js, etc.)
7
+ *
8
+ * Protocol (JSON messages over WebSocket):
9
+ * Gateway → Agent:
10
+ * { type: "http-request", id, method, url, headers, body? }
11
+ * { type: "ping" }
12
+ * Agent → Gateway:
13
+ * { type: "http-response", id, statusCode, headers, body? }
14
+ * { type: "pong" }
15
+ * { type: "agent-info", version, hostname, platform, capabilities }
16
+ * { type: "error-update", errors }
17
+ */
18
+ import WebSocket from 'ws';
19
+ import http from 'http';
20
+ import os from 'os';
21
+ export class TunnelClient {
22
+ ws = null;
23
+ options;
24
+ reconnectTimer = null;
25
+ shouldReconnect = true;
26
+ reconnectAttempts = 0;
27
+ maxReconnectAttempts = 50;
28
+ pingInterval = null;
29
+ requestsForwarded = 0;
30
+ connectedAt = 0;
31
+ constructor(options) {
32
+ this.options = options;
33
+ }
34
+ get isConnected() {
35
+ return this.ws?.readyState === WebSocket.OPEN;
36
+ }
37
+ get stats() {
38
+ return {
39
+ requestsForwarded: this.requestsForwarded,
40
+ connectedAt: this.connectedAt,
41
+ uptime: this.connectedAt ? Date.now() - this.connectedAt : 0,
42
+ };
43
+ }
44
+ async connect() {
45
+ return new Promise((resolve, reject) => {
46
+ const url = `${this.options.gatewayUrl}/tunnel/connect?token=${encodeURIComponent(this.options.token)}&projectId=${encodeURIComponent(this.options.projectId)}`;
47
+ this.ws = new WebSocket(url);
48
+ const connectTimeout = setTimeout(() => {
49
+ if (this.ws?.readyState !== WebSocket.OPEN) {
50
+ this.ws?.terminate();
51
+ reject(new Error('Connection timed out'));
52
+ }
53
+ }, 15_000);
54
+ this.ws.on('open', () => {
55
+ clearTimeout(connectTimeout);
56
+ this.reconnectAttempts = 0;
57
+ this.connectedAt = Date.now();
58
+ // Send enhanced agent info with capabilities
59
+ this.send({
60
+ type: 'agent-info',
61
+ version: '0.2.0',
62
+ hostname: os.hostname(),
63
+ platform: `${os.platform()} ${os.arch()}`,
64
+ capabilities: [
65
+ 'file-sync',
66
+ 'type-check',
67
+ 'install',
68
+ 'terminal',
69
+ 'dev-server',
70
+ 'hard-patterns',
71
+ 'normalize',
72
+ 'full-api',
73
+ ],
74
+ apiPort: this.options.apiPort,
75
+ devPort: this.options.devPort,
76
+ });
77
+ // Start keepalive ping
78
+ this.pingInterval = setInterval(() => {
79
+ this.send({ type: 'pong' });
80
+ }, 30_000);
81
+ resolve();
82
+ });
83
+ this.ws.on('message', (data) => {
84
+ try {
85
+ const msg = JSON.parse(data.toString());
86
+ this.handleMessage(msg);
87
+ }
88
+ catch (err) {
89
+ console.error(' [Tunnel] Invalid message:', err);
90
+ }
91
+ });
92
+ this.ws.on('close', (code, reason) => {
93
+ console.log(` [Tunnel] Disconnected (${code}: ${reason || 'no reason'})`);
94
+ this.cleanup();
95
+ this.scheduleReconnect();
96
+ });
97
+ this.ws.on('error', (err) => {
98
+ clearTimeout(connectTimeout);
99
+ if (this.reconnectAttempts === 0) {
100
+ reject(err);
101
+ }
102
+ console.error(` [Tunnel] Error: ${err.message}`);
103
+ });
104
+ });
105
+ }
106
+ disconnect() {
107
+ this.shouldReconnect = false;
108
+ this.cleanup();
109
+ if (this.ws) {
110
+ this.ws.close(1000, 'Agent shutting down');
111
+ this.ws = null;
112
+ }
113
+ }
114
+ /**
115
+ * Push an error update to the gateway (for WebSocket error notifications).
116
+ */
117
+ pushErrorUpdate(errors) {
118
+ this.send({
119
+ type: 'error-update',
120
+ projectId: this.options.projectId,
121
+ errors,
122
+ });
123
+ }
124
+ send(msg) {
125
+ if (this.ws?.readyState === WebSocket.OPEN) {
126
+ this.ws.send(JSON.stringify(msg));
127
+ }
128
+ }
129
+ handleMessage(msg) {
130
+ switch (msg.type) {
131
+ case 'ping':
132
+ this.send({ type: 'pong' });
133
+ break;
134
+ case 'http-request':
135
+ this.handleHttpRequest(msg);
136
+ break;
137
+ default:
138
+ console.warn(` [Tunnel] Unknown message type: ${msg.type}`);
139
+ }
140
+ }
141
+ /**
142
+ * Route incoming HTTP requests to the correct local server:
143
+ * - /live/* paths → Local API server (file sync, checks, etc.)
144
+ * - /health → Local API server
145
+ * - Everything else → Dev server (Vite HMR, page loads, etc.)
146
+ */
147
+ handleHttpRequest(request) {
148
+ const { url } = request;
149
+ const isApiRequest = url.startsWith('/live/') || url === '/health';
150
+ const targetPort = isApiRequest ? this.options.apiPort : this.options.devPort;
151
+ this.forwardToLocal(request, targetPort);
152
+ this.requestsForwarded++;
153
+ }
154
+ /**
155
+ * Forward an HTTP request to a local server port and return the response.
156
+ */
157
+ forwardToLocal(request, port) {
158
+ const { id, method, url, headers, body } = request;
159
+ const reqOptions = {
160
+ hostname: '127.0.0.1',
161
+ port,
162
+ path: url,
163
+ method,
164
+ headers: {
165
+ ...headers,
166
+ host: `127.0.0.1:${port}`,
167
+ 'x-forwarded-host': headers.host || '',
168
+ 'x-tunnel-request-id': id,
169
+ },
170
+ };
171
+ const proxyReq = http.request(reqOptions, (proxyRes) => {
172
+ const chunks = [];
173
+ proxyRes.on('data', (chunk) => chunks.push(chunk));
174
+ proxyRes.on('end', () => {
175
+ const responseBody = Buffer.concat(chunks);
176
+ this.send({
177
+ type: 'http-response',
178
+ id,
179
+ statusCode: proxyRes.statusCode || 200,
180
+ headers: proxyRes.headers,
181
+ body: responseBody.length > 0 ? responseBody.toString('base64') : undefined,
182
+ });
183
+ });
184
+ });
185
+ proxyReq.on('error', (err) => {
186
+ console.error(` [Tunnel] Local proxy error for ${method} ${url} (port ${port}): ${err.message}`);
187
+ this.send({
188
+ type: 'http-response',
189
+ id,
190
+ statusCode: 502,
191
+ headers: { 'content-type': 'application/json' },
192
+ body: Buffer.from(JSON.stringify({
193
+ error: `Local server error: ${err.message}`,
194
+ agentMode: true,
195
+ })).toString('base64'),
196
+ });
197
+ });
198
+ proxyReq.setTimeout(60_000, () => {
199
+ proxyReq.destroy();
200
+ this.send({
201
+ type: 'http-response',
202
+ id,
203
+ statusCode: 504,
204
+ headers: { 'content-type': 'application/json' },
205
+ body: Buffer.from(JSON.stringify({
206
+ error: 'Local server timeout',
207
+ agentMode: true,
208
+ })).toString('base64'),
209
+ });
210
+ });
211
+ if (body) {
212
+ proxyReq.write(Buffer.from(body, 'base64'));
213
+ }
214
+ proxyReq.end();
215
+ }
216
+ cleanup() {
217
+ if (this.reconnectTimer) {
218
+ clearTimeout(this.reconnectTimer);
219
+ this.reconnectTimer = null;
220
+ }
221
+ if (this.pingInterval) {
222
+ clearInterval(this.pingInterval);
223
+ this.pingInterval = null;
224
+ }
225
+ }
226
+ scheduleReconnect() {
227
+ if (!this.shouldReconnect)
228
+ return;
229
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
230
+ console.error(' [Tunnel] Max reconnect attempts reached. Giving up.');
231
+ return;
232
+ }
233
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30_000);
234
+ this.reconnectAttempts++;
235
+ console.log(` [Tunnel] Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})...`);
236
+ this.reconnectTimer = setTimeout(async () => {
237
+ try {
238
+ await this.connect();
239
+ console.log(' [Tunnel] Reconnected!');
240
+ }
241
+ catch {
242
+ // Will retry via close handler
243
+ }
244
+ }, delay);
245
+ }
246
+ }
247
+ //# sourceMappingURL=tunnel.js.map
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "nstantpage-agent",
3
+ "version": "0.2.0",
4
+ "description": "Local development agent for nstantpage.com — run your projects locally, preview in the cloud. Replaces cloud containers for faster builds.",
5
+ "type": "module",
6
+ "bin": {
7
+ "nstantpage": "./dist/cli.js",
8
+ "nstantpage-agent": "./dist/cli.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "files": [
12
+ "dist/**/*.js",
13
+ "dist/**/*.d.ts",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsx src/cli.ts",
19
+ "start": "node dist/cli.js",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/nstantpage/nstantpage-agent"
28
+ },
29
+ "homepage": "https://nstantpage.com",
30
+ "bugs": {
31
+ "url": "https://github.com/nstantpage/nstantpage-agent/issues"
32
+ },
33
+ "keywords": [
34
+ "nstantpage",
35
+ "preview",
36
+ "development",
37
+ "agent",
38
+ "tunnel",
39
+ "local-dev",
40
+ "dev-server",
41
+ "vite",
42
+ "nextjs"
43
+ ],
44
+ "license": "MIT",
45
+ "dependencies": {
46
+ "chalk": "^5.3.0",
47
+ "commander": "^12.1.0",
48
+ "conf": "^13.0.1",
49
+ "express": "^4.21.0",
50
+ "http-proxy": "^1.18.1",
51
+ "open": "^10.1.0",
52
+ "ws": "^8.18.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/express": "^4.17.21",
56
+ "@types/http-proxy": "^1.17.14",
57
+ "@types/node": "^20.14.0",
58
+ "@types/ws": "^8.5.10",
59
+ "tsx": "^4.19.0",
60
+ "typescript": "^5.5.0"
61
+ }
62
+ }