@plaited/development-skills 0.4.1 → 0.6.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 (33) hide show
  1. package/{.claude → .plaited}/rules/accuracy.md +3 -10
  2. package/{.claude → .plaited}/rules/code-review.md +2 -41
  3. package/.plaited/rules/git-workflow.md +36 -0
  4. package/.plaited/rules/module-organization.md +92 -0
  5. package/{.claude → .plaited}/rules/testing.md +81 -1
  6. package/package.json +3 -2
  7. package/src/lsp-analyze.ts +2 -2
  8. package/src/lsp-find.ts +15 -34
  9. package/src/lsp-hover.ts +2 -2
  10. package/src/lsp-references.ts +2 -2
  11. package/src/lsp-symbols.ts +2 -2
  12. package/src/resolve-file-path.ts +18 -28
  13. package/src/scaffold-rules.ts +148 -204
  14. package/src/tests/resolve-file-path.spec.ts +90 -51
  15. package/src/tests/scaffold-rules.spec.ts +148 -118
  16. package/.claude/commands/lsp-analyze.md +0 -66
  17. package/.claude/commands/lsp-find.md +0 -51
  18. package/.claude/commands/lsp-hover.md +0 -48
  19. package/.claude/commands/lsp-refs.md +0 -55
  20. package/.claude/commands/scaffold-rules.md +0 -221
  21. package/.claude/commands/validate-skill.md +0 -29
  22. package/.claude/rules/git-workflow.md +0 -66
  23. package/.claude/skills/code-documentation/SKILL.md +0 -47
  24. package/.claude/skills/code-documentation/references/internal-templates.md +0 -113
  25. package/.claude/skills/code-documentation/references/maintenance.md +0 -164
  26. package/.claude/skills/code-documentation/references/public-api-templates.md +0 -100
  27. package/.claude/skills/code-documentation/references/type-documentation.md +0 -116
  28. package/.claude/skills/code-documentation/references/workflow.md +0 -60
  29. package/.claude/skills/scaffold-rules/SKILL.md +0 -97
  30. package/.claude/skills/typescript-lsp/SKILL.md +0 -239
  31. package/.claude/skills/validate-skill/SKILL.md +0 -105
  32. /package/{.claude → .plaited}/rules/bun-apis.md +0 -0
  33. /package/{.claude → .plaited}/rules/github.md +0 -0
@@ -3,73 +3,112 @@ import { join } from 'node:path'
3
3
  import { resolveFilePath } from '../resolve-file-path.ts'
4
4
 
