@j0hanz/filesystem-mcp 2.8.1 → 2.9.1

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 +33 -27
  2. package/dist/cli-help.d.ts.map +1 -1
  3. package/dist/cli-help.js +10 -1
  4. package/dist/cli-help.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +10 -3
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/concurrency.d.ts +4 -4
  9. package/dist/core/concurrency.d.ts.map +1 -1
  10. package/dist/core/concurrency.js +1 -1
  11. package/dist/core/concurrency.js.map +1 -1
  12. package/dist/core/config.d.ts +2 -0
  13. package/dist/core/config.d.ts.map +1 -1
  14. package/dist/core/config.js.map +1 -1
  15. package/dist/core/errors.d.ts.map +1 -1
  16. package/dist/core/errors.js +7 -1
  17. package/dist/core/errors.js.map +1 -1
  18. package/dist/core/file-uri.d.ts +12 -3
  19. package/dist/core/file-uri.d.ts.map +1 -1
  20. package/dist/core/file-uri.js +35 -7
  21. package/dist/core/file-uri.js.map +1 -1
  22. package/dist/core/fmt.d.ts +2 -0
  23. package/dist/core/fmt.d.ts.map +1 -1
  24. package/dist/core/fmt.js +16 -0
  25. package/dist/core/fmt.js.map +1 -1
  26. package/dist/core/fs.d.ts +16 -1
  27. package/dist/core/fs.d.ts.map +1 -1
  28. package/dist/core/fs.js +127 -32
  29. package/dist/core/fs.js.map +1 -1
  30. package/dist/core/glob.d.ts +15 -0
  31. package/dist/core/glob.d.ts.map +1 -1
  32. package/dist/core/glob.js +119 -14
  33. package/dist/core/glob.js.map +1 -1
  34. package/dist/core/path-completer.d.ts.map +1 -1
  35. package/dist/core/path-completer.js +5 -3
  36. package/dist/core/path-completer.js.map +1 -1
  37. package/dist/core/path-utils.d.ts +42 -6
  38. package/dist/core/path-utils.d.ts.map +1 -1
  39. package/dist/core/path-utils.js +56 -10
  40. package/dist/core/path-utils.js.map +1 -1
  41. package/dist/core/path.d.ts +111 -7
  42. package/dist/core/path.d.ts.map +1 -1
  43. package/dist/core/path.js +453 -65
  44. package/dist/core/path.js.map +1 -1
  45. package/dist/core/read.d.ts +6 -0
  46. package/dist/core/read.d.ts.map +1 -1
  47. package/dist/core/read.js +26 -3
  48. package/dist/core/read.js.map +1 -1
  49. package/dist/core/schema.d.ts +9 -6
  50. package/dist/core/schema.d.ts.map +1 -1
  51. package/dist/core/schema.js +22 -19
  52. package/dist/core/schema.js.map +1 -1
  53. package/dist/core/search.d.ts +6 -0
  54. package/dist/core/search.d.ts.map +1 -1
  55. package/dist/core/search.js +161 -33
  56. package/dist/core/search.js.map +1 -1
  57. package/dist/core/sensitive.d.ts.map +1 -1
  58. package/dist/core/sensitive.js +64 -15
  59. package/dist/core/sensitive.js.map +1 -1
  60. package/dist/core/store.d.ts +1 -0
  61. package/dist/core/store.d.ts.map +1 -1
  62. package/dist/core/store.js +3 -0
  63. package/dist/core/store.js.map +1 -1
  64. package/dist/core/util.d.ts +12 -1
  65. package/dist/core/util.d.ts.map +1 -1
  66. package/dist/core/util.js +16 -1
  67. package/dist/core/util.js.map +1 -1
  68. package/dist/resources.d.ts.map +1 -1
  69. package/dist/resources.js +20 -11
  70. package/dist/resources.js.map +1 -1
  71. package/dist/server.js +1 -1
  72. package/dist/server.js.map +1 -1
  73. package/dist/tools/create.d.ts.map +1 -1
  74. package/dist/tools/create.js +121 -43
  75. package/dist/tools/create.js.map +1 -1
  76. package/dist/tools/define.d.ts +8 -4
  77. package/dist/tools/define.d.ts.map +1 -1
  78. package/dist/tools/define.js +36 -9
  79. package/dist/tools/define.js.map +1 -1
  80. package/dist/tools/delete.d.ts.map +1 -1
  81. package/dist/tools/delete.js +50 -11
  82. package/dist/tools/delete.js.map +1 -1
  83. package/dist/tools/diff.js +1 -1
  84. package/dist/tools/diff.js.map +1 -1
  85. package/dist/tools/edit.d.ts.map +1 -1
  86. package/dist/tools/edit.js +14 -12
  87. package/dist/tools/edit.js.map +1 -1
  88. package/dist/tools/find-files.d.ts.map +1 -1
  89. package/dist/tools/find-files.js +15 -9
  90. package/dist/tools/find-files.js.map +1 -1
  91. package/dist/tools/list.d.ts.map +1 -1
  92. package/dist/tools/list.js +141 -86
  93. package/dist/tools/list.js.map +1 -1
  94. package/dist/tools/move.d.ts.map +1 -1
  95. package/dist/tools/move.js +72 -26
  96. package/dist/tools/move.js.map +1 -1
  97. package/dist/tools/patch.js +13 -6
  98. package/dist/tools/patch.js.map +1 -1
  99. package/dist/tools/read.d.ts.map +1 -1
  100. package/dist/tools/read.js +113 -48
  101. package/dist/tools/read.js.map +1 -1
  102. package/dist/tools/replace-text.d.ts.map +1 -1
  103. package/dist/tools/replace-text.js +50 -35
  104. package/dist/tools/replace-text.js.map +1 -1
  105. package/dist/tools/search-text.d.ts.map +1 -1
  106. package/dist/tools/search-text.js +15 -33
  107. package/dist/tools/search-text.js.map +1 -1
  108. package/dist/tools/stat.d.ts.map +1 -1
  109. package/dist/tools/stat.js +26 -8
  110. package/dist/tools/stat.js.map +1 -1
  111. package/dist/transport/http.d.ts.map +1 -1
  112. package/dist/transport/http.js +1 -0
  113. package/dist/transport/http.js.map +1 -1
  114. package/dist/transport/stdio.js +1 -1
  115. package/dist/transport/stdio.js.map +1 -1
  116. package/package.json +5 -5
@@ -29,17 +29,53 @@ export declare const CHAR_COLON = 58;
29
29
  export declare const trimTrailingDotsAndSpaces: (segment: string) => string;
