nstantpage-agent 0.5.3 → 0.5.5

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
@@ -25,7 +25,7 @@ const program = new Command();
25
25
  program
26
26
  .name('nstantpage')
27
27
  .description('Local development agent for nstantpage.com — run projects on your machine, preview in the cloud')
28
- .version('0.5.3');
28
+ .version('0.5.4');
29
29
  program
30
30
  .command('login')
31
31
  .description('Authenticate with nstantpage.com')
@@ -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.3';
27
+ const VERSION = '0.5.5';
28
28
  /**
29
29
  * Resolve the backend API base URL.
30
30
  * - If --backend is passed, use it
@@ -226,20 +226,20 @@ export async function startCommand(directory, options) {
226
226
  console.log(chalk.gray(` Gateway: ${options.gateway}`));
227
227
  console.log(chalk.gray(` Backend: ${backendUrl}\n`));
228
228
  // 1. Fetch project files from the backend
229
+ const installer = new PackageInstaller({ projectDir });
229
230
  try {
230
231
  const { fileCount, isNew } = await fetchProjectFiles(backendUrl, projectId, projectDir, token);
231
- // 2. Install dependencies if this is a new download or node_modules is missing
232
- const hasNodeModules = fs.existsSync(path.join(projectDir, 'node_modules'));
233
- if (isNew || !hasNodeModules) {
232
+ // 2. Install dependencies if needed (verifies actual packages, not just folder)
233
+ if (!installer.areDependenciesInstalled()) {
234
234
  if (fs.existsSync(path.join(projectDir, 'package.json'))) {
235
235
  console.log(chalk.gray(' Installing dependencies...'));
236
- const installer = new PackageInstaller({ projectDir });
237
- const result = await installer.install([], false);
238
- if (result.success) {
236
+ try {
237
+ await installer.ensureDependencies();
239
238
  console.log(chalk.green(` ✓ Dependencies installed`));
240
239
  }
241
- else {
242
- console.log(chalk.yellow(` ⚠ Dependency installation had issues: ${result.output.slice(0, 200)}`));
240
+ catch (err) {
241
+ console.log(chalk.yellow(` ⚠ Install failed: ${err.message?.slice(0, 200)}`));
242
+ console.log(chalk.gray(` Will retry when dev server starts`));
243
243
  }
244
244
  }
245
245
  }
@@ -278,6 +278,10 @@ export async function startCommand(directory, options) {
278
278
  onStartProject: async (pid) => {
279
279
  await startAdditionalProject(pid, {
280
280
  token, backendUrl, gatewayUrl: options.gateway, deviceId, noDev: options.noDev,
281
+ onProgress: (phase, message) => {
282
+ // Send progress through the primary tunnel for multi-project starts
283
+ tunnel.sendSetupProgress(pid, phase, message);
284
+ },
281
285
  });
282
286
  },
283
287
  });
@@ -376,15 +380,21 @@ export async function startCommand(directory, options) {
376
380
  console.log(chalk.green(` ✓ API server on port ${apiPort}`));
377
381
  // Start dev server unless --no-dev flag
378
382
  if (!options.noDev) {
379
- console.log(chalk.gray(' Starting dev server...'));
380
- try {
381
- const devServer = localServer.getDevServer();
382
- await devServer.start();
383
- console.log(chalk.green(` ✓ Dev server on port ${devPort}`));
383
+ if (installer.areDependenciesInstalled()) {
384
+ console.log(chalk.gray(' Starting dev server...'));
385
+ try {
386
+ const devServer = localServer.getDevServer();
387
+ await devServer.start();
388
+ console.log(chalk.green(` ✓ Dev server on port ${devPort}`));
389
+ }
390
+ catch (err) {
391
+ console.log(chalk.yellow(` ⚠ Dev server failed to start: ${err.message}`));
392
+ console.log(chalk.gray(' You can start it later from the editor'));
393
+ }
384
394
  }
385
- catch (err) {
386
- console.log(chalk.yellow(` ⚠ Dev server failed to start: ${err.message}`));
387
- console.log(chalk.gray(' You can start it later from the editor'));
395
+ else {
396
+ console.log(chalk.yellow(` ⚠ Skipping dev server dependencies not fully installed`));
397
+ console.log(chalk.gray(' Dev server will start when browser opens the project'));
388
398
  }
389
399
  }
390
400
  else {
@@ -460,6 +470,9 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
460
470
  }
461
471
  const result = await startAdditionalProject(pid, {
462
472
  token, backendUrl, gatewayUrl: options.gateway, deviceId, noDev: options.noDev,
473
+ onProgress: (phase, message) => {
474
+ standbyTunnel.sendSetupProgress(pid, phase, message);
475
+ },
463
476
  });
464
477
  if (result)
465
478
  activeProjects.set(pid, result);
@@ -544,7 +557,9 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
544
557
  * sends a start-project command.
545
558
  */