5
5
  describe('resolveFilePath', () => {
6
- test('returns absolute path as-is', async () => {
7
- const absolutePath = '/Users/test/file.ts'
8
- const result = await resolveFilePath(absolutePath)
9
- expect(result).toBe(absolutePath)
6
+ describe('absolute paths', () => {
7
+ test('returns absolute path as-is', () => {
8
+ const absolutePath = '/Users/test/file.ts'
9
+ const result = resolveFilePath(absolutePath)
10
+ expect(result).toBe(absolutePath)
11
+ })
10
12
  })
11
13
 
12
- test('resolves relative path from cwd', async () => {
13
- const relativePath = './src/resolve-file-path.ts'
14
- const result = await resolveFilePath(relativePath)
15
- // join() normalizes paths, removing the ./
16
- expect(result).toBe(join(process.cwd(), relativePath))
17
- })
18
-
19
- test('resolves package export path via Bun.resolve', async () => {
20
- // Use typescript package which is installed as devDependency
21
- const packagePath = 'typescript'
22
- const result = await resolveFilePath(packagePath)
14
+ describe('relative paths with extension', () => {
15
+ test('resolves ./path from cwd', () => {
16
+ const relativePath = './src/resolve-file-path.ts'
17
+ const result = resolveFilePath(relativePath)
18
+ expect(result).toBe(join(process.cwd(), relativePath))
19
+ })
23
20
 
24
- // Should resolve to node_modules/typescript/...
25
- expect(result).toContain('node_modules/typescript')
26
- expect(result.startsWith('/')).toBe(true)
27
- })
21
+ test('resolves ../path from cwd', () => {
22
+ const relativePath = '../other/file.ts'
23
+ const result = resolveFilePath(relativePath)
24
+ expect(result).toBe(join(process.cwd(), relativePath))
25
+ })
28
26
 
29
- test('falls back to cwd for non-existent package', async () => {
30
- const invalidPath = 'nonexistent-package/file.ts'
31
- const result = await resolveFilePath(invalidPath)
27
+ test('resolves various file extensions', () => {
28
+ const extensions = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json']
32
29
 
33
- expect(result).toBe(join(process.cwd(), invalidPath))
30
+ for (const ext of extensions) {
31
+ const path = `./src/file.${ext}`
32
+ const result = resolveFilePath(path)
33
+ expect(result).toBe(join(process.cwd(), path))
34
+ }
35
+ })
34
36
  })
35
37
 
36
- test('resolves implicit relative path (src/foo.ts format) from cwd', async () => {
37
- // Paths that look like file paths (contain / and end with extension)
38
- // should resolve from cwd without trying Bun.resolve()
39
- const implicitRelative = 'src/utils/parser.ts'
40
- const result = await resolveFilePath(implicitRelative)
41
-
42
- expect(result).toBe(join(process.cwd(), implicitRelative))
38
+ describe('relative paths without extension', () => {
39
+ test('falls back to cwd when no exports match', () => {
40
+ // ./testing has no extension, tries Bun.resolveSync first
41
+ // Falls back to cwd since this project has no exports field
42
+ const path = './testing'
43
+ const result = resolveFilePath(path)
44
+ expect(result).toBe(join(process.cwd(), path))
45
+ })
43
46
  })
44
47
 
45
- test('resolves various file extensions as implicit relative paths', async () => {
46
- const extensions = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json']
48
+ describe('implicit relative paths', () => {
49
+ test('resolves src/foo.ts via fallback to cwd', () => {
50
+ // No ./ prefix, has extension - tries Bun.resolveSync (fails), falls back to cwd
51
+ const path = 'src/resolve-file-path.ts'
52
+ const result = resolveFilePath(path)
53
+ expect(result).toBe(join(process.cwd(), path))
54
+ })
47
55
 
48
- for (const ext of extensions) {
49
- const path = `src/file.${ext}`
50
- const result = await resolveFilePath(path)
56
+ test('resolves nested implicit path via fallback', () => {
57
+ const path = 'src/tests/fixtures/sample.ts'
58
+ const result = resolveFilePath(path)
51
59
  expect(result).toBe(join(process.cwd(), path))
52
- }
60
+ })
53
61
  })
54
62
 
55
- test('treats bare package names without extensions as package specifiers', async () => {
56
- // A path like 'typescript' (no slash, no extension) should try Bun.resolve()
57
- const barePkg = 'typescript'
58
- const result = await resolveFilePath(barePkg)
63
+ describe('bare package specifiers', () => {
64
+ test('resolves bare package name', () => {
65
+ const result = resolveFilePath('typescript')
66
+ expect(result).toContain('node_modules/typescript')
67
+ expect(result.startsWith('/')).toBe(true)
68
+ })
59
69
 
60
- // Should resolve to node_modules, not cwd/typescript
61
- expect(result).toContain('node_modules/typescript')
70
+ test('falls back to cwd for non-existent package', () => {
71
+ const invalidPath = 'nonexistent-package'
72
+ const result = resolveFilePath(invalidPath)
73
+ expect(result).toBe(join(process.cwd(), invalidPath))
74
+ })
62
75
  })
63
76
 
64
- test('treats scoped packages as package specifiers, not implicit file paths', async () => {
65
- // Scoped packages like @org/pkg/file.ts should NOT be treated as implicit
66
- // relative paths, even if they contain / and end with an extension.
67
- // They should go through Bun.resolve() first, falling back to cwd if not found.
68
- const scopedPkg = '@nonexistent/pkg/src/file.ts'
69
- const result = await resolveFilePath(scopedPkg)
77
+ describe('scoped package specifiers', () => {
78
+ test('resolves scoped package subpath export', () => {
79
+ // @modelcontextprotocol/sdk/client is a defined export
80
+ const result = resolveFilePath('@modelcontextprotocol/sdk/client')
81
+ expect(result).toContain('node_modules/@modelcontextprotocol/sdk')
82
+ expect(result).toContain('client')
83
+ })
84
+
85
+ test('resolves scoped package server subpath', () => {
86
+ const result = resolveFilePath('@modelcontextprotocol/sdk/server')
87
+ expect(result).toContain('node_modules/@modelcontextprotocol/sdk')
88
+ expect(result).toContain('server')
89
+ })
90
+
91
+ test('resolves scoped package deep subpath with extension', () => {
92
+ // Deep subpath with .js extension via wildcard export "./*"
93
+ const path = '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'
94
+ const result = resolveFilePath(path)
95
+ expect(result).toContain('node_modules/@modelcontextprotocol/sdk')
96
+ expect(result).toContain('bearerAuth')
97
+ })
98
+
99
+ test('falls back to cwd for non-existent scoped package', () => {
100
+ const scopedPkg = '@nonexistent/pkg/src/file.ts'
101
+ const result = resolveFilePath(scopedPkg)
102
+ expect(result).toBe(join(process.cwd(), scopedPkg))
103
+ })
104
+ })
70
105
 
71
- // Falls back to cwd since package doesn't exist, but importantly it tried
72
- // Bun.resolve() first (doesn't match looksLikeFilePath due to @ prefix)
73
- expect(result).toBe(join(process.cwd(), scopedPkg))
106
+ describe('package subpaths', () => {
107
+ test('resolves package subpath with extension', () => {
108
+ // typescript/lib/typescript.js is a real subpath
109
+ const result = resolveFilePath('typescript/lib/typescript.js')
110
+ expect(result).toContain('node_modules/typescript')
111
+ expect(result).toContain('typescript.js')
112
+ })
74
113
  })
75
114
  })
@@ -9,12 +9,9 @@ type Template = {
9
9
  }
10
10
 
11
11
  type ScaffoldOutput = {
12
- agent: string
13
12
  rulesPath: string
14
- agentsMdPath: string
15
- format: 'multi-file' | 'agents-md'
16
- supportsAgentsMd: boolean
17
- agentsMdContent?: string
13
+ claudeMdSection: string
14
+ agentsMdSection: string
18
15
  templates: Record<string, Template>
19
16
  }
20
17
 
@@ -22,7 +19,7 @@ const binDir = join(import.meta.dir, '../../bin')
22
19
 
23
20
  describe('scaffold-rules', () => {
24
21
  test('outputs JSON with all templates', async () => {
25
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
22
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
26
23
 
27
24
  expect(result).toHaveProperty('templates')
28
25
  expect(result.templates).toBeObject()
@@ -35,10 +32,11 @@ describe('scaffold-rules', () => {
35
32
  expect(templateKeys).toContain('git-workflow')
36
33
  expect(templateKeys).toContain('github')
37
34
  expect(templateKeys).toContain('testing')
35
+ expect(templateKeys).toContain('module-organization')
38
36
  })
39
37
 
40
38
  test('each template has required properties', async () => {
41
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
39
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
42
40
 
43
41
  for (const [ruleId, template] of Object.entries(result.templates)) {
44
42
  expect(template).toHaveProperty('filename')
@@ -52,7 +50,7 @@ describe('scaffold-rules', () => {
52
50
  })
53
51
 
54
52
  test('removes template headers from content', async () => {
55
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
53
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
56
54
 
57
55
  // Check that template headers are removed
58
56
  for (const template of Object.values(result.templates)) {
@@ -62,7 +60,7 @@ describe('scaffold-rules', () => {
62
60
  })
63
61
 
64
62
  test('processes development-skills conditionals', async () => {
65
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
63
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
66
64
 
67
65
  const accuracy = result.templates.accuracy
68
66
  expect(accuracy).toBeDefined()
@@ -78,8 +76,7 @@ describe('scaffold-rules', () => {
78
76
  })
79
77
 
80
78
  test('filters to specific rules when requested', async () => {
81
- const result: ScaffoldOutput =
82
- await $`bun ${binDir}/cli.ts scaffold-rules --rules testing --rules bun-apis --format=json`.json()
79
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --rules testing --rules bun-apis`.json()
83
80
 
84
81
  const templateKeys = Object.keys(result.templates)
85
82
 
@@ -94,7 +91,7 @@ describe('scaffold-rules', () => {
94
91
  })
95
92
 
96
93
  test('extracts meaningful descriptions', async () => {
97
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
94
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
98
95
 
99
96
  // Check a few descriptions
100
97
  const accuracy = result.templates.accuracy
@@ -108,179 +105,212 @@ describe('scaffold-rules', () => {
108
105
  expect(testing!.description.length).toBeGreaterThan(10)
109
106
  })
110
107
 
111
- test('exits with error for invalid agent', async () => {
112
- const proc = Bun.spawn(['bun', `${binDir}/cli.ts`, 'scaffold-rules', '--agent=invalid'], {
113
- stderr: 'pipe',
114
- })
115
-
116
- const exitCode = await proc.exited
117
- expect(exitCode).not.toBe(0)
118
- })
119
-
120
108
  test('handles missing bundled rules directory gracefully', async () => {
121
109
  // This test ensures the script fails gracefully if templates are missing
122
- // In production, .claude/rules/ should always be bundled with the package
123
- const result = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.nothrow().quiet()
110
+ // In production, .plaited/rules/ should always be bundled with the package
111
+ const result = await $`bun ${binDir}/cli.ts scaffold-rules`.nothrow().quiet()
124
112
 
125
- // Should succeed because .claude/rules/ exists in development
113
+ // Should succeed because .plaited/rules/ exists in development
126
114
  expect(result.exitCode).toBe(0)
127
115
  })
128
116
 
129
- describe('Claude Code target', () => {
130
- test('defaults to Claude Code format', async () => {
131
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
132
-
133
- expect(result.agent).toBe('claude')
134
- expect(result.rulesPath).toBe('.claude/rules')
135
- expect(result.format).toBe('multi-file')
136
- expect(result.supportsAgentsMd).toBe(false)
137
- })
138
-
139
- test('processes has-sandbox for Claude (sandbox environment)', async () => {
140
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=claude --format=json`.json()
141
-
142
- const gitWorkflow = result.templates['git-workflow']
143
- expect(gitWorkflow).toBeDefined()
144
-
145
- // Claude has sandbox - should include sandbox-specific content
146
- expect(gitWorkflow!.content).toContain('sandbox environment')
147
- expect(gitWorkflow!.content).toContain('single-quoted strings')
148
-
149
- // Should not have conditional syntax
150
- expect(gitWorkflow!.content).not.toContain('{{#if has-sandbox}}')
151
- expect(gitWorkflow!.content).not.toContain('{{/if}}')
152
- })
153
-
154
- test('processes supports-slash-commands for Claude', async () => {
155
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=claude --format=json`.json()
156
-
157
- const accuracy = result.templates.accuracy
158
- expect(accuracy).toBeDefined()
117
+ describe('output structure', () => {
118
+ test('defaults to .plaited/rules path', async () => {
119
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
159
120
 
160
- // Claude supports slash commands
161
- expect(accuracy!.content).toContain('/lsp-hover')
162
- expect(accuracy!.content).toContain('/lsp-find')
121
+ expect(result.rulesPath).toBe('.plaited/rules')
163
122
  })
164
123
 
165
- test('generates Claude-style cross-references', async () => {
166
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=claude --format=json`.json()
124
+ test('includes claudeMdSection with markers and @ syntax', async () => {
125
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
167
126
 
168
- const accuracy = result.templates.accuracy
169
- expect(accuracy).toBeDefined()
170
- expect(accuracy!.content).toContain('@.claude/rules/testing.md')
171
- expect(accuracy!.content).not.toContain('{{LINK:testing}}')
127
+ expect(result.claudeMdSection).toContain('<!-- PLAITED-RULES-START -->')
128
+ expect(result.claudeMdSection).toContain('<!-- PLAITED-RULES-END -->')
129
+ expect(result.claudeMdSection).toContain('@.plaited/rules/')
130
+ expect(result.claudeMdSection).toContain('## Project Rules')
172
131
  })
173
- })
174
132
 
175
- describe('AGENTS.md target (universal format)', () => {
176
- test('supports agents-md format', async () => {
177
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --format=json`.json()
133
+ test('includes agentsMdSection with markers and markdown links', async () => {
134
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
178
135
 
179
- expect(result.agent).toBe('agents-md')
180
- expect(result.rulesPath).toBe('.plaited/rules')
181
- expect(result.format).toBe('agents-md')
182
- expect(result.supportsAgentsMd).toBe(true)
136
+ expect(result.agentsMdSection).toContain('<!-- PLAITED-RULES-START -->')
137
+ expect(result.agentsMdSection).toContain('<!-- PLAITED-RULES-END -->')
138
+ expect(result.agentsMdSection).toContain('[')
139
+ expect(result.agentsMdSection).toContain('](.plaited/rules/')
140
+ expect(result.agentsMdSection).toContain('## Rules')
183
141
  })
184
142
 
185
- test('generates AGENTS.md content', async () => {
186
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --format=json`.json()
143
+ test('claudeMdSection lists all selected rules', async () => {
144
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
187
145
 
188
- expect(result.agentsMdContent).toBeDefined()
189
- expect(result.agentsMdContent).toContain('# AGENTS.md')
190
- expect(result.agentsMdContent).toContain('.plaited/rules/')
191
- expect(result.agentsMdContent).toContain('## Rules')
146
+ for (const template of Object.values(result.templates)) {
147
+ expect(result.claudeMdSection).toContain(`@.plaited/rules/${template.filename}`)
148
+ }
192
149
  })
193
150
 
194
- test('AGENTS.md links to all rule files', async () => {
195
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --format=json`.json()
151
+ test('agentsMdSection lists all selected rules', async () => {
152
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
196
153
 
197
- const agentsMd = result.agentsMdContent ?? ''
198
-
199
- // Should link to each rule file
200
154
  for (const [ruleId, template] of Object.entries(result.templates)) {
201
- expect(agentsMd).toContain(`[${ruleId}](.plaited/rules/${template.filename})`)
155
+ expect(result.agentsMdSection).toContain(`[${ruleId}](.plaited/rules/${template.filename})`)
202
156
  }
203
157
  })
158
+ })
204
159
 
205
- test('agents-md has no sandbox (uses standard commit format)', async () => {
206
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --format=json`.json()
160
+ describe('template content', () => {
161
+ test('git-workflow uses standard commit format', async () => {
162
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
207
163
 
208
164
  const gitWorkflow = result.templates['git-workflow']
209
165
  expect(gitWorkflow).toBeDefined()
210
- expect(gitWorkflow!.content).not.toContain('sandbox environment')
211
166
  expect(gitWorkflow!.content).toContain('multi-line commit')
167
+ expect(gitWorkflow!.content).toContain('git commit -m')
212
168
  })
213
169
 
214
- test('agents-md uses CLI syntax (no slash commands)', async () => {
215
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --format=json`.json()
170
+ test('accuracy uses CLI syntax for LSP tools', async () => {
171
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
216
172
 
217
173
  const accuracy = result.templates.accuracy
218
174
  expect(accuracy).toBeDefined()
219
175
 
220
- // Should use CLI instead of slash commands
176
+ // Should use CLI syntax
221
177
  expect(accuracy!.content).toContain('bunx @plaited/development-skills lsp-')
222
- expect(accuracy!.content).not.toContain('/lsp-hover')
223
178
  })
224
179
 
225
- test('generates agents-md-style cross-references', async () => {
226
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --format=json`.json()
180
+ test('cross-references use path syntax', async () => {
181
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
227
182
 
228
183
  const accuracy = result.templates.accuracy
229
184
  expect(accuracy).toBeDefined()
230
185
  expect(accuracy!.content).toContain('.plaited/rules/testing.md')
186
+ expect(accuracy!.content).not.toContain('{{LINK:testing}}')
231
187
  })
232
188
  })
233
189
 
234
190
  describe('path customization', () => {
235
- test('includes default agentsMdPath in output', async () => {
236
- const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --format=json`.json()
191
+ test('--rules-dir overrides default rules path', async () => {
192
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --rules-dir=.cursor/rules`.json()
237
193
 
238
- expect(result.agentsMdPath).toBe('AGENTS.md')
194
+ expect(result.rulesPath).toBe('.cursor/rules')
239
195
  })
240
196
 
241
- test('--rules-dir overrides default rules path', async () => {
242
- const result: ScaffoldOutput =
243
- await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --rules-dir=.cursor/rules --format=json`.json()
197
+ test('claudeMdSection uses custom path', async () => {
198
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --rules-dir=.cursor/rules`.json()
244
199
 
245
- expect(result.rulesPath).toBe('.cursor/rules')
246
- // AGENTS.md content should use custom path
247
- expect(result.agentsMdContent).toContain('.cursor/rules/')
248
- expect(result.agentsMdContent).not.toContain('.plaited/rules/')
200
+ expect(result.claudeMdSection).toContain('@.cursor/rules/')
201
+ expect(result.claudeMdSection).not.toContain('.plaited/rules/')
249
202
  })
250
203
 
251
- test('--agents-md-path overrides default AGENTS.md location', async () => {
252
- const result: ScaffoldOutput =
253
- await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --agents-md-path=docs/AGENTS.md --format=json`.json()
204
+ test('agentsMdSection uses custom path', async () => {
205
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --rules-dir=.cursor/rules`.json()
254
206
 
255
- expect(result.agentsMdPath).toBe('docs/AGENTS.md')
207
+ expect(result.agentsMdSection).toContain('.cursor/rules/')
208
+ expect(result.agentsMdSection).not.toContain('.plaited/rules/')
256
209
  })
257
210
 
258
211
  test('cross-references use custom rules-dir', async () => {
259
- const result: ScaffoldOutput =
260
- await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --rules-dir=.factory/rules --format=json`.json()
212
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --rules-dir=.factory/rules`.json()
261
213
 
262
214
  const accuracy = result.templates.accuracy
263
215
  expect(accuracy).toBeDefined()
264
216
  expect(accuracy!.content).toContain('.factory/rules/testing.md')
265
217
  })
266
218
 
267
- test('short flags work (-d and -m)', async () => {
268
- const result: ScaffoldOutput =
269
- await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md -d custom/rules -m custom/AGENTS.md --format=json`.json()
219
+ test('short flag -d works', async () => {
220
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules -d custom/rules`.json()
270
221
 
271
222
  expect(result.rulesPath).toBe('custom/rules')
272
- expect(result.agentsMdPath).toBe('custom/AGENTS.md')
273
223
  })
224
+ })
274
225
 
275
- test('claude agent also respects --rules-dir', async () => {
276
- const result: ScaffoldOutput =
277
- await $`bun ${binDir}/cli.ts scaffold-rules --agent=claude --rules-dir=.my-rules --format=json`.json()
226
+ describe('--list flag', () => {
227
+ test('outputs array of available rules', async () => {
228
+ const result = await $`bun ${binDir}/cli.ts scaffold-rules --list`.json()
229
+
230
+ expect(result).toBeArray()
231
+ expect(result.length).toBeGreaterThan(0)
232
+
233
+ // Each entry should have id and filename
234
+ for (const rule of result) {
235
+ expect(rule).toHaveProperty('id')
236
+ expect(rule).toHaveProperty('filename')
237
+ expect(rule.filename).toBe(`${rule.id}.md`)
238
+ }
239
+ })
240
+
241
+ test('includes expected rules', async () => {
242
+ const result = await $`bun ${binDir}/cli.ts scaffold-rules --list`.json()
243
+ const ids = result.map((r: { id: string }) => r.id)
244
+
245
+ expect(ids).toContain('accuracy')
246
+ expect(ids).toContain('testing')
247
+ expect(ids).toContain('bun-apis')
248
+ })
249
+
250
+ test('short flag -l works', async () => {
251
+ const result = await $`bun ${binDir}/cli.ts scaffold-rules -l`.json()
252
+
253
+ expect(result).toBeArray()
254
+ expect(result.length).toBeGreaterThan(0)
255
+ })
256
+ })
257
+
258
+ describe('--rules validation', () => {
259
+ test('warns about unknown rules', async () => {
260
+ const result = await $`bun ${binDir}/cli.ts scaffold-rules --rules nonexistent --rules testing`.nothrow()
261
+
262
+ // Should still succeed but with warning
263
+ expect(result.exitCode).toBe(0)
264
+ expect(result.stderr.toString()).toContain('Warning: Unknown rules: nonexistent')
265
+ })
266
+
267
+ test('shows available rules in warning', async () => {
268
+ const result = await $`bun ${binDir}/cli.ts scaffold-rules --rules fake-rule`.nothrow()
269
+
270
+ expect(result.stderr.toString()).toContain('Available rules:')
271
+ expect(result.stderr.toString()).toContain('testing')
272
+ })
273
+ })
274
+
275
+ describe('edge cases', () => {
276
+ test('handles all rules filtered out gracefully', async () => {
277
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --rules nonexistent`.nothrow().json()
278
+
279
+ // Should return valid output with empty templates
280
+ expect(result.templates).toEqual({})
281
+ expect(result.claudeMdSection).toContain('<!-- PLAITED-RULES-START -->')
282
+ expect(result.claudeMdSection).toContain('<!-- PLAITED-RULES-END -->')
283
+ })
284
+
285
+ test('description extraction falls back for heading-only content', async () => {
286
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
287
+
288
+ // All templates should have non-empty descriptions
289
+ for (const template of Object.values(result.templates)) {
290
+ expect(template.description).toBeTruthy()
291
+ expect(template.description.length).toBeGreaterThan(0)
292
+ }
293
+ })
294
+
295
+ test('processes nested conditionals correctly', async () => {
296
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --rules accuracy`.json()
278
297
 
279
- expect(result.rulesPath).toBe('.my-rules')
280
- // Cross-references should use custom path
281
298
  const accuracy = result.templates.accuracy
282
299
  expect(accuracy).toBeDefined()
283
- expect(accuracy!.content).toContain('@.my-rules/testing.md')
300
+
301
+ // Should not contain any unprocessed conditional syntax
302
+ expect(accuracy!.content).not.toContain('{{#if')
303
+ expect(accuracy!.content).not.toContain('{{^if')
304
+ expect(accuracy!.content).not.toContain('{{/if}}')
305
+ })
306
+
307
+ test('template content has no excessive blank lines', async () => {
308
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules`.json()
309
+
310
+ for (const template of Object.values(result.templates)) {
311
+ // Should not have 3+ consecutive newlines
312
+ expect(template.content).not.toMatch(/\n{3,}/)
313
+ }
284
314
  })
285
315
  })
286
316
  })
@@ -1,66 +0,0 @@
1
- ---
2
- description: Analyze a TypeScript file structure, exports, and symbols
3
- allowed-tools: Bash
4
- ---
5
-
6
- # LSP Analyze
7
-
8
- Batch analysis of a TypeScript/JavaScript file. Get an overview of exports, symbols, and optionally type info at specific positions.
9
-
10
- **Arguments:** $ARGUMENTS
11
-
12
- ## Usage
13
-
14
- ```
15
- /lsp-analyze <file> [options]
16
- ```
17
-
18
- Options:
19
- - `--symbols` or `-s`: List all symbols
20
- - `--exports` or `-e`: List only exported symbols (default if no options)
21
- - `--hover <line:char>`: Get type info at position (repeatable)
22
- - `--refs <line:char>`: Find references at position (repeatable)
23
- - `--all`: Run symbols + exports analysis
24
-
25
- ## Instructions
26
-
27
- ### Step 1: Parse Arguments
28
-
29
- Extract file path and options from `$ARGUMENTS`.
30
-
31
- If file is missing, show usage:
32
- ```
33
- Usage: /lsp-analyze <file> [options]
34
-
35
- Examples:
36
- /lsp-analyze src/utils/parser.ts --exports
37
- /lsp-analyze src/lib/config.ts --all
38
- /lsp-analyze src/index.ts --hover 50:10 --refs 60:5
39
- ```
40
-
41
- Default to `--exports` if no options provided.
42
-
43
- ### Step 2: Run LSP Analyze
44
-
45
- Execute the development-skills CLI command:
46
- ```bash
47
- bunx @plaited/development-skills lsp-analyze <file> [options]
48
- ```
49
-
50
- ### Step 3: Format Output
51
-
52
- Parse the JSON output and present in a structured format:
53
-
54
- **File:** `<file>`
55
-
56
- **Exports:**
57
- | Name | Kind | Line |
58
- |------|------|------|
59
- | ... | ... | ... |
60
-
61
- **Symbols:** (if requested)
62
- | Name | Kind | Line |
63
- |------|------|------|
64
- | ... | ... | ... |
65
-
66
- For hover/refs results, format as described in the individual commands.