30
30
  export declare function getReservedDeviceNameForPath(requestedPath: string): string | undefined;
31
31
  export declare function isWindowsDriveRelativePath(requestedPath: string): boolean;
32
+ /**
33
+ * True for a path that names a Windows drive: `C:\x`, `C:/x`, `C:`, and the
34
+ * drive-relative `C:x`. Only Windows resolves one. On POSIX it is a relative
35
+ * path, so `C:\x` would become a file of that name under the root.
36
+ */
37
+ export declare const isWindowsDrivePath: (requestedPath: string) => boolean;
38
+ /**
39
+ * A drive path this host refuses before resolving it: drive-relative anywhere,
40
+ * and any drive path where the host is not Windows.
41
+ */
42
+ export declare const isUnresolvableDrivePath: (requestedPath: string) => boolean;
43
+ export declare function expandHome(filepath: string): string;
32
44
  export declare function normalizePath(p: string): string;
33
- export declare function normalizeCaseForComparison(value: string): string;
34
45
  export declare function isSamePath(left: string, right: string): boolean;
35
46
  /**
36
- * On a case-insensitive filesystem a write target that already exists is
37
- * realpathed to its on-disk spelling, so a case-only rename (`foo.txt` ->
38
- * `Foo.txt`) resolves to the source itself. Returns the resolved target
39
- * re-spelled with the requested basename when the two differ only by case;
47
+ * What two names share when they differ only in Unicode form (NFC or NFD) or
48
+ * in which space character they use (U+0020, U+00A0, U+202F, and every other
49
+ * Zs), case-folded where the filesystem folds case. A model types back
50
+ * `caf\u00e9` and `shot PM.png` for the `cafe\u0301` and `shot\u202fPM.png` a
51
+ * listing showed it (#101).
52
+ */
53
+ export declare function unicodeMatchKey(name: string): string;
54
+ /**
55
+ * True when two paths are one path once case (where the filesystem folds it)
56
+ * and Unicode spelling are folded: the spellings the path guard resolves to
57
+ * one entry when only one of them is on disk.
58
+ */
59
+ export declare function isEquivalentPath(left: string, right: string): boolean;
60
+ /**
61
+ * False for a name of printable ASCII with no space, which no other entry can
62
+ * share a `unicodeMatchKey` with — except a name holding U+212A, U+037E or
63
+ * U+1FEF, which NFC maps to ASCII; those stay unmatched. A missed lookup of
64
+ * such a name skips the directory scan, so creating `new-file.ts` costs what
65
+ * it did.
66
+ */
67
+ export declare function mayHaveEquivalentSpelling(name: string): boolean;
68
+ /** `name` with each non-ASCII code unit as a `\uXXXX` escape, which a JSON string argument accepts. */
69
+ export declare function escapeNonAscii(name: string): string;
70
+ /**
71
+ * A write target that already exists is realpathed to its on-disk spelling,
72
+ * so a rename that changes only case (`foo.txt` -> `Foo.txt` on a
73
+ * case-insensitive filesystem) or only Unicode spelling (`cafe\u0301.txt` ->
74
+ * `caf\u00e9.txt`) resolves to the source itself. Returns the resolved target
75
+ * re-spelled with the requested basename when the two differ only that way;
40
76
  * otherwise the resolved target unchanged. Parent segments are not re-spelled.
41
77
  */
42
- export declare function respellCaseOnlyTarget(requestedPath: string, resolvedTarget: string): string;
78
+ export declare function respellSpellingOnlyTarget(requestedPath: string, resolvedTarget: string): string;
43
79
  export declare function normalizeAllowedDirectory(dir: string): string;
44
80
  export declare function isPathInsideDirectory(normalizedDirectory: string, normalizedCandidate: string): boolean;
45
81
  export declare function isPathWithinDirectories(normalizedPath: string, allowedDirs: readonly string[]): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnE,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,WAAW,IAAI,OAAO,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC;IAClB,cAAc,IAAI,OAAO,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAK9D;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAClC,IAAI,CAON;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAM9D;AAID,eAAO,MAAM,UAAU,SAA+B,CAAC;AAEvD,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OAAqC,CAAC;AAE7E,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OACoB,CAAC;AAE5D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAAqC,CAAC;AAElF,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhE;AAID,eAAO,MAAM,UAAU,KAAK,CAAC;AA4B7B;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,MAAM,KAAG,MAAuC,CAAC;AASpG,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAStF;AAED,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAOzE;AAUD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAa/C;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAK3F;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAa7D;AAMD,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAST;AAED,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAET"}
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnE,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,WAAW,IAAI,OAAO,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC;IAClB,cAAc,IAAI,OAAO,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAK9D;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAClC,IAAI,CAON;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAM9D;AAID,eAAO,MAAM,UAAU,SAA+B,CAAC;AAEvD,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OAAqC,CAAC;AAE7E,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OACoB,CAAC;AAE5D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAAqC,CAAC;AAElF,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhE;AAID,eAAO,MAAM,UAAU,KAAK,CAAC;AA4B7B;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,MAAM,KAAG,MAAuC,CAAC;AASpG,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAStF;AAED,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAOzE;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,eAAe,MAAM,KAAG,OACN,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAI,eAAe,MAAM,KAAG,OAC4B,CAAC;AAE7F,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMnD;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAa/C;AAQD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIrE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED,uGAAuG;AACvG,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOnD;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAO/F;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAa7D;AAMD,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAST;AAED,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAET"}
@@ -97,7 +97,18 @@ export function isWindowsDriveRelativePath(requestedPath) {
97
97
  const { root } = win32.parse(requestedPath);
98
98
  return root.length === 2;
99
99
  }
