| 1 | /** |
| 2 | * How one composition row's `name` reaches a module. |
| 3 | * |
| 4 | * A preset composition is read by `Include`, which rewrites its context's |
| 5 | * `baseUrl` to the composition's own directory. That is right for a row |
| 6 | * naming a file the preset ships and wrong for a row naming a package: a |
| 7 | * locally authored preset lives under the user's home, where Node's upward |
| 8 | * `node_modules` walk never reaches the harness's own dependencies. Both the |
| 9 | * mount's import override and discovery's health check therefore have to |
| 10 | * classify a row's name before they can act on it, and they must classify it |
| 11 | * the same way — a row discovery resolves from one base and the mount imports |
| 12 | * from another would be reported healthy and then fail to load. |
| 13 | * @module @deepseek-ai/dsh-agent-presets/specifier |
| 14 | */ |
| 15 | |
| 16 | import { isAbsolute } from 'node:path' |
| 17 | import { pathToFileURL } from 'node:url' |
| 18 | |
| 19 | /** One composition row's module specifier, classified by where it resolves. */ |
| 20 | export type RowSpecifier = |
| 21 | /** A `cordis:` builtin the Loader supplies; nothing is resolved. */ |
| 22 | | { readonly kind: 'builtin'; readonly specifier: string } |
| 23 | /** A path relative to the preset's own directory; the preset ships the file. */ |
| 24 | | { readonly kind: 'preset'; readonly specifier: string } |
| 25 | /** An absolute path or `file:` URL; it names one file and no base. */ |
| 26 | | { readonly kind: 'file'; readonly specifier: string } |
| 27 | /** A package name resolved from the installed harness. */ |
| 28 | | { readonly kind: 'package'; readonly specifier: string } |
| 29 | |
| 30 | /** |
| 31 | * Classify one row's `name`. |
| 32 | * |
| 33 | * An absolute filesystem path becomes a file URL here rather than at each |
| 34 | * call site, because Node's ESM resolver rejects a bare drive-letter path on |
| 35 | * Windows. A `file:` URL is already one and joins it: the Loader accepts both |
| 36 | * spellings for the same thing, and treating the URL as a package name would |
| 37 | * hand it to a resolver that only normalizes it, reporting a file that is not |
| 38 | * there as present. The `specifier` a caller receives is always the string to |
| 39 | * hand a resolver; only `kind` decides which base it goes with. |
| 40 | * @param name - the module specifier exactly as the row wrote it. |
| 41 | * @returns the classification, carrying the specifier to resolve. |
| 42 | */ |
| 43 | export function classifyRowSpecifier(name: string): RowSpecifier { |
| 44 | if (name.startsWith('cordis:')) return { kind: 'builtin', specifier: name } |
| 45 | if (name.startsWith('.')) return { kind: 'preset', specifier: name } |
| 46 | if (name.startsWith('file:')) return { kind: 'file', specifier: name } |
| 47 | if (isAbsolute(name)) return { kind: 'file', specifier: pathToFileURL(name).href } |
| 48 | return { kind: 'package', specifier: name } |
| 49 | } |
| 50 |