nstantpage-agent 0.5.11 → 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 +95 -10
- package/dist/projectDb.d.ts +30 -0
- package/dist/projectDb.js +132 -0
- package/dist/tunnel.js +1 -1
- package/package.json +3 -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.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) };
|
|
@@ -505,9 +575,19 @@ export class LocalServer {
|
|
|
505
575
|
shell.on('exit', (code) => handleExit(code));
|
|
506
576
|
}
|
|
507
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
|
+
}
|
|
508
587
|
// Return format matching frontend TerminalSessionInfo
|
|
509
588
|
this.json(res, {
|
|
510
589
|
success: true,
|
|
590
|
+
reused: false,
|
|
511
591
|
session: {
|
|
512
592
|
id: sessionId,
|
|
513
593
|
projectId: this.options.projectId,
|
|
@@ -573,7 +653,7 @@ export class LocalServer {
|
|
|
573
653
|
connected: true,
|
|
574
654
|
projectId: this.options.projectId,
|
|
575
655
|
agent: {
|
|
576
|
-
version: '0.5.
|
|
656
|
+
version: '0.5.11',
|
|
577
657
|
hostname: os.hostname(),
|
|
578
658
|
platform: `${os.platform()} ${os.arch()}`,
|
|
579
659
|
},
|
|
@@ -595,9 +675,10 @@ export class LocalServer {
|
|
|
595
675
|
async handleContainerStats(_req, res) {
|
|
596
676
|
const devStats = this.devServer.getStats();
|
|
597
677
|
const totalMemMb = Math.round(os.totalmem() / (1024 * 1024));
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
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;
|
|
601
682
|
// Return in the same format as the container stats (ContainerStatsSnapshot)
|
|
602
683
|
// so the frontend terminal panel can display it unchanged.
|
|
603
684
|
this.json(res, {
|
|
@@ -605,10 +686,10 @@ export class LocalServer {
|
|
|
605
686
|
running: this.devServer.isRunning,
|
|
606
687
|
agentMode: true,
|
|
607
688
|
stats: {
|
|
608
|
-
cpuPercent: devStats.cpuPercent,
|
|
609
|
-
memoryUsageBytes:
|
|
689
|
+
cpuPercent: Math.round(devStats.cpuPercent * 10) / 10,
|
|
690
|
+
memoryUsageBytes: processMemMb * 1024 * 1024,
|
|
610
691
|
memoryLimitBytes: totalMemMb * 1024 * 1024,
|
|
611
|
-
memoryUsageMb:
|
|
692
|
+
memoryUsageMb: processMemMb,
|
|
612
693
|
memoryLimitMb: totalMemMb,
|
|
613
694
|
memoryPercent: memPercent,
|
|
614
695
|
diskUsageMb: null,
|
|
@@ -640,6 +721,8 @@ export class LocalServer {
|
|
|
640
721
|
// ─── /live/dev ───────────────────────────────────────────────
|
|
641
722
|
async handleDev(_req, res, body) {
|
|
642
723
|
try {
|
|
724
|
+
// Ensure database is provisioned before starting backend
|
|
725
|
+
await this.ensureDatabase();
|
|
643
726
|
// Ensure dependencies are installed
|
|
644
727
|
await this.packageInstaller.ensureDependencies();
|
|
645
728
|
// Start dev server
|
|
@@ -675,6 +758,8 @@ export class LocalServer {
|
|
|
675
758
|
// ─── /live/open ──────────────────────────────────────────────
|
|
676
759
|
async handleOpen(_req, res) {
|
|
677
760
|
try {
|
|
761
|
+
// Ensure database is provisioned before starting backend
|
|
762
|
+
await this.ensureDatabase();
|
|
678
763
|
// Ensure deps are installed
|
|
679
764
|
await this.packageInstaller.ensureDependencies();
|
|
680
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,12 +49,14 @@
|
|
|
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"
|