100
- function expandHome(filepath) {
100
+ /**
101
+ * True for a path that names a Windows drive: `C:\x`, `C:/x`, `C:`, and the
102
+ * drive-relative `C:x`. Only Windows resolves one. On POSIX it is a relative
103
+ * path, so `C:\x` would become a file of that name under the root.
104
+ */
105
+ export const isWindowsDrivePath = (requestedPath) => /^[A-Za-z]:/u.test(win32.parse(requestedPath).root);
106
+ /**
107
+ * A drive path this host refuses before resolving it: drive-relative anywhere,
108
+ * and any drive path where the host is not Windows.
109
+ */
110
+ export const isUnresolvableDrivePath = (requestedPath) => IS_WINDOWS ? isWindowsDriveRelativePath(requestedPath) : isWindowsDrivePath(requestedPath);
111
+ export function expandHome(filepath) {
101
112
  if (filepath === '~' || filepath.startsWith('~/') || filepath.startsWith('~\\')) {
102
113
  const rest = filepath.slice(1).replace(/^[/\\]+/, '');
103
114
  return rest ? join(HOMEDIR, rest) : HOMEDIR;
@@ -117,7 +128,7 @@ export function normalizePath(p) {
117
128
  return resolved;
118
129
  }
119
130
  const IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';
120
- export function normalizeCaseForComparison(value) {
131
+ function normalizeCaseForComparison(value) {
121
132
  return IS_CASE_INSENSITIVE_FS ? value.toLowerCase() : value;
122
133
  }
123
134
  export function isSamePath(left, right) {
@@ -129,18 +140,53 @@ export function isSamePath(left, right) {
129
140
  return leftResolved === rightResolved;
130
141
  }
131
142
  /**
132
- * On a case-insensitive filesystem a write target that already exists is
133
- * realpathed to its on-disk spelling, so a case-only rename (`foo.txt` ->
134
- * `Foo.txt`) resolves to the source itself. Returns the resolved target
135
- * re-spelled with the requested basename when the two differ only by case;
143
+ * What two names share when they differ only in Unicode form (NFC or NFD) or
144
+ * in which space character they use (U+0020, U+00A0, U+202F, and every other
145
+ * Zs), case-folded where the filesystem folds case. A model types back
146
+ * `caf\u00e9` and `shot PM.png` for the `cafe\u0301` and `shot\u202fPM.png` a
147
+ * listing showed it (#101).
148
+ */
149
+ export function unicodeMatchKey(name) {
150
+ return normalizeCaseForComparison(name.normalize('NFC').replace(/\p{Zs}/gu, ' '));
151
+ }
152
+ /**
153
+ * True when two paths are one path once case (where the filesystem folds it)
154
+ * and Unicode spelling are folded: the spellings the path guard resolves to
155
+ * one entry when only one of them is on disk.
156
+ */
157
+ export function isEquivalentPath(left, right) {
158
+ return (isSamePath(left, right) || unicodeMatchKey(resolve(left)) === unicodeMatchKey(resolve(right)));
159
+ }
160
+ /**
161
+ * False for a name of printable ASCII with no space, which no other entry can
162
+ * share a `unicodeMatchKey` with — except a name holding U+212A, U+037E or
163
+ * U+1FEF, which NFC maps to ASCII; those stay unmatched. A missed lookup of
164
+ * such a name skips the directory scan, so creating `new-file.ts` costs what
165
+ * it did.
166
+ */
167
+ export function mayHaveEquivalentSpelling(name) {
168
+ return /[^\x21-\x7e]/u.test(name);
169
+ }
170
+ /** `name` with each non-ASCII code unit as a `\uXXXX` escape, which a JSON string argument accepts. */
171
+ export function escapeNonAscii(name) {
172
+ return name.replace(/[^\x20-\x7e]/gu, (char) => Array.from({ length: char.length }, (_, index) => `\\u${char.charCodeAt(index).toString(16).padStart(4, '0')}`).join(''));
173
+ }
174
+ /**
175
+ * A write target that already exists is realpathed to its on-disk spelling,
176
+ * so a rename that changes only case (`foo.txt` -> `Foo.txt` on a
177
+ * case-insensitive filesystem) or only Unicode spelling (`cafe\u0301.txt` ->
178
+ * `caf\u00e9.txt`) resolves to the source itself. Returns the resolved target
179
+ * re-spelled with the requested basename when the two differ only that way;
136
180
  * otherwise the resolved target unchanged. Parent segments are not re-spelled.
137
181
  */
138
- export function respellCaseOnlyTarget(requestedPath, resolvedTarget) {
182
+ export function respellSpellingOnlyTarget(requestedPath, resolvedTarget) {
139
183
  const requestedName = basename(requestedPath);
140
- if (requestedName === basename(resolvedTarget))
184
+ const resolvedName = basename(resolvedTarget);
185
+ if (requestedName === resolvedName)
141
186
  return resolvedTarget;
142
- const respelled = join(dirname(resolvedTarget), requestedName);
143
- return isSamePath(respelled, resolvedTarget) ? respelled : resolvedTarget;
187
+ return unicodeMatchKey(requestedName) === unicodeMatchKey(resolvedName)
188
+ ? join(dirname(resolvedTarget), requestedName)
189
+ : resolvedTarget;
144
190
  }
145
191
  export function normalizeAllowedDirectory(dir) {
146
192
  const trimmed = dir.trim();
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir, platform } from 'node:os';\nimport {\n basename,\n delimiter,\n dirname,\n join,\n parse,\n relative,\n resolve,\n sep,\n win32,\n} from 'node:path';\n\n// ─── Shared primitives (no intra-package imports; every core module may import this) ──\n\nexport type EntryType = 'file' | 'directory' | 'symlink' | 'other';\n\n/** The three predicates both `Dirent` and `Stats` expose - all `resolveEntryType` needs. */\nexport interface DirentLike {\n isDirectory(): boolean;\n isFile(): boolean;\n isSymbolicLink(): boolean;\n}\n\nexport function resolveEntryType(dirent: DirentLike): EntryType {\n if (dirent.isDirectory()) return 'directory';\n if (dirent.isFile()) return 'file';\n if (dirent.isSymbolicLink()) return 'symlink';\n return 'other';\n}\n\nconst warnedSettings = new Set<string>();\n\n/**\n * Warn once per (setting, value) that a configured value was rejected, and say\n * what was used instead. console.error, not Logger: this module stays\n * dependency-free to avoid import cycles, and a startup misconfiguration is\n * worth printing even when the operator asked for errors only.\n */\nexport function warnInvalidSetting(\n name: string,\n value: string,\n expected: string,\n fallback: string | number | boolean,\n): void {\n const key = `${name}:${value}`;\n if (warnedSettings.has(key)) return;\n warnedSettings.add(key);\n console.error(\n `[warning] Invalid ${name} value: ${value} (must be ${expected}). Using default: ${String(fallback)}`,\n );\n}\n\nexport function splitDirList(val: string | undefined): string[] {\n if (!val) return [];\n return val\n .split(delimiter)\n .map((p) => p.trim())\n .filter((p) => p.length > 0);\n}\n\n// ─── Character-level path primitives ─────────────────────────────────────────\n\nexport const IS_WINDOWS = process.platform === 'win32';\n\nexport const isSlash = (code: number): boolean => code === 47 || code === 92;\n\nexport const isAlpha = (code: number): boolean =>\n (code >= 65 && code <= 90) || (code >= 97 && code <= 122);\n\nexport const toPosixPath = (value: string): string => value.replaceAll('\\\\', '/');\n\n/** `path.relative` with forward slashes, so displayed paths match across platforms. */\nexport function toPosixRelative(from: string, to: string): string {\n return toPosixPath(relative(from, to));\n}\n\n// ─── Path normalization and containment ──────────────────────────────────────\n\nexport const CHAR_COLON = 58;\nconst HOMEDIR = homedir();\n\nconst RESERVED_DEVICE_NAMES = new Set<string>([\n 'CON',\n 'PRN',\n 'AUX',\n 'NUL',\n 'COM1',\n 'COM2',\n 'COM3',\n 'COM4',\n 'COM5',\n 'COM6',\n 'COM7',\n 'COM8',\n 'COM9',\n 'LPT1',\n 'LPT2',\n 'LPT3',\n 'LPT4',\n 'LPT5',\n 'LPT6',\n 'LPT7',\n 'LPT8',\n 'LPT9',\n]);\n\n/**\n * Win32 strips trailing dots and spaces at the syscall boundary (a request for\n * `.env ` creates `.env`), so every name check must trim them first or it is\n * bypassed on Windows.\n */\nexport const trimTrailingDotsAndSpaces = (segment: string): string => segment.replace(/[. ]+$/, '');\n\nfunction getReservedDeviceName(segment: string): string | undefined {\n const trimmed = trimTrailingDotsAndSpaces(segment);\n const withoutStream = trimmed.split(':')[0] ?? '';\n const baseName = (withoutStream.split('.')[0] ?? '').toUpperCase();\n return RESERVED_DEVICE_NAMES.has(baseName) ? baseName : undefined;\n}\n\nexport function getReservedDeviceNameForPath(requestedPath: string): string | undefined {\n const segments = requestedPath.split(/[\\\\/]/u);\n for (const segment of segments) {\n const reserved = getReservedDeviceName(segment);\n if (reserved) {\n return reserved;\n }\n }\n return undefined;\n}\n\nexport function isWindowsDriveRelativePath(requestedPath: string): boolean {\n // Check on all platforms so cross-platform clients cannot smuggle drive-relative\n // inputs (e.g. C:relative) to a POSIX-hosted server where path.resolve would\n // silently expand them relative to CWD. win32.parse is the platform-independent\n // binding, so this reads the same on a POSIX host.\n const { root } = win32.parse(requestedPath);\n return root.length === 2;\n}\n\nfunction expandHome(filepath: string): string {\n if (filepath === '~' || filepath.startsWith('~/') || filepath.startsWith('~\\\\')) {\n const rest = filepath.slice(1).replace(/^[/\\\\]+/, '');\n return rest ? join(HOMEDIR, rest) : HOMEDIR;\n }\n return filepath;\n}\n\nexport function normalizePath(p: string): string {\n const resolved = resolve(expandHome(p));\n\n // On Windows only the drive letter is lowercased (e.g. \"C:\\Foo\\Bar\").\n // The rest of the path retains its original casing.\n // IMPORTANT: callers must use isSamePath / isPathInsideDirectory for all\n // equality and containment checks — never raw string equality — because\n // those helpers apply full case-folding before comparing.\n if (IS_WINDOWS && resolved.length >= 2 && resolved.charCodeAt(1) === CHAR_COLON) {\n return resolved.charAt(0).toLowerCase() + resolved.slice(1);\n }\n\n return resolved;\n}\n\nconst IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';\n\nexport function normalizeCaseForComparison(value: string): string {\n return IS_CASE_INSENSITIVE_FS ? value.toLowerCase() : value;\n}\n\nexport function isSamePath(left: string, right: string): boolean {\n if (left === right) {\n return true;\n }\n const leftResolved = normalizeCaseForComparison(resolve(left));\n const rightResolved = normalizeCaseForComparison(resolve(right));\n return leftResolved === rightResolved;\n}\n\n/**\n * On a case-insensitive filesystem a write target that already exists is\n * realpathed to its on-disk spelling, so a case-only rename (`foo.txt` ->\n * `Foo.txt`) resolves to the source itself. Returns the resolved target\n * re-spelled with the requested basename when the two differ only by case;\n * otherwise the resolved target unchanged. Parent segments are not re-spelled.\n */\nexport function respellCaseOnlyTarget(requestedPath: string, resolvedTarget: string): string {\n const requestedName = basename(requestedPath);\n if (requestedName === basename(resolvedTarget)) return resolvedTarget;\n const respelled = join(dirname(resolvedTarget), requestedName);\n return isSamePath(respelled, resolvedTarget) ? respelled : resolvedTarget;\n}\n\nexport function normalizeAllowedDirectory(dir: string): string {\n const trimmed = dir.trim();\n if (trimmed.length === 0) return '';\n\n const normalized = normalizePath(trimmed);\n const { root } = parse(normalized);\n\n // Keep filesystem roots as-is (\"/\", \"c:\\\\\", \"\\\\\\\\server\\\\share\\\\\").\n if (isSamePath(normalized, root)) {\n return root;\n }\n\n return normalized.length > 1 && normalized.endsWith(sep) ? normalized.slice(0, -1) : normalized;\n}\n\n// `\\` separates path segments only on Windows. On POSIX it is a filename\n// character, so `/root\\x` names a sibling of `/root`, not a child.\nconst isPathSeparator = (code: number): boolean => code === 47 || (IS_WINDOWS && code === 92);\n\nexport function isPathInsideDirectory(\n normalizedDirectory: string,\n normalizedCandidate: string,\n): boolean {\n const root = normalizeCaseForComparison(normalizedDirectory);\n const candidate = normalizeCaseForComparison(normalizedCandidate);\n\n if (root === candidate) return true;\n if (!candidate.startsWith(root)) return false;\n\n if (isPathSeparator(root.charCodeAt(root.length - 1))) return true;\n return isPathSeparator(candidate.charCodeAt(root.length));\n}\n\nexport function isPathWithinDirectories(\n normalizedPath: string,\n allowedDirs: readonly string[],\n): boolean {\n return allowedDirs.some((dir) => isPathInsideDirectory(dir, normalizedPath));\n}\n"]}
1
+ {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EACL,QAAQ,EACR,SAAS,EACT,OAAO,EACP,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,OAAO,EACP,GAAG,EACH,KAAK,GACN,MAAM,WAAW,CAAC;AAanB,MAAM,UAAU,gBAAgB,CAAC,MAAkB;IACjD,IAAI,MAAM,CAAC,WAAW,EAAE;QAAE,OAAO,WAAW,CAAC;IAC7C,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,MAAM,CAAC;IACnC,IAAI,MAAM,CAAC,cAAc,EAAE;QAAE,OAAO,SAAS,CAAC;IAC9C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,KAAa,EACb,QAAgB,EAChB,QAAmC;IAEnC,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC;IAC/B,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO;IACpC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,OAAO,CAAC,KAAK,CACX,qBAAqB,IAAI,WAAW,KAAK,aAAa,QAAQ,qBAAqB,MAAM,CAAC,QAAQ,CAAC,EAAE,CACtG,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAuB;IAClD,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,GAAG;SACP,KAAK,CAAC,SAAS,CAAC;SAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,gFAAgF;AAEhF,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;AAEvD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,CAAC;AAE7E,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CAC/C,CAAC,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAElF,uFAAuF;AACvF,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,EAAU;IACtD,OAAO,WAAW,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,gFAAgF;AAEhF,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,CAAC;AAC7B,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC;AAE1B,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAS;IAC5C,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;CACP,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,OAAe,EAAU,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAEpG,SAAS,qBAAqB,CAAC,OAAe;IAC5C,MAAM,OAAO,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC;IACnD,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClD,MAAM,QAAQ,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACnE,OAAO,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,aAAqB;IAChE,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC/C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;QAChD,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,aAAqB;IAC9D,iFAAiF;IACjF,6EAA6E;IAC7E,gFAAgF;IAChF,mDAAmD;IACnD,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IAC5C,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,aAAqB,EAAW,EAAE,CACnE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC;AAEtD;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,aAAqB,EAAW,EAAE,CACxE,UAAU,CAAC,CAAC,CAAC,0BAA0B,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;AAE7F,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,IAAI,QAAQ,KAAK,GAAG,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAChF,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC9C,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAS;IACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAExC,sEAAsE;IACtE,oDAAoD;IACpD,yEAAyE;IACzE,wEAAwE;IACxE,0DAA0D;IAC1D,IAAI,UAAU,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,UAAU,EAAE,CAAC;QAChF,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,sBAAsB,GAAG,UAAU,IAAI,QAAQ,EAAE,KAAK,QAAQ,CAAC;AAErE,SAAS,0BAA0B,CAAC,KAAa;IAC/C,OAAO,sBAAsB,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAa;IACpD,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,YAAY,GAAG,0BAA0B,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/D,MAAM,aAAa,GAAG,0BAA0B,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IACjE,OAAO,YAAY,KAAK,aAAa,CAAC;AACxC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,0BAA0B,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,OAAO,CACL,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAC9F,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,EAAE,CAC7C,KAAK,CAAC,IAAI,CACR,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EACvB,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC3E,CAAC,IAAI,CAAC,EAAE,CAAC,CACX,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,aAAqB,EAAE,cAAsB;IACrF,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC9C,IAAI,aAAa,KAAK,YAAY;QAAE,OAAO,cAAc,CAAC;IAC1D,OAAO,eAAe,CAAC,aAAa,CAAC,KAAK,eAAe,CAAC,YAAY,CAAC;QACrE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC;QAC9C,CAAC,CAAC,cAAc,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;IAEnC,oEAAoE;IACpE,IAAI,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC;QACjC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;AAClG,CAAC;AAED,yEAAyE;AACzE,mEAAmE;AACnE,MAAM,eAAe,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,UAAU,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;AAE9F,MAAM,UAAU,qBAAqB,CACnC,mBAA2B,EAC3B,mBAA2B;IAE3B,MAAM,IAAI,GAAG,0BAA0B,CAAC,mBAAmB,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,0BAA0B,CAAC,mBAAmB,CAAC,CAAC;IAElE,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAE9C,IAAI,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,OAAO,eAAe,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,cAAsB,EACtB,WAA8B;IAE9B,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;AAC/E,CAAC","sourcesContent":["import { homedir, platform } from 'node:os';\nimport {\n basename,\n delimiter,\n dirname,\n join,\n parse,\n relative,\n resolve,\n sep,\n win32,\n} from 'node:path';\n\n// ─── Shared primitives (no intra-package imports; every core module may import this) ──\n\nexport type EntryType = 'file' | 'directory' | 'symlink' | 'other';\n\n/** The three predicates both `Dirent` and `Stats` expose - all `resolveEntryType` needs. */\nexport interface DirentLike {\n isDirectory(): boolean;\n isFile(): boolean;\n isSymbolicLink(): boolean;\n}\n\nexport function resolveEntryType(dirent: DirentLike): EntryType {\n if (dirent.isDirectory()) return 'directory';\n if (dirent.isFile()) return 'file';\n if (dirent.isSymbolicLink()) return 'symlink';\n return 'other';\n}\n\nconst warnedSettings = new Set<string>();\n\n/**\n * Warn once per (setting, value) that a configured value was rejected, and say\n * what was used instead. console.error, not Logger: this module stays\n * dependency-free to avoid import cycles, and a startup misconfiguration is\n * worth printing even when the operator asked for errors only.\n */\nexport function warnInvalidSetting(\n name: string,\n value: string,\n expected: string,\n fallback: string | number | boolean,\n): void {\n const key = `${name}:${value}`;\n if (warnedSettings.has(key)) return;\n warnedSettings.add(key);\n console.error(\n `[warning] Invalid ${name} value: ${value} (must be ${expected}). Using default: ${String(fallback)}`,\n );\n}\n\nexport function splitDirList(val: string | undefined): string[] {\n if (!val) return [];\n return val\n .split(delimiter)\n .map((p) => p.trim())\n .filter((p) => p.length > 0);\n}\n\n// ─── Character-level path primitives ─────────────────────────────────────────\n\nexport const IS_WINDOWS = process.platform === 'win32';\n\nexport const isSlash = (code: number): boolean => code === 47 || code === 92;\n\nexport const isAlpha = (code: number): boolean =>\n (code >= 65 && code <= 90) || (code >= 97 && code <= 122);\n\nexport const toPosixPath = (value: string): string => value.replaceAll('\\\\', '/');\n\n/** `path.relative` with forward slashes, so displayed paths match across platforms. */\nexport function toPosixRelative(from: string, to: string): string {\n return toPosixPath(relative(from, to));\n}\n\n// ─── Path normalization and containment ──────────────────────────────────────\n\nexport const CHAR_COLON = 58;\nconst HOMEDIR = homedir();\n\nconst RESERVED_DEVICE_NAMES = new Set<string>([\n 'CON',\n 'PRN',\n 'AUX',\n 'NUL',\n 'COM1',\n 'COM2',\n 'COM3',\n 'COM4',\n 'COM5',\n 'COM6',\n 'COM7',\n 'COM8',\n 'COM9',\n 'LPT1',\n 'LPT2',\n 'LPT3',\n 'LPT4',\n 'LPT5',\n 'LPT6',\n 'LPT7',\n 'LPT8',\n 'LPT9',\n]);\n\n/**\n * Win32 strips trailing dots and spaces at the syscall boundary (a request for\n * `.env ` creates `.env`), so every name check must trim them first or it is\n * bypassed on Windows.\n */\nexport const trimTrailingDotsAndSpaces = (segment: string): string => segment.replace(/[. ]+$/, '');\n\nfunction getReservedDeviceName(segment: string): string | undefined {\n const trimmed = trimTrailingDotsAndSpaces(segment);\n const withoutStream = trimmed.split(':')[0] ?? '';\n const baseName = (withoutStream.split('.')[0] ?? '').toUpperCase();\n return RESERVED_DEVICE_NAMES.has(baseName) ? baseName : undefined;\n}\n\nexport function getReservedDeviceNameForPath(requestedPath: string): string | undefined {\n const segments = requestedPath.split(/[\\\\/]/u);\n for (const segment of segments) {\n const reserved = getReservedDeviceName(segment);\n if (reserved) {\n return reserved;\n }\n }\n return undefined;\n}\n\nexport function isWindowsDriveRelativePath(requestedPath: string): boolean {\n // Check on all platforms so cross-platform clients cannot smuggle drive-relative\n // inputs (e.g. C:relative) to a POSIX-hosted server where path.resolve would\n // silently expand them relative to CWD. win32.parse is the platform-independent\n // binding, so this reads the same on a POSIX host.\n const { root } = win32.parse(requestedPath);\n return root.length === 2;\n}\n\n/**\n * True for a path that names a Windows drive: `C:\\x`, `C:/x`, `C:`, and the\n * drive-relative `C:x`. Only Windows resolves one. On POSIX it is a relative\n * path, so `C:\\x` would become a file of that name under the root.\n */\nexport const isWindowsDrivePath = (requestedPath: string): boolean =>\n /^[A-Za-z]:/u.test(win32.parse(requestedPath).root);\n\n/**\n * A drive path this host refuses before resolving it: drive-relative anywhere,\n * and any drive path where the host is not Windows.\n */\nexport const isUnresolvableDrivePath = (requestedPath: string): boolean =>\n IS_WINDOWS ? isWindowsDriveRelativePath(requestedPath) : isWindowsDrivePath(requestedPath);\n\nexport function expandHome(filepath: string): string {\n if (filepath === '~' || filepath.startsWith('~/') || filepath.startsWith('~\\\\')) {\n const rest = filepath.slice(1).replace(/^[/\\\\]+/, '');\n return rest ? join(HOMEDIR, rest) : HOMEDIR;\n }\n return filepath;\n}\n\nexport function normalizePath(p: string): string {\n const resolved = resolve(expandHome(p));\n\n // On Windows only the drive letter is lowercased (e.g. \"C:\\Foo\\Bar\").\n // The rest of the path retains its original casing.\n // IMPORTANT: callers must use isSamePath / isPathInsideDirectory for all\n // equality and containment checks — never raw string equality — because\n // those helpers apply full case-folding before comparing.\n if (IS_WINDOWS && resolved.length >= 2 && resolved.charCodeAt(1) === CHAR_COLON) {\n return resolved.charAt(0).toLowerCase() + resolved.slice(1);\n }\n\n return resolved;\n}\n\nconst IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';\n\nfunction normalizeCaseForComparison(value: string): string {\n return IS_CASE_INSENSITIVE_FS ? value.toLowerCase() : value;\n}\n\nexport function isSamePath(left: string, right: string): boolean {\n if (left === right) {\n return true;\n }\n const leftResolved = normalizeCaseForComparison(resolve(left));\n const rightResolved = normalizeCaseForComparison(resolve(right));\n return leftResolved === rightResolved;\n}\n\n/**\n * What two names share when they differ only in Unicode form (NFC or NFD) or\n * in which space character they use (U+0020, U+00A0, U+202F, and every other\n * Zs), case-folded where the filesystem folds case. A model types back\n * `caf\\u00e9` and `shot PM.png` for the `cafe\\u0301` and `shot\\u202fPM.png` a\n * listing showed it (#101).\n */\nexport function unicodeMatchKey(name: string): string {\n return normalizeCaseForComparison(name.normalize('NFC').replace(/\\p{Zs}/gu, ' '));\n}\n\n/**\n * True when two paths are one path once case (where the filesystem folds it)\n * and Unicode spelling are folded: the spellings the path guard resolves to\n * one entry when only one of them is on disk.\n */\nexport function isEquivalentPath(left: string, right: string): boolean {\n return (\n isSamePath(left, right) || unicodeMatchKey(resolve(left)) === unicodeMatchKey(resolve(right))\n );\n}\n\n/**\n * False for a name of printable ASCII with no space, which no other entry can\n * share a `unicodeMatchKey` with — except a name holding U+212A, U+037E or\n * U+1FEF, which NFC maps to ASCII; those stay unmatched. A missed lookup of\n * such a name skips the directory scan, so creating `new-file.ts` costs what\n * it did.\n */\nexport function mayHaveEquivalentSpelling(name: string): boolean {\n return /[^\\x21-\\x7e]/u.test(name);\n}\n\n/** `name` with each non-ASCII code unit as a `\\uXXXX` escape, which a JSON string argument accepts. */\nexport function escapeNonAscii(name: string): string {\n return name.replace(/[^\\x20-\\x7e]/gu, (char) =>\n Array.from(\n { length: char.length },\n (_, index) => `\\\\u${char.charCodeAt(index).toString(16).padStart(4, '0')}`,\n ).join(''),\n );\n}\n\n/**\n * A write target that already exists is realpathed to its on-disk spelling,\n * so a rename that changes only case (`foo.txt` -> `Foo.txt` on a\n * case-insensitive filesystem) or only Unicode spelling (`cafe\\u0301.txt` ->\n * `caf\\u00e9.txt`) resolves to the source itself. Returns the resolved target\n * re-spelled with the requested basename when the two differ only that way;\n * otherwise the resolved target unchanged. Parent segments are not re-spelled.\n */\nexport function respellSpellingOnlyTarget(requestedPath: string, resolvedTarget: string): string {\n const requestedName = basename(requestedPath);\n const resolvedName = basename(resolvedTarget);\n if (requestedName === resolvedName) return resolvedTarget;\n return unicodeMatchKey(requestedName) === unicodeMatchKey(resolvedName)\n ? join(dirname(resolvedTarget), requestedName)\n : resolvedTarget;\n}\n\nexport function normalizeAllowedDirectory(dir: string): string {\n const trimmed = dir.trim();\n if (trimmed.length === 0) return '';\n\n const normalized = normalizePath(trimmed);\n const { root } = parse(normalized);\n\n // Keep filesystem roots as-is (\"/\", \"c:\\\\\", \"\\\\\\\\server\\\\share\\\\\").\n if (isSamePath(normalized, root)) {\n return root;\n }\n\n return normalized.length > 1 && normalized.endsWith(sep) ? normalized.slice(0, -1) : normalized;\n}\n\n// `\\` separates path segments only on Windows. On POSIX it is a filename\n// character, so `/root\\x` names a sibling of `/root`, not a child.\nconst isPathSeparator = (code: number): boolean => code === 47 || (IS_WINDOWS && code === 92);\n\nexport function isPathInsideDirectory(\n normalizedDirectory: string,\n normalizedCandidate: string,\n): boolean {\n const root = normalizeCaseForComparison(normalizedDirectory);\n const candidate = normalizeCaseForComparison(normalizedCandidate);\n\n if (root === candidate) return true;\n if (!candidate.startsWith(root)) return false;\n\n if (isPathSeparator(root.charCodeAt(root.length - 1))) return true;\n return isPathSeparator(candidate.charCodeAt(root.length));\n}\n\nexport function isPathWithinDirectories(\n normalizedPath: string,\n allowedDirs: readonly string[],\n): boolean {\n return allowedDirs.some((dir) => isPathInsideDirectory(dir, normalizedPath));\n}\n"]}
@@ -3,6 +3,19 @@ export interface ValidatedPathDetails {
3
3
  resolvedPath: string;
4
4
  isSymlink: boolean;
5
5
  }
6
+ /** How one walk prints the paths it found; see `PathGuard.resultPaths`. */
7
+ export interface ResultPaths {
8
+ /** The root the relative paths are relative to, as configured; the walked directory when there is none. */
9
+ readonly root: string;
10
+ /** The walked directory is that root itself. */
11
+ readonly atRoot: boolean;
12
+ show(absolutePath: string): string;
13
+ }
14
+ /** A root and every spelling that names it: as configured, and its realpath when that differs. */
15
+ interface RootSpellings {
16
+ readonly root: string;
17
+ readonly spellings: readonly string[];
18
+ }
6
19
  export interface ServerOptions {
7
20
  allowCwd?: boolean;
8
21
  cliAllowedDirs?: string[];
@@ -13,11 +26,16 @@ export declare function resolveAllowedDirectoriesState(dirs: readonly string[],
13
26
  /**
14
27
  * TOCTOU note: validation returns a plain string and the fs operation is a
15
28
  * later syscall, so a symlink swapped in between could redirect it. Reads and
16
- * appends in core/fs.ts open with O_NOFOLLOW (final component)
17
- * and re-check the fd's real path on Linux (ancestor swap). Residual: write
18
- * paths that resolve a parent by name (`rename`, `mkdir`, `rm`, `cp`) would
19
- * need openat()-relative-to-dirfd, which Node lacks; non-Linux skips the fd
20
- * re-check.
29
+ * appends in core/fs.ts open with O_NOFOLLOW (final component) and re-check the
30
+ * fd's real path on Linux (ancestor swap). stat's directory count opens and
31
+ * re-checks the directory, and reads entries through that descriptor, only on
32
+ * Linux with procfs; elsewhere it reads the directory by path, so the swap
33
+ * window stays there, like the read path's non-Linux residual. Residuals: the
34
+ * directory walker behind list and the search tools (core/glob.ts) still
35
+ * resolves each entry by path, as the count does for a symlinked entry;
36
+ * write paths that resolve a parent by name
37
+ * (`rename`, `mkdir`, `rm`, `cp`) would need openat()-relative-to-dirfd, which
38
+ * Node lacks; non-Linux skips the fd re-check.
21
39
  */
22
40
  export declare class PathGuard {
23
41
  private allowedDirectoriesState;
@@ -39,14 +57,28 @@ export declare class PathGuard {
39
57
  * field — so the recompute can rebuild from it without re-filtering.
40
58
  */
41
59
  private grantedDirectories;
60
+ /**
61
+ * The grants a user approved for a path a tool asked about, as opposed to
62
+ * the workspace roots a client declared. Relative paths never resolve
63
+ * against these: one approval must not change what a relative path names,
64
+ * neither in a later round of the same call nor in a later call.
65
+ */
66
+ private accessGrants;
67
+ /**
68
+ * Where relative paths resolve: the roots minus `accessGrants`, each with
69
+ * its spellings, plus every spelling flattened for containment.
70
+ */
71
+ private relativeBases;
42
72
  private rootBoundaries;
43
73
  private readonly options;
44
74
  constructor(options?: ServerOptions);
45
75
  /**
46
76
  * `expanded` is the containment set (each root plus its realpath alias);
47
77
  * `roots` is what a caller choosing "the root" sees, one entry per root.
78
+ * `relativeBases` lists the roots relative paths resolve against, with
79
+ * their spellings; without it, every root, each spelled only as given.
48
80
  */
49
- initialize(expanded: readonly string[], roots?: readonly string[]): void;
81
+ initialize(expanded: readonly string[], roots?: readonly string[], relativeBases?: readonly RootSpellings[]): void;
50
82
  isInitialized(): boolean;
51
83
  getAllowedDirectories(): string[];
52
84
  /**
@@ -63,6 +95,16 @@ export declare class PathGuard {
63
95
  */
64
96
  getRoots(): string[];
65
97
  isSensitive(filePath: string): boolean;
98
+ /**
99
+ * True when a walked entry is inside the allowed directories and not sensitive,
100
+ * on both its requested and resolved paths. Every entry is realpath-resolved: a
101
+ * symlinked ancestor pointing outside the sandbox would otherwise pass the
102
+ * lexical check (fs.glob follows symlinks and yields external entries as
103
+ * non-symlink dirents). Escapes and sensitive targets throw ACCESS_DENIED and
104
+ * skippable errors filter the entry; anything else is fatal. The accepted
105
+ * TOCTOU window is documented on this class.
106
+ */
107
+ isEntryAccessible(entryPath: string): Promise<boolean>;
66
108
  validateExistingPath(requestedPath: string): Promise<string>;
67
109
  /**
68
110
  * True when a grant must never admit `targetDir` — an unsafe path (home,
@@ -98,20 +140,82 @@ export declare class PathGuard {
98
140
  * pre-checks passed. The caller leaves a refused path to fail with
99
141
  * ACCESS_DENIED during the operation. Idempotent: re-granting an already
100
142
  * allowed directory is a no-op via the dedup in `initialize`.
143
+ *
144
+ * `kind` says where the grant came from: `access` for a path the user
145
+ * approved when a tool asked, `workspace` for a root the client declared.
146
+ * Only a workspace root becomes a place relative paths resolve.
101
147
  */
102
- applyGrant(targetDir: string): Promise<boolean>;
148
+ applyGrant(targetDir: string, kind?: 'access' | 'workspace'): Promise<boolean>;
103
149
  private isWithinBoundary;
104
150
  private validateAccess;
105
151
  private handleRealpathError;
106
152
  validateExistingPathDetailed(requestedPath: string): Promise<ValidatedPathDetails>;
107
153
  validateExistingDirectory(requestedPath: string): Promise<string>;
108
154
  resolvePathOrRoot(pathValue: string | undefined): string;
155
+ /**
156
+ * Where the relative paths a caller sent should point, keyed by the spelling
157
+ * sent; a path that keeps its meaning is absent. A relative path resolves
158
+ * against the working directory, as it always has, while that lands inside
159
+ * a root. When it does not — a desktop host starts the server outside the
160
+ * project, and an HTTP client never shares its working directory — it
161
+ * resolves against the roots: the one root, or with several, the one it
162
+ * exists under. Searches print paths relative to the root (`resultPaths`),
163
+ * so this is what lets one tool's output feed the next.
164
+ *
165
+ * "Roots" here are the configured ones and those a client declared, never an
166
+ * access grant (see `accessGrants`). Absolute, `~` and drive paths keep their
167
+ * meaning, as does every path when there is no such root; validation judges
168
+ * them as before. A Windows drive path on POSIX is left for validation to
169
+ * refuse: joined to a root, `C:\x` would be an absolute path that passes. A
170
+ * relative path that exists under several roots, or under none of several,
171
+ * is refused rather than guessed.
172
+ */
173
+ resolveRelativePaths(paths: readonly string[]): Promise<Map<string, string>>;
174
+ private resolveAgainstRoots;
175
+ /**
176
+ * How a tool prints the paths a walk of `walkedDir` found, so the caller can
177
+ * pass each one straight back: relative to the root holding `walkedDir`
178
+ * when `resolveRelativePaths` maps that spelling back to the same path,
179
+ * absolute otherwise. A scoped search of `src` prints `src/a.ts`, not
180
+ * `a.ts`, which would name `<root>/a.ts`. `root` is the root as configured,
181
+ * as `list_roots` shows it, even when the walk went through its realpath.
182
+ *
183
+ * The absolute fallback covers a working directory in a subfolder of a root
184
+ * (relative paths resolve there), a directory under an access grant only
185
+ * (relative paths never resolve there), several roots with the working
186
+ * directory outside them (a name two roots share would read back as
187
+ * ambiguous, and which names are shared can change between the print and
188
+ * the read), and a name that would not read back as given: `~`, blank, or a
189
+ * POSIX name holding a backslash or starting with a drive (`C:`).
190
+ */
191
+ resultPaths(walkedDir: string): ResultPaths;
109
192
  isAllowedRoot(normalizedPath: string): boolean;
110
193
  private assertNotSensitiveFile;
111
194
  private throwAccessDenied;
112
195
  private resolveNearestExistingAncestor;
196
+ /**
197
+ * `normalized` re-spelled with the on-disk names of the components that are
198
+ * missing as typed but present under another Unicode spelling (#101), or
199
+ * `undefined` when no component changed. Only names below a root are
200
+ * matched, never the root's own. Each directory is scanned only after its
201
+ * real path is checked to be inside the roots, and each matched entry is
202
+ * realpathed and checked again, so a link found this way cannot lead out
203
+ * (`resolveLast: false` leaves the last one unresolved, for delete). The
204
+ * first component with no match ends the walk; it and the rest stay as
205
+ * typed, so a write still creates them. The result is a spelling only:
206
+ * the caller runs its own containment and denylist checks on it.
207
+ */
208
+ private respellMissing;
209
+ /**
210
+ * The entry of `realDir` that `name` names: `name` itself when it is
211
+ * there, else the one entry with the same `unicodeMatchKey`. Two or more
212
+ * such entries are refused, never chosen between. `undefined` when none
213
+ * matches or the directory cannot be read.
214
+ */
215
+ private findEquivalentEntry;
113
216
  validatePathForWrite(requestedPath: string): Promise<string>;
114
217
  validatePathForDelete(requestedPath: string): Promise<string>;
115
218
  recomputeAllowedDirectories(): Promise<void>;
116
219
  }
220
+ export {};
117
221
  //# sourceMappingURL=path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/core/path.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA0DD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAE7E;AAmBD,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAWnB;AAED;;;;;;;;GAQG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,uBAAuB,CAAuB;IACtD,8EAA8E;IAC9E,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,kBAAkB,CAAgB;IAC1C,OAAO,CAAC,cAAc,CAAgB;IAEtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;gBAExC,OAAO,CAAC,EAAE,aAAa;IAInC;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,GAAE,SAAS,MAAM,EAAa,GAAG,IAAI;IAKlF,aAAa,IAAI,OAAO;IAIxB,qBAAqB,IAAI,MAAM,EAAE;IAOjC;;;;;OAKG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAc/D;;;;OAIG;IACH,QAAQ,IAAI,MAAM,EAAE;IAIpB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIhC,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKlE;;;;;;;;OAQG;YACW,mBAAmB;IAYjC,8EAA8E;YAChE,qBAAqB;IAgBnC;;;;;;;;;;OAUG;IACG,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiCjE;;;;;;;;;;OAUG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAwCvC,gBAAgB;IAQ9B,OAAO,CAAC,cAAc;YAwDR,mBAAmB;IA0C3B,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAmClF,yBAAyB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAsBvE,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM;IAwBxD,aAAa,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAW9C,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,iBAAiB;YAOX,8BAA8B;IAwDtC,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoB5D,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoE7D,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC;CAwDnD"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/core/path.ts"],"names":[],"mappings":"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"}