nstantpage-agent 0.8.17 → 0.8.18

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.
@@ -129,6 +129,16 @@ export declare class LocalServer {
129
129
  private handleTree;
130
130
  private handleFileContent;
131
131
  private handleSaveFile;
132
+ private static readonly BROWSE_SKIP_DIRS;
133
+ private static readonly BROWSE_BINARY_EXTS;
134
+ private collectFilesRecursive;
135
+ private handleBrowseListFiles;
136
+ private handleBrowseFile;
137
+ private handleBrowseTree;
138
+ private handleBrowseSave;
139
+ private handleBrowseSearch;
140
+ private handleBrowseDelete;
141
+ private matchGlob;
132
142
  private handleSetDevPort;
133
143
  private handleHealth;
134
144
  /**
@@ -406,6 +406,13 @@ export class LocalServer {
406
406
  '/live/save-file': this.handleSaveFile,
407
407
  '/live/set-dev-port': this.handleSetDevPort,
408
408
  '/health': this.handleHealth,
409
+ // ── /browse/* endpoints (same as statusServer, but accessible through tunnel) ──
410
+ '/browse/list-files': this.handleBrowseListFiles,
411
+ '/browse/file': this.handleBrowseFile,
412
+ '/browse/tree': this.handleBrowseTree,
413
+ '/browse/save': this.handleBrowseSave,
414
+ '/browse/search': this.handleBrowseSearch,
415
+ '/browse/delete': this.handleBrowseDelete,
409
416
  };
410
417
  if (handlers[path])
411
418
  return handlers[path];
@@ -1226,6 +1233,199 @@ export class LocalServer {
1226
1233
  this.json(res, { error: error.message }, 500);
1227
1234
  }
1228
1235
  }
1236
+ // ─── /browse/* — Tunnel-accessible file operations ──────────
1237
+ // These mirror the statusServer's /browse/* endpoints but use the
1238
+ // project directory from this LocalServer instance. The backend's
1239
+ // DiskFileService routes through the gateway → tunnel → here.
1240
+ static BROWSE_SKIP_DIRS = new Set([
1241
+ 'node_modules', 'dist', '.git', '.vite-cache', '.next',
1242
+ '__pycache__', '.turbo', '.cache', 'build', '.svelte-kit',
1243
+ ]);
1244
+ static BROWSE_BINARY_EXTS = new Set([
1245
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg', '.bmp',
1246
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
1247
+ '.zip', '.tar', '.gz', '.rar', '.7z',
1248
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx',
1249
+ '.mp3', '.mp4', '.wav', '.avi', '.mov',
1250
+ '.exe', '.dll', '.so', '.dylib', '.o',
1251
+ ]);
1252
+ collectFilesRecursive(baseDir, currentDir, out) {
1253
+ let entries;
1254
+ try {
1255
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
1256
+ }
1257
+ catch {
1258
+ return;
1259
+ }
1260
+ for (const entry of entries) {
1261
+ if (entry.name.startsWith('.') && entry.name !== '.env')
1262
+ continue;
1263
+ const fullPath = path.join(currentDir, entry.name);
1264
+ const relativePath = path.relative(baseDir, fullPath);
1265
+ if (entry.isDirectory()) {
1266
+ if (LocalServer.BROWSE_SKIP_DIRS.has(entry.name))
1267
+ continue;
1268
+ this.collectFilesRecursive(baseDir, fullPath, out);
1269
+ }
1270
+ else {
1271
+ const ext = path.extname(entry.name).toLowerCase();
1272
+ if (LocalServer.BROWSE_BINARY_EXTS.has(ext)) {
1273
+ out.push({ filePath: relativePath, totalLines: 0 });
1274
+ }
1275
+ else {
1276
+ try {
1277
+ const content = fs.readFileSync(fullPath, 'utf-8');
1278
+ out.push({ filePath: relativePath, totalLines: content.split('\n').length });
1279
+ }
1280
+ catch {
1281
+ out.push({ filePath: relativePath, totalLines: 0 });
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ async handleBrowseListFiles(_req, res) {
1288
+ const dir = this.options.projectDir;
1289
+ try {
1290
+ const files = [];
1291
+ this.collectFilesRecursive(dir, dir, files);
1292
+ this.json(res, { files });
1293
+ }
1294
+ catch (error) {
1295
+ this.json(res, { error: error.message }, 500);
1296
+ }
1297
+ }
1298
+ async handleBrowseFile(_req, res, _body, url) {
1299
+ const filePath = url.searchParams.get('path');
1300
+ if (!filePath) {
1301
+ this.json(res, { error: 'path parameter required' }, 400);
1302
+ return;
1303
+ }
1304
+ const dir = this.options.projectDir;
1305
+ const fullPath = path.resolve(dir, filePath);
1306
+ if (!fullPath.startsWith(path.resolve(dir))) {
1307
+ this.json(res, { error: 'Path traversal not allowed' }, 403);
1308
+ return;
1309
+ }
1310
+ try {
1311
+ if (!fs.existsSync(fullPath)) {
1312
+ this.json(res, { error: 'File not found' }, 404);
1313
+ return;
1314
+ }
1315
+ const content = fs.readFileSync(fullPath, 'utf-8');
1316
+ this.json(res, { content });
1317
+ }
1318
+ catch (error) {
1319
+ this.json(res, { error: error.message }, 500);
1320
+ }
1321
+ }
1322
+ async handleBrowseTree(_req, res) {
1323
+ try {
1324
+ const tree = await this.fileManager.getFileTree();
1325
+ this.json(res, tree);
1326
+ }
1327
+ catch (error) {
1328
+ this.json(res, { error: error.message }, 500);
1329
+ }
1330
+ }
1331
+ async handleBrowseSave(_req, res, body) {
1332
+ try {
1333
+ const { filePath, content } = JSON.parse(body);
1334
+ if (!filePath || content === undefined) {
1335
+ this.json(res, { error: 'filePath and content required' }, 400);
1336
+ return;
1337
+ }
1338
+ const dir = this.options.projectDir;
1339
+ const fullPath = path.resolve(dir, filePath);
1340
+ if (!fullPath.startsWith(path.resolve(dir))) {
1341
+ this.json(res, { error: 'Path traversal not allowed' }, 403);
1342
+ return;
1343
+ }
1344
+ const parentDir = path.dirname(fullPath);
1345
+ if (!fs.existsSync(parentDir))
1346
+ fs.mkdirSync(parentDir, { recursive: true });
1347
+ fs.writeFileSync(fullPath, content, 'utf-8');
1348
+ this.json(res, { success: true });
1349
+ }
1350
+ catch (error) {
1351
+ this.json(res, { error: error.message }, 500);
1352
+ }
1353
+ }
1354
+ async handleBrowseSearch(_req, res, body) {
1355
+ try {
1356
+ const { query, isRegex, filePattern } = JSON.parse(body);
1357
+ if (!query) {
1358
+ this.json(res, { error: 'query required' }, 400);
1359
+ return;
1360
+ }
1361
+ const dir = this.options.projectDir;
1362
+ const files = [];
1363
+ this.collectFilesRecursive(dir, dir, files);
1364
+ const regex = isRegex ? new RegExp(query, 'gi') : null;
1365
+ const results = [];
1366
+ const maxResults = 100;
1367
+ for (const f of files) {
1368
+ if (results.length >= maxResults)
1369
+ break;
1370
+ if (filePattern && !this.matchGlob(f.filePath, filePattern))
1371
+ continue;
1372
+ const fullPath = path.resolve(dir, f.filePath);
1373
+ let fileContent;
1374
+ try {
1375
+ fileContent = fs.readFileSync(fullPath, 'utf-8');
1376
+ }
1377
+ catch {
1378
+ continue;
1379
+ }
1380
+ const lines = fileContent.split('\n');
1381
+ for (let i = 0; i < lines.length && results.length < maxResults; i++) {
1382
+ const matched = regex ? regex.test(lines[i]) : lines[i].toLowerCase().includes(query.toLowerCase());
1383
+ if (regex)
1384
+ regex.lastIndex = 0;
1385
+ if (matched) {
1386
+ results.push({ filePath: f.filePath, line: i + 1, content: lines[i] });
1387
+ }
1388
+ }
1389
+ }
1390
+ this.json(res, { results });
1391
+ }
1392
+ catch (error) {
1393
+ this.json(res, { error: error.message }, 500);
1394
+ }
1395
+ }
1396
+ async handleBrowseDelete(_req, res, body) {
1397
+ try {
1398
+ const { filePath } = JSON.parse(body);
1399
+ if (!filePath) {
1400
+ this.json(res, { error: 'filePath required' }, 400);
1401
+ return;
1402
+ }
1403
+ const dir = this.options.projectDir;
1404
+ const fullPath = path.resolve(dir, filePath);
1405
+ if (!fullPath.startsWith(path.resolve(dir))) {
1406
+ this.json(res, { error: 'Path traversal not allowed' }, 403);
1407
+ return;
1408
+ }
1409
+ if (!fs.existsSync(fullPath)) {
1410
+ this.json(res, { error: 'File not found' }, 404);
1411
+ return;
1412
+ }
1413
+ fs.unlinkSync(fullPath);
1414
+ this.json(res, { success: true });
1415
+ }
1416
+ catch (error) {
1417
+ this.json(res, { error: error.message }, 500);
1418
+ }
1419
+ }
1420
+ matchGlob(filePath, pattern) {
1421
+ const regexPattern = '^' + pattern
1422
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
1423
+ .replace(/\*\*/g, '<<<GLOBSTAR>>>')
1424
+ .replace(/\*/g, '[^/]*')
1425
+ .replace(/<<<GLOBSTAR>>>/g, '.*')
1426
+ .replace(/\?/g, '.') + '$';
1427
+ return new RegExp(regexPattern, 'i').test(filePath);
1428
+ }
1229
1429
  // ─── /live/set-dev-port ─────────────────────────────────────
1230
1430
  async handleSetDevPort(_req, res, body) {
1231
1431
  try {
package/dist/tunnel.js CHANGED
@@ -503,7 +503,7 @@ export class TunnelClient {
503
503
  */
504
504
  handleHttpRequest(request) {
505
505
  const { url, headers } = request;
506
- const isApiRequest = url.startsWith('/live/') || url === '/health';
506
+ const isApiRequest = url.startsWith('/live/') || url.startsWith('/browse/') || url === '/health';
507
507
  // Use x-target-port header if present (multi-port tunneling), else fall back to default
508
508
  const portHeader = headers?.['x-target-port'];
509
509
  const overridePort = portHeader ? parseInt(String(portHeader), 10) : NaN;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.8.17",
3
+ "version": "0.8.18",
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": {