nstantpage-agent 0.5.18 → 0.5.20
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 +11 -0
- package/dist/localServer.js +179 -1
- package/dist/projectDb.js +52 -0
- 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.20');
|
|
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.20';
|
|
29
29
|
/**
|
|
30
30
|
* Resolve the backend API base URL.
|
|
31
31
|
* - If --backend is passed, use it
|
package/dist/localServer.d.ts
CHANGED
|
@@ -114,6 +114,17 @@ export declare class LocalServer {
|
|
|
114
114
|
private handleStats;
|
|
115
115
|
private handleUsage;
|
|
116
116
|
private handleHealth;
|
|
117
|
+
/**
|
|
118
|
+
* Get a pg Pool connected to this project's local database.
|
|
119
|
+
* Lazily created and cached.
|
|
120
|
+
*/
|
|
121
|
+
private _dbPool;
|
|
122
|
+
private getDbPool;
|
|
123
|
+
private handleDbTables;
|
|
124
|
+
private handleDbColumns;
|
|
125
|
+
private handleDbRows;
|
|
126
|
+
private handleDbQuery;
|
|
127
|
+
private handleDbCreate;
|
|
117
128
|
private json;
|
|
118
129
|
private collectBody;
|
|
119
130
|
}
|
package/dist/localServer.js
CHANGED
|
@@ -22,7 +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
|
+
import { probeLocalPostgres, ensureLocalProjectDb, isLocalPgAvailable, writeDatabaseUrlToEnv } from './projectDb.js';
|
|
26
26
|
// ─── Try to load node-pty for real PTY support ─────────────────
|
|
27
27
|
let ptyModule = null;
|
|
28
28
|
try {
|
|
@@ -243,6 +243,11 @@ export class LocalServer {
|
|
|
243
243
|
'/live/grace-period': this.handleGracePeriod,
|
|
244
244
|
'/live/stats': this.handleStats,
|
|
245
245
|
'/live/usage': this.handleUsage,
|
|
246
|
+
'/live/db/tables': this.handleDbTables,
|
|
247
|
+
'/live/db/columns': this.handleDbColumns,
|
|
248
|
+
'/live/db/rows': this.handleDbRows,
|
|
249
|
+
'/live/db/query': this.handleDbQuery,
|
|
250
|
+
'/live/db/create': this.handleDbCreate,
|
|
246
251
|
'/health': this.handleHealth,
|
|
247
252
|
};
|
|
248
253
|
if (handlers[path])
|
|
@@ -942,6 +947,179 @@ export class LocalServer {
|
|
|
942
947
|
uptime: this.devServer.uptime,
|
|
943
948
|
});
|
|
944
949
|
}
|
|
950
|
+
// ─── /live/db/* — Data Studio endpoints (local PostgreSQL) ──
|
|
951
|
+
/**
|
|
952
|
+
* Get a pg Pool connected to this project's local database.
|
|
953
|
+
* Lazily created and cached.
|
|
954
|
+
*/
|
|
955
|
+
_dbPool = null;
|
|
956
|
+
async getDbPool() {
|
|
957
|
+
if (this._dbPool)
|
|
958
|
+
return this._dbPool;
|
|
959
|
+
const pg = await import('pg');
|
|
960
|
+
const dbUrl = this.options.env?.['DATABASE_URL'];
|
|
961
|
+
if (dbUrl) {
|
|
962
|
+
this._dbPool = new pg.default.Pool({ connectionString: dbUrl, max: 3, idleTimeoutMillis: 30000 });
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
// Fallback: construct from defaults
|
|
966
|
+
const dbName = `project_${this.options.projectId.replace(/[^a-zA-Z0-9_]/g, '_')}`;
|
|
967
|
+
this._dbPool = new pg.default.Pool({
|
|
968
|
+
host: '127.0.0.1', port: 5432,
|
|
969
|
+
user: os.userInfo().username, password: '',
|
|
970
|
+
database: dbName, max: 3, idleTimeoutMillis: 30000,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return this._dbPool;
|
|
974
|
+
}
|
|
975
|
+
// GET /live/db/tables
|
|
976
|
+
async handleDbTables(_req, res) {
|
|
977
|
+
try {
|
|
978
|
+
const pool = await this.getDbPool();
|
|
979
|
+
const { rows: tableRows } = await pool.query(`
|
|
980
|
+
SELECT c.relname AS table_name
|
|
981
|
+
FROM pg_class c
|
|
982
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
983
|
+
WHERE n.nspname = 'public' AND c.relkind = 'r'
|
|
984
|
+
ORDER BY c.relname
|
|
985
|
+
`);
|
|
986
|
+
const tables = [];
|
|
987
|
+
for (const t of tableRows) {
|
|
988
|
+
try {
|
|
989
|
+
const { rows } = await pool.query(`SELECT COUNT(*)::text AS cnt FROM "${t.table_name}"`);
|
|
990
|
+
tables.push({ table_name: t.table_name, row_count: rows[0].cnt });
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
tables.push({ table_name: t.table_name, row_count: '?' });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
this.json(res, { tables, dbExists: true });
|
|
997
|
+
}
|
|
998
|
+
catch (err) {
|
|
999
|
+
if (err.message?.includes('does not exist')) {
|
|
1000
|
+
this.json(res, { tables: [], dbExists: false, pgAvailable: isLocalPgAvailable() });
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
res.statusCode = 500;
|
|
1004
|
+
this.json(res, { error: err.message });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// GET /live/db/columns?table=xxx
|
|
1009
|
+
async handleDbColumns(_req, res, _body, url) {
|
|
1010
|
+
const table = url.searchParams.get('table');
|
|
1011
|
+
if (!table) {
|
|
1012
|
+
res.statusCode = 400;
|
|
1013
|
+
this.json(res, { error: 'Missing table' });
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
const pool = await this.getDbPool();
|
|
1018
|
+
const { rows } = await pool.query(`
|
|
1019
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
1020
|
+
FROM information_schema.columns
|
|
1021
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
1022
|
+
ORDER BY ordinal_position
|
|
1023
|
+
`, [table]);
|
|
1024
|
+
this.json(res, { columns: rows });
|
|
1025
|
+
}
|
|
1026
|
+
catch (err) {
|
|
1027
|
+
res.statusCode = 500;
|
|
1028
|
+
this.json(res, { error: err.message });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// GET /live/db/rows?table=xxx&limit=100&offset=0
|
|
1032
|
+
async handleDbRows(_req, res, _body, url) {
|
|
1033
|
+
const table = url.searchParams.get('table');
|
|
1034
|
+
if (!table) {
|
|
1035
|
+
res.statusCode = 400;
|
|
1036
|
+
this.json(res, { error: 'Missing table' });
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit') || '100', 10), 500);
|
|
1040
|
+
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
|
1041
|
+
const safe = table.replace(/[^a-zA-Z0-9_]/g, '');
|
|
1042
|
+
try {
|
|
1043
|
+
const pool = await this.getDbPool();
|
|
1044
|
+
const countResult = await pool.query(`SELECT COUNT(*)::int AS total FROM "${safe}"`);
|
|
1045
|
+
const total = countResult.rows[0]?.total || 0;
|
|
1046
|
+
const { rows } = await pool.query(`SELECT * FROM "${safe}" ORDER BY 1 LIMIT $1 OFFSET $2`, [limit, offset]);
|
|
1047
|
+
this.json(res, { rows, total });
|
|
1048
|
+
}
|
|
1049
|
+
catch (err) {
|
|
1050
|
+
res.statusCode = 500;
|
|
1051
|
+
this.json(res, { error: err.message });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// POST /live/db/query — body: { sql, readOnly? }
|
|
1055
|
+
async handleDbQuery(_req, res, body) {
|
|
1056
|
+
let parsed;
|
|
1057
|
+
try {
|
|
1058
|
+
parsed = JSON.parse(body);
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
res.statusCode = 400;
|
|
1062
|
+
this.json(res, { error: 'Invalid JSON' });
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (!parsed.sql || typeof parsed.sql !== 'string') {
|
|
1066
|
+
res.statusCode = 400;
|
|
1067
|
+
this.json(res, { error: 'Missing sql' });
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
const readOnly = parsed.readOnly !== false;
|
|
1071
|
+
try {
|
|
1072
|
+
const pool = await this.getDbPool();
|
|
1073
|
+
const client = await pool.connect();
|
|
1074
|
+
try {
|
|
1075
|
+
await client.query(readOnly ? 'BEGIN READ ONLY' : 'BEGIN');
|
|
1076
|
+
const result = await client.query(parsed.sql);
|
|
1077
|
+
await client.query('COMMIT');
|
|
1078
|
+
this.json(res, {
|
|
1079
|
+
rows: result.rows,
|
|
1080
|
+
fields: result.fields?.map((f) => ({ name: f.name, dataTypeID: f.dataTypeID })) || [],
|
|
1081
|
+
rowCount: result.rowCount || 0,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
catch (err) {
|
|
1085
|
+
await client.query('ROLLBACK').catch(() => { });
|
|
1086
|
+
throw err;
|
|
1087
|
+
}
|
|
1088
|
+
finally {
|
|
1089
|
+
client.release();
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
catch (err) {
|
|
1093
|
+
res.statusCode = 400;
|
|
1094
|
+
this.json(res, { error: err.message });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
// POST /live/db/create — create/ensure project database
|
|
1098
|
+
async handleDbCreate(_req, res) {
|
|
1099
|
+
try {
|
|
1100
|
+
const dbUrl = await ensureLocalProjectDb(this.options.projectId);
|
|
1101
|
+
if (dbUrl) {
|
|
1102
|
+
if (!this.options.env)
|
|
1103
|
+
this.options.env = {};
|
|
1104
|
+
this.options.env['DATABASE_URL'] = dbUrl;
|
|
1105
|
+
writeDatabaseUrlToEnv(this.options.projectDir, dbUrl);
|
|
1106
|
+
// Recreate pool with fresh URL
|
|
1107
|
+
if (this._dbPool) {
|
|
1108
|
+
await this._dbPool.end().catch(() => { });
|
|
1109
|
+
this._dbPool = null;
|
|
1110
|
+
}
|
|
1111
|
+
this.json(res, { success: true, database: `project_${this.options.projectId}` });
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
res.statusCode = 503;
|
|
1115
|
+
this.json(res, { error: 'PostgreSQL is not available on this machine' });
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
catch (err) {
|
|
1119
|
+
res.statusCode = 500;
|
|
1120
|
+
this.json(res, { error: err.message });
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
945
1123
|
// ─── Helpers ─────────────────────────────────────────────────
|
|
946
1124
|
json(res, data) {
|
|
947
1125
|
res.setHeader('Content-Type', 'application/json');
|
package/dist/projectDb.js
CHANGED
|
@@ -109,6 +109,58 @@ export async function ensureLocalProjectDb(projectId) {
|
|
|
109
109
|
await pool.query(`CREATE DATABASE ${name}`);
|
|
110
110
|
console.log(` [ProjectDb] Created database '${name}' for project ${projectId}`);
|
|
111
111
|
}
|
|
112
|
+
// Ensure the OS user has full access to all objects in the project database.
|
|
113
|
+
// Tables may have been created by 'postgres' (cloud migrations or container)
|
|
114
|
+
// but the agent connects as the OS user — grant privileges to prevent permission errors.
|
|
115
|
+
try {
|
|
116
|
+
const pg = await import('pg');
|
|
117
|
+
const projectPool = new pg.default.Pool({
|
|
118
|
+
host: PG_HOST,
|
|
119
|
+
port: parseInt(PG_PORT),
|
|
120
|
+
user: PG_USER,
|
|
121
|
+
password: PG_PASS,
|
|
122
|
+
database: name,
|
|
123
|
+
max: 1,
|
|
124
|
+
connectionTimeoutMillis: 5000,
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
// Check if there are tables owned by someone else
|
|
128
|
+
const { rows: foreignTables } = await projectPool.query(`SELECT tablename, tableowner FROM pg_tables WHERE schemaname = 'public' AND tableowner != $1`, [PG_USER]);
|
|
129
|
+
if (foreignTables.length > 0) {
|
|
130
|
+
// Need to grant access — use the admin pool which connects to 'postgres' db,
|
|
131
|
+
// but we need a connection to the project db as the owner. Try via psql.
|
|
132
|
+
const adminProjectPool = new pg.default.Pool({
|
|
133
|
+
host: PG_HOST,
|
|
134
|
+
port: parseInt(PG_PORT),
|
|
135
|
+
user: foreignTables[0].tableowner, // connect as the table owner
|
|
136
|
+
password: '',
|
|
137
|
+
database: name,
|
|
138
|
+
max: 1,
|
|
139
|
+
connectionTimeoutMillis: 5000,
|
|
140
|
+
});
|
|
141
|
+
try {
|
|
142
|
+
await adminProjectPool.query(`GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${PG_USER}`);
|
|
143
|
+
await adminProjectPool.query(`GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${PG_USER}`);
|
|
144
|
+
await adminProjectPool.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${PG_USER}`);
|
|
145
|
+
await adminProjectPool.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${PG_USER}`);
|
|
146
|
+
console.log(` [ProjectDb] Granted ${PG_USER} access to tables in '${name}' (owned by ${foreignTables[0].tableowner})`);
|
|
147
|
+
}
|
|
148
|
+
catch (grantErr) {
|
|
149
|
+
console.warn(` [ProjectDb] Could not grant table permissions: ${grantErr.message}`);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await adminProjectPool.end();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await projectPool.end();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (permErr) {
|
|
161
|
+
// Non-fatal — permissions may or may not be an issue
|
|
162
|
+
console.warn(` [ProjectDb] Permission check skipped: ${permErr.message}`);
|
|
163
|
+
}
|
|
112
164
|
verifiedDbs.add(name);
|
|
113
165
|
return buildDatabaseUrl(name);
|
|
114
166
|
}
|
package/package.json
CHANGED