nstantpage-agent 0.5.10 → 0.5.12
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 +40 -1
- package/dist/devServer.js +5 -1
- package/dist/localServer.d.ts +6 -0
- package/dist/localServer.js +113 -47
- package/dist/projectDb.d.ts +30 -0
- package/dist/projectDb.js +132 -0
- package/dist/tunnel.js +1 -1
- package/package.json +4 -2
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.11');
|
|
29
29
|
program
|
|
30
30
|
.command('login')
|
|
31
31
|
.description('Authenticate with nstantpage.com')
|
package/dist/commands/start.js
CHANGED
|
@@ -24,7 +24,8 @@ 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
|
-
|
|
27
|
+
import { probeLocalPostgres, ensureLocalProjectDb, closeAdminPool } from '../projectDb.js';
|
|
28
|
+
const VERSION = '0.5.12';
|
|
28
29
|
/**
|
|
29
30
|
* Resolve the backend API base URL.
|
|
30
31
|
* - If --backend is passed, use it
|
|
@@ -274,12 +275,32 @@ export async function startCommand(directory, options) {
|
|
|
274
275
|
if (!fs.existsSync(localConfigPath)) {
|
|
275
276
|
fs.writeFileSync(localConfigPath, JSON.stringify({ projectId }, null, 2), 'utf-8');
|
|
276
277
|
}
|
|
278
|
+
// 2.5. Auto-detect local PostgreSQL and provision project database
|
|
279
|
+
let databaseUrl = null;
|
|
280
|
+
try {
|
|
281
|
+
const hasPg = await probeLocalPostgres();
|
|
282
|
+
if (hasPg) {
|
|
283
|
+
databaseUrl = await ensureLocalProjectDb(projectId);
|
|
284
|
+
if (databaseUrl) {
|
|
285
|
+
console.log(chalk.green(` ✓ Database ready: project_${projectId}`));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
console.log(chalk.yellow(` ⚠ PostgreSQL setup failed: ${err.message} (will use PGlite fallback)`));
|
|
291
|
+
}
|
|
292
|
+
// Build env vars to pass to the dev server
|
|
293
|
+
const serverEnv = {};
|
|
294
|
+
if (databaseUrl) {
|
|
295
|
+
serverEnv['DATABASE_URL'] = databaseUrl;
|
|
296
|
+
}
|
|
277
297
|
// 3. Start local server (API + dev server)
|
|
278
298
|
const localServer = new LocalServer({
|
|
279
299
|
projectDir,
|
|
280
300
|
projectId,
|
|
281
301
|
apiPort,
|
|
282
302
|
devPort,
|
|
303
|
+
env: serverEnv,
|
|
283
304
|
});
|
|
284
305
|
// 4. Create tunnel client
|
|
285
306
|
const tunnel = new TunnelClient({
|
|
@@ -373,6 +394,7 @@ export async function startCommand(directory, options) {
|
|
|
373
394
|
tunnel.disconnect();
|
|
374
395
|
await localServer.stop();
|
|
375
396
|
await disconnectDevice();
|
|
397
|
+
await closeAdminPool();
|
|
376
398
|
clearProjectConfig(projectId);
|
|
377
399
|
process.exit(0);
|
|
378
400
|
};
|
|
@@ -540,6 +562,7 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
|
|
|
540
562
|
proj.tunnel.disconnect();
|
|
541
563
|
await proj.localServer.stop();
|
|
542
564
|
}
|
|
565
|
+
await closeAdminPool();
|
|
543
566
|
try {
|
|
544
567
|
await fetch(`${backendUrl}/api/agent/disconnect`, {
|
|
545
568
|
method: 'POST',
|
|
@@ -584,9 +607,25 @@ async function startAdditionalProject(projectId, opts) {
|
|
|
584
607
|
await new Promise(r => setTimeout(r, 50));
|
|
585
608
|
// ── Phase 1: API server + file fetch + tunnel connect (all parallel) ──
|
|
586
609
|
progress('fetching-files', 'Fetching project files...');
|
|
610
|
+
// Provision database if PostgreSQL is available
|
|
611
|
+
let databaseUrl = null;
|
|
612
|
+
try {
|
|
613
|
+
const hasPg = await probeLocalPostgres();
|
|
614
|
+
if (hasPg) {
|
|
615
|
+
databaseUrl = await ensureLocalProjectDb(projectId);
|
|
616
|
+
if (databaseUrl) {
|
|
617
|
+
console.log(chalk.green(` ✓ Database ready: project_${projectId}`));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
catch { }
|
|
622
|
+
const serverEnv = {};
|
|
623
|
+
if (databaseUrl)
|
|
624
|
+
serverEnv['DATABASE_URL'] = databaseUrl;
|
|
587
625
|
const localServer = new LocalServer({
|
|
588
626
|
projectDir, projectId,
|
|
589
627
|
apiPort: allocated.apiPort, devPort: allocated.devPort,
|
|
628
|
+
env: serverEnv,
|
|
590
629
|
});
|
|
591
630
|
const tunnel = new TunnelClient({
|
|
592
631
|
gatewayUrl: opts.gatewayUrl,
|
package/dist/devServer.js
CHANGED
|
@@ -65,6 +65,7 @@ export class DevServer {
|
|
|
65
65
|
...process.env,
|
|
66
66
|
...extraEnv,
|
|
67
67
|
PORT: String(backendPort),
|
|
68
|
+
SERVER_PORT: String(backendPort),
|
|
68
69
|
NODE_ENV: 'development',
|
|
69
70
|
},
|
|
70
71
|
shell: true,
|
|
@@ -100,7 +101,10 @@ export class DevServer {
|
|
|
100
101
|
PORT: String(port),
|
|
101
102
|
};
|
|
102
103
|
if (hasBackend) {
|
|
103
|
-
|
|
104
|
+
const backendPort = port + 1001;
|
|
105
|
+
frontendEnv['VITE_BACKEND_PORT'] = String(backendPort);
|
|
106
|
+
// SERVER_PORT is used by vite.config.ts proxy to route /api requests
|
|
107
|
+
frontendEnv['SERVER_PORT'] = String(backendPort);
|
|
104
108
|
}
|
|
105
109
|
console.log(` [DevServer] Starting: ${cmd} ${args.join(' ')} (port ${port})`);
|
|
106
110
|
this.process = spawn(cmd, args, {
|
package/dist/localServer.d.ts
CHANGED
|
@@ -69,6 +69,12 @@ export declare class LocalServer {
|
|
|
69
69
|
private packageInstaller;
|
|
70
70
|
private lastHeartbeat;
|
|
71
71
|
constructor(options: LocalServerOptions);
|
|
72
|
+
/**
|
|
73
|
+
* Ensure the project database is provisioned and DATABASE_URL is set in the DevServer env.
|
|
74
|
+
* Called lazily on /live/open and /live/dev to handle the case where PostgreSQL
|
|
75
|
+
* wasn't available during agent startup but is now (or was never probed).
|
|
76
|
+
*/
|
|
77
|
+
private ensureDatabase;
|
|
72
78
|
getDevServer(): DevServer;
|
|
73
79
|
getApiPort(): number;
|
|
74
80
|
getDevPort(): number;
|
package/dist/localServer.js
CHANGED
|
@@ -22,6 +22,7 @@ import { FileManager } from './fileManager.js';
|
|
|
22
22
|
import { Checker } from './checker.js';
|
|
23
23
|
import { ErrorStore, structuredErrorToString } from './errorStore.js';
|
|
24
24
|
import { PackageInstaller } from './packageInstaller.js';
|
|
25
|
+
import { probeLocalPostgres, ensureLocalProjectDb } from './projectDb.js';
|
|
25
26
|
// ─── Try to load node-pty for real PTY support ─────────────────
|
|
26
27
|
let ptyModule = null;
|
|
27
28
|
try {
|
|
@@ -112,6 +113,38 @@ export class LocalServer {
|
|
|
112
113
|
this.errorStore = new ErrorStore();
|
|
113
114
|
this.packageInstaller = new PackageInstaller({ projectDir: options.projectDir });
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Ensure the project database is provisioned and DATABASE_URL is set in the DevServer env.
|
|
118
|
+
* Called lazily on /live/open and /live/dev to handle the case where PostgreSQL
|
|
119
|
+
* wasn't available during agent startup but is now (or was never probed).
|
|
120
|
+
*/
|
|
121
|
+
async ensureDatabase() {
|
|
122
|
+
// Skip if DATABASE_URL is already set in the env
|
|
123
|
+
if (this.options.env?.['DATABASE_URL'])
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
const hasPg = await probeLocalPostgres();
|
|
127
|
+
if (hasPg) {
|
|
128
|
+
const dbUrl = await ensureLocalProjectDb(this.options.projectId);
|
|
129
|
+
if (dbUrl) {
|
|
130
|
+
// Update env for future DevServer starts
|
|
131
|
+
if (!this.options.env)
|
|
132
|
+
this.options.env = {};
|
|
133
|
+
this.options.env['DATABASE_URL'] = dbUrl;
|
|
134
|
+
// Also update the DevServer's env
|
|
135
|
+
this.devServer = new DevServer({
|
|
136
|
+
projectDir: this.options.projectDir,
|
|
137
|
+
port: this.options.devPort,
|
|
138
|
+
env: this.options.env,
|
|
139
|
+
});
|
|
140
|
+
console.log(` [LocalServer] Database provisioned: project_${this.options.projectId}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.warn(` [LocalServer] Database provisioning failed: ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
115
148
|
getDevServer() {
|
|
116
149
|
return this.devServer;
|
|
117
150
|
}
|
|
@@ -349,13 +382,14 @@ export class LocalServer {
|
|
|
349
382
|
// ─── /live/terminal ──────────────────────────────────────────
|
|
350
383
|
async handleTerminal(_req, res, body) {
|
|
351
384
|
const data = JSON.parse(body);
|
|
352
|
-
const { command, cwd } = data;
|
|
385
|
+
const { command, cwd, timeoutSeconds } = data;
|
|
353
386
|
if (!command) {
|
|
354
387
|
res.statusCode = 400;
|
|
355
388
|
this.json(res, { error: 'Missing command' });
|
|
356
389
|
return;
|
|
357
390
|
}
|
|
358
|
-
const
|
|
391
|
+
const timeoutMs = timeoutSeconds ? timeoutSeconds * 1000 : undefined;
|
|
392
|
+
const result = await this.devServer.exec(command, timeoutMs);
|
|
359
393
|
this.json(res, {
|
|
360
394
|
success: result.exitCode === 0,
|
|
361
395
|
exitCode: result.exitCode,
|
|
@@ -396,10 +430,46 @@ export class LocalServer {
|
|
|
396
430
|
parsed = body ? JSON.parse(body) : {};
|
|
397
431
|
}
|
|
398
432
|
catch { }
|
|
433
|
+
const isAiSession = parsed.isAiSession || false;
|
|
434
|
+
// Reuse existing AI session if available (prevents spawning new tab per AI command)
|
|
435
|
+
if (isAiSession) {
|
|
436
|
+
const existingAiSession = Array.from(terminalSessions.values())
|
|
437
|
+
.find(s => s.projectId === this.options.projectId && s.isAiSession && !s.exited);
|
|
438
|
+
if (existingAiSession) {
|
|
439
|
+
existingAiSession.lastActivity = Date.now();
|
|
440
|
+
if (parsed.label)
|
|
441
|
+
existingAiSession.label = parsed.label;
|
|
442
|
+
// Write the new command to the existing session's PTY
|
|
443
|
+
if (parsed.command) {
|
|
444
|
+
if (existingAiSession.ptyProcess) {
|
|
445
|
+
existingAiSession.ptyProcess.write(parsed.command + '\n');
|
|
446
|
+
}
|
|
447
|
+
else if (existingAiSession.shell?.stdin) {
|
|
448
|
+
existingAiSession.shell.stdin.write(parsed.command + '\n');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
this.json(res, {
|
|
452
|
+
success: true,
|
|
453
|
+
reused: true,
|
|
454
|
+
session: {
|
|
455
|
+
id: existingAiSession.id,
|
|
456
|
+
projectId: existingAiSession.projectId,
|
|
457
|
+
isAiSession: existingAiSession.isAiSession,
|
|
458
|
+
label: existingAiSession.label,
|
|
459
|
+
createdAt: existingAiSession.createdAt,
|
|
460
|
+
lastActivity: existingAiSession.lastActivity,
|
|
461
|
+
exited: false,
|
|
462
|
+
exitCode: null,
|
|
463
|
+
cols: existingAiSession.cols,
|
|
464
|
+
rows: existingAiSession.rows,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
399
470
|
const sessionId = generateSessionId();
|
|
400
471
|
const cols = parsed.cols || 120;
|
|
401
472
|
const rows = parsed.rows || 30;
|
|
402
|
-
const isAiSession = parsed.isAiSession || false;
|
|
403
473
|
// Determine shell
|
|
404
474
|
const shellCmd = process.platform === 'win32' ? 'cmd.exe' : (process.env.SHELL || '/bin/bash');
|
|
405
475
|
const shellEnv = { ...process.env, TERM: 'xterm-256color', COLUMNS: String(cols), LINES: String(rows) };
|
|
@@ -426,50 +496,31 @@ export class LocalServer {
|
|
|
426
496
|
}
|
|
427
497
|
}
|
|
428
498
|
if (!ptyProcess && !shell) {
|
|
499
|
+
// Fallback: raw spawn (no PTY, limited interactivity but works everywhere)
|
|
500
|
+
// NOTE: 'script' wrapper doesn't work with piped stdio (tcgetattr fails),
|
|
501
|
+
// so we skip it and go straight to raw shell spawn.
|
|
429
502
|
try {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
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)
|
|
503
|
+
shell = spawn(shellCmd, ['-i'], {
|
|
504
|
+
cwd: spawnCwd,
|
|
505
|
+
env: shellEnv,
|
|
506
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch (spawnErr) {
|
|
510
|
+
console.warn(` [Terminal] Interactive shell failed: ${spawnErr.message} — trying non-interactive`);
|
|
511
|
+
try {
|
|
448
512
|
shell = spawn(shellCmd, [], {
|
|
449
513
|
cwd: spawnCwd,
|
|
450
514
|
env: shellEnv,
|
|
451
515
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
452
516
|
});
|
|
453
517
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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;
|
|
518
|
+
catch (rawErr) {
|
|
519
|
+
console.error(` [Terminal] All spawn methods failed: ${rawErr.message}`);
|
|
520
|
+
res.statusCode = 500;
|
|
521
|
+
this.json(res, { success: false, error: `Failed to spawn terminal: ${rawErr.message}` });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
473
524
|
}
|
|
474
525
|
}
|
|
475
526
|
const session = {
|
|
@@ -524,9 +575,19 @@ export class LocalServer {
|
|
|
524
575
|
shell.on('exit', (code) => handleExit(code));
|
|
525
576
|
}
|
|
526
577
|
terminalSessions.set(sessionId, session);
|
|
578
|
+
// Write initial command to PTY if provided (AI session)
|
|
579
|
+
if (parsed.command) {
|
|
580
|
+
if (ptyProcess) {
|
|
581
|
+
ptyProcess.write(parsed.command + '\n');
|
|
582
|
+
}
|
|
583
|
+
else if (shell?.stdin) {
|
|
584
|
+
shell.stdin.write(parsed.command + '\n');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
527
587
|
// Return format matching frontend TerminalSessionInfo
|
|
528
588
|
this.json(res, {
|
|
529
589
|
success: true,
|
|
590
|
+
reused: false,
|
|
530
591
|
session: {
|
|
531
592
|
id: sessionId,
|
|
532
593
|
projectId: this.options.projectId,
|
|
@@ -592,7 +653,7 @@ export class LocalServer {
|
|
|
592
653
|
connected: true,
|
|
593
654
|
projectId: this.options.projectId,
|
|
594
655
|
agent: {
|
|
595
|
-
version: '0.5.
|
|
656
|
+
version: '0.5.11',
|
|
596
657
|
hostname: os.hostname(),
|
|
597
658
|
platform: `${os.platform()} ${os.arch()}`,
|
|
598
659
|
},
|
|
@@ -614,9 +675,10 @@ export class LocalServer {
|
|
|
614
675
|
async handleContainerStats(_req, res) {
|
|
615
676
|
const devStats = this.devServer.getStats();
|
|
616
677
|
const totalMemMb = Math.round(os.totalmem() / (1024 * 1024));
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
678
|
+
// Use process-specific memory from devStats, not system-wide (os.freemem)
|
|
679
|
+
// os.freemem() on macOS counts filesystem cache as "used", showing ~100%
|
|
680
|
+
const processMemMb = Math.round(devStats.memoryMb);
|
|
681
|
+
const memPercent = totalMemMb > 0 ? Math.round((processMemMb / totalMemMb) * 1000) / 10 : 0;
|
|
620
682
|
// Return in the same format as the container stats (ContainerStatsSnapshot)
|
|
621
683
|
// so the frontend terminal panel can display it unchanged.
|
|
622
684
|
this.json(res, {
|
|
@@ -624,10 +686,10 @@ export class LocalServer {
|
|
|
624
686
|
running: this.devServer.isRunning,
|
|
625
687
|
agentMode: true,
|
|
626
688
|
stats: {
|
|
627
|
-
cpuPercent: devStats.cpuPercent,
|
|
628
|
-
memoryUsageBytes:
|
|
689
|
+
cpuPercent: Math.round(devStats.cpuPercent * 10) / 10,
|
|
690
|
+
memoryUsageBytes: processMemMb * 1024 * 1024,
|
|
629
691
|
memoryLimitBytes: totalMemMb * 1024 * 1024,
|
|
630
|
-
memoryUsageMb:
|
|
692
|
+
memoryUsageMb: processMemMb,
|
|
631
693
|
memoryLimitMb: totalMemMb,
|
|
632
694
|
memoryPercent: memPercent,
|
|
633
695
|
diskUsageMb: null,
|
|
@@ -659,6 +721,8 @@ export class LocalServer {
|
|
|
659
721
|
// ─── /live/dev ───────────────────────────────────────────────
|
|
660
722
|
async handleDev(_req, res, body) {
|
|
661
723
|
try {
|
|
724
|
+
// Ensure database is provisioned before starting backend
|
|
725
|
+
await this.ensureDatabase();
|
|
662
726
|
// Ensure dependencies are installed
|
|
663
727
|
await this.packageInstaller.ensureDependencies();
|
|
664
728
|
// Start dev server
|
|
@@ -694,6 +758,8 @@ export class LocalServer {
|
|
|
694
758
|
// ─── /live/open ──────────────────────────────────────────────
|
|
695
759
|
async handleOpen(_req, res) {
|
|
696
760
|
try {
|
|
761
|
+
// Ensure database is provisioned before starting backend
|
|
762
|
+
await this.ensureDatabase();
|
|
697
763
|
// Ensure deps are installed
|
|
698
764
|
await this.packageInstaller.ensureDependencies();
|
|
699
765
|
// Start dev server if not running
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local PostgreSQL database provisioning for the nstantpage agent.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the PreviewGateway's projectDb.ts but for local (non-Docker) environments.
|
|
5
|
+
* On macOS, this detects Homebrew PostgreSQL (or any PG on localhost:5432).
|
|
6
|
+
* Each project gets an isolated database: project_{id}
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const url = await ensureLocalProjectDb('131');
|
|
10
|
+
* // → "postgresql://username:@127.0.0.1:5432/project_131"
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Probe for a local PostgreSQL instance.
|
|
14
|
+
* Returns true if PG is accessible on localhost.
|
|
15
|
+
*/
|
|
16
|
+
export declare function probeLocalPostgres(): Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Check if local PostgreSQL is available (must call probeLocalPostgres first).
|
|
19
|
+
*/
|
|
20
|
+
export declare function isLocalPgAvailable(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Ensure a project database exists and return its DATABASE_URL.
|
|
23
|
+
* Idempotent — safe to call multiple times.
|
|
24
|
+
* Returns null if PostgreSQL is not available.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ensureLocalProjectDb(projectId: string): Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Close the admin pool (for graceful shutdown).
|
|
29
|
+
*/
|
|
30
|
+
export declare function closeAdminPool(): Promise<void>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local PostgreSQL database provisioning for the nstantpage agent.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the PreviewGateway's projectDb.ts but for local (non-Docker) environments.
|
|
5
|
+
* On macOS, this detects Homebrew PostgreSQL (or any PG on localhost:5432).
|
|
6
|
+
* Each project gets an isolated database: project_{id}
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const url = await ensureLocalProjectDb('131');
|
|
10
|
+
* // → "postgresql://username:@127.0.0.1:5432/project_131"
|
|
11
|
+
*/
|
|
12
|
+
import os from 'os';
|
|
13
|
+
// Connection config — detect environment
|
|
14
|
+
const PG_HOST = process.env.PG_HOST || '127.0.0.1';
|
|
15
|
+
const PG_PORT = process.env.PG_PORT || '5432';
|
|
16
|
+
// Local macOS (Homebrew PG): current OS username, no password, trust auth
|
|
17
|
+
const PG_USER = process.env.PG_USER || os.userInfo().username;
|
|
18
|
+
const PG_PASS = process.env.PG_PASS || '';
|
|
19
|
+
let pgAvailable = null; // null = not yet probed
|
|
20
|
+
let adminPool = null;
|
|
21
|
+
const verifiedDbs = new Set();
|
|
22
|
+
/**
|
|
23
|
+
* Get or create the admin connection pool (connects to 'postgres' database).
|
|
24
|
+
*/
|
|
25
|
+
async function getAdminPool() {
|
|
26
|
+
if (!adminPool) {
|
|
27
|
+
const pg = await import('pg');
|
|
28
|
+
adminPool = new pg.default.Pool({
|
|
29
|
+
host: PG_HOST,
|
|
30
|
+
port: parseInt(PG_PORT),
|
|
31
|
+
user: PG_USER,
|
|
32
|
+
password: PG_PASS,
|
|
33
|
+
database: 'postgres',
|
|
34
|
+
max: 2,
|
|
35
|
+
idleTimeoutMillis: 30000,
|
|
36
|
+
connectionTimeoutMillis: 5000,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return adminPool;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Probe for a local PostgreSQL instance.
|
|
43
|
+
* Returns true if PG is accessible on localhost.
|
|
44
|
+
*/
|
|
45
|
+
export async function probeLocalPostgres() {
|
|
46
|
+
if (pgAvailable !== null)
|
|
47
|
+
return pgAvailable;
|
|
48
|
+
try {
|
|
49
|
+
const pool = await getAdminPool();
|
|
50
|
+
const client = await pool.connect();
|
|
51
|
+
await client.query('SELECT 1');
|
|
52
|
+
client.release();
|
|
53
|
+
pgAvailable = true;
|
|
54
|
+
console.log(` [ProjectDb] Auto-detected PostgreSQL at ${PG_HOST}:${PG_PORT} (user: ${PG_USER})`);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
pgAvailable = false;
|
|
59
|
+
// Reset admin pool so it doesn't hold broken connections
|
|
60
|
+
if (adminPool) {
|
|
61
|
+
adminPool.end().catch(() => { });
|
|
62
|
+
adminPool = null;
|
|
63
|
+
}
|
|
64
|
+
console.log(` [ProjectDb] No PostgreSQL found at ${PG_HOST}:${PG_PORT} — projects will use PGlite fallback (${err.message})`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if local PostgreSQL is available (must call probeLocalPostgres first).
|
|
70
|
+
*/
|
|
71
|
+
export function isLocalPgAvailable() {
|
|
72
|
+
return pgAvailable === true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Sanitize project ID to safe database name.
|
|
76
|
+
*/
|
|
77
|
+
function dbName(projectId) {
|
|
78
|
+
const sanitized = projectId.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
79
|
+
return `project_${sanitized}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build a DATABASE_URL for a project.
|
|
83
|
+
*/
|
|
84
|
+
function buildDatabaseUrl(name) {
|
|
85
|
+
const passStr = PG_PASS ? `:${PG_PASS}` : '';
|
|
86
|
+
return `postgresql://${PG_USER}${passStr}@${PG_HOST}:${PG_PORT}/${name}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Ensure a project database exists and return its DATABASE_URL.
|
|
90
|
+
* Idempotent — safe to call multiple times.
|
|
91
|
+
* Returns null if PostgreSQL is not available.
|
|
92
|
+
*/
|
|
93
|
+
export async function ensureLocalProjectDb(projectId) {
|
|
94
|
+
if (!pgAvailable)
|
|
95
|
+
return null;
|
|
96
|
+
const name = dbName(projectId);
|
|
97
|
+
// Fast path: already verified this session
|
|
98
|
+
if (verifiedDbs.has(name)) {
|
|
99
|
+
return buildDatabaseUrl(name);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const pool = await getAdminPool();
|
|
103
|
+
// Check if database exists
|
|
104
|
+
const { rows } = await pool.query('SELECT 1 FROM pg_database WHERE datname = $1', [name]);
|
|
105
|
+
if (rows.length === 0) {
|
|
106
|
+
// Create database — CREATE DATABASE can't use parameterized queries
|
|
107
|
+
await pool.query(`CREATE DATABASE ${name}`);
|
|
108
|
+
console.log(` [ProjectDb] Created database '${name}' for project ${projectId}`);
|
|
109
|
+
}
|
|
110
|
+
verifiedDbs.add(name);
|
|
111
|
+
return buildDatabaseUrl(name);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// Handle race condition: duplicate_database
|
|
115
|
+
if (err.code === '42P04') {
|
|
116
|
+
verifiedDbs.add(name);
|
|
117
|
+
return buildDatabaseUrl(name);
|
|
118
|
+
}
|
|
119
|
+
console.error(` [ProjectDb] Failed to ensure database for ${projectId}:`, err.message);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Close the admin pool (for graceful shutdown).
|
|
125
|
+
*/
|
|
126
|
+
export async function closeAdminPool() {
|
|
127
|
+
if (adminPool) {
|
|
128
|
+
await adminPool.end();
|
|
129
|
+
adminPool = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=projectDb.js.map
|
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.11',
|
|
67
67
|
hostname: os.hostname(),
|
|
68
68
|
platform: `${os.platform()} ${os.arch()}`,
|
|
69
69
|
deviceId: getDeviceId(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nstantpage-agent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.12",
|
|
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": {
|
|
@@ -49,17 +49,19 @@
|
|
|
49
49
|
"express": "^4.21.0",
|
|
50
50
|
"http-proxy": "^1.18.1",
|
|
51
51
|
"open": "^10.1.0",
|
|
52
|
+
"pg": "^8.13.0",
|
|
52
53
|
"ws": "^8.18.0"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@types/express": "^4.17.21",
|
|
56
57
|
"@types/http-proxy": "^1.17.14",
|
|
57
58
|
"@types/node": "^20.14.0",
|
|
59
|
+
"@types/pg": "^8.18.0",
|
|
58
60
|
"@types/ws": "^8.5.10",
|
|
59
61
|
"tsx": "^4.19.0",
|
|
60
62
|
"typescript": "^5.5.0"
|
|
61
63
|
},
|
|
62
64
|
"optionalDependencies": {
|
|
63
|
-
"node-pty": "^1.
|
|
65
|
+
"node-pty": "^1.2.0-beta.11"
|
|
64
66
|
}
|
|
65
67
|
}
|