@vibe-agent-toolkit/resources 0.1.3 → 0.1.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 (87) hide show
  1. package/README.md +0 -17
  2. package/dist/collection-matcher.d.ts +63 -0
  3. package/dist/collection-matcher.d.ts.map +1 -0
  4. package/dist/collection-matcher.js +127 -0
  5. package/dist/collection-matcher.js.map +1 -0
  6. package/dist/config-parser.d.ts +63 -0
  7. package/dist/config-parser.d.ts.map +1 -0
  8. package/dist/config-parser.js +113 -0
  9. package/dist/config-parser.js.map +1 -0
  10. package/dist/frontmatter-validator.d.ts +12 -2
  11. package/dist/frontmatter-validator.d.ts.map +1 -1
  12. package/dist/frontmatter-validator.js +174 -18
  13. package/dist/frontmatter-validator.js.map +1 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/link-validator.d.ts +25 -3
  19. package/dist/link-validator.d.ts.map +1 -1
  20. package/dist/link-validator.js +75 -49
  21. package/dist/link-validator.js.map +1 -1
  22. package/dist/multi-schema-validator.d.ts +42 -0
  23. package/dist/multi-schema-validator.d.ts.map +1 -0
  24. package/dist/multi-schema-validator.js +107 -0
  25. package/dist/multi-schema-validator.js.map +1 -0
  26. package/dist/pattern-expander.d.ts +63 -0
  27. package/dist/pattern-expander.d.ts.map +1 -0
  28. package/dist/pattern-expander.js +93 -0
  29. package/dist/pattern-expander.js.map +1 -0
  30. package/dist/resource-registry.d.ts +87 -6
  31. package/dist/resource-registry.d.ts.map +1 -1
  32. package/dist/resource-registry.js +215 -46
  33. package/dist/resource-registry.js.map +1 -1
  34. package/dist/schema-assignment.d.ts +49 -0
  35. package/dist/schema-assignment.d.ts.map +1 -0
  36. package/dist/schema-assignment.js +95 -0
  37. package/dist/schema-assignment.js.map +1 -0
  38. package/dist/schemas/project-config.d.ts +254 -0
  39. package/dist/schemas/project-config.d.ts.map +1 -0
  40. package/dist/schemas/project-config.js +57 -0
  41. package/dist/schemas/project-config.js.map +1 -0
  42. package/dist/schemas/resource-metadata.d.ts +3 -0
  43. package/dist/schemas/resource-metadata.d.ts.map +1 -1
  44. package/dist/schemas/resource-metadata.js +2 -0
  45. package/dist/schemas/resource-metadata.js.map +1 -1
  46. package/dist/schemas/validation-result.d.ts +2 -26
  47. package/dist/schemas/validation-result.d.ts.map +1 -1
  48. package/dist/schemas/validation-result.js +4 -20
  49. package/dist/schemas/validation-result.js.map +1 -1
  50. package/dist/types/resource-parser.d.ts +53 -0
  51. package/dist/types/resource-parser.d.ts.map +1 -0
  52. package/dist/types/resource-parser.js +233 -0
  53. package/dist/types/resource-parser.js.map +1 -0
  54. package/dist/types/resource-path-utils.d.ts +43 -0
  55. package/dist/types/resource-path-utils.d.ts.map +1 -0
  56. package/dist/types/resource-path-utils.js +89 -0
  57. package/dist/types/resource-path-utils.js.map +1 -0
  58. package/dist/types/resources.d.ts +140 -0
  59. package/dist/types/resources.d.ts.map +1 -0
  60. package/dist/types/resources.js +58 -0
  61. package/dist/types/resources.js.map +1 -0
  62. package/dist/types.d.ts +14 -2
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js +17 -0
  65. package/dist/types.js.map +1 -1
  66. package/dist/utils.d.ts +18 -0
  67. package/dist/utils.d.ts.map +1 -1
  68. package/dist/utils.js +39 -0
  69. package/dist/utils.js.map +1 -1
  70. package/package.json +2 -2
  71. package/src/collection-matcher.ts +148 -0
  72. package/src/config-parser.ts +125 -0
  73. package/src/frontmatter-validator.ts +202 -18
  74. package/src/index.ts +7 -2
  75. package/src/link-validator.ts +100 -51
  76. package/src/multi-schema-validator.ts +128 -0
  77. package/src/pattern-expander.ts +100 -0
  78. package/src/resource-registry.ts +322 -54
  79. package/src/schema-assignment.ts +119 -0
  80. package/src/schemas/project-config.ts +71 -0
  81. package/src/schemas/resource-metadata.ts +2 -0
  82. package/src/schemas/validation-result.ts +4 -23
  83. package/src/types/resource-parser.ts +302 -0
  84. package/src/types/resource-path-utils.ts +102 -0
  85. package/src/types/resources.ts +211 -0
  86. package/src/types.ts +81 -1
  87. package/src/utils.ts +43 -0
