nstantpage-agent 0.5.6 ā 0.5.8
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 +95 -60
- package/dist/devServer.js +4 -3
- package/dist/localServer.js +52 -26
- package/dist/packageInstaller.d.ts +15 -0
- package/dist/packageInstaller.js +77 -2
- package/dist/tunnel.js +1 -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.8');
|
|
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.8';
|
|
28
28
|
/**
|
|
29
29
|
* Resolve the backend API base URL.
|
|
30
30
|
* - If --backend is passed, use it
|
|
@@ -82,16 +82,29 @@ async function fetchProjectFiles(backendUrl, projectId, projectDir, token) {
|
|
|
82
82
|
console.log(chalk.gray(` Files up-to-date (version ${data.version})`));
|
|
83
83
|
return { fileCount: data.files.length, isNew: false };
|
|
84
84
|
}
|
|
85
|
-
// Write all files to disk
|
|
86
|
-
|
|
85
|
+
// Write all files to disk in parallel (batch I/O for speed)
|
|
86
|
+
// First, collect unique directories to create
|
|
87
|
+
const dirsToCreate = new Set();
|
|
87
88
|
for (const file of data.files) {
|
|
88
89
|
const filePath = path.join(projectDir, file.path);
|
|
89
|
-
|
|
90
|
+
dirsToCreate.add(path.dirname(filePath));
|
|
91
|
+
}
|
|
92
|
+
// Create all directories first (sync is fine, there are few unique dirs)
|
|
93
|
+
for (const dir of dirsToCreate) {
|
|
90
94
|
if (!fs.existsSync(dir)) {
|
|
91
95
|
fs.mkdirSync(dir, { recursive: true });
|
|
92
96
|
}
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
}
|
|
98
|
+
// Write files in parallel batches of 50
|
|
99
|
+
const BATCH_SIZE = 50;
|
|
100
|
+
let written = 0;
|
|
101
|
+
for (let i = 0; i < data.files.length; i += BATCH_SIZE) {
|
|
102
|
+
const batch = data.files.slice(i, i + BATCH_SIZE);
|
|
103
|
+
await Promise.all(batch.map(async (file) => {
|
|
104
|
+
const filePath = path.join(projectDir, file.path);
|
|
105
|
+
await fs.promises.writeFile(filePath, file.content, 'utf-8');
|
|
106
|
+
}));
|
|
107
|
+
written += batch.length;
|
|
95
108
|
}
|
|
96
109
|
// Write version marker
|
|
97
110
|
fs.writeFileSync(versionFile, String(data.versionId), 'utf-8');
|
|
@@ -215,8 +228,8 @@ export async function startCommand(directory, options) {
|
|
|
215
228
|
conf.set('projectId', projectId);
|
|
216
229
|
// Kill any leftover agent for THIS PROJECT only (not other projects)
|
|
217
230
|
cleanupPreviousAgent(projectId, apiPort, devPort);
|
|
218
|
-
//
|
|
219
|
-
await new Promise(r => setTimeout(r,
|
|
231
|
+
// Brief pause for OS to release ports (50ms is sufficient on macOS/Linux)
|
|
232
|
+
await new Promise(r => setTimeout(r, 50));
|
|
220
233
|
console.log(chalk.blue(`\nš nstantpage agent v${VERSION}\n`));
|
|
221
234
|
console.log(chalk.gray(` Project ID: ${projectId}`));
|
|
222
235
|
console.log(chalk.gray(` Device ID: ${deviceId.slice(0, 12)}...`));
|
|
@@ -558,57 +571,97 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
|
|
|
558
571
|
*/
|
|
559
572
|
async function startAdditionalProject(projectId, opts) {
|
|
560
573
|
const progress = opts.onProgress || (() => { });
|
|
574
|
+
const timings = {};
|
|
575
|
+
const t0 = Date.now();
|
|
561
576
|
console.log(chalk.blue(`\n š¦ Starting project ${projectId}...`));
|
|
562
|
-
progress('fetching-files', 'Fetching project files...');
|
|
563
577
|
try {
|
|
564
578
|
const allocated = allocatePortsForProject(projectId);
|
|
565
579
|
const projectDir = resolveProjectDir('.', projectId);
|
|
566
580
|
if (!fs.existsSync(projectDir))
|
|
567
581
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
568
|
-
//
|
|
582
|
+
// Ensure ports are clear for this project before starting
|
|
583
|
+
cleanupPreviousAgent(projectId, allocated.apiPort, allocated.devPort);
|
|
584
|
+
await new Promise(r => setTimeout(r, 50));
|
|
585
|
+
// āā Phase 1: Fetch files + start API server in parallel āāāāāāāāāā
|
|
586
|
+
progress('fetching-files', 'Fetching project files...');
|
|
587
|
+
const localServer = new LocalServer({
|
|
588
|
+
projectDir, projectId,
|
|
589
|
+
apiPort: allocated.apiPort, devPort: allocated.devPort,
|
|
590
|
+
});
|
|
591
|
+
// Start API server immediately (doesn't need project files)
|
|
592
|
+
const apiServerPromise = localServer.start().then(() => {
|
|
593
|
+
timings['api-server'] = Date.now() - t0;
|
|
594
|
+
console.log(chalk.green(` ā API server on port ${allocated.apiPort} (${timings['api-server']}ms)`));
|
|
595
|
+
});
|
|
596
|
+
// Fetch project files concurrently with API server start
|
|
597
|
+
const fileStart = Date.now();
|
|
569
598
|
try {
|
|
570
599
|
await fetchProjectFiles(opts.backendUrl, projectId, projectDir, opts.token);
|
|
571
|
-
|
|
600
|
+
timings['fetch-files'] = Date.now() - fileStart;
|
|
601
|
+
progress('fetching-files', `Files downloaded (${timings['fetch-files']}ms)`);
|
|
572
602
|
}
|
|
573
603
|
catch (err) {
|
|
574
604
|
console.log(chalk.yellow(` ā Could not fetch files: ${err.message}`));
|
|
575
605
|
progress('fetching-files', `Warning: ${err.message}`);
|
|
576
606
|
}
|
|
577
|
-
//
|
|
607
|
+
// Wait for API server before proceeding
|
|
608
|
+
await apiServerPromise;
|
|
609
|
+
progress('starting-server', `API server ready`);
|
|
610
|
+
// āā Phase 2: Install deps (if needed) āāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
578
611
|
const installer = new PackageInstaller({ projectDir });
|
|
579
|
-
|
|
612
|
+
const needsInstall = !installer.areDependenciesInstalled() && fs.existsSync(path.join(projectDir, 'package.json'));
|
|
613
|
+
if (needsInstall) {
|
|
614
|
+
const installStart = Date.now();
|
|
580
615
|
console.log(chalk.gray(` Installing dependencies...`));
|
|
581
616
|
progress('installing-deps', 'Installing npm dependencies...');
|
|
582
617
|
try {
|
|
583
618
|
await installer.ensureDependencies();
|
|
584
|
-
|
|
585
|
-
|
|
619
|
+
timings['install'] = Date.now() - installStart;
|
|
620
|
+
console.log(chalk.green(` ā Dependencies installed (${timings['install']}ms)`));
|
|
621
|
+
progress('installing-deps', `Dependencies installed (${timings['install']}ms)`);
|
|
586
622
|
}
|
|
587
623
|
catch (err) {
|
|
624
|
+
timings['install'] = Date.now() - installStart;
|
|
588
625
|
console.log(chalk.red(` ā Install failed: ${err.message}`));
|
|
589
626
|
console.log(chalk.yellow(` ā Dev server may fail ā retrying install on first request`));
|
|
590
627
|
progress('installing-deps', `Install failed: ${err.message}`);
|
|
591
628
|
}
|
|
592
629
|
}
|
|
593
630
|
else {
|
|
594
|
-
progress('installing-deps', 'Dependencies already installed');
|
|
631
|
+
progress('installing-deps', 'Dependencies already installed (cached)');
|
|
595
632
|
}
|
|
596
|
-
// Start
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
633
|
+
// āā Phase 3: Start dev server + connect tunnel in parallel āāāāāāā
|
|
634
|
+
const tunnel = new TunnelClient({
|
|
635
|
+
gatewayUrl: opts.gatewayUrl,
|
|
636
|
+
token: opts.token,
|
|
637
|
+
projectId,
|
|
638
|
+
apiPort: allocated.apiPort,
|
|
639
|
+
devPort: allocated.devPort,
|
|
601
640
|
});
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
641
|
+
// Connect tunnel immediately (don't wait for dev server ā tunnel is needed for status updates)
|
|
642
|
+
const tunnelPromise = (async () => {
|
|
643
|
+
progress('connecting', 'Connecting tunnel to gateway...');
|
|
644
|
+
try {
|
|
645
|
+
await tunnel.connect();
|
|
646
|
+
timings['tunnel'] = Date.now() - t0;
|
|
647
|
+
console.log(chalk.green(` ā Tunnel connected for project ${projectId} (${timings['tunnel']}ms)`));
|
|
648
|
+
progress('connecting', 'Tunnel connected');
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
console.log(chalk.yellow(` ā Tunnel: ${err.message}`));
|
|
652
|
+
tunnel.startBackgroundReconnect();
|
|
653
|
+
progress('connecting', `Warning: ${err.message} ā reconnecting`);
|
|
654
|
+
}
|
|
655
|
+
})();
|
|
656
|
+
// Start dev server in parallel with tunnel connection
|
|
606
657
|
if (!opts.noDev) {
|
|
607
658
|
if (installer.areDependenciesInstalled()) {
|
|
608
659
|
progress('starting-dev', 'Starting dev server...');
|
|
609
660
|
try {
|
|
661
|
+
const devStart = Date.now();
|
|
610
662
|
await localServer.getDevServer().start();
|
|
611
|
-
|
|
663
|
+
timings['dev-server'] = Date.now() - devStart;
|
|
664
|
+
console.log(chalk.green(` ā Dev server on port ${allocated.devPort} (${timings['dev-server']}ms)`));
|
|
612
665
|
progress('starting-dev', `Dev server on port ${allocated.devPort}`);
|
|
613
666
|
}
|
|
614
667
|
catch (err) {
|
|
@@ -622,40 +675,22 @@ async function startAdditionalProject(projectId, opts) {
|
|
|
622
675
|
progress('starting-dev', 'Skipped ā dependencies not installed');
|
|
623
676
|
}
|
|
624
677
|
}
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
tunnel.startBackgroundReconnect();
|
|
642
|
-
progress('connecting', `Warning: ${err.message} ā reconnecting`);
|
|
643
|
-
}
|
|
644
|
-
// Register project with backend
|
|
645
|
-
try {
|
|
646
|
-
await fetch(`${opts.backendUrl}/api/agent/register`, {
|
|
647
|
-
method: 'POST',
|
|
648
|
-
headers: { 'Authorization': `Bearer ${opts.token}`, 'Content-Type': 'application/json' },
|
|
649
|
-
body: JSON.stringify({
|
|
650
|
-
deviceId: opts.deviceId, name: os.hostname(), hostname: os.hostname(),
|
|
651
|
-
platform: `${os.platform()} ${os.arch()}`, agentVersion: VERSION,
|
|
652
|
-
projectId, capabilities: ['file-sync', 'type-check', 'install', 'terminal', 'dev-server'],
|
|
653
|
-
}),
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
catch { }
|
|
657
|
-
console.log(chalk.green(` ā Project ${projectId} is live!\n`));
|
|
658
|
-
progress('ready', 'Project is live!');
|
|
678
|
+
// Wait for tunnel to be connected
|
|
679
|
+
await tunnelPromise;
|
|
680
|
+
// Register project with backend (non-blocking)
|
|
681
|
+
fetch(`${opts.backendUrl}/api/agent/register`, {
|
|
682
|
+
method: 'POST',
|
|
683
|
+
headers: { 'Authorization': `Bearer ${opts.token}`, 'Content-Type': 'application/json' },
|
|
684
|
+
body: JSON.stringify({
|
|
685
|
+
deviceId: opts.deviceId, name: os.hostname(), hostname: os.hostname(),
|
|
686
|
+
platform: `${os.platform()} ${os.arch()}`, agentVersion: VERSION,
|
|
687
|
+
projectId, capabilities: ['file-sync', 'type-check', 'install', 'terminal', 'dev-server'],
|
|
688
|
+
}),
|
|
689
|
+
}).catch(() => { });
|
|
690
|
+
const totalTime = Date.now() - t0;
|
|
691
|
+
console.log(chalk.green(` ā Project ${projectId} is live! (total: ${totalTime}ms)\n`));
|
|
692
|
+
console.log(chalk.gray(` Timings: ${JSON.stringify(timings)}`));
|
|
693
|
+
progress('ready', `Project is live! (${totalTime}ms)`);
|
|
659
694
|
return { localServer, tunnel };
|
|
660
695
|
}
|
|
661
696
|
catch (err) {
|
package/dist/devServer.js
CHANGED
|
@@ -294,14 +294,15 @@ export class DevServer {
|
|
|
294
294
|
resolve();
|
|
295
295
|
});
|
|
296
296
|
req.on('error', () => {
|
|
297
|
-
setTimeout(check,
|
|
297
|
+
setTimeout(check, 300);
|
|
298
298
|
});
|
|
299
299
|
req.setTimeout(2000, () => {
|
|
300
300
|
req.destroy();
|
|
301
|
-
setTimeout(check,
|
|
301
|
+
setTimeout(check, 300);
|
|
302
302
|
});
|
|
303
303
|
};
|
|
304
|
-
|
|
304
|
+
// Start checking after a short delay (Vite can be ready in <500ms)
|
|
305
|
+
setTimeout(check, 300);
|
|
305
306
|
});
|
|
306
307
|
}
|
|
307
308
|
}
|
package/dist/localServer.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Requests arrive through the tunnel from the gateway.
|
|
14
14
|
*/
|
|
15
15
|
import http from 'http';
|
|
16
|
+
import fs from 'fs';
|
|
16
17
|
import os from 'os';
|
|
17
18
|
import { createRequire } from 'module';
|
|
18
19
|
import { spawn } from 'child_process';
|
|
@@ -202,7 +203,8 @@ export class LocalServer {
|
|
|
202
203
|
'/live/normalize': this.handleNormalize,
|
|
203
204
|
'/live/normalize-batch': this.handleNormalizeBatch,
|
|
204
205
|
'/live/invalidate': this.handleInvalidate,
|
|
205
|
-
'/live/refetch': this.handleRefetch,
|
|
206
|
+
'/live/refetch': this.handleRefetch, // Legacy ā no-op in agent mode
|
|
207
|
+
'/live/reset': this.handleRefetch, // New push-based reset ā also no-op in agent mode
|
|
206
208
|
'/live/grace-period': this.handleGracePeriod,
|
|
207
209
|
'/live/stats': this.handleStats,
|
|
208
210
|
'/live/usage': this.handleUsage,
|
|
@@ -405,6 +407,8 @@ export class LocalServer {
|
|
|
405
407
|
const label = parsed.label || `Terminal ${sessionCounter}`;
|
|
406
408
|
let shell = null;
|
|
407
409
|
let ptyProcess = null;
|
|
410
|
+
// Ensure project directory exists (posix_spawnp fails if cwd is missing)
|
|
411
|
+
const spawnCwd = fs.existsSync(this.options.projectDir) ? this.options.projectDir : process.cwd();
|
|
408
412
|
// Prefer node-pty for real PTY (interactive shell, echo, prompt, resize)
|
|
409
413
|
if (ptyModule) {
|
|
410
414
|
try {
|
|
@@ -412,39 +416,61 @@ export class LocalServer {
|
|
|
412
416
|
name: 'xterm-256color',
|
|
413
417
|
cols,
|
|
414
418
|
rows,
|
|
415
|
-
cwd:
|
|
419
|
+
cwd: spawnCwd,
|
|
416
420
|
env: shellEnv,
|
|
417
421
|
});
|
|
418
422
|
}
|
|
419
423
|
catch (ptyErr) {
|
|
420
424
|
console.warn(` [Terminal] node-pty spawn failed: ${ptyErr.message} ā falling back to script`);
|
|
421
425
|
ptyProcess = null;
|
|
422
|
-
// Fall through to script/spawn fallbacks below
|
|
423
426
|
}
|
|
424
427
|
}
|
|
425
|
-
if (!ptyProcess && !shell
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
428
|
+
if (!ptyProcess && !shell) {
|
|
429
|
+
try {
|
|
430
|
+
if (process.platform === 'darwin') {
|
|
431
|
+
// macOS fallback: use 'script' to allocate a PTY
|
|
432
|
+
shell = spawn('script', ['-q', '/dev/null', shellCmd], {
|
|
433
|
+
cwd: spawnCwd,
|
|
434
|
+
env: shellEnv,
|
|
435
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
else if (process.platform === 'linux') {
|
|
439
|
+
// Linux fallback: 'script' with -c flag
|
|
440
|
+
shell = spawn('script', ['-qc', shellCmd, '/dev/null'], {
|
|
441
|
+
cwd: spawnCwd,
|
|
442
|
+
env: shellEnv,
|
|
443
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Windows / other: raw spawn (limited interactivity)
|
|
448
|
+
shell = spawn(shellCmd, [], {
|
|
449
|
+
cwd: spawnCwd,
|
|
450
|
+
env: shellEnv,
|
|
451
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (scriptErr) {
|
|
456
|
+
console.warn(` [Terminal] script fallback failed: ${scriptErr.message} ā trying raw spawn`);
|
|
457
|
+
}
|
|
440
458
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
459
|
+
// Last resort: raw spawn with no PTY
|
|
460
|
+
if (!ptyProcess && !shell) {
|
|
461
|
+
try {
|
|
462
|
+
shell = spawn(shellCmd, [], {
|
|
463
|
+
cwd: spawnCwd,
|
|
464
|
+
env: shellEnv,
|
|
465
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
catch (rawErr) {
|
|
469
|
+
console.error(` [Terminal] All spawn methods failed: ${rawErr.message}`);
|
|
470
|
+
res.statusCode = 500;
|
|
471
|
+
this.json(res, { success: false, error: `Failed to spawn terminal: ${rawErr.message}` });
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
448
474
|
}
|
|
449
475
|
const session = {
|
|
450
476
|
id: sessionId,
|
|
@@ -566,7 +592,7 @@ export class LocalServer {
|
|
|
566
592
|
connected: true,
|
|
567
593
|
projectId: this.options.projectId,
|
|
568
594
|
agent: {
|
|
569
|
-
version: '0.5.
|
|
595
|
+
version: '0.5.8',
|
|
570
596
|
hostname: os.hostname(),
|
|
571
597
|
platform: `${os.platform()} ${os.arch()}`,
|
|
572
598
|
},
|
|
@@ -21,8 +21,23 @@ export declare class PackageInstaller {
|
|
|
21
21
|
/**
|
|
22
22
|
* Ensure all project dependencies are installed.
|
|
23
23
|
* Uses a lock to prevent concurrent installs.
|
|
24
|
+
* Skips install entirely if package.json + lockfile haven't changed (hash cache).
|
|
24
25
|
*/
|
|
25
26
|
ensureDependencies(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Build the fastest possible install command:
|
|
29
|
+
* - npm ci (clean install from lockfile) if package-lock.json exists
|
|
30
|
+
* - npm install --prefer-offline otherwise
|
|
31
|
+
* - pnpm install --frozen-lockfile if pnpm
|
|
32
|
+
*/
|
|
33
|
+
private buildFastInstallArgs;
|
|
34
|
+
/**
|
|
35
|
+
* Compute a hash of package.json + lockfile contents.
|
|
36
|
+
* Used to skip npm install when nothing has changed.
|
|
37
|
+
*/
|
|
38
|
+
private computeDepsHash;
|
|
39
|
+
private isDepsHashMatch;
|
|
40
|
+
private writeDepsHash;
|
|
26
41
|
/**
|
|
27
42
|
* Check if dependencies are actually installed (not just that node_modules/ exists).
|
|
28
43
|
* Verifies that at least one key dependency from package.json is present.
|
package/dist/packageInstaller.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import fs, { existsSync } from 'fs';
|
|
8
|
+
import crypto from 'crypto';
|
|
8
9
|
export class PackageInstaller {
|
|
9
10
|
projectDir;
|
|
10
11
|
/** Serializes concurrent install calls */
|
|
@@ -39,26 +40,100 @@ export class PackageInstaller {
|
|
|
39
40
|
/**
|
|
40
41
|
* Ensure all project dependencies are installed.
|
|
41
42
|
* Uses a lock to prevent concurrent installs.
|
|
43
|
+
* Skips install entirely if package.json + lockfile haven't changed (hash cache).
|
|
42
44
|
*/
|
|
43
45
|
async ensureDependencies() {
|
|
44
46
|
// Serialize with any in-flight install
|
|
45
47
|
await this.installLock;
|
|
46
|
-
if
|
|
48
|
+
// Fast path: check if deps are cached by hash
|
|
49
|
+
if (this.isDepsHashMatch()) {
|
|
50
|
+
console.log(` [Installer] Dependencies up-to-date (hash match) ā skipping install`);
|
|
47
51
|
return;
|
|
52
|
+
}
|
|
53
|
+
if (this.areDependenciesInstalled()) {
|
|
54
|
+
// node_modules exists and looks valid ā write hash and skip
|
|
55
|
+
this.writeDepsHash();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
48
58
|
// Acquire lock for this install
|
|
49
59
|
let releaseLock;
|
|
50
60
|
this.installLock = new Promise(resolve => { releaseLock = resolve; });
|
|
51
61
|
try {
|
|
52
62
|
console.log(` [Installer] Installing project dependencies...`);
|
|
53
63
|
const pm = this.detectPackageManager();
|
|
54
|
-
const args = pm
|
|
64
|
+
const args = this.buildFastInstallArgs(pm);
|
|
55
65
|
await this.runCommand(pm, args, 300_000);
|
|
66
|
+
this.writeDepsHash();
|
|
56
67
|
console.log(` [Installer] Dependencies installed`);
|
|
57
68
|
}
|
|
58
69
|
finally {
|
|
59
70
|
releaseLock();
|
|
60
71
|
}
|
|
61
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Build the fastest possible install command:
|
|
75
|
+
* - npm ci (clean install from lockfile) if package-lock.json exists
|
|
76
|
+
* - npm install --prefer-offline otherwise
|
|
77
|
+
* - pnpm install --frozen-lockfile if pnpm
|
|
78
|
+
*/
|
|
79
|
+
buildFastInstallArgs(pm) {
|
|
80
|
+
switch (pm) {
|
|
81
|
+
case 'pnpm': {
|
|
82
|
+
const hasLock = existsSync(path.join(this.projectDir, 'pnpm-lock.yaml'));
|
|
83
|
+
return hasLock ? ['install', '--frozen-lockfile', '--prefer-offline'] : ['install'];
|
|
84
|
+
}
|
|
85
|
+
case 'yarn': {
|
|
86
|
+
const hasLock = existsSync(path.join(this.projectDir, 'yarn.lock'));
|
|
87
|
+
return hasLock ? ['install', '--frozen-lockfile', '--prefer-offline'] : ['install'];
|
|
88
|
+
}
|
|
89
|
+
default: {
|
|
90
|
+
// npm: prefer ci (much faster) if lockfile exists
|
|
91
|
+
const hasLock = existsSync(path.join(this.projectDir, 'package-lock.json'));
|
|
92
|
+
return hasLock ? ['ci', '--prefer-offline'] : ['install', '--prefer-offline'];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Compute a hash of package.json + lockfile contents.
|
|
98
|
+
* Used to skip npm install when nothing has changed.
|
|
99
|
+
*/
|
|
100
|
+
computeDepsHash() {
|
|
101
|
+
const hash = crypto.createHash('sha256');
|
|
102
|
+
const pkgPath = path.join(this.projectDir, 'package.json');
|
|
103
|
+
if (existsSync(pkgPath))
|
|
104
|
+
hash.update(fs.readFileSync(pkgPath));
|
|
105
|
+
// Include lockfile if present
|
|
106
|
+
for (const lockFile of ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']) {
|
|
107
|
+
const lockPath = path.join(this.projectDir, lockFile);
|
|
108
|
+
if (existsSync(lockPath)) {
|
|
109
|
+
hash.update(fs.readFileSync(lockPath));
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return hash.digest('hex').slice(0, 16);
|
|
114
|
+
}
|
|
115
|
+
isDepsHashMatch() {
|
|
116
|
+
const hashFile = path.join(this.projectDir, 'node_modules', '.nstantpage-deps-hash');
|
|
117
|
+
if (!existsSync(hashFile))
|
|
118
|
+
return false;
|
|
119
|
+
try {
|
|
120
|
+
const stored = fs.readFileSync(hashFile, 'utf-8').trim();
|
|
121
|
+
return stored === this.computeDepsHash();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
writeDepsHash() {
|
|
128
|
+
const hashFile = path.join(this.projectDir, 'node_modules', '.nstantpage-deps-hash');
|
|
129
|
+
try {
|
|
130
|
+
const dir = path.dirname(hashFile);
|
|
131
|
+
if (!existsSync(dir))
|
|
132
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
133
|
+
fs.writeFileSync(hashFile, this.computeDepsHash(), 'utf-8');
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
}
|
|
62
137
|
/**
|
|
63
138
|
* Check if dependencies are actually installed (not just that node_modules/ exists).
|
|
64
139
|
* Verifies that at least one key dependency from package.json is present.
|
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.8',
|
|
67
67
|
hostname: os.hostname(),
|
|
68
68
|
platform: `${os.platform()} ${os.arch()}`,
|
|
69
69
|
deviceId: getDeviceId(),
|
package/package.json
CHANGED