@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,27 @@
1
+ /**
2
+ * Sample fixture for LSP tests
3
+ */
4
+ export type Config = {
5
+ name: string
6
+ value: number
7
+ }
8
+
9
+ export const parseConfig = (input: string): Config => {
10
+ return { name: input, value: 42 }
11
+ }
12
+
13
+ export const validateInput = (input: unknown): input is string => {
14
+ return typeof input === 'string'
15
+ }
16
+
17
+ export class ConfigManager {
18
+ #config: Config | null = null
19
+
20
+ load(input: string): void {
21
+ this.#config = parseConfig(input)
22
+ }
23
+
24
+ get(): Config | null {
25
+ return this.#config
26
+ }
27
+ }
@@ -0,0 +1,180 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
2
+ import { LspClient } from '../lsp-client.ts'
3
+
4
+ const rootUri = `file://${process.cwd()}`
5
+ const testFile = `${import.meta.dir}/fixtures/sample.ts`
6
+ const testUri = `file://${testFile}`
7
+
8
+ describe('LspClient', () => {
9
+ let client: LspClient
10
+
11
+ beforeAll(() => {
12
+ client = new LspClient({ rootUri })
13
+ })
14
+
15
+ afterAll(async () => {
16
+ if (client.isRunning()) {
17
+ await client.stop()
18
+ }
19
+ })
20
+
21
+ test('initializes with rootUri', () => {
22
+ expect(client).toBeDefined()
23
+ expect(client.isRunning()).toBe(false)
24
+ })
25
+
26
+ test('starts and stops LSP server', async () => {
27
+ await client.start()
28
+ expect(client.isRunning()).toBe(true)
29
+
30
+ await client.stop()
31
+ expect(client.isRunning()).toBe(false)
32
+ })
33
+
34
+ test('throws when starting already running server', async () => {
35
+ await client.start()
36
+ expect(client.isRunning()).toBe(true)
37
+
38
+ await expect(client.start()).rejects.toThrow('LSP server already running')
39
+
40
+ await client.stop()
41
+ })
42
+
43
+ test('handles stop on non-running server gracefully', async () => {
44
+ expect(client.isRunning()).toBe(false)
45
+ await client.stop()
46
+ expect(client.isRunning()).toBe(false)
47
+ })
48
+
49
+ describe('LSP operations', () => {
50
+ beforeAll(async () => {
51
+ await client.start()
52
+ })
53
+
54
+ afterAll(async () => {
55
+ await client.stop()
56
+ })
57
+
58
+ test('opens and closes document', async () => {
59
+ const text = await Bun.file(testFile).text()
60
+
61
+ client.openDocument(testUri, 'typescript', 1, text)
62
+ client.closeDocument(testUri)
63
+ })
64
+
65
+ test('gets hover information', async () => {
66
+ const text = await Bun.file(testFile).text()
67
+
68
+ client.openDocument(testUri, 'typescript', 1, text)
69
+
70
+ // Find 'export' keyword position for reliable hover
71
+ const lines = text.split('\n')
72
+ let line = 0
73
+ let char = 0
74
+ for (let i = 0; i < lines.length; i++) {
75
+ if (lines[i]?.startsWith('export')) {
76
+ line = i
77
+ char = 0
78
+ break
79
+ }
80
+ }
81
+
82
+ const result = await client.hover(testUri, line, char)
83
+ expect(result).toBeDefined()
84
+
85
+ client.closeDocument(testUri)
86
+ })
87
+
88
+ test('gets document symbols', async () => {
89
+ const text = await Bun.file(testFile).text()
90
+
91
+ client.openDocument(testUri, 'typescript', 1, text)
92
+
93
+ const result = await client.documentSymbols(testUri)
94
+
95
+ expect(result).toBeDefined()
96
+ expect(Array.isArray(result)).toBe(true)
97
+
98
+ client.closeDocument(testUri)
99
+ })
100
+
101
+ test('searches workspace symbols', async () => {
102
+ // Open a document first so LSP has a project context
103
+ const text = await Bun.file(testFile).text()
104
+ client.openDocument(testUri, 'typescript', 1, text)
105
+
106
+ const result = await client.workspaceSymbols('parseConfig')
107
+
108
+ expect(result).toBeDefined()
109
+ expect(Array.isArray(result)).toBe(true)
110
+
111
+ client.closeDocument(testUri)
112
+ })
113
+
114
+ test('finds references', async () => {
115
+ const text = await Bun.file(testFile).text()
116
+
117
+ client.openDocument(testUri, 'typescript', 1, text)
118
+
119
+ // Find an exported symbol
120
+ const lines = text.split('\n')
121
+ let line = 0
122
+ let char = 0
123
+ for (let i = 0; i < lines.length; i++) {
124
+ const currentLine = lines[i]
125
+ if (!currentLine) continue
126
+ const match = currentLine.match(/export\s+const\s+(\w+)/)
127
+ if (match?.[1]) {
128
+ line = i
129
+ char = currentLine.indexOf(match[1])
130
+ break
131
+ }
132
+ }
133
+
134
+ const result = await client.references(testUri, line, char)
135
+ expect(result).toBeDefined()
136
+
137
+ client.closeDocument(testUri)
138
+ })
139
+
140
+ test('gets definition', async () => {
141
+ const text = await Bun.file(testFile).text()
142
+
143
+ client.openDocument(testUri, 'typescript', 1, text)
144
+
145
+ // Find an import to get definition for
146
+ const lines = text.split('\n')
147
+ let line = 0
148
+ let char = 0
149
+ for (let i = 0; i < lines.length; i++) {
150
+ const currentLine = lines[i]
151
+ if (!currentLine) continue
152
+ const match = currentLine.match(/import\s+.*{\s*(\w+)/)
153
+ if (match?.[1]) {
154
+ line = i
155
+ char = currentLine.indexOf(match[1])
156
+ break
157
+ }
158
+ }
159
+
160
+ const result = await client.definition(testUri, line, char)
161
+ expect(result).toBeDefined()
162
+
163
+ client.closeDocument(testUri)
164
+ })
165
+ })
166
+
167
+ describe('error handling', () => {
168
+ test('throws on request when server not running', async () => {
169
+ const notRunningClient = new LspClient({ rootUri })
170
+
171
+ await expect(notRunningClient.hover('file:///test.ts', 0, 0)).rejects.toThrow('LSP server not running')
172
+ })
173
+
174
+ test('throws on notify when server not running', () => {
175
+ const notRunningClient = new LspClient({ rootUri })
176
+
177
+ expect(() => notRunningClient.notify('test')).toThrow('LSP server not running')
178
+ })
179
+ })
180
+ })
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { resolveFilePath } from '../resolve-file-path.ts'
3
+
4
+ describe('resolveFilePath', () => {
5
+ test('returns absolute path as-is', async () => {
6
+ const absolutePath = '/Users/test/file.ts'
7
+ const result = await resolveFilePath(absolutePath)
8
+ expect(result).toBe(absolutePath)
9
+ })
10
+
11
+ test('resolves relative path from cwd', async () => {
12
+ const relativePath = './plugin/skills/typescript-lsp/scripts/tests/fixtures/sample.ts'
13
+ const result = await resolveFilePath(relativePath)
14
+ expect(result).toBe(`${process.cwd()}/${relativePath}`)
15
+ })
16
+
17
+ test('resolves package export path via Bun.resolve', async () => {
18
+ // Use typescript package which is installed as devDependency
19
+ const packagePath = 'typescript'
20
+ const result = await resolveFilePath(packagePath)
21
+
22
+ // Should resolve to node_modules/typescript/...
23
+ expect(result).toContain('node_modules/typescript')
24
+ expect(result.startsWith('/')).toBe(true)
25
+ })
26
+
27
+ test('falls back to cwd for non-existent package', async () => {
28
+ const invalidPath = 'nonexistent-package/file.ts'
29
+ const result = await resolveFilePath(invalidPath)
30
+
31
+ expect(result).toBe(`${process.cwd()}/${invalidPath}`)
32
+ })
33
+ })
@@ -0,0 +1,286 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { join } from 'node:path'
3
+ import { $ } from 'bun'
4
+
5
+ type Template = {
6
+ filename: string
7
+ content: string
8
+ description: string
9
+ }
10
+
11
+ type ScaffoldOutput = {
12
+ agent: string
13
+ rulesPath: string
14
+ agentsMdPath: string
15
+ format: 'multi-file' | 'agents-md'
16
+ supportsAgentsMd: boolean
17
+ agentsMdContent?: string
18
+ templates: Record<string, Template>
19
+ }
20
+
21
+ const binDir = join(import.meta.dir, '../../bin')
22
+
23
+ describe('scaffold-rules', () => {
24
+ test('outputs JSON with all templates', async () => {
25
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
26
+
27
+ expect(result).toHaveProperty('templates')
28
+ expect(result.templates).toBeObject()
29
+
30
+ // Check that we have the expected templates
31
+ const templateKeys = Object.keys(result.templates)
32
+ expect(templateKeys).toContain('accuracy')
33
+ expect(templateKeys).toContain('bun-apis')
34
+ expect(templateKeys).toContain('code-review')
35
+ expect(templateKeys).toContain('git-workflow')
36
+ expect(templateKeys).toContain('github')
37
+ expect(templateKeys).toContain('testing')
38
+ })
39
+
40
+ test('each template has required properties', async () => {
41
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
42
+
43
+ for (const [ruleId, template] of Object.entries(result.templates)) {
44
+ expect(template).toHaveProperty('filename')
45
+ expect(template).toHaveProperty('content')
46
+ expect(template).toHaveProperty('description')
47
+
48
+ expect(template.filename).toBe(`${ruleId}.md`)
49
+ expect(template.content).toBeString()
50
+ expect(template.content.length).toBeGreaterThan(0)
51
+ }
52
+ })
53
+
54
+ test('removes template headers from content', async () => {
55
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
56
+
57
+ // Check that template headers are removed
58
+ for (const template of Object.values(result.templates)) {
59
+ expect(template.content).not.toContain('<!-- RULE TEMPLATE')
60
+ expect(template.content).not.toContain('Variables:')
61
+ }
62
+ })
63
+
64
+ test('processes development-skills conditionals', async () => {
65
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
66
+
67
+ const accuracy = result.templates.accuracy
68
+ expect(accuracy).toBeDefined()
69
+
70
+ // Should include development-skills content (always true when using CLI)
71
+ expect(accuracy!.content).toContain('TypeScript/JavaScript projects')
72
+ expect(accuracy!.content).toContain('lsp-find')
73
+ expect(accuracy!.content).toContain('lsp-hover')
74
+
75
+ // Should not have conditional syntax
76
+ expect(accuracy!.content).not.toContain('{{#if development-skills}}')
77
+ expect(accuracy!.content).not.toContain('{{/if}}')
78
+ })
79
+
80
+ 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()
83
+
84
+ const templateKeys = Object.keys(result.templates)
85
+
86
+ // Should only include requested rules
87
+ expect(templateKeys).toHaveLength(2)
88
+ expect(templateKeys).toContain('testing')
89
+ expect(templateKeys).toContain('bun-apis')
90
+
91
+ // Should not include other rules
92
+ expect(templateKeys).not.toContain('accuracy')
93
+ expect(templateKeys).not.toContain('git-workflow')
94
+ })
95
+
96
+ test('extracts meaningful descriptions', async () => {
97
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --format=json`.json()
98
+
99
+ // Check a few descriptions
100
+ const accuracy = result.templates.accuracy
101
+ expect(accuracy).toBeDefined()
102
+ expect(accuracy!.description).toBeString()
103
+ expect(accuracy!.description.length).toBeGreaterThan(10)
104
+
105
+ const testing = result.templates.testing
106
+ expect(testing).toBeDefined()
107
+ expect(testing!.description).toBeString()
108
+ expect(testing!.description.length).toBeGreaterThan(10)
109
+ })
110
+
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
+ test('handles missing bundled rules directory gracefully', async () => {
121
+ // 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()
124
+
125
+ // Should succeed because .claude/rules/ exists in development
126
+ expect(result.exitCode).toBe(0)
127
+ })
128
+
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()
159
+
160
+ // Claude supports slash commands
161
+ expect(accuracy!.content).toContain('/lsp-hover')
162
+ expect(accuracy!.content).toContain('/lsp-find')
163
+ })
164
+
165
+ test('generates Claude-style cross-references', async () => {
166
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=claude --format=json`.json()
167
+
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}}')
172
+ })
173
+ })
174
+
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()
178
+
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)
183
+ })
184
+
185
+ test('generates AGENTS.md content', async () => {
186
+ const result: ScaffoldOutput = await $`bun ${binDir}/cli.ts scaffold-rules --agent=agents-md --format=json`.json()
187
+
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')
192
+ })
193
+
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()
196
+
197
+ const agentsMd = result.agentsMdContent ?? ''
198
+
199
+ // Should link to each rule file
200
+ for (const [ruleId, template] of Object.entries(result.templates)) {
201
+ expect(agentsMd).toContain(`[${ruleId}](.plaited/rules/${template.filename})`)
202
+ }
203
+ })
204
+
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()
207
+
208
+ const gitWorkflow = result.templates['git-workflow']
209
+ expect(gitWorkflow).toBeDefined()
210
+ expect(gitWorkflow!.content).not.toContain('sandbox environment')
211
+ expect(gitWorkflow!.content).toContain('multi-line commit')
212
+ })
213
+
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()
216
+
217
+ const accuracy = result.templates.accuracy
218
+ expect(accuracy).toBeDefined()
219
+
220
+ // Should use CLI instead of slash commands
221
+ expect(accuracy!.content).toContain('bunx @plaited/development-skills lsp-')
222
+ expect(accuracy!.content).not.toContain('/lsp-hover')
223
+ })
224
+
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()
227
+
228
+ const accuracy = result.templates.accuracy
229
+ expect(accuracy).toBeDefined()
230
+ expect(accuracy!.content).toContain('.plaited/rules/testing.md')
231
+ })
232
+ })
233
+
234
+ 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()
237
+
238
+ expect(result.agentsMdPath).toBe('AGENTS.md')
239
+ })
240
+
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()
244
+
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/')
249
+ })
250
+
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()
254
+
255
+ expect(result.agentsMdPath).toBe('docs/AGENTS.md')
256
+ })
257
+
258
+ 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()
261
+
262
+ const accuracy = result.templates.accuracy
263
+ expect(accuracy).toBeDefined()
264
+ expect(accuracy!.content).toContain('.factory/rules/testing.md')
265
+ })
266
+
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()
270
+
271
+ expect(result.rulesPath).toBe('custom/rules')
272
+ expect(result.agentsMdPath).toBe('custom/AGENTS.md')
273
+ })
274
+
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()
278
+
279
+ expect(result.rulesPath).toBe('.my-rules')
280
+ // Cross-references should use custom path
281
+ const accuracy = result.templates.accuracy
282
+ expect(accuracy).toBeDefined()
283
+ expect(accuracy!.content).toContain('@.my-rules/testing.md')
284
+ })
285
+ })
286
+ })