@gmickel/gno 0.3.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 +256 -0
- package/assets/skill/SKILL.md +112 -0
- package/assets/skill/cli-reference.md +327 -0
- package/assets/skill/examples.md +234 -0
- package/assets/skill/mcp-reference.md +159 -0
- package/package.json +90 -0
- package/src/app/constants.ts +313 -0
- package/src/cli/colors.ts +65 -0
- package/src/cli/commands/ask.ts +545 -0
- package/src/cli/commands/cleanup.ts +105 -0
- package/src/cli/commands/collection/add.ts +120 -0
- package/src/cli/commands/collection/index.ts +10 -0
- package/src/cli/commands/collection/list.ts +108 -0
- package/src/cli/commands/collection/remove.ts +64 -0
- package/src/cli/commands/collection/rename.ts +95 -0
- package/src/cli/commands/context/add.ts +67 -0
- package/src/cli/commands/context/check.ts +153 -0
- package/src/cli/commands/context/index.ts +10 -0
- package/src/cli/commands/context/list.ts +109 -0
- package/src/cli/commands/context/rm.ts +52 -0
- package/src/cli/commands/doctor.ts +393 -0
- package/src/cli/commands/embed.ts +462 -0
- package/src/cli/commands/get.ts +356 -0
- package/src/cli/commands/index-cmd.ts +119 -0
- package/src/cli/commands/index.ts +102 -0
- package/src/cli/commands/init.ts +328 -0
- package/src/cli/commands/ls.ts +217 -0
- package/src/cli/commands/mcp/config.ts +300 -0
- package/src/cli/commands/mcp/index.ts +24 -0
- package/src/cli/commands/mcp/install.ts +203 -0
- package/src/cli/commands/mcp/paths.ts +470 -0
- package/src/cli/commands/mcp/status.ts +222 -0
- package/src/cli/commands/mcp/uninstall.ts +158 -0
- package/src/cli/commands/mcp.ts +20 -0
- package/src/cli/commands/models/clear.ts +103 -0
- package/src/cli/commands/models/index.ts +32 -0
- package/src/cli/commands/models/list.ts +214 -0
- package/src/cli/commands/models/path.ts +51 -0
- package/src/cli/commands/models/pull.ts +199 -0
- package/src/cli/commands/models/use.ts +85 -0
- package/src/cli/commands/multi-get.ts +400 -0
- package/src/cli/commands/query.ts +220 -0
- package/src/cli/commands/ref-parser.ts +108 -0
- package/src/cli/commands/reset.ts +191 -0
- package/src/cli/commands/search.ts +136 -0
- package/src/cli/commands/shared.ts +156 -0
- package/src/cli/commands/skill/index.ts +19 -0
- package/src/cli/commands/skill/install.ts +197 -0
- package/src/cli/commands/skill/paths-cmd.ts +81 -0
- package/src/cli/commands/skill/paths.ts +191 -0
- package/src/cli/commands/skill/show.ts +73 -0
- package/src/cli/commands/skill/uninstall.ts +141 -0
- package/src/cli/commands/status.ts +205 -0
- package/src/cli/commands/update.ts +68 -0
- package/src/cli/commands/vsearch.ts +188 -0
- package/src/cli/context.ts +64 -0
- package/src/cli/errors.ts +64 -0
- package/src/cli/format/search-results.ts +211 -0
- package/src/cli/options.ts +183 -0
- package/src/cli/program.ts +1330 -0
- package/src/cli/run.ts +213 -0
- package/src/cli/ui.ts +92 -0
- package/src/config/defaults.ts +20 -0
- package/src/config/index.ts +55 -0
- package/src/config/loader.ts +161 -0
- package/src/config/paths.ts +87 -0
- package/src/config/saver.ts +153 -0
- package/src/config/types.ts +280 -0
- package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
- package/src/converters/adapters/officeparser/adapter.ts +126 -0
- package/src/converters/canonicalize.ts +89 -0
- package/src/converters/errors.ts +218 -0
- package/src/converters/index.ts +51 -0
- package/src/converters/mime.ts +163 -0
- package/src/converters/native/markdown.ts +115 -0
- package/src/converters/native/plaintext.ts +56 -0
- package/src/converters/path.ts +48 -0
- package/src/converters/pipeline.ts +159 -0
- package/src/converters/registry.ts +74 -0
- package/src/converters/types.ts +123 -0
- package/src/converters/versions.ts +24 -0
- package/src/index.ts +27 -0
- package/src/ingestion/chunker.ts +238 -0
- package/src/ingestion/index.ts +32 -0
- package/src/ingestion/language.ts +276 -0
- package/src/ingestion/sync.ts +671 -0
- package/src/ingestion/types.ts +219 -0
- package/src/ingestion/walker.ts +235 -0
- package/src/llm/cache.ts +467 -0
- package/src/llm/errors.ts +191 -0
- package/src/llm/index.ts +58 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
- package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
- package/src/llm/nodeLlamaCpp/generation.ts +88 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
- package/src/llm/registry.ts +86 -0
- package/src/llm/types.ts +129 -0
- package/src/mcp/resources/index.ts +151 -0
- package/src/mcp/server.ts +229 -0
- package/src/mcp/tools/get.ts +220 -0
- package/src/mcp/tools/index.ts +160 -0
- package/src/mcp/tools/multi-get.ts +263 -0
- package/src/mcp/tools/query.ts +226 -0
- package/src/mcp/tools/search.ts +119 -0
- package/src/mcp/tools/status.ts +81 -0
- package/src/mcp/tools/vsearch.ts +198 -0
- package/src/pipeline/chunk-lookup.ts +44 -0
- package/src/pipeline/expansion.ts +256 -0
- package/src/pipeline/explain.ts +115 -0
- package/src/pipeline/fusion.ts +185 -0
- package/src/pipeline/hybrid.ts +535 -0
- package/src/pipeline/index.ts +64 -0
- package/src/pipeline/query-language.ts +118 -0
- package/src/pipeline/rerank.ts +223 -0
- package/src/pipeline/search.ts +261 -0
- package/src/pipeline/types.ts +328 -0
- package/src/pipeline/vsearch.ts +348 -0
- package/src/store/index.ts +41 -0
- package/src/store/migrations/001-initial.ts +196 -0
- package/src/store/migrations/index.ts +20 -0
- package/src/store/migrations/runner.ts +187 -0
- package/src/store/sqlite/adapter.ts +1242 -0
- package/src/store/sqlite/index.ts +7 -0
- package/src/store/sqlite/setup.ts +129 -0
- package/src/store/sqlite/types.ts +28 -0
- package/src/store/types.ts +506 -0
- package/src/store/vector/index.ts +13 -0
- package/src/store/vector/sqlite-vec.ts +373 -0
- package/src/store/vector/stats.ts +152 -0
- package/src/store/vector/types.ts +115 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingestion subsystem types.
|
|
3
|
+
* Defines Walker, Chunker, and Sync interfaces.
|
|
4
|
+
*
|
|
5
|
+
* @module src/ingestion/types
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Collection } from '../config/types';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Walker Types
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** File entry from walker */
|
|
15
|
+
export interface WalkEntry {
|
|
16
|
+
/** Absolute path to file */
|
|
17
|
+
absPath: string;
|
|
18
|
+
/** Relative path within collection (POSIX forward slashes) */
|
|
19
|
+
relPath: string;
|
|
20
|
+
/** File size in bytes */
|
|
21
|
+
size: number;
|
|
22
|
+
/** Modification time (ISO 8601) */
|
|
23
|
+
mtime: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Walker configuration */
|
|
27
|
+
export interface WalkConfig {
|
|
28
|
+
/** Collection root path (absolute) */
|
|
29
|
+
root: string;
|
|
30
|
+
/** Glob pattern (default: **\/*) */
|
|
31
|
+
pattern: string;
|
|
32
|
+
/** Extension allowlist (empty = all) */
|
|
33
|
+
include: string[];
|
|
34
|
+
/** Paths/patterns to exclude */
|
|
35
|
+
exclude: string[];
|
|
36
|
+
/** Max file size in bytes (files larger are skipped) */
|
|
37
|
+
maxBytes: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Skipped file entry (for error tracking) */
|
|
41
|
+
export interface SkippedEntry {
|
|
42
|
+
absPath: string;
|
|
43
|
+
relPath: string;
|
|
44
|
+
reason: 'TOO_LARGE' | 'EXCLUDED';
|
|
45
|
+
size?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Walker port interface */
|
|
49
|
+
export interface WalkerPort {
|
|
50
|
+
/**
|
|
51
|
+
* Walk collection directory yielding file entries.
|
|
52
|
+
* Filters by pattern, include, exclude.
|
|
53
|
+
* Files > maxBytes are tracked in skipped array.
|
|
54
|
+
*/
|
|
55
|
+
walk(config: WalkConfig): Promise<{
|
|
56
|
+
entries: WalkEntry[];
|
|
57
|
+
skipped: SkippedEntry[];
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// Chunker Types
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** Chunk parameters */
|
|
66
|
+
export interface ChunkParams {
|
|
67
|
+
/** Max tokens per chunk (default: 800) */
|
|
68
|
+
maxTokens: number;
|
|
69
|
+
/** Overlap percentage 0-1 (default: 0.15) */
|
|
70
|
+
overlapPercent: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Default chunk params */
|
|
74
|
+
export const DEFAULT_CHUNK_PARAMS: ChunkParams = {
|
|
75
|
+
maxTokens: 800,
|
|
76
|
+
overlapPercent: 0.15,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Chunked output */
|
|
80
|
+
export interface ChunkOutput {
|
|
81
|
+
/** Sequence number (0-indexed) */
|
|
82
|
+
seq: number;
|
|
83
|
+
/** Character position in source */
|
|
84
|
+
pos: number;
|
|
85
|
+
/** Chunk text */
|
|
86
|
+
text: string;
|
|
87
|
+
/** Start line (1-based) */
|
|
88
|
+
startLine: number;
|
|
89
|
+
/** End line (1-based) */
|
|
90
|
+
endLine: number;
|
|
91
|
+
/** Detected language (BCP-47 or null) */
|
|
92
|
+
language: string | null;
|
|
93
|
+
/** Token count estimate (null for char-based) */
|
|
94
|
+
tokenCount: number | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Chunker port interface */
|
|
98
|
+
export interface ChunkerPort {
|
|
99
|
+
/**
|
|
100
|
+
* Chunk markdown content.
|
|
101
|
+
* Returns deterministic chunks for (text, params).
|
|
102
|
+
*/
|
|
103
|
+
chunk(
|
|
104
|
+
markdown: string,
|
|
105
|
+
params?: ChunkParams,
|
|
106
|
+
documentLanguageHint?: string
|
|
107
|
+
): ChunkOutput[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
// Sync Types
|
|
112
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/** Sync options */
|
|
115
|
+
export interface SyncOptions {
|
|
116
|
+
/** Run git pull before scanning */
|
|
117
|
+
gitPull?: boolean;
|
|
118
|
+
/** Run collection updateCmd before scanning */
|
|
119
|
+
runUpdateCmd?: boolean;
|
|
120
|
+
/** Conversion limits override */
|
|
121
|
+
limits?: {
|
|
122
|
+
maxBytes?: number;
|
|
123
|
+
timeoutMs?: number;
|
|
124
|
+
maxOutputChars?: number;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Max concurrent file processing (default: 1).
|
|
128
|
+
* Higher values improve throughput but increase memory pressure.
|
|
129
|
+
* SQLite operations are serialized regardless of this setting.
|
|
130
|
+
*/
|
|
131
|
+
concurrency?: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Per-file sync status */
|
|
135
|
+
export type FileSyncStatus =
|
|
136
|
+
| 'added'
|
|
137
|
+
| 'updated'
|
|
138
|
+
| 'unchanged'
|
|
139
|
+
| 'error'
|
|
140
|
+
| 'skipped';
|
|
141
|
+
|
|
142
|
+
/** Per-file sync result */
|
|
143
|
+
export interface FileSyncResult {
|
|
144
|
+
relPath: string;
|
|
145
|
+
status: FileSyncStatus;
|
|
146
|
+
docid?: string;
|
|
147
|
+
mirrorHash?: string;
|
|
148
|
+
errorCode?: string;
|
|
149
|
+
errorMessage?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Collection sync summary */
|
|
153
|
+
export interface CollectionSyncResult {
|
|
154
|
+
collection: string;
|
|
155
|
+
filesProcessed: number;
|
|
156
|
+
filesAdded: number;
|
|
157
|
+
filesUpdated: number;
|
|
158
|
+
filesUnchanged: number;
|
|
159
|
+
filesErrored: number;
|
|
160
|
+
filesSkipped: number;
|
|
161
|
+
filesMarkedInactive: number;
|
|
162
|
+
durationMs: number;
|
|
163
|
+
errors: Array<{
|
|
164
|
+
relPath: string;
|
|
165
|
+
code: string;
|
|
166
|
+
message: string;
|
|
167
|
+
}>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Full sync summary */
|
|
171
|
+
export interface SyncResult {
|
|
172
|
+
collections: CollectionSyncResult[];
|
|
173
|
+
totalDurationMs: number;
|
|
174
|
+
totalFilesProcessed: number;
|
|
175
|
+
totalFilesAdded: number;
|
|
176
|
+
totalFilesUpdated: number;
|
|
177
|
+
totalFilesErrored: number;
|
|
178
|
+
totalFilesSkipped: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Decision for whether to process a file */
|
|
182
|
+
export type ProcessDecision =
|
|
183
|
+
| { kind: 'skip'; reason: string }
|
|
184
|
+
| { kind: 'process'; reason: string }
|
|
185
|
+
| { kind: 'repair'; reason: string };
|
|
186
|
+
|
|
187
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
+
// Language Detection Types
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/** Language detector port */
|
|
192
|
+
export interface LanguageDetectorPort {
|
|
193
|
+
/**
|
|
194
|
+
* Detect language from text.
|
|
195
|
+
* Returns BCP-47 code or null if undetermined.
|
|
196
|
+
* Must be deterministic for same input.
|
|
197
|
+
*/
|
|
198
|
+
detect(text: string): string | null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
// Helper to create WalkConfig from Collection
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Create WalkConfig from Collection with maxBytes override.
|
|
207
|
+
*/
|
|
208
|
+
export function collectionToWalkConfig(
|
|
209
|
+
collection: Collection,
|
|
210
|
+
maxBytes: number
|
|
211
|
+
): WalkConfig {
|
|
212
|
+
return {
|
|
213
|
+
root: collection.path,
|
|
214
|
+
pattern: collection.pattern,
|
|
215
|
+
include: collection.include,
|
|
216
|
+
exclude: collection.exclude,
|
|
217
|
+
maxBytes,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File walker implementation.
|
|
3
|
+
* Walks collection directories using Bun.Glob with include/exclude filtering.
|
|
4
|
+
*
|
|
5
|
+
* @module src/ingestion/walker
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// node:fs/promises - Bun has no realpath equivalent
|
|
9
|
+
import { realpath } from 'node:fs/promises';
|
|
10
|
+
// node:path - Bun has no path manipulation module
|
|
11
|
+
import {
|
|
12
|
+
extname,
|
|
13
|
+
isAbsolute,
|
|
14
|
+
normalize as normalizePath,
|
|
15
|
+
relative,
|
|
16
|
+
resolve,
|
|
17
|
+
sep,
|
|
18
|
+
} from 'node:path';
|
|
19
|
+
import type { SkippedEntry, WalkConfig, WalkEntry, WalkerPort } from './types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Regex to detect dangerous patterns with parent directory traversal.
|
|
23
|
+
* Matches ".." at start, after "/", or after "\" (Windows).
|
|
24
|
+
*/
|
|
25
|
+
const DANGEROUS_PATTERN_REGEX = /(?:^|[\\/])\.\./;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalize path to POSIX format (forward slashes).
|
|
29
|
+
*/
|
|
30
|
+
function toPosixPath(path: string): string {
|
|
31
|
+
if (sep === '/') {
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
return path.replaceAll(sep, '/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate glob pattern is safe (no directory traversal).
|
|
39
|
+
* Returns error message if invalid, null if valid.
|
|
40
|
+
*/
|
|
41
|
+
function validatePattern(pattern: string): string | null {
|
|
42
|
+
if (isAbsolute(pattern)) {
|
|
43
|
+
return 'Pattern must be relative, not absolute';
|
|
44
|
+
}
|
|
45
|
+
if (DANGEROUS_PATTERN_REGEX.test(pattern)) {
|
|
46
|
+
return 'Pattern contains dangerous parent directory reference (..)';
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compute safe relative path from root to file.
|
|
53
|
+
* Returns null if file is outside root (security check).
|
|
54
|
+
* Uses realpath to resolve symlinks and normalize case.
|
|
55
|
+
*/
|
|
56
|
+
async function safeRelPath(
|
|
57
|
+
rootReal: string,
|
|
58
|
+
absPath: string
|
|
59
|
+
): Promise<string | null> {
|
|
60
|
+
try {
|
|
61
|
+
const fileReal = await realpath(absPath);
|
|
62
|
+
const rel = relative(rootReal, fileReal);
|
|
63
|
+
|
|
64
|
+
// Reject if relative path escapes root
|
|
65
|
+
// Check for ".." at start followed by separator or end (not just ".." prefix)
|
|
66
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return toPosixPath(rel);
|
|
71
|
+
} catch {
|
|
72
|
+
// Can't resolve path (e.g., broken symlink)
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a path matches any exclude pattern.
|
|
79
|
+
*
|
|
80
|
+
* Exclude semantics (component-based matching):
|
|
81
|
+
* - Patterns match against path components (directory/file names)
|
|
82
|
+
* - "node_modules" matches any path containing "node_modules" as a component
|
|
83
|
+
* - ".git" matches ".git" directory at any level
|
|
84
|
+
* - Patterns are NOT globs - they match exact component names
|
|
85
|
+
*
|
|
86
|
+
* Examples:
|
|
87
|
+
* - exclude: [".git"] matches "foo/.git/bar" but not "foo/.github/..."
|
|
88
|
+
* - exclude: ["dist"] matches "dist/bundle.js" and "src/dist/output.js"
|
|
89
|
+
*/
|
|
90
|
+
function matchesExclude(relPath: string, excludes: string[]): boolean {
|
|
91
|
+
const parts = relPath.split('/');
|
|
92
|
+
|
|
93
|
+
for (const pattern of excludes) {
|
|
94
|
+
// Check if any path component matches exactly
|
|
95
|
+
if (parts.includes(pattern)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
// Check if path starts with pattern
|
|
99
|
+
if (relPath.startsWith(`${pattern}/`)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a file extension matches the include list.
|
|
109
|
+
* Include list contains extensions like ".md" or "md" (normalized).
|
|
110
|
+
*/
|
|
111
|
+
function matchesInclude(relPath: string, include: string[]): boolean {
|
|
112
|
+
if (include.length === 0) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ext = extname(relPath).toLowerCase();
|
|
117
|
+
if (!ext) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return include.some((inc) => {
|
|
122
|
+
const normalizedInc = inc.startsWith('.')
|
|
123
|
+
? inc.toLowerCase()
|
|
124
|
+
: `.${inc.toLowerCase()}`;
|
|
125
|
+
return ext === normalizedInc;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* File walker implementation using Bun.Glob.
|
|
131
|
+
*
|
|
132
|
+
* Security: Validates patterns and ensures all matched files are within
|
|
133
|
+
* the collection root directory. Files outside root are silently ignored.
|
|
134
|
+
*/
|
|
135
|
+
export class FileWalker implements WalkerPort {
|
|
136
|
+
async walk(config: WalkConfig): Promise<{
|
|
137
|
+
entries: WalkEntry[];
|
|
138
|
+
skipped: SkippedEntry[];
|
|
139
|
+
}> {
|
|
140
|
+
const entries: WalkEntry[] = [];
|
|
141
|
+
const skipped: SkippedEntry[] = [];
|
|
142
|
+
|
|
143
|
+
// Validate pattern for security
|
|
144
|
+
const patternError = validatePattern(config.pattern);
|
|
145
|
+
if (patternError) {
|
|
146
|
+
throw new Error(`Invalid glob pattern: ${patternError}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Resolve root to real path for consistent comparison
|
|
150
|
+
const rootAbs = resolve(config.root);
|
|
151
|
+
let rootReal: string;
|
|
152
|
+
try {
|
|
153
|
+
rootReal = await realpath(rootAbs);
|
|
154
|
+
} catch {
|
|
155
|
+
// Root doesn't exist
|
|
156
|
+
return { entries: [], skipped: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const glob = new Bun.Glob(config.pattern);
|
|
160
|
+
|
|
161
|
+
for await (const match of glob.scan({
|
|
162
|
+
cwd: rootReal,
|
|
163
|
+
absolute: true,
|
|
164
|
+
onlyFiles: true,
|
|
165
|
+
followSymlinks: false,
|
|
166
|
+
})) {
|
|
167
|
+
const absPath = normalizePath(match);
|
|
168
|
+
|
|
169
|
+
// Security: Compute safe relative path (validates file is within root)
|
|
170
|
+
const relPath = await safeRelPath(rootReal, absPath);
|
|
171
|
+
if (relPath === null) {
|
|
172
|
+
// File outside root or unresolvable - silently skip (security)
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check exclude patterns
|
|
177
|
+
if (matchesExclude(relPath, config.exclude)) {
|
|
178
|
+
skipped.push({
|
|
179
|
+
absPath,
|
|
180
|
+
relPath,
|
|
181
|
+
reason: 'EXCLUDED',
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check include extensions
|
|
187
|
+
if (!matchesInclude(relPath, config.include)) {
|
|
188
|
+
skipped.push({
|
|
189
|
+
absPath,
|
|
190
|
+
relPath,
|
|
191
|
+
reason: 'EXCLUDED',
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Stat file
|
|
197
|
+
const file = Bun.file(absPath);
|
|
198
|
+
let stat: { size: number; mtime: Date };
|
|
199
|
+
try {
|
|
200
|
+
stat = await file.stat();
|
|
201
|
+
} catch {
|
|
202
|
+
// Can't stat file, skip silently
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check maxBytes BEFORE reading
|
|
207
|
+
if (stat.size > config.maxBytes) {
|
|
208
|
+
skipped.push({
|
|
209
|
+
absPath,
|
|
210
|
+
relPath,
|
|
211
|
+
reason: 'TOO_LARGE',
|
|
212
|
+
size: stat.size,
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
entries.push({
|
|
218
|
+
absPath,
|
|
219
|
+
relPath,
|
|
220
|
+
size: stat.size,
|
|
221
|
+
mtime: stat.mtime.toISOString(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Sort entries by relPath for deterministic output
|
|
226
|
+
entries.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
227
|
+
|
|
228
|
+
return { entries, skipped };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Default walker instance.
|
|
234
|
+
*/
|
|
235
|
+
export const defaultWalker = new FileWalker();
|