nstantpage-agent 0.8.11 → 0.8.13

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.
@@ -5,6 +5,7 @@ import chalk from 'chalk';
5
5
  import open from 'open';
6
6
  import http from 'http';
7
7
  import { getConfig } from '../config.js';
8
+ import { serviceInstallCommand } from './service.js';
8
9
  /**
9
10
  * Resolve the frontend URL based on gateway.
10
11
  * - If gateway points to localhost → frontend is http://localhost:5001
@@ -178,6 +179,16 @@ export async function loginCommand(options = {}) {
178
179
  if (email)
179
180
  console.log(chalk.gray(` Account: ${email}`));
180
181
  console.log(chalk.gray(` Server: ${isLocal ? 'localhost (dev)' : 'nstantpage.com'}`));
181
- console.log(chalk.gray(' Run "nstantpage start" to connect your machine'));
182
+ // Auto-install background service with the new token
183
+ const gateway = conf.get('gatewayUrl') || 'wss://webprev.live';
184
+ try {
185
+ await serviceInstallCommand({ gateway });
186
+ console.log(chalk.green(' ✓ Agent is running as a background service.'));
187
+ console.log(chalk.gray(' Use "nstantpage logs" to view agent output.'));
188
+ }
189
+ catch (err) {
190
+ console.log(chalk.yellow(` ⚠ Could not install background service: ${err.message}`));
191
+ console.log(chalk.gray(' Run "nstantpage run" to start manually.'));
192
+ }
182
193
  }
183
194
  //# sourceMappingURL=login.js.map
@@ -579,11 +579,13 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
579
579
  existing.tunnel.disconnect();
580
580
  await existing.localServer.stop();
581
581
  activeProjects.delete(pid);
582
- // Wipe the project directory
583
- const projectDir = resolveProjectDir('.', pid);
584
- if (fs.existsSync(projectDir)) {
585
- console.log(chalk.gray(` Wiping project directory: ${projectDir}`));
586
- fs.rmSync(projectDir, { recursive: true, force: true });
582
+ // Wipe the project directory (only for non-LocalRepo)
583
+ if (!opts?.localFolderPath) {
584
+ const projectDir = resolveProjectDir('.', pid);
585
+ if (fs.existsSync(projectDir)) {
586
+ console.log(chalk.gray(` Wiping project directory: ${projectDir}`));
587
+ fs.rmSync(projectDir, { recursive: true, force: true });
588
+ }
587
589
  }
588
590
  }
589
591
  else if (activeProjects.has(pid)) {
@@ -592,6 +594,7 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
592
594
  }
593
595
  const result = await startAdditionalProject(pid, {
594
596
  token, backendUrl, gatewayUrl: options.gateway, deviceId, noDev: options.noDev,
597
+ localFolderPath: opts?.localFolderPath,
595
598
  onProgress: (phase, message) => {
596
599
  standbyTunnel.sendSetupProgress(pid, phase, message);
597
600
  },
@@ -678,6 +681,7 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
678
681
  }
679
682
  const result = await startAdditionalProject(projectId, {
680
683
  token, backendUrl, gatewayUrl: options.gateway, deviceId, noDev: options.noDev,
684
+ localFolderPath: opts?.localFolderPath,
681
685
  onProgress: (phase, message) => {
682
686
  standbyTunnel.sendSetupProgress(projectId, phase, message);
683
687
  },
@@ -736,29 +740,37 @@ async function startAdditionalProject(projectId, opts) {
736
740
  const progress = opts.onProgress || (() => { });
737
741
  const timings = {};
738
742
  const t0 = Date.now();
739
- console.log(chalk.blue(`\n 📦 Starting project ${projectId}...`));
743
+ const isLocalRepo = !!opts.localFolderPath;
744
+ console.log(chalk.blue(`\n 📦 Starting project ${projectId}${isLocalRepo ? ' (LocalRepo)' : ''}...`));
740
745
  try {
741
746
  const allocated = allocatePortsForProject(projectId);
742
- const projectDir = resolveProjectDir('.', projectId);
747
+ const projectDir = isLocalRepo ? opts.localFolderPath : resolveProjectDir('.', projectId);
743
748
  if (!fs.existsSync(projectDir))
744
749
  fs.mkdirSync(projectDir, { recursive: true });
745
750
  // Ensure ports are clear for this project before starting
746
751
  cleanupPreviousAgent(projectId, allocated.apiPort, allocated.devPort);
747
752
  await new Promise(r => setTimeout(r, 50));
748
753
  // ── Phase 1: API server + file fetch + tunnel connect (all parallel) ──
749
- progress('fetching-files', 'Fetching project files...');
750
- // Provision database if PostgreSQL is available
754
+ if (!isLocalRepo) {
755
+ progress('fetching-files', 'Fetching project files...');
756
+ }
757
+ else {
758
+ progress('starting-server', 'Starting local server...');
759
+ }
760
+ // Provision database if PostgreSQL is available (skip for LocalRepo)
751
761
  let databaseUrl = null;
752
- try {
753
- const hasPg = await probeLocalPostgres();
754
- if (hasPg) {
755
- databaseUrl = await ensureLocalProjectDb(projectId);
756
- if (databaseUrl) {
757
- console.log(chalk.green(` ✓ Database ready: project_${projectId}`));
762
+ if (!isLocalRepo) {
763
+ try {
764
+ const hasPg = await probeLocalPostgres();
765
+ if (hasPg) {
766
+ databaseUrl = await ensureLocalProjectDb(projectId);
767
+ if (databaseUrl) {
768
+ console.log(chalk.green(` ✓ Database ready: project_${projectId}`));
769
+ }
758
770
  }
759
771
  }
772
+ catch { }
760
773
  }
761
- catch { }
762
774
  const serverEnv = {};
763
775
  if (databaseUrl)
764
776
  serverEnv['DATABASE_URL'] = databaseUrl;
@@ -799,17 +811,20 @@ async function startAdditionalProject(projectId, opts) {
799
811
  tunnel.startBackgroundReconnect();
800
812
  }
801
813
  })();
814
+ // Fetch project files from backend (skip for LocalRepo — files are on disk)
802
815
  const fileStart = Date.now();
803
816
  let fetchedVersionId;
804
- try {
805
- const { versionId } = await fetchProjectFiles(opts.backendUrl, projectId, projectDir, opts.token);
806
- fetchedVersionId = versionId;
807
- timings['fetch-files'] = Date.now() - fileStart;
808
- progress('fetching-files', `Files downloaded (${timings['fetch-files']}ms)`);
809
- }
810
- catch (err) {
811
- console.log(chalk.yellow(` ⚠ Could not fetch files: ${err.message}`));
812
- progress('fetching-files', `Warning: ${err.message}`);
817
+ if (!isLocalRepo) {
818
+ try {
819
+ const { versionId } = await fetchProjectFiles(opts.backendUrl, projectId, projectDir, opts.token);
820
+ fetchedVersionId = versionId;
821
+ timings['fetch-files'] = Date.now() - fileStart;
822
+ progress('fetching-files', `Files downloaded (${timings['fetch-files']}ms)`);
823
+ }
824
+ catch (err) {
825
+ console.log(chalk.yellow(` ⚠ Could not fetch files: ${err.message}`));
826
+ progress('fetching-files', `Warning: ${err.message}`);
827
+ }
813
828
  }
814
829
  // Write DATABASE_URL to .env AFTER file fetch (so it doesn't get overwritten)
815
830
  if (databaseUrl) {
@@ -822,49 +837,51 @@ async function startAdditionalProject(projectId, opts) {
822
837
  if (fetchedVersionId) {
823
838
  localServer.markSynced(fetchedVersionId);
824
839
  }
825
- // ── Phase 2: Install deps (if needed) ────────────────────────────
826
- const installer = new PackageInstaller({ projectDir });
827
- const needsInstall = !installer.areDependenciesInstalled() && fs.existsSync(path.join(projectDir, 'package.json'));
828
- if (needsInstall) {
829
- const installStart = Date.now();
830
- console.log(chalk.gray(` Installing dependencies...`));
831
- progress('installing-deps', 'Installing npm dependencies...');
832
- try {
833
- await installer.ensureDependencies();
834
- timings['install'] = Date.now() - installStart;
835
- console.log(chalk.green(` ✓ Dependencies installed (${timings['install']}ms)`));
836
- progress('installing-deps', `Dependencies installed (${timings['install']}ms)`);
837
- }
838
- catch (err) {
839
- timings['install'] = Date.now() - installStart;
840
- console.log(chalk.red(` ✗ Install failed: ${err.message}`));
841
- console.log(chalk.yellow(` ⚠ Dev server may fail — retrying install on first request`));
842
- progress('installing-deps', `Install failed: ${err.message}`);
843
- }
844
- }
845
- else {
846
- progress('installing-deps', 'Dependencies already installed (cached)');
847
- }
848
- // ── Phase 3: Start dev server ────────────────────────────────────
849
- if (!opts.noDev) {
850
- if (installer.areDependenciesInstalled()) {
851
- progress('starting-dev', 'Starting dev server...');
840
+ // ── Phase 2: Install deps (if needed) — skip for LocalRepo ──
841
+ if (!isLocalRepo) {
842
+ const installer = new PackageInstaller({ projectDir });
843
+ const needsInstall = !installer.areDependenciesInstalled() && fs.existsSync(path.join(projectDir, 'package.json'));
844
+ if (needsInstall) {
845
+ const installStart = Date.now();
846
+ console.log(chalk.gray(` Installing dependencies...`));
847
+ progress('installing-deps', 'Installing npm dependencies...');
852
848
  try {
853
- const devStart = Date.now();
854
- await localServer.getDevServer().start();
855
- timings['dev-server'] = Date.now() - devStart;
856
- console.log(chalk.green(` Dev server on port ${allocated.devPort} (${timings['dev-server']}ms)`));
857
- progress('starting-dev', `Dev server on port ${allocated.devPort}`);
849
+ await installer.ensureDependencies();
850
+ timings['install'] = Date.now() - installStart;
851
+ console.log(chalk.green(` ✓ Dependencies installed (${timings['install']}ms)`));
852
+ progress('installing-deps', `Dependencies installed (${timings['install']}ms)`);
858
853
  }
859
854
  catch (err) {
860
- console.log(chalk.yellow(` ⚠ Dev server: ${err.message}`));
861
- progress('starting-dev', `Warning: ${err.message}`);
855
+ timings['install'] = Date.now() - installStart;
856
+ console.log(chalk.red(` ✗ Install failed: ${err.message}`));
857
+ console.log(chalk.yellow(` ⚠ Dev server may fail — retrying install on first request`));
858
+ progress('installing-deps', `Install failed: ${err.message}`);
862
859
  }
863
860
  }
864
861
  else {
865
- console.log(chalk.yellow(` ⚠ Skipping dev server — dependencies not installed`));
866
- console.log(chalk.gray(` Dev server will start when browser opens the project`));
867
- progress('starting-dev', 'Skippeddependencies not installed');
862
+ progress('installing-deps', 'Dependencies already installed (cached)');
863
+ }
864
+ // ── Phase 3: Start dev serverskip for LocalRepo ──
865
+ if (!opts.noDev) {
866
+ if (installer.areDependenciesInstalled()) {
867
+ progress('starting-dev', 'Starting dev server...');
868
+ try {
869
+ const devStart = Date.now();
870
+ await localServer.getDevServer().start();
871
+ timings['dev-server'] = Date.now() - devStart;
872
+ console.log(chalk.green(` ✓ Dev server on port ${allocated.devPort} (${timings['dev-server']}ms)`));
873
+ progress('starting-dev', `Dev server on port ${allocated.devPort}`);
874
+ }
875
+ catch (err) {
876
+ console.log(chalk.yellow(` ⚠ Dev server: ${err.message}`));
877
+ progress('starting-dev', `Warning: ${err.message}`);
878
+ }
879
+ }
880
+ else {
881
+ console.log(chalk.yellow(` ⚠ Skipping dev server — dependencies not installed`));
882
+ console.log(chalk.gray(` Dev server will start when browser opens the project`));
883
+ progress('starting-dev', 'Skipped — dependencies not installed');
884
+ }
868
885
  }
869
886
  }
870
887
  // Ensure tunnel is connected before declaring ready
@@ -1,4 +1,9 @@
1
1
  /**
2
- * Status command — show agent connection status
2
+ * Status command — show agent connection status.
3
+ *
4
+ * Checks three sources:
5
+ * 1. Status server at localhost:18999 (most accurate — shows live tunnel state)
6
+ * 2. OS service (launchd on macOS, systemd on Linux)
7
+ * 3. Stored config (auth, gateway)
3
8
  */
