@cmodjs/core 0.1.8 → 0.1.10

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 (86) hide show
  1. package/docs/commands.md +2 -2
  2. package/docs/hooks.md +62 -6
  3. package/docs/index.md +7 -4
  4. package/docs/install-steps.md +3 -3
  5. package/docs/jobs.md +5 -4
  6. package/docs/mod.md +57 -7
  7. package/docs/testing.md +25 -9
  8. package/docs/ui.md +134 -5
  9. package/jobs/check.js +2 -2
  10. package/jobs/context.d.ts.map +1 -1
  11. package/jobs/context.js +2 -0
  12. package/jobs/permissions/decide-permission.js +2 -2
  13. package/jobs/permissions/find-project-scope.d.ts +1 -1
  14. package/jobs/permissions/find-project-scope.d.ts.map +1 -1
  15. package/jobs/permissions/find-project-scope.js +9 -9
  16. package/jobs/permissions/match-target.d.ts.map +1 -1
  17. package/jobs/permissions/match-target.js +4 -3
  18. package/mod.d.ts +16 -6
  19. package/mod.d.ts.map +1 -1
  20. package/package.json +2 -1
  21. package/path.d.ts +2 -0
  22. package/path.d.ts.map +1 -0
  23. package/path.js +1 -0
  24. package/register.d.ts.map +1 -1
  25. package/register.js +8 -2
  26. package/runtime/claude.d.ts +4 -1
  27. package/runtime/claude.d.ts.map +1 -1
  28. package/runtime/hooks.d.ts +10 -2
  29. package/runtime/hooks.d.ts.map +1 -1
  30. package/runtime/hooks.js +56 -18
  31. package/runtime/lifecycle.d.ts.map +1 -1
  32. package/runtime/lifecycle.js +12 -2
  33. package/runtime/ui.d.ts.map +1 -1
  34. package/runtime/ui.js +42 -26
  35. package/shell.d.ts +2 -0
  36. package/shell.d.ts.map +1 -0
  37. package/shell.js +1 -0
  38. package/src/jobs/check.ts +2 -2
  39. package/src/jobs/context.ts +2 -0
  40. package/src/jobs/permissions/decide-permission.ts +2 -2
  41. package/src/jobs/permissions/find-project-scope.ts +11 -10
  42. package/src/jobs/permissions/match-target.ts +4 -3
  43. package/src/mod.ts +24 -4
  44. package/src/path.ts +1 -0
  45. package/src/register.ts +8 -2
  46. package/src/runtime/claude.ts +11 -1
  47. package/src/runtime/hooks.ts +62 -23
  48. package/src/runtime/lifecycle.ts +11 -4
  49. package/src/runtime/ui.ts +39 -26
  50. package/src/shell.ts +1 -0
  51. package/src/testing/fake-claude.ts +14 -5
  52. package/src/testing/fake-elements.ts +5 -7
  53. package/src/testing/fake-files.ts +5 -5
  54. package/src/testing.ts +83 -11
  55. package/src/ui/components.ts +70 -0
  56. package/src/ui/define-pane.ts +5 -1
  57. package/src/utils/call-effects.ts +12 -11
  58. package/src/utils/parse-shell.ts +28 -36
  59. package/src/utils/paths.ts +4 -19
  60. package/src/vendor.ts +1 -0
  61. package/testing/fake-claude.d.ts +5 -0
  62. package/testing/fake-claude.d.ts.map +1 -1
  63. package/testing/fake-claude.js +13 -3
  64. package/testing/fake-elements.d.ts +5 -8
  65. package/testing/fake-elements.d.ts.map +1 -1
  66. package/testing/fake-elements.js +6 -6
  67. package/testing/fake-files.js +5 -5
  68. package/testing.d.ts +4 -2
  69. package/testing.d.ts.map +1 -1
  70. package/testing.js +65 -9
  71. package/ui/components.d.ts +48 -0
  72. package/ui/components.d.ts.map +1 -0
  73. package/ui/components.js +56 -0
  74. package/ui/define-pane.d.ts +5 -1
  75. package/ui/define-pane.d.ts.map +1 -1
  76. package/utils/call-effects.d.ts.map +1 -1
  77. package/utils/call-effects.js +12 -11
  78. package/utils/parse-shell.d.ts +5 -9
  79. package/utils/parse-shell.d.ts.map +1 -1
  80. package/utils/parse-shell.js +5 -10
  81. package/utils/paths.d.ts +0 -3
  82. package/utils/paths.d.ts.map +1 -1
  83. package/utils/paths.js +3 -18
  84. package/vendor.d.ts +1 -0
  85. package/vendor.d.ts.map +1 -1
  86. package/vendor.js +209 -12
