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 +1 -1
- package/dist/commands/start.js +73 -29
- package/dist/localServer.js +1 -1
- package/dist/packageInstaller.d.ts +8 -0
- package/dist/packageInstaller.js +46 -9
- package/dist/tunnel.d.ts +7 -0
- package/dist/tunnel.js +12 -1
- package/package.json +1 -1
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.
|
|
28
|
+
.version('0.5.4');
|
|
29
29
|
program
|
|
30
30
|
.command('login')
|
|
31
31
|
.description('Authenticate with nstantpage.com')
|
package/dist/commands/start.js
CHANGED
|
@@ -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.
|
|
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
|
|
232
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
if (result.success) {
|
|
236
|
+
try {
|
|
237
|
+
await installer.ensureDependencies();
|
|
239
238
|
console.log(chalk.green(` ✓ Dependencies installed`));
|
|
240
239
|
}
|
|
241
|
-
|
|
242
|
-
console.log(chalk.yellow(` ⚠
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
386
|
-
console.log(chalk.yellow(` ⚠
|
|
387
|
-
console.log(chalk.gray('
|
|
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
|
|
562
|
-
if (!
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
583
|
-
console.log(chalk.yellow(` ⚠
|
|
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
|
}
|
package/dist/localServer.js
CHANGED
|
@@ -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
|
*/
|
package/dist/packageInstaller.js
CHANGED
|
@@ -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 ?
|
|
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
|
-
|
|
42
|
-
|
|
44
|
+
// Serialize with any in-flight install
|
|
45
|
+
await this.installLock;
|
|
46
|
+
if (this.areDependenciesInstalled())
|
|
43
47
|
return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
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