Skip to content

Commit ff76b44

Browse files
committed
refactor(scriptlets): extract metadata parser and menu template modules
- Extract metadata parsing logic into separate metadata-parser.ts module - Extract scriptlet menu template into scriptlet-menu-template.ts - Add VALID_TOOLS constant for tool type validation - Simplify utils.ts by removing duplicated functionality - Add comprehensive tests for new modules
1 parent 4b3a16e commit ff76b44

10 files changed

+915
-214
lines changed

src/core/constants.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,59 @@ export const SHELL_TOOLS = [
3838
"pwsh",
3939
"cmd"
4040
]
41+
42+
/**
43+
* All valid scriptlet tool types.
44+
* This includes:
45+
* - Script/Kit tools: '', 'kit', 'ts', 'js'
46+
* - Transform/Template: 'transform', 'template'
47+
* - Action tools: 'open', 'edit', 'paste', 'type', 'submit'
48+
* - Shell tools: bash, sh, zsh, fish, powershell, pwsh, cmd
49+
* - Language interpreters: ruby, python, python3, perl, php, node, etc.
50+
*/
51+
export const VALID_TOOLS = [
52+
// Script/Kit tools
53+
"",
54+
"kit",
55+
"ts",
56+
"js",
57+
// Transform/Template
58+
"transform",
59+
"template",
60+
// Action tools
61+
"open",
62+
"edit",
63+
"paste",
64+
"type",
65+
"submit",
66+
// Shell tools
67+
...SHELL_TOOLS,
68+
// Language interpreters
69+
"ruby",
70+
"python",
71+
"python3",
72+
"perl",
73+
"php",
74+
"node",
75+
"lua",
76+
"r",
77+
"groovy",
78+
"scala",
79+
"swift",
80+
"go",
81+
"rust",
82+
"java",
83+
"clojure",
84+
"elixir",
85+
"erlang",
86+
"ocaml",
87+
"osascript",
88+
"deno",
89+
"kotlin",
90+
"julia",
91+
"dart",
92+
"haskell",
93+
"csharp"
94+
] as const
95+
96+
export type ToolType = typeof VALID_TOOLS[number]

