@plaited/development-skills 0.4.0 → 0.5.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.
@@ -1,43 +1,66 @@
1
1
  ---
2
- description: Search for TypeScript symbols across the workspace by name
2
+ description: Search for TypeScript SYMBOLS (functions, types, classes) - NOT text
3
3
  allowed-tools: Bash
4
4
  ---
5
5
 
6
6
  # LSP Find
7
7
 
8
- Search for symbols (functions, types, classes, variables) across the TypeScript/JavaScript codebase.
8
+ Search for TypeScript **symbols** (functions, types, classes, variables) across the codebase.
9
9
 
10
10
  **Arguments:** $ARGUMENTS
11
11
 
12
+ ## When to Use Each Tool
13
+
14
+ | Tool | Purpose |
15
+ |------|---------|
16
+ | **Glob** | Find files by pattern |
17
+ | **Grep** | Search text content |
18
+ | **lsp-find** | Search TypeScript symbols |
19
+ | **lsp-hover** | Get type info + TSDoc documentation |
20
+
12
21
  ## Usage
13
22
 
14
23
  ```
15
- /lsp-find <query> [context-file]
24
+ /lsp-find <query> <context-file>
25
+ ```
26
+
27
+ - `query`: TypeScript symbol name (function, type, class, variable)
28
+ - `context-file`: Any `.ts` file in the project for LSP context
29
+
30
+ ## When NOT to Use
31
+
16
32
  ```
33
+ # ❌ WRONG: Searching for text (use Grep instead)
34
+ /lsp-find scaffold
35
+ /lsp-find TODO
17
36
 
18
- - `query`: Symbol name or partial name to search for
19
- - `context-file`: Optional file to open for project context
37
+ # WRONG: Missing context file
38
+ /lsp-find createClient
39
+
40
+ # ✅ CORRECT: Symbol search with context file
41
+ /lsp-find createClient src/lsp-client.ts
42
+ ```
20
43
 
21
44
  ## Instructions
22
45
 
23
46
  ### Step 1: Parse Arguments
24
47
 
25
- Extract query and optional context file from `$ARGUMENTS`.
48
+ Extract query and context file from `$ARGUMENTS`.
26
49
 
27
- If query is missing, show usage:
50
+ If either is missing, show usage:
28
51
  ```
29
- Usage: /lsp-find <query> [context-file]
52
+ Usage: /lsp-find <query> <context-file>
30
53
 
31
54
  Examples:
32
- /lsp-find bElement
33
- /lsp-find useTemplate src/main/use-template.ts
55
+ /lsp-find LspClient src/lsp-client.ts
56
+ /lsp-find createClient src/app.ts
34
57
  ```
35
58
 
36
59
  ### Step 2: Run LSP Find
37
60
 
38
61
  Execute the development-skills CLI command:
39
62
  ```bash
40
- bunx @plaited/development-skills lsp-find <query> [context-file]
63
+ bunx @plaited/development-skills lsp-find <query> <context-file>
41
64
  ```
42
65
 
43
66
  ### Step 3: Format Output
@@ -1,14 +1,23 @@
1
1
  ---
2
- description: Get TypeScript type information at a specific position in a file
2
+ description: Get TypeScript type signature + TSDoc documentation at a position
3
3
  allowed-tools: Bash
4
4
  ---
5
5
 
6
6
  # LSP Hover
7
7
 
8
- Get type information at a specific position in a TypeScript/JavaScript file.
8
+ Get type signature and TSDoc documentation at a specific position in a TypeScript/JavaScript file.
9
9
 
10
10
  **Arguments:** $ARGUMENTS
11
11
 
12
+ ## When to Use Each Tool
13
+
14
+ | Tool | Purpose |
15
+ |------|---------|
16
+ | **Glob** | Find files by pattern |
17
+ | **Grep** | Search text content |
18
+ | **lsp-find** | Search TypeScript symbols |
19
+ | **lsp-hover** | Get type info + TSDoc documentation |
20
+
12
21
  ## Usage
13
22
 
14
23
  ```
@@ -9,6 +9,15 @@ Find all references to a symbol at a specific position. Use this before modifyin
9
9
 
10
10
  **Arguments:** $ARGUMENTS
11
11
 
12
+ ## When to Use Each Tool
13
+
14
+ | Tool | Purpose |
15
+ |------|---------|
16
+ | **Glob** | Find files by pattern |
17
+ | **Grep** | Search text content |
18
+ | **lsp-find** | Search TypeScript symbols |
19
+ | **lsp-refs** | Find all references to a symbol |
20
+
12
21
  ## Usage
13
22
 
14
23
  ```
