thinkpool-pair 0.7.307 → 0.7.308
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/account.mjs +18 -0
- package/bridge.mjs +9 -5
- package/launcher.mjs +7 -1
- package/package.json +1 -1
- package/service.mjs +59 -0
package/account.mjs
CHANGED
|
@@ -28,6 +28,22 @@ const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./pack
|
|
|
28
28
|
|
|
29
29
|
const BRIDGE = fileURLToPath(new URL('./bridge.mjs', import.meta.url))
|
|
30
30
|
|
|
31
|
+
// Startup can spend several seconds waiting on the cross-process refresh lock or
|
|
32
|
+
// backing off after a transient auth failure. Those sleeps are intentionally unref'd
|
|
33
|
+
// once the supervisor is running, but before Realtime + the 15s tick exist they leave
|
|
34
|
+
// an unresolved top-level await as Node's only work; Node 22 exits 13 with
|
|
35
|
+
// "Detected unsettled top-level await". Hold one referenced timer only for bootstrap,
|
|
36
|
+
// then release it as soon as the supervisor's real lifetime interval exists.
|
|
37
|
+
export function createStartupAnchor({ set = setInterval, clear = clearInterval } = {}) {
|
|
38
|
+
const timer = set(() => {}, 60_000)
|
|
39
|
+
let released = false
|
|
40
|
+
return () => {
|
|
41
|
+
if (released) return
|
|
42
|
+
released = true
|
|
43
|
+
clear(timer)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
// ── single-instance lock ────────────────────────────────────────────────────
|
|
32
48
|
// Two account supervisors on one machine share ONE ~/.thinkpool-pair/auth.json
|
|
33
49
|
// refresh token and race its rotation — which on 2026-06-17 deleted a live Pro
|
|
@@ -360,6 +376,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
360
376
|
console.error('\n ◇ An account bridge is already running on this machine\n (~/.thinkpool-pair/account.lock). Not starting a second — two would race\n your saved login. Stop the other one first, or share a single room with\n `npx thinkpool-pair <ROOM>`.\n')
|
|
361
377
|
process.exit(0)
|
|
362
378
|
}
|
|
379
|
+
const releaseStartupAnchor = createStartupAnchor()
|
|
363
380
|
// F2: NEVER exit(1) on a refresh failure. The old code exited 1 here, and launchd
|
|
364
381
|
// KeepAlive respawned us within seconds → refresh again → the 16× crash storm (and,
|
|
365
382
|
// worse, hammering the refresh path burns rotations and can revoke the token family).
|
|
@@ -1043,6 +1060,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
1043
1060
|
|
|
1044
1061
|
await tick()
|
|
1045
1062
|
const iv = setInterval(tick, 15000)
|
|
1063
|
+
releaseStartupAnchor()
|
|
1046
1064
|
const updateTimers = [] // auto-update poll/apply timers — cleared in stop() so none fire post-exit
|
|
1047
1065
|
let stopping = false
|
|
1048
1066
|
|
package/bridge.mjs
CHANGED
|
@@ -249,7 +249,12 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
249
249
|
// always-on service); --auto-update opts into @latest tracking. (2026-06-22)
|
|
250
250
|
const autoUpdate = argv.includes('--auto-update')
|
|
251
251
|
const svc = await import('./service.mjs')
|
|
252
|
-
if (argv[0] === 'install-service')
|
|
252
|
+
if (argv[0] === 'install-service') {
|
|
253
|
+
const ok = autoUpdate
|
|
254
|
+
? svc.installService(svcRoom, svcCmd, { autoUpdate })
|
|
255
|
+
: await svc.installAndConfirmService(svcRoom, svcCmd)
|
|
256
|
+
process.exit(ok === false ? 1 : 0)
|
|
257
|
+
}
|
|
253
258
|
else svc.uninstallService(svcRoom)
|
|
254
259
|
process.exit(0)
|
|
255
260
|
}
|
|
@@ -350,10 +355,9 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
350
355
|
child.on('exit', (code) => process.exit(code == null ? 0 : code))
|
|
351
356
|
}),
|
|
352
357
|
serveAccountForeground: async () => { const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON) },
|
|
353
|
-
//
|
|
354
|
-
// and
|
|
355
|
-
|
|
356
|
-
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []) },
|
|
358
|
+
// Confirmed installs/updates close the launcher: the service is independently
|
|
359
|
+
// running and another menu cycle can only race it. Failures return to the menu.
|
|
360
|
+
installService: ({ room = null, agentCmd } = {}) => svc.installAndConfirmService(room, agentCmd ? [agentCmd] : []),
|
|
357
361
|
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room) },
|
|
358
362
|
restartService: ({ room = null } = {}) => { svc.restartService(room) },
|
|
359
363
|
// The menu waits for OS-level proof before claiming success. In-service web
|
package/launcher.mjs
CHANGED
|
@@ -277,7 +277,13 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
277
277
|
const pick = items[await askChoice('choose', items)]
|
|
278
278
|
if (pick.key === 'quit') return
|
|
279
279
|
if (pick.key === 'serve') { if (await ensureLoggedIn()) await actions.serveAccountForeground() }
|
|
280
|
-
else if (pick.key === 'service') {
|
|
280
|
+
else if (pick.key === 'service') {
|
|
281
|
+
if (await ensureLoggedIn()) {
|
|
282
|
+
const installed = await actions.installService({ room: null })
|
|
283
|
+
if (installed === true) return
|
|
284
|
+
resync()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
281
287
|
else if (pick.key === 'restart') {
|
|
282
288
|
const updated = await actions.restartUpdateService({ room: null })
|
|
283
289
|
// The launcher process cannot update its own in-memory VERSION. Returning to
|
package/package.json
CHANGED
package/service.mjs
CHANGED
|
@@ -456,6 +456,65 @@ export function installService(room, cmdArgs = [], { autoUpdate = false, version
|
|
|
456
456
|
return true
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
// Interactive installs must not present an armed launchd helper as a completed
|
|
460
|
+
// background service. installService() intentionally returns after staging because
|
|
461
|
+
// an in-service update may tear down its caller; this menu/CLI primitive waits for the
|
|
462
|
+
// helper receipt AND the exact live runtime before returning success.
|
|
463
|
+
export async function installAndConfirmService(room, cmdArgs = [], {
|
|
464
|
+
platform = process.platform,
|
|
465
|
+
version = VERSION,
|
|
466
|
+
staleProof = true,
|
|
467
|
+
install = installService,
|
|
468
|
+
exec = execSync,
|
|
469
|
+
snapshot = serviceRuntimeSnapshot,
|
|
470
|
+
readStatus = () => {
|
|
471
|
+
try { return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.thinkpool-pair', 'update-status.json'), 'utf8')) } catch { return null }
|
|
472
|
+
},
|
|
473
|
+
now = Date.now,
|
|
474
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
475
|
+
timeoutMs = 55000,
|
|
476
|
+
pollMs = 250,
|
|
477
|
+
stderr = process.stderr,
|
|
478
|
+
} = {}) {
|
|
479
|
+
if (install(room, cmdArgs, { version, staleProof }) === false) return false
|
|
480
|
+
|
|
481
|
+
const confirmation = () => {
|
|
482
|
+
const live = snapshot(room, { platform, exec })
|
|
483
|
+
if (live?.version === version) {
|
|
484
|
+
stderr.write(` ✓ bridge install confirmed — running v${version}.\n`)
|
|
485
|
+
return true
|
|
486
|
+
}
|
|
487
|
+
return false
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// systemd's restart is synchronous. Windows cannot prove a live Startup process,
|
|
491
|
+
// so this exact-runtime confirmation is used only where a service manager exists.
|
|
492
|
+
if (platform !== 'darwin') {
|
|
493
|
+
if (platform === 'win32') return true
|
|
494
|
+
const confirmed = confirmation()
|
|
495
|
+
if (!confirmed) stderr.write(` ⚠ install v${version} was not confirmed: the service manager cannot prove that runtime is running.\n`)
|
|
496
|
+
return confirmed
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const deadline = now() + Math.max(0, Number(timeoutMs) || 0)
|
|
500
|
+
const expectedTarget = label(room)
|
|
501
|
+
for (;;) {
|
|
502
|
+
const status = readStatus()
|
|
503
|
+
if (status && typeof status === 'object' && status.target === expectedTarget && status.version === version) {
|
|
504
|
+
if (status.ok === false) {
|
|
505
|
+
stderr.write(` ⚠ install v${version} was not confirmed: ${status.error || 'the launchd handoff failed'}.\n`)
|
|
506
|
+
return false
|
|
507
|
+
}
|
|
508
|
+
if (status.ok === true && confirmation()) return true
|
|
509
|
+
}
|
|
510
|
+
if (now() >= deadline) {
|
|
511
|
+
stderr.write(` ⚠ install v${version} timed out waiting for launchd confirmation; no running service was proven.\n`)
|
|
512
|
+
return false
|
|
513
|
+
}
|
|
514
|
+
await sleep(Math.max(1, Number(pollMs) || 1))
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
459
518
|
// Restart an already-installed boot-persistent service. launchd/systemd relaunch
|
|
460
519
|
// the stable runtime (or legacy auto-update npx command), so active sessions resume
|
|
461
520
|
// from disk. Use it to recover a wedged bridge or re-read provider.json.
|