@cmodjs/core 0.1.13 → 0.2.0

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 (95) hide show
  1. package/docs/commands.md +31 -19
  2. package/docs/index.md +14 -4
  3. package/docs/install-steps.md +30 -4
  4. package/docs/jobs.md +25 -9
  5. package/docs/mod.md +103 -37
  6. package/docs/options.md +93 -0
  7. package/docs/permissions.md +88 -0
  8. package/docs/state.md +4 -31
  9. package/docs/testing.md +13 -2
  10. package/installer.d.ts +10 -0
  11. package/installer.d.ts.map +1 -0
  12. package/installer.js +8 -0
  13. package/jobs/context.d.ts +2 -2
  14. package/jobs/context.d.ts.map +1 -1
  15. package/jobs/context.js +5 -3
  16. package/jobs/permissions.js +1 -1
  17. package/jobs/program.d.ts.map +1 -1
  18. package/jobs/program.js +5 -1
  19. package/jobs/prompt.js +5 -5
  20. package/jobs/schedule.d.ts.map +1 -1
  21. package/jobs/schedule.js +2 -1
  22. package/jobs/slash-command.d.ts.map +1 -1
  23. package/jobs/slash-command.js +3 -2
  24. package/jobs/status-line.d.ts.map +1 -1
  25. package/jobs/status-line.js +3 -2
  26. package/jobs/tool.d.ts.map +1 -1
  27. package/jobs/tool.js +3 -2
  28. package/mod.d.ts +48 -10
  29. package/mod.d.ts.map +1 -1
  30. package/mod.js +3 -0
  31. package/options.d.ts +58 -0
  32. package/options.d.ts.map +1 -0
  33. package/options.js +66 -0
  34. package/package.json +1 -1
  35. package/records.d.ts +14 -1
  36. package/records.d.ts.map +1 -1
  37. package/records.js +106 -7
  38. package/register.d.ts +4 -2
  39. package/register.d.ts.map +1 -1
  40. package/register.js +8 -2
  41. package/runtime/claude.d.ts +5 -1
  42. package/runtime/claude.d.ts.map +1 -1
  43. package/runtime/grants.d.ts +28 -0
  44. package/runtime/grants.d.ts.map +1 -0
  45. package/runtime/grants.js +286 -0
  46. package/runtime/installer.d.ts +23 -0
  47. package/runtime/installer.d.ts.map +1 -0
  48. package/runtime/installer.js +113 -0
  49. package/runtime/lifecycle.d.ts +6 -3
  50. package/runtime/lifecycle.d.ts.map +1 -1
  51. package/runtime/lifecycle.js +220 -36
  52. package/runtime/metadata.d.ts +20 -0
  53. package/runtime/metadata.d.ts.map +1 -0
  54. package/runtime/metadata.js +143 -0
  55. package/runtime/options.d.ts +21 -0
  56. package/runtime/options.d.ts.map +1 -0
  57. package/runtime/options.js +100 -0
  58. package/runtime/state.d.ts +1 -0
  59. package/runtime/state.d.ts.map +1 -1
  60. package/runtime/state.js +5 -75
  61. package/src/installer.ts +17 -0
  62. package/src/jobs/context.ts +5 -3
  63. package/src/jobs/permissions.ts +1 -1
  64. package/src/jobs/program.ts +4 -1
  65. package/src/jobs/prompt.ts +5 -5
  66. package/src/jobs/schedule.ts +2 -1
  67. package/src/jobs/slash-command.ts +3 -2
  68. package/src/jobs/status-line.ts +3 -2
  69. package/src/jobs/tool.ts +3 -2
  70. package/src/mod.ts +46 -11
  71. package/src/options.ts +91 -0
  72. package/src/records.ts +117 -8
  73. package/src/register.ts +12 -3
  74. package/src/runtime/claude.ts +2 -0
  75. package/src/runtime/grants.ts +306 -0
  76. package/src/runtime/installer.ts +163 -0
  77. package/src/runtime/lifecycle.ts +220 -45
  78. package/src/runtime/metadata.ts +153 -0
  79. package/src/runtime/options.ts +102 -0
  80. package/src/runtime/state.ts +7 -70
  81. package/src/testing/fake-claude.ts +6 -1
  82. package/src/testing/fake-files.ts +9 -3
  83. package/src/testing.ts +40 -15
  84. package/src/utils/metadata.ts +242 -0
  85. package/testing/fake-claude.d.ts +4 -0
  86. package/testing/fake-claude.d.ts.map +1 -1
  87. package/testing/fake-claude.js +5 -1
  88. package/testing/fake-files.d.ts.map +1 -1
  89. package/testing/fake-files.js +9 -3
  90. package/testing.d.ts +6 -2
  91. package/testing.d.ts.map +1 -1
  92. package/testing.js +29 -13
  93. package/utils/metadata.d.ts +21 -0
  94. package/utils/metadata.d.ts.map +1 -0
  95. package/utils/metadata.js +250 -0