@@ -2,21 +2,42 @@
2
2
  * Link validation for markdown resources.
3
3
  *
4
4
  * Validates different types of links:
5
- * - local_file: Checks if file exists, validates anchors if present
5
+ * - local_file: Checks if file exists, validates anchors if present, checks git-ignore safety
6
6
  * - anchor: Validates heading exists in current or target file
7
7
  * - external: Returns info (not validated)
8
8
  * - email: Returns null (valid by default)
9
9
  * - unknown: Returns warning
10
+ *
11
+ * Git-ignore safety (Phase 3):
12
+ * - Non-ignored files cannot link to ignored files (error: link_to_gitignored)
13
+ * - Ignored files CAN link to ignored files (no error)
14
+ * - Ignored files CAN link to non-ignored files (no error)
15
+ * - External resources (outside project) skip git-ignore checks
10
16
  */
11
17
 
12
- import fs from 'node:fs/promises';
13
18
  import path from 'node:path';
14
19
 
15
- import { isGitignored } from '@vibe-agent-toolkit/utils';
20
+ import {
21
+ isGitIgnored,
22
+ type GitTracker,
23
+ verifyCaseSensitiveFilename,
24
+ } from '@vibe-agent-toolkit/utils';
16
25
 
17
26
  import type { ValidationIssue } from './schemas/validation-result.js';
18
27
  import type { HeadingNode, ResourceLink } from './types.js';
19
- import { splitHrefAnchor } from './utils.js';
28
+ import { isWithinProject, splitHrefAnchor } from './utils.js';
29
+
30
+ /**
31
+ * Options for link validation.
32
+ */
33
+ export interface ValidateLinkOptions {
34
+ /** Project root directory (for git-ignore checking) */
35
+ projectRoot?: string;
36
+ /** Skip git-ignore checks (optimization when checkGitIgnored is false) */
37
+ skipGitIgnoreCheck?: boolean;
38
+ /** Git tracker for efficient git-ignore checking (optional, improves performance) */
39
+ gitTracker?: GitTracker;
40
+ }
20
41
 