@@ -0,0 +1,70 @@
1
+ import type { RenderChildren, RenderElement } from 'claude-code'
2
+ import { Box, Button, Text } from './elements.js'
3
+
4
+ export type Tab = { readonly key: string; readonly label: string }
5
+
6
+ export function Tabs({ tabs, selected, onSelect, children }: { readonly tabs: readonly Tab[]; readonly selected: string; readonly onSelect: (key: string) => void; readonly children?: RenderChildren }): RenderElement {
7
+ const row = tabs.map((tab, index) => {
8
+ const hotkey = index < 9 ? `${index + 1}` : undefined
9
+ return tab.key === selected
10
+ ? Text({ children: [...(hotkey === undefined ? [] : [Text({ color: 'claude', children: hotkey }), ': ']), Text({ bold: true, underline: true, children: tab.label })] })
11
+ : Button({ plain: true, dimColor: true, key: `tab:${tab.key}`, ...(hotkey === undefined ? {} : { hotkey }), label: tab.label, onPress: () => onSelect(tab.key) })
12
+ })
13
+ return Box({ flexDirection: 'column', gap: 1, children: [Box({ gap: 3, children: row }), children] })
14
+ }
15
+
16
+ export function Split({ children }: { readonly children: readonly RenderElement[] }): RenderElement {
17
+ const width = `${Math.floor(100 / Math.max(children.length, 1))}%`
18
+ return Box({ gap: 1, children: children.map((child) => Box({ width, children: child })) })
19
+ }
20
+
21
+ export function Panel({ title, children }: { readonly title: string; readonly children?: RenderChildren }): RenderElement {
22
+ return Box({ flexDirection: 'column', flexGrow: 1, borderStyle: 'round', borderColor: 'subtle', paddingX: 1, children: [Text({ bold: true, children: title }), children] })
23
+ }
24
+
25
+ export function Toggle({ label, checked, onChange }: { readonly label: string; readonly checked: boolean; readonly onChange: (checked: boolean) => void }): RenderElement {
26
+ return Button({ plain: true, key: `toggle:${label}`, label: `${checked ? '[x]' : '[ ]'} ${label}`, onPress: () => onChange(!checked) })
27
+ }
28
+
29
+ export function Tooltip({ text, children }: { readonly text: string; readonly children?: RenderChildren }): RenderElement {
30
+ return Box({ key: `tooltip:${text}`, children: [children, card(text, false)] })
31
+ }
32
+
33
+ export function Help({ text, isOpen, onOpenChange }: { readonly text: string; readonly isOpen: boolean; readonly onOpenChange: (isOpen: boolean) => void }): RenderElement {
34
+ return Box({ key: `help:${text}`, children: [Button({ plain: true, key: `help-button:${text}`, label: '?', onPress: () => onOpenChange(!isOpen) }), card(text, isOpen)] })
35
+ }
36
+
37
+ export function Dialog({ title, children, actions }: { readonly title: string; readonly children?: RenderChildren; readonly actions: RenderChildren }): RenderElement {
38
+ return Box({ flexDirection: 'column', gap: 1, paddingX: 1, children: [Text({ bold: true, children: title }), children, Box({ gap: 2, children: actions })] })
39
+ }
40
+
41
+ export function Pagination({ page, pages, onPage }: { readonly page: number; readonly pages: number; readonly onPage: (page: number) => void }): RenderElement {
42
+ return Box({
43
+ gap: 2,
44
+ children: [
45
+ page > 1 ? Button({ plain: true, hotkey: 'p', label: 'Previous', onPress: () => onPage(page - 1) }) : Text({ dimColor: true, children: 'p: Previous' }),
46
+ Text({ dimColor: true, children: `${page} of ${pages}` }),
47
+ page < pages ? Button({ plain: true, hotkey: 'n', label: 'Next', onPress: () => onPage(page + 1) }) : Text({ dimColor: true, children: 'n: Next' }),
48
+ ],
49
+ })
50
+ }
51
+
52
+ export function ProgressBar({ done, total, width = 24 }: { readonly done: number; readonly total: number; readonly width?: number }): RenderElement {
53
+ const filled = total > 0 ? Math.round(Math.min(done / total, 1) * width) : 0
54
+ return Text({ children: [Text({ color: 'claude', children: '█'.repeat(filled) }), Text({ color: 'subtle', children: '░'.repeat(width - filled) }), ' ', Text({ dimColor: true, children: `${done}/${total}` })] })
55
+ }
56
+
57
+ const cardColumns = 36
58
+
59
+ function card(text: string, isShown: boolean): RenderElement {
60
+ return Box({
61
+ position: 'absolute',
62
+ bottom: 1,
63
+ left: 0,
64
+ width: Math.min(text.length + 2, cardColumns),
65
+ ...(isShown ? {} : { display: 'none' as const, hover: { display: 'flex' as const } }),
66
+ backgroundColor: 'text',
67
+ paddingX: 1,
68
+ children: Text({ color: 'inverseText', wrap: 'wrap', children: text }),
69
+ })
70
+ }
@@ -1,4 +1,4 @@
1
- import type { Frozen, RenderElement, RenderPropsOf } from 'claude-code'
1
+ import type { Frozen, PaneCloseInput, RenderElement, RenderPropsOf, UiScrollInput } from 'claude-code'
2
2
  import type { Mod } from '../mod.js'
3
3
 
