nstantpage-agent 0.5.11 → 0.5.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.
- package/dist/cli.js +1 -1
- package/dist/commands/start.js +42 -1
- package/dist/devServer.js +5 -1
- package/dist/localServer.d.ts +6 -0
- package/dist/localServer.js +97 -10
- package/dist/projectDb.d.ts +38 -0
- package/dist/projectDb.js +172 -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, writeDatabaseUrlToEnv } from '../projectDb.js';
|
|
28
|
+
const VERSION = '0.5.13';
|
|
28
29
|
/**
|
|
29
30
|
* Resolve the backend API base URL.
|
|
30
31
|
* - If --backend is passed, use it
|
|
@@ -274,12 +275,33 @@ 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
|
+
writeDatabaseUrlToEnv(projectDir, databaseUrl);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
console.log(chalk.yellow(` ⚠ PostgreSQL setup failed: ${err.message} (will use PGlite fallback)`));
|
|
292
|
+
}
|
|
293
|
+
// Build env vars to pass to the dev server
|
|
294
|
+
const serverEnv = {};
|
|
295
|
+
if (databaseUrl) {
|
|
296
|
+
serverEnv['DATABASE_URL'] = databaseUrl;
|
|
297
|
+
}
|
|
277
298
|
// 3. Start local server (API + dev server)
|
|
278
299
|
const localServer = new LocalServer({
|
|
279
300
|
projectDir,
|
|
280
301
|
projectId,
|
|
281
302
|
apiPort,
|
|
282
303
|
devPort,
|
|
304
|
+
env: serverEnv,
|
|
283
305
|
});
|
|
284
306
|
// 4. Create tunnel client
|
|
285
307
|
const tunnel = new TunnelClient({
|
|
@@ -373,6 +395,7 @@ export async function startCommand(directory, options) {
|
|
|
373
395
|
tunnel.disconnect();
|
|
374
396
|
await localServer.stop();
|
|
375
397
|
await disconnectDevice();
|
|
398
|
+
await closeAdminPool();
|
|
376
399
|
clearProjectConfig(projectId);
|
|
377
400
|
process.exit(0);
|
|
378
401
|
};
|
|
@@ -540,6 +563,7 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
|
|
|
540
563
|
proj.tunnel.disconnect();
|
|
541
564
|
await proj.localServer.stop();
|
|
542
565
|
}
|
|
566
|
+
await closeAdminPool();
|
|
543
567
|
try {
|
|
544
568
|
await fetch(`${backendUrl}/api/agent/disconnect`, {
|
|
545
569
|
method: 'POST',
|
|
@@ -584,9 +608,26 @@ async function startAdditionalProject(projectId, opts) {
|
|
|
584
608
|
await new Promise(r => setTimeout(r, 50));
|
|
585
609
|
// ── Phase 1: API server + file fetch + tunnel connect (all parallel) ──
|
|
586
610
|
progress('fetching-files', 'Fetching project files...');
|
|
611
|
+
// Provision database if PostgreSQL is available
|
|
612
|
+
let databaseUrl = null;
|
|
613
|
+
try {
|
|
614
|
+
const hasPg = await probeLocalPostgres();
|
|
615
|
+
if (hasPg) {
|
|
616
|
+
databaseUrl = await ensureLocalProjectDb(projectId);
|
|
617
|
+
if (databaseUrl) {
|
|
618
|
+
console.log(chalk.green(` ✓ Database ready: project_${projectId}`));
|
|
619
|
+
writeDatabaseUrlToEnv(projectDir, databaseUrl);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch { }
|
|
624
|
+
const serverEnv = {};
|
|
625
|
+
if (databaseUrl)
|
|
626
|
+
serverEnv['DATABASE_URL'] = databaseUrl;
|
|
587
627
|
const localServer = new LocalServer({
|
|
588
628
|
projectDir, projectId,
|
|
589
629
|
apiPort: allocated.apiPort, devPort: allocated.devPort,
|
|
630
|
+
env: serverEnv,
|
|
590
631
|
});
|
|
591
632
|
const tunnel = new TunnelClient({
|
|
592
633
|
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, writeDatabaseUrlToEnv } 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,40 @@ 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
|
+
// Write to .env so manual `npm run dev` also works
|
|
135
|
+
writeDatabaseUrlToEnv(this.options.projectDir, dbUrl);
|
|
136
|
+
// Also update the DevServer's env
|
|
137
|
+
this.devServer = new DevServer({
|
|
138
|
+
projectDir: this.options.projectDir,
|
|
139
|
+
port: this.options.devPort,
|
|
140
|
+
env: this.options.env,
|
|
141
|
+
});
|
|
142
|
+
console.log(` [LocalServer] Database provisioned: project_${this.options.projectId}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.warn(` [LocalServer] Database provisioning failed: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
115
150
|
getDevServer() {
|
|
116
151
|
return this.devServer;
|
|
117
152
|
}
|
|
@@ -349,13 +384,14 @@ export class LocalServer {
|
|
|
349
384
|
// ─── /live/terminal ──────────────────────────────────────────
|
|
350
385
|
async handleTerminal(_req, res, body) {
|
|
351
386
|
const data = JSON.parse(body);
|
|
352
|
-
const { command, cwd } = data;
|
|
387
|
+
const { command, cwd, timeoutSeconds } = data;
|
|
353
388
|
if (!command) {
|
|
354
389
|
res.statusCode = 400;
|
|
355
390
|
this.json(res, { error: 'Missing command' });
|
|
356
391
|
return;
|
|
357
392
|
}
|
|
358
|
-
const
|
|
393
|
+
const timeoutMs = timeoutSeconds ? timeoutSeconds * 1000 : undefined;
|
|
394
|
+
const result = await this.devServer.exec(command, timeoutMs);
|
|
359
395
|
this.json(res, {
|
|
360
396
|
success: result.exitCode === 0,
|
|
361
397
|
exitCode: result.exitCode,
|
|
@@ -396,10 +432,46 @@ export class LocalServer {
|
|
|
396
432
|
parsed = body ? JSON.parse(body) : {};
|
|
397
433
|
}
|
|
398
434
|
catch { }
|
|
435
|
+
const isAiSession = parsed.isAiSession || false;
|
|
436
|
+
// Reuse existing AI session if available (prevents spawning new tab per AI command)
|
|
437
|
+
if (isAiSession) {
|
|
438
|
+
const existingAiSession = Array.from(terminalSessions.values())
|
|
439
|
+
.find(s => s.projectId === this.options.projectId && s.isAiSession && !s.exited);
|
|
440
|
+
if (existingAiSession) {
|
|
441
|
+
existingAiSession.lastActivity = Date.now();
|
|
442
|
+
if (parsed.label)
|
|
443
|
+
existingAiSession.label = parsed.label;
|
|
444
|
+
// Write the new command to the existing session's PTY
|
|
445
|
+
if (parsed.command) {
|
|
446
|
+
if (existingAiSession.ptyProcess) {
|
|
447
|
+
existingAiSession.ptyProcess.write(parsed.command + '\n');
|
|
448
|
+
}
|
|
449
|
+
else if (existingAiSession.shell?.stdin) {
|
|
450
|
+
existingAiSession.shell.stdin.write(parsed.command + '\n');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
this.json(res, {
|
|
454
|
+
success: true,
|
|
455
|
+
reused: true,
|
|
456
|
+
session: {
|
|
457
|
+
id: existingAiSession.id,
|
|
458
|
+
projectId: existingAiSession.projectId,
|
|
459
|
+
isAiSession: existingAiSession.isAiSession,
|
|
460
|
+
label: existingAiSession.label,
|
|
461
|
+
createdAt: existingAiSession.createdAt,
|
|
462
|
+
lastActivity: existingAiSession.lastActivity,
|
|
463
|
+
exited: false,
|
|
464
|
+
exitCode: null,
|
|
465
|
+
cols: existingAiSession.cols,
|
|
466
|
+
rows: existingAiSession.rows,
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
399
472
|
const sessionId = generateSessionId();
|
|
400
473
|
const cols = parsed.cols || 120;
|
|
401
474
|
const rows = parsed.rows || 30;
|
|
402
|
-
const isAiSession = parsed.isAiSession || false;
|
|
403
475
|
// Determine shell
|
|
404
476
|
const shellCmd = process.platform === 'win32' ? 'cmd.exe' : (process.env.SHELL || '/bin/bash');
|
|
405
477
|
const shellEnv = { ...process.env, TERM: 'xterm-256color', COLUMNS: String(cols), LINES: String(rows) };
|
|
@@ -505,9 +577,19 @@ export class LocalServer {
|
|
|
505
577
|
shell.on('exit', (code) => handleExit(code));
|
|
506
578
|
}
|
|
507
579
|
terminalSessions.set(sessionId, session);
|
|
580
|
+
// Write initial command to PTY if provided (AI session)
|
|
581
|
+
if (parsed.command) {
|
|
582
|
+
if (ptyProcess) {
|
|
583
|
+
ptyProcess.write(parsed.command + '\n');
|
|
584
|
+
}
|
|
585
|
+
else if (shell?.stdin) {
|
|
586
|
+
shell.stdin.write(parsed.command + '\n');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
508
589
|
// Return format matching frontend TerminalSessionInfo
|
|
509
590
|
this.json(res, {
|
|
510
591
|
success: true,
|
|
592
|
+
reused: false,
|
|
511
593
|
session: {
|
|
512
594
|
id: sessionId,
|
|
513
595
|
projectId: this.options.projectId,
|
|
@@ -573,7 +655,7 @@ export class LocalServer {
|
|
|
573
655
|
connected: true,
|
|
574
656
|
projectId: this.options.projectId,
|
|
575
657
|
agent: {
|
|
576
|
-
version: '0.5.
|
|
658
|
+
version: '0.5.11',
|
|
577
659
|
hostname: os.hostname(),
|
|
578
660
|
platform: `${os.platform()} ${os.arch()}`,
|
|
579
661
|
},
|
|
@@ -595,9 +677,10 @@ export class LocalServer {
|
|
|
595
677
|
async handleContainerStats(_req, res) {
|
|
596
678
|
const devStats = this.devServer.getStats();
|
|
597
679
|
const totalMemMb = Math.round(os.totalmem() / (1024 * 1024));
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
680
|
+
// Use process-specific memory from devStats, not system-wide (os.freemem)
|
|
681
|
+
// os.freemem() on macOS counts filesystem cache as "used", showing ~100%
|
|
682
|
+
const processMemMb = Math.round(devStats.memoryMb);
|
|
683
|
+
const memPercent = totalMemMb > 0 ? Math.round((processMemMb / totalMemMb) * 1000) / 10 : 0;
|
|
601
684
|
// Return in the same format as the container stats (ContainerStatsSnapshot)
|
|
602
685
|
// so the frontend terminal panel can display it unchanged.
|
|
603
686
|
this.json(res, {
|
|
@@ -605,10 +688,10 @@ export class LocalServer {
|
|
|
605
688
|
running: this.devServer.isRunning,
|
|
606
689
|
agentMode: true,
|
|
607
690
|
stats: {
|
|
608
|
-
cpuPercent: devStats.cpuPercent,
|
|
609
|
-
memoryUsageBytes:
|
|
691
|
+
cpuPercent: Math.round(devStats.cpuPercent * 10) / 10,
|
|
692
|
+
memoryUsageBytes: processMemMb * 1024 * 1024,
|
|
610
693
|
memoryLimitBytes: totalMemMb * 1024 * 1024,
|
|
611
|
-
memoryUsageMb:
|
|
694
|
+
memoryUsageMb: processMemMb,
|
|
612
695
|
memoryLimitMb: totalMemMb,
|
|
613
696
|
memoryPercent: memPercent,
|
|
614
697
|
diskUsageMb: null,
|
|
@@ -640,6 +723,8 @@ export class LocalServer {
|
|
|
640
723
|
// ─── /live/dev ───────────────────────────────────────────────
|
|
641
724
|
async handleDev(_req, res, body) {
|
|
642
725
|
try {
|
|
726
|
+
// Ensure database is provisioned before starting backend
|
|
727
|
+
await this.ensureDatabase();
|
|
643
728
|
// Ensure dependencies are installed
|
|
644
729
|
await this.packageInstaller.ensureDependencies();
|
|
645
730
|
// Start dev server
|
|
@@ -675,6 +760,8 @@ export class LocalServer {
|
|
|
675
760
|
// ─── /live/open ──────────────────────────────────────────────
|
|
676
761
|
async handleOpen(_req, res) {
|
|
677
762
|
try {
|
|
763
|
+
// Ensure database is provisioned before starting backend
|
|
764
|
+
await this.ensureDatabase();
|
|
678
765
|
// Ensure deps are installed
|
|
679
766
|
await this.packageInstaller.ensureDependencies();
|
|
680
767
|
// Start dev server if not running
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
* Write DATABASE_URL to the project's .env file so manual `npm run dev` works.
|
|
29
|
+
* - Creates .env if it doesn't exist
|
|
30
|
+
* - Updates DATABASE_URL if already present but different
|
|
31
|
+
* - Appends DATABASE_URL if not present
|
|
32
|
+
* Idempotent — safe to call multiple times.
|
|
33
|
+
*/
|
|
34
|
+
export declare function writeDatabaseUrlToEnv(projectDir: string, databaseUrl: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Close the admin pool (for graceful shutdown).
|
|
37
|
+
*/
|
|
38
|
+
export declare function closeAdminPool(): Promise<void>;
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
// Connection config — detect environment
|
|
16
|
+
const PG_HOST = process.env.PG_HOST || '127.0.0.1';
|
|
17
|
+
const PG_PORT = process.env.PG_PORT || '5432';
|
|
18
|
+
// Local macOS (Homebrew PG): current OS username, no password, trust auth
|
|
19
|
+
const PG_USER = process.env.PG_USER || os.userInfo().username;
|
|
20
|
+
const PG_PASS = process.env.PG_PASS || '';
|
|
21
|
+
let pgAvailable = null; // null = not yet probed
|
|
22
|
+
let adminPool = null;
|
|
23
|
+
const verifiedDbs = new Set();
|
|
24
|
+
/**
|
|
25
|
+
* Get or create the admin connection pool (connects to 'postgres' database).
|
|
26
|
+
*/
|
|
27
|
+
async function getAdminPool() {
|
|
28
|
+
if (!adminPool) {
|
|
29
|
+
const pg = await import('pg');
|
|
30
|
+
adminPool = new pg.default.Pool({
|
|
31
|
+
host: PG_HOST,
|
|
32
|
+
port: parseInt(PG_PORT),
|
|
33
|
+
user: PG_USER,
|
|
34
|
+
password: PG_PASS,
|
|
35
|
+
database: 'postgres',
|
|
36
|
+
max: 2,
|
|
37
|
+
idleTimeoutMillis: 30000,
|
|
38
|
+
connectionTimeoutMillis: 5000,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return adminPool;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Probe for a local PostgreSQL instance.
|
|
45
|
+
* Returns true if PG is accessible on localhost.
|
|
46
|
+
*/
|
|
47
|
+
export async function probeLocalPostgres() {
|
|
48
|
+
if (pgAvailable !== null)
|
|
49
|
+
return pgAvailable;
|
|
50
|
+
try {
|
|
51
|
+
const pool = await getAdminPool();
|
|
52
|
+
const client = await pool.connect();
|
|
53
|
+
await client.query('SELECT 1');
|
|
54
|
+
client.release();
|
|
55
|
+
pgAvailable = true;
|
|
56
|
+
console.log(` [ProjectDb] Auto-detected PostgreSQL at ${PG_HOST}:${PG_PORT} (user: ${PG_USER})`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
pgAvailable = false;
|
|
61
|
+
// Reset admin pool so it doesn't hold broken connections
|
|
62
|
+
if (adminPool) {
|
|
63
|
+
adminPool.end().catch(() => { });
|
|
64
|
+
adminPool = null;
|
|
65
|
+
}
|
|
66
|
+
console.log(` [ProjectDb] No PostgreSQL found at ${PG_HOST}:${PG_PORT} — projects will use PGlite fallback (${err.message})`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if local PostgreSQL is available (must call probeLocalPostgres first).
|
|
72
|
+
*/
|
|
73
|
+
export function isLocalPgAvailable() {
|
|
74
|
+
return pgAvailable === true;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Sanitize project ID to safe database name.
|
|
78
|
+
*/
|
|
79
|
+
function dbName(projectId) {
|
|
80
|
+
const sanitized = projectId.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
81
|
+
return `project_${sanitized}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build a DATABASE_URL for a project.
|
|
85
|
+
*/
|
|
86
|
+
function buildDatabaseUrl(name) {
|
|
87
|
+
const passStr = PG_PASS ? `:${PG_PASS}` : '';
|
|
88
|
+
return `postgresql://${PG_USER}${passStr}@${PG_HOST}:${PG_PORT}/${name}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Ensure a project database exists and return its DATABASE_URL.
|
|
92
|
+
* Idempotent — safe to call multiple times.
|
|
93
|
+
* Returns null if PostgreSQL is not available.
|
|
94
|
+
*/
|
|
95
|
+
export async function ensureLocalProjectDb(projectId) {
|
|
96
|
+
if (!pgAvailable)
|
|
97
|
+
return null;
|
|
98
|
+
const name = dbName(projectId);
|
|
99
|
+
// Fast path: already verified this session
|
|
100
|
+
if (verifiedDbs.has(name)) {
|
|
101
|
+
return buildDatabaseUrl(name);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const pool = await getAdminPool();
|
|
105
|
+
// Check if database exists
|
|
106
|
+
const { rows } = await pool.query('SELECT 1 FROM pg_database WHERE datname = $1', [name]);
|
|
107
|
+
if (rows.length === 0) {
|
|
108
|
+
// Create database — CREATE DATABASE can't use parameterized queries
|
|
109
|
+
await pool.query(`CREATE DATABASE ${name}`);
|
|
110
|
+
console.log(` [ProjectDb] Created database '${name}' for project ${projectId}`);
|
|
111
|
+
}
|
|
112
|
+
verifiedDbs.add(name);
|
|
113
|
+
return buildDatabaseUrl(name);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
// Handle race condition: duplicate_database
|
|
117
|
+
if (err.code === '42P04') {
|
|
118
|
+
verifiedDbs.add(name);
|
|
119
|
+
return buildDatabaseUrl(name);
|
|
120
|
+
}
|
|
121
|
+
console.error(` [ProjectDb] Failed to ensure database for ${projectId}:`, err.message);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Write DATABASE_URL to the project's .env file so manual `npm run dev` works.
|
|
127
|
+
* - Creates .env if it doesn't exist
|
|
128
|
+
* - Updates DATABASE_URL if already present but different
|
|
129
|
+
* - Appends DATABASE_URL if not present
|
|
130
|
+
* Idempotent — safe to call multiple times.
|
|
131
|
+
*/
|
|
132
|
+
export function writeDatabaseUrlToEnv(projectDir, databaseUrl) {
|
|
133
|
+
const envPath = path.join(projectDir, '.env');
|
|
134
|
+
try {
|
|
135
|
+
let content = '';
|
|
136
|
+
if (fs.existsSync(envPath)) {
|
|
137
|
+
content = fs.readFileSync(envPath, 'utf-8');
|
|
138
|
+
}
|
|
139
|
+
const lines = content.split('\n');
|
|
140
|
+
const dbLineIdx = lines.findIndex(l => l.startsWith('DATABASE_URL='));
|
|
141
|
+
if (dbLineIdx >= 0) {
|
|
142
|
+
// Already has DATABASE_URL — update if different
|
|
143
|
+
const existing = lines[dbLineIdx].split('=').slice(1).join('=');
|
|
144
|
+
if (existing === databaseUrl)
|
|
145
|
+
return; // already correct
|
|
146
|
+
lines[dbLineIdx] = `DATABASE_URL=${databaseUrl}`;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Append DATABASE_URL
|
|
150
|
+
// Ensure there's a newline before appending if file doesn't end with one
|
|
151
|
+
if (content.length > 0 && !content.endsWith('\n')) {
|
|
152
|
+
lines.push('');
|
|
153
|
+
}
|
|
154
|
+
lines.push(`DATABASE_URL=${databaseUrl}`);
|
|
155
|
+
}
|
|
156
|
+
fs.writeFileSync(envPath, lines.join('\n'), 'utf-8');
|
|
157
|
+
console.log(` [ProjectDb] Wrote DATABASE_URL to ${envPath}`);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.warn(` [ProjectDb] Failed to write .env: ${err.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Close the admin pool (for graceful shutdown).
|
|
165
|
+
*/
|
|
166
|
+
export async function closeAdminPool() {
|
|
167
|
+
if (adminPool) {
|
|
168
|
+
await adminPool.end();
|
|
169
|
+
adminPool = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# 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.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": {
|
|
@@ -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"
|