@phenx-inc/ctlsurf 0.16.1 → 0.17.1

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.
Files changed (37) hide show
  1. package/bin/ctlsurf-worker.js +39 -6
  2. package/out/headless/index.mjs +549 -116
  3. package/out/headless/index.mjs.map +4 -4
  4. package/out/main/index.js +416 -38
  5. package/out/preload/index.js +3 -0
  6. package/out/renderer/assets/{cssMode-DwHD6JSn.js → cssMode-BtQxI2Eb.js} +3 -3
  7. package/out/renderer/assets/{freemarker2-Na9-bXYw.js → freemarker2-DRW5dmY5.js} +1 -1
  8. package/out/renderer/assets/{handlebars-D7gzKLS8.js → handlebars-CFLF3cP7.js} +1 -1
  9. package/out/renderer/assets/{html-DDK7xN27.js → html-DtnBOkwp.js} +1 -1
  10. package/out/renderer/assets/{htmlMode-BfbLv-NB.js → htmlMode-DMVysDCg.js} +3 -3
  11. package/out/renderer/assets/{index-DID1LlMU.css → index-CEjwKjtd.css} +45 -0
  12. package/out/renderer/assets/{index-HoDaBul3.js → index-Dm70hk5s.js} +252 -26
  13. package/out/renderer/assets/{javascript-DJjWOxID.js → javascript-ChlyA1sf.js} +2 -2
  14. package/out/renderer/assets/{jsonMode-BileWbig.js → jsonMode-D1_oHzeN.js} +3 -3
  15. package/out/renderer/assets/{liquid-DvNU_dI9.js → liquid-D4ftdy1G.js} +1 -1
  16. package/out/renderer/assets/{lspLanguageFeatures-Bb27EY8c.js → lspLanguageFeatures-Ckqu5ubG.js} +1 -1
  17. package/out/renderer/assets/{mdx-CLkNPVgD.js → mdx-B9wsVYSC.js} +1 -1
  18. package/out/renderer/assets/{python-DOJdL4kC.js → python-e23tD6IS.js} +1 -1
  19. package/out/renderer/assets/{razor-CSr8UlJc.js → razor-CrDyXj3d.js} +1 -1
  20. package/out/renderer/assets/{tsMode-sbsAvk21.js → tsMode-BwGC5o4O.js} +1 -1
  21. package/out/renderer/assets/{typescript-r5QB4yRW.js → typescript-Do-98d_O.js} +1 -1
  22. package/out/renderer/assets/{xml-CYhKh3Pi.js → xml-Dpdu5R3m.js} +1 -1
  23. package/out/renderer/assets/{yaml-B8DsoaYW.js → yaml-B0Zt7LCJ.js} +1 -1
  24. package/out/renderer/index.html +2 -2
  25. package/package.json +1 -1
  26. package/scripts/extract-electron.js +105 -0
  27. package/src/main/headless.ts +12 -0
  28. package/src/main/index.ts +8 -0
  29. package/src/main/monitor.ts +540 -0
  30. package/src/main/orchestrator.ts +10 -0
  31. package/src/main/pty.ts +5 -0
  32. package/src/main/tui.ts +37 -3
  33. package/src/preload/index.ts +4 -0
  34. package/src/renderer/App.tsx +9 -1
  35. package/src/renderer/components/Resources.tsx +217 -0
  36. package/src/renderer/components/StatusBar.tsx +6 -1
  37. package/src/renderer/styles.css +45 -0
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Fallback installer for the Electron binary inside node_modules.
5
+ *
6
+ * electron <40 unzips its binary in postinstall with extract-zip@2/yauzl@2,
7
+ * which on Node >= 26 silently stops partway through the archive: the
8
+ * process exits 0, dist/ is left half-extracted and path.txt is never
9
+ * written, so require('electron') throws "Electron failed to install
10
+ * correctly". npm then considers the package installed and won't re-run
11
+ * the postinstall.
12
+ *
13
+ * This fetches the same zip through @electron/get (normally a cache hit —
14
+ * the broken postinstall already downloaded it), extracts it with the OS's
15
+ * own unzip tool, and writes path.txt the same way electron/install.js does.
16
+ *
17
+ * Run as a child process by bin/ctlsurf-worker.js; exits non-zero on failure.
18
+ */
19
+
20
+ const fs = require('fs')
21
+ const path = require('path')
22
+ const { execFileSync } = require('child_process')
23
+
24
+ const ROOT = path.resolve(__dirname, '..')
25
+ const ELECTRON_DIR = path.join(ROOT, 'node_modules', 'electron')
26
+ const SENTINEL = path.join(ELECTRON_DIR, '.ctlsurf-rebranded')
27
+
28
+ function getPlatformPath() {
29
+ switch (process.platform) {
30
+ case 'darwin': return 'Electron.app/Contents/MacOS/Electron'
31
+ case 'linux': return 'electron'
32
+ case 'win32': return 'electron.exe'
33
+ default: throw new Error(`Electron builds are not available on platform: ${process.platform}`)
34
+ }
35
+ }
36
+
37
+ function getArch() {
38
+ // Mirror electron/install.js: x64 node under Rosetta still wants arm64.
39
+ if (process.platform === 'darwin' && process.arch === 'x64') {
40
+ try {
41
+ if (execFileSync('sysctl', ['-in', 'sysctl.proc_translated']).toString().trim() === '1') return 'arm64'
42
+ } catch { /* ignore */ }
43
+ }
44
+ return process.arch
45
+ }
46
+
47
+ function unzip(zipPath, distDir) {
48
+ if (process.platform === 'darwin') {
49
+ // ditto keeps the framework symlinks inside Electron.app intact
50
+ execFileSync('ditto', ['-x', '-k', zipPath, distDir], { stdio: 'inherit' })
51
+ } else if (process.platform === 'win32') {
52
+ // bsdtar ships with Windows 10+ and reads zip archives
53
+ execFileSync('tar', ['-xf', zipPath, '-C', distDir], { stdio: 'inherit' })
54
+ } else {
55
+ execFileSync('unzip', ['-q', '-o', zipPath, '-d', distDir], { stdio: 'inherit' })
56
+ }
57
+ }
58
+
59
+ async function extractElectron() {
60
+ const platformPath = getPlatformPath()
61
+ const { version } = require(path.join(ELECTRON_DIR, 'package.json'))
62
+ const { downloadArtifact } = require(require.resolve('@electron/get', { paths: [ELECTRON_DIR] }))
63
+
64
+ const zipPath = await downloadArtifact({
65
+ version,
66
+ artifactName: 'electron',
67
+ cacheRoot: process.env.electron_config_cache,
68
+ checksums: require(path.join(ELECTRON_DIR, 'checksums.json')),
69
+ platform: process.platform,
70
+ arch: getArch(),
71
+ })
72
+
73
+ const distDir = path.join(ELECTRON_DIR, 'dist')
74
+ fs.rmSync(distDir, { recursive: true, force: true })
75
+ fs.rmSync(path.join(ELECTRON_DIR, 'path.txt'), { force: true })
76
+ // Fresh binary — let rebrand-electron.js run again
77
+ fs.rmSync(SENTINEL, { force: true })
78
+ fs.mkdirSync(distDir, { recursive: true })
79
+ unzip(zipPath, distDir)
80
+
81
+ // Same housekeeping as electron/install.js
82
+ const srcTypeDefs = path.join(distDir, 'electron.d.ts')
83
+ if (fs.existsSync(srcTypeDefs)) {
84
+ fs.renameSync(srcTypeDefs, path.join(ELECTRON_DIR, 'electron.d.ts'))
85
+ }
86
+
87
+ if (!fs.existsSync(path.join(distDir, platformPath))) {
88
+ throw new Error(`binary missing after extract: dist/${platformPath}`)
89
+ }
90
+ fs.writeFileSync(path.join(ELECTRON_DIR, 'path.txt'), platformPath)
91
+ }
92
+
93
+ // ─── Entry ──────────────────────────────────────────
94
+
95
+ if (require.main === module) {
96
+ extractElectron().then(
97
+ () => process.exit(0),
98
+ (err) => {
99
+ console.error(`[ctlsurf] Electron extract failed: ${err.message}`)
100
+ process.exit(1)
101
+ }
102
+ )
103
+ }
104
+
105
+ module.exports = { extractElectron }
@@ -31,6 +31,7 @@ import { Orchestrator } from './orchestrator'
31
31
  import { getSettingsDir } from './settingsDir'