4
4
  export type Pane<State extends object = Record<never, never>> = {
@@ -6,7 +6,11 @@ export type Pane<State extends object = Record<never, never>> = {
6
6
  readonly title: string
7
7
  readonly columns?: number | ((state: State) => number | undefined)
8
8
  readonly rows?: number | ((state: State) => number | undefined)
9
+ readonly closeOnEscape?: true
10
+ readonly holdToasts?: true
9
11
  render(mod: Mod<State>, props: Frozen<RenderPropsOf['Pane']>): RenderElement
12
+ onScroll?(mod: Mod<State>, e: Frozen<UiScrollInput>): void | Promise<void>
13
+ onClose?(mod: Mod<State>, e: Frozen<PaneCloseInput>): void | Promise<void>
10
14
  }
11
15
 
12
16
  const paneId = /^[A-Za-z0-9_-]{1,64}$/
@@ -1,5 +1,6 @@
1
- import { parseShellCommands, type ShellCommand } from './parse-shell.js'
2
- import { expandHome, resolvePath, type FileSystem } from './paths.js'
1
+ import { parseShell, type ShellCommand } from './parse-shell.js'
2
+ import { expandHome, type FileSystem } from './paths.js'
3
+ import { resolve } from '../vendor.js'
3
4
 
4
5
  export type ToolUse = { tool: string; input: unknown; agentId?: string; agentType?: string }
5
6
 
@@ -21,13 +22,13 @@ export type CallEffects = {
21
22
 
22
23
  export function callEffects(use: ToolUse, workspace: { cwd: string; home: string; fs: FileSystem }): CallEffects {
23
24
  const { fs } = workspace
24
- const resolve = (path: string, folder = workspace.cwd) => resolvePath(expandHome(path, workspace.home), folder)
25
- const access = (path: string): FileAccess => ({ path: resolve(path), contents: undefined })
25
+ const absolute = (path: string, folder = workspace.cwd) => resolve(folder, expandHome(path, workspace.home))
26
+ const access = (path: string): FileAccess => ({ path: absolute(path), contents: undefined })
26
27
  const effects: CallEffects = { shell: undefined, reads: [], writes: [], urls: [], subagent: undefined }
27
28
  if (use.tool === 'Bash') {
28
29
  const line = textField(use, 'command')
29
- const parsed = parseShellCommands(line)
30
- const commands = parsed.commands.map(({ argv, folder }) => ({ argv, folder: resolve(folder) }))
30
+ const parsed = parseShell(line)
31
+ const commands = parsed.commands.map(({ argv, folder }) => ({ argv, folder: absolute(folder) }))
31
32
  effects.shell = { line, commands, isFullyParsed: parsed.isFullyParsed }
32
33
  effects.reads = parsed.reads.map(access)
33
34
  effects.writes = parsed.writes.map(access)
@@ -35,22 +36,22 @@ export function callEffects(use: ToolUse, workspace: { cwd: string; home: string
35
36
  } else if (use.tool === 'PowerShell') {
36
37
  effects.shell = { line: textField(use, 'command'), commands: [], isFullyParsed: false }
37
38
  } else if (use.tool === 'Edit') {
38
- const path = resolve(textField(use, 'file_path'))
39
+ const path = absolute(textField(use, 'file_path'))
39
40
  effects.writes = [{ path, contents: once(() => editedContents(use, path, fs)) }]
40
41
  } else if (use.tool === 'Write') {
41
- const path = resolve(textField(use, 'file_path'))
42
+ const path = absolute(textField(use, 'file_path'))
42
43
  const content = textField(use, 'content')
43
44
  effects.writes = [{ path, contents: once(async () => contentsOf(content, await previousContentOf(path, fs))) }]
44
45
  } else if (use.tool === 'NotebookEdit') {
45
- const path = resolve(textField(use, 'notebook_path'))
46
+ const path = absolute(textField(use, 'notebook_path'))
46
47
  effects.writes = [{ path, contents: once(async () => contentsOf(undefined, await previousContentOf(path, fs))) }]
47
48
  } else if (use.tool === 'Read') {
48
49
  effects.reads = [access(textField(use, 'file_path'))]
49
50
  } else if (use.tool === 'Grep') {
50
51
  effects.reads = [access(optionalTextField(use, 'path') ?? '')]
51
52
  } else if (use.tool === 'Glob') {
52
- const folder = resolve(optionalTextField(use, 'path') ?? '')
53
- effects.reads = [{ path: resolve(textField(use, 'pattern'), folder), contents: undefined }]
53
+ const folder = absolute(optionalTextField(use, 'path') ?? '')
54
+ effects.reads = [{ path: absolute(textField(use, 'pattern'), folder), contents: undefined }]
54
55
  } else if (use.tool === 'WebFetch') {
55
56
  effects.urls = [textField(use, 'url')]
56
57
  } else if (use.tool === 'Agent') {
@@ -1,18 +1,15 @@
1
- import { nameOf } from './paths.js'
2
- import { parse, type GlobPattern, type ParseEntry } from '../vendor.js'
1
+ import { basename, parse, type GlobPattern, type ParseEntry } from '../vendor.js'
2
+
3
+ export type ShellCommand = { argv: [string, ...string[]]; folder: string }
3
4
 
4
5
  export type ParsedShell = {
5
- commands: [string, ...string[]][]
6
+ commands: ShellCommand[]
6
7
  writes: string[]
7
8
  reads: string[]
8
9
  fetches: string[]
9
10
  isFullyParsed: boolean
10
11
  }
11
12
 
12
- export type ShellCommand = { argv: [string, ...string[]]; folder: string }
13
-
14
- export type ParsedShellCommands = Omit<ParsedShell, 'commands'> & { commands: ShellCommand[] }
15
-
16
13
  type Word = { text: string; isGlob: boolean; isDynamic: boolean }
17
14
  type Marker = { raw: string; code: string[] }
18
15
  type Expansion = Marker & { end: number }
@@ -200,17 +197,12 @@ const inlineCodeFlags = new Map<string, readonly string[]>([
200
197
  ])
201
198
 
202
199
  export function parseShell(line: string): ParsedShell {
203
- const parsed = parseShellCommands(line)
204
- return { ...parsed, commands: parsed.commands.map((command) => command.argv) }
205
- }
206
-
207
- export function parseShellCommands(line: string): ParsedShellCommands {
208
- const result: ParsedShellCommands = { commands: [], writes: [], reads: [], fetches: [], isFullyParsed: true }
200
+ const result: ParsedShell = { commands: [], writes: [], reads: [], fetches: [], isFullyParsed: true }
209
201
  parseLine(line, '', result)
210
202
  return result
211
203
  }
212
204
 
213
- function parseLine(line: string, folder: string, result: ParsedShellCommands): void {
205
+ function parseLine(line: string, folder: string, result: ParsedShell): void {
214
206
  if (line.includes('\u0001')) {
215
207
  result.isFullyParsed = false
216
208
  return
@@ -475,7 +467,7 @@ function readHeredocBodies(source: string, from: number, heredocs: Heredoc[]): n
475
467
  return index
476
468
  }
477
469
 
478
- function walk(tokens: ParseEntry[], markers: Marker[], start: string, result: ParsedShellCommands): void {
470
+ function walk(tokens: ParseEntry[], markers: Marker[], start: string, result: ParsedShell): void {
479
471
  let folder = start
480
472
  const subshells: string[] = []
481
473
  let command = newCommand(false)
@@ -533,7 +525,7 @@ function isWordToken(token: ParseEntry): token is string | GlobPattern {
533
525
  return typeof token === 'string' || ('op' in token && token.op === 'glob')
534
526
  }
535
527
 
536
- function wordOf(token: string | GlobPattern, markers: Marker[], folder: string, result: ParsedShellCommands): Word {
528
+ function wordOf(token: string | GlobPattern, markers: Marker[], folder: string, result: ParsedShell): Word {
537
529
  const raw = typeof token === 'string' ? token : token.pattern
538
530
  for (const match of raw.matchAll(markerPattern)) {
539
531
  for (const code of markers[Number(match[1])]!.code) parseLine(code, folder, result)
@@ -541,11 +533,11 @@ function wordOf(token: string | GlobPattern, markers: Marker[], folder: string,
541
533
  return {
542
534
  text: raw.replace(markerPattern, (_, index: string) => markers[Number(index)]!.raw),
543
535
  isGlob: typeof token !== 'string',
544
- isDynamic: /[$\u0001]/.test(nameOf(raw)),
536
+ isDynamic: /[$\u0001]/.test(basename(raw)),
545
537
  }
546
538
  }
547
539
 
548
- function finish(command: Command, operator: string, folder: string, result: ParsedShellCommands): string {
540
+ function finish(command: Command, operator: string, folder: string, result: ParsedShell): string {
549
541
  for (const redirect of command.redirects) addRedirect(redirect, folder, result)
550
542
  const words = withoutPrefixes(command.words)
551
543
  const [first, ...args] = words
@@ -569,14 +561,14 @@ function withoutPrefixes(words: Word[]): Word[] {
569
561
  return words.slice(start)
570
562
  }
571
563
 
572
- function addRedirect({ operator, target }: Redirect, folder: string, result: ParsedShellCommands): void {
564
+ function addRedirect({ operator, target }: Redirect, folder: string, result: ParsedShell): void {
573
565
  if (operator === '<' || operator === '<>') addPath(result, 'reads', target, folder)
574
566
  if (writeRedirects.has(operator) || (operator === '>&' && !/^([0-9]+|-)$/.test(target.text))) addPath(result, 'writes', target, folder)
575
567
  }
576
568
 
577
- function addCommand(first: Word, args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShellCommands): string | undefined {
569
+ function addCommand(first: Word, args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShell): string | undefined {
578
570
  if (first.isDynamic || first.isGlob) result.isFullyParsed = false
579
- const program = nameOf(first.text)
571
+ const program = basename(first.text)
580
572
  if (program === 'git') addGit(first, args, folder, result)
581
573
  else if (program === 'find') addFind(first, args, folder, result)
582
574
  else if (wrappers.has(program)) addWrapper(first, program, args, folder, stdin, result)
@@ -589,7 +581,7 @@ function addCommand(first: Word, args: Word[], folder: string, stdin: Stdin | un
589
581
  return undefined
590
582
  }
591
583
 
592
- function addGit(first: Word, args: Word[], folder: string, result: ParsedShellCommands): void {
584
+ function addGit(first: Word, args: Word[], folder: string, result: ParsedShell): void {
593
585
  let index = 0
594
586
  let gitFolder = folder
595
587
  for (let text = args[0]?.text; text?.startsWith('-') === true; text = args[index]?.text) {
@@ -600,7 +592,7 @@ function addGit(first: Word, args: Word[], folder: string, result: ParsedShellCo
600
592
  result.commands.push({ argv: [first.text, ...expandFlags('git', args.slice(index))], folder: gitFolder })
601
593
  }
602
594
 
603
- function addFind(first: Word, args: Word[], folder: string, result: ParsedShellCommands): void {
595
+ function addFind(first: Word, args: Word[], folder: string, result: ParsedShell): void {
604
596
  const own: string[] = []
605
597
  const actions: Word[][] = []
606
598
  for (let index = 0; index < args.length; index += 1) {
@@ -628,7 +620,7 @@ function findStarts(args: Word[]): Word[] {
628
620
  return starts.length > 0 ? starts : [literal('.')]
629
621
  }
630
622
 
631
- function addWrapper(first: Word, program: string, args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShellCommands): void {
623
+ function addWrapper(first: Word, program: string, args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShell): void {
632
624
  const wrapper = wrappers.get(program)!
633
625
  if (program === 'xargs' && stdin?.kind !== 'text') result.isFullyParsed = false
634
626
  const { options, operands } = readArguments(args, wrapper, true)
@@ -651,7 +643,7 @@ function addWrapper(first: Word, program: string, args: Word[], folder: string,
651
643
  if (innerFirst !== undefined) addCommand(innerFirst, innerArgs, wrappedFolder, stdin, result)
652
644
  }
653
645
 
654
- function addEffects(program: string, args: Word[], flags: string[], folder: string, stdin: Stdin | undefined, result: ParsedShellCommands): void {
646
+ function addEffects(program: string, args: Word[], flags: string[], folder: string, stdin: Stdin | undefined, result: ParsedShell): void {
655
647
  const fileProgram = fileReaders.get(program)
656
648
  const fileWriter = fileWriters.get(program)
657
649
  const copyProgram = copyPrograms.get(program)
@@ -675,7 +667,7 @@ function addEffects(program: string, args: Word[], flags: string[], folder: stri
675
667
  }
676
668
  }
677
669
 
678
- function addShell(args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShellCommands): void {
670
+ function addShell(args: Word[], folder: string, stdin: Stdin | undefined, result: ParsedShell): void {
679
671
  const { options, operands } = readArguments(args, { valued: 'oO', longValued: ['--rcfile', '--init-file'] }, true)
680
672
  const script = operands[0]
681
673
  if (options.some((option) => option.name === '-c')) {
@@ -683,35 +675,35 @@ function addShell(args: Word[], folder: string, stdin: Stdin | undefined, result
683
675
  } else if (script === undefined || options.some((option) => option.name === '-s')) addStdinCode(stdin, folder, result)
684
676
  }
685
677
 
686
- function addSsh(args: Word[], stdin: Stdin | undefined, result: ParsedShellCommands): void {
678
+ function addSsh(args: Word[], stdin: Stdin | undefined, result: ParsedShell): void {
687
679
  const { operands } = readArguments(args, { valued: 'BbcDEeFIiJLlmOoPpQRSWw' }, true)
688
680
  const remote = operands.slice(1)
689
681
  if (remote.length > 0) parseLine(remote.map((word) => word.text).join(' '), '~', result)
690
682
  else if (operands.length > 0) addStdinCode(stdin, '~', result)
691
683
  }
692
684
 
693
- function addSu(args: Word[], folder: string, result: ParsedShellCommands): void {
685
+ function addSu(args: Word[], folder: string, result: ParsedShell): void {
694
686
  const code = readArguments(args, su, false).options.findLast((option) => suCodeOptions.includes(option.name))?.value
695
687
  if (code !== undefined) parseLine(code.text, folder, result)
696
688
  }
697
689
 
698
- function addFetches(fetcher: OptionSpec, args: Word[], result: ParsedShellCommands): void {
690
+ function addFetches(fetcher: OptionSpec, args: Word[], result: ParsedShell): void {
699
691
  const { options, operands } = readArguments(args, fetcher, false)
700
692
  const urls = [...options.filter((option) => option.name === '--url').flatMap((option) => (option.value === undefined ? [] : [option.value])), ...operands]
701
693
  for (const url of urls) result.fetches.push(/^[a-z][a-z0-9+.-]*:\/\//i.test(url.text) ? url.text : `http://${url.text}`)
702
694
  }
703
695
 
704
- function addStdinCode(stdin: Stdin | undefined, folder: string, result: ParsedShellCommands): void {
696
+ function addStdinCode(stdin: Stdin | undefined, folder: string, result: ParsedShell): void {
705
697
  if (stdin?.kind === 'text') parseLine(stdin.text, folder, result)
706
698
  if (stdin?.kind === 'pipe') result.isFullyParsed = false
707
699
  }
708
700
 
709
- function addCopy(program: CopyProgram, args: Word[], folder: string, result: ParsedShellCommands): void {
701
+ function addCopy(program: CopyProgram, args: Word[], folder: string, result: ParsedShell): void {
710
702
  const { options, operands } = readArguments(args, program, false)
711
703
  const target = options.findLast((option) => program.targetOptions?.includes(option.name) === true)?.value
712
704
  const only = operands[0]
713
705
  if (program.linksHere === true && target === undefined && operands.length === 1 && only !== undefined) {
714
- addPath(result, 'writes', { ...only, text: nameOf(only.text) }, folder)
706
+ addPath(result, 'writes', { ...only, text: basename(only.text) }, folder)
715
707
  return
716
708
  }
717
709
  const destination = target ?? operands.at(-1)
@@ -720,12 +712,12 @@ function addCopy(program: CopyProgram, args: Word[], folder: string, result: Par
720
712
  const isLocal = (word: Word) => program.skipsRemote !== true || !/^[^/]*:/.test(word.text)
721
713
  for (const source of sources.filter(isLocal)) {
722
714
  if (program.sources !== undefined) addPath(result, program.sources, source, folder)
723
- if (isLocal(destination) && !source.isGlob && !destination.isGlob) addPathText(result, 'writes', `${destination.text.replace(/\/+$/, '')}/${nameOf(source.text)}`, folder)
715
+ if (isLocal(destination) && !source.isGlob && !destination.isGlob) addPathText(result, 'writes', `${destination.text.replace(/\/+$/, '')}/${basename(source.text)}`, folder)
724
716
  }
725
717
  if (isLocal(destination)) addPath(result, 'writes', destination, folder)
726
718
  }
727
719
 
728
- function addFileArguments(program: FileProgram, args: Word[], folder: string, result: ParsedShellCommands): void {
720
+ function addFileArguments(program: FileProgram, args: Word[], folder: string, result: ParsedShell): void {
729
721
  const { options, operands } = readArguments(args, program, false)
730
722
  const inPlace = options.find((option) => program.inPlaceOptions?.includes(option.name) === true && (program !== awk || option.value?.text === 'inplace'))
731
723
  let files = operands
@@ -812,11 +804,11 @@ function changedFolder(args: Word[], folder: string): string {
812
804
  return joinFolder(folder, target.text === '-' ? '$OLDPWD' : target.text)
813
805
  }
814
806
 
815
- function addPath(result: ParsedShellCommands, access: Access, word: Word | undefined, folder: string): void {
807
+ function addPath(result: ParsedShell, access: Access, word: Word | undefined, folder: string): void {
816
808
  if (word !== undefined && !word.isGlob) addPathText(result, access, word.text, folder)
817
809
  }
818
810
 
819
- function addPathText(result: ParsedShellCommands, access: Access, path: string, folder: string): void {
811
+ function addPathText(result: ParsedShell, access: Access, path: string, folder: string): void {
820
812
  if (path === '' || path.startsWith('/dev/')) return
821
813
  const joined = joinFolder(folder, path)
822
814
  if (dynamicPattern.test(joined.replace(homePrefix, ''))) result.isFullyParsed = false
@@ -1,3 +1,5 @@
1
+ import { basename, dirname, resolve } from '../vendor.js'
2
+
1
3
  export type FileSystem = {
2
4
  read: (path: string) => Promise<string>
3
5
  stat: (path: string) => Promise<{ kind: 'file' | 'dir' | 'other'; realPath?: string }>
@@ -9,23 +11,6 @@ export function expandHome(path: string, home: string): string {
9
11
  return prefix === undefined ? path : `${home}${path.slice(prefix.length)}`
10
12
  }
11
13
 
12
- export function resolvePath(path: string, folder: string): string {
13
- const parts: string[] = []
14
- for (const part of (path.startsWith('/') ? path : `${folder}/${path}`).split('/')) {
15
- if (part === '..') parts.pop()
16
- else if (part !== '' && part !== '.') parts.push(part)
17
- }
18
- return `/${parts.join('/')}`
19
- }
20
-
21
- export function parentOf(path: string): string {
22
- return resolvePath('..', path)
23
- }
24
-
25
- export function nameOf(path: string): string {
26
- return path.slice(path.lastIndexOf('/') + 1)
27
- }
28
-
29
14
  export function relativePath(base: string, path: string): string | undefined {
30
15
  if (path === base) return ''
31
16
  const prefix = base === '/' ? '/' : `${base}/`
@@ -35,6 +20,6 @@ export function relativePath(base: string, path: string): string | undefined {
35
20
  export async function realPathOf(path: string, fs: FileSystem): Promise<string | undefined> {
36
21
  const stat = await fs.stat(path).catch(() => undefined)
37
22
  if (stat !== undefined || path === '/') return stat?.realPath
38
- const folder = await realPathOf(parentOf(path), fs)
39
- return folder === undefined ? undefined : resolvePath(nameOf(path), folder)
23
+ const folder = await realPathOf(dirname(path), fs)
24
+ return folder === undefined ? undefined : resolve(folder, basename(path))
40
25
  }
package/src/vendor.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { Validator } from '@cfworker/json-schema'
2
+ export { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve } from 'pathe'
2
3
  export { default as picomatch } from 'picomatch/posix'
3
4
  export { parse, quote, type GlobPattern, type ParseEntry } from 'shell-quote'
@@ -24,9 +24,14 @@ export type Fakes = {
24
24
  ui: {
25
25
  ask?: Claude['ui']['ask'];
26
26
  open?: Claude['ui']['open'];
27
+ scroll?: Claude['ui']['scroll'];
28
+ };
29
+ session: {
30
+ messages?: Claude['session']['messages'];
27
31
  };
28
32
  agent: {
29
33
  list?: Claude['agent']['list'];
34
+ spawn?: Claude['agent']['spawn'];
30
35
  };
31
36
  clock: {
32
37
  after?: Claude['clock']['after'];
@@ -1 +1 @@
1
- {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAGlD,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC/E,EAAE,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC7J,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IACzC,QAAQ,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC/C,EAAE,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC9D,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IACzC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC7E,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;CACxC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAA;IACzC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB,CAAA;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CA8H/F"}
1
+ {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAGlD,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC/E,EAAE,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC7J,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IACzC,QAAQ,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAC/C,EAAE,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;IAC/F,OAAO,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAA;KAAE,CAAA;IACrD,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC3E,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;IAC7E,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;CACxC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAA;IACzC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB,CAAA;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAsI/F"}
@@ -1,9 +1,10 @@
1
1
  import { elements } from './fake-elements.js';
2
2
  export function fakeClaude(plugin) {
3
3
  const calls = [];
4
- const fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, agent: {}, clock: {}, cmod: {} };
4
+ const fakes = { process: {}, fs: {}, http: {}, settings: {}, ui: {}, session: {}, agent: {}, clock: {}, cmod: {} };
5
5
  const shown = { toasts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [] };
6
6
  const unplacedPanes = new Set();
7
+ let focusedPane;
7
8
  const store = new Map();
8
9
  const faked = (call, answer) => {
9
10
  return (...args) => {
@@ -41,6 +42,8 @@ export function fakeClaude(plugin) {
41
42
  if (answer.isPlaced) {
42
43
  shown.openPanes.add(pane.id);
43
44
  unplacedPanes.delete(pane.id);
45
+ if (pane.focus === true)
46
+ focusedPane = pane.id;
44
47
  }
45
48
  else if (!shown.openPanes.has(pane.id)) {
46
49
  unplacedPanes.add(pane.id);
@@ -51,11 +54,14 @@ export function fakeClaude(plugin) {
51
54
  calls.push({ call: 'ui.close', args: [pane] });
52
55
  shown.openPanes.delete(pane.id);
53
56
  unplacedPanes.delete(pane.id);
57
+ if (focusedPane === pane.id)
58
+ focusedPane = undefined;
54
59
  },
55
60
  panes: async () => [
56
- ...[...shown.openPanes].map((id) => ({ id, title: id, isShown: true, isFocused: false, isPlaced: true })),
61
+ ...[...shown.openPanes].map((id) => ({ id, title: id, isShown: true, isFocused: id === focusedPane, isPlaced: true })),
57
62
  ...[...unplacedPanes].map((id) => ({ id, title: id, isShown: false, isFocused: false, isPlaced: false })),
58
63
  ],
64
+ scroll: rejected('ui.scroll', () => fakes.ui.scroll ?? (async () => ({}))),
59
65
  resolve: () => elements,
60
66
  },
61
67
  process: {
@@ -71,7 +77,10 @@ export function fakeClaude(plugin) {
71
77
  },
72
78
  http: { fetch: rejected('http.fetch', () => fakes.http.fetch) },
73
79
  settings: { read: rejected('settings.read', () => fakes.settings.read) },
74
- agent: { list: rejected('agent.list', () => fakes.agent.list) },
80
+ agent: {
81
+ list: rejected('agent.list', () => fakes.agent.list),
82
+ spawn: rejected('agent.spawn', () => fakes.agent.spawn),
83
+ },
75
84
  store: {
76
85
  get: async (key) => store.get(key),
77
86
  set: async (key, value) => {
@@ -94,6 +103,7 @@ export function fakeClaude(plugin) {
94
103
  model: async () => 'test-model',
95
104
  usage: async () => ({ context: { window: 200000 }, rateLimits: {}, cost: { usd: 0 } }),
96
105
  surfaces: async () => ['terminal'],
106
+ messages: rejected('session.messages', () => fakes.session.messages),
97
107
  },
98
108
  command: {
99
109
  register: async (command) => {
@@ -1,14 +1,11 @@
1
1
  import type { ElementTable, RenderNode } from 'claude-code';
2
- type Button = {
3
- readonly type: 'Button';
4
- readonly props: {
5
- readonly key: string;
6
- readonly label: string;
7
- readonly onPress: () => unknown;
8
- };
2
+ type Drawn = {
3
+ readonly type: string;
4
+ readonly props: Record<string, unknown>;
5
+ readonly children?: readonly RenderNode[];
9
6
  };
10
7
  export declare const elements: ElementTable;
11
- export declare function findButton(node: RenderNode, key: string): Button | undefined;
8
+ export declare function findElement(node: RenderNode, type: string, key: string): Drawn['props'] | undefined;
12
9
  export declare function rowsOf(node: RenderNode, width: number): string[];
13
10
  export declare function textOf(node: RenderNode): string;
14
11
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"fake-elements.d.ts","sourceRoot":"","sources":["../src/testing/fake-elements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAiB,UAAU,EAAE,MAAM,aAAa,CAAA;AAI1E,KAAK,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAA;KAAE,CAAA;CAAE,CAAA;AAiB5I,eAAO,MAAM,QAAQ,EAUL,YAAY,CAAA;AAE5B,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS5E;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMhE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiB/C"}
1
+ {"version":3,"file":"fake-elements.d.ts","sourceRoot":"","sources":["../src/testing/fake-elements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAiB,UAAU,EAAE,MAAM,aAAa,CAAA;AAE1E,KAAK,KAAK,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAAA;AAiB1H,eAAO,MAAM,QAAQ,EAUL,YAAY,CAAA;AAE5B,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,SAAS,CASnG;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMhE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiB/C"}
@@ -23,14 +23,14 @@ export const elements = {
23
23
  Select: element('Select'),
24
24
  Image: element('Image'),
25
25
  };
26
- export function findButton(node, key) {
26
+ export function findElement(node, type, key) {
27
27
  if (typeof node === 'string')
28
28
  return undefined;
29
- const { type, props, children = [] } = insideOf(node);
30
- if (type === 'Button' && (props['key'] === key || props['label'] === key))
31
- return node;
32
- for (const child of children) {
33
- const found = findButton(child, key);
29
+ const drawn = insideOf(node);
30
+ if (drawn.type === type && (drawn.props['key'] === key || (type === 'Button' && drawn.props['label'] === key)))
31
+ return drawn.props;
32
+ for (const child of drawn.children ?? []) {
33
+ const found = findElement(child, type, key);
34
34
  if (found !== undefined)
35
35
  return found;
36
36
  }
@@ -1,8 +1,8 @@
1
- import { resolvePath } from '../utils/paths.js';
1
+ import { resolve } from '../vendor.js';
2
2
  export function fakeFiles(files, links = {}) {
3
- const contents = new Map(Object.entries(files).map(([path, text]) => [resolvePath(path, '/'), text]));
3
+ const contents = new Map(Object.entries(files).map(([path, text]) => [resolve('/', path), text]));
4
4
  const realPathOf = (path) => {
5
- const absolute = resolvePath(path, '/');
5
+ const absolute = resolve('/', path);
6
6
  for (const [link, target] of Object.entries(links)) {
7
7
  if (absolute === link || absolute.startsWith(`${link}/`))
8
8
  return realPathOf(`${target}${absolute.slice(link.length)}`);
@@ -37,7 +37,7 @@ export function fakeFiles(files, links = {}) {
37
37
  throw new Error(`ENOENT: no such directory, list '${path}'`);
38
38
  const names = new Set(below(folder).map((file) => file.split('/')[0]));
39
39
  return [...names].map((name) => {
40
- const entry = resolvePath(name, folder);
40
+ const entry = resolve(folder, name);
41
41
  if (Object.hasOwn(links, entry))
42
42
  return { name, kind: 'other', size: 0, mtimeMs: 0, isLink: true };
43
43
  return { name, kind: contents.has(entry) ? 'file' : 'dir', size: sizeOf(entry), mtimeMs: 0, isLink: false };
@@ -47,7 +47,7 @@ export function fakeFiles(files, links = {}) {
47
47
  stat: async (path, options) => {
48
48
  const realPath = realPathOf(path);
49
49
  const kind = kindOf(realPath);
50
- const isLink = Object.hasOwn(links, resolvePath(path, '/'));
50
+ const isLink = Object.hasOwn(links, resolve('/', path));
51
51
  if (kind === undefined && isLink)
52
52
  return { kind: 'other', size: 0, mtimeMs: 0, isLink };
53
53
  if (kind === undefined)
package/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Args, ClassicHookInputs, CmodDependencies, EventResult, RenderComponent } from 'claude-code';
1
+ import type { Args, ClassicHookInputs, CmodDependencies, EventResult, RenderComponent, UiInputArgument } from 'claude-code';
2
2
  import type { Reply } from './jobs/slash-command.js';
3
3
  import type { ModDefinition, ModEvent } from './mod.js';
4
4
  import type { RoutedEvent } from './runtime/hooks.js';
@@ -30,7 +30,9 @@ export type TestedMod<State extends object> = {
30
30
  }>>(slot: S, props: Omit<SlotProps<S>, 'Default'>, requestId?: string): Promise<string[]>;
31
31
  type(line: string): Promise<Reply>;
32
32
  callTool(name: string, input: Record<string, unknown>): Promise<EventResult<'tool.call'>>;
33
- press(paneId: string, key: string): Promise<void>;
33
+ press(paneId: string, key: string, link?: string): Promise<void>;
34
+ input(paneId: string, key: string, text: string, kind?: UiInputArgument['kind']): Promise<void>;
35
+ select(paneId: string, key: string, value: string): Promise<void>;
34
36
  moveTo(projectRoot: string, cwd?: string): Promise<void>;
35
37
  readonly state: Readonly<State>;
36
38
  readonly calls: readonly TestCall[];
package/testing.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["src/testing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["src/testing.ts"],"names":[],"mappings":"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"}