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.
@@ -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
- * The agent fully replaces Docker containers:
13
- * - Files are on the user's disk (no docker cp needed)
14
- * - Dev server runs natively (no container overhead)
15
- * - Type checking runs locally (full IDE speed)
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;
@@ -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
- * The agent fully replaces Docker containers:
13
- * - Files are on the user's disk (no docker cp needed)
14
- * - Dev server runs natively (no container overhead)
15
- * - Type checking runs locally (full IDE speed)
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.3.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 (kill old PID + free ports).
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(conf, apiPort, devPort) {
133
- // 1. Try to kill the previously stored agent PID
134
- const oldPid = conf.get('agentPid');
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 in case orphaned processes are still holding them
146
- killPort(apiPort);
147
- killPort(devPort);
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
- // Kill any leftover agent / free ports from a previous run
187
- cleanupPreviousAgent(conf, apiPort, devPort);
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
- conf.delete('agentPid');
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 for "nstantpage stop"
260
- conf.set('agentPid', process.pid);
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
- // Connect tunnel to gateway (non-fatal — dev server keeps running even if tunnel fails)
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
- conf.delete('agentPid');
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
@@ -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 stats = this.devServer.getStats();
302
- const totalMem = os.totalmem() / (1024 * 1024);
303
- const freeMem = os.freemem() / (1024 * 1024);
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
- cpu: { percent: stats.cpuPercent, cores: os.cpus().length },
308
- memory: {
309
- usedMb: stats.memoryMb,
310
- totalMb: Math.round(totalMem),
311
- freeMb: Math.round(freeMem),
312
- percent: totalMem > 0 ? Math.round((stats.memoryMb / totalMem) * 100) : 0,
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
- type: l.type,
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.3.2',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
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": {