@vibe-agent-toolkit/utils 0.1.41 → 0.1.42

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 (116) hide show
  1. package/README.md +146 -31
  2. package/dist/asset.d.ts +9 -0
  3. package/dist/asset.d.ts.map +1 -0
  4. package/dist/asset.js +9 -0
  5. package/dist/asset.js.map +1 -0
  6. package/dist/crawl.d.ts +19 -0
  7. package/dist/crawl.d.ts.map +1 -0
  8. package/dist/crawl.js +19 -0
  9. package/dist/crawl.js.map +1 -0
  10. package/dist/file-crawler.d.ts +19 -2
  11. package/dist/file-crawler.d.ts.map +1 -1
  12. package/dist/file-crawler.js +19 -1
  13. package/dist/file-crawler.js.map +1 -1
  14. package/dist/fs-utils.d.ts +60 -3
  15. package/dist/fs-utils.d.ts.map +1 -1
  16. package/dist/fs-utils.js +88 -10
  17. package/dist/fs-utils.js.map +1 -1
  18. package/dist/fs.d.ts +7 -6
  19. package/dist/fs.d.ts.map +1 -1
  20. package/dist/fs.js +7 -6
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git-utils.d.ts +2 -0
  23. package/dist/git-utils.d.ts.map +1 -1
  24. package/dist/git-utils.js +16 -0
  25. package/dist/git-utils.js.map +1 -1
  26. package/dist/git.d.ts +23 -0
  27. package/dist/git.d.ts.map +1 -0
  28. package/dist/git.js +23 -0
  29. package/dist/git.js.map +1 -0
  30. package/dist/gitignore-checker.d.ts +0 -9
  31. package/dist/gitignore-checker.d.ts.map +1 -1
  32. package/dist/gitignore-checker.js +0 -12
  33. package/dist/gitignore-checker.js.map +1 -1
  34. package/dist/glob/glob-pattern.js +1 -1
  35. package/dist/glob/glob-pattern.js.map +1 -1
  36. package/dist/glob.d.ts +8 -0
  37. package/dist/glob.d.ts.map +1 -0
  38. package/dist/glob.js +8 -0
  39. package/dist/glob.js.map +1 -0
  40. package/dist/path-core.d.ts +196 -0
  41. package/dist/path-core.d.ts.map +1 -0
  42. package/dist/path-core.js +251 -0
  43. package/dist/path-core.js.map +1 -0
  44. package/dist/path-utils.d.ts +12 -183
  45. package/dist/path-utils.d.ts.map +1 -1
  46. package/dist/path-utils.js +13 -237
  47. package/dist/path-utils.js.map +1 -1
  48. package/dist/path.d.ts +12 -0
  49. package/dist/path.d.ts.map +1 -0
  50. package/dist/path.js +12 -0
  51. package/dist/path.js.map +1 -0
  52. package/dist/process.d.ts +7 -4
  53. package/dist/process.d.ts.map +1 -1
  54. package/dist/process.js +7 -4
  55. package/dist/process.js.map +1 -1
  56. package/dist/project.d.ts +31 -0
  57. package/dist/project.d.ts.map +1 -0
  58. package/dist/project.js +31 -0
  59. package/dist/project.js.map +1 -0
  60. package/dist/safe-exec.d.ts.map +1 -1
  61. package/dist/safe-exec.js +8 -4
  62. package/dist/safe-exec.js.map +1 -1
  63. package/dist/spawn-hardened.d.ts.map +1 -1
  64. package/dist/spawn-hardened.js +4 -17
  65. package/dist/spawn-hardened.js.map +1 -1
  66. package/dist/template-entry.d.ts +10 -0
  67. package/dist/template-entry.d.ts.map +1 -0
  68. package/dist/template-entry.js +10 -0
  69. package/dist/template-entry.js.map +1 -0
  70. package/dist/testing.d.ts +8 -0
  71. package/dist/testing.d.ts.map +1 -0
  72. package/dist/testing.js +8 -0
  73. package/dist/testing.js.map +1 -0
  74. package/dist/windows-shell.d.ts +140 -8
  75. package/dist/windows-shell.d.ts.map +1 -1
  76. package/dist/windows-shell.js +218 -11
  77. package/dist/windows-shell.js.map +1 -1
  78. package/dist/yaml.d.ts +9 -0
  79. package/dist/yaml.d.ts.map +1 -0
  80. package/dist/yaml.js +9 -0
  81. package/dist/yaml.js.map +1 -0
  82. package/dist/zod.d.ts +10 -0
  83. package/dist/zod.d.ts.map +1 -0
  84. package/dist/zod.js +10 -0
  85. package/dist/zod.js.map +1 -0
  86. package/eslint/README.md +217 -0
  87. package/eslint/index.cjs +161 -0
  88. package/eslint/index.d.cts +60 -0
  89. package/eslint/rules/dead-import.cjs +201 -0
  90. package/eslint/rules/eslint-rule-factory.cjs +410 -0
  91. package/eslint/rules/exempt-path-matcher.cjs +265 -0
  92. package/eslint/rules/no-bare-dynamic-import-path.cjs +142 -0
  93. package/eslint/rules/no-child-process-execSync.cjs +23 -0
  94. package/eslint/rules/no-command-direct-factory.cjs +246 -0
  95. package/eslint/rules/no-file-url-string-concat.cjs +77 -0
  96. package/eslint/rules/no-fs-mkdirSync.cjs +23 -0
  97. package/eslint/rules/no-fs-promises-cp.cjs +36 -0
  98. package/eslint/rules/no-fs-realpathSync.cjs +23 -0
  99. package/eslint/rules/no-hardcoded-path-split.cjs +141 -0
  100. package/eslint/rules/no-manual-path-normalize.cjs +161 -0
  101. package/eslint/rules/no-os-tmpdir.cjs +24 -0
  102. package/eslint/rules/no-path-join.cjs +14 -0
  103. package/eslint/rules/no-path-operations-in-comparisons.cjs +148 -0
  104. package/eslint/rules/no-path-relative.cjs +14 -0
  105. package/eslint/rules/no-path-resolve.cjs +14 -0
  106. package/eslint/rules/no-path-sep-in-strings.cjs +130 -0
  107. package/eslint/rules/no-path-startswith.cjs +139 -0
  108. package/eslint/rules/no-test-scoped-functions.cjs +134 -0
  109. package/eslint/rules/no-unix-shell-commands.cjs +152 -0
  110. package/eslint/rules/no-unsafe-root-join.cjs +85 -0
  111. package/eslint/rules/no-url-pathname-for-fs.cjs +107 -0
  112. package/eslint/rules/path-function-rule-factory.cjs +417 -0
  113. package/eslint/rules/prefer-startswith-over-regex.cjs +257 -0
  114. package/eslint/rules/require-justified-skip.cjs +348 -0
  115. package/eslint/rules/safe-import.cjs +164 -0
  116. package/package.json +65 -7
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Pure path-string helpers.
3
+ *
4
+ * **This module's only import is `node:path`.** Nothing here touches the
5
+ * filesystem, the OS, or URLs — that is the whole point: the `./path` and
6
+ * `./glob` subpath entries re-export from here so importing them can never
7
+ * pull `node:fs`, `node:os`, or `node:url` into a consumer's graph.
8
+ *
9
+ * Filesystem-touching path helpers (`normalizePath`, `normalizedTmpdir`,
10
+ * `mkdirSyncReal`, `resolveFromImportMeta`, `dynamicImportPath`) live in
11
+ * `./path-utils.ts` and are exposed via the `./fs` entry.
12
+ */
13
+ import path from 'node:path';
14
+ /**
15
+ * Check if a path is absolute
16
+ *
17
+ * Cross-platform detection of absolute paths:
18
+ * - Unix: /path/to/file
19
+ * - Windows: C:\path\to\file or C:/path/to/file
20
+ *
21
+ * @param p - Path to check
22
+ * @returns True if path is absolute
23
+ *
24
+ * @example
25
+ * isAbsolutePath('/path/to/file') // true
26
+ * isAbsolutePath('./relative') // false
27
+ * isAbsolutePath('C:/Windows') // true (Windows)
28
+ */
29
+ export function isAbsolutePath(p) {
30
+ return path.isAbsolute(p);
31
+ }
32
+ /**
33
+ * True if `p` is absolute on ANY platform — a POSIX root path (`/etc`), a
34
+ * Windows drive-letter path (`C:\…` or `C:/…`), or a UNC path (`\\host\share`).
35
+ *
36
+ * Unlike {@link isAbsolutePath} (host-platform only), this is host-independent,
37
+ * so config-containment checks reject Windows-absolute paths even when run on
38
+ * POSIX CI, and vice versa. Used to keep config-supplied relative paths (skill
39
+ * `files:` dest) from escaping their anchor directory (zip-slip class).
40
+ *
41
+ * @example
42
+ * isAbsoluteAnyPlatform('/etc/passwd') // true (POSIX)
43
+ * isAbsoluteAnyPlatform('C:\\Users') // true (Windows drive)
44
+ * isAbsoluteAnyPlatform('scripts/cli') // false (relative)
45
+ */
46
+ export function isAbsoluteAnyPlatform(p) {
47
+ return path.posix.isAbsolute(p) || path.win32.isAbsolute(p);
48
+ }
49
+ /**
50
+ * True if `p` contains a `..` parent-directory traversal segment.
51
+ *
52
+ * Forward-slash-normalized, then inspects each `/`-delimited segment — so a
53
+ * `..` is caught regardless of the original OS separator. A containment guard
54
+ * for config-supplied relative paths (skill `files:` dest values, glob magic
55
+ * remainders) that must never climb above their anchor directory.
56
+ *
57
+ * @example
58
+ * hasParentTraversalSegment('a/../b') // true
59
+ * hasParentTraversalSegment('a/b/c') // false
60
+ * hasParentTraversalSegment('..\\evil') // true (backslash normalized)
61
+ * hasParentTraversalSegment('a..b/c') // false (".." must be a whole segment)
62
+ */
63
+ export function hasParentTraversalSegment(p) {
64
+ return toForwardSlash(p).split('/').includes('..');
65
+ }
66
+ /**
67
+ * Compute a `ValidationIssue.location`: an absolute source file path made
68
+ * relative to the scan/project root, forward-slashed.
69
+ *
70
+ * This is the ONE relativizer every VAT validation lane uses. `location` is
71
+ * contractually project-relative (see `ValidationIssue` in
72
+ * `@vibe-agent-toolkit/agent-schema`), so producers must route through here
73
+ * rather than emitting `skillPath` directly — absolute locations leak the
74
+ * developer's home directory into CI logs and make `validation.allow` globs,
75
+ * which match against `location`, unwritable.
76
+ *
77
+ * `projectRoot` is required precisely because "relative to what?" has no safe
78
+ * default: a caller with no root must decide one (the skill directory, the
79
+ * scan root) rather than silently falling back to an absolute path.
80
+ *
81
+ * @param sourceFilePath - Absolute path to the file the issue was found in.
82
+ * @param projectRoot - Root the location is expressed relative to.
83
+ * @returns Forward-slashed relative location.
84
+ *
85
+ * @example
86
+ * issueLocation('/repo/skills/foo/SKILL.md', '/repo') // 'skills/foo/SKILL.md'
87
+ */
88
+ export function issueLocation(sourceFilePath, projectRoot) {
89
+ return toForwardSlash(path.relative(projectRoot, sourceFilePath));
90
+ }
91
+ /**
92
+ * Convert a relative path to absolute
93
+ *
94
+ * If path is already absolute, returns it normalized.
95
+ * Otherwise resolves relative to baseDir.
96
+ *
97
+ * @param p - Path to convert
98
+ * @param baseDir - Base directory for resolution
99
+ * @returns Absolute path with **forward slashes** (cross-platform safe)
100
+ *
101
+ * @example
102
+ * toAbsolutePath('./docs/README.md', '/project')
103
+ * // Returns: '/project/docs/README.md'
104
+ *
105
+ * toAbsolutePath('/absolute/path.md', '/project')
106
+ * // Returns: '/absolute/path.md'
107
+ */
108
+ export function toAbsolutePath(p, baseDir) {
109
+ if (path.isAbsolute(p)) {
110
+ return toForwardSlash(path.normalize(p));
111
+ }
112
+ return toForwardSlash(path.resolve(baseDir, p));
113
+ }
114
+ /**
115
+ * Get the relative path from one file to another
116
+ *
117
+ * Useful for generating relative links between markdown files.
118
+ *
119
+ * @param from - Source file path (absolute)
120
+ * @param to - Target file path (absolute)
121
+ * @returns Relative path from source to target with **forward slashes** (cross-platform safe)
122
+ *
123
+ * @example
124
+ * getRelativePath('/project/docs/guide.md', '/project/README.md')
125
+ * // Returns: '../README.md'
126
+ *
127
+ * getRelativePath('/project/README.md', '/project/docs/api.md')
128
+ * // Returns: 'docs/api.md'
129
+ */
130
+ export function getRelativePath(from, to) {
131
+ // Get directory of source file (not the file itself)
132
+ const fromDir = path.dirname(from);
133
+ // Calculate relative path from source directory to target file
134
+ return toForwardSlash(path.relative(fromDir, to));
135
+ }
136
+ /**
137
+ * Convert a path to forward slashes
138
+ *
139
+ * Windows accepts both forward slashes and backslashes as path separators.
140
+ * This function normalizes all paths to use forward slashes for consistency.
141
+ * Useful for glob pattern matching, cross-platform comparisons, and string operations.
142
+ *
143
+ * @param p - Path to convert
144
+ * @returns Path with forward slashes
145
+ *
146
+ * @example
147
+ * toForwardSlash('C:\\Users\\docs\\README.md')
148
+ * // Returns: 'C:/Users/docs/README.md'
149
+ *
150
+ * toForwardSlash('/project/docs/README.md')
151
+ * // Returns: '/project/docs/README.md' (unchanged)
152
+ */
153
+ export function toForwardSlash(p) {
154
+ return p.replaceAll('\\', '/');
155
+ }
156
+ /**
157
+ * Cross-platform safe path operations.
158
+ *
159
+ * Wraps Node's `path.join()`, `path.resolve()`, and `path.relative()` to always
160
+ * return forward-slash paths. On Windows, the native `path.*` functions return
161
+ * backslashes, which causes bugs when paths are used as Map keys, compared as
162
+ * strings, or matched with glob patterns.
163
+ *
164
+ * **Use these instead of importing from `node:path` directly.**
165
+ * ESLint rules enforce this — see `no-path-join`, `no-path-resolve`, `no-path-relative`.
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * import { safePath } from '@vibe-agent-toolkit/utils';
170
+ *
171
+ * // Always forward slashes, even on Windows
172
+ * safePath.join('C:\\Users', 'docs', 'file.md') // → 'C:/Users/docs/file.md'
173
+ * safePath.resolve('/project', './docs') // → '/project/docs'
174
+ * safePath.relative('/project/docs', '/project') // → '..'
175
+ * safePath.joinUnderRoot('/harness', 'skill-abc') // → '/harness/skill-abc'
176
+ * safePath.joinUnderRoot('/harness', '../escape') // throws Error
177
+ * ```
178
+ */
179
+ export const safePath = {
180
+ /** Like `path.join()` but always returns forward slashes. */
181
+ join(...paths) {
182
+ return toForwardSlash(path.join(...paths));
183
+ },
184
+ /** Like `path.resolve()` but always returns forward slashes. */
185
+ resolve(...paths) {
186
+ return toForwardSlash(path.resolve(...paths));
187
+ },
188
+ /** Like `path.relative()` but always returns forward slashes. */
189
+ relative(from, to) {
190
+ return toForwardSlash(path.relative(from, to));
191
+ },
192
+ /**
193
+ * Join path segments under a security root, throwing if the result would escape.
194
+ *
195
+ * Resolves `root + segments` and verifies the result is strictly inside `root`
196
+ * (or equal to it). Throws when any segment would cause the result to escape:
197
+ *
198
+ * - A `..` traversal that climbs above root
199
+ * - An absolute POSIX path segment (e.g. `/etc/passwd`)
200
+ * - A Windows drive-letter segment (e.g. `C:\Users\evil`)
201
+ *
202
+ * On success returns a forward-slash-normalized absolute path (consistent with
203
+ * the other `safePath` helpers).
204
+ *
205
+ * **Use this instead of `safePath.join(root, segment)` whenever `segment` may
206
+ * contain caller-controlled input** — this is the bug class that the original
207
+ * skill-test staging code was vulnerable to on Windows.
208
+ *
209
+ * @returns Forward-slash absolute path guaranteed to be inside `root`.
210
+ * @throws {Error} If the resolved path would escape `root`.
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * // ✅ Safe — throws if caller passes '../../../etc'
215
+ * const dest = safePath.joinUnderRoot(harnessRoot, stagedDirName(item.name));
216
+ *
217
+ * // ❌ Unsafe — silently escapes on Windows with absolute segment
218
+ * const dest = safePath.join(harnessRoot, item.name);
219
+ * ```
220
+ */
221
+ joinUnderRoot(root, ...segments) {
222
+ // Eagerly reject any segment that is absolute (POSIX or Windows drive-letter)
223
+ // BEFORE resolving, so the error message can name the offending segment.
224
+ for (const seg of segments) {
225
+ if (path.isAbsolute(seg)) {
226
+ throw new Error(`safePath.joinUnderRoot: segment "${seg}" is absolute and escapes root "${root}".`);
227
+ }
228
+ // Windows drive-letter check for POSIX hosts (path.isAbsolute won't catch
229
+ // 'C:\...' on POSIX, but node's path.win32.isAbsolute does).
230
+ if (path.win32.isAbsolute(seg)) {
231
+ throw new Error(`safePath.joinUnderRoot: segment "${seg}" contains a Windows drive letter and escapes root "${root}".`);
232
+ }
233
+ }
234
+ const resolvedRoot = path.resolve(root);
235
+ const resolvedResult = segments.length > 0
236
+ ? path.resolve(resolvedRoot, ...segments)
237
+ : resolvedRoot;
238
+ // Containment check: normalize both to forward slashes so the comparison
239
+ // is platform-independent and no path.sep is needed in string operations.
240
+ const fwdRoot = toForwardSlash(resolvedRoot);
241
+ const fwdResult = toForwardSlash(resolvedResult);
242
+ // Result must equal root or start with root + '/' (not just startsWith(root)
243
+ // which would match '/rootEvil' when root is '/root').
244
+ const rootPrefix = fwdRoot.endsWith('/') ? fwdRoot : `${fwdRoot}/`;
245
+ if (fwdResult !== fwdRoot && !fwdResult.startsWith(rootPrefix)) {
246
+ throw new Error(`safePath.joinUnderRoot: result "${fwdResult}" escapes root "${fwdRoot}".`);
247
+ }
248
+ return fwdResult;
249
+ },
250
+ };
251
+ //# sourceMappingURL=path-core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-core.js","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CAAC,CAAS;IAC7C,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,yBAAyB,CAAC,CAAS;IACjD,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,aAAa,CAAC,cAAsB,EAAE,WAAmB;IACvE,OAAO,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS,EAAE,OAAe;IACvD,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,EAAU;IACtD,qDAAqD;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC,+DAA+D;IAC/D,OAAO,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,6DAA6D;IAC7D,IAAI,CAAC,GAAG,KAAe;QACrB,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,gEAAgE;IAChE,OAAO,CAAC,GAAG,KAAe;QACxB,OAAO,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,iEAAiE;IACjE,QAAQ,CAAC,IAAY,EAAE,EAAU;QAC/B,OAAO,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,aAAa,CAAC,IAAY,EAAE,GAAG,QAAkB;QAC/C,8EAA8E;QAC9E,yEAAyE;QACzE,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,KAAK,CACb,oCAAoC,GAAG,mCAAmC,IAAI,IAAI,CACnF,CAAC;YACJ,CAAC;YACD,0EAA0E;YAC1E,6DAA6D;YAC7D,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC/B,MAAM,IAAI,KAAK,CACb,oCAAoC,GAAG,uDAAuD,IAAI,IAAI,CACvG,CAAC;YACJ,CAAC;QACH,CAAC;QAED,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC;YACxC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,QAAQ,CAAC;YACzC,CAAC,CAAC,YAAY,CAAC;QAEjB,yEAAyE;QACzE,0EAA0E;QAC1E,MAAM,OAAO,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,cAAc,CAAC,cAAc,CAAC,CAAC;QACjD,6EAA6E;QAC7E,uDAAuD;QACvD,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC;QAEnE,IAAI,SAAS,KAAK,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CACb,mCAAmC,SAAS,mBAAmB,OAAO,IAAI,CAC3E,CAAC;QACJ,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;CACO,CAAC"}
@@ -1,4 +1,16 @@
1
1
  import { mkdirSync } from 'node:fs';
