thinkpool-pair 0.7.57 → 0.7.58
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/bridge.mjs +5 -62
- package/claude-session.mjs +3 -0
- package/launcher.mjs +4 -22
- package/package.json +1 -2
- package/cmux.mjs +0 -139
package/bridge.mjs
CHANGED
|
@@ -149,14 +149,7 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
149
149
|
const autoUpdate = argv.includes('--auto-update')
|
|
150
150
|
const svc = await import('./service.mjs')
|
|
151
151
|
if (argv[0] === 'install-service') svc.installService(svcRoom, svcCmd, { autoUpdate })
|
|
152
|
-
else
|
|
153
|
-
// Account scope (room=null) may be the cmux-hosted tier (cmux-service.json) —
|
|
154
|
-
// that's torn down via cmux, not launchctl/systemctl. Room scope is always OS.
|
|
155
|
-
const cmux = await import('./cmux.mjs')
|
|
156
|
-
if (svcRoom === null && cmux.isCmuxServiceInstalled()) {
|
|
157
|
-
cmux.uninstallCmuxService(); process.stderr.write(' ◆ removed the cmux-hosted account service.\n')
|
|
158
|
-
} else svc.uninstallService(svcRoom)
|
|
159
|
-
}
|
|
152
|
+
else svc.uninstallService(svcRoom)
|
|
160
153
|
process.exit(0)
|
|
161
154
|
}
|
|
162
155
|
|
|
@@ -165,10 +158,8 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
165
158
|
if (argv[0] === 'restart-service') {
|
|
166
159
|
const arg1 = argv[1] || ''
|
|
167
160
|
const svcRoom = (!arg1 || arg1.startsWith('-')) ? null : arg1.toUpperCase().trim()
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
cmux.restartCmuxService(); process.stderr.write(' ◆ restarted the cmux-hosted account service — sessions resume in place.\n')
|
|
171
|
-
} else { const svc = await import('./service.mjs'); svc.restartService(svcRoom) }
|
|
161
|
+
const svc = await import('./service.mjs')
|
|
162
|
+
svc.restartService(svcRoom)
|
|
172
163
|
process.exit(0)
|
|
173
164
|
}
|
|
174
165
|
|
|
@@ -189,39 +180,6 @@ if (argv[0] === 'login') { const { runLogin } = await import('./account.mjs'); a
|
|
|
189
180
|
if (argv[0] === 'bind') { const { runBind } = await import('./account.mjs'); runBind(argv[1], argv[2]) }
|
|
190
181
|
if (argv[0] === 'provider') { const { runProvider } = await import('./provider.mjs'); await runProvider(argv.slice(1)); process.exit(0) }
|
|
191
182
|
|
|
192
|
-
// Explicit account-serve entrypoint — used by the cmux-hosted service tier (cmux.mjs).
|
|
193
|
-
// A cmux surface HAS a TTY, so a bare invocation would open the interactive launcher
|
|
194
|
-
// instead of serving; `serve` runs account mode directly, regardless of TTY. With
|
|
195
|
-
// `--supervise` it first wraps itself in a crash-restart loop (re-execing `serve`
|
|
196
|
-
// without the flag): cmux, unlike launchd KeepAlive, will not relaunch a surface
|
|
197
|
-
// whose command exits, so the account supervisor must restart itself.
|
|
198
|
-
if (argv[0] === 'serve') {
|
|
199
|
-
if (argv.includes('--supervise') || argv.includes('--keep-alive')) {
|
|
200
|
-
const childArgs = process.argv.slice(1).filter((a) => a !== '--supervise' && a !== '--keep-alive')
|
|
201
|
-
let backoff = 1000, stopping = false, child = null
|
|
202
|
-
const spawnChild = () => {
|
|
203
|
-
const t0 = Date.now()
|
|
204
|
-
child = spawn(process.execPath, childArgs, { stdio: 'inherit' })
|
|
205
|
-
child.on('exit', (code, signal) => {
|
|
206
|
-
if (stopping || code === 0) process.exit(code || 0)
|
|
207
|
-
const ranLong = Date.now() - t0 > 60000
|
|
208
|
-
const wait = ranLong ? 1000 : backoff
|
|
209
|
-
backoff = ranLong ? 1000 : Math.min(backoff * 2, 30000)
|
|
210
|
-
process.stderr.write(`\n ⚠ account supervisor exited (${signal || `code ${code}`}); restarting in ${Math.round(wait / 1000)}s…\n`)
|
|
211
|
-
setTimeout(spawnChild, wait)
|
|
212
|
-
})
|
|
213
|
-
child.on('error', () => { /* spawn failure → exit handler retries */ })
|
|
214
|
-
}
|
|
215
|
-
for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { stopping = true; try { child?.kill(sig) } catch { /* noop */ } setTimeout(() => process.exit(0), 400) })
|
|
216
|
-
process.stderr.write('\n ◆ supervisor up — keeping the account bridge alive (auto-restart on crash).\n')
|
|
217
|
-
spawnChild()
|
|
218
|
-
await new Promise(() => {}) // block forever; the child IS the account supervisor
|
|
219
|
-
}
|
|
220
|
-
const { runAccount } = await import('./account.mjs')
|
|
221
|
-
await runAccount(SUPABASE_URL, SUPABASE_ANON)
|
|
222
|
-
process.exit(0)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
183
|
// One-command launcher: a bare invocation on a TTY (or the `setup` alias) opens the
|
|
226
184
|
// interactive menu instead of going straight to account mode. The menu only ASSEMBLES
|
|
227
185
|
// existing commands — it re-execs `<ROOM>`, or calls installService / runLogin /
|
|
@@ -231,8 +189,6 @@ if (argv[0] === 'serve') {
|
|
|
231
189
|
if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
232
190
|
const { runLauncher, detectState } = await import('./launcher.mjs')
|
|
233
191
|
const svc = await import('./service.mjs')
|
|
234
|
-
const cmux = await import('./cmux.mjs')
|
|
235
|
-
const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return 'latest' } })()
|
|
236
192
|
const BRIDGE = fileURLToPath(import.meta.url)
|
|
237
193
|
const io = {
|
|
238
194
|
print: (s) => process.stderr.write(s + '\n'),
|
|
@@ -254,21 +210,8 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
254
210
|
// notes, and RETURN so the launcher loop drops you back at the menu (the service
|
|
255
211
|
// runs in its own launchd/systemd process; this launcher isn't itself a bridge).
|
|
256
212
|
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []) },
|
|
257
|
-
uninstallService: ({ room = null } = {}) => {
|
|
258
|
-
|
|
259
|
-
else svc.uninstallService(room)
|
|
260
|
-
},
|
|
261
|
-
restartService: ({ room = null } = {}) => {
|
|
262
|
-
if (room === null && cmux.isCmuxServiceInstalled()) { cmux.restartCmuxService(); process.stderr.write('\n ◆ restarted the cmux-hosted account service — sessions resume in place.\n') }
|
|
263
|
-
else svc.restartService(room)
|
|
264
|
-
},
|
|
265
|
-
// Account service hosted as a cmux surface — offered only inside cmux, with its
|
|
266
|
-
// weaker-persistence caveat shown first (launcher.mjs serveAccount).
|
|
267
|
-
detectCmux: () => cmux.detectCmux(),
|
|
268
|
-
installAccountCmux: () => {
|
|
269
|
-
const rec = cmux.installCmuxService({ version: PKG_VERSION })
|
|
270
|
-
process.stderr.write(`\n ◆ account service now runs as a cmux surface ("${cmux.WS_NAME}") — visible and killable.\n ◆ it restarts itself on crash; stop it with: npx thinkpool-pair uninstall-service\n ◆ pinned to v${rec.version}.\n\n`)
|
|
271
|
-
},
|
|
213
|
+
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room) },
|
|
214
|
+
restartService: ({ room = null } = {}) => { svc.restartService(room) },
|
|
272
215
|
login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
|
|
273
216
|
// Provider config is NOT terminal — it writes provider.json and returns to the
|
|
274
217
|
// menu so you can pick a model, switch back, or do something else. (It used to
|
package/claude-session.mjs
CHANGED
|
@@ -343,6 +343,9 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
343
343
|
'Bias strongly toward DOING the work, not planning it. For a normal "add / fix / change / build X" request, just make the change directly.',
|
|
344
344
|
'Do NOT enter plan mode, do NOT call ExitPlanMode, and do NOT auto-invoke a brainstorming/planning skill UNLESS the user has switched the room into Plan mode or explicitly asks you to plan, design, or brainstorm first.',
|
|
345
345
|
'Any host-global instruction that says you must always brainstorm or plan before creative work does NOT apply here — this room is the exception.',
|
|
346
|
+
'LINKS & ARTIFACTS (hard rule for ThinkPool Code rooms): the room is driven from a phone or browser, and the bridge runs on the host machine — so a local filesystem path, a file:// URL, or a localhost address only opens on the host and is useless to the room (a partner literally cannot open it).',
|
|
347
|
+
'Whenever you produce an HTML artifact (a demo, mockup, preview, report, or page) or surface ANY link meant to be opened or shared, NEVER hand the room a local path, a file:// URL, or a localhost/127.0.0.1 address. Publish it to a GitHub-shareable URL that renders in a browser — push the HTML to a GitHub repo and give its GitHub Pages URL (or an equivalent raw-HTML URL that actually renders, not raw.githubusercontent.com which serves HTML as plain text) — so anyone in the room can open it. The same applies to any other link you surface: it must be one the room can reach, not a host-only path.',
|
|
348
|
+
'If you cannot publish a shareable link, say so and ask how to proceed — do not fall back to handing over a local path.',
|
|
346
349
|
].join(' '),
|
|
347
350
|
// Needed for live thinking-token progress (SDKThinkingTokensMessage) to
|
|
348
351
|
// flow during a turn. We ignore the fine-grained stream_event partials in
|
package/launcher.mjs
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
import os from 'node:os'
|
|
14
14
|
import fs from 'node:fs'
|
|
15
15
|
import path from 'node:path'
|
|
16
|
-
import { isCmuxServiceInstalled } from './cmux.mjs'
|
|
17
16
|
|
|
18
17
|
const HOME = os.homedir()
|
|
19
18
|
const CFG_DIR = path.join(HOME, '.thinkpool-pair')
|
|
@@ -61,11 +60,7 @@ export function detectState() {
|
|
|
61
60
|
provider: isCustom ? `Custom (${provider.baseUrl || 'endpoint'})` : 'Anthropic (default)',
|
|
62
61
|
providerModel: isCustom ? (provider.model || null) : null,
|
|
63
62
|
agents: KNOWN_AGENTS.filter(a => onPath(a.cmd)),
|
|
64
|
-
accountSvc: fs.existsSync(serviceFile(null))
|
|
65
|
-
// Which tier hosts the account service, so the menu can label it honestly.
|
|
66
|
-
// Cheap, read-only (file existence) — no cmux socket call here; the live
|
|
67
|
-
// cmux detection (which pings) happens in serveAccount at install time.
|
|
68
|
-
accountSvcKind: isCmuxServiceInstalled() ? 'cmux' : (fs.existsSync(serviceFile(null)) ? 'os' : null),
|
|
63
|
+
accountSvc: fs.existsSync(serviceFile(null)),
|
|
69
64
|
cwd: process.cwd(),
|
|
70
65
|
}
|
|
71
66
|
}
|
|
@@ -100,7 +95,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
100
95
|
io.print(` ${C.dim('│ provider ')} ${state.provider}`)
|
|
101
96
|
io.print(` ${C.dim('│ agents ')} ${state.agents.length ? state.agents.map(a => a.label).join(', ') : C.yellow('none on PATH')}`)
|
|
102
97
|
io.print(` ${C.dim('│ directory')} ${C.dim(state.cwd)}`)
|
|
103
|
-
io.print(` ${C.dim('│ service ')} ${state.accountSvc ? C.green('account service installed'
|
|
98
|
+
io.print(` ${C.dim('│ service ')} ${state.accountSvc ? C.green('account service installed') : 'none'}`)
|
|
104
99
|
io.print(' ' + C.dim('└' + '─'.repeat(54)) + '\n')
|
|
105
100
|
}
|
|
106
101
|
|
|
@@ -133,21 +128,8 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
133
128
|
{ label: '← Back to main menu', hint: '' },
|
|
134
129
|
])
|
|
135
130
|
if (how === 2) return
|
|
136
|
-
if (how ===
|
|
137
|
-
|
|
138
|
-
// surface (visible + killable) — but flag its weaker persistence FIRST and let
|
|
139
|
-
// the user opt back into the durable OS service.
|
|
140
|
-
if (actions.detectCmux && actions.detectCmux()) {
|
|
141
|
-
io.print('\n ' + C.cyan('Detected cmux') + ' — I can run the service as a cmux surface you can see and kill,')
|
|
142
|
-
io.print(' alongside your other terminals. Heads up — this is ' + C.yellow('less durable') + ' than the system service:')
|
|
143
|
-
io.print(' ' + C.dim('•') + ' stays up while cmux is running')
|
|
144
|
-
io.print(' ' + C.dim('•') + ' restarts itself on crash (--supervise)')
|
|
145
|
-
io.print(' ' + C.dim('•') + ' best-effort return via cmux session-restore')
|
|
146
|
-
io.print(' ' + C.dim('•') + ' ' + C.yellow('does NOT survive logout/reboot') + ' unless cmux launches at login')
|
|
147
|
-
const useCmux = await askYesNo('\n Run it as a cmux surface? (n = always-on system service)', true)
|
|
148
|
-
if (useCmux) { await actions.installAccountCmux(); resync(); return }
|
|
149
|
-
}
|
|
150
|
-
await actions.installService({ room: null }); resync()
|
|
131
|
+
if (how === 0) { await actions.installService({ room: null }); resync() }
|
|
132
|
+
else await actions.serveAccountForeground()
|
|
151
133
|
}
|
|
152
134
|
|
|
153
135
|
const serviceMenu = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.58",
|
|
4
4
|
"description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"transcript-sanitize.mjs",
|
|
16
16
|
"session-store.mjs",
|
|
17
17
|
"service.mjs",
|
|
18
|
-
"cmux.mjs",
|
|
19
18
|
"account.mjs",
|
|
20
19
|
"auth-store.mjs",
|
|
21
20
|
"provider.mjs",
|
package/cmux.mjs
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/* ─────────────────────────────────────────────────────────────
|
|
2
|
-
cmux.mjs — host the ACCOUNT service as a cmux surface instead of
|
|
3
|
-
a detached launchd/systemd daemon, when the bridge was launched
|
|
4
|
-
from inside cmux.
|
|
5
|
-
|
|
6
|
-
Why a separate tier: the OS service (service.mjs) is invisible —
|
|
7
|
-
it logs to a file and is supervised by launchd KeepAlive. Inside
|
|
8
|
-
cmux, the user would rather see the service running in a normal
|
|
9
|
-
surface they can watch and kill, alongside everything else. cmux,
|
|
10
|
-
unlike launchd, does NOT relaunch a surface whose command exits,
|
|
11
|
-
so the surface runs `serve --supervise` (bridge.mjs) which restarts
|
|
12
|
-
the account supervisor itself on crash.
|
|
13
|
-
|
|
14
|
-
Detection is deliberately strict: being on $PATH is NOT proof cmux
|
|
15
|
-
spawned us (claude-session.mjs makes the same distinction). Only the
|
|
16
|
-
CMUX_WORKSPACE_ID / CMUX_SURFACE_ID env vars — which cmux injects
|
|
17
|
-
into every surface it creates — prove it, and we additionally confirm
|
|
18
|
-
the socket actually answers before committing to this tier. If either
|
|
19
|
-
check fails the caller falls back to the OS service.
|
|
20
|
-
|
|
21
|
-
Persistence trade-off (flagged to the user in launcher.mjs before
|
|
22
|
-
install): up while cmux runs · crash-restart via --supervise ·
|
|
23
|
-
best-effort return on cmux session-restore · does NOT survive
|
|
24
|
-
logout/reboot unless cmux launches at login.
|
|
25
|
-
|
|
26
|
-
Spec: docs/specs/2026-06-23-cmux-hosted-account-service.md
|
|
27
|
-
───────────────────────────────────────────────────────────── */
|
|
28
|
-
|
|
29
|
-
import os from 'node:os'
|
|
30
|
-
import fs from 'node:fs'
|
|
31
|
-
import path from 'node:path'
|
|
32
|
-
import { execFileSync } from 'node:child_process'
|
|
33
|
-
|
|
34
|
-
const HOME = os.homedir()
|
|
35
|
-
const CFG_DIR = path.join(HOME, '.thinkpool-pair')
|
|
36
|
-
// One stable record per machine → exactly one cmux-hosted account service.
|
|
37
|
-
export const RECORD = path.join(CFG_DIR, 'cmux-service.json')
|
|
38
|
-
export const WS_NAME = 'thinkpool-pair service'
|
|
39
|
-
|
|
40
|
-
// Run a cmux CLI command; return trimmed stdout, or throw. Isolated so the rest
|
|
41
|
-
// of the module (and its tests) can run without the socket by injecting `run`.
|
|
42
|
-
function cmux(args, { timeout = 6000 } = {}) {
|
|
43
|
-
return execFileSync('cmux', args, { encoding: 'utf8', timeout, stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// cmux injects these into every surface it spawns. PATH containing a cmux shim is
|
|
47
|
-
// NOT enough — only the env proves cmux launched this shell.
|
|
48
|
-
export function inCmux(env = process.env) {
|
|
49
|
-
return !!(env.CMUX_WORKSPACE_ID || env.CMUX_SURFACE_ID)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Can we actually drive cmux right now? (socket answers a ping.) Returns false on
|
|
53
|
-
// any failure — a broken/absent socket means we cannot host a surface, so fall back.
|
|
54
|
-
export function cmuxReachable(run = cmux) {
|
|
55
|
-
try { run(['ping'], { timeout: 4000 }); return true } catch { return false }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Use the cmux tier only when BOTH hold: cmux spawned us AND the socket answers.
|
|
59
|
-
export function detectCmux(env = process.env, run = cmux) {
|
|
60
|
-
return inCmux(env) && cmuxReachable(run)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function readRecord(recordFile = RECORD) {
|
|
64
|
-
try { return JSON.parse(fs.readFileSync(recordFile, 'utf8')) } catch { return null }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function isCmuxServiceInstalled(recordFile = RECORD) {
|
|
68
|
-
return !!readRecord(recordFile)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// The command the cmux surface runs. PINNED to the installing version (same safety
|
|
72
|
-
// default as service.mjs — a bad publish can't auto-roll to the always-on bridge).
|
|
73
|
-
// `serve` runs account mode regardless of TTY (a cmux surface has a TTY, so a bare
|
|
74
|
-
// invocation would open the interactive launcher instead). `--supervise` restarts
|
|
75
|
-
// the account supervisor on crash, since cmux won't re-run a dead surface.
|
|
76
|
-
export function serviceCommand(version) {
|
|
77
|
-
return `npx -y thinkpool-pair@${version || 'latest'} serve --supervise`
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Pull a stable handle out of cmux output. Prefer a UUID (stable across reorders);
|
|
81
|
-
// fall back to a positional `workspace:N` ref; null if neither is present.
|
|
82
|
-
export function parseWorkspaceRef(out) {
|
|
83
|
-
const uuid = (String(out).match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i) || [])[0]
|
|
84
|
-
if (uuid) return uuid
|
|
85
|
-
const ref = (String(out).match(/workspace:\d+/) || [])[0]
|
|
86
|
-
return ref || null
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Best-effort: find the service workspace by its name when we have no stored ref
|
|
90
|
-
// (e.g. the record predates ref capture, or parsing failed at install time).
|
|
91
|
-
function resolveByName(run, name = WS_NAME) {
|
|
92
|
-
try {
|
|
93
|
-
const out = run(['--id-format', 'uuids', 'list-workspaces'])
|
|
94
|
-
const line = String(out).split('\n').find((l) => l.includes(name))
|
|
95
|
-
return line ? parseWorkspaceRef(line) : null
|
|
96
|
-
} catch { return null }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function spawnSurface(run, command, cwd = HOME) {
|
|
100
|
-
// --focus false: never steal focus from the user's current surface.
|
|
101
|
-
const out = run(['--id-format', 'uuids', 'new-workspace', '--name', WS_NAME, '--cwd', cwd, '--command', command, '--focus', 'false'])
|
|
102
|
-
return parseWorkspaceRef(out)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function closeSurface(run, rec) {
|
|
106
|
-
const ref = rec.workspace || resolveByName(run)
|
|
107
|
-
if (!ref) return
|
|
108
|
-
try { run(['close-workspace', '--workspace', ref]) } catch { /* may already be gone */ }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function installCmuxService({ version, cwd = HOME, run = cmux, recordFile = RECORD } = {}) {
|
|
112
|
-
const command = serviceCommand(version)
|
|
113
|
-
const workspace = spawnSurface(run, command, cwd)
|
|
114
|
-
const rec = { kind: 'cmux', version: version || 'latest', name: WS_NAME, command, workspace, installedAt: new Date().toISOString() }
|
|
115
|
-
fs.mkdirSync(path.dirname(recordFile), { recursive: true })
|
|
116
|
-
fs.writeFileSync(recordFile, JSON.stringify(rec, null, 2))
|
|
117
|
-
return rec
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Restart = close the surface + spawn a fresh one. The account supervisor restores
|
|
121
|
-
// every active session from disk (session-store crash-resume), so sessions resume
|
|
122
|
-
// in place — same contract as an OS-service restart.
|
|
123
|
-
export function restartCmuxService({ run = cmux, recordFile = RECORD } = {}) {
|
|
124
|
-
const rec = readRecord(recordFile)
|
|
125
|
-
if (!rec) return false
|
|
126
|
-
closeSurface(run, rec)
|
|
127
|
-
rec.workspace = spawnSurface(run, rec.command)
|
|
128
|
-
rec.installedAt = new Date().toISOString()
|
|
129
|
-
fs.writeFileSync(recordFile, JSON.stringify(rec, null, 2))
|
|
130
|
-
return true
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function uninstallCmuxService({ run = cmux, recordFile = RECORD } = {}) {
|
|
134
|
-
const rec = readRecord(recordFile)
|
|
135
|
-
if (!rec) return false
|
|
136
|
-
closeSurface(run, rec)
|
|
137
|
-
try { fs.rmSync(recordFile, { force: true }) } catch { /* noop */ }
|
|
138
|
-
return true
|
|
139
|
-
}
|