@plaited/development-skills 0.3.5

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 (40) hide show
  1. package/.claude/commands/lsp-analyze.md +66 -0
  2. package/.claude/commands/lsp-find.md +51 -0
  3. package/.claude/commands/lsp-hover.md +48 -0
  4. package/.claude/commands/lsp-refs.md +55 -0
  5. package/.claude/commands/scaffold-rules.md +221 -0
  6. package/.claude/commands/validate-skill.md +29 -0
  7. package/.claude/rules/accuracy.md +64 -0
  8. package/.claude/rules/bun-apis.md +80 -0
  9. package/.claude/rules/code-review.md +276 -0
  10. package/.claude/rules/git-workflow.md +66 -0
  11. package/.claude/rules/github.md +154 -0
  12. package/.claude/rules/testing.md +125 -0
  13. package/.claude/settings.local.json +47 -0
  14. package/.claude/skills/code-documentation/SKILL.md +47 -0
  15. package/.claude/skills/code-documentation/references/internal-templates.md +113 -0
  16. package/.claude/skills/code-documentation/references/maintenance.md +164 -0
  17. package/.claude/skills/code-documentation/references/public-api-templates.md +100 -0
  18. package/.claude/skills/code-documentation/references/type-documentation.md +116 -0
  19. package/.claude/skills/code-documentation/references/workflow.md +60 -0
  20. package/.claude/skills/scaffold-rules/SKILL.md +97 -0
  21. package/.claude/skills/typescript-lsp/SKILL.md +239 -0
  22. package/.claude/skills/validate-skill/SKILL.md +105 -0
  23. package/LICENSE +15 -0
  24. package/README.md +149 -0
  25. package/bin/cli.ts +109 -0
  26. package/package.json +57 -0
  27. package/src/lsp-analyze.ts +223 -0
  28. package/src/lsp-client.ts +400 -0
  29. package/src/lsp-find.ts +100 -0
  30. package/src/lsp-hover.ts +87 -0
  31. package/src/lsp-references.ts +83 -0
  32. package/src/lsp-symbols.ts +73 -0
  33. package/src/resolve-file-path.ts +28 -0
  34. package/src/scaffold-rules.ts +435 -0
  35. package/src/tests/fixtures/sample.ts +27 -0
  36. package/src/tests/lsp-client.spec.ts +180 -0
  37. package/src/tests/resolve-file-path.spec.ts +33 -0
  38. package/src/tests/scaffold-rules.spec.ts +286 -0
  39. package/src/tests/validate-skill.spec.ts +231 -0
  40. package/src/validate-skill.ts +492 -0
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Get type information at a position in a TypeScript/JavaScript file
4
+ *
5
+ * Usage: bun lsp-hover.ts <file> <line> <character>
6
+ */
7
+
8
+ import { parseArgs } from 'node:util'
9
+ import { LspClient } from './lsp-client.ts'
10
+ import { resolveFilePath } from './resolve-file-path.ts'
11
+
12
+ /**
13
+ * Get type information at a cursor position in TypeScript/JavaScript files
14
+ *
15
+ * @param args - Command line arguments [file, line, character]
16
+ */
17
+ export const lspHover = async (args: string[]) => {
18
+ const { positionals } = parseArgs({
19
+ args,
20
+ allowPositionals: true,
21
+ })
22
+
23
+ const [filePath, lineStr, charStr] = positionals
24
+
25
+ if (!filePath || !lineStr || !charStr) {
26
+ console.error('Usage: lsp-hover <file> <line> <character>')
27
+ console.error(' file: Path to TypeScript/JavaScript file')
28
+ console.error(' line: Line number (0-indexed)')
29
+ console.error(' character: Character position (0-indexed)')
30
+ process.exit(1)
31
+ }
32
+
33
+ const line = parseInt(lineStr, 10)
34
+ const character = parseInt(charStr, 10)
35
+
36
+ if (Number.isNaN(line) || Number.isNaN(character)) {
37
+ console.error('Error: line and character must be numbers')
38
+ process.exit(1)
39
+ }
40
+
41
+ const absolutePath = await resolveFilePath(filePath)
42
+ const uri = `file://${absolutePath}`
43
+ const rootUri = `file://${process.cwd()}`
44
+
45
+ const client = new LspClient({ rootUri })
46
+
47
+ try {
48
+ await client.start()
49
+
50
+ const file = Bun.file(absolutePath)
51
+ if (!(await file.exists())) {
52
+ console.error(`Error: File not found: ${absolutePath}`)
53
+ process.exit(1)
54
+ }
55
+
56
+ const text = await file.text()
57
+ const languageId = absolutePath.endsWith('.tsx')
58
+ ? 'typescriptreact'
59
+ : absolutePath.endsWith('.ts')
60
+ ? 'typescript'
61
+ : absolutePath.endsWith('.jsx')
62
+ ? 'javascriptreact'
63
+ : 'javascript'
64
+
65
+ client.openDocument(uri, languageId, 1, text)
66
+
67
+ const result = await client.hover(uri, line, character)
68
+
69
+ client.closeDocument(uri)
70
+ await client.stop()
71
+
72
+ if (result) {
73
+ console.log(JSON.stringify(result, null, 2))
74
+ } else {
75
+ console.log('null')
76
+ }
77
+ } catch (error) {
78
+ console.error(`Error: ${error}`)
79
+ await client.stop()
80
+ process.exit(1)
81
+ }
82
+ }
83
+
84
+ // Keep executable entry point for direct execution
85
+ if (import.meta.main) {
86
+ await lspHover(Bun.argv.slice(2))
87
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Find all references to a symbol at a position
4
+ *
5
+ * Usage: bun lsp-references.ts <file> <line> <character>
6
+ */
7
+
8
+ import { parseArgs } from 'node:util'
9
+ import { LspClient } from './lsp-client.ts'
10
+ import { resolveFilePath } from './resolve-file-path.ts'
11
+
12
+ /**
13
+ * Find all references to a symbol at a cursor position
14
+ *
15
+ * @param args - Command line arguments [file, line, character]
16
+ */
17
+ export const lspRefs = async (args: string[]) => {
18
+ const { positionals } = parseArgs({
19
+ args,
20
+ allowPositionals: true,
21
+ })
22
+
23
+ const [filePath, lineStr, charStr] = positionals
24
+
25
+ if (!filePath || !lineStr || !charStr) {
26
+ console.error('Usage: lsp-refs <file> <line> <character>')
27
+ console.error(' file: Path to TypeScript/JavaScript file')
28
+ console.error(' line: Line number (0-indexed)')
29
+ console.error(' character: Character position (0-indexed)')
30
+ process.exit(1)
31
+ }
32
+
33
+ const line = parseInt(lineStr, 10)
34
+ const character = parseInt(charStr, 10)
35
+
36
+ if (Number.isNaN(line) || Number.isNaN(character)) {
37
+ console.error('Error: line and character must be numbers')
38
+ process.exit(1)
39
+ }
40
+
41
+ const absolutePath = await resolveFilePath(filePath)
42
+ const uri = `file://${absolutePath}`
43
+ const rootUri = `file://${process.cwd()}`
44
+
45
+ const client = new LspClient({ rootUri })
46
+
47
+ try {
48
+ await client.start()
49
+
50
+ const file = Bun.file(absolutePath)
51
+ if (!(await file.exists())) {
52
+ console.error(`Error: File not found: ${absolutePath}`)
53
+ process.exit(1)
54
+ }
55
+
56
+ const text = await file.text()
57
+ const languageId = absolutePath.endsWith('.tsx')
58
+ ? 'typescriptreact'
59
+ : absolutePath.endsWith('.ts')
60
+ ? 'typescript'
61
+ : absolutePath.endsWith('.jsx')
62
+ ? 'javascriptreact'
63
+ : 'javascript'
64
+
65
+ client.openDocument(uri, languageId, 1, text)
66
+
67
+ const result = await client.references(uri, line, character, true)
68
+
69
+ client.closeDocument(uri)
70
+ await client.stop()
71
+
72
+ console.log(JSON.stringify(result, null, 2))
73
+ } catch (error) {
74
+ console.error(`Error: ${error}`)
75
+ await client.stop()
76
+ process.exit(1)
77
+ }
78
+ }
79
+
80
+ // Keep executable entry point for direct execution
81
+ if (import.meta.main) {
82
+ await lspRefs(Bun.argv.slice(2))
83
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Get all symbols (functions, classes, types, etc.) in a TypeScript/JavaScript file
4
+ *
5
+ * Usage: bun lsp-symbols.ts <file>
6
+ */
7
+
8
+ import { parseArgs } from 'node:util'
9
+ import { LspClient } from './lsp-client.ts'
10
+ import { resolveFilePath } from './resolve-file-path.ts'
11
+
12
+ /**
13
+ * Get all symbols in a TypeScript/JavaScript file
14
+ *
15
+ * @param args - Command line arguments [file]
16
+ */
17
+ export const lspSymbols = async (args: string[]) => {
18
+ const { positionals } = parseArgs({
19
+ args,
20
+ allowPositionals: true,
21
+ })
22
+
23
+ const [filePath] = positionals
24
+
25
+ if (!filePath) {
26
+ console.error('Usage: lsp-symbols <file>')
27
+ console.error(' file: Path to TypeScript/JavaScript file')
28
+ process.exit(1)
29
+ }
30
+
31
+ const absolutePath = await resolveFilePath(filePath)
32
+ const uri = `file://${absolutePath}`
33
+ const rootUri = `file://${process.cwd()}`
34
+
35
+ const client = new LspClient({ rootUri })
36
+
37
+ try {
38
+ await client.start()
39
+
40
+ const file = Bun.file(absolutePath)
41
+ if (!(await file.exists())) {
42
+ console.error(`Error: File not found: ${absolutePath}`)
43
+ process.exit(1)
44
+ }
45
+
46
+ const text = await file.text()
47
+ const languageId = absolutePath.endsWith('.tsx')
48
+ ? 'typescriptreact'
49
+ : absolutePath.endsWith('.ts')
50
+ ? 'typescript'
51
+ : absolutePath.endsWith('.jsx')
52
+ ? 'javascriptreact'
53
+ : 'javascript'
54
+
55
+ client.openDocument(uri, languageId, 1, text)
56
+
57
+ const result = await client.documentSymbols(uri)
58
+
59
+ client.closeDocument(uri)
60
+ await client.stop()
61
+
62
+ console.log(JSON.stringify(result, null, 2))
63
+ } catch (error) {
64
+ console.error(`Error: ${error}`)
65
+ await client.stop()
66
+ process.exit(1)
67
+ }
68
+ }
69
+
70
+ // Keep executable entry point for direct execution
71
+ if (import.meta.main) {
72
+ await lspSymbols(Bun.argv.slice(2))
73
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolve a file path to an absolute path
3
+ *
4
+ * @remarks
5
+ * Handles three types of paths:
6
+ * - Absolute paths (starting with `/`) - returned as-is
7
+ * - Relative paths (starting with `.`) - resolved from cwd
8
+ * - Package export paths (e.g., `plaited/workshop/get-paths.ts`) - resolved via Bun.resolve()
9
+ */
10
+ export const resolveFilePath = async (filePath: string): Promise<string> => {
11
+ // Absolute path
12
+ if (filePath.startsWith('/')) {
13
+ return filePath
14
+ }
15
+
16
+ // Relative path from cwd
17
+ if (filePath.startsWith('.')) {
18
+ return `${process.cwd()}/${filePath}`
19
+ }
20
+
21
+ // Try package export path resolution
22
+ try {
23
+ return await Bun.resolve(filePath, process.cwd())
24
+ } catch {
25
+ // Fall back to relative path from cwd
26
+ return `${process.cwd()}/${filePath}`
27
+ }
28
+ }
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Scaffold development rules from templates
4
+ *
5
+ * Reads bundled rule templates, processes template variables and conditionals,
6
+ * and outputs JSON for agent consumption.
7
+ *
8
+ * Supports 2 target formats:
9
+ * - claude: Claude Code with .claude/rules/ directory
10
+ * - agents-md: Universal AGENTS.md format with .plaited/rules/
11
+ * (works with Cursor, Factory, Copilot, Windsurf, Cline, Aider, and 60,000+ others)
12
+ *
13
+ * Options:
14
+ * - --agent, -a: Target format (claude | agents-md)
15
+ * - --rules-dir, -d: Custom rules directory path (overrides default)
16
+ * - --agents-md-path, -m: Custom AGENTS.md file path (default: AGENTS.md)
17
+ * - --rules, -r: Filter to specific rules (can be used multiple times)
18
+ *
19
+ * Template syntax:
20
+ * - {{LINK:rule-id}} - Cross-reference to another rule
21
+ * - {{#if development-skills}}...{{/if}} - Conditional block
22
+ * - {{#if capability}}...{{/if}} - Capability-based conditional
23
+ * - {{^if condition}}...{{/if}} - Inverse conditional
24
+ * - <!-- RULE TEMPLATE ... --> - Template header (removed)
25
+ *
26
+ * Capabilities:
27
+ * - has-sandbox: Agent runs in sandboxed environment (Claude Code only)
28
+ * - supports-slash-commands: Agent has /command syntax (Claude Code only)
29
+ *
30
+ * @example
31
+ * ```bash
32
+ * # Default paths
33
+ * bunx @plaited/development-skills scaffold-rules --agent=claude
34
+ * bunx @plaited/development-skills scaffold-rules --agent=agents-md
35
+ *
36
+ * # Custom paths
37
+ * bunx @plaited/development-skills scaffold-rules --agent=agents-md --rules-dir=.cursor/rules
38
+ * bunx @plaited/development-skills scaffold-rules --agent=agents-md --agents-md-path=docs/AGENTS.md
39
+ * ```
40
+ */
41
+
42
+ import { readdir } from 'node:fs/promises'
43
+ import { join } from 'node:path'
44
+ import { parseArgs } from 'node:util'
45
+
46
+ /**
47
+ * Supported agent targets
48
+ *
49
+ * @remarks
50
+ * - claude: Claude Code with its own .claude/ directory structure
51
+ * - agents-md: Universal AGENTS.md format (works with Cursor, Factory, Copilot,
52
+ * Windsurf, Cline, Aider, and 60,000+ other projects)
53
+ */
54
+ type Agent = 'claude' | 'agents-md'
55
+
56
+ type AgentCapabilities = {
57
+ hasSandbox: boolean
58
+ multiFileRules: boolean
59
+ supportsSlashCommands: boolean
60
+ supportsAgentsMd: boolean
61
+ }
62
+
63
+ type TemplateContext = {
64
+ agent: Agent
65
+ capabilities: AgentCapabilities
66
+ hasDevelopmentSkills: boolean
67
+ rulesPath: string
68
+ }
69
+
70
+ type ProcessedTemplate = {
71
+ filename: string
72
+ content: string
73
+ description: string
74
+ }
75
+
76
+ type ScaffoldOutput = {
77
+ agent: Agent
78
+ rulesPath: string
79
+ agentsMdPath: string
80
+ format: 'multi-file' | 'agents-md'
81
+ supportsAgentsMd: boolean
82
+ agentsMdContent?: string
83
+ templates: Record<string, ProcessedTemplate>
84
+ }
85
+
86
+ /**
87
+ * Agent capabilities matrix
88
+ *
89
+ * @remarks
90
+ * - hasSandbox: Runs in restricted environment (affects git commands, temp files)
91
+ * - multiFileRules: Supports directory of rule files vs single file
92
+ * - supportsSlashCommands: Has /command syntax for invoking tools
93
+ * - supportsAgentsMd: Reads AGENTS.md format
94
+ *
95
+ * Note: We only support 2 targets now:
96
+ * - claude: Claude Code with unique .claude/ directory system
97
+ * - agents-md: Universal format for all AGENTS.md-compatible agents
98
+ * (Cursor, Factory, Copilot, Windsurf, Cline, Aider, and 60,000+ others)
99
+ */
100
+ const AGENT_CAPABILITIES: Record<Agent, AgentCapabilities> = {
101
+ claude: {
102
+ hasSandbox: true,
103
+ multiFileRules: true,
104
+ supportsSlashCommands: true,
105
+ supportsAgentsMd: false,
106
+ },
107
+ 'agents-md': {
108
+ hasSandbox: false,
109
+ multiFileRules: true,
110
+ supportsSlashCommands: false,
111
+ supportsAgentsMd: true,
112
+ },
113
+ }
114
+
115
+ /**
116
+ * Evaluate a single condition against context
117
+ */
118
+ const evaluateCondition = (condition: string, context: TemplateContext): boolean => {
119
+ // Check development-skills
120
+ if (condition === 'development-skills') {
121
+ return context.hasDevelopmentSkills
122
+ }
123
+
124
+ // Check capability-based conditions
125
+ if (condition === 'has-sandbox') {
126
+ return context.capabilities.hasSandbox
127
+ }
128
+ if (condition === 'multi-file-rules') {
129
+ return context.capabilities.multiFileRules
130
+ }
131
+ if (condition === 'supports-slash-commands') {
132
+ return context.capabilities.supportsSlashCommands
133
+ }
134
+ if (condition === 'supports-agents-md') {
135
+ return context.capabilities.supportsAgentsMd
136
+ }
137
+
138
+ // Check agent-specific conditions (legacy: agent:name)
139
+ const agentMatch = condition.match(/^agent:(\w+)$/)
140
+ if (agentMatch) {
141
+ return context.agent === agentMatch[1]
142
+ }
143
+
144
+ return false
145
+ }
146
+
147
+ /**
148
+ * Process template conditionals
149
+ *
150
+ * Handles:
151
+ * - {{#if development-skills}}...{{/if}}
152
+ * - {{#if capability}}...{{/if}} (has-sandbox, multi-file-rules, etc.)
153
+ * - {{#if agent:name}}...{{/if}} (legacy, still supported)
154
+ * - {{^if condition}}...{{/if}} (inverse)
155
+ *
156
+ * Processes iteratively to handle nested conditionals correctly.
157
+ */
158
+ const processConditionals = (content: string, context: TemplateContext): string => {
159
+ let result = content
160
+ let previousResult = ''
161
+
162
+ // Process iteratively until no more changes (handles nested conditionals)
163
+ while (result !== previousResult) {
164
+ previousResult = result
165
+
166
+ // Process positive conditionals {{#if condition}}...{{/if}}
167
+ // Use non-greedy match that doesn't cross other conditional boundaries
168
+ // Note: Nested quantifiers are safe here - input is trusted bundled templates, not user content
169
+ result = result.replace(
170
+ /\{\{#if ([\w:-]+)\}\}((?:(?!\{\{#if )(?!\{\{\^if )(?!\{\{\/if\}\})[\s\S])*?)\{\{\/if\}\}/g,
171
+ (_, condition, block) => {
172
+ return evaluateCondition(condition, context) ? block : ''
173
+ },
174
+ )
175
+
176
+ // Process inverse conditionals {{^if condition}}...{{/if}}
177
+ result = result.replace(
178
+ /\{\{\^if ([\w:-]+)\}\}((?:(?!\{\{#if )(?!\{\{\^if )(?!\{\{\/if\}\})[\s\S])*?)\{\{\/if\}\}/g,
179
+ (_, condition, block) => {
180
+ return evaluateCondition(condition, context) ? '' : block
181
+ },
182
+ )
183
+ }
184
+
185
+ return result
186
+ }
187
+
188
+ /**
189
+ * Process template variables
190
+ *
191
+ * Handles:
192
+ * - {{LINK:rule-id}} - Generate cross-reference
193
+ * - {{AGENT_NAME}} - Agent name
194
+ * - {{RULES_PATH}} - Rules path
195
+ */
196
+ const processVariables = (content: string, context: TemplateContext): string => {
197
+ let result = content
198
+
199
+ // Replace {{LINK:rule-id}} with appropriate cross-reference
200
+ result = result.replace(/\{\{LINK:(\w+)\}\}/g, (_, ruleId) => {
201
+ return generateCrossReference(ruleId, context)
202
+ })
203
+
204
+ // Replace {{AGENT_NAME}}
205
+ result = result.replace(/\{\{AGENT_NAME\}\}/g, context.agent)
206
+
207
+ // Replace {{RULES_PATH}}
208
+ result = result.replace(/\{\{RULES_PATH\}\}/g, context.rulesPath)
209
+
210
+ return result
211
+ }
212
+
213
+ /**
214
+ * Generate cross-reference based on agent format
215
+ */
216
+ const generateCrossReference = (ruleId: string, context: TemplateContext): string => {
217
+ if (context.agent === 'claude') {
218
+ // Claude Code uses @ syntax for file references
219
+ return `@${context.rulesPath}/${ruleId}.md`
220
+ }
221
+ // Use context.rulesPath for cross-references (supports custom --rules-dir)
222
+ return `${context.rulesPath}/${ruleId}.md`
223
+ }
224
+
225
+ /**
226
+ * Remove template headers
227
+ */
228
+ const removeTemplateHeaders = (content: string): string => {
229
+ return content.replace(/<!--[\s\S]*?-->\n*/g, '')
230
+ }
231
+
232
+ /**
233
+ * Extract description from rule content
234
+ */
235
+ const extractDescription = (content: string): string => {
236
+ // Look for first paragraph or heading after main title
237
+ const lines = content.split('\n')
238
+ let description = ''
239
+
240
+ for (let i = 1; i < lines.length; i++) {
241
+ const line = lines[i]?.trim()
242
+ if (line && !line.startsWith('#') && !line.startsWith('**')) {
243
+ description = line
244
+ break
245
+ }
246
+ }
247
+
248
+ return description || 'Development rule'
249
+ }
250
+
251
+ /**
252
+ * Process a template with context
253
+ */
254
+ const processTemplate = (content: string, context: TemplateContext): string => {
255
+ let result = content
256
+
257
+ // 1. Remove template headers
258
+ result = removeTemplateHeaders(result)
259
+
260
+ // 2. Process conditionals
261
+ result = processConditionals(result, context)
262
+
263
+ // 3. Process variables
264
+ result = processVariables(result, context)
265
+
266
+ // 4. Clean up extra blank lines
267
+ result = result.replace(/\n{3,}/g, '\n\n')
268
+
269
+ return result
270
+ }
271
+
272
+ /**
273
+ * Get rules path for agent
274
+ */
275
+ const getRulesPath = (agent: Agent): string => {
276
+ return agent === 'claude' ? '.claude/rules' : '.plaited/rules'
277
+ }
278
+
279
+ /**
280
+ * Get output format for agent
281
+ */
282
+ const getOutputFormat = (agent: Agent): 'multi-file' | 'agents-md' => {
283
+ return agent === 'claude' ? 'multi-file' : 'agents-md'
284
+ }
285
+
286
+ /**
287
+ * Generate AGENTS.md content that links to rules directory
288
+ */
289
+ const generateAgentsMd = (templates: Record<string, ProcessedTemplate>, rulesPath: string): string => {
290
+ const lines = [
291
+ '# AGENTS.md',
292
+ '',
293
+ 'Development rules for AI coding agents.',
294
+ '',
295
+ '## Rules',
296
+ '',
297
+ `This project uses modular development rules stored in \`${rulesPath}/\`.`,
298
+ 'Each rule file covers a specific topic:',
299
+ '',
300
+ ]
301
+
302
+ for (const [ruleId, template] of Object.entries(templates)) {
303
+ lines.push(`- [${ruleId}](${rulesPath}/${template.filename}) - ${template.description}`)
304
+ }
305
+
306
+ lines.push('')
307
+ lines.push('## Quick Reference')
308
+ lines.push('')
309
+ lines.push('For detailed guidance on each topic, see the linked rule files above.')
310
+ lines.push('')
311
+
312
+ return lines.join('\n')
313
+ }
314
+
315
+ /**
316
+ * Main scaffold-rules function
317
+ */
318
+ export const scaffoldRules = async (args: string[]): Promise<void> => {
319
+ const { values } = parseArgs({
320
+ args,
321
+ options: {
322
+ agent: {
323
+ type: 'string',
324
+ short: 'a',
325
+ default: 'claude',
326
+ },
327
+ format: {
328
+ type: 'string',
329
+ short: 'f',
330
+ default: 'json',
331
+ },
332
+ rules: {
333
+ type: 'string',
334
+ short: 'r',
335
+ multiple: true,
336
+ },
337
+ 'rules-dir': {
338
+ type: 'string',
339
+ short: 'd',
340
+ },
341
+ 'agents-md-path': {
342
+ type: 'string',
343
+ short: 'm',
344
+ },
345
+ },
346
+ allowPositionals: true,
347
+ strict: false,
348
+ })
349
+
350
+ const agent = values.agent as Agent
351
+ const rulesFilter = values.rules as string[] | undefined
352
+ const customRulesDir = values['rules-dir'] as string | undefined
353
+ const customAgentsMdPath = values['agents-md-path'] as string | undefined
354
+
355
+ // Validate agent
356
+ const validAgents: Agent[] = ['claude', 'agents-md']
357
+ if (!validAgents.includes(agent)) {
358
+ console.error(`Error: Invalid agent "${agent}". Must be one of: ${validAgents.join(', ')}`)
359
+ console.error(
360
+ 'Note: Use "agents-md" for Cursor, Factory, Copilot, Windsurf, Cline, Aider, and other AGENTS.md-compatible tools.',
361
+ )
362
+ process.exit(1)
363
+ }
364
+
365
+ // Get bundled templates directory
366
+ const packageRulesDir = join(import.meta.dir, '../.claude/rules')
367
+
368
+ // Read template files
369
+ const templateFiles = await readdir(packageRulesDir)
370
+
371
+ // Filter to .md files
372
+ const mdFiles = templateFiles.filter((f) => f.endsWith('.md'))
373
+
374
+ // Filter if specific rules requested
375
+ const rulesToProcess = rulesFilter ? mdFiles.filter((f) => rulesFilter.includes(f.replace('.md', ''))) : mdFiles
376
+
377
+ // Process each template
378
+ const templates: Record<string, ProcessedTemplate> = {}
379
+ const capabilities = AGENT_CAPABILITIES[agent]
380
+
381
+ // Use custom paths or defaults
382
+ const rulesPath = customRulesDir ?? getRulesPath(agent)
383
+ const agentsMdPath = customAgentsMdPath ?? 'AGENTS.md'
384
+
385
+ const context: TemplateContext = {
386
+ agent,
387
+ capabilities,
388
+ hasDevelopmentSkills: true, // Always true when using our CLI
389
+ rulesPath,
390
+ }
391
+
392
+ for (const file of rulesToProcess) {
393
+ const templatePath = join(packageRulesDir, file)
394
+ const ruleId = file.replace('.md', '')
395
+
396
+ try {
397
+ const content = await Bun.file(templatePath).text()
398
+
399
+ // Process template
400
+ const processed = processTemplate(content, context)
401
+
402
+ templates[ruleId] = {
403
+ filename: file,
404
+ content: processed,
405
+ description: extractDescription(processed),
406
+ }
407
+ } catch (error) {
408
+ const message = error instanceof Error ? error.message : String(error)
409
+ console.error(`Error processing template ${file}: ${message}`)
410
+ process.exit(1)
411
+ }
412
+ }
413
+
414
+ // Build output
415
+ const output: ScaffoldOutput = {
416
+ agent,
417
+ rulesPath,
418
+ agentsMdPath,
419
+ format: getOutputFormat(agent),
420
+ supportsAgentsMd: capabilities.supportsAgentsMd,
421
+ templates,
422
+ }
423
+
424
+ // Generate AGENTS.md content for agents-md format
425
+ if (agent === 'agents-md') {
426
+ output.agentsMdContent = generateAgentsMd(templates, rulesPath)
427
+ }
428
+
429
+ console.log(JSON.stringify(output, null, 2))
430
+ }
431
+
432
+ // CLI entry point
433
+ if (import.meta.main) {
434
+ await scaffoldRules(Bun.argv.slice(2))
435
+ }