nstantpage-agent 0.3.4 → 0.4.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.
- package/dist/commands/start.d.ts +5 -5
- package/dist/commands/start.js +132 -28
- package/dist/config.d.ts +35 -0
- package/dist/config.js +88 -0
- package/dist/localServer.js +28 -13
- package/dist/tunnel.js +4 -2
- package/package.json +1 -1
package/dist/commands/start.d.ts
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* 4. Start local API server (handles /live/* requests for file sync, checks, etc.)
|
|
9
9
|
* 5. Start local dev server (Vite/Next.js — runs project on user's machine)
|
|
10
10
|
* 6. Connect tunnel to gateway (relays requests from cloud to local machine)
|
|
11
|
+
* 7. Register device with backend (track connected devices)
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* - Package installation uses local npm/pnpm
|
|
13
|
+
* Multi-project support:
|
|
14
|
+
* - Each project gets its own PID file and port allocation
|
|
15
|
+
* - Cleanup only kills the agent for the SAME project
|
|
16
|
+
* - Ports are auto-assigned per project (based on hash) to avoid collisions
|
|
17
17
|
*/
|
|
18
18
|
interface StartOptions {
|
|
19
19
|
port: string;
|
package/dist/commands/start.js
CHANGED
|
@@ -8,23 +8,23 @@
|
|
|
8
8
|
* 4. Start local API server (handles /live/* requests for file sync, checks, etc.)
|
|
9
9
|
* 5. Start local dev server (Vite/Next.js — runs project on user's machine)
|
|
10
10
|
* 6. Connect tunnel to gateway (relays requests from cloud to local machine)
|
|
11
|
+
* 7. Register device with backend (track connected devices)
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* - Package installation uses local npm/pnpm
|
|
13
|
+
* Multi-project support:
|
|
14
|
+
* - Each project gets its own PID file and port allocation
|
|
15
|
+
* - Cleanup only kills the agent for the SAME project
|
|
16
|
+
* - Ports are auto-assigned per project (based on hash) to avoid collisions
|
|
17
17
|
*/
|
|
18
18
|
import chalk from 'chalk';
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import fs from 'fs';
|
|
21
21
|
import os from 'os';
|
|
22
22
|
import { execSync } from 'child_process';
|
|
23
|
-
import { getConfig } from '../config.js';
|
|
23
|
+
import { getConfig, getProjectConfig, setProjectConfig, clearProjectConfig, getDeviceId, allocatePortsForProject } from '../config.js';
|
|
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.
|
|
27
|
+
const VERSION = '0.4.0';
|
|
28
28
|
/**
|
|
29
29
|
* Resolve the backend API base URL.
|
|
30
30
|
* - If --backend is passed, use it
|
|
@@ -127,24 +127,36 @@ function killPort(port) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
-
* Clean up any previous agent instance
|
|
130
|
+
* Clean up any previous agent instance for THIS PROJECT ONLY.
|
|
131
|
+
* Uses per-project config instead of global config so multiple projects can coexist.
|
|
131
132
|
*/
|
|
132
|
-
function cleanupPreviousAgent(
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
function cleanupPreviousAgent(projectId, apiPort, devPort) {
|
|
134
|
+
const projectConf = getProjectConfig(projectId);
|
|
135
|
+
// 1. Try to kill the previously stored agent PID for this project
|
|
136
|
+
const oldPid = projectConf.pid;
|
|
135
137
|
if (oldPid && oldPid !== process.pid) {
|
|
136
138
|
try {
|
|
137
139
|
process.kill(oldPid, 'SIGTERM');
|
|
138
|
-
console.log(chalk.gray(` Stopped previous agent (PID ${oldPid})`));
|
|
140
|
+
console.log(chalk.gray(` Stopped previous agent for project ${projectId} (PID ${oldPid})`));
|
|
139
141
|
}
|
|
140
142
|
catch {
|
|
141
143
|
// Already dead
|
|
142
144
|
}
|
|
143
|
-
conf.delete('agentPid');
|
|
144
145
|
}
|
|
145
|
-
// 2. Free the ports
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
// 2. Free the ports that THIS project was using
|
|
147
|
+
const oldApiPort = projectConf.apiPort;
|
|
148
|
+
const oldDevPort = projectConf.devPort;
|
|
149
|
+
if (oldApiPort)
|
|
150
|
+
killPort(oldApiPort);
|
|
151
|
+
if (oldDevPort)
|
|
152
|
+
killPort(oldDevPort);
|
|
153
|
+
// 3. Also free the requested ports (in case they changed)
|
|
154
|
+
if (apiPort !== oldApiPort)
|
|
155
|
+
killPort(apiPort);
|
|
156
|
+
if (devPort !== oldDevPort)
|
|
157
|
+
killPort(devPort);
|
|
158
|
+
// 4. Clear stale project config
|
|
159
|
+
clearProjectConfig(projectId);
|
|
148
160
|
}
|
|
149
161
|
export async function startCommand(directory, options) {
|
|
150
162
|
const conf = getConfig();
|
|
@@ -153,7 +165,6 @@ export async function startCommand(directory, options) {
|
|
|
153
165
|
conf.set('token', options.token);
|
|
154
166
|
}
|
|
155
167
|
// Check authentication
|
|
156
|
-
// Allow unauthenticated connections to local gateways (ws://localhost, ws://127.0.0.1)
|
|
157
168
|
const isLocalGateway = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(options.gateway);
|
|
158
169
|
let token = conf.get('token');
|
|
159
170
|
if (!token && !isLocalGateway) {
|
|
@@ -164,8 +175,6 @@ export async function startCommand(directory, options) {
|
|
|
164
175
|
if (!token && isLocalGateway) {
|
|
165
176
|
token = 'local-dev';
|
|
166
177
|
}
|
|
167
|
-
const devPort = parseInt(options.port, 10);
|
|
168
|
-
const apiPort = parseInt(options.apiPort, 10);
|
|
169
178
|
// Determine project ID
|
|
170
179
|
let projectId = options.projectId || conf.get('projectId');
|
|
171
180
|
if (!projectId) {
|
|
@@ -174,6 +183,22 @@ export async function startCommand(directory, options) {
|
|
|
174
183
|
console.log(chalk.gray(' Example: npx nstantpage-agent start --project-id 1234'));
|
|
175
184
|
process.exit(1);
|
|
176
185
|
}
|
|
186
|
+
// Auto-assign ports per project (unless user explicitly specified them)
|
|
187
|
+
const userSpecifiedPort = options.port !== '3000';
|
|
188
|
+
const userSpecifiedApiPort = options.apiPort !== '18924';
|
|
189
|
+
let devPort;
|
|
190
|
+
let apiPort;
|
|
191
|
+
if (userSpecifiedPort || userSpecifiedApiPort) {
|
|
192
|
+
// User explicitly chose ports — respect their choice
|
|
193
|
+
devPort = parseInt(options.port, 10);
|
|
194
|
+
apiPort = parseInt(options.apiPort, 10);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Auto-allocate unique ports per project
|
|
198
|
+
const allocated = allocatePortsForProject(projectId);
|
|
199
|
+
devPort = allocated.devPort;
|
|
200
|
+
apiPort = allocated.apiPort;
|
|
201
|
+
}
|
|
177
202
|
// Resolve project directory
|
|
178
203
|
const projectDir = resolveProjectDir(directory, projectId, options.dir);
|
|
179
204
|
// Create directory if it doesn't exist
|
|
@@ -183,12 +208,14 @@ export async function startCommand(directory, options) {
|
|
|
183
208
|
// Save project ID
|
|
184
209
|
conf.set('projectId', projectId);
|
|
185
210
|
const backendUrl = resolveBackendUrl(options);
|
|
186
|
-
|
|
187
|
-
|
|
211
|
+
const deviceId = getDeviceId();
|
|
212
|
+
// Kill any leftover agent for THIS PROJECT only (not other projects)
|
|
213
|
+
cleanupPreviousAgent(projectId, apiPort, devPort);
|
|
188
214
|
// Small delay to let ports release
|
|
189
215
|
await new Promise(r => setTimeout(r, 300));
|
|
190
216
|
console.log(chalk.blue(`\n🚀 nstantpage agent v${VERSION}\n`));
|
|
191
217
|
console.log(chalk.gray(` Project ID: ${projectId}`));
|
|
218
|
+
console.log(chalk.gray(` Device ID: ${deviceId.slice(0, 12)}...`));
|
|
192
219
|
console.log(chalk.gray(` Directory: ${projectDir}`));
|
|
193
220
|
console.log(chalk.gray(` Dev server: port ${devPort}`));
|
|
194
221
|
console.log(chalk.gray(` API server: port ${apiPort}`));
|
|
@@ -219,7 +246,6 @@ export async function startCommand(directory, options) {
|
|
|
219
246
|
catch (err) {
|
|
220
247
|
console.log(chalk.yellow(` ⚠ Could not fetch project files: ${err.message}`));
|
|
221
248
|
console.log(chalk.gray(' Continuing with existing local files (if any)...'));
|
|
222
|
-
// Don't exit — maybe local files exist from a previous run
|
|
223
249
|
if (!fs.existsSync(path.join(projectDir, 'package.json'))) {
|
|
224
250
|
console.log(chalk.red(`\n✗ No package.json found and cannot fetch files from backend.`));
|
|
225
251
|
console.log(chalk.gray(' Check your project ID and authentication.'));
|
|
@@ -246,18 +272,94 @@ export async function startCommand(directory, options) {
|
|
|
246
272
|
apiPort,
|
|
247
273
|
devPort,
|
|
248
274
|
});
|
|
275
|
+
// 5. Register device with backend
|
|
276
|
+
let heartbeatInterval = null;
|
|
277
|
+
const registerDevice = async () => {
|
|
278
|
+
try {
|
|
279
|
+
const res = await fetch(`${backendUrl}/api/agent/register`, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: {
|
|
282
|
+
'Authorization': `Bearer ${token}`,
|
|
283
|
+
'Content-Type': 'application/json',
|
|
284
|
+
},
|
|
285
|
+
body: JSON.stringify({
|
|
286
|
+
deviceId,
|
|
287
|
+
name: os.hostname(),
|
|
288
|
+
hostname: os.hostname(),
|
|
289
|
+
platform: `${os.platform()} ${os.arch()}`,
|
|
290
|
+
agentVersion: VERSION,
|
|
291
|
+
projectId: projectId,
|
|
292
|
+
capabilities: ['file-sync', 'type-check', 'install', 'terminal', 'dev-server'],
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
if (res.ok) {
|
|
296
|
+
console.log(chalk.green(` ✓ Device registered with backend`));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
console.log(chalk.gray(` ⚠ Device registration: ${res.status} (non-fatal)`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
console.log(chalk.gray(` ⚠ Could not register device: ${err.message} (non-fatal)`));
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const startHeartbeat = () => {
|
|
307
|
+
heartbeatInterval = setInterval(async () => {
|
|
308
|
+
try {
|
|
309
|
+
await fetch(`${backendUrl}/api/agent/heartbeat`, {
|
|
310
|
+
method: 'POST',
|
|
311
|
+
headers: {
|
|
312
|
+
'Authorization': `Bearer ${token}`,
|
|
313
|
+
'Content-Type': 'application/json',
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
deviceId,
|
|
317
|
+
activeProjectIds: [projectId],
|
|
318
|
+
agentVersion: VERSION,
|
|
319
|
+
}),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Heartbeat failure is non-fatal
|
|
324
|
+
}
|
|
325
|
+
}, 60_000); // Every 60s
|
|
326
|
+
};
|
|
327
|
+
const disconnectDevice = async () => {
|
|
328
|
+
try {
|
|
329
|
+
await fetch(`${backendUrl}/api/agent/disconnect`, {
|
|
330
|
+
method: 'POST',
|
|
331
|
+
headers: {
|
|
332
|
+
'Authorization': `Bearer ${token}`,
|
|
333
|
+
'Content-Type': 'application/json',
|
|
334
|
+
},
|
|
335
|
+
body: JSON.stringify({ deviceId, projectId }),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Best-effort
|
|
340
|
+
}
|
|
341
|
+
};
|
|
249
342
|
// Handle graceful shutdown
|
|
250
343
|
const shutdown = async () => {
|
|
251
344
|
console.log(chalk.yellow('\n\n Shutting down...\n'));
|
|
345
|
+
if (heartbeatInterval)
|
|
346
|
+
clearInterval(heartbeatInterval);
|
|
252
347
|
tunnel.disconnect();
|
|
253
348
|
await localServer.stop();
|
|
254
|
-
|
|
349
|
+
await disconnectDevice();
|
|
350
|
+
clearProjectConfig(projectId);
|
|
255
351
|
process.exit(0);
|
|
256
352
|
};
|
|
257
353
|
process.on('SIGTERM', shutdown);
|
|
258
354
|
process.on('SIGINT', shutdown);
|
|
259
|
-
// Store PID
|
|
260
|
-
|
|
355
|
+
// Store per-project PID and ports
|
|
356
|
+
setProjectConfig(projectId, {
|
|
357
|
+
pid: process.pid,
|
|
358
|
+
apiPort,
|
|
359
|
+
devPort,
|
|
360
|
+
startedAt: new Date().toISOString(),
|
|
361
|
+
deviceId,
|
|
362
|
+
});
|
|
261
363
|
try {
|
|
262
364
|
// Start local API server
|
|
263
365
|
console.log(chalk.gray(' Starting local API server...'));
|
|
@@ -279,7 +381,9 @@ export async function startCommand(directory, options) {
|
|
|
279
381
|
else {
|
|
280
382
|
console.log(chalk.gray(' Dev server skipped (--no-dev)'));
|
|
281
383
|
}
|
|
282
|
-
//
|
|
384
|
+
// Register device (non-blocking)
|
|
385
|
+
registerDevice().then(() => startHeartbeat());
|
|
386
|
+
// Connect tunnel to gateway (non-fatal)
|
|
283
387
|
console.log(chalk.gray(' Connecting to gateway...'));
|
|
284
388
|
let tunnelConnected = false;
|
|
285
389
|
try {
|
|
@@ -291,7 +395,6 @@ export async function startCommand(directory, options) {
|
|
|
291
395
|
console.log(chalk.yellow(` ⚠ Tunnel connection failed: ${err.message || 'connection refused'}`));
|
|
292
396
|
console.log(chalk.gray(' Local dev server is still running. Tunnel will retry in background.'));
|
|
293
397
|
console.log(chalk.gray(` Is the gateway running at ${options.gateway}?\n`));
|
|
294
|
-
// Start background reconnection
|
|
295
398
|
tunnel.startBackgroundReconnect();
|
|
296
399
|
}
|
|
297
400
|
// Display status
|
|
@@ -308,6 +411,7 @@ export async function startCommand(directory, options) {
|
|
|
308
411
|
console.log(chalk.white(` │ Files: ${projectDir}`));
|
|
309
412
|
console.log(chalk.blue.bold(` └──────────────────────────────────────────────┘\n`));
|
|
310
413
|
console.log(chalk.gray(` Mode: ${chalk.green('Agent')} (no containers needed)`));
|
|
414
|
+
console.log(chalk.gray(` Device: ${os.hostname()} (${os.platform()} ${os.arch()})`));
|
|
311
415
|
console.log(chalk.gray(` All builds, checks, and previews run on this machine.`));
|
|
312
416
|
console.log(chalk.gray(` Press Ctrl+C to stop\n`));
|
|
313
417
|
// Keep alive
|
|
@@ -316,7 +420,7 @@ export async function startCommand(directory, options) {
|
|
|
316
420
|
catch (err) {
|
|
317
421
|
console.error(chalk.red(`\n✗ Failed to start: ${err.message}`));
|
|
318
422
|
await localServer.stop();
|
|
319
|
-
|
|
423
|
+
clearProjectConfig(projectId);
|
|
320
424
|
process.exit(1);
|
|
321
425
|
}
|
|
322
426
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent configuration store (persisted to disk)
|
|
3
|
+
*
|
|
4
|
+
* Two-tier config:
|
|
5
|
+
* 1. Global config: ~/.config/nstantpage-agent/config.json — shared settings (token, gateway)
|
|
6
|
+
* 2. Per-project config: ~/.nstantpage/projects/{projectId}/agent.json — PID, ports, etc.
|
|
7
|
+
*
|
|
8
|
+
* This allows running multiple agent instances (one per project) without conflicts.
|
|
3
9
|
*/
|
|
4
10
|
import Conf from 'conf';
|
|
5
11
|
export declare function getConfig(): Conf;
|
|
12
|
+
export interface ProjectConfig {
|
|
13
|
+
pid?: number;
|
|
14
|
+
apiPort?: number;
|
|
15
|
+
devPort?: number;
|
|
16
|
+
startedAt?: string;
|
|
17
|
+
deviceId?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function getProjectConfig(projectId: string): ProjectConfig;
|
|
20
|
+
export declare function setProjectConfig(projectId: string, config: ProjectConfig): void;
|
|
21
|
+
export declare function clearProjectConfig(projectId: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Generate a stable device ID for this machine.
|
|
24
|
+
* Based on hostname + username + platform → SHA256 hash.
|
|
25
|
+
* This allows the backend to recognize the same physical machine across restarts.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getDeviceId(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Allocate a unique port range for a project.
|
|
30
|
+
* Uses the projectId to deterministically pick ports, avoiding conflicts
|
|
31
|
+
* when running multiple agents on the same machine.
|
|
32
|
+
*
|
|
33
|
+
* Port ranges:
|
|
34
|
+
* API port: 18900 + (hash % 1000) → range 18900-19899
|
|
35
|
+
* Dev port: 3000 + (hash % 1000) → range 3000-3999
|
|
36
|
+
*/
|
|
37
|
+
export declare function allocatePortsForProject(projectId: string): {
|
|
38
|
+
apiPort: number;
|
|
39
|
+
devPort: number;
|
|
40
|
+
};
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent configuration store (persisted to disk)
|
|
3
|
+
*
|
|
4
|
+
* Two-tier config:
|
|
5
|
+
* 1. Global config: ~/.config/nstantpage-agent/config.json — shared settings (token, gateway)
|
|
6
|
+
* 2. Per-project config: ~/.nstantpage/projects/{projectId}/agent.json — PID, ports, etc.
|
|
7
|
+
*
|
|
8
|
+
* This allows running multiple agent instances (one per project) without conflicts.
|
|
3
9
|
*/
|
|
4
10
|
import Conf from 'conf';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
// ── Global config (shared across all projects) ──────────────
|
|
5
16
|
let _conf = null;
|
|
6
17
|
export function getConfig() {
|
|
7
18
|
if (!_conf) {
|
|
@@ -11,13 +22,90 @@ export function getConfig() {
|
|
|
11
22
|
token: { type: 'string', default: '' },
|
|
12
23
|
gatewayUrl: { type: 'string', default: 'wss://webprev.live' },
|
|
13
24
|
projectId: { type: 'string', default: '' },
|
|
25
|
+
// Legacy fields (kept for backward compat, but per-project config is preferred)
|
|
14
26
|
agentPid: { type: 'number' },
|
|
15
27
|
lastConnected: { type: 'string' },
|
|
16
28
|
devPort: { type: 'number', default: 3000 },
|
|
17
29
|
apiPort: { type: 'number', default: 18924 },
|
|
30
|
+
deviceId: { type: 'string', default: '' },
|
|
18
31
|
},
|
|
19
32
|
});
|
|
20
33
|
}
|
|
21
34
|
return _conf;
|
|
22
35
|
}
|
|
36
|
+
function projectConfigPath(projectId) {
|
|
37
|
+
return path.join(os.homedir(), '.nstantpage', 'projects', projectId, 'agent.json');
|
|
38
|
+
}
|
|
39
|
+
export function getProjectConfig(projectId) {
|
|
40
|
+
const configPath = projectConfigPath(projectId);
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(configPath)) {
|
|
43
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
export function setProjectConfig(projectId, config) {
|
|
50
|
+
const configPath = projectConfigPath(projectId);
|
|
51
|
+
const dir = path.dirname(configPath);
|
|
52
|
+
if (!fs.existsSync(dir)) {
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
export function clearProjectConfig(projectId) {
|
|
58
|
+
const configPath = projectConfigPath(projectId);
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(configPath)) {
|
|
61
|
+
fs.unlinkSync(configPath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
}
|
|
66
|
+
// ── Device ID (stable per machine) ──────────────────────────
|
|
67
|
+
let _deviceId = null;
|
|
68
|
+
/**
|
|
69
|
+
* Generate a stable device ID for this machine.
|
|
70
|
+
* Based on hostname + username + platform → SHA256 hash.
|
|
71
|
+
* This allows the backend to recognize the same physical machine across restarts.
|
|
72
|
+
*/
|
|
73
|
+
export function getDeviceId() {
|
|
74
|
+
if (_deviceId)
|
|
75
|
+
return _deviceId;
|
|
76
|
+
// Check if we already stored a deviceId
|
|
77
|
+
const conf = getConfig();
|
|
78
|
+
const stored = conf.get('deviceId');
|
|
79
|
+
if (stored) {
|
|
80
|
+
_deviceId = stored;
|
|
81
|
+
return stored;
|
|
82
|
+
}
|
|
83
|
+
// Generate new stable ID
|
|
84
|
+
const raw = `${os.hostname()}|${os.userInfo().username}|${os.platform()}|${os.arch()}`;
|
|
85
|
+
_deviceId = crypto.createHash('sha256').update(raw).digest('hex').slice(0, 32);
|
|
86
|
+
conf.set('deviceId', _deviceId);
|
|
87
|
+
return _deviceId;
|
|
88
|
+
}
|
|
89
|
+
// ── Port allocation ─────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Allocate a unique port range for a project.
|
|
92
|
+
* Uses the projectId to deterministically pick ports, avoiding conflicts
|
|
93
|
+
* when running multiple agents on the same machine.
|
|
94
|
+
*
|
|
95
|
+
* Port ranges:
|
|
96
|
+
* API port: 18900 + (hash % 1000) → range 18900-19899
|
|
97
|
+
* Dev port: 3000 + (hash % 1000) → range 3000-3999
|
|
98
|
+
*/
|
|
99
|
+
export function allocatePortsForProject(projectId) {
|
|
100
|
+
// Simple hash of projectId for port offset
|
|
101
|
+
let hash = 0;
|
|
102
|
+
for (let i = 0; i < projectId.length; i++) {
|
|
103
|
+
hash = ((hash << 5) - hash + projectId.charCodeAt(i)) | 0;
|
|
104
|
+
}
|
|
105
|
+
const offset = Math.abs(hash) % 1000;
|
|
106
|
+
return {
|
|
107
|
+
apiPort: 18900 + offset,
|
|
108
|
+
devPort: 3000 + offset,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
23
111
|
//# sourceMappingURL=config.js.map
|
package/dist/localServer.js
CHANGED
|
@@ -290,6 +290,8 @@ export class LocalServer {
|
|
|
290
290
|
async handleContainerStatus(_req, res, _body, url) {
|
|
291
291
|
this.json(res, {
|
|
292
292
|
running: this.devServer.isRunning,
|
|
293
|
+
status: this.devServer.isRunning ? 'running' : 'stopped',
|
|
294
|
+
projectId: this.options.projectId,
|
|
293
295
|
mode: 'agent',
|
|
294
296
|
agentMode: true,
|
|
295
297
|
hostname: os.hostname(),
|
|
@@ -298,32 +300,45 @@ export class LocalServer {
|
|
|
298
300
|
}
|
|
299
301
|
// ─── /live/container-stats ───────────────────────────────────
|
|
300
302
|
async handleContainerStats(_req, res) {
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const
|
|
303
|
+
const devStats = this.devServer.getStats();
|
|
304
|
+
const totalMemMb = Math.round(os.totalmem() / (1024 * 1024));
|
|
305
|
+
const freeMemMb = Math.round(os.freemem() / (1024 * 1024));
|
|
306
|
+
const usedMemMb = totalMemMb - freeMemMb;
|
|
307
|
+
const memPercent = totalMemMb > 0 ? Math.round((usedMemMb / totalMemMb) * 100) : 0;
|
|
308
|
+
// Return in the same format as the container stats (ContainerStatsSnapshot)
|
|
309
|
+
// so the frontend terminal panel can display it unchanged.
|
|
304
310
|
this.json(res, {
|
|
305
311
|
success: true,
|
|
312
|
+
running: this.devServer.isRunning,
|
|
306
313
|
agentMode: true,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
314
|
+
stats: {
|
|
315
|
+
cpuPercent: devStats.cpuPercent,
|
|
316
|
+
memoryUsageBytes: usedMemMb * 1024 * 1024,
|
|
317
|
+
memoryLimitBytes: totalMemMb * 1024 * 1024,
|
|
318
|
+
memoryUsageMb: usedMemMb,
|
|
319
|
+
memoryLimitMb: totalMemMb,
|
|
320
|
+
memoryPercent: memPercent,
|
|
321
|
+
diskUsageMb: null,
|
|
322
|
+
diskUsageBytes: null,
|
|
323
|
+
},
|
|
324
|
+
agentInfo: {
|
|
325
|
+
hostname: os.hostname(),
|
|
326
|
+
platform: `${os.platform()} ${os.arch()}`,
|
|
327
|
+
cpuCores: os.cpus().length,
|
|
328
|
+
pid: devStats.pid,
|
|
329
|
+
uptime: this.devServer.uptime,
|
|
313
330
|
},
|
|
314
|
-
pid: stats.pid,
|
|
315
|
-
uptime: this.devServer.uptime,
|
|
316
331
|
});
|
|
317
332
|
}
|
|
318
333
|
// ─── /live/logs ──────────────────────────────────────────────
|
|
319
334
|
async handleLogs(_req, res, _body, url) {
|
|
320
|
-
const limit = parseInt(url.searchParams.get('limit') || '100', 10);
|
|
335
|
+
const limit = parseInt(url.searchParams.get('limit') || url.searchParams.get('tail') || '100', 10);
|
|
321
336
|
const logs = this.devServer.getLogs(limit);
|
|
322
337
|
this.json(res, {
|
|
323
338
|
success: true,
|
|
324
339
|
logs: logs.map(l => ({
|
|
325
340
|
timestamp: new Date(l.timestamp).toISOString(),
|
|
326
|
-
|
|
341
|
+
stream: l.type, // 'stdout' | 'stderr' — matches what the terminal panel expects
|
|
327
342
|
message: l.message,
|
|
328
343
|
source: 'frontend',
|
|
329
344
|
})),
|
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 { getDeviceId } from './config.js';
|
|
21
22
|
export class TunnelClient {
|
|
22
23
|
ws = null;
|
|
23
24
|
options;
|
|
@@ -55,12 +56,13 @@ export class TunnelClient {
|
|
|
55
56
|
clearTimeout(connectTimeout);
|
|
56
57
|
this.reconnectAttempts = 0;
|
|
57
58
|
this.connectedAt = Date.now();
|
|
58
|
-
// Send enhanced agent info with capabilities
|
|
59
|
+
// Send enhanced agent info with capabilities and deviceId
|
|
59
60
|
this.send({
|
|
60
61
|
type: 'agent-info',
|
|
61
|
-
version: '0.
|
|
62
|
+
version: '0.4.0',
|
|
62
63
|
hostname: os.hostname(),
|
|
63
64
|
platform: `${os.platform()} ${os.arch()}`,
|
|
65
|
+
deviceId: getDeviceId(),
|
|
64
66
|
capabilities: [
|
|
65
67
|
'file-sync',
|
|
66
68
|
'type-check',
|
package/package.json
CHANGED