2
+ /**
3
+ * Re-export every pure path helper so the package barrel keeps exposing the
4
+ * exact same surface it always has. The pure definitions themselves live in
5
+ * `./path-core.ts`, which imports nothing but `node:path` — see that file's
6
+ * header for why the split exists.
7
+ *
8
+ * This preserves the BARREL only. `@vibe-agent-toolkit/utils/fs` no longer
9
+ * re-exports the pure helpers — they moved to `@vibe-agent-toolkit/utils/path`,
10
+ * which is a breaking change for `./fs` consumers (see CHANGELOG and
11
+ * `test/path-fs-subpaths.test.ts`).
12
+ */
13
+ export * from './path-core.js';
2
14
  /**
3
15
  * Normalize any path (resolve short names on Windows)
4
16
  *
@@ -90,189 +102,6 @@ export declare function normalizedTmpdir(): string;
90
102
  * ```
91
103
  */
92
104
  export declare function mkdirSyncReal(dirPath: string, options?: Parameters<typeof mkdirSync>[1]): string;
93
- /**
94
- * Check if a path is absolute
95
- *
96
- * Cross-platform detection of absolute paths:
97
- * - Unix: /path/to/file
98
- * - Windows: C:\path\to\file or C:/path/to/file
99
- *
100
- * @param p - Path to check
101
- * @returns True if path is absolute
102
- *
103
- * @example
104
- * isAbsolutePath('/path/to/file') // true
105
- * isAbsolutePath('./relative') // false
106
- * isAbsolutePath('C:/Windows') // true (Windows)
107
- */
108
- export declare function isAbsolutePath(p: string): boolean;
109
- /**
110
- * True if `p` is absolute on ANY platform — a POSIX root path (`/etc`), a
111
- * Windows drive-letter path (`C:\…` or `C:/…`), or a UNC path (`\\host\share`).
112
- *
113
- * Unlike {@link isAbsolutePath} (host-platform only), this is host-independent,
114
- * so config-containment checks reject Windows-absolute paths even when run on
115
- * POSIX CI, and vice versa. Used to keep config-supplied relative paths (skill
116
- * `files:` dest) from escaping their anchor directory (zip-slip class).
117
- *
118
- * @example
119
- * isAbsoluteAnyPlatform('/etc/passwd') // true (POSIX)
120
- * isAbsoluteAnyPlatform('C:\\Users') // true (Windows drive)
121
- * isAbsoluteAnyPlatform('scripts/cli') // false (relative)
122
- */
123
- export declare function isAbsoluteAnyPlatform(p: string): boolean;
124
- /**
125
- * True if `p` contains a `..` parent-directory traversal segment.
126
- *
127
- * Forward-slash-normalized, then inspects each `/`-delimited segment — so a
128
- * `..` is caught regardless of the original OS separator. A containment guard
129
- * for config-supplied relative paths (skill `files:` dest values, glob magic
130
- * remainders) that must never climb above their anchor directory.
131
- *
132
- * @example
133
- * hasParentTraversalSegment('a/../b') // true
134
- * hasParentTraversalSegment('a/b/c') // false
135
- * hasParentTraversalSegment('..\\evil') // true (backslash normalized)
136
- * hasParentTraversalSegment('a..b/c') // false (".." must be a whole segment)
137
- */
138
- export declare function hasParentTraversalSegment(p: string): boolean;
139
- /**
140
- * Compute a `ValidationIssue.location`: an absolute source file path made
141
- * relative to the scan/project root, forward-slashed.
142
- *
143
- * This is the ONE relativizer every VAT validation lane uses. `location` is
144
- * contractually project-relative (see `ValidationIssue` in
145
- * `@vibe-agent-toolkit/agent-schema`), so producers must route through here
146
- * rather than emitting `skillPath` directly — absolute locations leak the
147
- * developer's home directory into CI logs and make `validation.allow` globs,
148
- * which match against `location`, unwritable.
149
- *
150
- * `projectRoot` is required precisely because "relative to what?" has no safe
151
- * default: a caller with no root must decide one (the skill directory, the
152
- * scan root) rather than silently falling back to an absolute path.
153
- *
154
- * @param sourceFilePath - Absolute path to the file the issue was found in.
155
- * @param projectRoot - Root the location is expressed relative to.
156
- * @returns Forward-slashed relative location.
157
- *
158
- * @example
159
- * issueLocation('/repo/skills/foo/SKILL.md', '/repo') // 'skills/foo/SKILL.md'
160
- */
161
- export declare function issueLocation(sourceFilePath: string, projectRoot: string): string;
162
- /**
163
- * Convert a relative path to absolute
164
- *
165
- * If path is already absolute, returns it normalized.
166
- * Otherwise resolves relative to baseDir.
167
- *
168
- * @param p - Path to convert
169
- * @param baseDir - Base directory for resolution
170
- * @returns Absolute path with **forward slashes** (cross-platform safe)
171
- *
172
- * @example
173
- * toAbsolutePath('./docs/README.md', '/project')
174
- * // Returns: '/project/docs/README.md'
175
- *
176
- * toAbsolutePath('/absolute/path.md', '/project')
177
- * // Returns: '/absolute/path.md'
178
- */
179
- export declare function toAbsolutePath(p: string, baseDir: string): string;
180
- /**
181
- * Get the relative path from one file to another
182
- *
183
- * Useful for generating relative links between markdown files.
184
- *
185
- * @param from - Source file path (absolute)
186
- * @param to - Target file path (absolute)
187
- * @returns Relative path from source to target with **forward slashes** (cross-platform safe)
188
- *
189
- * @example
190
- * getRelativePath('/project/docs/guide.md', '/project/README.md')
191
- * // Returns: '../README.md'
192
- *
193
- * getRelativePath('/project/README.md', '/project/docs/api.md')
194
- * // Returns: 'docs/api.md'
195
- */
196
- export declare function getRelativePath(from: string, to: string): string;
197
- /**
198
- * Convert a path to forward slashes
199
- *
200
- * Windows accepts both forward slashes and backslashes as path separators.
201
- * This function normalizes all paths to use forward slashes for consistency.
202
- * Useful for glob pattern matching, cross-platform comparisons, and string operations.
203
- *
204
- * @param p - Path to convert
205
- * @returns Path with forward slashes
206
- *
207
- * @example
208
- * toForwardSlash('C:\\Users\\docs\\README.md')
209
- * // Returns: 'C:/Users/docs/README.md'
210
- *
211
- * toForwardSlash('/project/docs/README.md')
212
- * // Returns: '/project/docs/README.md' (unchanged)
213
- */
214
- export declare function toForwardSlash(p: string): string;
215
- /**
216
- * Cross-platform safe path operations.
217
- *
218
- * Wraps Node's `path.join()`, `path.resolve()`, and `path.relative()` to always
219
- * return forward-slash paths. On Windows, the native `path.*` functions return
220
- * backslashes, which causes bugs when paths are used as Map keys, compared as
221
- * strings, or matched with glob patterns.
222
- *
223
- * **Use these instead of importing from `node:path` directly.**
224
- * ESLint rules enforce this — see `no-path-join`, `no-path-resolve`, `no-path-relative`.
225
- *
226
- * @example
227
- * ```typescript
228
- * import { safePath } from '@vibe-agent-toolkit/utils';
229
- *
230
- * // Always forward slashes, even on Windows
231
- * safePath.join('C:\\Users', 'docs', 'file.md') // → 'C:/Users/docs/file.md'
232
- * safePath.resolve('/project', './docs') // → '/project/docs'
233
- * safePath.relative('/project/docs', '/project') // → '..'
234
- * safePath.joinUnderRoot('/harness', 'skill-abc') // → '/harness/skill-abc'
235
- * safePath.joinUnderRoot('/harness', '../escape') // throws Error
236
- * ```
237
- */
238
- export declare const safePath: {
239
- /** Like `path.join()` but always returns forward slashes. */
240
- readonly join: (...paths: string[]) => string;
241
- /** Like `path.resolve()` but always returns forward slashes. */
242
- readonly resolve: (...paths: string[]) => string;
243
- /** Like `path.relative()` but always returns forward slashes. */
244
- readonly relative: (from: string, to: string) => string;
245
- /**
246
- * Join path segments under a security root, throwing if the result would escape.
247
- *
248
- * Resolves `root + segments` and verifies the result is strictly inside `root`
249
- * (or equal to it). Throws when any segment would cause the result to escape:
250
- *
251
- * - A `..` traversal that climbs above root
252
- * - An absolute POSIX path segment (e.g. `/etc/passwd`)
253
- * - A Windows drive-letter segment (e.g. `C:\Users\evil`)
254
- *
255
- * On success returns a forward-slash-normalized absolute path (consistent with
256
- * the other `safePath` helpers).
257
- *
258
- * **Use this instead of `safePath.join(root, segment)` whenever `segment` may
259
- * contain caller-controlled input** — this is the bug class that the original
260
- * skill-test staging code was vulnerable to on Windows.
261
- *
262
- * @returns Forward-slash absolute path guaranteed to be inside `root`.
263
- * @throws {Error} If the resolved path would escape `root`.
264
- *
265
- * @example
266
- * ```typescript
267
- * // ✅ Safe — throws if caller passes '../../../etc'
268
- * const dest = safePath.joinUnderRoot(harnessRoot, stagedDirName(item.name));
269
- *
270
- * // ❌ Unsafe — silently escapes on Windows with absolute segment
271
- * const dest = safePath.join(harnessRoot, item.name);
272
- * ```
273
- */
274
- readonly joinUnderRoot: (root: string, ...segments: string[]) => string;
275
- };
276
105
  /**
277
106
  * Resolve an OS-native absolute path from an ESM module's `import.meta.url` and
278
107
  * optional relative path segments.
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AAKlD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,aAAa,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CA+BxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAezC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,GACxC,MAAM,CAiBR;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,QAAQ;IACnB,6DAA6D;8BAC9C,MAAM,EAAE,KAAG,MAAM;IAIhC,gEAAgE;iCAC9C,MAAM,EAAE,KAAG,MAAM;IAInC,iEAAiE;8BAClD,MAAM,MAAM,MAAM,KAAG,MAAM;IAI1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;mCACiB,MAAM,eAAe,MAAM,EAAE,KAAG,MAAM;CAuClD,CAAC;AAEX;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ1F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGhF"}
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AAOlD;;;;;;;;;;GAUG;AACH,cAAc,gBAAgB,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,aAAa,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CA+BxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAezC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,GACxC,MAAM,CAiBR;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ1F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGhF"}