546
559
  async function startAdditionalProject(projectId, opts) {
560
+ const progress = opts.onProgress || (() => { });
547
561
  console.log(chalk.blue(`\n 📦 Starting project ${projectId}...`));
562
+ progress('fetching-files', 'Fetching project files...');
548
563
  try {
549
564
  const allocated = allocatePortsForProject(projectId);
550
565
  const projectDir = resolveProjectDir('.', projectId);
@@ -553,37 +568,62 @@ async function startAdditionalProject(projectId, opts) {
553
568
  // Fetch project files
554
569
  try {
555
570
  await fetchProjectFiles(opts.backendUrl, projectId, projectDir, opts.token);
571
+ progress('fetching-files', 'Project files downloaded');
556
572
  }
557
573
  catch (err) {
558
574
  console.log(chalk.yellow(` ⚠ Could not fetch files: ${err.message}`));
575
+ progress('fetching-files', `Warning: ${err.message}`);
559
576
  }
560
- // Install dependencies
561
- const hasNodeModules = fs.existsSync(path.join(projectDir, 'node_modules'));
562
- if (!hasNodeModules && fs.existsSync(path.join(projectDir, 'package.json'))) {
577
+ // Install dependencies (must complete before dev server can start)
578
+ const installer = new PackageInstaller({ projectDir });
579
+ if (!installer.areDependenciesInstalled() && fs.existsSync(path.join(projectDir, 'package.json'))) {
563
580
  console.log(chalk.gray(` Installing dependencies...`));
564
- const installer = new PackageInstaller({ projectDir });
565
- const result = await installer.install([], false);
566
- if (result.success)
581
+ progress('installing-deps', 'Installing npm dependencies...');
582
+ try {
583
+ await installer.ensureDependencies();
567
584
  console.log(chalk.green(` ✓ Dependencies installed`));
585
+ progress('installing-deps', 'Dependencies installed');
586
+ }
587
+ catch (err) {
588
+ console.log(chalk.red(` ✗ Install failed: ${err.message}`));
589
+ console.log(chalk.yellow(` ⚠ Dev server may fail — retrying install on first request`));
590
+ progress('installing-deps', `Install failed: ${err.message}`);
591
+ }
592
+ }
593
+ else {
594
+ progress('installing-deps', 'Dependencies already installed');
568
595
  }
569
596
  // Start local server
597
+ progress('starting-server', 'Starting API server...');
570
598
  const localServer = new LocalServer({
571
599
  projectDir, projectId,
572
600
  apiPort: allocated.apiPort, devPort: allocated.devPort,
573
601
  });
574
602
  await localServer.start();
575
603
  console.log(chalk.green(` ✓ API server on port ${allocated.apiPort}`));
576
- // Start dev server
604
+ progress('starting-server', `API server on port ${allocated.apiPort}`);
605
+ // Start dev server (only if dependencies are installed)
577
606
  if (!opts.noDev) {
578
- try {
579
- await localServer.getDevServer().start();
580
- console.log(chalk.green(` ✓ Dev server on port ${allocated.devPort}`));
607
+ if (installer.areDependenciesInstalled()) {
608
+ progress('starting-dev', 'Starting dev server...');
609
+ try {
610
+ await localServer.getDevServer().start();
611
+ console.log(chalk.green(` ✓ Dev server on port ${allocated.devPort}`));
612
+ progress('starting-dev', `Dev server on port ${allocated.devPort}`);
613
+ }
614
+ catch (err) {
615
+ console.log(chalk.yellow(` ⚠ Dev server: ${err.message}`));
616
+ progress('starting-dev', `Warning: ${err.message}`);
617
+ }
581
618
  }
582
- catch (err) {
583
- console.log(chalk.yellow(` ⚠ Dev server: ${err.message}`));
619
+ else {
620
+ console.log(chalk.yellow(` ⚠ Skipping dev server — dependencies not installed`));
621
+ console.log(chalk.gray(` Dev server will start when browser opens the project`));
622
+ progress('starting-dev', 'Skipped — dependencies not installed');
584
623
  }
585
624
  }
586
625
  // Connect project tunnel
626
+ progress('connecting', 'Connecting tunnel to gateway...');
587
627
  const tunnel = new TunnelClient({
588
628
  gatewayUrl: opts.gatewayUrl,
589
629
  token: opts.token,
@@ -594,10 +634,12 @@ async function startAdditionalProject(projectId, opts) {
594
634
  try {
595
635
  await tunnel.connect();
596
636
  console.log(chalk.green(` ✓ Tunnel connected for project ${projectId}`));
637
+ progress('connecting', 'Tunnel connected');
597
638
  }
598
639
  catch (err) {
599
640
  console.log(chalk.yellow(` ⚠ Tunnel: ${err.message}`));
600
641
  tunnel.startBackgroundReconnect();
642
+ progress('connecting', `Warning: ${err.message} — reconnecting`);
601
643
  }
602
644
  // Register project with backend
603
645
  try {
@@ -613,10 +655,12 @@ async function startAdditionalProject(projectId, opts) {
613
655
  }
614
656
  catch { }
615
657
  console.log(chalk.green(` ✓ Project ${projectId} is live!\n`));
658
+ progress('ready', 'Project is live!');
616
659
  return { localServer, tunnel };
617
660
  }
618
661
  catch (err) {
619
662
  console.error(chalk.red(` ✗ Failed to start project ${projectId}: ${err.message}`));
663
+ progress('error', err.message);
620
664
  return null;
621
665
  }
622
666
  }
@@ -559,7 +559,7 @@ export class LocalServer {
559
559
  connected: true,
560
560
  projectId: this.options.projectId,
561
561
  agent: {
562
- version: '0.5.3',
562
+ version: '0.5.4',
563
563
  hostname: os.hostname(),
564
564
  platform: `${os.platform()} ${os.arch()}`,
565
565
  },
@@ -7,6 +7,8 @@ export interface PackageInstallerOptions {
7
7
  }
8
8
  export declare class PackageInstaller {
9
9
  private projectDir;
10
+ /** Serializes concurrent install calls */
11
+ private installLock;
10
12
  constructor(options: PackageInstallerOptions);
11
13
  /**
12
14
  * Install packages into the project.
@@ -18,8 +20,14 @@ export declare class PackageInstaller {
18
20
  }>;
19
21
  /**
20
22
  * Ensure all project dependencies are installed.
23
+ * Uses a lock to prevent concurrent installs.
21
24
  */
22
25
  ensureDependencies(): Promise<void>;
26
+ /**
27
+ * Check if dependencies are actually installed (not just that node_modules/ exists).
28
+ * Verifies that at least one key dependency from package.json is present.
29
+ */
30
+ areDependenciesInstalled(): boolean;
23
31
  /**
24
32
  * Detect which package manager is used (pnpm, yarn, npm).
25
33
  */
@@ -4,9 +4,11 @@
4
4
  */
5
5
  import { spawn } from 'child_process';
6
6
  import path from 'path';
7
- import { existsSync } from 'fs';
7
+ import fs, { existsSync } from 'fs';
8
8
  export class PackageInstaller {
9
9
  projectDir;
10
+ /** Serializes concurrent install calls */
11
+ installLock = Promise.resolve();
10
12
  constructor(options) {
11
13
  this.projectDir = options.projectDir;
12
14
  }
@@ -18,7 +20,7 @@ export class PackageInstaller {
18
20
  const args = this.buildInstallArgs(pm, packages, dev);
19
21
  console.log(` [Installer] ${pm} ${args.join(' ')}`);
20
22
  try {
21
- const timeout = packages.length === 0 ? 120_000 : 60_000;
23
+ const timeout = packages.length === 0 ? 300_000 : 120_000;
22
24
  const output = await this.runCommand(pm, args, timeout);
23
25
  return {
24
26
  success: true,
@@ -36,16 +38,51 @@ export class PackageInstaller {
36
38
  }
37
39
  /**
38
40
  * Ensure all project dependencies are installed.
41
+ * Uses a lock to prevent concurrent installs.
39
42
  */
40
43
  async ensureDependencies() {
41
- const nodeModules = path.join(this.projectDir, 'node_modules');
42
- if (existsSync(nodeModules))
44
+ // Serialize with any in-flight install
45
+ await this.installLock;
46
+ if (this.areDependenciesInstalled())
43
47
  return;
44
- console.log(` [Installer] Installing project dependencies...`);
45
- const pm = this.detectPackageManager();
46
- const args = pm === 'pnpm' ? ['install'] : ['install'];
47
- await this.runCommand(pm, args, 120_000);
48
- console.log(` [Installer] Dependencies installed`);
48
+ // Acquire lock for this install
49
+ let releaseLock;
50
+ this.installLock = new Promise(resolve => { releaseLock = resolve; });
51
+ try {
52
+ console.log(` [Installer] Installing project dependencies...`);
53
+ const pm = this.detectPackageManager();
54
+ const args = pm === 'pnpm' ? ['install'] : ['install'];
55
+ await this.runCommand(pm, args, 300_000);
56
+ console.log(` [Installer] Dependencies installed`);
57
+ }
58
+ finally {
59
+ releaseLock();
60
+ }
61
+ }
62
+ /**
63
+ * Check if dependencies are actually installed (not just that node_modules/ exists).
64
+ * Verifies that at least one key dependency from package.json is present.
65
+ */
66
+ areDependenciesInstalled() {
67
+ const nodeModules = path.join(this.projectDir, 'node_modules');
68
+ if (!existsSync(nodeModules))
69
+ return false;
70
+ // Check that at least one real dependency is installed
71
+ try {
72
+ const pkgPath = path.join(this.projectDir, 'package.json');
73
+ if (!existsSync(pkgPath))
74
+ return true; // no package.json → nothing to install
75
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
76
+ const deps = Object.keys(pkg.dependencies || {});
77
+ if (deps.length === 0)
78
+ return true; // no deps → nothing needed
79
+ // Check if the first listed dependency has its folder in node_modules
80
+ const firstDep = deps[0];
81
+ return existsSync(path.join(nodeModules, firstDep));
82
+ }
83
+ catch {
84
+ return existsSync(nodeModules);
85
+ }
49
86
  }
50
87
  /**
51
88
  * Detect which package manager is used (pnpm, yarn, npm).
package/dist/tunnel.d.ts CHANGED
@@ -25,6 +25,8 @@ interface TunnelClientOptions {
25
25
  devPort: number;
26
26
  /** Callback when gateway requests starting a new project on this device */
27
27
  onStartProject?: (projectId: string) => Promise<void>;
28
+ /** Callback for setup progress — lets the standby tunnel relay phases to gateway */
29
+ onSetupProgress?: (projectId: string, phase: string, message: string) => void;
28
30
  }
29
31
  export declare class TunnelClient {
30
32
  private ws;
@@ -82,6 +84,11 @@ export declare class TunnelClient {
82
84
  * Called when user clicks "Connect" in the web editor's Cloud panel.
83
85
  */
84
86
  private handleStartProject;
87
+ /**
88
+ * Send setup progress for a project through this tunnel.
89
+ * Used by the standby tunnel to relay progress from startAdditionalProject to the gateway.
90
+ */
91
+ sendSetupProgress(projectId: string, phase: string, message: string): void;
85
92
  private send;
86
93
  private handleMessage;
87
94
  /**
package/dist/tunnel.js CHANGED
@@ -63,7 +63,7 @@ export class TunnelClient {
63
63
  // Send enhanced agent info with capabilities and deviceId
64
64
  this.send({
65
65
  type: 'agent-info',
66
- version: '0.5.3',
66
+ version: '0.5.4',
67
67
  hostname: os.hostname(),
68
68
  platform: `${os.platform()} ${os.arch()}`,
69
69
  deviceId: getDeviceId(),
@@ -253,11 +253,15 @@ export class TunnelClient {
253
253
  }
254
254
  if (this.options.onStartProject) {
255
255
  try {
256
+ // Send initial progress
257
+ this.sendSetupProgress(projectId, 'starting', 'Starting project setup...');
256
258
  await this.options.onStartProject(projectId);
259
+ this.sendSetupProgress(projectId, 'ready', 'Project is live!');
257
260
  this.send({ type: 'start-project-result', projectId, success: true });
258
261
  }
259
262
  catch (err) {
260
263
  console.error(` [Tunnel] Failed to start project ${projectId}:`, err.message);
264
+ this.sendSetupProgress(projectId, 'error', err.message);
261
265
  this.send({ type: 'start-project-result', projectId, success: false, error: err.message });
262
266
  }
263
267
  }
@@ -265,6 +269,13 @@ export class TunnelClient {
265
269
  this.send({ type: 'start-project-result', success: false, error: 'Agent does not support remote project start' });
266
270
  }
267
271
  }
272
+ /**
273
+ * Send setup progress for a project through this tunnel.
274
+ * Used by the standby tunnel to relay progress from startAdditionalProject to the gateway.
275
+ */
276
+ sendSetupProgress(projectId, phase, message) {
277
+ this.send({ type: 'setup-progress', projectId, phase, message, timestamp: Date.now() });
278
+ }
268
279
  send(msg) {
269
280
  if (this.ws?.readyState === WebSocket.OPEN) {
270
281
  this.ws.send(JSON.stringify(msg));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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": {