4
9
  export declare function statusCommand(): Promise<void>;
@@ -1,53 +1,103 @@
1
1
  /**
2
- * Status command — show agent connection status
2
+ * Status command — show agent connection status.
3
+ *
4
+ * Checks three sources:
5
+ * 1. Status server at localhost:18999 (most accurate — shows live tunnel state)
6
+ * 2. OS service (launchd on macOS, systemd on Linux)
7
+ * 3. Stored config (auth, gateway)
3
8
  */
4
9
  import chalk from 'chalk';
10
+ import os from 'os';
11
+ import { execSync } from 'child_process';
5
12
  import { getConfig } from '../config.js';
13
+ import { getPackageVersion } from '../version.js';
14
+ const STATUS_PORT = 18999;
15
+ async function fetchAgentStatus() {
16
+ try {
17
+ const controller = new AbortController();
18
+ const timeout = setTimeout(() => controller.abort(), 2000);
19
+ const res = await fetch(`http://localhost:${STATUS_PORT}/status`, { signal: controller.signal });
20
+ clearTimeout(timeout);
21
+ if (!res.ok)
22
+ return null;
23
+ return await res.json();
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function checkOsService() {
30
+ const platform = os.platform();
31
+ if (platform === 'darwin') {
32
+ try {
33
+ const result = execSync('launchctl list | grep com.nstantpage.agent', { encoding: 'utf-8' }).trim();
34
+ if (result) {
35
+ const pid = result.split('\t')[0];
36
+ return { installed: true, pid: pid !== '-' ? pid : undefined };
37
+ }
38
+ }
39
+ catch { }
40
+ }
41
+ else if (platform === 'linux') {
42
+ try {
43
+ const result = execSync('systemctl --user is-active nstantpage-agent 2>/dev/null', { encoding: 'utf-8' }).trim();
44
+ return { installed: true, pid: result === 'active' ? 'active' : undefined };
45
+ }
46
+ catch { }
47
+ }
48
+ return { installed: false };
49
+ }
50
+ function formatUptime(ms) {
51
+ const s = Math.floor(ms / 1000);
52
+ if (s < 60)
53
+ return `${s}s`;
54
+ const m = Math.floor(s / 60);
55
+ if (m < 60)
56
+ return `${m}m ${s % 60}s`;
57
+ const h = Math.floor(m / 60);
58
+ return `${h}h ${m % 60}m`;
59
+ }
6
60
  export async function statusCommand() {
7
61
  const conf = getConfig();
8
62
  const token = conf.get('token');
9
- const projectId = conf.get('projectId');
10
- const pid = conf.get('agentPid');
11
- const lastConnected = conf.get('lastConnected');
12
- const devPort = conf.get('devPort') || 3000;
13
- const apiPort = conf.get('apiPort') || 18924;
14
- console.log(chalk.blue('\n nstantpage agent v0.2.0\n'));
63
+ const email = conf.get('email');
64
+ const gateway = conf.get('gatewayUrl') || 'wss://webprev.live';
65
+ const isLocal = /^wss?:\/\/(localhost|127\.0\.0\.1)/.test(gateway);
66
+ console.log(chalk.blue(`\n nstantpage agent v${getPackageVersion()}\n`));
15
67
  // Auth
16
68
  if (token) {
17
- console.log(chalk.green(' ✓ Authenticated'));
69
+ console.log(chalk.green(` ✓ Authenticated${email ? ` (${email})` : ''}`));
18
70
  }
19
71
  else {
20
72
  console.log(chalk.red(' ✗ Not authenticated (run "nstantpage login")'));
21
73
  }
22
- // Project
23
- if (projectId) {
24
- console.log(chalk.gray(` Project: ${projectId}`));
25
- console.log(chalk.gray(` Preview: https://${projectId}.webprev.live`));
26
- }
27
- else {
28
- console.log(chalk.gray(' No project linked'));
29
- }
30
- // Running
31
- if (pid) {
32
- try {
33
- process.kill(pid, 0); // Check if process exists
34
- console.log(chalk.green(` ✓ Agent running (PID ${pid})`));
35
- console.log(chalk.gray(` Dev server: http://localhost:${devPort}`));
36
- console.log(chalk.gray(` API server: http://localhost:${apiPort}`));
74
+ console.log(chalk.gray(` Server: ${isLocal ? 'localhost (dev)' : 'nstantpage.com'}`));
75
+ // Try live status from the status server
76
+ const live = await fetchAgentStatus();
77
+ const service = checkOsService();
78
+ if (live) {
79
+ console.log(chalk.green(` ✓ Agent running`));
80
+ console.log(chalk.gray(` Tunnel: ${live.tunnelStatus}`));
81
+ console.log(chalk.gray(` Uptime: ${formatUptime(live.uptime)}`));
82
+ console.log(chalk.gray(` Gateway: ${live.gatewayUrl}`));
83
+ if (live.activeProjects.length > 0) {
84
+ console.log(chalk.gray(` Projects: ${live.activeProjects.map(p => p.projectId).join(', ')}`));
85
+ }
86
+ else {
87
+ console.log(chalk.gray(` Projects: none (standby mode)`));
37
88
  }
38
- catch {
39
- console.log(chalk.gray(' Agent not running'));
40
- conf.delete('agentPid');
89
+ }
90
+ else if (service.installed) {
91
+ console.log(chalk.yellow(` ⚠ Service installed but agent not responding`));
92
+ if (service.pid) {
93
+ console.log(chalk.gray(` PID: ${service.pid}`));
41
94
  }
95
+ console.log(chalk.gray(' Try: nstantpage logs'));
42
96
  }
43
97
  else {
44
98
  console.log(chalk.gray(' Agent not running'));
99
+ console.log(chalk.gray(' Run "nstantpage run" to start'));
45
100
  }
46
- // Last connected
47
- if (lastConnected) {
48
- console.log(chalk.gray(` Last connected: ${lastConnected}`));
49
- }
50
- console.log(chalk.gray(`\n Mode: Agent (replaces cloud containers)`));
51
101
  console.log('');
52
102
  }
53
103
  //# sourceMappingURL=status.js.map
package/dist/tunnel.d.ts CHANGED
@@ -27,6 +27,7 @@ interface TunnelClientOptions {
27
27
  /** Callback when gateway requests starting a new project on this device */
28
28
  onStartProject?: (projectId: string, options?: {
29
29
  clean?: boolean;
30
+ localFolderPath?: string;
30
31
  }) => Promise<void>;
31
32
  /** Callback for setup progress — lets the standby tunnel relay phases to gateway */
32
33
  onSetupProgress?: (projectId: string, phase: string, message: string) => void;
package/dist/tunnel.js CHANGED
@@ -403,8 +403,8 @@ export class TunnelClient {
403
403
  * Handle start-project: gateway wants this agent to start serving a new project.
404
404
  * Called when user clicks "Connect" in the web editor's Cloud panel.
405
405
  */
406
- async handleStartProject(projectId, clean) {
407
- console.log(` [Tunnel] Received start-project command for ${projectId}${clean ? ' (clean)' : ''}`);
406
+ async handleStartProject(projectId, clean, localFolderPath) {
407
+ console.log(` [Tunnel] Received start-project command for ${projectId}${clean ? ' (clean)' : ''}${localFolderPath ? ` (local: ${localFolderPath})` : ''}`);
408
408
  if (!projectId) {
409
409
  this.send({ type: 'start-project-result', success: false, error: 'Missing projectId' });
410
410
  return;
@@ -413,7 +413,7 @@ export class TunnelClient {
413
413
  try {
414
414
  // Send initial progress
415
415
  this.sendSetupProgress(projectId, 'starting', 'Starting project setup...');
416
- await this.options.onStartProject(projectId, clean ? { clean: true } : undefined);
416
+ await this.options.onStartProject(projectId, { ...(clean ? { clean: true } : {}), ...(localFolderPath ? { localFolderPath } : {}) });
417
417
  this.sendSetupProgress(projectId, 'ready', 'Project is live!');
418
418
  this.send({ type: 'start-project-result', projectId, success: true });
419
419
  }
@@ -474,7 +474,7 @@ export class TunnelClient {
474
474
  this.handleWsClose(msg.wsId);
475
475
  break;
476
476
  case 'start-project':
477
- this.handleStartProject(msg.projectId, msg.clean);
477
+ this.handleStartProject(msg.projectId, msg.clean, msg.localFolderPath);
478
478
  break;
479
479
  case 'force-disconnect':
480
480
  console.log(' [Tunnel] Received force-disconnect — will not reconnect');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.8.11",
3
+ "version": "0.8.13",
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": {