nstantpage-agent 0.5.34 → 0.6.0

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.
@@ -24426,6 +24426,48 @@ var FileManager = class {
24426
24426
  getProjectDir() {
24427
24427
  return this.projectDir;
24428
24428
  }
24429
+ /**
24430
+ * Build a recursive file tree of the project directory.
24431
+ * Returns a structure matching the backend's /api/projects/{id}/tree format.
24432
+ */
24433
+ async getFileTree() {
24434
+ const root = {
24435
+ name: path8.basename(this.projectDir),
24436
+ path: "",
24437
+ isDirectory: true,
24438
+ children: []
24439
+ };
24440
+ if (existsSync(this.projectDir)) {
24441
+ root.children = await this.buildTree(this.projectDir, "");
24442
+ }
24443
+ return root;
24444
+ }
24445
+ async buildTree(dir, relativePath) {
24446
+ const SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", ".vite-cache", ".next", "__pycache__", ".turbo", ".cache"]);
24447
+ const entries = await fs10.readdir(dir, { withFileTypes: true });
24448
+ const result = [];
24449
+ const sorted = entries.sort((a, b) => {
24450
+ if (a.isDirectory() && !b.isDirectory())
24451
+ return -1;
24452
+ if (!a.isDirectory() && b.isDirectory())
24453
+ return 1;
24454
+ return a.name.localeCompare(b.name);
24455
+ });
24456
+ for (const entry of sorted) {
24457
+ if (entry.name.startsWith(".") && entry.name !== ".env")
24458
+ continue;
24459
+ const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
24460
+ if (entry.isDirectory()) {
24461
+ if (SKIP_DIRS3.has(entry.name))
24462
+ continue;
24463
+ const children = await this.buildTree(path8.join(dir, entry.name), entryRelPath);
24464
+ result.push({ name: entry.name, path: entryRelPath, isDirectory: true, children });
24465
+ } else {
24466
+ result.push({ name: entry.name, path: entryRelPath, isDirectory: false, children: [] });
24467
+ }
24468
+ }
24469
+ return result;
24470
+ }
24429
24471
  /**
24430
24472
  * Check if a path exists.
24431
24473
  */
@@ -26033,6 +26075,9 @@ Access-Control-Allow-Origin: *\r
26033
26075
  "/live/push-to-backend": this.handlePushToBackend,
26034
26076
  "/live/pull-from-backend": this.handlePullFromBackend,
26035
26077
  "/live/disk-file": this.handleDiskFile,
26078
+ "/live/tree": this.handleTree,
26079
+ "/live/file-content": this.handleFileContent,
26080
+ "/live/save-file": this.handleSaveFile,
26036
26081
  "/health": this.handleHealth
26037
26082
  };
26038
26083
  if (handlers[path17])
@@ -26834,6 +26879,47 @@ Access-Control-Allow-Origin: *\r
26834
26879
  this.json(res, { success: false, error: error.message }, 500);
26835
26880
  }
26836
26881
  }
26882
+ // ─── /live/tree ──────────────────────────────────────────────
26883
+ async handleTree(_req, res) {
26884
+ try {
26885
+ const tree = await this.fileManager.getFileTree();
26886
+ this.json(res, tree);
26887
+ } catch (error) {
26888
+ this.json(res, { error: error.message }, 500);
26889
+ }
26890
+ }
26891
+ // ─── /live/file-content ──────────────────────────────────────
26892
+ async handleFileContent(_req, res, _body, url) {
26893
+ const filePath = url.searchParams.get("path");
26894
+ if (!filePath) {
26895
+ this.json(res, { error: "Missing path parameter" }, 400);
26896
+ return;
26897
+ }
26898
+ try {
26899
+ const content = await this.fileManager.readFile(filePath);
26900
+ if (content === null) {
26901
+ this.json(res, { error: "File not found" }, 404);
26902
+ return;
26903
+ }
26904
+ this.json(res, { content });
26905
+ } catch (error) {
26906
+ this.json(res, { error: error.message }, 500);
26907
+ }
26908
+ }
26909
+ // ─── /live/save-file ────────────────────────────────────────
26910
+ async handleSaveFile(_req, res, body) {
26911
+ try {
26912
+ const { filePath, content } = JSON.parse(body);
26913
+ if (!filePath || content === void 0) {
26914
+ this.json(res, { error: "filePath and content required" }, 400);
26915
+ return;
26916
+ }
26917
+ await this.fileManager.writeFiles({ [filePath]: content });
26918
+ this.json(res, { success: true, message: "File saved" });
26919
+ } catch (error) {
26920
+ this.json(res, { error: error.message }, 500);
26921
+ }
26922
+ }
26837
26923
  // ─── /health ─────────────────────────────────────────────────
