claude-discover 1.10.4 → 1.10.6
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/bin/claude/statusline.mjs +125 -125
- package/bin/claude-discover.mjs +32 -1
- package/bin/update.mjs +42 -42
- package/out/main/main.js +190 -126
- package/out/preload/preload.cjs +1 -0
- package/out/renderer/assets/{index-DsnXk_cj.css → index-BQftbGlQ.css} +470 -286
- package/out/renderer/assets/{index-B6LVyX-b.js → index-CirBBtX2.js} +331 -90
- package/out/renderer/assets/{vendors-CpMM-Zb0.js → vendors-BRqWyPqt.js} +17 -7
- package/out/renderer/index.html +3 -3
- package/package.json +85 -81
- package/out/preload/findPreload.cjs +0 -16
- package/out/renderer/assets/find-CxcUmpSm.css +0 -88
- package/out/renderer/assets/find-DSEw-KLs.js +0 -59
- package/out/renderer/find/find.html +0 -26
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Claude Code status line, installed by StatuslineSwitch. Reads the hook JSON from stdin and
|
|
3
|
-
// prints one colored line — model and effort, context usage, token totals, rate-limit windows:
|
|
4
|
-
// [Opus 4.8 high] Context: ▓▓▓░░░░░░░ 32% used (45.2k, 87% cached) | Tokens: 1.2M total (+45.2k, 3 turns) | Daily: 42% used (resets in 2h 30m)
|
|
5
|
-
|
|
6
|
-
import { readFileSync } from 'node:fs'
|
|
7
|
-
import { styleText } from 'node:util'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// Formatting
|
|
11
|
-
const THRESHOLDS = { // (white, yellow, red)
|
|
12
|
-
context: [0, 100_000, 250_000],
|
|
13
|
-
limit: [0, 60, 90],
|
|
14
|
-
cache: [90, 60, 0],
|
|
15
|
-
tokens: [0, 250_000, 1_000_000],
|
|
16
|
-
session: [0, 3_000_000, 8_000_000],
|
|
17
|
-
turns: [0, 10, 25],
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const NO_COLOR = process.env.NO_COLOR !== undefined // standard opt-out; styleText can't honor it for piped output, so gate here
|
|
21
|
-
|
|
22
|
-
function colorize(kind, text, value = 0) {
|
|
23
|
-
const bounds = THRESHOLDS[kind]
|
|
24
|
-
const n = bounds.filter(b => value >= b).length
|
|
25
|
-
const TIERS = [null, 'yellow', 'red'] // indexed by severity tier
|
|
26
|
-
const idx = bounds[0] < bounds.at(-1) ? n - 1 : bounds.length - n
|
|
27
|
-
const color = TIERS[idx]
|
|
28
|
-
// validateStream:false forces codes through the pipe (statusline output isn't a TTY)
|
|
29
|
-
return color && !NO_COLOR ? styleText(color, text, { validateStream: false }) : text
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Collect stdin data
|
|
34
|
-
const data = JSON.parse(readFileSync(0, 'utf-8')) // fd 0 = stdin
|
|
35
|
-
const modelName = data.model?.display_name?.replace('Claude ', '') || '?'
|
|
36
|
-
const effort = data.effort?.level // absent when the model doesn't support effort
|
|
37
|
-
const parts = []
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Context window
|
|
42
|
-
const ctx = data.context_window || {}
|
|
43
|
-
const total = ctx.context_window_size || 0
|
|
44
|
-
if (total) {
|
|
45
|
-
const usage = ctx.current_usage || {}
|
|
46
|
-
const hit = usage.cache_read_input_tokens || 0
|
|
47
|
-
const used = hit + (usage.cache_creation_input_tokens || 0) + (usage.input_tokens || 0)
|
|
48
|
-
const pct = Math.round((used / total) * 100)
|
|
49
|
-
const cpct = used ? Math.floor((hit * 100) / used) : 0
|
|
50
|
-
const filled = Math.floor(pct / 10)
|
|
51
|
-
const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled)
|
|
52
|
-
const head = colorize('context', `${bar} ${pct}% used`, used)
|
|
53
|
-
const tokens = colorize('context', `${(used / 1000).toFixed(1)}k`, used)
|
|
54
|
-
const cached = used ? colorize('cache', `${cpct}% cached`, cpct) : `${cpct}% cached`
|
|
55
|
-
parts.push(`Context: ${head} (${tokens}, ${cached})`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Total tokens (per session and per reply loop)
|
|
61
|
-
function agentLoopUsage(transcriptPath) { // Read the transcript JSONL and sum API usage for the current agent loop and the whole session.
|
|
62
|
-
const lines = readFileSync(transcriptPath, 'utf-8').split(/\r?\n/).filter(Boolean)
|
|
63
|
-
let loop = { total: 0, turns: 0 } // last prompt-reply breakdown
|
|
64
|
-
const session = { total: 0, turns: 0 } // whole-session breakdown
|
|
65
|
-
let seen = new Set() // One API response spans multiple lines (one per content block), each repeating the usage block, so dedupe by message.id.
|
|
66
|
-
|
|
67
|
-
for (const line of lines) {
|
|
68
|
-
let e
|
|
69
|
-
try { e = JSON.parse(line) } catch { continue } // the transcript may be mid-append — skip a half-written line
|
|
70
|
-
const msg = e.message || {}
|
|
71
|
-
|
|
72
|
-
if (e.type === 'user' && !e.isMeta) {
|
|
73
|
-
const isToolReply = Array.isArray(msg.content) && msg.content.some(b => b.type === 'tool_result')
|
|
74
|
-
if (!isToolReply) { // real user prompt = new loop
|
|
75
|
-
loop = { total: 0, turns: 0 }
|
|
76
|
-
seen = new Set()
|
|
77
|
-
}
|
|
78
|
-
} else if (e.type === 'assistant' && !seen.has(msg.id)) {
|
|
79
|
-
seen.add(msg.id)
|
|
80
|
-
const u = msg.usage || {}
|
|
81
|
-
const tokens = (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0)
|
|
82
|
-
for (const d of [loop, session]) {
|
|
83
|
-
d.total += tokens
|
|
84
|
-
d.turns += 1
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return [loop, session]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (data.transcript_path) {
|
|
92
|
-
const [loop, session] = agentLoopUsage(data.transcript_path)
|
|
93
|
-
const fmt = n => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${(n / 1000).toFixed(1)}k`
|
|
94
|
-
const tokensS = colorize('tokens', fmt(loop.total), loop.total)
|
|
95
|
-
const sessS = colorize('session', fmt(session.total), session.total)
|
|
96
|
-
const turnsS = colorize('turns', `${loop.turns} turn${loop.turns !== 1 ? 's' : ''}`, loop.turns)
|
|
97
|
-
parts.push(`Tokens: ${sessS} total (+${tokensS}, ${turnsS})`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Rate limits (5-hour and weekly windows)
|
|
103
|
-
function fmtReset(resetsAt) { // Format remaining time until a unix-epoch reset, e.g. "2h 30m" (null if no timestamp).
|
|
104
|
-
if (!resetsAt) return null
|
|
105
|
-
const secs = Math.max(0, Math.floor(Number(resetsAt)) - Math.floor(Date.now() / 1000))
|
|
106
|
-
const min = Math.floor(secs / 60) % 60
|
|
107
|
-
const hrs = Math.floor(secs / 3600) % 24
|
|
108
|
-
const days = Math.floor(secs / 86400)
|
|
109
|
-
if (days) return `${days}d ${hrs}h`
|
|
110
|
-
if (hrs) return `${hrs}h ${min}m`
|
|
111
|
-
return `${min}m`
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const rl = data.rate_limits || {}
|
|
115
|
-
for (const [label, window] of [['Daily', rl.five_hour], ['Weekly', rl.seven_day]]) {
|
|
116
|
-
if (!window) continue
|
|
117
|
-
const pct = Math.floor(window.used_percentage || 0)
|
|
118
|
-
const reset = fmtReset(window.resets_at)
|
|
119
|
-
const resetStr = reset ? ` (resets in ${reset})` : ''
|
|
120
|
-
parts.push(`${label}: ${colorize('limit', `${pct}% used${resetStr}`, pct)}`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Status line
|
|
125
|
-
process.stdout.write(`[${modelName}${effort ? ` ${effort}` : ''}] ` + parts.join(' | ') + '\n')
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code status line, installed by StatuslineSwitch. Reads the hook JSON from stdin and
|
|
3
|
+
// prints one colored line — model and effort, context usage, token totals, rate-limit windows:
|
|
4
|
+
// [Opus 4.8 high] Context: ▓▓▓░░░░░░░ 32% used (45.2k, 87% cached) | Tokens: 1.2M total (+45.2k, 3 turns) | Daily: 42% used (resets in 2h 30m)
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { styleText } from 'node:util'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// Formatting
|
|
11
|
+
const THRESHOLDS = { // (white, yellow, red)
|
|
12
|
+
context: [0, 100_000, 250_000],
|
|
13
|
+
limit: [0, 60, 90], // keep in sync with src/renderer/utils/thresholds.js
|
|
14
|
+
cache: [90, 60, 0],
|
|
15
|
+
tokens: [0, 250_000, 1_000_000],
|
|
16
|
+
session: [0, 3_000_000, 8_000_000],
|
|
17
|
+
turns: [0, 10, 25],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const NO_COLOR = process.env.NO_COLOR !== undefined // standard opt-out; styleText can't honor it for piped output, so gate here
|
|
21
|
+
|
|
22
|
+
function colorize(kind, text, value = 0) {
|
|
23
|
+
const bounds = THRESHOLDS[kind]
|
|
24
|
+
const n = bounds.filter(b => value >= b).length
|
|
25
|
+
const TIERS = [null, 'yellow', 'red'] // indexed by severity tier
|
|
26
|
+
const idx = bounds[0] < bounds.at(-1) ? n - 1 : bounds.length - n
|
|
27
|
+
const color = TIERS[idx]
|
|
28
|
+
// validateStream:false forces codes through the pipe (statusline output isn't a TTY)
|
|
29
|
+
return color && !NO_COLOR ? styleText(color, text, { validateStream: false }) : text
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// Collect stdin data
|
|
34
|
+
const data = JSON.parse(readFileSync(0, 'utf-8')) // fd 0 = stdin
|
|
35
|
+
const modelName = data.model?.display_name?.replace('Claude ', '') || '?'
|
|
36
|
+
const effort = data.effort?.level // absent when the model doesn't support effort
|
|
37
|
+
const parts = []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// Context window
|
|
42
|
+
const ctx = data.context_window || {}
|
|
43
|
+
const total = ctx.context_window_size || 0
|
|
44
|
+
if (total) {
|
|
45
|
+
const usage = ctx.current_usage || {}
|
|
46
|
+
const hit = usage.cache_read_input_tokens || 0
|
|
47
|
+
const used = hit + (usage.cache_creation_input_tokens || 0) + (usage.input_tokens || 0)
|
|
48
|
+
const pct = Math.round((used / total) * 100)
|
|
49
|
+
const cpct = used ? Math.floor((hit * 100) / used) : 0
|
|
50
|
+
const filled = Math.floor(pct / 10)
|
|
51
|
+
const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled)
|
|
52
|
+
const head = colorize('context', `${bar} ${pct}% used`, used)
|
|
53
|
+
const tokens = colorize('context', `${(used / 1000).toFixed(1)}k`, used)
|
|
54
|
+
const cached = used ? colorize('cache', `${cpct}% cached`, cpct) : `${cpct}% cached`
|
|
55
|
+
parts.push(`Context: ${head} (${tokens}, ${cached})`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
// Total tokens (per session and per reply loop)
|
|
61
|
+
function agentLoopUsage(transcriptPath) { // Read the transcript JSONL and sum API usage for the current agent loop and the whole session.
|
|
62
|
+
const lines = readFileSync(transcriptPath, 'utf-8').split(/\r?\n/).filter(Boolean)
|
|
63
|
+
let loop = { total: 0, turns: 0 } // last prompt-reply breakdown
|
|
64
|
+
const session = { total: 0, turns: 0 } // whole-session breakdown
|
|
65
|
+
let seen = new Set() // One API response spans multiple lines (one per content block), each repeating the usage block, so dedupe by message.id.
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
let e
|
|
69
|
+
try { e = JSON.parse(line) } catch { continue } // the transcript may be mid-append — skip a half-written line
|
|
70
|
+
const msg = e.message || {}
|
|
71
|
+
|
|
72
|
+
if (e.type === 'user' && !e.isMeta) {
|
|
73
|
+
const isToolReply = Array.isArray(msg.content) && msg.content.some(b => b.type === 'tool_result')
|
|
74
|
+
if (!isToolReply) { // real user prompt = new loop
|
|
75
|
+
loop = { total: 0, turns: 0 }
|
|
76
|
+
seen = new Set()
|
|
77
|
+
}
|
|
78
|
+
} else if (e.type === 'assistant' && !seen.has(msg.id)) {
|
|
79
|
+
seen.add(msg.id)
|
|
80
|
+
const u = msg.usage || {}
|
|
81
|
+
const tokens = (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0)
|
|
82
|
+
for (const d of [loop, session]) {
|
|
83
|
+
d.total += tokens
|
|
84
|
+
d.turns += 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [loop, session]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (data.transcript_path) {
|
|
92
|
+
const [loop, session] = agentLoopUsage(data.transcript_path)
|
|
93
|
+
const fmt = n => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${(n / 1000).toFixed(1)}k`
|
|
94
|
+
const tokensS = colorize('tokens', fmt(loop.total), loop.total)
|
|
95
|
+
const sessS = colorize('session', fmt(session.total), session.total)
|
|
96
|
+
const turnsS = colorize('turns', `${loop.turns} turn${loop.turns !== 1 ? 's' : ''}`, loop.turns)
|
|
97
|
+
parts.push(`Tokens: ${sessS} total (+${tokensS}, ${turnsS})`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
// Rate limits (5-hour and weekly windows)
|
|
103
|
+
function fmtReset(resetsAt) { // Format remaining time until a unix-epoch reset, e.g. "2h 30m" (null if no timestamp).
|
|
104
|
+
if (!resetsAt) return null
|
|
105
|
+
const secs = Math.max(0, Math.floor(Number(resetsAt)) - Math.floor(Date.now() / 1000))
|
|
106
|
+
const min = Math.floor(secs / 60) % 60
|
|
107
|
+
const hrs = Math.floor(secs / 3600) % 24
|
|
108
|
+
const days = Math.floor(secs / 86400)
|
|
109
|
+
if (days) return `${days}d ${hrs}h`
|
|
110
|
+
if (hrs) return `${hrs}h ${min}m`
|
|
111
|
+
return `${min}m`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rl = data.rate_limits || {}
|
|
115
|
+
for (const [label, window] of [['Daily', rl.five_hour], ['Weekly', rl.seven_day]]) {
|
|
116
|
+
if (!window) continue
|
|
117
|
+
const pct = Math.floor(window.used_percentage || 0)
|
|
118
|
+
const reset = fmtReset(window.resets_at)
|
|
119
|
+
const resetStr = reset ? ` (resets in ${reset})` : ''
|
|
120
|
+
parts.push(`${label}: ${colorize('limit', `${pct}% used${resetStr}`, pct)}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
// Status line
|
|
125
|
+
process.stdout.write(`[${modelName}${effort ? ` ${effort}` : ''}] ` + parts.join(' | ') + '\n')
|
package/bin/claude-discover.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Electron binary at the package root, whose package.json main is out/main/main.js.
|
|
4
4
|
import { spawn } from 'node:child_process'
|
|
5
5
|
import { existsSync } from 'node:fs'
|
|
6
|
+
import { access, constants } from 'node:fs/promises'
|
|
6
7
|
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import electron from 'electron' // under plain node the electron package exports its binary path
|
|
8
8
|
|
|
9
9
|
const appDir = fileURLToPath(new URL('..', import.meta.url))
|
|
10
10
|
if (!existsSync(new URL('../out/main/main.js', import.meta.url))) {
|
|
@@ -12,5 +12,36 @@ if (!existsSync(new URL('../out/main/main.js', import.meta.url))) {
|
|
|
12
12
|
process.exit(1)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
if (process.platform === 'linux') { // @linux
|
|
17
|
+
// Importing electron downloads its binary on first run, which can't unzip into a root-owned `sudo npm i -g` install,
|
|
18
|
+
// so it's imported dynamically, only after this check (a static import is hoisted and would run first)
|
|
19
|
+
|
|
20
|
+
const electronDir = new URL('.', import.meta.resolve('electron'))
|
|
21
|
+
const isWritable = await access(electronDir, constants.W_OK).then(() => true, () => false)
|
|
22
|
+
if (!existsSync(new URL('path.txt', electronDir)) && !isWritable) {
|
|
23
|
+
console.error(`
|
|
24
|
+
Electron can't finish installing — its folder isn't writable by your user.
|
|
25
|
+
|
|
26
|
+
Global npm packages shouldn't need sudo. The recommended setup is Node from a version
|
|
27
|
+
manager like nvm, which keeps global npm packages in your home folder:
|
|
28
|
+
|
|
29
|
+
sudo npm uninstall -g claude-discover
|
|
30
|
+
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
|
|
31
|
+
|
|
32
|
+
Then open a new terminal and run:
|
|
33
|
+
|
|
34
|
+
nvm install --lts
|
|
35
|
+
npm install -g claude-discover
|
|
36
|
+
claude-discover
|
|
37
|
+
|
|
38
|
+
See https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally`)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
const { default: electron } = await import('electron') // under plain node the electron package exports its binary path
|
|
45
|
+
|
|
15
46
|
spawn(electron, [appDir, ...process.argv.slice(2)], { stdio: 'inherit' })
|
|
16
47
|
.on('close', code => process.exit(code ?? 0))
|
package/bin/update.mjs
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
// Updates an `npm i -g` install from outside it. AutoUpdate starts it detached as the app quits:
|
|
2
|
-
// Windows locks the app's running electron.exe, so npm can only replace the package once the app is gone.
|
|
3
|
-
|
|
4
|
-
import { spawn, spawnSync } from 'node:child_process'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
import { setTimeout as sleep } from 'node:timers/promises'
|
|
7
|
-
import { parseArgs } from 'node:util'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// ── 1. CLI, then wait for the app to exit ────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
const { values, positionals: [pid, version] } = parseArgs({
|
|
13
|
-
allowPositionals: true,
|
|
14
|
-
options: {
|
|
15
|
-
'no-reopen': { type: 'boolean' },
|
|
16
|
-
'help': { type: 'boolean', short: 'h' },
|
|
17
|
-
},
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
if (values.help || !version) {
|
|
21
|
-
console.error('Usage: node bin/update.mjs <pid> <version> [--no-reopen]')
|
|
22
|
-
process.exit(1)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
while (true) {
|
|
26
|
-
try {
|
|
27
|
-
process.kill(Number(pid), 0)
|
|
28
|
-
} catch (err) {
|
|
29
|
-
if (err.code !== 'EPERM') break // EPERM: alive, just not ours to signal
|
|
30
|
-
}
|
|
31
|
-
await sleep(200)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// ── 2. Update and reopen ─────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
const { status } = spawnSync(`npm i -g claude-discover@${version} --loglevel=http`, { shell: true, stdio: 'inherit' })
|
|
38
|
-
if (status !== 0) await sleep(5_000) // leaves npm's error readable before the window closes
|
|
39
|
-
|
|
40
|
-
// Reopen even after a failed update; dev passes --no-reopen, as its Vite renderer died with the app
|
|
41
|
-
const globalBin = path.join(spawnSync('npm root -g', { shell: true, encoding: 'utf8' }).stdout.trim(), 'claude-discover/bin/claude-discover.mjs')
|
|
42
|
-
if (!values['no-reopen']) spawn(process.execPath, [globalBin], { detached: true, stdio: 'ignore' }).unref()
|
|
1
|
+
// Updates an `npm i -g` install from outside it. AutoUpdate starts it detached as the app quits:
|
|
2
|
+
// Windows locks the app's running electron.exe, so npm can only replace the package once the app is gone.
|
|
3
|
+
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
7
|
+
import { parseArgs } from 'node:util'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// ── 1. CLI, then wait for the app to exit ────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const { values, positionals: [pid, version] } = parseArgs({
|
|
13
|
+
allowPositionals: true,
|
|
14
|
+
options: {
|
|
15
|
+
'no-reopen': { type: 'boolean' },
|
|
16
|
+
'help': { type: 'boolean', short: 'h' },
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
if (values.help || !version) {
|
|
21
|
+
console.error('Usage: node bin/update.mjs <pid> <version> [--no-reopen]')
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
while (true) {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(Number(pid), 0)
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err.code !== 'EPERM') break // EPERM: alive, just not ours to signal
|
|
30
|
+
}
|
|
31
|
+
await sleep(200)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// ── 2. Update and reopen ─────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const { status } = spawnSync(`npm i -g claude-discover@${version} --loglevel=http`, { shell: true, stdio: 'inherit' })
|
|
38
|
+
if (status !== 0) await sleep(5_000) // leaves npm's error readable before the window closes
|
|
39
|
+
|
|
40
|
+
// Reopen even after a failed update; dev passes --no-reopen, as its Vite renderer died with the app
|
|
41
|
+
const globalBin = path.join(spawnSync('npm root -g', { shell: true, encoding: 'utf8' }).stdout.trim(), 'claude-discover/bin/claude-discover.mjs')
|
|
42
|
+
if (!values['no-reopen']) spawn(process.execPath, [globalBin], { detached: true, stdio: 'ignore' }).unref()
|