nstantpage-agent 0.5.19 → 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 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.19');
28
+ .version('0.5.20');
29
29
  program
30
30
  .command('login')
31
31
  .description('Authenticate with nstantpage.com')
@@ -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.19';
28
+ const VERSION = '0.5.20';
29
29
  /**
30
30
  * Resolve the backend API base URL.
31
31
  * - If --backend is passed, use it
@@ -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
  }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.5.19",
3
+ "version": "0.5.20",
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": {