26838
26924
  async handleHealth(_req, res) {
26839
26925
  this.json(res, {
@@ -27583,6 +27669,7 @@ var StatusServer = class {
27583
27669
  this.server = http5.createServer((req, res) => {
27584
27670
  res.setHeader("Access-Control-Allow-Origin", "*");
27585
27671
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
27672
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
27586
27673
  res.setHeader("Content-Type", "application/json");
27587
27674
  if (req.method === "OPTIONS") {
27588
27675
  res.writeHead(204);
@@ -27642,6 +27729,85 @@ var StatusServer = class {
27642
27729
  });
27643
27730
  return;
27644
27731
  }
27732
+ const parsedUrl = new URL(req.url || "/", `http://localhost:${STATUS_PORT}`);
27733
+ if (req.method === "GET" && url === "/browse/tree") {
27734
+ const dir = parsedUrl.searchParams.get("dir");
27735
+ if (!dir || !fs15.existsSync(dir)) {
27736
+ res.writeHead(400);
27737
+ res.end(JSON.stringify({ error: "Missing or invalid dir parameter" }));
27738
+ return;
27739
+ }
27740
+ try {
27741
+ const tree = buildFileTree(dir, path14.basename(dir));
27742
+ res.writeHead(200);
27743
+ res.end(JSON.stringify(tree));
27744
+ } catch (err) {
27745
+ res.writeHead(500);
27746
+ res.end(JSON.stringify({ error: err.message }));
27747
+ }
27748
+ return;
27749
+ }
27750
+ if (req.method === "GET" && url === "/browse/file") {
27751
+ const dir = parsedUrl.searchParams.get("dir");
27752
+ const filePath = parsedUrl.searchParams.get("path");
27753
+ if (!dir || !filePath) {
27754
+ res.writeHead(400);
27755
+ res.end(JSON.stringify({ error: "dir and path parameters required" }));
27756
+ return;
27757
+ }
27758
+ const fullPath = path14.resolve(dir, filePath);
27759
+ if (!fullPath.startsWith(path14.resolve(dir))) {
27760
+ res.writeHead(403);
27761
+ res.end(JSON.stringify({ error: "Path traversal not allowed" }));
27762
+ return;
27763
+ }
27764
+ try {
27765
+ if (!fs15.existsSync(fullPath)) {
27766
+ res.writeHead(404);
27767
+ res.end(JSON.stringify({ error: "File not found" }));
27768
+ return;
27769
+ }
27770
+ const content = fs15.readFileSync(fullPath, "utf-8");
27771
+ res.writeHead(200);
27772
+ res.end(JSON.stringify({ content }));
27773
+ } catch (err) {
27774
+ res.writeHead(500);
27775
+ res.end(JSON.stringify({ error: err.message }));
27776
+ }
27777
+ return;
27778
+ }
27779
+ if (req.method === "POST" && url === "/browse/save") {
27780
+ let body = "";
27781
+ req.on("data", (chunk) => {
27782
+ body += chunk.toString();
27783
+ });
27784
+ req.on("end", () => {
27785
+ try {
27786
+ const { dir, filePath, content } = JSON.parse(body);
27787
+ if (!dir || !filePath || content === void 0) {
27788
+ res.writeHead(400);
27789
+ res.end(JSON.stringify({ error: "dir, filePath, and content required" }));
27790
+ return;
27791
+ }
27792
+ const fullPath = path14.resolve(dir, filePath);
27793
+ if (!fullPath.startsWith(path14.resolve(dir))) {
27794
+ res.writeHead(403);
27795
+ res.end(JSON.stringify({ error: "Path traversal not allowed" }));
27796
+ return;
27797
+ }
27798
+ const parentDir = path14.dirname(fullPath);
27799
+ if (!fs15.existsSync(parentDir))
27800
+ fs15.mkdirSync(parentDir, { recursive: true });
27801
+ fs15.writeFileSync(fullPath, content, "utf-8");
27802
+ res.writeHead(200);
27803
+ res.end(JSON.stringify({ success: true }));
27804
+ } catch (err) {
27805
+ res.writeHead(500);
27806
+ res.end(JSON.stringify({ error: err.message }));
27807
+ }
27808
+ });
27809
+ return;
27810
+ }
27645
27811
  res.writeHead(404);
27646
27812
  res.end(JSON.stringify({ error: "Not found" }));
27647
27813
  });
@@ -27681,6 +27847,45 @@ var StatusServer = class {
27681
27847
  }, null, 2), "utf-8");
27682
27848
  }
