bestviber 0.3.0 → 0.3.2
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/bestviber.mjs +3 -0
- package/package.json +1 -1
- package/src/commands.mjs +1 -1
- package/src/format.mjs +10 -1
- package/src/hooks.mjs +25 -2
- package/src/hooks.test.mjs +40 -0
package/bin/bestviber.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs'
|
|
|
3
3
|
import { parse } from '../src/args.mjs'
|
|
4
4
|
import { CliError } from '../src/client.mjs'
|
|
5
5
|
import { commands } from '../src/commands.mjs'
|
|
6
|
+
import { refreshHooksIfMoved } from '../src/hooks.mjs'
|
|
6
7
|
|
|
7
8
|
const pkg = JSON.parse(
|
|
8
9
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
@@ -49,6 +50,8 @@ async function main() {
|
|
|
49
50
|
const json = flags.json === true
|
|
50
51
|
delete flags.json
|
|
51
52
|
const result = await command.run({ flags, args, json })
|
|
53
|
+
// Keep the Claude Code hooks pointing at this copy after an upgrade.
|
|
54
|
+
if (name !== 'hook') await refreshHooksIfMoved()
|
|
52
55
|
if (result === undefined) return
|
|
53
56
|
console.log(
|
|
54
57
|
json && typeof result !== 'string'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bestviber",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Focus timer and focus analytics for you and your AI agents. Start, pause and finish sessions, and get daily, weekly and monthly stats from the terminal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"focus",
|
package/src/commands.mjs
CHANGED
|
@@ -502,7 +502,7 @@ export const commands = {
|
|
|
502
502
|
return rows
|
|
503
503
|
.map(
|
|
504
504
|
(s) =>
|
|
505
|
-
`${new Date(s.startedAt).toLocaleDateString()} ${clock(s.startedAt)} ${duration(s.durationMs).padStart(7)}${s.notes ? ` ${s.notes}` : ''}`,
|
|
505
|
+
`${new Date(s.startedAt).toLocaleDateString()} ${clock(s.startedAt)} ${duration(s.durationMs).padStart(7)}${s.project ? ` ${s.project}` : ''}${s.notes ? ` ${s.notes}` : ''}`,
|
|
506
506
|
)
|
|
507
507
|
.join('\n')
|
|
508
508
|
},
|
package/src/format.mjs
CHANGED
|
@@ -41,6 +41,10 @@ export function stats(stats) {
|
|
|
41
41
|
lines.push(`${title} (${range.tz})`)
|
|
42
42
|
lines.push('')
|
|
43
43
|
lines.push(` focused ${duration(stats.totalMs)}`)
|
|
44
|
+
if (stats.parallelMs >= 60_000)
|
|
45
|
+
lines.push(
|
|
46
|
+
` in parallel ${duration(stats.parallelMs)} (up to ${stats.maxParallel} sessions at once; ${duration(stats.sessionTimeMs)} of session time)`,
|
|
47
|
+
)
|
|
44
48
|
lines.push(` sessions ${stats.sessionCount}`)
|
|
45
49
|
if (stats.sessionCount) {
|
|
46
50
|
lines.push(` average ${duration(stats.averageSessionMs)}`)
|
|
@@ -85,7 +89,12 @@ export function stats(stats) {
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
if (stats.byProject?.length) {
|
|
88
|
-
lines.push(
|
|
92
|
+
lines.push(
|
|
93
|
+
'',
|
|
94
|
+
stats.parallelMs >= 60_000
|
|
95
|
+
? ' by project (each counts its own time, so parallel time is in each)'
|
|
96
|
+
: ' by project',
|
|
97
|
+
)
|
|
89
98
|
const width = Math.max(
|
|
90
99
|
16,
|
|
91
100
|
...stats.byProject.map((/** @type {any} */ p) => p.project.length),
|
package/src/hooks.mjs
CHANGED
|
@@ -236,12 +236,15 @@ function hookCommand() {
|
|
|
236
236
|
} catch {
|
|
237
237
|
// Not on PATH (for example run through pnpm dlx).
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
// Never fail at Claude Code: if the CLI moved (an upgrade outside PATH),
|
|
240
|
+
// the hook does nothing until any `bestviber` command repairs the path.
|
|
241
|
+
const run = onPath ? q(onPath) : `${q(process.execPath)} ${q(BIN)}`
|
|
242
|
+
return `${run} hook 2>/dev/null || true`
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
const isOurs = (/** @type {any} */ h) =>
|
|
243
246
|
typeof h?.command === 'string' &&
|
|
244
|
-
/bestviber(\.mjs)?"?\s+hook
|
|
247
|
+
/bestviber(\.mjs)?"?\s+hook\b/.test(h.command)
|
|
245
248
|
|
|
246
249
|
/** Remove our hook entries from every event, keeping everything else. */
|
|
247
250
|
function withoutOurs(/** @type {Record<string, any>} */ hooks) {
|
|
@@ -307,6 +310,26 @@ export async function installHooks() {
|
|
|
307
310
|
return { settings: settingsPath(), command }
|
|
308
311
|
}
|
|
309
312
|
|
|
313
|
+
/**
|
|
314
|
+
* If tracking is on but the hooks point at another copy of the CLI (it was
|
|
315
|
+
* upgraded or moved), point them at this one. Cheap; runs with every command.
|
|
316
|
+
*/
|
|
317
|
+
export async function refreshHooksIfMoved() {
|
|
318
|
+
try {
|
|
319
|
+
const hooks = (await readSettings()).hooks ?? {}
|
|
320
|
+
const ours = Object.values(hooks)
|
|
321
|
+
.flatMap((groups) => (Array.isArray(groups) ? groups : []))
|
|
322
|
+
.flatMap((g) => (Array.isArray(g?.hooks) ? g.hooks : []))
|
|
323
|
+
.filter(isOurs)
|
|
324
|
+
if (ours.length === 0) return
|
|
325
|
+
const command = hookCommand()
|
|
326
|
+
if (ours.every((h) => h.command === command)) return
|
|
327
|
+
await installHooks()
|
|
328
|
+
} catch {
|
|
329
|
+
// Best-effort: never break the command the user actually ran.
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
310
333
|
export async function uninstallHooks() {
|
|
311
334
|
if (!existsSync(settingsPath())) return false
|
|
312
335
|
const settings = await readSettings()
|
package/src/hooks.test.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
__test,
|
|
7
7
|
hooksInstalled,
|
|
8
8
|
installHooks,
|
|
9
|
+
refreshHooksIfMoved,
|
|
9
10
|
uninstallHooks,
|
|
10
11
|
} from './hooks.mjs'
|
|
11
12
|
|
|
@@ -91,6 +92,45 @@ describe('installing into ~/.claude/settings.json', () => {
|
|
|
91
92
|
expect(await hooksInstalled()).toBe(false)
|
|
92
93
|
})
|
|
93
94
|
|
|
95
|
+
test('hooks never fail at Claude Code, even if the CLI is gone', async () => {
|
|
96
|
+
await installHooks()
|
|
97
|
+
const s = await settings()
|
|
98
|
+
expect(s.hooks.Stop[0].hooks[0].command).toMatch(
|
|
99
|
+
/ hook 2>\/dev\/null \|\| true$/,
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('after an upgrade moves the CLI, any command repoints the hooks', async () => {
|
|
104
|
+
const stale =
|
|
105
|
+
'"/old/node" "/old/node_modules/bestviber/bin/bestviber.mjs" hook'
|
|
106
|
+
await writeFile(
|
|
107
|
+
join(dir, 'settings.json'),
|
|
108
|
+
JSON.stringify({
|
|
109
|
+
hooks: {
|
|
110
|
+
UserPromptSubmit: [{ hooks: [{ type: 'command', command: stale }] }],
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
await refreshHooksIfMoved()
|
|
115
|
+
const s = await settings()
|
|
116
|
+
const commands = JSON.stringify(s.hooks)
|
|
117
|
+
expect(commands).not.toContain('/old/node')
|
|
118
|
+
expect(s.hooks.UserPromptSubmit).toHaveLength(1)
|
|
119
|
+
expect(s.hooks.SessionEnd).toHaveLength(1)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('hooks written by older versions are still recognised on logout', async () => {
|
|
123
|
+
const old = '"/usr/local/bin/bestviber" hook'
|
|
124
|
+
await writeFile(
|
|
125
|
+
join(dir, 'settings.json'),
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
hooks: { Stop: [{ hooks: [{ type: 'command', command: old }] }] },
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
expect(await uninstallHooks()).toBe(true)
|
|
131
|
+
expect((await settings()).hooks).toBeUndefined()
|
|
132
|
+
})
|
|
133
|
+
|
|
94
134
|
test('a settings file that is not valid JSON is left alone', async () => {
|
|
95
135
|
await writeFile(join(dir, 'settings.json'), '{ not json')
|
|
96
136
|
await expect(installHooks()).rejects.toThrow(/Left it unchanged/)
|