21
42
  /**
22
43
  * Validate a single link in a markdown resource.
@@ -24,11 +45,15 @@ import { splitHrefAnchor } from './utils.js';
24
45
  * @param link - The link to validate
25
46
  * @param sourceFilePath - Absolute path to the file containing the link
26
47
  * @param headingsByFile - Map of file paths to their heading trees
48
+ * @param options - Validation options (projectRoot, skipGitIgnoreCheck)
27
49
  * @returns ValidationIssue if link is broken, null if valid
28
50
  *
29
51
  * @example
30
52
  * ```typescript
31
- * const issue = await validateLink(link, '/project/docs/guide.md', headingsMap);
53
+ * const issue = await validateLink(link, '/project/docs/guide.md', headingsMap, {
54
+ * projectRoot: '/project',
55
+ * skipGitIgnoreCheck: false
56
+ * });
32
57
  * if (issue) {
33
58
  * console.log(`${issue.severity}: ${issue.message}`);
34
59
  * }
@@ -37,25 +62,19 @@ import { splitHrefAnchor } from './utils.js';
37
62
  export async function validateLink(
38
63
  link: ResourceLink,
39
64
  sourceFilePath: string,
40
- headingsByFile: Map<string, HeadingNode[]>
65
+ headingsByFile: Map<string, HeadingNode[]>,
66
+ options?: ValidateLinkOptions
41
67
  ): Promise<ValidationIssue | null> {
42
68
  switch (link.type) {
43
69
  case 'local_file':
44
- return await validateLocalFileLink(link, sourceFilePath, headingsByFile);
70
+ return await validateLocalFileLink(link, sourceFilePath, headingsByFile, options);
45
71
 
46
72
  case 'anchor':
47
73
  return await validateAnchorLink(link, sourceFilePath, headingsByFile);
48
74
 
49
75
  case 'external':
50
- // External URLs are not validated - return info
51
- return {
52
- severity: 'info',
53
- resourcePath: sourceFilePath,
54
- line: link.line,
55
- type: 'external_url',
56
- link: link.href,
57
- message: 'External URL not validated',
58
- };
76
+ // External URLs are not validated - don't report them
77
+ return null;
59
78
 
60
79
  case 'email':
61
80
  // Email links are valid by default
@@ -63,7 +82,6 @@ export async function validateLink(
63
82
 
64
83
  case 'unknown':
65
84
  return {
66
- severity: 'warning',
67
85
  resourcePath: sourceFilePath,
68
86
  line: link.line,
69
87
  type: 'unknown_link',
@@ -85,7 +103,8 @@ export async function validateLink(
85
103
  async function validateLocalFileLink(
86
104
  link: ResourceLink,
87
105
  sourceFilePath: string,
88
- headingsByFile: Map<string, HeadingNode[]>
106
+ headingsByFile: Map<string, HeadingNode[]>,
107
+ options?: ValidateLinkOptions
89
108
  ): Promise<ValidationIssue | null> {
90
109
  // Extract file path and anchor from href
91
110
  const [filePath, anchor] = splitHrefAnchor(link.href);
@@ -94,29 +113,58 @@ async function validateLocalFileLink(
94
113
  const fileResult = await validateLocalFile(filePath, sourceFilePath);
95
114
 
96
115
  if (!fileResult.exists) {
116
+ // Check if it's a case mismatch
117
+ if (fileResult.actualName) {
118
+ const expectedName = path.basename(fileResult.resolvedPath);
119
+ return {
120
+ resourcePath: sourceFilePath,
121
+ line: link.line,
122
+ type: 'broken_file',
123
+ link: link.href,
124
+ message: `File found but case mismatch: expected "${expectedName}" but found "${fileResult.actualName}". This will fail on case-sensitive filesystems (Linux). Update the link to match the actual filename.`,
125
+ suggestion: `Use "${fileResult.actualName}" instead of "${expectedName}"`,
126
+ };
127
+ }
128
+
97
129
  return {
98
- severity: 'error',
99
130
  resourcePath: sourceFilePath,
100
131
  line: link.line,
101
132
  type: 'broken_file',
102
133
  link: link.href,
103
134
  message: `File not found: ${fileResult.resolvedPath}`,
104
- suggestion: 'Check that the file path is correct and the file exists',
135
+ suggestion: '',
105
136
  };
106
137
  }
107
138
 
108
- // Check if the file is gitignored
109
- if (fileResult.isGitignored) {
110
- return {
111
- severity: 'error',
112
- resourcePath: sourceFilePath,
113
- line: link.line,
114
- type: 'broken_file',
115
- link: link.href,
116
- message: `File is gitignored: ${fileResult.resolvedPath}`,
117
- suggestion:
118
- 'Gitignored files are local-only and will not exist in the repository. Remove this link or unignore the target file.',
119
- };
139
+ // Check git-ignore safety (Phase 3)
140
+ // Only check if:
141
+ // 1. skipGitIgnoreCheck is NOT true
142
+ // 2. projectRoot is provided
143
+ // 3. target is within project (skip for external resources)
144
+ if (
145
+ options?.skipGitIgnoreCheck !== true &&
146
+ options?.projectRoot !== undefined &&
147
+ isWithinProject(fileResult.resolvedPath, options.projectRoot)
148
+ ) {
149
+ // Use GitTracker if available (cached), otherwise fall back to isGitIgnored
150
+ const sourceIsIgnored = options.gitTracker
151
+ ? options.gitTracker.isIgnored(sourceFilePath)
152
+ : isGitIgnored(sourceFilePath, options.projectRoot);
153
+ const targetIsIgnored = options.gitTracker
154
+ ? options.gitTracker.isIgnored(fileResult.resolvedPath)
155
+ : isGitIgnored(fileResult.resolvedPath, options.projectRoot);
156
+
157
+ // Error ONLY if: source is NOT ignored AND target IS ignored
158
+ if (!sourceIsIgnored && targetIsIgnored) {
159
+ return {
160
+ resourcePath: sourceFilePath,
161
+ line: link.line,
162
+ type: 'link_to_gitignored',
163
+ link: link.href,
164
+ message: `Non-ignored file links to gitignored file: ${fileResult.resolvedPath}. Gitignored files are local-only and will not exist in the repository. Remove this link or unignore the target file.`,
165
+ suggestion: '',
166
+ };
167
+ }
120
168
  }
121
169
 
122
170
  // If there's an anchor, validate it too
@@ -129,13 +177,12 @@ async function validateLocalFileLink(
129
177
 
130
178
  if (!anchorValid) {
131
179
  return {
132
- severity: 'error',
133
180
  resourcePath: sourceFilePath,
134
181
  line: link.line,
135
182
  type: 'broken_anchor',
136
183
  link: link.href,
137
184
  message: `Anchor not found: #${anchor} in ${fileResult.resolvedPath}`,
138
- suggestion: 'Check that the heading exists in the target file',
185
+ suggestion: '',
139
186
  };
140
187
  }
141
188
  }
@@ -159,13 +206,12 @@ async function validateAnchorLink(
159
206
 
160
207
  if (!isValid) {
161
208
  return {
162
- severity: 'error',
163
209
  resourcePath: sourceFilePath,
164
210
  line: link.line,
165
211
  type: 'broken_anchor',
166
212
  link: link.href,
167
213
  message: `Anchor not found: ${link.href}`,
168
- suggestion: 'Check that the heading exists in this file',
214
+ suggestion: '',
169
215
  };
170
216
  }
171
217
 
@@ -174,41 +220,44 @@ async function validateAnchorLink(
174
220
 
175
221
 
176
222
  /**
177
- * Validate that a local file exists and is not gitignored.
223
+ * Validate that a local file exists with the correct case.
178
224
  *
179
225
  * @param href - The href to the file (relative or absolute)
180
226
  * @param sourceFilePath - Absolute path to the source file
181
- * @returns Object with exists flag, resolved absolute path, and gitignored flag
227
+ * @returns Object with exists flag, resolved absolute path, and optional case mismatch info
182
228
  *
183
229
  * @example
184
230
  * ```typescript
185
231
  * const result = await validateLocalFile('./docs/guide.md', '/project/README.md');
186
- * if (result.exists && !result.isGitignored) {
232
+ * if (result.exists) {
187
233
  * console.log('File exists at:', result.resolvedPath);
234
+ * } else if (result.actualName) {
235
+ * console.log('Case mismatch:', result.actualName);
188
236
  * }
189
237
  * ```
190
238
  */