@@ -0,0 +1,242 @@
1
+ export type MetadataFormat = { readonly kind: 'frontmatter' } | { readonly kind: 'comment'; readonly prefix: string } | { readonly kind: 'sidecar' }
2
+
3
+ export type ReadMetadata = {
4
+ readonly metadata: Readonly<Record<string, string>>
5
+ readonly name?: string
6
+ }
7
+
8
+ export class MetadataError extends Error {
9
+ readonly line: number
10
+
11
+ constructor(line: number, message: string) {
12
+ super(`line ${line}: ${message}`)
13
+ this.line = line
14
+ }
15
+ }
16
+
17
+ const markdown: readonly string[] = ['md', 'markdown', 'mdx']
18
+ const hashComments: readonly string[] = ['sh', 'bash', 'zsh', 'fish', 'py', 'rb', 'pl', 'r', 'yaml', 'yml', 'toml', 'ps1', 'nu', 'tcl', 'mk', 'conf', 'cfg', 'env']
19
+ const slashComments: readonly string[] = ['ts', 'tsx', 'mts', 'cts', 'js', 'jsx', 'mjs', 'cjs', 'go', 'rs', 'swift', 'kt', 'kts', 'java', 'c', 'h', 'cc', 'cpp', 'hpp', 'cs', 'scala', 'dart', 'zig', 'jsonc', 'json5']
20
+ const dashComments: readonly string[] = ['lua', 'sql', 'hs', 'elm']
21
+ const hashCommentNames: readonly string[] = ['Makefile', 'Dockerfile', 'Brewfile', 'Gemfile', 'Rakefile', 'Justfile', 'justfile']
22
+
23
+ export const sidecarSuffix = '.meta'
24
+
25
+ export function formatOf(path: string, firstLine: string | undefined): MetadataFormat {
26
+ const name = path.slice(path.lastIndexOf('/') + 1)
27
+ const dot = name.lastIndexOf('.')
28
+ const extension = dot <= 0 ? '' : name.slice(dot + 1).toLowerCase()
29
+ if (markdown.includes(extension)) return { kind: 'frontmatter' }
30
+ if (hashComments.includes(extension) || hashCommentNames.includes(name)) return { kind: 'comment', prefix: '#' }
31
+ if (slashComments.includes(extension)) return { kind: 'comment', prefix: '//' }
32
+ if (dashComments.includes(extension)) return { kind: 'comment', prefix: '--' }
33
+ if (extension === '' && firstLine?.startsWith('#!') === true) return { kind: 'comment', prefix: '#' }
34
+ return { kind: 'sidecar' }
35
+ }
36
+
37
+ type Region = { readonly start: number; readonly end: number; readonly lines: readonly string[] }
38
+
39
+ type Entry = { readonly key: string; readonly value: string }
40
+
41
+ type Block = { readonly start: number; readonly end: number; readonly entries: readonly Entry[] }
42
+
43
+ export function readMetadata(text: string, format: MetadataFormat): ReadMetadata {
44
+ const lines = text.split('\n')
45
+ if (format.kind === 'comment') {
46
+ const region = commentRegion(lines, format.prefix)
47
+ return { metadata: region === undefined ? {} : asRecord(mapEntries(region.lines, region.start + 2, 0)) }
48
+ }
49
+ const region = format.kind === 'frontmatter' ? frontmatterRegion(lines) : { start: -1, end: lines.length, lines }
50
+ if (region === undefined) return { metadata: {} }
51
+ const block = metadataBlock(region)
52
+ const name = format.kind === 'frontmatter' ? topLevelScalar(region, 'name') : undefined
53
+ return { metadata: block === undefined ? {} : asRecord(block.entries), ...(name === undefined ? {} : { name }) }
54
+ }
55
+
56
+ export function writeMetadata(text: string, format: MetadataFormat, metadata: Readonly<Record<string, string>>): string {
57
+ const lines = text.split('\n')
58
+ const entries = Object.entries(metadata)
59
+ if (format.kind === 'comment') {
60
+ const { prefix } = format
61
+ const block = entries.length === 0 ? [] : [`${prefix} /// metadata`, ...entries.map(([key, value]) => `${prefix} ${scalar(key)}: ${scalar(value)}`), `${prefix} ///`]
62
+ const region = commentRegion(lines, prefix)
63
+ if (region !== undefined) return [...lines.slice(0, region.start), ...block, ...lines.slice(region.end + 1)].join('\n')
64
+ if (block.length === 0) return text
65
+ const at = lines[0]?.startsWith('#!') === true ? 1 : 0
66
+ return [...lines.slice(0, at), ...block, ...lines.slice(at)].join('\n')
67
+ }
68
+ const map = entries.length === 0 ? [] : ['metadata:', ...entries.map(([key, value]) => ` ${scalar(key)}: ${scalar(value)}`)]
69
+ if (format.kind === 'sidecar') {
70
+ const block = metadataBlock({ start: -1, end: lines.length, lines })
71
+ const rest = block === undefined ? lines : [...lines.slice(0, block.start), ...lines.slice(block.end)]
72
+ const kept = rest.join('\n').trim()
73
+ return [...(kept === '' ? [] : [kept]), ...map].join('\n') + (map.length === 0 && kept === '' ? '' : '\n')
74
+ }
75
+ const region = frontmatterRegion(lines)
76
+ if (region === undefined) return map.length === 0 ? text : ['---', ...map, '---', ...lines].join('\n')
77
+ const block = metadataBlock(region)
78
+ const inner = block === undefined ? [...region.lines, ...map] : [...lines.slice(region.start + 1, block.start), ...map, ...lines.slice(block.end, region.end)]
79
+ if (inner.every((line) => line.trim() === '')) return lines.slice(region.end + 1).join('\n')
80
+ return [...lines.slice(0, region.start + 1), ...inner, ...lines.slice(region.end)].join('\n')
81
+ }
82
+
83
+ function frontmatterRegion(lines: readonly string[]): Region | undefined {
84
+ if (lines[0]?.replace(/^\uFEFF/, '').trimEnd() !== '---') return undefined
85
+ const end = lines.findIndex((line, index) => index > 0 && line.trimEnd() === '---')
86
+ if (end === -1) throw new MetadataError(1, 'the frontmatter starts with --- and never ends. Close it with a --- line.')
87
+ return { start: 0, end, lines: lines.slice(1, end) }
88
+ }
89
+
90
+ function commentRegion(lines: readonly string[], prefix: string): Region | undefined {
91
+ const start = lines.findIndex((line) => line.trimEnd() === `${prefix} /// metadata`)
92
+ if (start === -1) return undefined
93
+ const content: string[] = []
94
+ for (let index = start + 1; index < lines.length; index += 1) {
95
+ const line = (lines[index] ?? '').trimEnd()
96
+ if (line === `${prefix} ///`) return { start, end: index, lines: content }
97
+ if (line !== prefix && !line.startsWith(`${prefix} `)) break
98
+ content.push(line.slice(prefix.length + 1))
99
+ }
100
+ throw new MetadataError(start + 1, `the metadata block never ends. Close it with a "${prefix} ///" line.`)
101
+ }
102
+
103
+ function metadataBlock(region: Region): Block | undefined {
104
+ const at = region.lines.findIndex((line) => /^metadata\s*:/.test(line))
105
+ if (at === -1) return undefined
106
+ const lineNumber = region.start + 2 + at
107
+ const rest = withoutComment((region.lines[at] ?? '').replace(/^metadata\s*:/, '')).trim()
108
+ const start = region.start + 1 + at
109
+ if (rest.startsWith('{')) return { start, end: start + 1, entries: flowEntries(rest, lineNumber) }
110
+ if (rest !== '') throw new MetadataError(lineNumber, 'metadata holds a single value. Write a map of names to text, such as metadata: { mymod.key: value }.')
111
+ let index = at + 1
112
+ while (index < region.lines.length && (/^\s/.test(region.lines[index] ?? '') || (region.lines[index] ?? '').trim() === '')) index += 1
113
+ while (index > at + 1 && (region.lines[index - 1] ?? '').trim() === '') index -= 1
114
+ const children = region.lines.slice(at + 1, index)
115
+ return { start, end: region.start + 1 + index, entries: mapEntries(children, lineNumber + 1, indentOf(children)) }
116
+ }
117
+
118
+ function indentOf(lines: readonly string[]): number {
119
+ const first = lines.find((line) => line.trim() !== '' && !line.trim().startsWith('#'))
120
+ return first === undefined ? 0 : (/^\s*/.exec(first)?.[0].length ?? 0)
121
+ }
122
+
123
+ function mapEntries(lines: readonly string[], firstLine: number, indent: number): Entry[] {
124
+ const entries: Entry[] = []
125
+ lines.forEach((raw, index) => {
126
+ const lineNumber = firstLine + index
127
+ const line = withoutComment(raw)
128
+ if (line.trim() === '') return
129
+ const depth = /^\s*/.exec(line)?.[0].length ?? 0
130
+ if (depth !== indent) throw new MetadataError(lineNumber, 'a metadata value holds a list or a map. Each value is one line of text, and a list is one space-separated text.')
131
+ const body = line.trim()
132
+ if (body.startsWith('- ')) throw new MetadataError(lineNumber, 'metadata holds a list. Write a map of names to text, and a list as one space-separated text.')
133
+ entries.push(entryOf(body, lineNumber))
134
+ })
135
+ return entries
136
+ }
137
+
138
+ function flowEntries(text: string, lineNumber: number): Entry[] {
139
+ if (!text.endsWith('}')) throw new MetadataError(lineNumber, 'the metadata map starts with { and does not end on the same line. Close it with }, or write one entry per line.')
140
+ const body = text.slice(1, -1).trim()
141
+ if (body === '') return []
142
+ return splitOutsideQuotes(body, ',', lineNumber)
143
+ .map((part) => part.trim())
144
+ .filter((part) => part !== '')
145
+ .map((part) => entryOf(part, lineNumber))
146
+ }
147
+
148
+ function entryOf(text: string, lineNumber: number): Entry {
149
+ const [keyText, ...rest] = splitOutsideQuotes(text, ':', lineNumber, true)
150
+ if (keyText === undefined || rest.length === 0) throw new MetadataError(lineNumber, `"${text}" has no ":". Write each entry as name: value.`)
151
+ const valueText = rest.join(':').trim()
152
+ if (valueText.startsWith('{') || valueText.startsWith('[') || valueText === '|' || valueText === '>') throw new MetadataError(lineNumber, `${keyText.trim()} holds a list or a map. Each value is one line of text, and a list is one space-separated text.`)
153
+ return { key: unquoted(keyText.trim(), lineNumber), value: unquoted(valueText, lineNumber) }
154
+ }
155
+
156
+ function splitOutsideQuotes(text: string, separator: string, lineNumber: number, needsSpace = false): string[] {
157
+ const parts: string[] = []
158
+ let quote: string | undefined
159
+ let current = ''
160
+ for (let index = 0; index < text.length; index += 1) {
161
+ const char = text[index] as string
162
+ if (quote !== undefined) {
163
+ current += char
164
+ if (char === '\\' && quote === '"') {
165
+ current += text[index + 1] ?? ''
166
+ index += 1
167
+ } else if (char === quote) quote = undefined
168
+ continue
169
+ }
170
+ const before = current.trimEnd().at(-1)
171
+ if ((char === '"' || char === "'") && (before === undefined || before === ':')) quote = char
172
+ const next = text[index + 1]
173
+ if (char === separator && (!needsSpace || next === undefined || next === ' ' || next === '\t')) {
174
+ parts.push(current)
175
+ current = ''
176
+ if (needsSpace) {
177
+ parts.push(text.slice(index + 1))
178
+ return parts
179
+ }
180
+ continue
181
+ }
182
+ current += char
183
+ }
184
+ if (quote !== undefined) throw new MetadataError(lineNumber, `a ${quote === '"' ? 'double' : 'single'} quote never closes.`)
185
+ parts.push(current)
186
+ return parts
187
+ }
188
+
189
+ function withoutComment(line: string): string {
190
+ let quote: string | undefined
191
+ for (let index = 0; index < line.length; index += 1) {
192
+ const char = line[index]
193
+ if (quote !== undefined) {
194
+ if (char === '\\' && quote === '"') index += 1
195
+ else if (char === quote) quote = undefined
196
+ continue
197
+ }
198
+ if (char === '"' || char === "'") quote = char
199
+ if (char === '#' && (index === 0 || /\s/.test(line[index - 1] ?? ''))) return line.slice(0, index)
200
+ }
201
+ return line
202
+ }
203
+
204
+ function unquoted(text: string, lineNumber: number): string {
205
+ if (text.startsWith('"')) {
206
+ if (!text.endsWith('"') || text.length < 2) throw new MetadataError(lineNumber, `${text} opens a double quote that never closes.`)
207
+ try {
208
+ return JSON.parse(text) as string
209
+ } catch {
210
+ throw new MetadataError(lineNumber, `${text} is not a double-quoted text. Escape a " inside it as \\".`)
211
+ }
212
+ }
213
+ if (text.startsWith("'")) {
214
+ if (!text.endsWith("'") || text.length < 2) throw new MetadataError(lineNumber, `${text} opens a single quote that never closes.`)
215
+ return text.slice(1, -1).replaceAll("''", "'")
216
+ }
217
+ return text
218
+ }
219
+
220
+ function topLevelScalar(region: Region, key: string): string | undefined {
221
+ const at = region.lines.findIndex((line) => line.startsWith(`${key}:`))
222
+ if (at === -1) return undefined
223
+ const text = withoutComment((region.lines[at] ?? '').slice(key.length + 1)).trim()
224
+ if (text === '') return undefined
225
+ try {
226
+ return unquoted(text, region.start + 2 + at)
227
+ } catch {
228
+ return undefined
229
+ }
230
+ }
231
+
232
+ function asRecord(entries: readonly Entry[]): Readonly<Record<string, string>> {
233
+ return Object.freeze(Object.fromEntries(entries.map(({ key, value }) => [key, value])))
234
+ }
235
+
236
+ const plain = /^[A-Za-z0-9_./+~$%^()=\\][^#]*$/
237
+ const special = /^(true|false|yes|no|on|off|null|~|[-+]?(\d[\d_]*)?\.?\d+([eE][-+]?\d+)?|0x[0-9a-fA-F]+|\.inf|\.nan)$/i
238
+
239
+ function scalar(text: string): string {
240
+ const isPlain = plain.test(text) && text === text.trim() && !text.includes(': ') && !text.endsWith(':') && !text.includes(' #') && !special.test(text) && !/["',{}[\]]/.test(text)
241
+ return isPlain ? text : JSON.stringify(text)
242
+ }
@@ -21,6 +21,10 @@ export type Fakes = {
21
21
  settings: {
22
22
  read?: Claude['settings']['read'];
23
23
  };
24
+ config: {
25
+ list?: Claude['config']['list'];
26
+ set?: Claude['config']['set'];
27
+ };
24
28
  ui: {
25
29
  ask?: Claude['ui']['ask'];
26
30
  open?: Claude['ui']['open'];
@@ -1 +1 @@
1
- {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fake-claude.d.ts","sourceRoot":"","sources":["../src/testing/fake-claude.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
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: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {}, tool: {} };
4
+ const fakes = { process: {}, fs: {}, http: {}, settings: {}, config: {}, ui: {}, session: {}, prompt: {}, model: {}, agent: {}, clock: {}, cmod: {}, tool: {} };
5
5
  const shown = { toasts: [], notes: [], prompts: [], logs: [], debug: [], statuses: [], openPanes: new Set(), commands: [], tools: [], schedules: [] };
6
6
  const unplacedPanes = new Set();
7
7
  let focusedPane;
@@ -103,6 +103,10 @@ export function fakeClaude(plugin) {
103
103
  },
104
104
  http: { fetch: rejected('http.fetch', () => fakes.http.fetch) },
105
105
  settings: { read: rejected('settings.read', () => fakes.settings.read) },
106
+ config: {
107
+ list: rejected('config.list', () => fakes.config.list ?? (async () => [])),
108
+ set: rejected('config.set', () => fakes.config.set ?? (async ({ value }) => ({ value }))),
109
+ },
106
110
  agent: {
107
111
  list: rejected('agent.list', () => fakes.agent.list),
108
112
  spawn: rejected('agent.spawn', () => fakes.agent.spawn),
@@ -1 +1 @@
1
- {"version":3,"file":"fake-files.d.ts","sourceRoot":"","sources":["../src/testing/fake-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAGlD,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAiD7H"}
1
+ {"version":3,"file":"fake-files.d.ts","sourceRoot":"","sources":["../src/testing/fake-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAGlD,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAuD7H"}
@@ -19,6 +19,9 @@ export function fakeFiles(files, links = {}) {
19
19
  return below(path).length > 0 ? 'dir' : undefined;
20
20
  };
21
21
  const sizeOf = (path) => new TextEncoder().encode(contents.get(path) ?? '').length;
22
+ const modified = new Map();
23
+ let writes = 0;
24
+ const mtimeOf = (path) => modified.get(path) ?? 0;
22
25
  return {
23
26
  read: async (path) => {
24
27
  const text = contents.get(realPathOf(path));
@@ -27,7 +30,10 @@ export function fakeFiles(files, links = {}) {
27
30
  return text;
28
31
  },
29
32
  write: async (path, text) => {
30
- contents.set(realPathOf(path), text);
33
+ const realPath = realPathOf(path);
34
+ contents.set(realPath, text);
35
+ writes += 1;
36
+ modified.set(realPath, writes);
31
37
  },
32
38
  list: async (path) => {
33
39
  if (path === undefined)
@@ -40,7 +46,7 @@ export function fakeFiles(files, links = {}) {
40
46
  const entry = resolve(folder, name);
41
47
  if (Object.hasOwn(links, entry))
42
48
  return { name, kind: 'other', size: 0, mtimeMs: 0, isLink: true };
43
- return { name, kind: contents.has(entry) ? 'file' : 'dir', size: sizeOf(entry), mtimeMs: 0, isLink: false };
49
+ return { name, kind: contents.has(entry) ? 'file' : 'dir', size: sizeOf(entry), mtimeMs: mtimeOf(entry), isLink: false };
44
50
  });
45
51
  },
46
52
  exists: async (path) => kindOf(realPathOf(path)) !== undefined,
@@ -52,7 +58,7 @@ export function fakeFiles(files, links = {}) {
52
58
  return { kind: 'other', size: 0, mtimeMs: 0, isLink };
53
59
  if (kind === undefined)
54
60
  throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
55
- return { kind, size: sizeOf(realPath), mtimeMs: 0, isLink, ...(options?.resolve === true ? { realPath } : {}) };
61
+ return { kind, size: sizeOf(realPath), mtimeMs: mtimeOf(realPath), isLink, ...(options?.resolve === true ? { realPath } : {}) };
56
62
  },
57
63
  };
58
64
  }
package/testing.d.ts CHANGED
@@ -1,14 +1,17 @@
1
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
+ import type { Options, OptionValues } from './options.js';
4
5
  import type { RoutedEvent } from './runtime/hooks.js';
5
6
  import { type Fakes, type Shown, type TestCall } from './testing/fake-claude.js';
6
7
  import type { Slot, SlotProps } from './ui/slots.js';
7
8
  export type { Fakes, Shown, TestCall } from './testing/fake-claude.js';
8
- export type TestOptions<State extends object> = {
9
+ export type TestOptions<State extends object, Declared extends Options = Options> = {
9
10
  readonly state?: {
10
11
  readonly [Lifetime in keyof State]?: Partial<State[Lifetime]>;
11
12
  };
13
+ readonly options?: Partial<OptionValues<Declared>>;
14
+ readonly permissions?: readonly string[];
12
15
  readonly scope?: 'user' | 'project';
13
16
  readonly projectRoot?: string;
14
17
  readonly cwd?: string;
@@ -16,6 +19,7 @@ export type TestOptions<State extends object> = {
16
19
  readonly [Name in keyof CmodDependencies]?: CmodDependencies[Name];
17
20
  };
18
21
  readonly files?: Readonly<Record<string, string>>;
22
+ readonly links?: Readonly<Record<string, string>>;
19
23
  };
20
24
  type FilledField = 'session_id' | 'transcript_path' | 'cwd' | 'hook_event_name' | 'tool_use_id';
21
25
  export type TestInput<E extends ModEvent> = Omit<ClassicHookInputs[E], FilledField> & Partial<Pick<ClassicHookInputs[E], FilledField & keyof ClassicHookInputs[E]>>;
@@ -39,5 +43,5 @@ export type TestedMod<State extends object> = {
39
43
  readonly fakes: Fakes;
40
44
  readonly shown: Shown;
41
45
  };
42
- export declare function testMod<State extends object>(definition: ModDefinition<State>, options?: TestOptions<State>): TestedMod<State>;
46
+ export declare function testMod<State extends object, Declared extends Options = Options>(definition: ModDefinition<State, string, Declared>, options?: TestOptions<State, Declared>): TestedMod<State>;
43
47
  //# sourceMappingURL=testing.d.ts.map
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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"}
package/testing.js CHANGED
@@ -1,10 +1,12 @@
1
- import { storeFolder } from './records.js';
1
+ import { readSteps, storeFolder } from './records.js';
2
2
  import { answerCall, notInstalled } from './runtime/dependencies.js';
3
3
  import { toolInputOf } from './runtime/tool-calls.js';
4
4
  import { createLifecycle } from './runtime/lifecycle.js';
5
+ import { MissingOptions } from './runtime/options.js';
5
6
  import { fakeClaude } from './testing/fake-claude.js';
6
7
  import { elements, findElement, rowsOf } from './testing/fake-elements.js';
7
8
  import { fakeFiles } from './testing/fake-files.js';
9
+ import { listed } from './utils/text.js';
8
10
  const defaultColumns = 80;
9
11
  const defaultRows = 24;
10
12
  const presentation = { isFullscreen: false, columns: defaultColumns };
@@ -32,25 +34,34 @@ export function testMod(definition, options = {}) {
32
34
  return listAgents();
33
35
  return [...firedAgents].map(([id, type]) => ({ id, type, description: '', status: 'running' }));
34
36
  };
35
- Object.assign(fake.fakes.fs, fakeFiles(options.files ?? {}));
37
+ Object.assign(fake.fakes.fs, fakeFiles(options.files ?? {}, options.links));
36
38
  const starting = options.state;
37
39
  const declared = Object.entries(definition.state ?? {});
38
40
  const state = Object.fromEntries(declared.map(([lifetime, values]) => [lifetime, { ...values, ...starting?.[lifetime] }]));
39
- const lifecycle = createLifecycle({ ...definition, state });
41
+ const lifecycle = createLifecycle({ ...definition, state }, () => true, (options.options ?? {}), false);
40
42
  let started;
41
43
  const start = async () => {
42
- started ??= lifecycle.start(fake.claude, async (claude) => ({
43
- name,
44
- root,
45
- version: '0.0.0',
46
- store: storeFolder({ HOME: await claude.env.home(), XDG_DATA_HOME: await claude.env.dataHome() }),
47
- isInstalled: true,
48
- shouldRecord: false,
49
- keys: {},
50
- }));
44
+ started ??= lifecycle.start(fake.claude, async (claude) => {
45
+ const steps = await modSteps();
46
+ return {
47
+ name,
48
+ root,
49
+ version: '0.0.0',
50
+ store: storeFolder({ HOME: await claude.env.home(), XDG_DATA_HOME: await claude.env.dataHome() }),
51
+ isInstalled: true,
52
+ shouldRecord: false,
53
+ steps,
54
+ granted: options.permissions ?? steps.permissions ?? [],
55
+ };
56
+ });
51
57
  await started;
58
+ const { failure } = lifecycle;
59
+ if (failure instanceof MissingOptions) {
60
+ const given = failure.keys.map((key) => `${key}: …`).join(', ');
61
+ throw new Error(`${name} needs ${listed(failure.titles)}, which a person sets in /config. Give ${failure.keys.length === 1 ? 'it' : 'them'} to the tested mod: testMod(mod, { options: { ${given} } }).`);
62
+ }
52
63
  if (lifecycle.phase !== 'active')
53
- throw lifecycle.failure ?? new Error(`${name} did not start: the lifecycle is ${lifecycle.phase}.`);
64
+ throw failure ?? new Error(`${name} did not start: the lifecycle is ${lifecycle.phase}.`);
54
65
  };
55
66
  const route = async (event, input, below) => {
56
67
  await start();
@@ -226,6 +237,11 @@ function plainOutput({ component, props }) {
226
237
  function replyOf({ text, context }) {
227
238
  return { ...(text === undefined ? {} : { text }), ...(context === undefined ? {} : { context: context.join('\n') }) };
228
239
  }
240
+ async function modSteps() {
241
+ const manifest = '../../../package.json';
242
+ const loaded = await import(manifest, { with: { type: 'json' } }).then((module) => module.default, () => undefined);
243
+ return readSteps(loaded) ?? {};
244
+ }
229
245
  function installJsx() {
230
246
  const scope = globalThis;
231
247
  scope['h'] ??= (tag, props, ...children) => {
@@ -0,0 +1,21 @@
1
+ export type MetadataFormat = {
2
+ readonly kind: 'frontmatter';
3
+ } | {
4
+ readonly kind: 'comment';
5
+ readonly prefix: string;
6
+ } | {
7
+ readonly kind: 'sidecar';
8
+ };
9
+ export type ReadMetadata = {
10
+ readonly metadata: Readonly<Record<string, string>>;
11
+ readonly name?: string;
12
+ };
13
+ export declare class MetadataError extends Error {
14
+ readonly line: number;
15
+ constructor(line: number, message: string);
16
+ }
17
+ export declare const sidecarSuffix = ".meta";
18
+ export declare function formatOf(path: string, firstLine: string | undefined): MetadataFormat;
19
+ export declare function readMetadata(text: string, format: MetadataFormat): ReadMetadata;
20
+ export declare function writeMetadata(text: string, format: MetadataFormat, metadata: Readonly<Record<string, string>>): string;
21
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/utils/metadata.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAA;AAEpJ,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAGxC;CACF;AAQD,eAAO,MAAM,aAAa,UAAU,CAAA;AAEpC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,CAUpF;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,YAAY,CAW/E;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAyBtH"}