27683
27849
  };
27850
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
27851
+ "node_modules",
27852
+ "dist",
27853
+ ".git",
27854
+ ".vite-cache",
27855
+ ".next",
27856
+ "__pycache__",
27857
+ ".turbo",
27858
+ ".cache",
27859
+ "build",
27860
+ ".svelte-kit"
27861
+ ]);
27862
+ function buildFileTree(dirPath, name, relativePath = "") {
27863
+ const node = { name, path: relativePath, isDirectory: true, children: [] };
27864
+ let entries;
27865
+ try {
27866
+ entries = fs15.readdirSync(dirPath, { withFileTypes: true });
27867
+ } catch {
27868
+ return node;
27869
+ }
27870
+ const dirs = [];
27871
+ const files = [];
27872
+ for (const entry of entries) {
27873
+ if (entry.name.startsWith(".") && entry.name !== ".env")
27874
+ continue;
27875
+ const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
27876
+ if (entry.isDirectory()) {
27877
+ if (SKIP_DIRS2.has(entry.name))
27878
+ continue;
27879
+ dirs.push(buildFileTree(path14.join(dirPath, entry.name), entry.name, childRelative));
27880
+ } else {
27881
+ files.push({ name: entry.name, path: childRelative, isDirectory: false, children: [] });
27882
+ }
27883
+ }
27884
+ dirs.sort((a, b) => a.name.localeCompare(b.name));
27885
+ files.sort((a, b) => a.name.localeCompare(b.name));
27886
+ node.children = [...dirs, ...files];
27887
+ return node;
27888
+ }
27684
27889
 
27685
27890
  // dist/commands/start.js
27686
27891
  var VERSION = "0.5.29";
@@ -27786,6 +27991,11 @@ function cleanupPreviousAgent(projectId, apiPort, devPort) {
27786
27991
  killPort(apiPort);
27787
27992
  if (devPort !== oldDevPort)
27788
27993
  killPort(devPort);
27994
+ const backendPort = devPort + 1001;
27995
+ killPort(backendPort);
27996
+ const oldBackendPort = oldDevPort ? oldDevPort + 1001 : 0;
27997
+ if (oldBackendPort && oldBackendPort !== backendPort)
27998
+ killPort(oldBackendPort);
27789
27999
  clearProjectConfig(projectId);
27790
28000
  }