191
239
  async function validateLocalFile(
192
240
  href: string,
193
241
  sourceFilePath: string
194
- ): Promise<{ exists: boolean; resolvedPath: string; isGitignored: boolean }> {
242
+ ): Promise<{ exists: boolean; resolvedPath: string; actualName?: string }> {
195
243
  // Resolve the path relative to the source file's directory
196
244
  const sourceDir = path.dirname(sourceFilePath);
197
245
  const resolvedPath = path.resolve(sourceDir, href);
198
246
 
199
- // Check if file exists
200
- let exists = false;
201
- try {
202
- await fs.access(resolvedPath, fs.constants.F_OK);
203
- exists = true;
204
- } catch {
205
- exists = false;
206
- }
247
+ // Check if file exists with correct case
248
+ const verification = await verifyCaseSensitiveFilename(resolvedPath);
207
249
 
208
- // Check if file is gitignored (only if it exists)
209
- const gitignored = exists && isGitignored(resolvedPath);
250
+ // Build result with optional actualName (only include if present)
251
+ const result: { exists: boolean; resolvedPath: string; actualName?: string } = {
252
+ exists: verification.exists,
253
+ resolvedPath,
254
+ };
255
+
256
+ if (verification.actualName) {
257
+ result.actualName = verification.actualName;
258
+ }
210
259
 
211
- return { exists, resolvedPath, isGitignored: gitignored };
260
+ return result;
212
261
  }
213
262
 
