thinkpool-pair 0.7.72 → 0.7.74
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/launcher.mjs +14 -1
- package/package.json +1 -1
- package/service.mjs +39 -6
package/launcher.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import os from 'node:os'
|
|
14
14
|
import fs from 'node:fs'
|
|
15
15
|
import path from 'node:path'
|
|
16
|
+
import { execSync } from 'node:child_process'
|
|
16
17
|
|
|
17
18
|
const HOME = os.homedir()
|
|
18
19
|
const CFG_DIR = path.join(HOME, '.thinkpool-pair')
|
|
@@ -48,6 +49,18 @@ function serviceFile(room) {
|
|
|
48
49
|
return path.join(startup, `thinkpool-pair-${room || 'account'}.cmd`)
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Ground truth for the header: is the service actually LOADED, not just a plist on disk?
|
|
53
|
+
// `launchctl load` can silently no-op, leaving a plist that never loads — file-existence
|
|
54
|
+
// would report "installed" while the bridge is off (the 2026-06-26 bug). Query the manager.
|
|
55
|
+
function serviceLoaded(room) {
|
|
56
|
+
const label = room ? `io.thinkpool.pair.${room.toLowerCase()}` : 'io.thinkpool.pair.account'
|
|
57
|
+
try {
|
|
58
|
+
if (process.platform === 'darwin') { execSync(`launchctl print gui/$(id -u)/${label}`, { stdio: 'ignore' }); return true }
|
|
59
|
+
if (process.platform === 'linux') { execSync(`systemctl --user is-active --quiet ${label}.service`); return true }
|
|
60
|
+
return fs.existsSync(serviceFile(room)) // Windows: no daemon, file presence is the only proxy
|
|
61
|
+
} catch { return false }
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
export function detectState() {
|
|
52
65
|
const auth = readJson(path.join(CFG_DIR, 'auth.json'))
|
|
53
66
|
const provider = readJson(path.join(CFG_DIR, 'provider.json'))
|
|
@@ -60,7 +73,7 @@ export function detectState() {
|
|
|
60
73
|
provider: isCustom ? `Custom (${provider.baseUrl || 'endpoint'})` : 'Anthropic (default)',
|
|
61
74
|
providerModel: isCustom ? (provider.model || null) : null,
|
|
62
75
|
agents: KNOWN_AGENTS.filter(a => onPath(a.cmd)),
|
|
63
|
-
accountSvc:
|
|
76
|
+
accountSvc: serviceLoaded(null),
|
|
64
77
|
cwd: process.cwd(),
|
|
65
78
|
}
|
|
66
79
|
}
|
package/package.json
CHANGED
package/service.mjs
CHANGED
|
@@ -79,7 +79,24 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
79
79
|
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
80
80
|
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(process.env.PATH || '')}</string>${autoUpdate ? '<key>THINKPOOL_PAIR_AUTOUPDATE</key><string>1</string>' : ''}</dict>
|
|
81
81
|
</dict></plist>\n`
|
|
82
|
-
|
|
82
|
+
// Modern bootstrap/enable, NOT the deprecated `launchctl load`. On current macOS
|
|
83
|
+
// (Darwin 24/25) `load` silently no-ops for a GUI LaunchAgent — the plist lands on
|
|
84
|
+
// disk but never loads, so the bridge is "installed" yet off (the 2026-06-26 bug).
|
|
85
|
+
// Sequence: bootout any existing instance (idempotent reinstall) → WAIT for it to
|
|
86
|
+
// actually leave the domain (bootout is async; bootstrapping while teardown is in
|
|
87
|
+
// flight throws "Bootstrap failed: 5: Input/output error") → enable (clears a stale
|
|
88
|
+
// `disabled` override from a prior bootout / the account-storm cleanup) → bootstrap,
|
|
89
|
+
// retried on the EIO race → a final `print` so the command's exit code reports the
|
|
90
|
+
// real loaded state (installService warns if it ends not-loaded).
|
|
91
|
+
const lbl = label(room), dom = 'gui/$(id -u)'
|
|
92
|
+
const post = [
|
|
93
|
+
`launchctl bootout ${dom}/${lbl} 2>/dev/null`,
|
|
94
|
+
`for i in $(seq 1 20); do launchctl print ${dom}/${lbl} >/dev/null 2>&1 || break; sleep 0.2; done`,
|
|
95
|
+
`launchctl enable ${dom}/${lbl}`,
|
|
96
|
+
`for i in $(seq 1 20); do launchctl bootstrap ${dom} "${file}" 2>/dev/null && break; sleep 0.3; done`,
|
|
97
|
+
`launchctl print ${dom}/${lbl} >/dev/null 2>&1`,
|
|
98
|
+
].join('; ')
|
|
99
|
+
return { file, content, logDir, post: [post], note: 'Loaded as a LaunchAgent — starts at login, restarts on crash.' }
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
if (platform === 'linux') {
|
|
@@ -173,18 +190,34 @@ export function restartService(room) {
|
|
|
173
190
|
}
|
|
174
191
|
}
|
|
175
192
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
193
|
+
// Ground truth: is the OS service manager ACTUALLY running our service right now?
|
|
194
|
+
// A leftover plist/unit on disk does NOT mean it's loaded — `launchctl load` can
|
|
195
|
+
// silently no-op (the 2026-06-26 "says installed but it's off" bug), and a unit can
|
|
196
|
+
// be left disabled. Ask the manager, don't trust file presence. room falsy → account.
|
|
197
|
+
export function serviceActive(room) {
|
|
198
|
+
const plat = process.platform
|
|
199
|
+
const id = label(room)
|
|
200
|
+
try {
|
|
201
|
+
if (plat === 'darwin') { execSync(`launchctl print gui/$(id -u)/${id}`, { stdio: 'ignore' }); return true }
|
|
202
|
+
if (plat === 'linux') { execSync(`systemctl --user is-active --quiet ${id}.service`); return true }
|
|
203
|
+
// Windows has no managed daemon — the Startup-folder script's presence is the only proxy.
|
|
204
|
+
return fs.existsSync(buildArtifact(plat, { room }).file)
|
|
205
|
+
} catch { return false }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Is a boot-persistent service installed AND loaded? room falsy → the ACCOUNT service.
|
|
209
|
+
// Lets the main run skip the offer + avoid starting a second bridge that races the
|
|
210
|
+
// service. Reflects launchd/systemd reality, not just file existence (so a plist that
|
|
211
|
+
// failed to load reads as not-installed → the menu re-offers install, which self-heals).
|
|
179
212
|
export function isServiceInstalled(room) {
|
|
180
|
-
|
|
213
|
+
return serviceActive(room)
|
|
181
214
|
}
|
|
182
215
|
|
|
183
216
|
export function uninstallService(room) {
|
|
184
217
|
const plat = process.platform
|
|
185
218
|
let a
|
|
186
219
|
try { a = buildArtifact(plat, { room }) } catch (e) { process.stderr.write(` ⚠ ${e.message}\n`); return }
|
|
187
|
-
if (plat === 'darwin') { try { execSync(`launchctl
|
|
220
|
+
if (plat === 'darwin') { try { execSync(`launchctl bootout gui/$(id -u)/${label(room)} 2>/dev/null`) } catch { /* noop */ } }
|
|
188
221
|
if (plat === 'linux') { try { execSync(`systemctl --user disable --now ${label(room)}.service 2>/dev/null`) } catch { /* noop */ } }
|
|
189
222
|
try { fs.rmSync(a.file, { force: true }); process.stderr.write(` ◆ removed ${a.file}\n`) } catch (e) { process.stderr.write(` ⚠ ${e.message}\n`) }
|
|
190
223
|
}
|