/dev-stack-next <spec>: show the progress tree of a plan's stack

hooks/register.ts 43 lines1import type { Register } from 'claude-code'
2
3const STACK = '/.dotfiles/.ai-shared/bin/dev-stack.sh'
4const USAGE = 'Usage: /dev-stack-next <root spec file> [--md]'
5
6export const register: Register = on => {
7 on('session.start', async ($, e, next) => {
8 await $.command.register({
9 name: 'dev-stack-next',
10 description: 'Show the progress tree of a plan stack (dev-stack.sh next)',
11 argumentHint: '<root spec file> [--md]',
12 // Read-only, so it can show progress while a turn, such as run-stack, is still working.
13 immediate: true,
14 })
15 return next(e)
16 })
17
18 // dev-stack.sh runs in the session's directory, which must be in the plan's repository.
19 on('command.run', { command: 'dev-stack-next' }, async ($, e) => {
20 // Everything but the --md flag is the path, so a path with spaces still works.
21 const text = (e.args || '').trim()
22 const view = /(^|\s)--md$/.test(text) ? '--md' : '--tree'
23 const spec = text.replace(/(^|\s)--md$/, '').trim()
24 if (!spec) return { text: USAGE }
25
26 const home = await $.env.get('HOME')
27 if (!home) return { text: 'dev-stack-next: HOME is not set.' }
28 try {
29 const ran = await $.process.run(['bash', home + STACK, 'next', spec, view])
30 if (ran.exitCode === 0) return { text: ran.stdout.trimEnd() || '(no output)' }
31
32 const err = (ran.stderr || ran.stdout).trimEnd()
33 // dev-stack.sh names only the plan format or the missing repository; say what it needs from the caller.
34 const hint = /Plan must be|main working-tree root/.test(err)
35 ? "\nRun it from the plan's repository, with its root plan (lowest pr<N>)."
36 : ''
37 return { text: err + hint }
38 } catch (error) {
39 return { text: 'dev-stack.sh failed: ' + String((error as Error)?.message ?? error) }
40 }
41 })
42}
43