27791
28001
  async function startCommand(directory, options) {
package/dist/cli.js CHANGED
@@ -21,11 +21,13 @@ import { logoutCommand } from './commands/logout.js';
21
21
  import { startCommand } from './commands/start.js';
22
22
  import { statusCommand } from './commands/status.js';
23
23
  import { serviceInstallCommand, serviceUninstallCommand, serviceStatusCommand, serviceStartCommand, serviceStopCommand } from './commands/service.js';
24
+ import { updateCommand } from './commands/update.js';
25
+ import { getPackageVersion } from './version.js';
24
26
  const program = new Command();
25
27
  program
26
28
  .name('nstantpage')
27
29
  .description('Local development agent for nstantpage.com — run projects on your machine, preview in the cloud')
28
- .version('0.5.29');
30
+ .version(getPackageVersion());
29
31
  program
30
32
  .command('login')
31
33
  .description('Authenticate with nstantpage.com')
@@ -102,5 +104,11 @@ service
102
104
  .command('status')
103
105
  .description('Check if the background service is running')
104
106
  .action(serviceStatusCommand);
107
+ program
108
+ .command('update')
109
+ .description('Update CLI and desktop app to the latest version')
110
+ .option('--cli', 'Update only the CLI package')
111
+ .option('--desktop', 'Update only the desktop app')
112
+ .action(updateCommand);
105
113
  program.parse();
106
114
  //# sourceMappingURL=cli.js.map
@@ -130,7 +130,7 @@ export async function serviceInstallCommand(options = {}) {
130
130
  }
131
131
  // Fallback: headless service (no tray icon)
132
132
  if (electronAppPath === null) {
133
- console.log(chalk.gray(' Tip: Install the nstantpage desktop app for a system tray icon.'));
133
+ console.log(chalk.gray(' Tip: Run "nstantpage update --desktop" to download the desktop app with tray icon.'));
134
134
  }
135
135
  if (platform === 'darwin') {
136
136
  await installLaunchd(gateway, token);
@@ -234,6 +234,8 @@ function findElectronApp() {
234
234
  if (platform === 'darwin') {
235
235
  // Check common macOS install paths
236
236
  const candidates = [
237
+ // Downloaded by postinstall (npm i -g nstantpage-agent)
238
+ path.join(os.homedir(), '.nstantpage', 'desktop', 'nstantpage.app'),
237
239
  '/Applications/nstantpage.app',
238
240
  path.join(os.homedir(), 'Applications', 'nstantpage.app'),
239
241
  // Dev build location (from electron-builder --dir)
@@ -247,6 +249,8 @@ function findElectronApp() {
247
249
  }
248
250
  else if (platform === 'win32') {
249
251
  const candidates = [
252
+ // Downloaded by postinstall (npm i -g nstantpage-agent)
253
+ path.join(os.homedir(), '.nstantpage', 'desktop', 'nstantpage.exe'),
250
254
  path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'nstantpage', 'nstantpage.exe'),
251
255
  path.join('C:\\Program Files', 'nstantpage', 'nstantpage.exe'),
252
256
  ];
@@ -26,7 +26,8 @@ import { LocalServer } from '../localServer.js';
26
26
  import { PackageInstaller } from '../packageInstaller.js';
27
27
  import { probeLocalPostgres, ensureLocalProjectDb, closeAdminPool, writeDatabaseUrlToEnv } from '../projectDb.js';
28
28
  import { StatusServer } from '../statusServer.js';
29
- const VERSION = '0.5.29';
29
+ import { getPackageVersion } from '../version.js';
30
+ const VERSION = getPackageVersion();
30
31
  /**
31
32
  * Resolve the backend API base URL.
32
33
  * - If --backend is passed, use it
@@ -170,7 +171,13 @@ function cleanupPreviousAgent(projectId, apiPort, devPort) {
170
171
  killPort(apiPort);
171
172
  if (devPort !== oldDevPort)
172
173
  killPort(devPort);
173
- // 4. Clear stale project config
174
+ // 4. Also free the backend port (devPort + 1001) that DevServer uses for Express
175
+ const backendPort = devPort + 1001;
176
+ killPort(backendPort);
177
+ const oldBackendPort = oldDevPort ? oldDevPort + 1001 : 0;
178
+ if (oldBackendPort && oldBackendPort !== backendPort)
179
+ killPort(oldBackendPort);
180
+ // 5. Clear stale project config
174
181
  clearProjectConfig(projectId);
175
182
  }
176
183
  export async function startCommand(directory, options) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Update command — update both CLI (npm) and desktop app (GitHub releases).
3
+ *
4
+ * Usage:
5
+ * nstantpage update — Update both CLI and desktop
6
+ * nstantpage update --cli — Update CLI only (npm)
7
+ * nstantpage update --desktop — Update desktop only (GitHub releases)
8
+ */
9
+ interface UpdateOptions {
10
+ cli?: boolean;
11
+ desktop?: boolean;
12
+ }
13
+ export declare function updateCommand(options?: UpdateOptions): Promise<void>;
14
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Update command — update both CLI (npm) and desktop app (GitHub releases).
3
+ *
4
+ * Usage:
5
+ * nstantpage update — Update both CLI and desktop
6
+ * nstantpage update --cli — Update CLI only (npm)
7
+ * nstantpage update --desktop — Update desktop only (GitHub releases)
8
+ */
9
+ import chalk from 'chalk';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import { execSync } from 'child_process';
14
+ import { getPackageVersion } from '../version.js';
15
+ const DESKTOP_DIR = path.join(os.homedir(), '.nstantpage', 'desktop');
16
+ const VERSION_FILE = path.join(DESKTOP_DIR, '.version');
17
+ export async function updateCommand(options = {}) {
18
+ const updateBoth = !options.cli && !options.desktop;
19
+ const currentVersion = getPackageVersion();
20
+ console.log(chalk.blue(`\n nstantpage v${currentVersion}\n`));
21
+ if (updateBoth || options.cli) {
22
+ await updateCli(currentVersion);
23
+ }
24
+ if (updateBoth || options.desktop) {
25
+ await updateDesktop();
26
+ }
27
+ console.log('');
28
+ }
29
+ async function updateCli(currentVersion) {
30
+ console.log(chalk.gray(' Checking npm for CLI updates...'));
31
+ try {
32
+ const latest = execSync('npm view nstantpage-agent version 2>/dev/null', {
33
+ encoding: 'utf-8',
34
+ }).trim();
35
+ if (latest === currentVersion) {
36
+ console.log(chalk.green(` ✓ CLI is up to date (${currentVersion})`));
37
+ return;
38
+ }
39
+ console.log(chalk.yellow(` Updating CLI: ${currentVersion} → ${latest}`));
40
+ execSync('npm install -g nstantpage-agent@latest', {
41
+ stdio: 'inherit',
42
+ });
43
+ console.log(chalk.green(` ✓ CLI updated to ${latest}`));
44
+ }
45
+ catch (err) {
46
+ console.log(chalk.red(` ✗ CLI update failed: ${err.message}`));
47
+ console.log(chalk.gray(' Try manually: npm install -g nstantpage-agent@latest'));
48
+ }
49
+ }
50
+ async function updateDesktop() {
51
+ console.log(chalk.gray(' Checking GitHub for desktop updates...'));
52
+ try {
53
+ // Re-run the postinstall script which handles download + version check
54
+ const scriptPath = path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', 'scripts', 'postinstall.mjs');
55
+ if (fs.existsSync(scriptPath)) {
56
+ execSync(`node "${scriptPath}"`, { stdio: 'inherit' });
57
+ }
58
+ else {
59
+ // If installed globally, the script is in the package root
60
+ const altPath = path.join(path.dirname(new URL(import.meta.url).pathname), '..', 'scripts', 'postinstall.mjs');
61
+ if (fs.existsSync(altPath)) {
62
+ execSync(`node "${altPath}"`, { stdio: 'inherit' });
63
+ }
64
+ else {
65
+ console.log(chalk.yellow(' ⚠ Desktop updater script not found. Reinstall to fix: npm i -g nstantpage-agent'));
66
+ }
67
+ }
68
+ }
69
+ catch (err) {
70
+ console.log(chalk.red(` ✗ Desktop update failed: ${err.message}`));
71
+ }
72
+ }
73
+ //# sourceMappingURL=update.js.map
@@ -36,6 +36,17 @@ export declare class FileManager {
36
36
  * Get the project directory path.
37
37
  */
38
38
  getProjectDir(): string;
39
+ /**
40
+ * Build a recursive file tree of the project directory.
41
+ * Returns a structure matching the backend's /api/projects/{id}/tree format.
42
+ */
43
+ getFileTree(): Promise<{
44
+ name: string;
45
+ path: string;
46
+ isDirectory: boolean;
47
+ children: any[];
48
+ }>;
49
+ private buildTree;
39
50
  /**
40
51
  * Check if a path exists.
41
52
  */
@@ -101,6 +101,50 @@ export class FileManager {
101
101
  getProjectDir() {
102
102
  return this.projectDir;
103
103
  }
104
+ /**
105
+ * Build a recursive file tree of the project directory.
106
+ * Returns a structure matching the backend's /api/projects/{id}/tree format.
107
+ */
108
+ async getFileTree() {
109
+ const root = {
110
+ name: path.basename(this.projectDir),
111
+ path: '',
112
+ isDirectory: true,
113
+ children: [],
114
+ };
115
+ if (existsSync(this.projectDir)) {
116
+ root.children = await this.buildTree(this.projectDir, '');
117
+ }
118
+ return root;
119
+ }
120
+ async buildTree(dir, relativePath) {
121
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.vite-cache', '.next', '__pycache__', '.turbo', '.cache']);
122
+ const entries = await fs.readdir(dir, { withFileTypes: true });
123
+ const result = [];
124
+ // Sort: directories first, then alphabetically
125
+ const sorted = entries.sort((a, b) => {
126
+ if (a.isDirectory() && !b.isDirectory())
127
+ return -1;
128
+ if (!a.isDirectory() && b.isDirectory())
129
+ return 1;
130
+ return a.name.localeCompare(b.name);
131
+ });
132
+ for (const entry of sorted) {
133
+ if (entry.name.startsWith('.') && entry.name !== '.env')
134
+ continue;
135
+ const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
136
+ if (entry.isDirectory()) {
137
+ if (SKIP_DIRS.has(entry.name))
138
+ continue;
139
+ const children = await this.buildTree(path.join(dir, entry.name), entryRelPath);
140
+ result.push({ name: entry.name, path: entryRelPath, isDirectory: true, children });
141
+ }
142
+ else {
143
+ result.push({ name: entry.name, path: entryRelPath, isDirectory: false, children: [] });
144
+ }
145
+ }
146
+ return result;
147
+ }
104
148
  /**
105
149
  * Check if a path exists.
106
150
  */
@@ -129,6 +129,9 @@ export declare class LocalServer {
129
129
  private handlePushToBackend;
130
130
  private handlePullFromBackend;
131
131
  private handleDiskFile;
132
+ private handleTree;
133
+ private handleFileContent;
134
+ private handleSaveFile;
132
135
  private handleHealth;
133
136
  /**
134
137
  * Get a pg Pool connected to this project's local database.
@@ -25,6 +25,7 @@ import { ErrorStore, structuredErrorToString } from './errorStore.js';
25
25
  import { PackageInstaller } from './packageInstaller.js';
26
26
  import { probeLocalPostgres, ensureLocalProjectDb, isLocalPgAvailable, writeDatabaseUrlToEnv } from './projectDb.js';
27
27
  import { AgentSync } from './agentSync.js';
28
+ import { getPackageVersion } from './version.js';
28
29
  // ─── Try to load node-pty for real PTY support ─────────────────
29
30
  let ptyModule = null;
30
31
  try {
@@ -405,6 +406,9 @@ export class LocalServer {
405
406
  '/live/push-to-backend': this.handlePushToBackend,
406
407
  '/live/pull-from-backend': this.handlePullFromBackend,
407
408
  '/live/disk-file': this.handleDiskFile,
409
+ '/live/tree': this.handleTree,
410
+ '/live/file-content': this.handleFileContent,
411
+ '/live/save-file': this.handleSaveFile,
408
412
  '/health': this.handleHealth,
409
413
  };
410
414
  if (handlers[path])
@@ -828,7 +832,7 @@ export class LocalServer {
828
832
  connected: true,
829
833
  projectId: this.options.projectId,
830
834
  agent: {
831
- version: '0.5.11',
835
+ version: getPackageVersion(),
832
836
  hostname: os.hostname(),
833
837
  platform: `${os.platform()} ${os.arch()}`,
834
838
  },
@@ -1272,6 +1276,50 @@ export class LocalServer {
1272
1276
  this.json(res, { success: false, error: error.message }, 500);
1273
1277
  }
1274
1278
  }
1279
+ // ─── /live/tree ──────────────────────────────────────────────
1280
+ async handleTree(_req, res) {
1281
+ try {
1282
+ const tree = await this.fileManager.getFileTree();
1283
+ this.json(res, tree);
1284
+ }
1285
+ catch (error) {
1286
+ this.json(res, { error: error.message }, 500);
1287
+ }
1288
+ }
1289
+ // ─── /live/file-content ──────────────────────────────────────
1290
+ async handleFileContent(_req, res, _body, url) {
1291
+ const filePath = url.searchParams.get('path');
1292
+ if (!filePath) {
1293
+ this.json(res, { error: 'Missing path parameter' }, 400);
1294
+ return;
1295
+ }
1296
+ try {
1297
+ const content = await this.fileManager.readFile(filePath);
1298
+ if (content === null) {
1299
+ this.json(res, { error: 'File not found' }, 404);
1300
+ return;
1301
+ }
1302
+ this.json(res, { content });
1303
+ }
1304
+ catch (error) {
1305
+ this.json(res, { error: error.message }, 500);
1306
+ }
1307
+ }
1308
+ // ─── /live/save-file ────────────────────────────────────────
1309
+ async handleSaveFile(_req, res, body) {
1310
+ try {
1311
+ const { filePath, content } = JSON.parse(body);
1312
+ if (!filePath || content === undefined) {
1313
+ this.json(res, { error: 'filePath and content required' }, 400);
1314
+ return;
1315
+ }
1316
+ await this.fileManager.writeFiles({ [filePath]: content });
1317
+ this.json(res, { success: true, message: 'File saved' });
1318
+ }
1319
+ catch (error) {
1320
+ this.json(res, { error: error.message }, 500);
1321
+ }
1322
+ }
1275
1323
  // ─── /health ─────────────────────────────────────────────────
1276
1324
  async handleHealth(_req, res) {
1277
1325
  this.json(res, {
@@ -62,7 +62,20 @@ export class PackageInstaller {
62
62
  console.log(` [Installer] Installing project dependencies...`);
63
63
  const pm = this.detectPackageManager();
64
64
  const args = this.buildFastInstallArgs(pm);
65
- await this.runCommand(pm, args, 300_000);
65
+ try {
66
+ await this.runCommand(pm, args, 300_000);
67
+ }
68
+ catch (err) {
69
+ // Frozen-lockfile failed (lockfile out-of-date) — retry without it
70
+ if (String(err.message || err).includes('ERR_PNPM_OUTDATED_LOCKFILE') ||
71
+ String(err.message || err).includes('frozen-lockfile')) {
72
+ console.log(` [Installer] Lockfile outdated — retrying without frozen-lockfile...`);
73
+ await this.runCommand(pm, ['install', '--no-frozen-lockfile'], 300_000);
74
+ }
75
+ else {
76
+ throw err;
77
+ }
78
+ }
66
79
  this.writeDepsHash();
67
80
  console.log(` [Installer] Dependencies installed`);
68
81
  }