@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.
- package/bin/ctlsurf-worker.js +39 -6
- package/out/headless/index.mjs +549 -116
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +416 -38
- package/out/preload/index.js +3 -0
- package/out/renderer/assets/{cssMode-DwHD6JSn.js → cssMode-BtQxI2Eb.js} +3 -3
- package/out/renderer/assets/{freemarker2-Na9-bXYw.js → freemarker2-DRW5dmY5.js} +1 -1
- package/out/renderer/assets/{handlebars-D7gzKLS8.js → handlebars-CFLF3cP7.js} +1 -1
- package/out/renderer/assets/{html-DDK7xN27.js → html-DtnBOkwp.js} +1 -1
- package/out/renderer/assets/{htmlMode-BfbLv-NB.js → htmlMode-DMVysDCg.js} +3 -3
- package/out/renderer/assets/{index-DID1LlMU.css → index-CEjwKjtd.css} +45 -0
- package/out/renderer/assets/{index-HoDaBul3.js → index-Dm70hk5s.js} +252 -26
- package/out/renderer/assets/{javascript-DJjWOxID.js → javascript-ChlyA1sf.js} +2 -2
- package/out/renderer/assets/{jsonMode-BileWbig.js → jsonMode-D1_oHzeN.js} +3 -3
- package/out/renderer/assets/{liquid-DvNU_dI9.js → liquid-D4ftdy1G.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Bb27EY8c.js → lspLanguageFeatures-Ckqu5ubG.js} +1 -1
- package/out/renderer/assets/{mdx-CLkNPVgD.js → mdx-B9wsVYSC.js} +1 -1
- package/out/renderer/assets/{python-DOJdL4kC.js → python-e23tD6IS.js} +1 -1
- package/out/renderer/assets/{razor-CSr8UlJc.js → razor-CrDyXj3d.js} +1 -1
- package/out/renderer/assets/{tsMode-sbsAvk21.js → tsMode-BwGC5o4O.js} +1 -1
- package/out/renderer/assets/{typescript-r5QB4yRW.js → typescript-Do-98d_O.js} +1 -1
- package/out/renderer/assets/{xml-CYhKh3Pi.js → xml-Dpdu5R3m.js} +1 -1
- package/out/renderer/assets/{yaml-B8DsoaYW.js → yaml-B0Zt7LCJ.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/scripts/extract-electron.js +105 -0
- package/src/main/headless.ts +12 -0
- package/src/main/index.ts +8 -0
- package/src/main/monitor.ts +540 -0
- package/src/main/orchestrator.ts +10 -0
- package/src/main/pty.ts +5 -0
- package/src/main/tui.ts +37 -3
- package/src/preload/index.ts +4 -0
- package/src/renderer/App.tsx +9 -1
- package/src/renderer/components/Resources.tsx +217 -0
- package/src/renderer/components/StatusBar.tsx +6 -1
- 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 }
|
package/src/main/headless.ts
CHANGED
|
@@ -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)),
|