@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.
- package/docs/commands.md +2 -2
- package/docs/hooks.md +62 -6
- package/docs/index.md +7 -4
- package/docs/install-steps.md +3 -3
- package/docs/jobs.md +5 -4
- package/docs/mod.md +57 -7
- package/docs/testing.md +25 -9
- package/docs/ui.md +134 -5
- package/jobs/check.js +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +2 -0
- package/jobs/permissions/decide-permission.js +2 -2
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +9 -9
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions/match-target.js +4 -3
- package/mod.d.ts +16 -6
- package/mod.d.ts.map +1 -1
- package/package.json +2 -1
- package/path.d.ts +2 -0
- package/path.d.ts.map +1 -0
- package/path.js +1 -0
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +4 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/hooks.d.ts +10 -2
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +56 -18
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +12 -2
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +42 -26
- package/shell.d.ts +2 -0
- package/shell.d.ts.map +1 -0
- package/shell.js +1 -0
- package/src/jobs/check.ts +2 -2
- package/src/jobs/context.ts +2 -0
- package/src/jobs/permissions/decide-permission.ts +2 -2
- package/src/jobs/permissions/find-project-scope.ts +11 -10
- package/src/jobs/permissions/match-target.ts +4 -3
- package/src/mod.ts +24 -4
- package/src/path.ts +1 -0
- package/src/register.ts +8 -2
- package/src/runtime/claude.ts +11 -1
- package/src/runtime/hooks.ts +62 -23
- package/src/runtime/lifecycle.ts +11 -4
- package/src/runtime/ui.ts +39 -26
- package/src/shell.ts +1 -0
- package/src/testing/fake-claude.ts +14 -5
- package/src/testing/fake-elements.ts +5 -7
- package/src/testing/fake-files.ts +5 -5
- package/src/testing.ts +83 -11
- package/src/ui/components.ts +70 -0
- package/src/ui/define-pane.ts +5 -1
- package/src/utils/call-effects.ts +12 -11
- package/src/utils/parse-shell.ts +28 -36
- package/src/utils/paths.ts +4 -19
- package/src/vendor.ts +1 -0
- package/testing/fake-claude.d.ts +5 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +13 -3
- package/testing/fake-elements.d.ts +5 -8
- package/testing/fake-elements.d.ts.map +1 -1
- package/testing/fake-elements.js +6 -6
- package/testing/fake-files.js +5 -5
- package/testing.d.ts +4 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +65 -9
- package/ui/components.d.ts +48 -0
- package/ui/components.d.ts.map +1 -0
- package/ui/components.js +56 -0
- package/ui/define-pane.d.ts +5 -1
- package/ui/define-pane.d.ts.map +1 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +12 -11
- package/utils/parse-shell.d.ts +5 -9
- package/utils/parse-shell.d.ts.map +1 -1
- package/utils/parse-shell.js +5 -10
- package/utils/paths.d.ts +0 -3
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +3 -18
- package/vendor.d.ts +1 -0
- package/vendor.d.ts.map +1 -1
- 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
|
+
}
|
package/src/ui/define-pane.ts
CHANGED
|
@@ -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 {
|
|
2
|
-
import { expandHome,
|
|
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
|
|
25
|
-
const access = (path: string): FileAccess => ({ path:
|
|
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 =
|
|
30
|
-
const commands = parsed.commands.map(({ argv, folder }) => ({ argv, 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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
53
|
-
effects.reads = [{ path:
|
|
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') {
|
package/src/utils/parse-shell.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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: [
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
536
|
+
isDynamic: /[$\u0001]/.test(basename(raw)),
|
|
545
537
|
}
|
|
546
538
|
}
|
|
547
539
|
|
|
548
|
-
function finish(command: Command, operator: string, folder: string, result:
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(/\/+$/, '')}/${
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
package/src/utils/paths.ts
CHANGED
|
@@ -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(
|
|
39
|
-
return folder === undefined ? undefined :
|
|
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'
|
package/testing/fake-claude.d.ts
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
|
package/testing/fake-claude.js
CHANGED
|
@@ -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:
|
|
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: {
|
|
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
|
|
3
|
-
readonly type:
|
|
4
|
-
readonly props:
|
|
5
|
-
|
|
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
|
|
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;
|
|
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"}
|
package/testing/fake-elements.js
CHANGED
|
@@ -23,14 +23,14 @@ export const elements = {
|
|
|
23
23
|
Select: element('Select'),
|
|
24
24
|
Image: element('Image'),
|
|
25
25
|
};
|
|
26
|
-
export function
|
|
26
|
+
export function findElement(node, type, key) {
|
|
27
27
|
if (typeof node === 'string')
|
|
28
28
|
return undefined;
|
|
29
|
-
const
|
|
30
|
-
if (type ===
|
|
31
|
-
return
|
|
32
|
-
for (const child of children) {
|
|
33
|
-
const found =
|
|
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
|
}
|
package/testing/fake-files.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from '../vendor.js';
|
|
2
2
|
export function fakeFiles(files, links = {}) {
|
|
3
|
-
const contents = new Map(Object.entries(files).map(([path, text]) => [
|
|
3
|
+
const contents = new Map(Object.entries(files).map(([path, text]) => [resolve('/', path), text]));
|
|
4
4
|
const realPathOf = (path) => {
|
|
5
|
-
const absolute =
|
|
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 =
|
|
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,
|
|
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":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["src/testing.ts"],"names":[],"mappings":"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"}
|