thinkpool-pair 0.7.193 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/service.mjs +45 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.193",
3
+ "version": "0.7.194",
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": {
package/service.mjs CHANGED
@@ -66,9 +66,30 @@ function npxPath(platform) {
66
66
  try { fs.accessSync(cand); return cand } catch { return platform === 'win32' ? 'npx.cmd' : 'npx' }
67
67
  }
68
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
+
69
90
  // Pure artifact builder — returned shape is testable without side effects.
70
91
  // room falsy → ACCOUNT service (bare `thinkpool-pair`, auto-serves all sessions).
71
- 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 } = {}) {
72
93
  const npx = npxPath(platform)
73
94
  const cwd = process.cwd()
74
95
  const id = slug(room)
@@ -97,7 +118,9 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
97
118
  if (platform === 'darwin') {
98
119
  // launchd KeepAlive supervises. staleProof → wrap in a cache-wiping /bin/bash launch so
99
120
  // every (re)start re-fetches the pinned version fresh (deliberate-update path).
100
- const args = staleProof
121
+ const args = runtimeEntry
122
+ ? [process.execPath, runtimeEntry, ...(room ? [room] : []), ...tail]
123
+ : staleProof
101
124
  ? ['/bin/bash', '-lc', `${cacheWipe('"$HOME"')}; exec ${[npx, ...pkgArgs, ...tail].map(shq).join(' ')}`]
102
125
  : [npx, ...pkgArgs, ...tail]
103
126
  const file = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label(room)}.plist`)
@@ -153,11 +176,15 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
153
176
  }
154
177
 
155
178
  if (platform === 'linux') {
156
- const args = [npx, ...pkgArgs, ...tail] // systemd Restart=always supervises
179
+ const args = runtimeEntry
180
+ ? [process.execPath, runtimeEntry, ...(room ? [room] : []), ...tail]
181
+ : [npx, ...pkgArgs, ...tail] // systemd Restart=always supervises
157
182
  const file = path.join(os.homedir(), '.config', 'systemd', 'user', `${label(room)}.service`)
158
183
  // staleProof → cache-wiping /bin/bash launch (same intent as darwin). `$$HOME` so systemd
159
184
  // emits a literal $HOME to bash rather than expanding it itself (empty + warning).
160
- const execStart = staleProof
185
+ const execStart = runtimeEntry
186
+ ? args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')
187
+ : staleProof
161
188
  ? `/bin/bash -lc "${cacheWipe('$$HOME')}; exec ${[npx, ...pkgArgs, ...tail].join(' ')}"`
162
189
  : args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')
163
190
  const content = `[Unit]
@@ -192,7 +219,9 @@ WantedBy=default.target
192
219
  // so it gets login-persistence only — no in-process crash-restart on Windows.
193
220
  const startup = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
194
221
  const file = path.join(startup, `thinkpool-pair-${id}.cmd`)
195
- const inner = room
222
+ const inner = runtimeEntry
223
+ ? [process.execPath, runtimeEntry, ...(room ? [room, '--supervise'] : []), ...tail].map((s) => `"${s}"`).join(' ')
224
+ : room
196
225
  ? ['npx', '-y', ...onlineFlag, verSpec, room, '--supervise', ...tail].join(' ')
197
226
  : ['npx', '-y', ...onlineFlag, verSpec].join(' ')
198
227
  const content = `@echo off\r\ntitle thinkpool-pair ${id}\r\n${inner}\r\n`
@@ -299,7 +328,14 @@ export function sweepExistingServices(keepLabel, platform = process.platform) {
299
328
  // pinned version is fetched fresh on every (re)start. buildArtifact keeps staleProof=false
300
329
  // so its unit tests assert the pure pinned shape.
301
330
  export function installService(room, cmdArgs = [], { autoUpdate = false, version = VERSION, staleProof = true } = {}) {
302
- const a = buildArtifact(process.platform, { room, cmdArgs, autoUpdate, version, staleProof })
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 })
303
339
  // F4: clear every OTHER pre-existing pair service first, so only the one we install remains
304
340
  // (the "two account services live" storm trigger). Our own label is bootout+bootstrapped
305
341
  // by a.post below, so keep it.
@@ -323,10 +359,11 @@ export function installService(room, cmdArgs = [], { autoUpdate = false, version
323
359
  ? (process.platform === 'win32'
324
360
  ? 'auto-update ON: a new thinkpool-pair@latest applies on the next login/restart (Windows has no in-process self-update).'
325
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).`)
326
- : `PINNED to ${version}${staleProof ? ' (stale-proof: every restart re-fetches it fresh, never a stale npx cache)' : ''} (safe default — a bad publish can't auto-roll to this always-on bridge). To update: use "Restart & update the bridge" (or re-run install-service from a newer thinkpool-pair). Pass --auto-update to track @latest instead.`
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.`
327
363
  const removeArg = room ? ` ${room}` : ''
328
364
  const what = room ? `room ${room}` : 'your account (auto-serves every session)'
329
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
330
367
  }
331
368
 
332
369
  // Restart an already-installed boot-persistent service. launchd/systemd relaunch
@@ -376,8 +413,7 @@ export function updateService(room, { exec = execSync, install = installService,
376
413
  return false
377
414
  }
378
415
  process.stderr.write(`\n ◆ updating the background service to v${target} and restarting…\n`)
379
- install(room, [], { version: target, staleProof: true })
380
- return true
416
+ return install(room, [], { version: target, staleProof: true }) !== false
381
417
  }
382
418
 
383
419
  // Ground truth: is the OS service manager ACTUALLY running our service right now?