nstantpage-agent 0.5.19 → 0.5.21
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 +1 -1
- package/dist/localServer.d.ts +12 -0
- package/dist/localServer.js +255 -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.21');
|
|
29
29
|
program
|
|
30
30
|
.command('login')
|
|
31
31
|
.description('Authenticate with nstantpage.com')
|
package/dist/commands/start.js
CHANGED
|
@@ -25,7 +25,7 @@ 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.
|
|
28
|
+
const VERSION = '0.5.21';
|
|
29
29
|
/**
|
|
30
30
|
* Resolve the backend API base URL.
|
|
31
31
|
* - If --backend is passed, use it
|
package/dist/localServer.d.ts
CHANGED
|
@@ -113,7 +113,19 @@ export declare class LocalServer {
|
|
|
113
113
|
private handleGracePeriod;
|
|
114
114
|
private handleStats;
|
|
115
115
|
private handleUsage;
|
|
116
|
+
private handleBuild;
|
|
116
117
|
private handleHealth;
|
|
118
|
+
/**
|
|
119
|
+
* Get a pg Pool connected to this project's local database.
|
|
120
|
+
* Lazily created and cached.
|
|
121
|
+
*/
|
|
122
|
+
private _dbPool;
|
|
123
|
+
private getDbPool;
|
|
124
|
+
private handleDbTables;
|
|
125
|
+
private handleDbColumns;
|
|
126
|
+
private handleDbRows;
|
|
127
|
+
private handleDbQuery;
|
|
128
|
+
private handleDbCreate;
|
|
117
129
|
private json;
|
|
118
130
|
private collectBody;
|
|
119
131
|
}
|
package/dist/localServer.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import http from 'http';
|
|
16
16
|
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
17
18
|
import os from 'os';
|
|
18
19
|
import { createRequire } from 'module';
|
|
19
20
|
import { spawn } from 'child_process';
|
|
@@ -22,7 +23,7 @@ import { FileManager } from './fileManager.js';
|
|
|
22
23
|
import { Checker } from './checker.js';
|
|
23
24
|
import { ErrorStore, structuredErrorToString } from './errorStore.js';
|
|
24
25
|
import { PackageInstaller } from './packageInstaller.js';
|
|
25
|
-
import { probeLocalPostgres, ensureLocalProjectDb, writeDatabaseUrlToEnv } from './projectDb.js';
|
|
26
|
+
import { probeLocalPostgres, ensureLocalProjectDb, isLocalPgAvailable, writeDatabaseUrlToEnv } from './projectDb.js';
|
|
26
27
|
// ─── Try to load node-pty for real PTY support ─────────────────
|
|
27
28
|
let ptyModule = null;
|
|
28
29
|
try {
|
|
@@ -243,6 +244,12 @@ export class LocalServer {
|
|
|
243
244
|
'/live/grace-period': this.handleGracePeriod,
|
|
244
245
|
'/live/stats': this.handleStats,
|
|
245
246
|
'/live/usage': this.handleUsage,
|
|
247
|
+
'/live/db/tables': this.handleDbTables,
|
|
248
|
+
'/live/db/columns': this.handleDbColumns,
|
|
249
|
+
'/live/db/rows': this.handleDbRows,
|
|
250
|
+
'/live/db/query': this.handleDbQuery,
|
|
251
|
+
'/live/db/create': this.handleDbCreate,
|
|
252
|
+
'/live/build': this.handleBuild,
|
|
246
253
|
'/health': this.handleHealth,
|
|
247
254
|
};
|
|
248
255
|
if (handlers[path])
|
|
@@ -932,6 +939,80 @@ export class LocalServer {
|
|
|
932
939
|
},
|
|
933
940
|
});
|
|
934
941
|
}
|
|
942
|
+
// ─── /live/build — run production build and return artifacts ──
|
|
943
|
+
async handleBuild(_req, res) {
|
|
944
|
+
const projectDir = this.options.projectDir;
|
|
945
|
+
console.log(` [LocalServer] Running production build for project ${this.options.projectId}...`);
|
|
946
|
+
try {
|
|
947
|
+
// Determine build command
|
|
948
|
+
const hasBuildScript = fs.existsSync(path.join(projectDir, 'script', 'build.ts'));
|
|
949
|
+
const buildCmd = hasBuildScript
|
|
950
|
+
? 'npx tsx script/build.ts'
|
|
951
|
+
: 'npx vite build';
|
|
952
|
+
// Run build
|
|
953
|
+
const buildResult = await new Promise((resolve) => {
|
|
954
|
+
const proc = spawn('sh', ['-c', buildCmd], {
|
|
955
|
+
cwd: projectDir,
|
|
956
|
+
env: { ...process.env, ...this.options.env, NODE_OPTIONS: '--max-old-space-size=512' },
|
|
957
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
958
|
+
});
|
|
959
|
+
let stdout = '', stderr = '';
|
|
960
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
961
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
962
|
+
proc.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
963
|
+
proc.on('error', (err) => resolve({ exitCode: 1, stdout: '', stderr: err.message }));
|
|
964
|
+
});
|
|
965
|
+
if (buildResult.exitCode !== 0) {
|
|
966
|
+
console.error(` [LocalServer] Build failed: ${buildResult.stderr}`);
|
|
967
|
+
this.json(res, { success: false, error: buildResult.stderr || 'Build failed' });
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
// Collect dist/ files
|
|
971
|
+
const distDir = path.join(projectDir, 'dist');
|
|
972
|
+
if (!fs.existsSync(distDir)) {
|
|
973
|
+
this.json(res, { success: false, error: 'dist/ not found after build' });
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const files = {};
|
|
977
|
+
const collectFiles = (dir, prefix) => {
|
|
978
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
979
|
+
for (const entry of entries) {
|
|
980
|
+
const fullPath = path.join(dir, entry.name);
|
|
981
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
982
|
+
if (entry.isDirectory()) {
|
|
983
|
+
collectFiles(fullPath, relPath);
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
// Base64-encode binary files, utf-8 for text
|
|
987
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
988
|
+
const isBinary = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.ogg', '.wav'].includes(ext);
|
|
989
|
+
if (isBinary) {
|
|
990
|
+
files[relPath] = 'base64:' + fs.readFileSync(fullPath).toString('base64');
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
files[relPath] = fs.readFileSync(fullPath, 'utf-8');
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
collectFiles(distDir, '');
|
|
999
|
+
// Check for backend
|
|
1000
|
+
const hasBackend = fs.existsSync(path.join(projectDir, 'server', 'index.ts')) ||
|
|
1001
|
+
fs.existsSync(path.join(projectDir, 'server', 'index.js'));
|
|
1002
|
+
console.log(` [LocalServer] Build completed: ${Object.keys(files).length} files (backend=${hasBackend})`);
|
|
1003
|
+
this.json(res, {
|
|
1004
|
+
success: true,
|
|
1005
|
+
files,
|
|
1006
|
+
fileCount: Object.keys(files).length,
|
|
1007
|
+
hasBackend,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
catch (err) {
|
|
1011
|
+
console.error(` [LocalServer] Build error: ${err.message}`);
|
|
1012
|
+
res.statusCode = 500;
|
|
1013
|
+
this.json(res, { success: false, error: err.message });
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
935
1016
|
// ─── /health ─────────────────────────────────────────────────
|
|
936
1017
|
async handleHealth(_req, res) {
|
|
937
1018
|
this.json(res, {
|
|
@@ -942,6 +1023,179 @@ export class LocalServer {
|
|
|
942
1023
|
uptime: this.devServer.uptime,
|
|
943
1024
|
});
|
|
944
1025
|
}
|
|
1026
|
+
// ─── /live/db/* — Data Studio endpoints (local PostgreSQL) ──
|
|
1027
|
+
/**
|
|
1028
|
+
* Get a pg Pool connected to this project's local database.
|
|
1029
|
+
* Lazily created and cached.
|
|
1030
|
+
*/
|
|
1031
|
+
_dbPool = null;
|
|
1032
|
+
async getDbPool() {
|
|
1033
|
+
if (this._dbPool)
|
|
1034
|
+
return this._dbPool;
|
|
1035
|
+
const pg = await import('pg');
|
|
1036
|
+
const dbUrl = this.options.env?.['DATABASE_URL'];
|
|
1037
|
+
if (dbUrl) {
|
|
1038
|
+
this._dbPool = new pg.default.Pool({ connectionString: dbUrl, max: 3, idleTimeoutMillis: 30000 });
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
// Fallback: construct from defaults
|
|
1042
|
+
const dbName = `project_${this.options.projectId.replace(/[^a-zA-Z0-9_]/g, '_')}`;
|
|
1043
|
+
this._dbPool = new pg.default.Pool({
|
|
1044
|
+
host: '127.0.0.1', port: 5432,
|
|
1045
|
+
user: os.userInfo().username, password: '',
|
|
1046
|
+
database: dbName, max: 3, idleTimeoutMillis: 30000,
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
return this._dbPool;
|
|
1050
|
+
}
|
|
1051
|
+
// GET /live/db/tables
|
|
1052
|
+
async handleDbTables(_req, res) {
|
|
1053
|
+
try {
|
|
1054
|
+
const pool = await this.getDbPool();
|
|
1055
|
+
const { rows: tableRows } = await pool.query(`
|
|
1056
|
+
SELECT c.relname AS table_name
|
|
1057
|
+
FROM pg_class c
|
|
1058
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
1059
|
+
WHERE n.nspname = 'public' AND c.relkind = 'r'
|
|
1060
|
+
ORDER BY c.relname
|
|
1061
|
+
`);
|
|
1062
|
+
const tables = [];
|
|
1063
|
+
for (const t of tableRows) {
|
|
1064
|
+
try {
|
|
1065
|
+
const { rows } = await pool.query(`SELECT COUNT(*)::text AS cnt FROM "${t.table_name}"`);
|
|
1066
|
+
tables.push({ table_name: t.table_name, row_count: rows[0].cnt });
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
tables.push({ table_name: t.table_name, row_count: '?' });
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
this.json(res, { tables, dbExists: true });
|
|
1073
|
+
}
|
|
1074
|
+
catch (err) {
|
|
1075
|
+
if (err.message?.includes('does not exist')) {
|
|
1076
|
+
this.json(res, { tables: [], dbExists: false, pgAvailable: isLocalPgAvailable() });
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
res.statusCode = 500;
|
|
1080
|
+
this.json(res, { error: err.message });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
// GET /live/db/columns?table=xxx
|
|
1085
|
+
async handleDbColumns(_req, res, _body, url) {
|
|
1086
|
+
const table = url.searchParams.get('table');
|
|
1087
|
+
if (!table) {
|
|
1088
|
+
res.statusCode = 400;
|
|
1089
|
+
this.json(res, { error: 'Missing table' });
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
const pool = await this.getDbPool();
|
|
1094
|
+
const { rows } = await pool.query(`
|
|
1095
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
1096
|
+
FROM information_schema.columns
|
|
1097
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
1098
|
+
ORDER BY ordinal_position
|
|
1099
|
+
`, [table]);
|
|
1100
|
+
this.json(res, { columns: rows });
|
|
1101
|
+
}
|
|
1102
|
+
catch (err) {
|
|
1103
|
+
res.statusCode = 500;
|
|
1104
|
+
this.json(res, { error: err.message });
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
// GET /live/db/rows?table=xxx&limit=100&offset=0
|
|
1108
|
+
async handleDbRows(_req, res, _body, url) {
|
|
1109
|
+
const table = url.searchParams.get('table');
|
|
1110
|
+
if (!table) {
|
|
1111
|
+
res.statusCode = 400;
|
|
1112
|
+
this.json(res, { error: 'Missing table' });
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit') || '100', 10), 500);
|
|
1116
|
+
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
|
1117
|
+
const safe = table.replace(/[^a-zA-Z0-9_]/g, '');
|
|
1118
|
+
try {
|
|
1119
|
+
const pool = await this.getDbPool();
|
|
1120
|
+
const countResult = await pool.query(`SELECT COUNT(*)::int AS total FROM "${safe}"`);
|
|
1121
|
+
const total = countResult.rows[0]?.total || 0;
|
|
1122
|
+
const { rows } = await pool.query(`SELECT * FROM "${safe}" ORDER BY 1 LIMIT $1 OFFSET $2`, [limit, offset]);
|
|
1123
|
+
this.json(res, { rows, total });
|
|
1124
|
+
}
|
|
1125
|
+
catch (err) {
|
|
1126
|
+
res.statusCode = 500;
|
|
1127
|
+
this.json(res, { error: err.message });
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// POST /live/db/query — body: { sql, readOnly? }
|
|
1131
|
+
async handleDbQuery(_req, res, body) {
|
|
1132
|
+
let parsed;
|
|
1133
|
+
try {
|
|
1134
|
+
parsed = JSON.parse(body);
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
res.statusCode = 400;
|
|
1138
|
+
this.json(res, { error: 'Invalid JSON' });
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (!parsed.sql || typeof parsed.sql !== 'string') {
|
|
1142
|
+
res.statusCode = 400;
|
|
1143
|
+
this.json(res, { error: 'Missing sql' });
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const readOnly = parsed.readOnly !== false;
|
|
1147
|
+
try {
|
|
1148
|
+
const pool = await this.getDbPool();
|
|
1149
|
+
const client = await pool.connect();
|
|
1150
|
+
try {
|
|
1151
|
+
await client.query(readOnly ? 'BEGIN READ ONLY' : 'BEGIN');
|
|
1152
|
+
const result = await client.query(parsed.sql);
|
|
1153
|
+
await client.query('COMMIT');
|
|
1154
|
+
this.json(res, {
|
|
1155
|
+
rows: result.rows,
|
|
1156
|
+
fields: result.fields?.map((f) => ({ name: f.name, dataTypeID: f.dataTypeID })) || [],
|
|
1157
|
+
rowCount: result.rowCount || 0,
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
catch (err) {
|
|
1161
|
+
await client.query('ROLLBACK').catch(() => { });
|
|
1162
|
+
throw err;
|
|
1163
|
+
}
|
|
1164
|
+
finally {
|
|
1165
|
+
client.release();
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
catch (err) {
|
|
1169
|
+
res.statusCode = 400;
|
|
1170
|
+
this.json(res, { error: err.message });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
// POST /live/db/create — create/ensure project database
|
|
1174
|
+
async handleDbCreate(_req, res) {
|
|
1175
|
+
try {
|
|
1176
|
+
const dbUrl = await ensureLocalProjectDb(this.options.projectId);
|
|
1177
|
+
if (dbUrl) {
|
|
1178
|
+
if (!this.options.env)
|
|
1179
|
+
this.options.env = {};
|
|
1180
|
+
this.options.env['DATABASE_URL'] = dbUrl;
|
|
1181
|
+
writeDatabaseUrlToEnv(this.options.projectDir, dbUrl);
|
|
1182
|
+
// Recreate pool with fresh URL
|
|
1183
|
+
if (this._dbPool) {
|
|
1184
|
+
await this._dbPool.end().catch(() => { });
|
|
1185
|
+
this._dbPool = null;
|
|
1186
|
+
}
|
|
1187
|
+
this.json(res, { success: true, database: `project_${this.options.projectId}` });
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
res.statusCode = 503;
|
|
1191
|
+
this.json(res, { error: 'PostgreSQL is not available on this machine' });
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
res.statusCode = 500;
|
|
1196
|
+
this.json(res, { error: err.message });
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
945
1199
|
// ─── Helpers ─────────────────────────────────────────────────
|
|
946
1200
|
json(res, data) {
|
|
947
1201
|
res.setHeader('Content-Type', 'application/json');
|
package/package.json
CHANGED