@j0hanz/filesystem-mcp 1.19.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +333 -838
- package/dist/cli-help.d.ts +3 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +169 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +168 -123
- package/dist/cli.js.map +1 -0
- package/dist/core/concurrency.d.ts +70 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +173 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.d.ts +19 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +11 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cursor.d.ts +23 -0
- package/dist/core/cursor.d.ts.map +1 -0
- package/dist/core/cursor.js +62 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/errors.d.ts +96 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +356 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/file-uri.d.ts +24 -0
- package/dist/core/file-uri.d.ts.map +1 -0
- package/dist/core/file-uri.js +52 -0
- package/dist/core/file-uri.js.map +1 -0
- package/dist/core/fmt.d.ts +38 -0
- package/dist/core/fmt.d.ts.map +1 -0
- package/dist/core/fmt.js +109 -0
- package/dist/core/fmt.js.map +1 -0
- package/dist/core/fs.d.ts +108 -0
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/fs.js +243 -0
- package/dist/core/fs.js.map +1 -0
- package/dist/core/glob.d.ts +41 -0
- package/dist/core/glob.d.ts.map +1 -0
- package/dist/core/glob.js +399 -0
- package/dist/core/glob.js.map +1 -0
- package/dist/core/input-required.d.ts +147 -0
- package/dist/core/input-required.d.ts.map +1 -0
- package/dist/core/input-required.js +261 -0
- package/dist/core/input-required.js.map +1 -0
- package/dist/core/mime.d.ts +15 -0
- package/dist/core/mime.d.ts.map +1 -0
- package/dist/core/mime.js +188 -0
- package/dist/core/mime.js.map +1 -0
- package/dist/core/observability.d.ts +10 -0
- package/dist/core/observability.d.ts.map +1 -0
- package/dist/core/observability.js +76 -0
- package/dist/core/observability.js.map +1 -0
- package/dist/core/page-store.d.ts +41 -0
- package/dist/core/page-store.d.ts.map +1 -0
- package/dist/core/page-store.js +74 -0
- package/dist/core/page-store.js.map +1 -0
- package/dist/core/path-completer.d.ts +10 -0
- package/dist/core/path-completer.d.ts.map +1 -0
- package/dist/core/path-completer.js +227 -0
- package/dist/core/path-completer.js.map +1 -0
- package/dist/core/path-discovery.d.ts +8 -0
- package/dist/core/path-discovery.d.ts.map +1 -0
- package/dist/core/path-discovery.js +105 -0
- package/dist/core/path-discovery.js.map +1 -0
- package/dist/core/path-utils.d.ts +9 -0
- package/dist/core/path-utils.d.ts.map +1 -0
- package/dist/core/path-utils.js +121 -0
- package/dist/core/path-utils.js.map +1 -0
- package/dist/core/path.d.ts +148 -0
- package/dist/core/path.d.ts.map +1 -0
- package/dist/core/path.js +674 -0
- package/dist/core/path.js.map +1 -0
- package/dist/core/primitives.d.ts +23 -0
- package/dist/core/primitives.d.ts.map +1 -0
- package/dist/core/primitives.js +54 -0
- package/dist/core/primitives.js.map +1 -0
- package/dist/core/read.d.ts +74 -0
- package/dist/core/read.d.ts.map +1 -0
- package/dist/core/read.js +562 -0
- package/dist/core/read.js.map +1 -0
- package/dist/core/schema.d.ts +139 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +345 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.d.ts +100 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +248 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/sensitive.d.ts +6 -0
- package/dist/core/sensitive.d.ts.map +1 -0
- package/dist/core/sensitive.js +142 -0
- package/dist/core/sensitive.js.map +1 -0
- package/dist/core/store.d.ts +57 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/util.d.ts +26 -0
- package/dist/core/util.d.ts.map +1 -0
- package/dist/core/util.js +66 -0
- package/dist/core/util.js.map +1 -0
- package/dist/core/watcher-registry.d.ts +68 -0
- package/dist/core/watcher-registry.d.ts.map +1 -0
- package/dist/core/watcher-registry.js +312 -0
- package/dist/core/watcher-registry.js.map +1 -0
- package/dist/http-policy.d.ts +121 -0
- package/dist/http-policy.d.ts.map +1 -0
- package/dist/http-policy.js +340 -0
- package/dist/http-policy.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -41
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +71 -0
- package/dist/instructions.js.map +1 -0
- package/dist/prompts.d.ts +8 -6
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +74 -168
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +73 -9
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +392 -163
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +62 -1
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +139 -1
- package/dist/server.js.map +1 -0
- package/dist/tools/batch.d.ts +89 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +157 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/create.d.ts +2 -0
- package/dist/tools/create.d.ts.map +1 -0
- package/dist/tools/create.js +142 -0
- package/dist/tools/create.js.map +1 -0
- package/dist/tools/define.d.ts +110 -0
- package/dist/tools/define.d.ts.map +1 -0
- package/dist/tools/define.js +394 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/delete-file.d.ts +2 -4
- package/dist/tools/delete-file.d.ts.map +1 -0
- package/dist/tools/delete-file.js +330 -87
- package/dist/tools/delete-file.js.map +1 -0
- package/dist/tools/diff.d.ts +2 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +94 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +442 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +26 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/list.d.ts +2 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/list.js +291 -0
- package/dist/tools/list.js.map +1 -0
- package/dist/tools/move.d.ts +2 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +321 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/patch.d.ts +2 -0
- package/dist/tools/patch.d.ts.map +1 -0
- package/dist/tools/patch.js +155 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/progress.d.ts +52 -0
- package/dist/tools/progress.d.ts.map +1 -0
- package/dist/tools/progress.js +172 -0
- package/dist/tools/progress.js.map +1 -0
- package/dist/tools/read.d.ts +2 -4
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +398 -152
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/replace-in-files.d.ts +2 -4
- package/dist/tools/replace-in-files.d.ts.map +1 -0
- package/dist/tools/replace-in-files.js +435 -264
- package/dist/tools/replace-in-files.js.map +1 -0
- package/dist/tools/roots.d.ts +2 -4
- package/dist/tools/roots.d.ts.map +1 -0
- package/dist/tools/roots.js +29 -52
- package/dist/tools/roots.js.map +1 -0
- package/dist/tools/search-content.d.ts +2 -4
- package/dist/tools/search-content.d.ts.map +1 -0
- package/dist/tools/search-content.js +262 -228
- package/dist/tools/search-content.js.map +1 -0
- package/dist/tools/search-files.d.ts +2 -4
- package/dist/tools/search-files.d.ts.map +1 -0
- package/dist/tools/search-files.js +162 -161
- package/dist/tools/search-files.js.map +1 -0
- package/dist/tools/stat.d.ts +2 -4
- package/dist/tools/stat.d.ts.map +1 -0
- package/dist/tools/stat.js +202 -62
- package/dist/tools/stat.js.map +1 -0
- package/dist/transport/http.d.ts +5 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +276 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/shared.d.ts +45 -0
- package/dist/transport/shared.d.ts.map +1 -0
- package/dist/transport/shared.js +65 -0
- package/dist/transport/shared.js.map +1 -0
- package/dist/transport/stdio.d.ts +32 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +279 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +4 -0
- package/dist/transport.js.map +1 -0
- package/package.json +37 -46
- package/dist/assets/logo.svg +0 -35
- package/dist/completions.d.ts +0 -2
- package/dist/completions.js +0 -572
- package/dist/config.d.ts +0 -119
- package/dist/config.js +0 -31
- package/dist/lib/abort.d.ts +0 -7
- package/dist/lib/abort.js +0 -81
- package/dist/lib/constants.d.ts +0 -42
- package/dist/lib/constants.js +0 -313
- package/dist/lib/errors.d.ts +0 -28
- package/dist/lib/errors.js +0 -301
- package/dist/lib/file-operations/core.d.ts +0 -53
- package/dist/lib/file-operations/core.js +0 -144
- package/dist/lib/file-operations/metadata.d.ts +0 -77
- package/dist/lib/file-operations/metadata.js +0 -878
- package/dist/lib/file-operations/search.d.ts +0 -46
- package/dist/lib/file-operations/search.js +0 -1194
- package/dist/lib/file-operations/traversal.d.ts +0 -40
- package/dist/lib/file-operations/traversal.js +0 -310
- package/dist/lib/fs-helpers.d.ts +0 -46
- package/dist/lib/fs-helpers.js +0 -606
- package/dist/lib/globs.d.ts +0 -2
- package/dist/lib/globs.js +0 -19
- package/dist/lib/logger.d.ts +0 -27
- package/dist/lib/logger.js +0 -91
- package/dist/lib/observability.d.ts +0 -33
- package/dist/lib/observability.js +0 -373
- package/dist/lib/paths.d.ts +0 -35
- package/dist/lib/paths.js +0 -634
- package/dist/lib/resource-store.d.ts +0 -28
- package/dist/lib/resource-store.js +0 -195
- package/dist/lib/utils.d.ts +0 -7
- package/dist/lib/utils.js +0 -37
- package/dist/lib/zod-codecs.d.ts +0 -2
- package/dist/lib/zod-codecs.js +0 -18
- package/dist/pkg-info.d.ts +0 -7
- package/dist/pkg-info.js +0 -9
- package/dist/resources/generated-instructions.d.ts +0 -1
- package/dist/resources/generated-instructions.js +0 -105
- package/dist/resources/tool-catalog.d.ts +0 -2
- package/dist/resources/tool-catalog.js +0 -78
- package/dist/resources/tool-info.d.ts +0 -9
- package/dist/resources/tool-info.js +0 -206
- package/dist/resources/workflows.d.ts +0 -1
- package/dist/resources/workflows.js +0 -72
- package/dist/schemas.d.ts +0 -505
- package/dist/schemas.js +0 -676
- package/dist/server/bootstrap.d.ts +0 -6
- package/dist/server/bootstrap.js +0 -563
- package/dist/server/event-store.d.ts +0 -18
- package/dist/server/event-store.js +0 -71
- package/dist/server/roots-manager.d.ts +0 -28
- package/dist/server/roots-manager.js +0 -227
- package/dist/server/task-store.d.ts +0 -10
- package/dist/server/task-store.js +0 -91
- package/dist/tools/apply-patch.d.ts +0 -4
- package/dist/tools/apply-patch.js +0 -235
- package/dist/tools/calculate-hash.d.ts +0 -4
- package/dist/tools/calculate-hash.js +0 -187
- package/dist/tools/contract.d.ts +0 -50
- package/dist/tools/contract.js +0 -1
- package/dist/tools/create-directory.d.ts +0 -4
- package/dist/tools/create-directory.js +0 -68
- package/dist/tools/diff-files.d.ts +0 -4
- package/dist/tools/diff-files.js +0 -136
- package/dist/tools/edit-file.d.ts +0 -4
- package/dist/tools/edit-file.js +0 -256
- package/dist/tools/icons.d.ts +0 -15
- package/dist/tools/icons.js +0 -24
- package/dist/tools/list-directory.d.ts +0 -4
- package/dist/tools/list-directory.js +0 -218
- package/dist/tools/move-file.d.ts +0 -4
- package/dist/tools/move-file.js +0 -178
- package/dist/tools/read-multiple.d.ts +0 -4
- package/dist/tools/read-multiple.js +0 -207
- package/dist/tools/shared.d.ts +0 -172
- package/dist/tools/shared.js +0 -599
- package/dist/tools/stat-many.d.ts +0 -4
- package/dist/tools/stat-many.js +0 -104
- package/dist/tools/task-support.d.ts +0 -29
- package/dist/tools/task-support.js +0 -472
- package/dist/tools/tree.d.ts +0 -4
- package/dist/tools/tree.js +0 -102
- package/dist/tools/write-file.d.ts +0 -4
- package/dist/tools/write-file.js +0 -60
- package/dist/tools.d.ts +0 -5
- package/dist/tools.js +0 -54
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { cli } from './config.js';
|
|
2
|
+
// RFC 5424 severities, most severe first. A message is emitted when its
|
|
3
|
+
// severity is at least as high as the configured minimum.
|
|
4
|
+
const LEVEL_ORDER = [
|
|
5
|
+
'emergency',
|
|
6
|
+
'alert',
|
|
7
|
+
'critical',
|
|
8
|
+
'error',
|
|
9
|
+
'warning',
|
|
10
|
+
'notice',
|
|
11
|
+
'info',
|
|
12
|
+
'debug',
|
|
13
|
+
];
|
|
14
|
+
function isLoggingLevel(value) {
|
|
15
|
+
return LEVEL_ORDER.includes(value);
|
|
16
|
+
}
|
|
17
|
+
function parseLogLevel(raw) {
|
|
18
|
+
if (!raw)
|
|
19
|
+
return 'info';
|
|
20
|
+
// `warn` is the common short form; the canonical RFC 5424 level is `warning`.
|
|
21
|
+
if (raw === 'warn')
|
|
22
|
+
return 'warning';
|
|
23
|
+
if (isLoggingLevel(raw))
|
|
24
|
+
return raw;
|
|
25
|
+
console.error(`[warning] Invalid FS_LOG_LEVEL value: ${raw} (must be ${LEVEL_ORDER.join('|')}). Using default: info`);
|
|
26
|
+
return 'info';
|
|
27
|
+
}
|
|
28
|
+
// Seeded to the value `parseLogLevel(undefined)` returns, so an unset
|
|
29
|
+
// LOG_LEVEL — the initial `cachedRaw` — needs no first-call special case.
|
|
30
|
+
let cachedRaw;
|
|
31
|
+
let cachedLevel = 'info';
|
|
32
|
+
/**
|
|
33
|
+
* Minimum severity that reaches stderr, from `FS_LOG_LEVEL` / `--log-level`.
|
|
34
|
+
* Memoized on the raw value, not resolved once: `cli.logLevel` lands after
|
|
35
|
+
* `parseArgs`, and writes happen before that. Keying on the raw string keeps it
|
|
36
|
+
* live while the invalid-value warning fires once per setting, not per line.
|
|
37
|
+
*/
|
|
38
|
+
function getLogLevel() {
|
|
39
|
+
const raw = (cli.logLevel ?? process.env['FS_LOG_LEVEL'])?.trim().toLowerCase();
|
|
40
|
+
if (raw !== cachedRaw) {
|
|
41
|
+
cachedRaw = raw;
|
|
42
|
+
cachedLevel = parseLogLevel(raw);
|
|
43
|
+
}
|
|
44
|
+
return cachedLevel;
|
|
45
|
+
}
|
|
46
|
+
/** True when `level` is at least as severe as the configured minimum. */
|
|
47
|
+
function isLevelEnabled(level, minimum = getLogLevel()) {
|
|
48
|
+
return LEVEL_ORDER.indexOf(level) <= LEVEL_ORDER.indexOf(minimum);
|
|
49
|
+
}
|
|
50
|
+
function write(level, message, args) {
|
|
51
|
+
if (!isLevelEnabled(level))
|
|
52
|
+
return;
|
|
53
|
+
const prefix = `[${level}]`;
|
|
54
|
+
console.error(`${prefix} ${message}`, ...args);
|
|
55
|
+
}
|
|
56
|
+
export const Logger = {
|
|
57
|
+
emit: (level, message) => {
|
|
58
|
+
write(level, message, []);
|
|
59
|
+
},
|
|
60
|
+
info: (message, ...args) => {
|
|
61
|
+
write('info', message, args);
|
|
62
|
+
},
|
|
63
|
+
warn: (message, ...args) => {
|
|
64
|
+
write('warning', message, args);
|
|
65
|
+
},
|
|
66
|
+
error: (message, ...args) => {
|
|
67
|
+
write('error', message, args);
|
|
68
|
+
},
|
|
69
|
+
debug: (message, ...args) => {
|
|
70
|
+
write('debug', message, args);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
export function logRuntimeFailure(id, scope, method, error) {
|
|
74
|
+
write('error', `Runtime failure: ${id} [${scope}.${method}]`, [error]);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=observability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/core/observability.ts"],"names":[],"mappings":"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","sourcesContent":["import { cli } from './config.js';\n\nexport type LoggingLevel =\n 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';\n\n// RFC 5424 severities, most severe first. A message is emitted when its\n// severity is at least as high as the configured minimum.\nconst LEVEL_ORDER: readonly LoggingLevel[] = [\n 'emergency',\n 'alert',\n 'critical',\n 'error',\n 'warning',\n 'notice',\n 'info',\n 'debug',\n];\n\nfunction isLoggingLevel(value: string): value is LoggingLevel {\n return (LEVEL_ORDER as readonly string[]).includes(value);\n}\n\nfunction parseLogLevel(raw: string | undefined): LoggingLevel {\n if (!raw) return 'info';\n // `warn` is the common short form; the canonical RFC 5424 level is `warning`.\n if (raw === 'warn') return 'warning';\n if (isLoggingLevel(raw)) return raw;\n console.error(\n `[warning] Invalid FS_LOG_LEVEL value: ${raw} (must be ${LEVEL_ORDER.join('|')}). Using default: info`,\n );\n return 'info';\n}\n\n// Seeded to the value `parseLogLevel(undefined)` returns, so an unset\n// LOG_LEVEL — the initial `cachedRaw` — needs no first-call special case.\nlet cachedRaw: string | undefined;\nlet cachedLevel: LoggingLevel = 'info';\n\n/**\n * Minimum severity that reaches stderr, from `FS_LOG_LEVEL` / `--log-level`.\n * Memoized on the raw value, not resolved once: `cli.logLevel` lands after\n * `parseArgs`, and writes happen before that. Keying on the raw string keeps it\n * live while the invalid-value warning fires once per setting, not per line.\n */\nfunction getLogLevel(): LoggingLevel {\n const raw = (cli.logLevel ?? process.env['FS_LOG_LEVEL'])?.trim().toLowerCase();\n if (raw !== cachedRaw) {\n cachedRaw = raw;\n cachedLevel = parseLogLevel(raw);\n }\n return cachedLevel;\n}\n\n/** True when `level` is at least as severe as the configured minimum. */\nfunction isLevelEnabled(level: LoggingLevel, minimum: LoggingLevel = getLogLevel()): boolean {\n return LEVEL_ORDER.indexOf(level) <= LEVEL_ORDER.indexOf(minimum);\n}\n\nfunction write(level: LoggingLevel, message: string, args: readonly unknown[]): void {\n if (!isLevelEnabled(level)) return;\n const prefix = `[${level}]`;\n console.error(`${prefix} ${message}`, ...args);\n}\n\nexport const Logger = {\n emit: (level: LoggingLevel, message: string) => {\n write(level, message, []);\n },\n info: (message: string, ...args: unknown[]) => {\n write('info', message, args);\n },\n warn: (message: string, ...args: unknown[]) => {\n write('warning', message, args);\n },\n error: (message: string, ...args: unknown[]) => {\n write('error', message, args);\n },\n debug: (message: string, ...args: unknown[]) => {\n write('debug', message, args);\n },\n};\n\nexport function logRuntimeFailure(id: string, scope: string, method: string, error: unknown): void {\n write('error', `Runtime failure: ${id} [${scope}.${method}]`, [error]);\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FsError } from './errors.js';
|
|
2
|
+
export interface PageSnapshot<T = unknown, M = unknown> {
|
|
3
|
+
readonly items: readonly T[];
|
|
4
|
+
readonly metadata: M;
|
|
5
|
+
}
|
|
6
|
+
export interface PageSnapshotStoreOptions {
|
|
7
|
+
readonly maxSnapshots?: number;
|
|
8
|
+
readonly ttlMs?: number;
|
|
9
|
+
readonly now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The one cursor rejection: a cursor whose snapshot expired, was evicted, was
|
|
13
|
+
* already consumed past its end, or belongs to a different query. Every case
|
|
14
|
+
* has the same remedy, so they share one message.
|
|
15
|
+
*/
|
|
16
|
+
export declare function invalidCursor(): FsError;
|
|
17
|
+
/**
|
|
18
|
+
* Short-lived snapshots of a completed query's full result set, so later pages
|
|
19
|
+
* slice a stored array instead of re-scanning and re-sorting the filesystem.
|
|
20
|
+
*
|
|
21
|
+
* ponytail: bounded by snapshot count and TTL, not by bytes — 32 x 20,000
|
|
22
|
+
* `list` entries or 32 x 10,000 `search_text` matches, order of a few hundred MB
|
|
23
|
+
* held for 60s, and the HTTP leg shares one store so any caller can drive it.
|
|
24
|
+
* If that shows up as memory pressure, meter bytes the way `ResourceStore` does.
|
|
25
|
+
*/
|
|
26
|
+
export declare class PageSnapshotStore {
|
|
27
|
+
private readonly byId;
|
|
28
|
+
private readonly maxSnapshots;
|
|
29
|
+
private readonly ttlMs;
|
|
30
|
+
private readonly now;
|
|
31
|
+
constructor(options?: PageSnapshotStoreOptions);
|
|
32
|
+
private pruneExpired;
|
|
33
|
+
create(params: {
|
|
34
|
+
queryKey: string;
|
|
35
|
+
items: readonly unknown[];
|
|
36
|
+
metadata?: unknown;
|
|
37
|
+
}): string;
|
|
38
|
+
read<T, M = undefined>(snapshotId: string, queryKey: string): PageSnapshot<T, M>;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=page-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-store.d.ts","sourceRoot":"","sources":["../../src/core/page-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,OAAO,EAAE,MAAM,aAAa,CAAC;AAYjD,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAED;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,GAAE,wBAA6B;IAMlD,OAAO,CAAC,YAAY;IAOpB,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM;IAmB3F,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IAYhF,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ErrorCode, FsError } from './errors.js';
|
|
3
|
+
const DEFAULT_MAX_SNAPSHOTS = 32;
|
|
4
|
+
const DEFAULT_TTL_MS = 60 * 1000;
|
|
5
|
+
/**
|
|
6
|
+
* The one cursor rejection: a cursor whose snapshot expired, was evicted, was
|
|
7
|
+
* already consumed past its end, or belongs to a different query. Every case
|
|
8
|
+
* has the same remedy, so they share one message.
|
|
9
|
+
*/
|
|
10
|
+
export function invalidCursor() {
|
|
11
|
+
return new FsError(ErrorCode.INVALID_INPUT, 'Invalid cursor. Request the first page without a cursor.');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Short-lived snapshots of a completed query's full result set, so later pages
|
|
15
|
+
* slice a stored array instead of re-scanning and re-sorting the filesystem.
|
|
16
|
+
*
|
|
17
|
+
* ponytail: bounded by snapshot count and TTL, not by bytes — 32 x 20,000
|
|
18
|
+
* `list` entries or 32 x 10,000 `search_text` matches, order of a few hundred MB
|
|
19
|
+
* held for 60s, and the HTTP leg shares one store so any caller can drive it.
|
|
20
|
+
* If that shows up as memory pressure, meter bytes the way `ResourceStore` does.
|
|
21
|
+
*/
|
|
22
|
+
export class PageSnapshotStore {
|
|
23
|
+
byId = new Map();
|
|
24
|
+
maxSnapshots;
|
|
25
|
+
ttlMs;
|
|
26
|
+
now;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.maxSnapshots = options.maxSnapshots ?? DEFAULT_MAX_SNAPSHOTS;
|
|
29
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
30
|
+
this.now = options.now ?? Date.now;
|
|
31
|
+
}
|
|
32
|
+
pruneExpired() {
|
|
33
|
+
const now = this.now();
|
|
34
|
+
for (const [snapshotId, entry] of this.byId) {
|
|
35
|
+
if (entry.expiresAt <= now)
|
|
36
|
+
this.byId.delete(snapshotId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
create(params) {
|
|
40
|
+
this.pruneExpired();
|
|
41
|
+
const snapshotId = randomUUID();
|
|
42
|
+
this.byId.set(snapshotId, {
|
|
43
|
+
queryKey: params.queryKey,
|
|
44
|
+
items: params.items,
|
|
45
|
+
metadata: params.metadata,
|
|
46
|
+
expiresAt: this.now() + this.ttlMs,
|
|
47
|
+
});
|
|
48
|
+
// Oldest first: Map keeps insertion order and `read` re-inserts, so the
|
|
49
|
+
// first key is the least recently used snapshot.
|
|
50
|
+
while (this.byId.size > this.maxSnapshots) {
|
|
51
|
+
const oldest = this.byId.keys().next();
|
|
52
|
+
if (oldest.done)
|
|
53
|
+
break;
|
|
54
|
+
this.byId.delete(oldest.value);
|
|
55
|
+
}
|
|
56
|
+
return snapshotId;
|
|
57
|
+
}
|
|
58
|
+
read(snapshotId, queryKey) {
|
|
59
|
+
this.pruneExpired();
|
|
60
|
+
const entry = this.byId.get(snapshotId);
|
|
61
|
+
if (entry?.queryKey !== queryKey)
|
|
62
|
+
throw invalidCursor();
|
|
63
|
+
this.byId.delete(snapshotId);
|
|
64
|
+
this.byId.set(snapshotId, entry);
|
|
65
|
+
return {
|
|
66
|
+
items: entry.items,
|
|
67
|
+
metadata: entry.metadata,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
clear() {
|
|
71
|
+
this.byId.clear();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=page-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-store.js","sourceRoot":"","sources":["../../src/core/page-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,qBAAqB,GAAG,EAAE,CAAC;AACjC,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,CAAC;AAoBjC;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,OAAO,CAChB,SAAS,CAAC,aAAa,EACvB,0DAA0D,CAC3D,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,iBAAiB;IACX,IAAI,GAAG,IAAI,GAAG,EAA8B,CAAC;IAC7C,YAAY,CAAS;IACrB,KAAK,CAAS;IACd,GAAG,CAAe;IAEnC,YAAY,UAAoC,EAAE;QAChD,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;QAClE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,cAAc,CAAC;QAC7C,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACrC,CAAC;IAEO,YAAY;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,MAAM,CAAC,MAA2E;QAChF,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE;YACxB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK;SACnC,CAAC,CAAC;QACH,wEAAwE;QACxE,iDAAiD;QACjD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YACvC,IAAI,MAAM,CAAC,IAAI;gBAAE,MAAM;YACvB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,CAAmB,UAAkB,EAAE,QAAgB;QACzD,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACxC,IAAI,KAAK,EAAE,QAAQ,KAAK,QAAQ;YAAE,MAAM,aAAa,EAAE,CAAC;QACxD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACjC,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,KAAqB;YAClC,QAAQ,EAAE,KAAK,CAAC,QAAa;SAC9B,CAAC;IACJ,CAAC;IAED,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;CACF","sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { ErrorCode, FsError } from './errors.js';\n\nconst DEFAULT_MAX_SNAPSHOTS = 32;\nconst DEFAULT_TTL_MS = 60 * 1000;\n\ninterface StoredPageSnapshot {\n readonly queryKey: string;\n readonly items: readonly unknown[];\n readonly metadata: unknown;\n readonly expiresAt: number;\n}\n\nexport interface PageSnapshot<T = unknown, M = unknown> {\n readonly items: readonly T[];\n readonly metadata: M;\n}\n\nexport interface PageSnapshotStoreOptions {\n readonly maxSnapshots?: number;\n readonly ttlMs?: number;\n readonly now?: () => number;\n}\n\n/**\n * The one cursor rejection: a cursor whose snapshot expired, was evicted, was\n * already consumed past its end, or belongs to a different query. Every case\n * has the same remedy, so they share one message.\n */\nexport function invalidCursor(): FsError {\n return new FsError(\n ErrorCode.INVALID_INPUT,\n 'Invalid cursor. Request the first page without a cursor.',\n );\n}\n\n/**\n * Short-lived snapshots of a completed query's full result set, so later pages\n * slice a stored array instead of re-scanning and re-sorting the filesystem.\n *\n * ponytail: bounded by snapshot count and TTL, not by bytes — 32 x 20,000\n * `list` entries or 32 x 10,000 `search_text` matches, order of a few hundred MB\n * held for 60s, and the HTTP leg shares one store so any caller can drive it.\n * If that shows up as memory pressure, meter bytes the way `ResourceStore` does.\n */\nexport class PageSnapshotStore {\n private readonly byId = new Map<string, StoredPageSnapshot>();\n private readonly maxSnapshots: number;\n private readonly ttlMs: number;\n private readonly now: () => number;\n\n constructor(options: PageSnapshotStoreOptions = {}) {\n this.maxSnapshots = options.maxSnapshots ?? DEFAULT_MAX_SNAPSHOTS;\n this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;\n this.now = options.now ?? Date.now;\n }\n\n private pruneExpired(): void {\n const now = this.now();\n for (const [snapshotId, entry] of this.byId) {\n if (entry.expiresAt <= now) this.byId.delete(snapshotId);\n }\n }\n\n create(params: { queryKey: string; items: readonly unknown[]; metadata?: unknown }): string {\n this.pruneExpired();\n const snapshotId = randomUUID();\n this.byId.set(snapshotId, {\n queryKey: params.queryKey,\n items: params.items,\n metadata: params.metadata,\n expiresAt: this.now() + this.ttlMs,\n });\n // Oldest first: Map keeps insertion order and `read` re-inserts, so the\n // first key is the least recently used snapshot.\n while (this.byId.size > this.maxSnapshots) {\n const oldest = this.byId.keys().next();\n if (oldest.done) break;\n this.byId.delete(oldest.value);\n }\n return snapshotId;\n }\n\n read<T, M = undefined>(snapshotId: string, queryKey: string): PageSnapshot<T, M> {\n this.pruneExpired();\n const entry = this.byId.get(snapshotId);\n if (entry?.queryKey !== queryKey) throw invalidCursor();\n this.byId.delete(snapshotId);\n this.byId.set(snapshotId, entry);\n return {\n items: entry.items as readonly T[],\n metadata: entry.metadata as M,\n };\n }\n\n clear(): void {\n this.byId.clear();\n }\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PathGuard } from './path.js';
|
|
2
|
+
export declare class PathCompleter {
|
|
3
|
+
private cache;
|
|
4
|
+
private readonly pathGuard;
|
|
5
|
+
constructor(pathGuard: PathGuard);
|
|
6
|
+
suggest(value: string): Promise<string[]>;
|
|
7
|
+
private setCacheValue;
|
|
8
|
+
private completePath;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=path-completer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-completer.d.ts","sourceRoot":"","sources":["../../src/core/path-completer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AA4M3C,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;gBAE1B,SAAS,EAAE,SAAS;IAI1B,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAa/C,OAAO,CAAC,aAAa;YAOP,YAAY;CA8B3B"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { opendir, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, parse, resolve, sep } from 'node:path';
|
|
3
|
+
import { isNodeError, isNotFoundErrno, rethrowIfAborted } from './errors.js';
|
|
4
|
+
import { Logger } from './observability.js';
|
|
5
|
+
import { isPathWithinDirectories, isSamePath, normalizePath } from './path-utils.js';
|
|
6
|
+
import { isSlash, resolveRealPath, toPosixPath } from './path.js';
|
|
7
|
+
const MAX_COMPLETION_ITEMS = 100;
|
|
8
|
+
const COMPLETION_RATE_LIMIT_MS = 100;
|
|
9
|
+
const MAX_COMPLETION_CACHE_KEYS = 128;
|
|
10
|
+
// ─── pure path-completion helpers (no instance state) ───────────────────────
|
|
11
|
+
// Formerly static methods on PathCompleter. They take `allowed` explicitly and
|
|
12
|
+
// touch no state, so they read as plain functions rather than a class used as
|
|
13
|
+
// a namespace. PathCompleter keeps only the cache and the path guard.
|
|
14
|
+
function hasTrailingSeparator(value) {
|
|
15
|
+
return value.length > 0 && isSlash(value.charCodeAt(value.length - 1));
|
|
16
|
+
}
|
|
17
|
+
function resolveFromBase(base, rawValue, trailingSeparator) {
|
|
18
|
+
const normalizedValue = normalizePath(resolve(base, rawValue));
|
|
19
|
+
if (trailingSeparator)
|
|
20
|
+
return { searchDir: normalizedValue, prefix: '' };
|
|
21
|
+
return {
|
|
22
|
+
searchDir: dirname(normalizedValue),
|
|
23
|
+
prefix: basename(normalizedValue),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function parseNamedRootInput(value) {
|
|
27
|
+
const normalizedInput = toPosixPath(value);
|
|
28
|
+
if (!normalizedInput)
|
|
29
|
+
return undefined;
|
|
30
|
+
const slashIndex = normalizedInput.indexOf('/');
|
|
31
|
+
if (slashIndex === -1)
|
|
32
|
+
return { rootName: normalizedInput, remainder: '' };
|
|
33
|
+
const rootName = normalizedInput.slice(0, slashIndex);
|
|
34
|
+
if (!rootName)
|
|
35
|
+
return undefined;
|
|
36
|
+
return { rootName, remainder: normalizedInput.slice(slashIndex + 1) };
|
|
37
|
+
}
|
|
38
|
+
function findAllowedRootByName(rootName, allowed) {
|
|
39
|
+
const normalizedRootName = rootName.toLowerCase();
|
|
40
|
+
return allowed.find((candidate) => basename(candidate).toLowerCase() === normalizedRootName);
|
|
41
|
+
}
|
|
42
|
+
function resolveNamedRootContext(currentValue, allowed) {
|
|
43
|
+
const parsed = parseNamedRootInput(currentValue);
|
|
44
|
+
if (!parsed)
|
|
45
|
+
return undefined;
|
|
46
|
+
const root = findAllowedRootByName(parsed.rootName, allowed);
|
|
47
|
+
if (!root)
|
|
48
|
+
return undefined;
|
|
49
|
+
const trailingSeparator = hasTrailingSeparator(currentValue);
|
|
50
|
+
return resolveFromBase(root, parsed.remainder, trailingSeparator);
|
|
51
|
+
}
|
|
52
|
+
async function isAllowedCompletionDirectory(path, allowed) {
|
|
53
|
+
if (!isPathWithinDirectories(path, allowed))
|
|
54
|
+
return false;
|
|
55
|
+
try {
|
|
56
|
+
const stats = await stat(path);
|
|
57
|
+
if (!stats.isDirectory())
|
|
58
|
+
return false;
|
|
59
|
+
const real = await resolveRealPath(path);
|
|
60
|
+
return real !== null && isPathWithinDirectories(real, allowed);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (!isNotFoundErrno(err) && (!isNodeError(err) || err.code !== 'EACCES')) {
|
|
64
|
+
Logger.debug('isAllowedCompletionDirectory: unexpected probe error', {
|
|
65
|
+
path,
|
|
66
|
+
error: String(err),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function withDirectorySeparator(value) {
|
|
73
|
+
return value.endsWith(sep) ? value : `${value}${sep}`;
|
|
74
|
+
}
|
|
75
|
+
function collectAllowedRoots(allowed, predicate) {
|
|
76
|
+
const matches = [];
|
|
77
|
+
for (const root of allowed) {
|
|
78
|
+
if (predicate(root))
|
|
79
|
+
matches.push(withDirectorySeparator(root));
|
|
80
|
+
}
|
|
81
|
+
return matches;
|
|
82
|
+
}
|
|
83
|
+
function getRootPrefix(currentValue) {
|
|
84
|
+
const normalizedInput = toPosixPath(currentValue);
|
|
85
|
+
const slashIndex = normalizedInput.indexOf('/');
|
|
86
|
+
return (slashIndex === -1 ? normalizedInput : normalizedInput.slice(0, slashIndex)).toLowerCase();
|
|
87
|
+
}
|
|
88
|
+
function findRootPrefixMatches(currentValue, allowed) {
|
|
89
|
+
const rootPrefix = getRootPrefix(currentValue);
|
|
90
|
+
if (!rootPrefix)
|
|
91
|
+
return collectAllowedRoots(allowed, () => true);
|
|
92
|
+
return collectAllowedRoots(allowed, (root) => basename(root).toLowerCase().startsWith(rootPrefix));
|
|
93
|
+
}
|
|
94
|
+
function findMatchingRoots(searchDir, prefix, allowed) {
|
|
95
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
96
|
+
const normalizedSearchDir = normalizePath(searchDir);
|
|
97
|
+
return collectAllowedRoots(allowed, (root) => {
|
|
98
|
+
const rootDir = dirname(root);
|
|
99
|
+
if (!isSamePath(rootDir, normalizedSearchDir))
|
|
100
|
+
return false;
|
|
101
|
+
return basename(root).toLowerCase().startsWith(lowerPrefix);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function sortCompletionMatches(matches) {
|
|
105
|
+
const sepCode = sep.charCodeAt(0);
|
|
106
|
+
matches.sort((left, right) => {
|
|
107
|
+
const leftIsDir = left.charCodeAt(left.length - 1) === sepCode;
|
|
108
|
+
const rightIsDir = right.charCodeAt(right.length - 1) === sepCode;
|
|
109
|
+
if (leftIsDir && !rightIsDir)
|
|
110
|
+
return -1;
|
|
111
|
+
if (!leftIsDir && rightIsDir)
|
|
112
|
+
return 1;
|
|
113
|
+
return left.localeCompare(right);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function mergeCompletionMatches(...matchGroups) {
|
|
117
|
+
const uniqueMatches = new Set();
|
|
118
|
+
for (const group of matchGroups) {
|
|
119
|
+
for (const match of group)
|
|
120
|
+
uniqueMatches.add(match);
|
|
121
|
+
}
|
|
122
|
+
const merged = Array.from(uniqueMatches);
|
|
123
|
+
sortCompletionMatches(merged);
|
|
124
|
+
return merged;
|
|
125
|
+
}
|
|
126
|
+
async function findMatchesInDirectory(searchDir, prefix, allowed, isSensitive) {
|
|
127
|
+
const matches = [];
|
|
128
|
+
if (!(await isAllowedCompletionDirectory(searchDir, allowed)))
|
|
129
|
+
return matches;
|
|
130
|
+
try {
|
|
131
|
+
// Stream via opendir and collect every match; the MAX_COMPLETION_ITEMS
|
|
132
|
+
// cap is applied AFTER the alphabetical sort in mergeCompletionMatches
|
|
133
|
+
// (slice at the call site). Capping here would keep the opendir-first
|
|
134
|
+
// 100, not the alphabetically-first 100 — the sort would only reorder
|
|
135
|
+
// an already-arbitrary subset.
|
|
136
|
+
const dir = await opendir(searchDir);
|
|
137
|
+
try {
|
|
138
|
+
const lowerPrefix = prefix.toLowerCase();
|
|
139
|
+
for await (const entry of dir) {
|
|
140
|
+
if (prefix !== '' && !entry.name.toLowerCase().startsWith(lowerPrefix))
|
|
141
|
+
continue;
|
|
142
|
+
const fullPath = join(searchDir, entry.name);
|
|
143
|
+
if (isSensitive?.(fullPath))
|
|
144
|
+
continue;
|
|
145
|
+
matches.push(entry.isDirectory() ? `${fullPath}${sep}` : fullPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await dir.close().catch(() => {
|
|
150
|
+
/* dir already closed or opendir never resolved */
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
if (!isNodeError(err) || (err.code !== 'ENOENT' && err.code !== 'EACCES')) {
|
|
156
|
+
Logger.warn('PathCompleter.findMatchesInDirectory: readdir failed', {
|
|
157
|
+
searchDir,
|
|
158
|
+
error: String(err),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return matches;
|
|
163
|
+
}
|
|
164
|
+
function getSearchContext(currentValue, allowed) {
|
|
165
|
+
const trailingSeparator = hasTrailingSeparator(currentValue);
|
|
166
|
+
if (isAbsolute(currentValue)) {
|
|
167
|
+
return resolveFromBase(parse(currentValue).root || sep, currentValue, trailingSeparator);
|
|
168
|
+
}
|
|
169
|
+
const namedRootContext = resolveNamedRootContext(currentValue, allowed);
|
|
170
|
+
if (namedRootContext)
|
|
171
|
+
return namedRootContext;
|
|
172
|
+
if (allowed.length === 1) {
|
|
173
|
+
const base = allowed[0];
|
|
174
|
+
if (base)
|
|
175
|
+
return resolveFromBase(base, currentValue, trailingSeparator);
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
// ─── PathCompleter: cache + path guard ───────────────────────────────────────
|
|
180
|
+
export class PathCompleter {
|
|
181
|
+
cache = new Map();
|
|
182
|
+
pathGuard;
|
|
183
|
+
constructor(pathGuard) {
|
|
184
|
+
this.pathGuard = pathGuard;
|
|
185
|
+
}
|
|
186
|
+
async suggest(value) {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const cacheEntry = this.cache.get(value);
|
|
189
|
+
if (cacheEntry && now - cacheEntry.ms < COMPLETION_RATE_LIMIT_MS) {
|
|
190
|
+
return cacheEntry.result;
|
|
191
|
+
}
|
|
192
|
+
const results = await this.completePath(value);
|
|
193
|
+
this.setCacheValue(value, { ms: now, result: results });
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
setCacheValue(key, entry) {
|
|
197
|
+
// Entries are stale after COMPLETION_RATE_LIMIT_MS anyway, so the cap only
|
|
198
|
+
// has to bound memory — dropping the whole map beats per-key LRU eviction.
|
|
199
|
+
if (this.cache.size >= MAX_COMPLETION_CACHE_KEYS)
|
|
200
|
+
this.cache.clear();
|
|
201
|
+
this.cache.set(key, entry);
|
|
202
|
+
}
|
|
203
|
+
async completePath(value) {
|
|
204
|
+
const allowed = this.pathGuard.getAllowedDirectories();
|
|
205
|
+
try {
|
|
206
|
+
if (!value) {
|
|
207
|
+
return allowed.slice(0, MAX_COMPLETION_ITEMS);
|
|
208
|
+
}
|
|
209
|
+
const context = getSearchContext(value, allowed);
|
|
210
|
+
if (!context) {
|
|
211
|
+
return findRootPrefixMatches(value, allowed).slice(0, MAX_COMPLETION_ITEMS);
|
|
212
|
+
}
|
|
213
|
+
const { searchDir, prefix } = context;
|
|
214
|
+
const dirMatches = await findMatchesInDirectory(searchDir, prefix, allowed, this.pathGuard.isSensitive.bind(this.pathGuard));
|
|
215
|
+
const rootMatches = findMatchingRoots(searchDir, prefix, allowed);
|
|
216
|
+
return mergeCompletionMatches(dirMatches, rootMatches).slice(0, MAX_COMPLETION_ITEMS);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
rethrowIfAborted(error);
|
|
220
|
+
Logger.warn('PathCompleter: completion failed, returning empty list', {
|
|
221
|
+
error: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
222
|
+
});
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=path-completer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-completer.js","sourceRoot":"","sources":["../../src/core/path-completer.ts"],"names":[],"mappings":"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","sourcesContent":["import { opendir, stat } from 'node:fs/promises';\nimport { basename, dirname, isAbsolute, join, parse, resolve, sep } from 'node:path';\n\nimport { isNodeError, isNotFoundErrno, rethrowIfAborted } from './errors.js';\nimport { Logger } from './observability.js';\nimport { isPathWithinDirectories, isSamePath, normalizePath } from './path-utils.js';\nimport type { PathGuard } from './path.js';\nimport { isSlash, resolveRealPath, toPosixPath } from './path.js';\n\nconst MAX_COMPLETION_ITEMS = 100;\nconst COMPLETION_RATE_LIMIT_MS = 100;\nconst MAX_COMPLETION_CACHE_KEYS = 128;\n\ninterface CacheEntry {\n ms: number;\n result: string[];\n}\n\n// ─── pure path-completion helpers (no instance state) ───────────────────────\n// Formerly static methods on PathCompleter. They take `allowed` explicitly and\n// touch no state, so they read as plain functions rather than a class used as\n// a namespace. PathCompleter keeps only the cache and the path guard.\n\nfunction hasTrailingSeparator(value: string): boolean {\n return value.length > 0 && isSlash(value.charCodeAt(value.length - 1));\n}\n\nfunction resolveFromBase(\n base: string,\n rawValue: string,\n trailingSeparator: boolean,\n): { searchDir: string; prefix: string } {\n const normalizedValue = normalizePath(resolve(base, rawValue));\n if (trailingSeparator) return { searchDir: normalizedValue, prefix: '' };\n return {\n searchDir: dirname(normalizedValue),\n prefix: basename(normalizedValue),\n };\n}\n\nfunction parseNamedRootInput(value: string): { rootName: string; remainder: string } | undefined {\n const normalizedInput = toPosixPath(value);\n if (!normalizedInput) return undefined;\n const slashIndex = normalizedInput.indexOf('/');\n if (slashIndex === -1) return { rootName: normalizedInput, remainder: '' };\n const rootName = normalizedInput.slice(0, slashIndex);\n if (!rootName) return undefined;\n return { rootName, remainder: normalizedInput.slice(slashIndex + 1) };\n}\n\nfunction findAllowedRootByName(rootName: string, allowed: readonly string[]): string | undefined {\n const normalizedRootName = rootName.toLowerCase();\n return allowed.find((candidate) => basename(candidate).toLowerCase() === normalizedRootName);\n}\n\nfunction resolveNamedRootContext(\n currentValue: string,\n allowed: readonly string[],\n): { searchDir: string; prefix: string } | undefined {\n const parsed = parseNamedRootInput(currentValue);\n if (!parsed) return undefined;\n const root = findAllowedRootByName(parsed.rootName, allowed);\n if (!root) return undefined;\n const trailingSeparator = hasTrailingSeparator(currentValue);\n return resolveFromBase(root, parsed.remainder, trailingSeparator);\n}\n\nasync function isAllowedCompletionDirectory(\n path: string,\n allowed: readonly string[],\n): Promise<boolean> {\n if (!isPathWithinDirectories(path, allowed)) return false;\n try {\n const stats = await stat(path);\n if (!stats.isDirectory()) return false;\n const real = await resolveRealPath(path);\n return real !== null && isPathWithinDirectories(real, allowed);\n } catch (err) {\n if (!isNotFoundErrno(err) && (!isNodeError(err) || err.code !== 'EACCES')) {\n Logger.debug('isAllowedCompletionDirectory: unexpected probe error', {\n path,\n error: String(err),\n });\n }\n return false;\n }\n}\n\nfunction withDirectorySeparator(value: string): string {\n return value.endsWith(sep) ? value : `${value}${sep}`;\n}\n\nfunction collectAllowedRoots(\n allowed: readonly string[],\n predicate: (root: string) => boolean,\n): string[] {\n const matches: string[] = [];\n for (const root of allowed) {\n if (predicate(root)) matches.push(withDirectorySeparator(root));\n }\n return matches;\n}\n\nfunction getRootPrefix(currentValue: string): string {\n const normalizedInput = toPosixPath(currentValue);\n const slashIndex = normalizedInput.indexOf('/');\n return (slashIndex === -1 ? normalizedInput : normalizedInput.slice(0, slashIndex)).toLowerCase();\n}\n\nfunction findRootPrefixMatches(currentValue: string, allowed: readonly string[]): string[] {\n const rootPrefix = getRootPrefix(currentValue);\n if (!rootPrefix) return collectAllowedRoots(allowed, () => true);\n return collectAllowedRoots(allowed, (root) =>\n basename(root).toLowerCase().startsWith(rootPrefix),\n );\n}\n\nfunction findMatchingRoots(\n searchDir: string,\n prefix: string,\n allowed: readonly string[],\n): string[] {\n const lowerPrefix = prefix.toLowerCase();\n const normalizedSearchDir = normalizePath(searchDir);\n return collectAllowedRoots(allowed, (root) => {\n const rootDir = dirname(root);\n if (!isSamePath(rootDir, normalizedSearchDir)) return false;\n return basename(root).toLowerCase().startsWith(lowerPrefix);\n });\n}\n\nfunction sortCompletionMatches(matches: string[]): void {\n const sepCode = sep.charCodeAt(0);\n matches.sort((left, right) => {\n const leftIsDir = left.charCodeAt(left.length - 1) === sepCode;\n const rightIsDir = right.charCodeAt(right.length - 1) === sepCode;\n if (leftIsDir && !rightIsDir) return -1;\n if (!leftIsDir && rightIsDir) return 1;\n return left.localeCompare(right);\n });\n}\n\nfunction mergeCompletionMatches(...matchGroups: readonly (readonly string[])[]): string[] {\n const uniqueMatches = new Set<string>();\n for (const group of matchGroups) {\n for (const match of group) uniqueMatches.add(match);\n }\n const merged = Array.from(uniqueMatches);\n sortCompletionMatches(merged);\n return merged;\n}\n\nasync function findMatchesInDirectory(\n searchDir: string,\n prefix: string,\n allowed: readonly string[],\n isSensitive?: (path: string) => boolean,\n): Promise<string[]> {\n const matches: string[] = [];\n if (!(await isAllowedCompletionDirectory(searchDir, allowed))) return matches;\n try {\n // Stream via opendir and collect every match; the MAX_COMPLETION_ITEMS\n // cap is applied AFTER the alphabetical sort in mergeCompletionMatches\n // (slice at the call site). Capping here would keep the opendir-first\n // 100, not the alphabetically-first 100 — the sort would only reorder\n // an already-arbitrary subset.\n const dir = await opendir(searchDir);\n try {\n const lowerPrefix = prefix.toLowerCase();\n for await (const entry of dir) {\n if (prefix !== '' && !entry.name.toLowerCase().startsWith(lowerPrefix)) continue;\n const fullPath = join(searchDir, entry.name);\n if (isSensitive?.(fullPath)) continue;\n matches.push(entry.isDirectory() ? `${fullPath}${sep}` : fullPath);\n }\n } finally {\n await dir.close().catch(() => {\n /* dir already closed or opendir never resolved */\n });\n }\n } catch (err) {\n if (!isNodeError(err) || (err.code !== 'ENOENT' && err.code !== 'EACCES')) {\n Logger.warn('PathCompleter.findMatchesInDirectory: readdir failed', {\n searchDir,\n error: String(err),\n });\n }\n }\n return matches;\n}\n\nfunction getSearchContext(\n currentValue: string,\n allowed: readonly string[],\n): { searchDir: string; prefix: string } | undefined {\n const trailingSeparator = hasTrailingSeparator(currentValue);\n if (isAbsolute(currentValue)) {\n return resolveFromBase(parse(currentValue).root || sep, currentValue, trailingSeparator);\n }\n const namedRootContext = resolveNamedRootContext(currentValue, allowed);\n if (namedRootContext) return namedRootContext;\n if (allowed.length === 1) {\n const base = allowed[0];\n if (base) return resolveFromBase(base, currentValue, trailingSeparator);\n }\n return undefined;\n}\n\n// ─── PathCompleter: cache + path guard ───────────────────────────────────────\n\nexport class PathCompleter {\n private cache = new Map<string, CacheEntry>();\n private readonly pathGuard: PathGuard;\n\n constructor(pathGuard: PathGuard) {\n this.pathGuard = pathGuard;\n }\n\n async suggest(value: string): Promise<string[]> {\n const now = Date.now();\n const cacheEntry = this.cache.get(value);\n\n if (cacheEntry && now - cacheEntry.ms < COMPLETION_RATE_LIMIT_MS) {\n return cacheEntry.result;\n }\n\n const results = await this.completePath(value);\n this.setCacheValue(value, { ms: now, result: results });\n return results;\n }\n\n private setCacheValue(key: string, entry: CacheEntry): void {\n // Entries are stale after COMPLETION_RATE_LIMIT_MS anyway, so the cap only\n // has to bound memory — dropping the whole map beats per-key LRU eviction.\n if (this.cache.size >= MAX_COMPLETION_CACHE_KEYS) this.cache.clear();\n this.cache.set(key, entry);\n }\n\n private async completePath(value: string): Promise<string[]> {\n const allowed = this.pathGuard.getAllowedDirectories();\n\n try {\n if (!value) {\n return allowed.slice(0, MAX_COMPLETION_ITEMS);\n }\n\n const context = getSearchContext(value, allowed);\n if (!context) {\n return findRootPrefixMatches(value, allowed).slice(0, MAX_COMPLETION_ITEMS);\n }\n\n const { searchDir, prefix } = context;\n const dirMatches = await findMatchesInDirectory(\n searchDir,\n prefix,\n allowed,\n this.pathGuard.isSensitive.bind(this.pathGuard),\n );\n const rootMatches = findMatchingRoots(searchDir, prefix, allowed);\n return mergeCompletionMatches(dirMatches, rootMatches).slice(0, MAX_COMPLETION_ITEMS);\n } catch (error) {\n rethrowIfAborted(error);\n Logger.warn('PathCompleter: completion failed, returning empty list', {\n error: error instanceof Error ? (error.stack ?? error.message) : String(error),\n });\n return [];\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function resolveConfiguredDirs(envVar: string, opts?: {
|
|
2
|
+
allowMissing?: boolean;
|
|
3
|
+
resolveReal?: boolean;
|
|
4
|
+
rawValue?: string;
|
|
5
|
+
}): Promise<string[]>;
|
|
6
|
+
export declare function isUnsafeCwdPath(normalizedCwd: string): boolean;
|
|
7
|
+
export declare function findProjectRoot(startDir: string, ceiling: string[]): Promise<string>;
|
|
8
|
+
//# sourceMappingURL=path-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-discovery.d.ts","sourceRoot":"","sources":["../../src/core/path-discovery.ts"],"names":[],"mappings":"AAiBA,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9E,OAAO,CAAC,MAAM,EAAE,CAAC,CAwBnB;AAED,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAiC9D;AAID,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAwC1F"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, parse } from 'node:path';
|
|
4
|
+
import { formatUnknownErrorMessage } from './errors.js';
|
|
5
|
+
import { Logger } from './observability.js';
|
|
6
|
+
import { isPathWithinDirectories, isSamePath, normalizePath } from './path-utils.js';
|
|
7
|
+
import { parseEnvDirList, splitDirList } from './primitives.js';
|
|
8
|
+
// Resolve a configured env-var directory list (FS_ALLOWED_DIRS / FS_ROOT_BOUNDARY)
|
|
9
|
+
// into normalized, verified directories. Each entry is stat'd; a non-directory
|
|
10
|
+
// warns and is dropped, a missing entry warns unless `allowMissing` is set (in
|
|
11
|
+
// which case the normalized path is kept). When `resolveReal` is set, a
|
|
12
|
+
// directory entry is pushed as its realpath (normalized) instead of the raw
|
|
13
|
+
// path — FS_ROOT_BOUNDARY uses this so a symlinked root resolves to its target.
|
|
14
|
+
// A CLI override supplies `rawValue`, which beats the environment entirely.
|
|
15
|
+
// Both warning messages are templated on `envVar` so operator output is stable.
|
|
16
|
+
export async function resolveConfiguredDirs(envVar, opts = {}) {
|
|
17
|
+
const raw = opts.rawValue !== undefined ? splitDirList(opts.rawValue) : parseEnvDirList(envVar);
|
|
18
|
+
const result = [];
|
|
19
|
+
for (const rawPath of raw) {
|
|
20
|
+
const normalized = normalizePath(rawPath);
|
|
21
|
+
try {
|
|
22
|
+
const s = await stat(normalized);
|
|
23
|
+
if (s.isDirectory()) {
|
|
24
|
+
result.push(opts.resolveReal ? normalizePath(await realpath(normalized)) : normalized);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
Logger.emit('warning', `Path configured in ${envVar} is not a directory: ${rawPath}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (opts.allowMissing) {
|
|
32
|
+
result.push(normalized);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
Logger.emit('warning', `Path configured in ${envVar} is invalid or does not exist: ${rawPath} (${formatUnknownErrorMessage(error)})`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
export function isUnsafeCwdPath(normalizedCwd) {
|
|
42
|
+
const norm = normalizedCwd.toLowerCase();
|
|
43
|
+
// 1. Filesystem root check
|
|
44
|
+
const root = parse(normalizedCwd).root;
|
|
45
|
+
if (isSamePath(normalizedCwd, root)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
// 2. Home directory check
|
|
49
|
+
if (isSamePath(normalizedCwd, homedir())) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
// 3. Hard-coded unsafe paths check
|
|
53
|
+
const unsafePaths = new Set([
|
|
54
|
+
'/usr',
|
|
55
|
+
'/etc',
|
|
56
|
+
'/bin',
|
|
57
|
+
'/sbin',
|
|
58
|
+
'/System',
|
|
59
|
+
'C:\\Windows',
|
|
60
|
+
'C:\\Program Files',
|
|
61
|
+
'C:\\Program Files (x86)',
|
|
62
|
+
].map((p) => normalizePath(p).toLowerCase()));
|
|
63
|
+
if (unsafePaths.has(norm)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const MAX_PROJECT_ROOT_WALK_DEPTH = 32;
|
|
69
|
+
export async function findProjectRoot(startDir, ceiling) {
|
|
70
|
+
const normCeiling = ceiling.map(normalizePath);
|
|
71
|
+
let current = normalizePath(startDir);
|
|
72
|
+
let depth = 0;
|
|
73
|
+
for (;;) {
|
|
74
|
+
if (depth++ >= MAX_PROJECT_ROOT_WALK_DEPTH) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
// Check if the current directory contains any markers
|
|
78
|
+
const markers = ['.git', 'package.json', 'pyproject.toml'];
|
|
79
|
+
for (const marker of markers) {
|
|
80
|
+
const markerPath = join(current, marker);
|
|
81
|
+
try {
|
|
82
|
+
const s = await stat(markerPath);
|
|
83
|
+
if (s.isDirectory() || s.isFile()) {
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (_error) {
|
|
88
|
+
// Skip and try next marker
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Move to parent directory
|
|
92
|
+
const parent = normalizePath(dirname(current));
|
|
93
|
+
// Check if we hit the filesystem root
|
|
94
|
+
if (parent === current) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
// Check if the parent is still within the ceiling
|
|
98
|
+
if (!isPathWithinDirectories(parent, normCeiling)) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
current = parent;
|
|
102
|
+
}
|
|
103
|
+
return normalizePath(startDir);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=path-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-discovery.js","sourceRoot":"","sources":["../../src/core/path-discovery.ts"],"names":[],"mappings":"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","sourcesContent":["import { realpath, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, join, parse } from 'node:path';\n\nimport { formatUnknownErrorMessage } from './errors.js';\nimport { Logger } from './observability.js';\nimport { isPathWithinDirectories, isSamePath, normalizePath } from './path-utils.js';\nimport { parseEnvDirList, splitDirList } from './primitives.js';\n\n// Resolve a configured env-var directory list (FS_ALLOWED_DIRS / FS_ROOT_BOUNDARY)\n// into normalized, verified directories. Each entry is stat'd; a non-directory\n// warns and is dropped, a missing entry warns unless `allowMissing` is set (in\n// which case the normalized path is kept). When `resolveReal` is set, a\n// directory entry is pushed as its realpath (normalized) instead of the raw\n// path — FS_ROOT_BOUNDARY uses this so a symlinked root resolves to its target.\n// A CLI override supplies `rawValue`, which beats the environment entirely.\n// Both warning messages are templated on `envVar` so operator output is stable.\nexport async function resolveConfiguredDirs(\n envVar: string,\n opts: { allowMissing?: boolean; resolveReal?: boolean; rawValue?: string } = {},\n): Promise<string[]> {\n const raw = opts.rawValue !== undefined ? splitDirList(opts.rawValue) : parseEnvDirList(envVar);\n const result: string[] = [];\n for (const rawPath of raw) {\n const normalized = normalizePath(rawPath);\n try {\n const s = await stat(normalized);\n if (s.isDirectory()) {\n result.push(opts.resolveReal ? normalizePath(await realpath(normalized)) : normalized);\n } else {\n Logger.emit('warning', `Path configured in ${envVar} is not a directory: ${rawPath}`);\n }\n } catch (error) {\n if (opts.allowMissing) {\n result.push(normalized);\n } else {\n Logger.emit(\n 'warning',\n `Path configured in ${envVar} is invalid or does not exist: ${rawPath} (${formatUnknownErrorMessage(error)})`,\n );\n }\n }\n }\n return result;\n}\n\nexport function isUnsafeCwdPath(normalizedCwd: string): boolean {\n const norm = normalizedCwd.toLowerCase();\n\n // 1. Filesystem root check\n const root = parse(normalizedCwd).root;\n if (isSamePath(normalizedCwd, root)) {\n return true;\n }\n\n // 2. Home directory check\n if (isSamePath(normalizedCwd, homedir())) {\n return true;\n }\n\n // 3. Hard-coded unsafe paths check\n const unsafePaths = new Set(\n [\n '/usr',\n '/etc',\n '/bin',\n '/sbin',\n '/System',\n 'C:\\\\Windows',\n 'C:\\\\Program Files',\n 'C:\\\\Program Files (x86)',\n ].map((p) => normalizePath(p).toLowerCase()),\n );\n\n if (unsafePaths.has(norm)) {\n return true;\n }\n\n return false;\n}\n\nconst MAX_PROJECT_ROOT_WALK_DEPTH = 32;\n\nexport async function findProjectRoot(startDir: string, ceiling: string[]): Promise<string> {\n const normCeiling = ceiling.map(normalizePath);\n let current = normalizePath(startDir);\n let depth = 0;\n\n for (;;) {\n if (depth++ >= MAX_PROJECT_ROOT_WALK_DEPTH) {\n break;\n }\n // Check if the current directory contains any markers\n const markers = ['.git', 'package.json', 'pyproject.toml'];\n for (const marker of markers) {\n const markerPath = join(current, marker);\n try {\n const s = await stat(markerPath);\n if (s.isDirectory() || s.isFile()) {\n return current;\n }\n } catch (_error) {\n // Skip and try next marker\n }\n }\n\n // Move to parent directory\n const parent = normalizePath(dirname(current));\n\n // Check if we hit the filesystem root\n if (parent === current) {\n break;\n }\n\n // Check if the parent is still within the ceiling\n if (!isPathWithinDirectories(parent, normCeiling)) {\n break;\n }\n\n current = parent;\n }\n\n return normalizePath(startDir);\n}\n"]}
|