@@ -221,38 +221,6 @@ import config from './config.json'
221
221
 
222
222
  **Rationale:** Import attributes (ES2025) explicitly declare module types to the runtime. The `with { type: 'json' }` syntax is the standard (replacing the deprecated `assert` keyword). This provides runtime enforcement—if the file isn't valid JSON, the import fails.
223
223
 
224
- ## Module Organization
225
-
226
- Organize module exports into separate files by category:
227
-
228
- ```
229
- module/
230
- ├── module.types.ts # Type definitions only
231
- ├── module.schemas.ts # Zod schemas and schema-inferred types
232
- ├── module.constants.ts # Constants and enum-like objects
233
- ├── module-client.ts # Implementation
234
- └── index.ts # Public API exports (barrel file)
235
- ```
236
-
237
- ### Key Principles
238
-
239
- - **Types file**: Contains only type definitions, does NOT re-export from sibling files
240
- - **Schemas file**: Contains Zod schemas and their inferred types, does NOT export constants
241
- - **Constants file**: Contains all constant values (error codes, method names, defaults)
242
- - **Barrel file**: Uses wildcard exports; non-public files are simply not included
243
-
244
- ```typescript
245
- // ✅ Good: Direct imports from specific files
246
- import type { Config } from './module.types.ts'
247
- import { ConfigSchema } from './module.schemas.ts'
248
- import { METHODS, ERROR_CODES } from './module.constants.ts'
249
-
250
- // ❌ Avoid: Expecting types file to re-export everything
251
- import { Config, ConfigSchema, METHODS } from './module.types.ts'
252
- ```
253
-
254
- **Rationale:** Prevents circular dependencies, makes dependencies explicit, improves tree-shaking.
255
-
256
224
  ## Documentation Standards
257
225
 
258
226
  ### Mermaid Diagrams Only