214
263
  /**
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Multi-schema validation for resources
3
+ *
4
+ * Validates a resource against multiple schemas from different sources,
5
+ * tracking validation results per schema.
6
+ *
7
+ * Supports validation modes:
8
+ * - strict: Enforce schema exactly (respect additionalProperties: false)
9
+ * - permissive: Allow extra fields (schema layering use case)
10
+ */
11
+
12
+ import { promises as fs } from 'node:fs';
13
+ import path from 'node:path';
14
+
15
+ import { validateFrontmatter } from './frontmatter-validator.js';
16
+ import type { ValidationMode } from './schemas/project-config.js';
17
+ import type { ValidationIssue } from './schemas/validation-result.js';
18
+ import type { SchemaReference } from './types/resources.js';
19
+
20
+ /**
21
+ * Load a JSON Schema from a file path
22
+ *
23
+ * @param schemaPath - Path to JSON Schema file
24
+ * @param projectRoot - Optional project root for resolving relative paths
25
+ * @returns Parsed JSON Schema object
26
+ */
27
+ async function loadSchema(schemaPath: string, projectRoot?: string): Promise<object> {
28
+ let resolvedPath = schemaPath;
29
+
30
+ // If path is relative and we have a project root, resolve it
31
+ if (!path.isAbsolute(schemaPath) && projectRoot) {
32
+ resolvedPath = path.join(projectRoot, schemaPath);
33
+ }
34
+
35
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
36
+ const content = await fs.readFile(resolvedPath, 'utf-8');
37
+ return JSON.parse(content) as object;
38
+ }
39
+
40
+ /**
41
+ * Validate frontmatter against multiple schemas
42
+ *
43
+ * Each schema is validated independently and results are tracked separately.
44
+ * The resource-level validation status: fails if ANY schema fails.
45
+ *
46
+ * @param frontmatter - Parsed frontmatter object (or undefined if no frontmatter)
47
+ * @param schemas - Schema references to validate against
48
+ * @param resourcePath - File path for error reporting
49
+ * @param mode - Validation mode (strict or permissive)
50
+ * @param projectRoot - Optional project root for resolving relative schema paths
51
+ * @returns Updated schema references with validation results
52
+ */
53
+ export async function validateFrontmatterMultiSchema(
54
+ frontmatter: Record<string, unknown> | undefined,
55
+ schemas: SchemaReference[],
56
+ resourcePath: string,
57
+ mode: ValidationMode,
58
+ projectRoot?: string,
59
+ ): Promise<SchemaReference[]> {
60
+ const results: SchemaReference[] = [];
61
+
62
+ for (const schemaRef of schemas) {
63
+ try {
64
+ // Load schema
65
+ const schema = await loadSchema(schemaRef.schema, projectRoot);
66
+
67
+ // Validate frontmatter
68
+ const issues = validateFrontmatter(frontmatter, schema, resourcePath, mode);
69
+
70
+ // Update schema reference with results
71
+ const result: SchemaReference = {
72
+ ...schemaRef,
73
+ applied: true,
74
+ valid: issues.length === 0,
75
+ };
76
+
77
+ // Only set errors if there are any (exactOptionalPropertyTypes)
78
+ if (issues.length > 0) {
79
+ result.errors = issues;
80
+ }
81
+
82
+ results.push(result);
83
+ } catch (error) {
84
+ // Schema loading or validation failed
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ results.push({
87
+ ...schemaRef,
88
+ applied: true,
89
+ valid: false,
90
+ errors: [{
91
+ resourcePath,
92
+ line: 1,
93
+ type: 'frontmatter_schema_error',
94
+ link: '',
95
+ message: `Failed to load or validate schema ${schemaRef.schema}: ${message}`,
96
+ }],
97
+ });
98
+ }
99
+ }
100
+
101
+ return results;
102
+ }
103
+
104
+ /**
105
+ * Check if any schema validation failed
106
+ *
107
+ * @param schemas - Schema references with validation results
108
+ * @returns True if any schema failed validation
109
+ */
110
+ export function hasSchemaErrors(schemas: SchemaReference[]): boolean {
111
+ return schemas.some((ref) => ref.valid === false);
112
+ }
113
+
114
+ /**
115
+ * Get all validation errors from all schemas
116
+ *
117
+ * @param schemas - Schema references with validation results
118
+ * @returns Flat array of all validation issues
119
+ */
120
+ export function getAllSchemaErrors(schemas: SchemaReference[]): ValidationIssue[] {
121
+ const allErrors: ValidationIssue[] = [];
122
+ for (const ref of schemas) {
123
+ if (ref.errors) {
124
+ allErrors.push(...ref.errors);
125
+ }
126
+ }
127
+ return allErrors;
128
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Pattern expansion utilities for converting paths to glob patterns.
3
+ *
4
+ * Expands directory paths to glob patterns while preserving explicit glob patterns.
5
+ */
6
+
7
+ const DEFAULT_EXTENSIONS = '**/*.{md,json}';
8
+
9
+ /**
10
+ * Check if a string is a glob pattern or a plain path.
11
+ *
12
+ * A string is considered a glob pattern if it contains glob metacharacters.
13
+ *
14
+ * @param pattern - String to check
15
+ * @returns True if string contains glob metacharacters
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * isGlobPattern('docs') // false
20
+ * isGlobPattern('docs/**\/*.md') // true
21
+ * isGlobPattern('**\/*.json') // true
22
+ * isGlobPattern('path/to/file.md') // false
23
+ * ```
24
+ */
25
+ export function isGlobPattern(pattern: string): boolean {
26
+ return /[*?[\]{}]/.test(pattern);
27
+ }
28
+
29
+ /**
30
+ * Expand a path to a glob pattern.
31
+ *
32
+ * - If input already starts with **\/ or is absolute (starts with /), return as-is
33
+ * - If input is a glob pattern without **\/, prepend **\/ to match absolute paths
34
+ * - If input is a path, expand to **\/path/**\/*.{md,json}
35
+ * - Trailing slashes are stripped before expansion
36
+ * - The **\/ prefix ensures patterns match absolute paths from any location
37
+ *
38
+ * Note: Patterns are expected to use forward slashes. Use toForwardSlash() on paths before
39
+ * passing to this function if they might contain backslashes (Windows).
40
+ *
41
+ * @param pathOrPattern - Path or glob pattern (with forward slashes)
42
+ * @returns Expanded glob pattern
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * expandPattern('docs') // '**\/docs/**\/*.{md,json}'
47
+ * expandPattern('docs/') // '**\/docs/**\/*.{md,json}'
48
+ * expandPattern('docs/**\/*.md') // '**\/docs/**\/*.md' (prepend **\/)
49
+ * expandPattern('**\/*.schema.json') // '**\/*.schema.json' (unchanged)
50
+ * expandPattern('*.md') // '*.md' (root-level pattern, no prefix)
51
+ * ```
52
+ */
53
+ export function expandPattern(pathOrPattern: string): string {
54
+ // Note: This function expects pattern strings (from config), not file paths.
55
+ // Pattern strings from YAML config should already use forward slashes (YAML standard).
56
+ // This function doesn't normalize because it operates on pattern syntax, not paths.
57
+
58
+ // If already starts with **/ or is absolute, return as-is
59
+ // Pattern strings are always forward-slash based (YAML/config standard)
60
+ // eslint-disable-next-line local/no-path-startswith -- checking pattern syntax, not paths
61
+ if (pathOrPattern.startsWith('**/') || pathOrPattern.startsWith('/')) {
62
+ return pathOrPattern;
63
+ }
64
+
65
+ // If it's a glob pattern
66
+ if (isGlobPattern(pathOrPattern)) {
67
+ // Root-level patterns (*.md, *.json) should match only root level
68
+ // These are purely glob metacharacters, not paths
69
+ // eslint-disable-next-line local/no-path-startswith -- checking pattern syntax, not paths
70
+ if (pathOrPattern.startsWith('*')) {
71
+ return pathOrPattern;
72
+ }
73
+ // Other glob patterns need **/ prefix to match absolute paths
74
+ return `**/${pathOrPattern}`;
75
+ }
76
+
77
+ // Strip trailing slash for plain paths
78
+ const normalizedPath = pathOrPattern.replace(/\/$/, '');
79
+
80
+ // Expand path to pattern with **/ prefix for absolute path matching
81
+ return `**/${normalizedPath}/${DEFAULT_EXTENSIONS}`;
82
+ }
83
+
84
+ /**
85
+ * Expand an array of paths/patterns to glob patterns.
86
+ *
87
+ * Each item is processed individually using expandPattern().
88
+ *
89
+ * @param patterns - Array of paths or glob patterns
90
+ * @returns Array of expanded glob patterns
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * expandPatterns(['docs', 'src/**\/*.ts', 'README.md'])
95
+ * // ['docs/**\/*.{md,json}', 'src/**\/*.ts', 'README.md/**\/*.{md,json}']
96
+ * ```
97
+ */
98
+ export function expandPatterns(patterns: string[]): string[] {
99
+ return patterns.map(expandPattern);
100
+ }