32
32
  import { getBuiltinAgents, getAvailableAgents, isCodingAgent, type AgentConfig } from './agents'
33
33
  import { Tui } from './tui'
34
+ import { ResourceMonitor, summarizeSnapshot } from './monitor'
34
35
  import { fetchLatestNpmVersion, compareSemver, NPM_PACKAGE } from './updateCheck'
35
36
 
36
37
  function getCurrentVersion(): string {
@@ -177,6 +178,16 @@ async function main() {
177
178
  await orchestrator.spawnAgent(HEADLESS_TAB, agent, args.cwd, { trackTime: trackTimeOverride })
178
179
  orchestrator.resizePty(HEADLESS_TAB, ptySize.cols, ptySize.rows)
179
180
 
181
+ // CPU / RAM and the agent's plan quota, appended to the tab title.
182
+ const monitor = new ResourceMonitor(() => orchestrator.getTabProcesses())
183
+ const refreshResources = () => {
184
+ monitor.snapshot()
185
+ .then(snap => tui.update({ resources: summarizeSnapshot(snap) }))
186
+ .catch(() => { /* keep the last title */ })
187
+ }
188
+ refreshResources()
189
+ setInterval(refreshResources, 10_000).unref()
190
+
180
191
  // For coding agents, send an initial prompt to kick-start them
181
192
  if (isCodingAgent(agent)) {
182
193
  setTimeout(() => {
@@ -221,6 +232,7 @@ async function main() {
221
232
  if (process.stdin.isTTY) {
222
233
  process.stdin.setRawMode(false)
223
234
  }
235
+ monitor.stop()
224
236
  tui.destroy()
225
237
  await orchestrator.shutdown()
226
238
  process.exit(0)
package/src/main/index.ts CHANGED
@@ -27,6 +27,7 @@ import { getSettingsDir } from './settingsDir'
27
27
  import { Orchestrator } from './orchestrator'
28
28
  import { AttentionController } from './agentState'
29
29
  import { listSshHosts, getRecentHosts, addRecentHost, probeRemote, buildRemoteAgent, type RemoteAction } from './ssh'
30
+ import { ResourceMonitor } from './monitor'
30
31
 
31
32
  const SETTINGS_DIR = getSettingsDir(true)
32
33
 
@@ -498,6 +499,12 @@ async function checkForUpdate(): Promise<void> {
498
499
  ipcMain.handle('app:getVersion', () => app.getVersion())
499
500
  ipcMain.handle('app:getUpdateInfo', () => updateInfo)
500
501
 
502
+ // ─── Resources + plan quota (status bar) ──────────
503
+
504
+ // Samples only while the renderer keeps asking; `fast` = the panel is open.
505
+ const resourceMonitor = new ResourceMonitor(() => orchestrator.getTabProcesses())
506
+ ipcMain.handle('monitor:snapshot', (_event, fast: boolean) => resourceMonitor.snapshot(!!fast))
507
+
501
508
  // ─── Filesystem IPC ───────────────────────────────
502
509
 
503
510
  ipcMain.handle('fs:readDir', async (_event, dirPath: string) => {
@@ -831,6 +838,7 @@ let shutdownStarted = false
831
838
  function shutdownThenQuit(): void {
832
839
  if (shutdownStarted) return
833
840
  shutdownStarted = true
841
+ resourceMonitor.stop()
834
842
  void Promise.race([
835
843
  orchestrator.shutdown(),
836
844
  new Promise((resolve) => setTimeout(resolve, 10_000)),