@@ -0,0 +1,92 @@
1
+ <!--
2
+ RULE TEMPLATE - Distributed via /scaffold-rules
3
+ Variables: {{#if bun}}
4
+ -->
5
+
6
+ # Module Organization
7
+
8
+ ## No Index Files
9
+
10
+ **Never use `index.ts` or `index.js` files.** These create implicit magic and complicate debugging.
11
+
12
+ ### Re-export Pattern
13
+
14
+ Use named re-export files at the parent level, matching the folder name:
15
+
16
+ ```
17
+ src/
18
+ ├── acp/ # Feature module
19
+ │ ├── acp.types.ts
20
+ │ ├── acp.schemas.ts
21
+ │ └── acp.ts # Main implementation
22
+ ├── acp.ts # Re-exports public API from acp/
23
+ ├── utils/
24
+ │ └── format.ts
25
+ └── utils.ts # Re-exports public API from utils/
26
+ ```
27
+
28
+ ### Single Feature Packages
29
+
30
+ When a package has one primary feature, expose that re-export file directly as main:
31
+
32
+ ```json
33
+ {
34
+ "main": "src/acp.ts",
35
+ "exports": {
36
+ ".": "./src/acp.ts",
37
+ "./utils": "./src/utils.ts"
38
+ }
39
+ }
40
+ ```
41
+
42
+ Only use `main.ts` if the package truly has multiple co-equal entry points that need a unified export.
43
+
44
+ ## Explicit Import Extensions
45
+
46
+ {{#if bun}}
47
+ Always include `.ts` extensions in imports. Bun runs TypeScript natively—no compilation required:
48
+ {{/if}}
49
+
50
+ ```typescript
51
+ // ✅ Good
52
+ import { Config } from './module.types.ts'
53
+ import { createClient } from '../acp/acp.ts'
54
+
55
+ // ❌ Avoid
56
+ import { Config } from './module.types'
57
+ ```
58
+
59
+ **Rationale:** Explicit extensions enable direct execution, clearer module graphs, and align with ES module standards. With `allowImportingTsExtensions: true`, TypeScript supports this pattern.
60
+
61
+ ## File Organization Within Modules
62
+
63
+ ```
64
+ feature/
65
+ ├── feature.types.ts # Type definitions only
66
+ ├── feature.schemas.ts # Zod schemas + inferred types
67
+ ├── feature.constants.ts # Constants, error codes
68
+ └── feature.ts # Main implementation
69
+ ```
70
+
71
+ - **Types file**: Type definitions only, no re-exports from siblings
72
+ - **Schemas file**: Zod schemas and `z.infer<>` types
73
+ - **Constants file**: All constant values
74
+ - **Main file**: Primary implementation, named after the feature
75
+
76
+ ### Key Principles
77
+
78
+ - **Direct imports**: Import from specific files, not through re-exports within a module
79
+ - **Re-exports at boundaries**: Only use re-export files to expose public API at module boundaries
80
+ - **No circular re-exports**: Types file does NOT re-export from siblings
81
+
82
+ ```typescript
83
+ // ✅ Good: Direct imports from specific files
84
+ import type { Config } from './feature.types.ts'
85
+ import { ConfigSchema } from './feature.schemas.ts'
86
+ import { ERROR_CODES } from './feature.constants.ts'
87
+
88
+ // ❌ Avoid: Expecting one file to re-export everything
89
+ import { Config, ConfigSchema, ERROR_CODES } from './feature.types.ts'
90
+ ```
91
+
92
+ **Rationale:** Prevents circular dependencies, makes dependencies explicit, improves tree-shaking.
@@ -75,6 +75,86 @@ If a value might not exist, the test should either:
75
75
  2. Assert that it doesn't exist (if that's the expected behavior)
76
76
  3. Restructure the test to ensure the value is always present
77
77
 
78
+ ## Test Coverage Principles
79
+
80
+ ### Expansion Checklist
81
+
82
+ When writing tests, ensure coverage of:
83
+
84
+ - [ ] **Happy path** - Expected normal usage
85
+ - [ ] **Edge cases** - Empty strings, special characters, boundary values
86
+ - [ ] **Fallback paths** - What happens when try/catch catches?
87
+ - [ ] **Real dependencies** - Use installed packages (e.g., `@modelcontextprotocol/sdk`) not fake paths
88
+ - [ ] **Category organization** - Group related tests in `describe` blocks
89
+
90
+ ### Meaningful Test Coverage
91
+
92
+ Tests should verify behavior, not just exercise code. Each test should answer: "What breaks if this test fails?"
93
+
94
+ **Coverage priorities:**
95
+ 1. **Happy path** - Normal expected usage
96
+ 2. **Edge cases** - Boundary conditions, empty inputs, unusual formats
97
+ 3. **Error paths** - Invalid inputs, missing dependencies, fallback behavior
98
+ 4. **Real integrations** - Use actual installed packages when testing module resolution
99
+
100
+ ### Use Real Dependencies
101
+
102
+ When testing features that interact with packages or the file system, prefer real installed packages over mocked/fake paths:
103
+
104
+ ```typescript
105
+ // ✅ Good: Real scoped package with subpath exports
106
+ test('resolves scoped package subpath', () => {
107
+ const result = resolveFilePath('@modelcontextprotocol/sdk/client')
108
+ expect(result).toContain('node_modules/@modelcontextprotocol/sdk')
109
+ })
110
+
111
+ // ✅ Good: Real package deep subpath
112
+ test('resolves deep subpath with extension', () => {
113
+ const result = resolveFilePath('@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js')
114
+ expect(result).toContain('bearerAuth')
115
+ })
116
+ ```
117
+
118
+ ### Test Both Branches
119
+
120
+ When code has conditional logic, try/catch, or fallbacks, test both paths:
121
+
122
+ ```typescript
123
+ describe('package resolution', () => {
124
+ test('success: resolves existing package', () => {
125
+ const result = resolveFilePath('typescript')
126
+ expect(result).toContain('node_modules/typescript')
127
+ })
128
+
129
+ test('fallback: returns cwd path for non-existent package', () => {
130
+ const result = resolveFilePath('nonexistent-package')
131
+ expect(result).toBe(join(cwd, 'nonexistent-package'))
132
+ })
133
+ })
134
+ ```
135
+
136
+ ### Organize with Describe Blocks
137
+
138
+ Group tests by category for better readability and failure diagnosis:
139
+
140
+ ```typescript
141
+ describe('resolveFilePath', () => {
142
+ describe('absolute paths', () => {
143
+ test('returns as-is', () => { /* ... */ })
144
+ })
145
+
146
+ describe('relative paths with extension', () => {
147
+ test('resolves ./ paths', () => { /* ... */ })
148
+ test('resolves ../ paths', () => { /* ... */ })
149
+ })
150
+
151
+ describe('scoped package specifiers', () => {
152
+ test('resolves subpath exports', () => { /* ... */ })
153
+ test('falls back for non-existent', () => { /* ... */ })
154
+ })
155
+ })
156
+ ```
157
+
78
158
  ## Docker Integration Tests
79
159
 
80
160
  Tests that require external services or API keys can run in Docker containers for consistent, isolated execution.
@@ -66,6 +66,13 @@ Bun test runner conventions:
66
66
  - No conditionals around assertions
67
67
  - Assert existence before checking values
68
68
 
69
+ ### Module Organization
70
+ Import/export patterns for Bun/TypeScript projects:
71
+ - No `index.ts` files—use named re-export files matching folder names
72
+ - Single feature packages expose the feature file directly as main
73
+ - Explicit `.ts` extensions in all imports
74
+ - Flat re-export structure: `src/acp.ts` re-exports from `src/acp/`
75
+
69
76
  ## Merge Behavior
70
77
 
71
78
  **Always scans existing rules first.** When existing rules are found:
@@ -1,12 +1,11 @@
1
1
  ---
2
2
  name: typescript-lsp
3
- description: REQUIRED for searching code in *.ts, *.tsx, *.js, *.jsx files. Use INSTEAD of Grep for TypeScript/JavaScript - provides type-aware symbol search that understands imports, exports, and relationships. Activate before reading, editing, or searching TypeScript code to verify signatures and find references.
3
+ description: Search TypeScript SYMBOLS (functions, types, classes) - NOT text. Use Glob to find files, Grep for text search, LSP for symbol search. Provides type-aware results that understand imports, exports, and relationships.
4
4
  license: ISC
5
5
  compatibility: Requires bun
6
6
  allowed-tools: Bash
7
7
  metadata:
8
8
  file-triggers: "*.ts,*.tsx,*.js,*.jsx"
9
- replaces-tools: Grep
10
9
  ---
11
10
 
12
11
  # TypeScript LSP Skill
@@ -24,14 +23,25 @@ Use these tools to:
24
23
  - **Verify before editing** - Check all usages before modifying or deleting exports
25
24
  - **Navigate code** - Jump to definitions, find implementations
26
25
 
27
- ## When to Use LSP vs Grep/Glob
26
+ ## When to Use Each Tool
27
+
28
+ | Tool | Purpose |
29
+ |------|---------|
30
+ | **Glob** | Find files by pattern |
31
+ | **Grep** | Search text content |
32
+ | **lsp-find** | Search TypeScript symbols |
33
+ | **lsp-hover** | Get type info + TSDoc documentation |
34
+ | **lsp-refs** | Find all references to a symbol |
35
+ | **lsp-analyze** | Batch analysis of file structure |
36
+
37
+ ### LSP vs Grep/Glob
28
38
 
29
39
  | Task | Use LSP | Use Grep/Glob |
30
40
  |------|---------|---------------|
31
- | Find all usages of a function/type | ✅ `lsp-references` | ❌ Misses re-exports, aliases |
41
+ | Find all usages of a function/type | ✅ `lsp-refs` | ❌ Misses re-exports, aliases |
32
42
  | Search for a symbol by name | ✅ `lsp-find` | ❌ Matches strings, comments |
43
+ | Get type signature + TSDoc | ✅ `lsp-hover` | ❌ Not possible |
33
44
  | Understand file exports | ✅ `lsp-analyze --exports` | ❌ Doesn't resolve re-exports |
34
- | Get type signature | ✅ `lsp-hover` | ❌ Not possible |
35
45
  | Find files by pattern | ❌ | ✅ `Glob` |
36
46
  | Search non-TS files (md, json) | ❌ | ✅ `Grep` |
37
47
  | Search for text in comments/strings | ❌ | ✅ `Grep` |
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  ISC License
2
2
 
3
- Copyright (c) 2025 Plaited
3
+ Copyright (c) 2026 Plaited Labs
4
4
 
5
5
  Permission to use, copy, modify, and/or distribute this software for any
6
6
  purpose with or without fee is hereby granted, provided that the above
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # development-skills
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@plaited/development-skills.svg)](https://www.npmjs.com/package/@plaited/development-skills)
4
+ [![CI](https://github.com/plaited/acp-harness/actions/workflows/ci.yml/badge.svg)](https://github.com/plaited/development-skills/actions/workflows/ci.yml)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
6
+
7
+
3
8
  TypeScript LSP, code documentation, and validation tools. Available as both a CLI tool and as installable skills for AI coding agents.
4
9
 
5
10
  ## CLI Tool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaited/development-skills",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Development skills for Claude Code - TypeScript LSP, code documentation, and validation tools",
5
5
  "license": "ISC",
6
6
  "engines": {
@@ -48,6 +48,7 @@
48
48
  },
49
49
  "devDependencies": {
50
50
  "@biomejs/biome": "2.3.11",
51
+ "@modelcontextprotocol/sdk": "1.22.0",
51
52
  "@types/bun": "1.3.6",
52
53
  "format-package": "7.0.0",
53
54
  "lint-staged": "16.2.7",
@@ -125,7 +125,7 @@ Examples:
125
125
  console.error('Error: File path required')
126
126
  process.exit(1)
127
127
  }
128
- const absolutePath = await resolveFilePath(filePath)
128
+ const absolutePath = resolveFilePath(filePath)
129
129
  const uri = `file://${absolutePath}`
130
130
  const rootUri = `file://${process.cwd()}`
131
131
 
package/src/lsp-find.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Search for symbols across the workspace by name
3
+ * Search for TypeScript symbols across the workspace by name
4
4
  *
5
- * Usage: bun lsp-find.ts <query> [file]
5
+ * Usage: bun lsp-find.ts <query> <context-file>
6
6
  */
7
7
 
8
8
  import { parseArgs } from 'node:util'
@@ -10,25 +10,9 @@ import { LspClient } from './lsp-client.ts'
10
10
  import { resolveFilePath } from './resolve-file-path.ts'
11
11
 
12
12
  /**
13
- * Find a default context file when none is provided
13
+ * Search for TypeScript symbols across the workspace by name
14
14
  *
15
- * @remarks
16
- * Checks common TypeScript entry points in order of preference
17
- */
18
- const findDefaultContextFile = async (): Promise<string | null> => {
19
- const candidates = [`${process.cwd()}/src/index.ts`, `${process.cwd()}/src/main.ts`, `${process.cwd()}/index.ts`]
20
- for (const candidate of candidates) {
21
- if (await Bun.file(candidate).exists()) {
22
- return candidate
23
- }
24
- }
25
- return null
26
- }
27
-
28
- /**
29
- * Search for symbols across the workspace by name
30
- *
31
- * @param args - Command line arguments [query, file?]
15
+ * @param args - Command line arguments [query, context-file]
32
16
  */
33
17
  export const lspFind = async (args: string[]) => {
34
18
  const { positionals } = parseArgs({
@@ -38,10 +22,15 @@ export const lspFind = async (args: string[]) => {
38
22
 
39
23
  const [query, filePath] = positionals
40
24
 
41
- if (!query) {
42
- console.error('Usage: lsp-find <query> [file]')
43
- console.error(' query: Symbol name or partial name to search')
44
- console.error(' file: Optional file to open for project context')
25
+ if (!query || !filePath) {
26
+ console.error('Error: context-file required')
27
+ console.error('')
28
+ console.error('lsp-find searches TypeScript SYMBOLS (functions, types, classes).')
29
+ console.error(' - Use Glob to find files by pattern')
30
+ console.error(' - Use Grep to search text content')
31
+ console.error(' - Use lsp-find <query> <context-file> for symbol search')
32
+ console.error('')
33
+ console.error('Provide any .ts file as context-file (e.g., src/app.ts)')
45
34
  process.exit(1)
46
35
  }
47
36
 
@@ -51,15 +40,7 @@ export const lspFind = async (args: string[]) => {
51
40
  try {
52
41
  await client.start()
53
42
 
54
- // Open a file to establish project context if provided, otherwise find a default
55
- const contextFile = filePath ? await resolveFilePath(filePath) : await findDefaultContextFile()
56
-
57
- if (!contextFile) {
58
- console.error('Error: No context file found.')
59
- console.error('Provide a file path or ensure src/index.ts, src/main.ts, or index.ts exists.')
60
- await client.stop()
61
- process.exit(1)
62
- }
43
+ const contextFile = resolveFilePath(filePath)
63
44
 
64
45
  const file = Bun.file(contextFile)
65
46
  if (!(await file.exists())) {
package/src/lsp-hover.ts CHANGED
@@ -38,7 +38,7 @@ export const lspHover = async (args: string[]) => {
38
38
  process.exit(1)
39
39
  }
40
40
 
41
- const absolutePath = await resolveFilePath(filePath)
41
+ const absolutePath = resolveFilePath(filePath)
42
42
  const uri = `file://${absolutePath}`
43
43
  const rootUri = `file://${process.cwd()}`
44
44
 
@@ -38,7 +38,7 @@ export const lspRefs = async (args: string[]) => {
38
38
  process.exit(1)
39
39
  }
40
40
 
41
- const absolutePath = await resolveFilePath(filePath)
41
+ const absolutePath = resolveFilePath(filePath)
42
42
  const uri = `file://${absolutePath}`
43
43
  const rootUri = `file://${process.cwd()}`
44
44
 
@@ -28,7 +28,7 @@ export const lspSymbols = async (args: string[]) => {
28
28
  process.exit(1)
29
29
  }
30
30
 
31
- const absolutePath = await resolveFilePath(filePath)
31
+ const absolutePath = resolveFilePath(filePath)
32
32
  const uri = `file://${absolutePath}`
33
33
  const rootUri = `file://${process.cwd()}`
34
34
 
@@ -1,28 +1,42 @@
1
+ import { join } from 'node:path'
2
+
3
+ /**
4
+ * Check if a path has a source file extension
5
+ */
6
+ const hasSourceExtension = (path: string): boolean => /\.(tsx?|jsx?|mjs|cjs|json)$/.test(path)
7
+
1
8
  /**
2
9
  * Resolve a file path to an absolute path
3
10
  *
4
11
  * @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()
12
+ * Resolution order:
13
+ * 1. Absolute paths (starting with `/`) - returned as-is
14
+ * 2. Explicit relative with extension (`./foo.ts`, `../bar.ts`) - resolved from cwd
15
+ * 3. Everything else - try Bun.resolveSync(), fallback to cwd
16
+ *
17
+ * The "everything else" category includes:
18
+ * - Package specifiers: `@org/pkg`, `lodash`, `typescript/lib/typescript.js`
19
+ * - Relative without extension: `./testing` (for package.json exports)
20
+ * - Implicit relative: `src/foo.ts` (fails Bun.resolveSync, falls back to cwd)
9
21
  */
10
- export const resolveFilePath = async (filePath: string): Promise<string> => {
11
- // Absolute path
22
+ export const resolveFilePath = (filePath: string): string => {
23
+ const cwd = process.cwd()
24
+
25
+ // Absolute path - return as-is
12
26
  if (filePath.startsWith('/')) {
13
27
  return filePath
14
28
  }
15
29
 
16
- // Relative path from cwd
17
- if (filePath.startsWith('.')) {
18
- return `${process.cwd()}/${filePath}`
30
+ // Explicit relative with extension - resolve directly from cwd
31
+ if (filePath.startsWith('.') && hasSourceExtension(filePath)) {
32
+ return join(cwd, filePath)
19
33
  }
20
34
 
21
- // Try package export path resolution
35
+ // Everything else: try Bun.resolveSync
36
+ // Handles packages, exports field resolution, and implicit relative paths (via fallback)
22
37
  try {
23
- return await Bun.resolve(filePath, process.cwd())
38
+ return Bun.resolveSync(filePath, cwd)
24
39
  } catch {
25
- // Fall back to relative path from cwd
26
- return `${process.cwd()}/${filePath}`
40
+ return join(cwd, filePath)
27
41
  }
28
42
  }
@@ -1,33 +1,114 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
+ import { join } from 'node:path'
2
3
  import { resolveFilePath } from '../resolve-file-path.ts'
3
4
 
4
5
  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)
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
+ })
9
12
  })
10
13
 
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}`)
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
+ })
20
+
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
+ })
26
+
27
+ test('resolves various file extensions', () => {
28
+ const extensions = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json']
29
+
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
+ })
36
+ })
37
+
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
+ })
46
+ })
47
+
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
+ })
55
+
56
+ test('resolves nested implicit path via fallback', () => {
57
+ const path = 'src/tests/fixtures/sample.ts'
58
+ const result = resolveFilePath(path)
59
+ expect(result).toBe(join(process.cwd(), path))
60
+ })
15
61
  })
16
62
 
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)
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
+ })
21
69
 
22
- // Should resolve to node_modules/typescript/...
23
- expect(result).toContain('node_modules/typescript')
24
- expect(result.startsWith('/')).toBe(true)
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
+ })
25
75
  })
26
76
 
27
- test('falls back to cwd for non-existent package', async () => {
28
- const invalidPath = 'nonexistent-package/file.ts'
29
- const result = await resolveFilePath(invalidPath)
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
+ })
30
105
 
31
- expect(result).toBe(`${process.cwd()}/${invalidPath}`)
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
+ })
32
113
  })
33
114
  })