src/core/metadata-parser.test.ts

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import ava from 'ava'
2+
import {
3+
parseMetadataComments,
4+
parseSnippetMetadata,
5+
VALID_METADATA_KEYS_SET,
6+
VALID_METADATA_KEYS
7+
} from './metadata-parser.js'
8+
9+
// Test basic metadata parsing
10+
ava('parseMetadataComments - parses basic comment metadata', (t) => {
11+
const content = `
12+
// Name: Test Script
13+
// Description: A test script
14+
// Shortcut: cmd+shift+t
15+
`.trim()
16+
17+
const { metadata, warnings } = parseMetadataComments(content)
18+
19+
t.is(metadata.name, 'Test Script')
20+
t.is(metadata.description, 'A test script')
21+
t.is(metadata.shortcut, 'cmd+shift+t')
22+
t.is(warnings.length, 0)
23+
})
24+
25+
// Test hash-style comments
26+
ava('parseMetadataComments - parses hash-style comments', (t) => {
27+
const content = `
28+
# Name: Python Script
29+
# Description: A Python script
30+
`.trim()
31+
32+
const { metadata, warnings } = parseMetadataComments(content)
33+
34+
t.is(metadata.name, 'Python Script')
35+
t.is(metadata.description, 'A Python script')
36+
t.is(warnings.length, 0)
37+
})
38+
39+
// Test boolean parsing
40+
ava('parseMetadataComments - parses boolean values', (t) => {
41+
const content = `
42+
// Background: true
43+
// LongRunning: false
44+
`.trim()
45+
46+
const { metadata } = parseMetadataComments(content)
47+
48+
t.is(metadata.background, true)
49+
t.is(metadata.longRunning, false)
50+
})
51+
52+
// Test number parsing
53+
ava('parseMetadataComments - parses timeout as number', (t) => {
54+
const content = `
55+
// Timeout: 5000
56+
// Index: 10
57+
`.trim()
58+
59+
const { metadata } = parseMetadataComments(content)
60+
61+
t.is(metadata.timeout, 5000)
62+
t.is(metadata.index, 10)
63+
})
64+
65+
// Test validation warnings for unknown keys
66+
ava('parseMetadataComments - warns on unknown keys', (t) => {
67+
const content = `
68+
// Name: Test Script
69+
// UnknownKey: some value
70+
// AnotherBadKey: another value
71+
`.trim()
72+
73+
const { metadata, warnings, raw } = parseMetadataComments(content)
74+
75+
t.is(metadata.name, 'Test Script')
76+
t.is(warnings.length, 2)
77+
t.is(warnings[0].key, 'unknownKey')
78+
t.true(warnings[0].message.includes('Unknown metadata key'))
79+
// Raw should contain the invalid keys
80+
t.is(raw.unknownKey, 'some value')
81+
t.is(raw.anotherBadKey, 'another value')
82+
})
83+
84+
// Test typo suggestions
85+
ava('parseMetadataComments - suggests corrections for typos', (t) => {
86+
const content = `
87+
// Shotcut: cmd+t
88+
`.trim()
89+
90+
const { warnings } = parseMetadataComments(content)
91+
92+
t.is(warnings.length, 1)
93+
t.is(warnings[0].key, 'shotcut')
94+
t.truthy(warnings[0].suggestion)
95+
t.true(warnings[0].suggestion?.includes('shortcut'))
96+
})
97+
98+
// Test ignoring URLs
99+
ava('parseMetadataComments - ignores URLs in comments', (t) => {
100+
const content = `
101+
// Name: Test Script
102+
// See https://example.com for more info
103+
// API docs: http://api.example.com
104+
`.trim()
105+
106+
const { metadata, warnings } = parseMetadataComments(content)
107+
108+
t.is(metadata.name, 'Test Script')
109+
// Should not create warnings for URL-like keys
110+
t.is(warnings.length, 0)
111+
})
112+
113+
// Test ignoring TODO/FIXME
114+
ava('parseMetadataComments - ignores TODO/FIXME comments', (t) => {
115+
const content = `
116+
// Name: Test Script
117+
// TODO: Fix this later
118+
// FIXME: This is broken
119+
// NOTE: Important info
120+
`.trim()
121+
122+
const { metadata, warnings } = parseMetadataComments(content)
123+
124+
t.is(metadata.name, 'Test Script')
125+
t.is(warnings.length, 0)
126+
})
127+
128+
// Test multiline comment skipping
129+
ava('parseMetadataComments - skips multiline comments', (t) => {
130+
const content = `
131+
// Name: Test Script
132+
/* This is a multiline
133+
comment that should be
134+
skipped entirely */
135+
// Description: After multiline
136+
`.trim()
137+
138+
const { metadata, warnings } = parseMetadataComments(content)
139+
140+
t.is(metadata.name, 'Test Script')
141+
t.is(metadata.description, 'After multiline')
142+
t.is(warnings.length, 0)
143+
})
144+
145+
// Test first value wins
146+
ava('parseMetadataComments - first value wins for duplicate keys', (t) => {
147+
const content = `
148+
// Name: First Name
149+
// Name: Second Name
150+
`.trim()
151+
152+
const { metadata } = parseMetadataComments(content)
153+
154+
t.is(metadata.name, 'First Name')
155+
})
156+
157+
// Test case normalization
158+
ava('parseMetadataComments - normalizes key case', (t) => {
159+
const content = `
160+
// NAME: Test
161+
// ShortCut: cmd+t
162+
// BACKGROUND: true
163+
`.trim()
164+
165+
const { metadata } = parseMetadataComments(content)
166+
167+
// Keys should be normalized to camelCase
168+
t.is(metadata.name, 'Test')
169+
t.is(metadata.shortcut, 'cmd+t')
170+
t.is(metadata.background, true)
171+
})
172+
173+
// Test validation disabled
174+
ava('parseMetadataComments - allows unknown keys when validation disabled', (t) => {
175+
const content = `
176+
// CustomKey: custom value
177+
// Name: Test
178+
`.trim()
179+
180+
const { metadata, warnings, raw } = parseMetadataComments(content, { validate: false })
181+
182+
// With validation disabled, unknown keys go to metadata
183+
t.is(warnings.length, 0)
184+
t.is((metadata as any).customKey, 'custom value')
185+
t.is(metadata.name, 'Test')
186+
})
187+
188+
// Test maxLines option
189+
ava('parseMetadataComments - respects maxLines option', (t) => {
190+
const content = `
191+
// Name: Test Script
192+
// Description: Line 2
193+
// Shortcut: cmd+t
194+
// Background: true
195+
// Schedule: 0 * * * *
196+
`.trim()
197+
198+
const { metadata } = parseMetadataComments(content, { maxLines: 3 })
199+
200+
t.is(metadata.name, 'Test Script')
201+
t.is(metadata.description, 'Line 2')
202+
t.is(metadata.shortcut, 'cmd+t')
203+
t.is(metadata.background, undefined)
204+
t.is(metadata.schedule, undefined)
205+
})
206+
207+
// Test parseSnippetMetadata
208+
ava('parseSnippetMetadata - parses snippet with body', (t) => {
209+
const content = `
210+
// Name: My Snippet
211+
// Snippet: test
212+
console.log("Hello World");
213+
`.trim()
214+
215+
const result = parseSnippetMetadata(content)
216+
217+
t.is(result.metadata.name, 'My Snippet')
218+
t.is(result.snippetKey, 'test')
219+
t.is(result.postfix, false)
220+
t.is(result.snippetBody, 'console.log("Hello World");')
221+
})
222+
223+
// Test parseSnippetMetadata with postfix
224+
ava('parseSnippetMetadata - handles postfix snippets', (t) => {
225+
const content = `
226+
// Name: Postfix Snippet
227+
// Expand: *postfix
228+
body content
229+
`.trim()
230+
231+
const result = parseSnippetMetadata(content)
232+
233+
t.is(result.snippetKey, 'postfix')
234+
t.is(result.postfix, true)
235+
t.is(result.snippetBody, 'body content')
236+
})
237+
238+
// Test parseSnippetMetadata with hash comments
239+
ava('parseSnippetMetadata - handles hash-style comments', (t) => {
240+
const content = `
241+
# Name: Shell Snippet
242+
# Snippet: shell
243+
echo "Hello"
244+
`.trim()
245+
246+
const result = parseSnippetMetadata(content)
247+
248+
t.is(result.metadata.name, 'Shell Snippet')
249+
t.is(result.snippetKey, 'shell')
250+
t.is(result.snippetBody, 'echo "Hello"')
251+
})
252+
253+
// Test all valid keys are in the set
254+
ava('VALID_METADATA_KEYS_SET - contains all valid keys', (t) => {
255+
const expectedKeys = [
256+
'author', 'name', 'description', 'enter', 'alias', 'image', 'emoji',
257+
'shortcut', 'shortcode', 'trigger', 'snippet', 'expand', 'keyword',
258+
'pass', 'group', 'exclude', 'watch', 'log', 'background', 'system',
259+
'schedule', 'index', 'access', 'response', 'tag', 'longRunning',
260+
'mcp', 'timeout', 'cache', 'bin', 'postfix'
261+
]
262+
263+
for (const key of expectedKeys) {
264+
t.true(VALID_METADATA_KEYS_SET.has(key), `Expected '${key}' to be in valid keys set`)
265+
}
266+
})
267+
268+
// Test empty content
269+
ava('parseMetadataComments - handles empty content', (t) => {
270+
const { metadata, warnings } = parseMetadataComments('')
271+
272+
t.deepEqual(metadata, {})
273+
t.is(warnings.length, 0)
274+
})
275+
276+
// Test whitespace variations
277+
ava('parseMetadataComments - handles various whitespace patterns', (t) => {
278+
// Test each pattern individually
279+
t.deepEqual(parseMetadataComments('//Name:Test').metadata, { name: 'Test' })
280+
t.deepEqual(parseMetadataComments('//Name: Test').metadata, { name: 'Test' })
281+
t.deepEqual(parseMetadataComments('// Name:Test').metadata, { name: 'Test' })
282+
t.deepEqual(parseMetadataComments('// Name: Test').metadata, { name: 'Test' })
283+
t.deepEqual(parseMetadataComments('// Name:Test').metadata, { name: 'Test' })
284+
t.deepEqual(parseMetadataComments('// Name: Test').metadata, { name: 'Test' })
285+
})

0 commit comments

Comments
 (0)