thinkpool-pair 0.7.192 → 0.7.194
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 +6 -26
- package/package.json +1 -1
- package/service.mjs +72 -9
package/bridge.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import os from 'node:os'
|
|
|
32
32
|
import fs from 'node:fs'
|
|
33
33
|
import path from 'node:path'
|
|
34
34
|
import readline from 'node:readline'
|
|
35
|
-
import { spawn,
|
|
35
|
+
import { spawn, execFileSync } from 'node:child_process'
|
|
36
36
|
import { fileURLToPath } from 'node:url'
|
|
37
37
|
import { createRequire } from 'node:module'
|
|
38
38
|
import { randomUUID } from 'node:crypto'
|
|
@@ -232,8 +232,9 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
232
232
|
if (argv[0] === 'restart-service') {
|
|
233
233
|
const arg1 = argv[1] || ''
|
|
234
234
|
const svcRoom = (!arg1 || arg1.startsWith('-')) ? null : arg1.toUpperCase().trim()
|
|
235
|
-
const svc = await import('./service.mjs')
|
|
236
|
-
|
|
235
|
+
const svc = await import('./service.mjs')
|
|
236
|
+
const ok = argv.includes('--current') ? svc.restartService(svcRoom) : svc.updateService(svcRoom)
|
|
237
|
+
process.exit(ok ? 0 : 1)
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
// Account mode (Phase 1, docs/specs/2026-06-16-account-bridge.md): one bridge per
|
|
@@ -270,7 +271,6 @@ if (argv[0] === 'provider') { const { runProvider } = await import('./provider.m
|
|
|
270
271
|
if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
271
272
|
const { runLauncher, detectState } = await import('./launcher.mjs')
|
|
272
273
|
const svc = await import('./service.mjs')
|
|
273
|
-
const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return 'latest' } })()
|
|
274
274
|
const BRIDGE = fileURLToPath(import.meta.url)
|
|
275
275
|
const io = {
|
|
276
276
|
print: (s) => process.stderr.write(s + '\n'),
|
|
@@ -294,28 +294,8 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
294
294
|
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []) },
|
|
295
295
|
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room) },
|
|
296
296
|
restartService: ({ room = null } = {}) => { svc.restartService(room) },
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
// each start so it can't re-serve an old build). We must NOT trust PKG_VERSION here:
|
|
300
|
-
// npx can run a STALE cached launcher, so this very menu may be an old version — that's
|
|
301
|
-
// the bug that left the bridge stuck on 0.7.75. `npm view` hits the registry live, so it
|
|
302
|
-
// updates correctly even from a stale launcher. Pinned to the resolved number — no auto-roll.
|
|
303
|
-
restartUpdateService: ({ room = null } = {}) => {
|
|
304
|
-
let target = null
|
|
305
|
-
try { target = execSync('npm view thinkpool-pair version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() } catch { /* offline / no npm */ }
|
|
306
|
-
if (!/^\d+\.\d+\.\d+/.test(target || '')) {
|
|
307
|
-
// Can't reach npm to learn the newest version — DON'T silently re-pin this
|
|
308
|
-
// launcher's PKG_VERSION (npx may have run a stale cached launcher, so it could
|
|
309
|
-
// be OLDER than what's installed). Just RESTART the current pinned service (no
|
|
310
|
-
// version change) and say so honestly — never call a same-version restart an
|
|
311
|
-
// "update".
|
|
312
|
-
process.stderr.write(`\n ⚠ couldn't reach npm to check for a newer version — restarting the current version, NO update applied. Try again when online.\n`)
|
|
313
|
-
svc.restartService(room)
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
process.stderr.write(`\n ◆ updating the background service to v${target} (newest published) and restarting…\n`)
|
|
317
|
-
svc.installService(room, [], { version: target, staleProof: true }) // rewrites plist/unit at resolved version + reloads = update + restart
|
|
318
|
-
},
|
|
297
|
+
// Menu and CLI share the same registry-resolve → repin → reload implementation.
|
|
298
|
+
restartUpdateService: ({ room = null } = {}) => { svc.updateService(room) },
|
|
319
299
|
login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
|
|
320
300
|
// Provider config is NOT terminal — it writes provider.json and returns to the
|
|
321
301
|
// menu so you can pick a model, switch back, or do something else. (It used to
|
package/package.json
CHANGED
package/service.mjs
CHANGED
|
@@ -43,7 +43,12 @@ const shq = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`
|
|
|
43
43
|
// which is what left Max's account bridge stuck on 0.7.75 while @latest was 0.7.78.
|
|
44
44
|
// `home` is the platform-correct, runtime-expanded home token ("$HOME" on darwin under
|
|
45
45
|
// /bin/bash; "$$HOME" inside a systemd unit so systemd emits a literal $HOME to bash).
|
|
46
|
-
|
|
46
|
+
// npx caches the temporary execution project as a unit. Removing only the package can
|
|
47
|
+
// leave its generated .bin/package-lock metadata behind, producing the contradictory
|
|
48
|
+
// state where `npm view` sees a version but `npx` reports ETARGET or no executable.
|
|
49
|
+
// The whole _npx directory is disposable; clear its children so the next launch is a
|
|
50
|
+
// genuinely clean install.
|
|
51
|
+
const cacheWipe = (home) => `rm -rf ${home}/.npm/_npx/* 2>/dev/null`
|
|
47
52
|
|
|
48
53
|
// The version installing the service. By DEFAULT the service is pinned to this exact
|
|
49
54
|
// version (not @latest) so a future bad npm publish can't auto-roll to every machine's
|
|
@@ -61,9 +66,30 @@ function npxPath(platform) {
|
|
|
61
66
|
try { fs.accessSync(cand); return cand } catch { return platform === 'win32' ? 'npx.cmd' : 'npx' }
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
function npmPath(platform = process.platform) {
|
|
70
|
+
const bin = path.dirname(process.execPath)
|
|
71
|
+
const cand = path.join(bin, platform === 'win32' ? 'npm.cmd' : 'npm')
|
|
72
|
+
try { fs.accessSync(cand); return cand } catch { return platform === 'win32' ? 'npm.cmd' : 'npm' }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Persistent services never execute through npx. Provision an immutable, versioned
|
|
76
|
+
// runtime from the tarball URL (which bypasses stale npm packument metadata), then point
|
|
77
|
+
// launchd/systemd directly at its bridge.mjs with the current Node binary.
|
|
78
|
+
export function provisionRuntime(version, { exec = execSync } = {}) {
|
|
79
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(version))) throw new Error(`invalid runtime version: ${version}`)
|
|
80
|
+
const dir = path.join(os.homedir(), '.thinkpool-pair', 'runtimes', String(version))
|
|
81
|
+
const entry = path.join(dir, 'node_modules', 'thinkpool-pair', 'bridge.mjs')
|
|
82
|
+
if (fs.existsSync(entry)) return entry
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
84
|
+
const tarball = `https://registry.npmjs.org/thinkpool-pair/-/thinkpool-pair-${version}.tgz`
|
|
85
|
+
exec(`${shq(npmPath())} install --prefix ${shq(dir)} ${shq(tarball)} --prefer-online --no-audit --no-fund`, { stdio: 'inherit', shell: '/bin/bash' })
|
|
86
|
+
if (!fs.existsSync(entry)) throw new Error(`runtime install completed without ${entry}`)
|
|
87
|
+
return entry
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
// Pure artifact builder — returned shape is testable without side effects.
|
|
65
91
|
// room falsy → ACCOUNT service (bare `thinkpool-pair`, auto-serves all sessions).
|
|
66
|
-
export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate = false, version = VERSION, staleProof = false } = {}) {
|
|
92
|
+
export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate = false, version = VERSION, staleProof = false, runtimeEntry = null } = {}) {
|
|
67
93
|
const npx = npxPath(platform)
|
|
68
94
|
const cwd = process.cwd()
|
|
69
95
|
const id = slug(room)
|
|
@@ -83,7 +109,7 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
83
109
|
// 0.7.87). `--prefer-online` forces npx to REVALIDATE the packument against the
|
|
84
110
|
// registry every resolve, so `@latest` always means the true latest. Only needed for
|
|
85
111
|
// the @latest (autoUpdate) tier — a pinned exact version is immutable, no staleness.
|
|
86
|
-
const onlineFlag = autoUpdate ? ['--prefer-online'] : []
|
|
112
|
+
const onlineFlag = (autoUpdate || staleProof) ? ['--prefer-online'] : []
|
|
87
113
|
const pkgArgs = room ? ['-y', ...onlineFlag, verSpec, room] : ['-y', ...onlineFlag, verSpec]
|
|
88
114
|
// In-process self-update (account.mjs / bridge.mjs) is gated on this env — only on when
|
|
89
115
|
// the user explicitly opted into auto-update.
|
|
@@ -92,7 +118,9 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
92
118
|
if (platform === 'darwin') {
|
|
93
119
|
// launchd KeepAlive supervises. staleProof → wrap in a cache-wiping /bin/bash launch so
|
|
94
120
|
// every (re)start re-fetches the pinned version fresh (deliberate-update path).
|
|
95
|
-
const args =
|
|
121
|
+
const args = runtimeEntry
|
|
122
|
+
? [process.execPath, runtimeEntry, ...(room ? [room] : []), ...tail]
|
|
123
|
+
: staleProof
|
|
96
124
|
? ['/bin/bash', '-lc', `${cacheWipe('"$HOME"')}; exec ${[npx, ...pkgArgs, ...tail].map(shq).join(' ')}`]
|
|
97
125
|
: [npx, ...pkgArgs, ...tail]
|
|
98
126
|
const file = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label(room)}.plist`)
|
|
@@ -148,11 +176,15 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
148
176
|
}
|
|
149
177
|
|
|
150
178
|
if (platform === 'linux') {
|
|
151
|
-
const args =
|
|
179
|
+
const args = runtimeEntry
|
|
180
|
+
? [process.execPath, runtimeEntry, ...(room ? [room] : []), ...tail]
|
|
181
|
+
: [npx, ...pkgArgs, ...tail] // systemd Restart=always supervises
|
|
152
182
|
const file = path.join(os.homedir(), '.config', 'systemd', 'user', `${label(room)}.service`)
|
|
153
183
|
// staleProof → cache-wiping /bin/bash launch (same intent as darwin). `$$HOME` so systemd
|
|
154
184
|
// emits a literal $HOME to bash rather than expanding it itself (empty + warning).
|
|
155
|
-
const execStart =
|
|
185
|
+
const execStart = runtimeEntry
|
|
186
|
+
? args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')
|
|
187
|
+
: staleProof
|
|
156
188
|
? `/bin/bash -lc "${cacheWipe('$$HOME')}; exec ${[npx, ...pkgArgs, ...tail].join(' ')}"`
|
|
157
189
|
: args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')
|
|
158
190
|
const content = `[Unit]
|
|
@@ -187,7 +219,9 @@ WantedBy=default.target
|
|
|
187
219
|
// so it gets login-persistence only — no in-process crash-restart on Windows.
|
|
188
220
|
const startup = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
|
|
189
221
|
const file = path.join(startup, `thinkpool-pair-${id}.cmd`)
|
|
190
|
-
const inner =
|
|
222
|
+
const inner = runtimeEntry
|
|
223
|
+
? [process.execPath, runtimeEntry, ...(room ? [room, '--supervise'] : []), ...tail].map((s) => `"${s}"`).join(' ')
|
|
224
|
+
: room
|
|
191
225
|
? ['npx', '-y', ...onlineFlag, verSpec, room, '--supervise', ...tail].join(' ')
|
|
192
226
|
: ['npx', '-y', ...onlineFlag, verSpec].join(' ')
|
|
193
227
|
const content = `@echo off\r\ntitle thinkpool-pair ${id}\r\n${inner}\r\n`
|
|
@@ -294,7 +328,14 @@ export function sweepExistingServices(keepLabel, platform = process.platform) {
|
|
|
294
328
|
// pinned version is fetched fresh on every (re)start. buildArtifact keeps staleProof=false
|
|
295
329
|
// so its unit tests assert the pure pinned shape.
|
|
296
330
|
export function installService(room, cmdArgs = [], { autoUpdate = false, version = VERSION, staleProof = true } = {}) {
|
|
297
|
-
|
|
331
|
+
// Pinned services get a stable local runtime. The legacy @latest opt-in keeps its
|
|
332
|
+
// existing npx behavior until it resolves a concrete version through updateService.
|
|
333
|
+
let runtimeEntry = null
|
|
334
|
+
if (!autoUpdate) {
|
|
335
|
+
try { runtimeEntry = provisionRuntime(version) }
|
|
336
|
+
catch (e) { process.stderr.write(` ⚠ runtime install failed; service left unchanged: ${e.message}\n`); return false }
|
|
337
|
+
}
|
|
338
|
+
const a = buildArtifact(process.platform, { room, cmdArgs, autoUpdate, version, staleProof, runtimeEntry })
|
|
298
339
|
// F4: clear every OTHER pre-existing pair service first, so only the one we install remains
|
|
299
340
|
// (the "two account services live" storm trigger). Our own label is bootout+bootstrapped
|
|
300
341
|
// by a.post below, so keep it.
|
|
@@ -318,10 +359,11 @@ export function installService(room, cmdArgs = [], { autoUpdate = false, version
|
|
|
318
359
|
? (process.platform === 'win32'
|
|
319
360
|
? 'auto-update ON: a new thinkpool-pair@latest applies on the next login/restart (Windows has no in-process self-update).'
|
|
320
361
|
: `auto-update ON: checks npm ~every 30 min and restarts to apply a new @latest once sessions are idle (≥${idleSecs}s quiet; they resume in place).`)
|
|
321
|
-
: `PINNED to ${version}
|
|
362
|
+
: `PINNED to ${version} (stable local runtime; restarts do not depend on npx or the network). To update: use "Restart & update the bridge" (or re-run install-service from a newer thinkpool-pair). Pass --auto-update to track @latest instead.`
|
|
322
363
|
const removeArg = room ? ` ${room}` : ''
|
|
323
364
|
const what = room ? `room ${room}` : 'your account (auto-serves every session)'
|
|
324
365
|
process.stderr.write(` ◆ ${what}\n ◆ ${a.note}\n ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: thinkpool-pair uninstall-service${removeArg}\n\n`)
|
|
366
|
+
return true
|
|
325
367
|
}
|
|
326
368
|
|
|
327
369
|
// Restart an already-installed boot-persistent service. launchd/systemd relaunch
|
|
@@ -353,6 +395,27 @@ export function restartService(room) {
|
|
|
353
395
|
}
|
|
354
396
|
}
|
|
355
397
|
|
|
398
|
+
// One authoritative update path for both the interactive menu and `restart-service`.
|
|
399
|
+
// Resolve the registry version first, then rewrite + reload the service pinned to that
|
|
400
|
+
// exact build. If npm is unavailable, fail without pretending an old-version restart
|
|
401
|
+
// was an update.
|
|
402
|
+
export function updateService(room, { exec = execSync, install = installService, active = isServiceInstalled } = {}) {
|
|
403
|
+
if (!active(room)) {
|
|
404
|
+
process.stderr.write(' ⚠ no background service installed to update — install one first.\n')
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
let target = ''
|
|
408
|
+
try {
|
|
409
|
+
target = String(exec('npm view thinkpool-pair version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })).trim()
|
|
410
|
+
} catch { /* reported below */ }
|
|
411
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(target)) {
|
|
412
|
+
process.stderr.write(" ⚠ couldn't reach npm or resolve the published version — bridge left unchanged; NO update or restart was applied.\n")
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
process.stderr.write(`\n ◆ updating the background service to v${target} and restarting…\n`)
|
|
416
|
+
return install(room, [], { version: target, staleProof: true }) !== false
|
|
417
|
+
}
|
|
418
|
+
|
|
356
419
|
// Ground truth: is the OS service manager ACTUALLY running our service right now?
|
|
357
420
|
// A leftover plist/unit on disk does NOT mean it's loaded — `launchctl load` can
|
|
358
421
|
// silently no-op (the 2026-06-26 "says installed but it's off" bug), and a unit can
|