@lucas-bur/pix 0.9.0 → 0.10.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/dist/index.mjs +525 -170
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
4
|
-
import { Clock, Console, Context, Data, Effect, Layer, Option } from "effect";
|
|
5
3
|
import { Args, CliConfig, Command, Options } from "@effect/cli";
|
|
6
|
-
import
|
|
4
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
5
|
+
import { Clock, Context, Data, Effect, Layer, Option, Ref } from "effect";
|
|
6
|
+
import { styleText } from "node:util";
|
|
7
|
+
import * as clack from "@clack/prompts";
|
|
7
8
|
import { FileSystem } from "@effect/platform";
|
|
9
|
+
import crypto from "node:crypto";
|
|
8
10
|
import { env } from "@huggingface/transformers";
|
|
9
11
|
import ignore from "ignore";
|
|
10
12
|
//#region src/domain/ports.ts
|
|
11
13
|
var ConfigStore = class extends Context.Tag("ConfigStore")() {};
|
|
12
14
|
var Scanner = class extends Context.Tag("Scanner")() {};
|
|
15
|
+
var ContentExtractor = class extends Context.Tag("ContentExtractor")() {};
|
|
13
16
|
var Chunker = class extends Context.Tag("Chunker")() {};
|
|
14
17
|
var Embedder = class extends Context.Tag("Embedder")() {};
|
|
15
18
|
var VectorStore = class extends Context.Tag("VectorStore")() {};
|
|
@@ -32,6 +35,104 @@ var GetStatus = class extends Effect.Service()("GetStatus", {
|
|
|
32
35
|
return { getStatus };
|
|
33
36
|
})
|
|
34
37
|
}) {};
|
|
38
|
+
Data.taggedEnum();
|
|
39
|
+
/** Display context tag — commands use `yield* Display` to produce output */
|
|
40
|
+
var Display = class extends Context.Tag("Display")() {};
|
|
41
|
+
/** Maps severity to the corresponding @clack/prompts log function */
|
|
42
|
+
const severityToClack = {
|
|
43
|
+
info: clack.log.info,
|
|
44
|
+
success: clack.log.success,
|
|
45
|
+
warn: clack.log.warning,
|
|
46
|
+
error: clack.log.error
|
|
47
|
+
};
|
|
48
|
+
/** Styling helpers using node:util styleText (zero-deps, Node 21+) */
|
|
49
|
+
const terminalStyle = {
|
|
50
|
+
status: (message) => styleText("bold", message),
|
|
51
|
+
dim: (message) => styleText("dim", message)
|
|
52
|
+
};
|
|
53
|
+
/** Extract the message text from an UpdateInteractivePayload */
|
|
54
|
+
const payloadText = (p) => typeof p === "string" ? p : p.message;
|
|
55
|
+
/**
|
|
56
|
+
* Compute the delta for a progress bar from the payload + current state. Returns 0 if there is no
|
|
57
|
+
* numeric payload or if the active element is a spinner.
|
|
58
|
+
*/
|
|
59
|
+
const computeDelta = (p, state) => {
|
|
60
|
+
if (typeof p === "string") return 0;
|
|
61
|
+
if ("advanceBy" in p && p.advanceBy !== void 0) return Math.max(-state.value, p.advanceBy);
|
|
62
|
+
if ("setTo" in p && p.setTo !== void 0) return Math.max(0, Math.min(state.max, p.setTo)) - state.value;
|
|
63
|
+
if ("setToPercent" in p && p.setToPercent !== void 0) {
|
|
64
|
+
const target = Math.floor(state.max * p.setToPercent / 100);
|
|
65
|
+
return Math.max(-state.value, Math.min(state.max - state.value, target - state.value));
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Extracts the "guarded interactive" pattern: skip if already active, otherwise
|
|
71
|
+
* acquire-use-release.
|
|
72
|
+
*/
|
|
73
|
+
const withInteractive = (activeRef, acquire, setActive, release, effect) => Ref.get(activeRef).pipe(Effect.flatMap((current) => current !== null ? effect : Effect.acquireUseRelease(acquire.pipe(Effect.tap((h) => Ref.set(activeRef, setActive(h)))), () => effect, (h, exit) => Ref.set(activeRef, null).pipe(Effect.andThen(release(h, exit))))));
|
|
74
|
+
/** Display implementation using @clack/prompts for interactive terminal output */
|
|
75
|
+
const ClackDisplay = { layer: Layer.effect(Display, Effect.gen(function* () {
|
|
76
|
+
const activeRef = yield* Ref.make(null);
|
|
77
|
+
return {
|
|
78
|
+
intro: (title) => Effect.sync(() => clack.intro(styleText("inverse", ` ${title} `))),
|
|
79
|
+
outro: (message) => Effect.sync(() => clack.outro(message)),
|
|
80
|
+
log: (message, severity) => Effect.sync(() => severityToClack[severity](terminalStyle.status(message))),
|
|
81
|
+
note: (content, title) => Effect.sync(() => clack.note(content, title)),
|
|
82
|
+
text: (message) => Effect.sync(() => clack.log.message(message)),
|
|
83
|
+
spinner: (message, effect) => withInteractive(activeRef, Effect.sync(() => {
|
|
84
|
+
const s = clack.spinner();
|
|
85
|
+
s.start(message);
|
|
86
|
+
return s;
|
|
87
|
+
}), (s) => ({
|
|
88
|
+
type: "spinner",
|
|
89
|
+
handle: s
|
|
90
|
+
}), (s, exit) => Effect.sync(() => s.stop(exit._tag === "Success" ? message : `${message} (failed)`)), effect),
|
|
91
|
+
progress: (opts, effect) => withInteractive(activeRef, Effect.sync(() => {
|
|
92
|
+
const bar = clack.progress({
|
|
93
|
+
max: opts.max,
|
|
94
|
+
style: opts.style ?? "heavy",
|
|
95
|
+
size: opts.size ?? 40,
|
|
96
|
+
indicator: opts.indicator ?? "dots"
|
|
97
|
+
});
|
|
98
|
+
bar.start(opts.message);
|
|
99
|
+
return bar;
|
|
100
|
+
}), (bar) => ({
|
|
101
|
+
type: "progress",
|
|
102
|
+
handle: bar,
|
|
103
|
+
value: 0,
|
|
104
|
+
max: opts.max
|
|
105
|
+
}), (bar, exit) => Effect.sync(() => exit._tag === "Success" ? bar.stop(opts.message) : bar.error(opts.message)), effect),
|
|
106
|
+
updateInteractive: (payload) => Ref.get(activeRef).pipe(Effect.flatMap((active) => {
|
|
107
|
+
if (!active) return Effect.void;
|
|
108
|
+
if (active.type === "spinner") return Effect.sync(() => active.handle.message(payloadText(payload)));
|
|
109
|
+
const delta = computeDelta(payload, {
|
|
110
|
+
value: active.value,
|
|
111
|
+
max: active.max
|
|
112
|
+
});
|
|
113
|
+
const newValue = Math.max(0, Math.min(active.max, active.value + delta));
|
|
114
|
+
return Effect.sync(() => {
|
|
115
|
+
active.handle.advance(delta, payloadText(payload));
|
|
116
|
+
}).pipe(Effect.andThen(Ref.update(activeRef, (current) => current && current.type === "progress" ? {
|
|
117
|
+
...current,
|
|
118
|
+
value: newValue
|
|
119
|
+
} : current)));
|
|
120
|
+
})),
|
|
121
|
+
json: () => Effect.void
|
|
122
|
+
};
|
|
123
|
+
})) };
|
|
124
|
+
/** Display implementation for --json mode — no-ops interactive methods, writes JSON to stdout */
|
|
125
|
+
const JsonDisplay = { layer: Layer.succeed(Display, {
|
|
126
|
+
intro: () => Effect.void,
|
|
127
|
+
outro: () => Effect.void,
|
|
128
|
+
log: () => Effect.void,
|
|
129
|
+
note: () => Effect.void,
|
|
130
|
+
text: () => Effect.void,
|
|
131
|
+
spinner: (_message, effect) => effect,
|
|
132
|
+
progress: (_opts, effect) => effect,
|
|
133
|
+
updateInteractive: () => Effect.void,
|
|
134
|
+
json: (data) => Effect.sync(() => process.stdout.write(`${JSON.stringify(data)}\n`))
|
|
135
|
+
}) };
|
|
35
136
|
//#endregion
|
|
36
137
|
//#region src/domain/config.ts
|
|
37
138
|
var ConfigError = class extends Data.TaggedError("ConfigError") {};
|
|
@@ -40,7 +141,23 @@ const DEFAULT_CONFIG = {
|
|
|
40
141
|
chunkLines: 60,
|
|
41
142
|
overlapLines: 10,
|
|
42
143
|
chunkConcurrency: 8,
|
|
43
|
-
|
|
144
|
+
skipExtensions: [],
|
|
145
|
+
ignoredPaths: [
|
|
146
|
+
".pix",
|
|
147
|
+
"node_modules",
|
|
148
|
+
".git",
|
|
149
|
+
"dist",
|
|
150
|
+
"build",
|
|
151
|
+
".next",
|
|
152
|
+
".agents",
|
|
153
|
+
".claude",
|
|
154
|
+
".vscode",
|
|
155
|
+
".github",
|
|
156
|
+
"coverage",
|
|
157
|
+
"*-lock.yaml",
|
|
158
|
+
"*-lock.json",
|
|
159
|
+
"*.lock"
|
|
160
|
+
],
|
|
44
161
|
embedder: {
|
|
45
162
|
model: "Xenova/all-MiniLM-L6-v2",
|
|
46
163
|
device: "auto",
|
|
@@ -48,10 +165,158 @@ const DEFAULT_CONFIG = {
|
|
|
48
165
|
}
|
|
49
166
|
};
|
|
50
167
|
//#endregion
|
|
168
|
+
//#region src/domain/errors.ts
|
|
169
|
+
/** Config file or directory does not exist. Run pix init first. */
|
|
170
|
+
var ConfigNotFoundError = class extends Data.TaggedError("ConfigNotFoundError") {};
|
|
171
|
+
/** Config file exists but contains invalid JSON. */
|
|
172
|
+
var ConfigMalformedError = class extends Data.TaggedError("ConfigMalformedError") {};
|
|
173
|
+
/** Index files (chunks.jsonl, vectors.bin) do not exist. Run pix index first. */
|
|
174
|
+
var NoIndexError = class extends Data.TaggedError("NoIndexError") {};
|
|
175
|
+
/** Disk is full — write operation could not complete. */
|
|
176
|
+
var DiskFullError = class extends Data.TaggedError("DiskFullError") {};
|
|
177
|
+
/** Generic index store I/O failure (read, write, delete). */
|
|
178
|
+
var StoreError = class extends Data.TaggedError("StoreError") {};
|
|
179
|
+
/** Source file could not be read during chunking (binary, permissions, encoding). */
|
|
180
|
+
var ChunkerError = class extends Data.TaggedError("ChunkerError") {};
|
|
181
|
+
/** Embedding model could not be downloaded or loaded. */
|
|
182
|
+
var ModelLoadError = class extends Data.TaggedError("ModelLoadError") {};
|
|
183
|
+
/** Embedding model failed during inference. */
|
|
184
|
+
var InferenceError = class extends Data.TaggedError("InferenceError") {};
|
|
185
|
+
/**
|
|
186
|
+
* Fatal scan failure — gitignore loading failed entirely. Non-fatal per-entry skips are reported
|
|
187
|
+
* via ScanResult.skipped.
|
|
188
|
+
*/
|
|
189
|
+
var ScanFailed = class extends Data.TaggedError("ScanFailed") {};
|
|
190
|
+
/** File type is unsupported for text extraction. */
|
|
191
|
+
var UnsupportedFormat = class extends Data.TaggedError("UnsupportedFormat") {};
|
|
192
|
+
/** Text extraction failed for a supported file type. */
|
|
193
|
+
var ExtractionFailed = class extends Data.TaggedError("ExtractionFailed") {};
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/services/processors/identity.ts
|
|
196
|
+
const identityProcessor = (file) => FileSystem.FileSystem.pipe(Effect.flatMap((fs) => fs.readFileString(file)), Effect.mapError((cause) => new ExtractionFailed({
|
|
197
|
+
message: `Failed to read file for extraction: ${file}`,
|
|
198
|
+
file,
|
|
199
|
+
cause
|
|
200
|
+
})));
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/services/processors/skip.ts
|
|
203
|
+
const skipProcessor = (extension) => {
|
|
204
|
+
const error = new UnsupportedFormat({
|
|
205
|
+
message: `Unsupported file type: ${extension}`,
|
|
206
|
+
extension
|
|
207
|
+
});
|
|
208
|
+
return (_file) => Effect.fail(error);
|
|
209
|
+
};
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/services/processors/index.ts
|
|
212
|
+
const DEFAULT_PROCESSOR_MAP = {
|
|
213
|
+
".ts": identityProcessor,
|
|
214
|
+
".tsx": identityProcessor,
|
|
215
|
+
".js": identityProcessor,
|
|
216
|
+
".jsx": identityProcessor,
|
|
217
|
+
".py": identityProcessor,
|
|
218
|
+
".rs": identityProcessor,
|
|
219
|
+
".go": identityProcessor,
|
|
220
|
+
".java": identityProcessor,
|
|
221
|
+
".c": identityProcessor,
|
|
222
|
+
".cpp": identityProcessor,
|
|
223
|
+
".h": identityProcessor,
|
|
224
|
+
".hpp": identityProcessor,
|
|
225
|
+
".json": identityProcessor,
|
|
226
|
+
".yaml": identityProcessor,
|
|
227
|
+
".yml": identityProcessor,
|
|
228
|
+
".toml": identityProcessor,
|
|
229
|
+
".xml": identityProcessor,
|
|
230
|
+
".csv": identityProcessor,
|
|
231
|
+
".md": identityProcessor,
|
|
232
|
+
".mdx": identityProcessor,
|
|
233
|
+
".txt": identityProcessor,
|
|
234
|
+
".rst": identityProcessor,
|
|
235
|
+
".html": identityProcessor,
|
|
236
|
+
".css": identityProcessor,
|
|
237
|
+
".scss": identityProcessor,
|
|
238
|
+
".less": identityProcessor,
|
|
239
|
+
".sql": identityProcessor,
|
|
240
|
+
".graphql": identityProcessor,
|
|
241
|
+
".sh": identityProcessor,
|
|
242
|
+
".bash": identityProcessor,
|
|
243
|
+
".ps1": identityProcessor,
|
|
244
|
+
".bat": identityProcessor,
|
|
245
|
+
".cmake": identityProcessor,
|
|
246
|
+
".dockerfile": identityProcessor,
|
|
247
|
+
dockerfile: identityProcessor,
|
|
248
|
+
makefile: identityProcessor,
|
|
249
|
+
gemfile: identityProcessor,
|
|
250
|
+
".pdf": skipProcessor(".pdf"),
|
|
251
|
+
".png": skipProcessor(".png"),
|
|
252
|
+
".jpg": skipProcessor(".jpg"),
|
|
253
|
+
".jpeg": skipProcessor(".jpeg"),
|
|
254
|
+
".gif": skipProcessor(".gif"),
|
|
255
|
+
".svg": identityProcessor,
|
|
256
|
+
".ico": skipProcessor(".ico"),
|
|
257
|
+
".webp": skipProcessor(".webp"),
|
|
258
|
+
".mp3": skipProcessor(".mp3"),
|
|
259
|
+
".mp4": skipProcessor(".mp4"),
|
|
260
|
+
".wav": skipProcessor(".wav"),
|
|
261
|
+
".avi": skipProcessor(".avi"),
|
|
262
|
+
".mov": skipProcessor(".mov"),
|
|
263
|
+
".mkv": skipProcessor(".mkv"),
|
|
264
|
+
".exe": skipProcessor(".exe"),
|
|
265
|
+
".dll": skipProcessor(".dll"),
|
|
266
|
+
".so": skipProcessor(".so"),
|
|
267
|
+
".zip": skipProcessor(".zip"),
|
|
268
|
+
".tar": skipProcessor(".tar"),
|
|
269
|
+
".gz": skipProcessor(".gz"),
|
|
270
|
+
".7z": skipProcessor(".7z"),
|
|
271
|
+
".rar": skipProcessor(".rar"),
|
|
272
|
+
".ttf": skipProcessor(".ttf"),
|
|
273
|
+
".woff": skipProcessor(".woff"),
|
|
274
|
+
".woff2": skipProcessor(".woff2"),
|
|
275
|
+
".eot": skipProcessor(".eot"),
|
|
276
|
+
".otf": skipProcessor(".otf"),
|
|
277
|
+
".lock": identityProcessor,
|
|
278
|
+
lock: identityProcessor
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Builds the processor map by merging domain defaults with user-specified skip extensions. Skip
|
|
282
|
+
* extensions override any existing mapping with a skip processor. Unknown extensions remain absent
|
|
283
|
+
* from the map — callers decide how to handle them.
|
|
284
|
+
*/
|
|
285
|
+
function buildProcessorMap(skipExtensions) {
|
|
286
|
+
const mapped = { ...DEFAULT_PROCESSOR_MAP };
|
|
287
|
+
for (const ext of skipExtensions) mapped[ext] = skipProcessor(ext);
|
|
288
|
+
return mapped;
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
51
291
|
//#region src/application/index-project.ts
|
|
292
|
+
function getExtension(file) {
|
|
293
|
+
const lastSlash = file.lastIndexOf("/");
|
|
294
|
+
const name = lastSlash >= 0 ? file.slice(lastSlash + 1) : file;
|
|
295
|
+
const dotIndex = name.lastIndexOf(".");
|
|
296
|
+
if (dotIndex === -1) return name.toLowerCase();
|
|
297
|
+
return name.slice(dotIndex).toLowerCase();
|
|
298
|
+
}
|
|
299
|
+
const classifyFiles = (files, processorMap) => {
|
|
300
|
+
const knownFiles = [];
|
|
301
|
+
const skippedFiles = [];
|
|
302
|
+
const unknownExtensions = /* @__PURE__ */ new Set();
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
const ext = getExtension(file);
|
|
305
|
+
if (!processorMap[ext]) {
|
|
306
|
+
unknownExtensions.add(ext);
|
|
307
|
+
skippedFiles.push(file);
|
|
308
|
+
} else knownFiles.push(file);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
knownFiles,
|
|
312
|
+
skippedFiles,
|
|
313
|
+
unknownExtensions
|
|
314
|
+
};
|
|
315
|
+
};
|
|
52
316
|
/**
|
|
53
|
-
* Use case: index project files. Pipeline: scan → chunk → embed → store. Depends
|
|
54
|
-
* Scanner, Chunker, Embedder, VectorStore via Effect
|
|
317
|
+
* Use case: index project files. Pipeline: scan → ContentExtractor → chunk → embed → store. Depends
|
|
318
|
+
* on ConfigStore, Scanner, Chunker, Embedder, VectorStore, Display, ContentExtractor via Effect
|
|
319
|
+
* tags.
|
|
55
320
|
*/
|
|
56
321
|
var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
57
322
|
accessors: true,
|
|
@@ -61,32 +326,50 @@ var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
|
61
326
|
const chunker = yield* Chunker;
|
|
62
327
|
const embedder = yield* Embedder;
|
|
63
328
|
const vectorStore = yield* VectorStore;
|
|
329
|
+
const d = yield* Display;
|
|
330
|
+
const extractor = yield* ContentExtractor;
|
|
64
331
|
const index = () => Effect.gen(function* () {
|
|
65
332
|
if (!(yield* configStore.configExists())) yield* configStore.writeConfig(DEFAULT_CONFIG);
|
|
66
333
|
const config = yield* configStore.readConfig();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
334
|
+
const processorMap = buildProcessorMap(config.skipExtensions);
|
|
335
|
+
yield* d.updateInteractive("Scanning source files...");
|
|
336
|
+
const ignoredPaths = config.ignoredPaths ?? DEFAULT_CONFIG.ignoredPaths;
|
|
337
|
+
const { knownFiles, skippedFiles, unknownExtensions } = classifyFiles((yield* scanner.scanFiles(ignoredPaths)).files, processorMap);
|
|
338
|
+
if (unknownExtensions.size > 0) yield* d.log(`Skipped ${skippedFiles.length} files with unknown extensions: ${[...unknownExtensions].join(", ")}`, "warn");
|
|
339
|
+
if (knownFiles.length === 0) return {
|
|
340
|
+
success: true,
|
|
341
|
+
status: {
|
|
342
|
+
chunks: 0,
|
|
343
|
+
files: 0,
|
|
344
|
+
totalLines: 0,
|
|
345
|
+
byteSize: 0
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
yield* d.updateInteractive(`Processing ${knownFiles.length} files...`);
|
|
349
|
+
const allChunks = (yield* Effect.forEach(knownFiles, (file) => Effect.gen(function* () {
|
|
350
|
+
const result = yield* Effect.either(extractor.extract(file));
|
|
351
|
+
if (result._tag === "Left") {
|
|
352
|
+
if (result.left._tag === "UnsupportedFormat") {
|
|
353
|
+
yield* d.log(`Skipping ${file}: ${result.left.message}`, "warn");
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
return yield* Effect.fail(result.left);
|
|
357
|
+
}
|
|
358
|
+
return yield* chunker.chunkText(result.right, file);
|
|
359
|
+
}), { concurrency: Math.max(1, config.chunkConcurrency ?? 8) })).flat();
|
|
75
360
|
const totalChunks = allChunks.length;
|
|
76
361
|
const totalFiles = new Set(allChunks.map((c) => c.file)).size;
|
|
77
362
|
const totalLines = allChunks.reduce((sum, c) => sum + (c.endLine - c.startLine + 1), 0);
|
|
78
|
-
if (totalChunks === 0) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
}
|
|
363
|
+
if (totalChunks === 0) return {
|
|
364
|
+
success: true,
|
|
365
|
+
status: {
|
|
366
|
+
chunks: 0,
|
|
367
|
+
files: 0,
|
|
368
|
+
totalLines: 0,
|
|
369
|
+
byteSize: 0
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
yield* d.updateInteractive(`Embedding ${totalChunks} chunks...`);
|
|
90
373
|
const texts = allChunks.map((c) => c.text);
|
|
91
374
|
const embeddings = yield* embedder.batch(texts);
|
|
92
375
|
yield* vectorStore.store(allChunks, embeddings);
|
|
@@ -183,41 +466,41 @@ const formatError = (error) => JSON.stringify({
|
|
|
183
466
|
message: messageFromError(error),
|
|
184
467
|
cause: causeFromError(error)
|
|
185
468
|
});
|
|
186
|
-
/** Log the error
|
|
187
|
-
const reportError = (error) =>
|
|
469
|
+
/** Log the error to Display in human + agent format, then re-fail to preserve non-zero exit code. */
|
|
470
|
+
const reportError = (error) => Effect.gen(function* () {
|
|
471
|
+
const d = yield* Display;
|
|
472
|
+
yield* d.log(`${codeFromError(error)}: ${messageFromError(error)}`, "error");
|
|
473
|
+
yield* d.json(JSON.parse(formatError(error)));
|
|
474
|
+
return yield* Effect.fail(error);
|
|
475
|
+
});
|
|
188
476
|
//#endregion
|
|
189
477
|
//#region src/commands/index-cmd.ts
|
|
190
|
-
const logFlagWarnings = (force, verbose, json) => {
|
|
191
|
-
if (json) return Effect.void;
|
|
192
|
-
const warnings = [force ? "--force is currently not implemented and only a placeholder." : void 0, verbose ? "--verbose is currently not implemented and only a placeholder." : void 0].filter((msg) => msg !== void 0);
|
|
193
|
-
return Effect.forEach(warnings, (msg) => Effect.logInfo(msg), { discard: true });
|
|
194
|
-
};
|
|
195
|
-
const logHumanOutput = (chunks, files, duration) => Effect.logInfo(`Indexed ${chunks} chunks from ${files} files in ${duration}.`);
|
|
196
478
|
/** CLI command: pix index [--force] [--verbose] [--json] */
|
|
197
479
|
const indexCommand = Command.make("index", {
|
|
198
480
|
force: Options.boolean("force").pipe(Options.withDefault(false)),
|
|
199
481
|
verbose: Options.boolean("verbose").pipe(Options.withDefault(false)),
|
|
200
482
|
json: Options.boolean("json").pipe(Options.withDefault(false))
|
|
201
|
-
}, ({ force, verbose
|
|
202
|
-
yield*
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
483
|
+
}, ({ force, verbose }) => Effect.gen(function* () {
|
|
484
|
+
const d = yield* Display;
|
|
485
|
+
if (force) yield* d.log("--force is currently not implemented and only a placeholder.", "warn");
|
|
486
|
+
if (verbose) yield* d.log("--verbose is currently not implemented and only a placeholder.", "warn");
|
|
487
|
+
const result = yield* d.spinner("Indexing project...", IndexProject.index());
|
|
488
|
+
yield* d.json({
|
|
207
489
|
chunks: result.status.chunks,
|
|
208
|
-
files: result.status.files
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
yield*
|
|
490
|
+
files: result.status.files
|
|
491
|
+
});
|
|
492
|
+
if (result.status.chunks === 0) yield* d.log("No chunks to index.", "warn");
|
|
493
|
+
else yield* d.log(`Indexed ${result.status.chunks} chunks from ${result.status.files} files.`, "success");
|
|
212
494
|
}).pipe(Effect.catchAll(reportError)));
|
|
213
495
|
//#endregion
|
|
214
496
|
//#region src/commands/init.ts
|
|
215
497
|
/** CLI command: pix init [--json] */
|
|
216
|
-
const initCommand = Command.make("init", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, (
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
yield*
|
|
220
|
-
yield*
|
|
498
|
+
const initCommand = Command.make("init", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, () => Effect.gen(function* () {
|
|
499
|
+
const d = yield* Display;
|
|
500
|
+
const result = yield* d.spinner("Initializing...", InitProject.init());
|
|
501
|
+
yield* d.json(result);
|
|
502
|
+
yield* d.log("Created .pix/config.json with default settings.", "success");
|
|
503
|
+
yield* d.note("Add `.pix` to your `.gitignore` file to avoid committing the index.", "Reminder");
|
|
221
504
|
}).pipe(Effect.catchTags({
|
|
222
505
|
ConfigError: reportError,
|
|
223
506
|
DiskFullError: reportError
|
|
@@ -258,30 +541,22 @@ const toJsonOutput = (results, ctxLines) => results.map((r) => ({
|
|
|
258
541
|
...ctxLines > 0 && r.contextBefore && { contextBefore: r.contextBefore },
|
|
259
542
|
...ctxLines > 0 && r.contextAfter && { contextAfter: r.contextAfter }
|
|
260
543
|
}));
|
|
261
|
-
const renderResults = (results) => Effect.gen(function* () {
|
|
262
|
-
if (results.length === 0) {
|
|
263
|
-
yield* Effect.logInfo("No results found");
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
for (const result of results) {
|
|
267
|
-
yield* Console.log(formatResult(result));
|
|
268
|
-
yield* Console.log("---");
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
544
|
/** CLI command: pix query "<text>" [--top N] [--json] [--context-lines N] */
|
|
272
545
|
const queryCommand = Command.make("query", {
|
|
273
546
|
queryText: Args.text({ name: "query" }),
|
|
274
547
|
top: Options.integer("top").pipe(Options.withDefault(DEFAULT_TOP_K), Options.optional),
|
|
275
548
|
json: Options.boolean("json").pipe(Options.withDefault(false)),
|
|
276
549
|
contextLines: Options.integer("context-lines").pipe(Options.withDefault(DEFAULT_CONTEXT_LINES), Options.optional)
|
|
277
|
-
}, ({ queryText, top,
|
|
550
|
+
}, ({ queryText, top, contextLines }) => Effect.gen(function* () {
|
|
551
|
+
const d = yield* Display;
|
|
278
552
|
const topK = Option.getOrElse(top, () => DEFAULT_TOP_K);
|
|
279
553
|
const ctxLines = Option.getOrElse(contextLines, () => DEFAULT_CONTEXT_LINES);
|
|
280
554
|
const clamped = clampTopK(topK);
|
|
281
|
-
if (clamped.clamped
|
|
282
|
-
const results = yield* QueryProject.queryProject(queryText, clamped.value);
|
|
283
|
-
|
|
284
|
-
yield*
|
|
555
|
+
if (clamped.clamped) yield* d.log(`topK clamped from ${topK} to ${clamped.value}`, "warn");
|
|
556
|
+
const results = yield* d.spinner("Searching...", QueryProject.queryProject(queryText, clamped.value));
|
|
557
|
+
yield* d.json(toJsonOutput(results, ctxLines));
|
|
558
|
+
if (results.length === 0) yield* d.log("No results found", "warn");
|
|
559
|
+
else for (const result of results) yield* d.text(formatResult(result));
|
|
285
560
|
}).pipe(Effect.catchTags({
|
|
286
561
|
ModelLoadError: reportError,
|
|
287
562
|
InferenceError: reportError,
|
|
@@ -305,30 +580,26 @@ const formatBytes = (bytes) => {
|
|
|
305
580
|
};
|
|
306
581
|
//#endregion
|
|
307
582
|
//#region src/commands/reset.ts
|
|
308
|
-
const logJsonResult = (result, elapsedMs) => Console.log(JSON.stringify({
|
|
309
|
-
status: "ok",
|
|
310
|
-
deletedChunks: result.deletedChunks,
|
|
311
|
-
deletedVectors: result.deletedVectors,
|
|
312
|
-
freedBytes: result.freedBytes,
|
|
313
|
-
elapsedMs
|
|
314
|
-
}));
|
|
315
|
-
const logHumanResult = (result, elapsedMs) => Effect.gen(function* () {
|
|
316
|
-
const deletedParts = [result.deletedChunks ? "chunks.jsonl" : null, result.deletedVectors ? "vectors.bin" : null].filter((part) => part !== null);
|
|
317
|
-
if (deletedParts.length === 0) {
|
|
318
|
-
yield* Effect.logInfo("Nothing to reset.");
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
yield* Effect.logInfo(`Deleted: ${deletedParts.join(", ")}`);
|
|
322
|
-
yield* Effect.logInfo(`Freed: ${formatBytes(result.freedBytes)}`);
|
|
323
|
-
yield* Effect.logInfo(`Time: ${elapsedMs}ms`);
|
|
324
|
-
});
|
|
325
583
|
/** CLI command: pix reset [--json] */
|
|
326
|
-
const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, (
|
|
584
|
+
const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, () => Effect.gen(function* () {
|
|
585
|
+
const d = yield* Display;
|
|
327
586
|
const start = yield* Clock.currentTimeMillis;
|
|
328
|
-
const result = yield* ResetIndex.reset();
|
|
587
|
+
const result = yield* d.spinner("Resetting index...", ResetIndex.reset());
|
|
329
588
|
const elapsedMs = (yield* Clock.currentTimeMillis) - start;
|
|
330
|
-
|
|
331
|
-
|
|
589
|
+
yield* d.json({
|
|
590
|
+
status: "ok",
|
|
591
|
+
deletedChunks: result.deletedChunks,
|
|
592
|
+
deletedVectors: result.deletedVectors,
|
|
593
|
+
freedBytes: result.freedBytes,
|
|
594
|
+
elapsedMs
|
|
595
|
+
});
|
|
596
|
+
const deletedParts = [result.deletedChunks ? "chunks.jsonl" : null, result.deletedVectors ? "vectors.bin" : null].filter((part) => part !== null);
|
|
597
|
+
if (deletedParts.length === 0) yield* d.log("Nothing to reset.", "info");
|
|
598
|
+
else {
|
|
599
|
+
yield* d.log(`Deleted: ${deletedParts.join(", ")}`, "success");
|
|
600
|
+
yield* d.log(`Freed: ${formatBytes(result.freedBytes)}`, "info");
|
|
601
|
+
yield* d.log(`Time: ${elapsedMs}ms`, "info");
|
|
602
|
+
}
|
|
332
603
|
}).pipe(Effect.catchTags({
|
|
333
604
|
DiskFullError: reportError,
|
|
334
605
|
StoreError: reportError
|
|
@@ -336,22 +607,22 @@ const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(
|
|
|
336
607
|
//#endregion
|
|
337
608
|
//#region src/commands/status.ts
|
|
338
609
|
/** CLI command: pix status [--json] */
|
|
339
|
-
const statusCommand = Command.make("status", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, (
|
|
610
|
+
const statusCommand = Command.make("status", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, () => Effect.gen(function* () {
|
|
611
|
+
const d = yield* Display;
|
|
340
612
|
const result = yield* GetStatus.getStatus();
|
|
341
|
-
|
|
342
|
-
const lastIndexStr = result.lastIndex > 0 ? new Date(result.lastIndex).
|
|
343
|
-
yield*
|
|
344
|
-
yield*
|
|
345
|
-
yield*
|
|
346
|
-
yield*
|
|
347
|
-
yield*
|
|
613
|
+
yield* d.json(result);
|
|
614
|
+
const lastIndexStr = result.lastIndex > 0 ? new Date(result.lastIndex).toLocaleString() : "never";
|
|
615
|
+
yield* d.log(`Indexed: ${result.chunks} chunks across ${result.files} files`, "info");
|
|
616
|
+
yield* d.log(`Model: ${result.model || "none"}`, "info");
|
|
617
|
+
yield* d.log(`Total lines: ${result.totalLines.toLocaleString()}`, "info");
|
|
618
|
+
yield* d.log(`Index size: ${result.byteSize.toLocaleString()} bytes`, "info");
|
|
619
|
+
yield* d.log(`Last indexed: ${lastIndexStr}`, "info");
|
|
348
620
|
}).pipe(Effect.catchTags({ StoreError: reportError })));
|
|
349
621
|
//#endregion
|
|
350
622
|
//#region src/cli.ts
|
|
351
623
|
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
352
624
|
const pix = Command.make("pix", {}, () => Effect.gen(function* () {
|
|
353
|
-
yield*
|
|
354
|
-
yield* Effect.logInfo("Use `pix --help` to see available commands.");
|
|
625
|
+
yield* (yield* Display).log(`pix v${VERSION} - Lightweight local semantic project indexer`, "info");
|
|
355
626
|
})).pipe(Command.withSubcommands([
|
|
356
627
|
initCommand,
|
|
357
628
|
statusCommand,
|
|
@@ -359,33 +630,50 @@ const pix = Command.make("pix", {}, () => Effect.gen(function* () {
|
|
|
359
630
|
queryCommand,
|
|
360
631
|
resetCommand
|
|
361
632
|
]));
|
|
362
|
-
const cli = (args) =>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
633
|
+
const cli = (args) => {
|
|
634
|
+
const displayLayer = args.some((a) => a === "--json") ? JsonDisplay.layer : ClackDisplay.layer;
|
|
635
|
+
return {
|
|
636
|
+
effect: Command.run(pix, {
|
|
637
|
+
name: "pix",
|
|
638
|
+
version: VERSION
|
|
639
|
+
})(args),
|
|
640
|
+
displayLayer
|
|
641
|
+
};
|
|
642
|
+
};
|
|
366
643
|
//#endregion
|
|
367
|
-
//#region src/
|
|
368
|
-
/** Config file or directory does not exist. Run pix init first. */
|
|
369
|
-
var ConfigNotFoundError = class extends Data.TaggedError("ConfigNotFoundError") {};
|
|
370
|
-
/** Config file exists but contains invalid JSON. */
|
|
371
|
-
var ConfigMalformedError = class extends Data.TaggedError("ConfigMalformedError") {};
|
|
372
|
-
/** Index files (chunks.jsonl, vectors.bin) do not exist. Run pix index first. */
|
|
373
|
-
var NoIndexError = class extends Data.TaggedError("NoIndexError") {};
|
|
374
|
-
/** Disk is full — write operation could not complete. */
|
|
375
|
-
var DiskFullError = class extends Data.TaggedError("DiskFullError") {};
|
|
376
|
-
/** Generic index store I/O failure (read, write, delete). */
|
|
377
|
-
var StoreError = class extends Data.TaggedError("StoreError") {};
|
|
378
|
-
/** Source file could not be read during chunking (binary, permissions, encoding). */
|
|
379
|
-
var ChunkerError = class extends Data.TaggedError("ChunkerError") {};
|
|
380
|
-
/** Embedding model could not be downloaded or loaded. */
|
|
381
|
-
var ModelLoadError = class extends Data.TaggedError("ModelLoadError") {};
|
|
382
|
-
/** Embedding model failed during inference. */
|
|
383
|
-
var InferenceError = class extends Data.TaggedError("InferenceError") {};
|
|
644
|
+
//#region src/display/terminalCleanup.ts
|
|
384
645
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
646
|
+
* Terminal cleanup for abrupt exits.
|
|
647
|
+
*
|
|
648
|
+
* @clack/prompts' spinner and taskLog call stdin.setRawMode(true) and hide
|
|
649
|
+
* the cursor via escape sequences. When the process is killed by a signal
|
|
650
|
+
* handler that calls process.exit() directly, clack's own cleanup is
|
|
651
|
+
* bypassed and the terminal is left in raw mode with the cursor hidden.
|
|
652
|
+
*
|
|
653
|
+
* Registering a process 'exit' listener that restores these guarantees the
|
|
654
|
+
* terminal is always left in a usable state.
|
|
387
655
|
*/
|
|
388
|
-
|
|
656
|
+
/** Escape sequence to show the cursor (DECTCEM). */
|
|
657
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
658
|
+
/**
|
|
659
|
+
* Creates a synchronous exit handler that restores terminal state. Extracted as a pure function so
|
|
660
|
+
* it can be unit-tested without side effects.
|
|
661
|
+
*/
|
|
662
|
+
const makeTerminalCleanupHandler = (stdin, stdout) => () => {
|
|
663
|
+
if (stdin.isTTY && stdin.setRawMode) try {
|
|
664
|
+
stdin.setRawMode(false);
|
|
665
|
+
} catch {}
|
|
666
|
+
if (stdout.isTTY) try {
|
|
667
|
+
stdout.write(SHOW_CURSOR);
|
|
668
|
+
} catch {}
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* Registers the terminal cleanup handler on process 'exit'. Call once at program startup
|
|
672
|
+
* (index.ts).
|
|
673
|
+
*/
|
|
674
|
+
const setupTerminalCleanup = () => {
|
|
675
|
+
process.on("exit", makeTerminalCleanupHandler(process.stdin, process.stdout));
|
|
676
|
+
};
|
|
389
677
|
//#endregion
|
|
390
678
|
//#region src/services/chunker.ts
|
|
391
679
|
const MIN_CHUNK_CHARS = 20;
|
|
@@ -418,17 +706,24 @@ const buildChunks = (file, content, config) => {
|
|
|
418
706
|
}
|
|
419
707
|
return chunks;
|
|
420
708
|
};
|
|
421
|
-
const make$
|
|
709
|
+
const make$5 = Effect.gen(function* () {
|
|
422
710
|
const fs = yield* FileSystem.FileSystem;
|
|
423
711
|
const config = yield* (yield* ConfigStore).readConfig().pipe(Effect.catchAll(() => Effect.succeed(DEFAULT_CONFIG)));
|
|
712
|
+
const chunkText = (text, file) => Effect.sync(() => {
|
|
713
|
+
if (text === "") return [];
|
|
714
|
+
return buildChunks(file, text, config);
|
|
715
|
+
});
|
|
424
716
|
const chunkFile = (file) => Effect.gen(function* () {
|
|
425
717
|
const content = yield* readFileContent(fs, file);
|
|
426
718
|
if (content === "") return [];
|
|
427
719
|
return buildChunks(file, content, config);
|
|
428
720
|
});
|
|
429
|
-
return {
|
|
721
|
+
return {
|
|
722
|
+
chunkFile,
|
|
723
|
+
chunkText
|
|
724
|
+
};
|
|
430
725
|
});
|
|
431
|
-
const ChunkerLive = Layer.effect(Chunker, make$
|
|
726
|
+
const ChunkerLive = Layer.effect(Chunker, make$5);
|
|
432
727
|
//#endregion
|
|
433
728
|
//#region src/services/config-store.ts
|
|
434
729
|
const CONFIG_DIR = ".pix";
|
|
@@ -445,7 +740,7 @@ const mapConfigWriteError = (cause, path, action) => {
|
|
|
445
740
|
cause
|
|
446
741
|
});
|
|
447
742
|
};
|
|
448
|
-
const make$
|
|
743
|
+
const make$4 = Effect.gen(function* () {
|
|
449
744
|
const fs = yield* FileSystem.FileSystem;
|
|
450
745
|
const writeConfig = (config) => Effect.gen(function* () {
|
|
451
746
|
const configJson = JSON.stringify(config, null, 2);
|
|
@@ -482,7 +777,28 @@ const make$3 = Effect.gen(function* () {
|
|
|
482
777
|
configExists
|
|
483
778
|
};
|
|
484
779
|
});
|
|
485
|
-
const ConfigStoreLive = Layer.effect(ConfigStore, make$
|
|
780
|
+
const ConfigStoreLive = Layer.effect(ConfigStore, make$4);
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/services/content-extractor.ts
|
|
783
|
+
const make$3 = Effect.gen(function* () {
|
|
784
|
+
const fs = yield* FileSystem.FileSystem;
|
|
785
|
+
const processorMap = buildProcessorMap([]);
|
|
786
|
+
const extract = (file) => {
|
|
787
|
+
const lastSlash = file.lastIndexOf("/");
|
|
788
|
+
const name = lastSlash >= 0 ? file.slice(lastSlash + 1) : file;
|
|
789
|
+
const dotIndex = name.lastIndexOf(".");
|
|
790
|
+
const ext = dotIndex === -1 ? name.toLowerCase() : name.slice(dotIndex).toLowerCase();
|
|
791
|
+
const processor = processorMap[ext];
|
|
792
|
+
if (!processor) return Effect.fail({
|
|
793
|
+
_tag: "UnsupportedFormat",
|
|
794
|
+
message: `No processor for extension: ${ext}`,
|
|
795
|
+
extension: ext
|
|
796
|
+
});
|
|
797
|
+
return processor(file).pipe(Effect.provideService(FileSystem.FileSystem, fs));
|
|
798
|
+
};
|
|
799
|
+
return { extract };
|
|
800
|
+
});
|
|
801
|
+
const ContentExtractorLive = Layer.effect(ContentExtractor, make$3);
|
|
486
802
|
//#endregion
|
|
487
803
|
//#region src/domain/models.ts
|
|
488
804
|
/** Registry of supported embedding models. */
|
|
@@ -559,7 +875,13 @@ const createExtractor = (opts) => Effect.tryPromise(async () => {
|
|
|
559
875
|
const createExtractorWithFallback = (opts) => {
|
|
560
876
|
if (opts.device === "cpu") return createExtractor(opts);
|
|
561
877
|
return createExtractor(opts).pipe(Effect.catchAll((originalError) => Effect.gen(function* () {
|
|
562
|
-
|
|
878
|
+
const d = yield* Display;
|
|
879
|
+
yield* d.log(`GPU (${opts.device}) failed, falling back to CPU...`, "warn");
|
|
880
|
+
yield* d.json({
|
|
881
|
+
event: "embedder_fallback",
|
|
882
|
+
originalDevice: opts.device,
|
|
883
|
+
reason: originalError.message
|
|
884
|
+
});
|
|
563
885
|
return yield* createExtractor({
|
|
564
886
|
...opts,
|
|
565
887
|
device: "cpu"
|
|
@@ -567,7 +889,9 @@ const createExtractorWithFallback = (opts) => {
|
|
|
567
889
|
})));
|
|
568
890
|
};
|
|
569
891
|
const make$2 = Effect.gen(function* () {
|
|
570
|
-
const
|
|
892
|
+
const configStore = yield* ConfigStore;
|
|
893
|
+
const d = yield* Display;
|
|
894
|
+
const cfg = yield* resolveEmbedderConfig(configStore);
|
|
571
895
|
const getExtractor = yield* Effect.cached(createExtractorWithFallback(cfg));
|
|
572
896
|
const embed = (text) => Effect.gen(function* () {
|
|
573
897
|
const extractor = yield* getExtractor;
|
|
@@ -582,7 +906,7 @@ const make$2 = Effect.gen(function* () {
|
|
|
582
906
|
vector: normalize(data),
|
|
583
907
|
dims: cfg.dims
|
|
584
908
|
};
|
|
585
|
-
});
|
|
909
|
+
}).pipe(Effect.provideService(Display, d));
|
|
586
910
|
const batch = (texts) => Effect.gen(function* () {
|
|
587
911
|
const extractor = yield* getExtractor;
|
|
588
912
|
const results = [];
|
|
@@ -606,7 +930,7 @@ const make$2 = Effect.gen(function* () {
|
|
|
606
930
|
vector,
|
|
607
931
|
dims: cfg.dims
|
|
608
932
|
}));
|
|
609
|
-
});
|
|
933
|
+
}).pipe(Effect.provideService(Display, d));
|
|
610
934
|
return {
|
|
611
935
|
embed,
|
|
612
936
|
batch
|
|
@@ -615,14 +939,6 @@ const make$2 = Effect.gen(function* () {
|
|
|
615
939
|
const OnnxEmbedderLive = Layer.provideMerge(Layer.effect(Embedder, make$2), ConfigStoreLive);
|
|
616
940
|
//#endregion
|
|
617
941
|
//#region src/services/scanner.ts
|
|
618
|
-
const ALWAYS_IGNORE = new Set([
|
|
619
|
-
".pix",
|
|
620
|
-
"node_modules",
|
|
621
|
-
".git",
|
|
622
|
-
"dist",
|
|
623
|
-
"build",
|
|
624
|
-
".next"
|
|
625
|
-
]);
|
|
626
942
|
const make$1 = Effect.gen(function* () {
|
|
627
943
|
const fs = yield* FileSystem.FileSystem;
|
|
628
944
|
const readFileWithSkip = (path, mkReason) => fs.readFileString(path).pipe(Effect.map((content) => ({
|
|
@@ -655,47 +971,84 @@ const make$1 = Effect.gen(function* () {
|
|
|
655
971
|
reason: `Could not stat: ${String(error)}`
|
|
656
972
|
}
|
|
657
973
|
})));
|
|
658
|
-
const
|
|
974
|
+
const computeRelative = (fullPath, cwd) => fullPath.startsWith(cwd) ? fullPath.slice(cwd.length + 1) : fullPath;
|
|
975
|
+
const loadIgnoreFile = (filePath, ig, skipped) => Effect.gen(function* () {
|
|
976
|
+
const result = yield* readFileWithSkip(filePath, (error) => `Could not read ignore file: ${String(error)}`);
|
|
977
|
+
if (result.skipped) skipped.push(result.skipped);
|
|
978
|
+
if (result.content.trim()) ig.add(result.content.split("\n"));
|
|
979
|
+
});
|
|
980
|
+
const loadGitignoreRules = (ignoredPaths, cwd) => Effect.gen(function* () {
|
|
659
981
|
const ig = ignore();
|
|
660
|
-
const cwd = process.cwd();
|
|
661
982
|
const skipped = [];
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (
|
|
983
|
+
if (ignoredPaths.length > 0) ig.add(ignoredPaths);
|
|
984
|
+
const gitignorePath = `${cwd}/.gitignore`;
|
|
985
|
+
if (yield* fs.exists(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) yield* loadIgnoreFile(gitignorePath, ig, skipped);
|
|
665
986
|
const excludePath = `${cwd}/.git/info/exclude`;
|
|
666
|
-
if (yield* fs.exists(excludePath))
|
|
667
|
-
const excludeContent = yield* readFileWithSkip(excludePath, (error) => `Could not read exclude file: ${String(error)}`);
|
|
668
|
-
if (excludeContent.skipped) skipped.push(excludeContent.skipped);
|
|
669
|
-
if (excludeContent.content.trim()) ig.add(excludeContent.content.split("\n"));
|
|
670
|
-
}
|
|
987
|
+
if (yield* fs.exists(excludePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) yield* loadIgnoreFile(excludePath, ig, skipped);
|
|
671
988
|
return {
|
|
672
989
|
ig,
|
|
673
990
|
skipped
|
|
674
991
|
};
|
|
675
992
|
});
|
|
676
|
-
const
|
|
993
|
+
const processEntry = (entry, dir, ig, cwd) => Effect.gen(function* () {
|
|
994
|
+
const fullPath = `${dir}/${entry}`;
|
|
995
|
+
const statResult = yield* statWithSkip(fullPath);
|
|
996
|
+
if (statResult.skipped) return {
|
|
997
|
+
files: [],
|
|
998
|
+
skipped: [statResult.skipped]
|
|
999
|
+
};
|
|
1000
|
+
if (!statResult.info) return {
|
|
1001
|
+
files: [],
|
|
1002
|
+
skipped: []
|
|
1003
|
+
};
|
|
1004
|
+
const info = statResult.info;
|
|
1005
|
+
if (info.type === "Directory") {
|
|
1006
|
+
const relativeDir = computeRelative(fullPath, cwd);
|
|
1007
|
+
if (ig.ignores(relativeDir)) return {
|
|
1008
|
+
files: [],
|
|
1009
|
+
skipped: [{
|
|
1010
|
+
path: fullPath,
|
|
1011
|
+
reason: `Ignored by config pattern: ${relativeDir}`
|
|
1012
|
+
}]
|
|
1013
|
+
};
|
|
1014
|
+
return {
|
|
1015
|
+
files: [],
|
|
1016
|
+
skipped: [],
|
|
1017
|
+
recurse: true
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
if (info.type === "File") {
|
|
1021
|
+
const relativePath = computeRelative(fullPath, cwd);
|
|
1022
|
+
if (ig.ignores(relativePath)) return {
|
|
1023
|
+
files: [],
|
|
1024
|
+
skipped: [{
|
|
1025
|
+
path: fullPath,
|
|
1026
|
+
reason: `Ignored by config pattern: ${relativePath}`
|
|
1027
|
+
}]
|
|
1028
|
+
};
|
|
1029
|
+
return {
|
|
1030
|
+
files: [fullPath],
|
|
1031
|
+
skipped: []
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
files: [],
|
|
1036
|
+
skipped: []
|
|
1037
|
+
};
|
|
1038
|
+
});
|
|
1039
|
+
const walk = (dir, ig, cwd) => Effect.gen(function* () {
|
|
677
1040
|
const result = yield* readDirectoryWithSkip(dir);
|
|
678
1041
|
let files = [];
|
|
679
1042
|
const skipped = [];
|
|
680
1043
|
if (result.skipped) skipped.push(result.skipped);
|
|
681
1044
|
for (const entry of result.entries) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
if (!info.info) continue;
|
|
690
|
-
if (info.info.type === "Directory") {
|
|
691
|
-
const sub = yield* walk(fullPath, extensions);
|
|
1045
|
+
const entryResult = yield* processEntry(entry, dir, ig, cwd);
|
|
1046
|
+
files.push(...entryResult.files);
|
|
1047
|
+
skipped.push(...entryResult.skipped);
|
|
1048
|
+
if ("recurse" in entryResult) {
|
|
1049
|
+
const sub = yield* walk(`${dir}/${entry}`, ig, cwd);
|
|
692
1050
|
files.push(...sub.files);
|
|
693
1051
|
skipped.push(...sub.skipped);
|
|
694
|
-
} else if (info.info.type === "File") {
|
|
695
|
-
const dotIndex = entry.lastIndexOf(".");
|
|
696
|
-
if (dotIndex === -1) continue;
|
|
697
|
-
const ext = entry.slice(dotIndex);
|
|
698
|
-
if (extensions.has(ext)) files.push(fullPath);
|
|
699
1052
|
}
|
|
700
1053
|
}
|
|
701
1054
|
return {
|
|
@@ -703,16 +1056,15 @@ const make$1 = Effect.gen(function* () {
|
|
|
703
1056
|
skipped
|
|
704
1057
|
};
|
|
705
1058
|
});
|
|
706
|
-
const scanFiles = (
|
|
707
|
-
const
|
|
1059
|
+
const scanFiles = (ignoredPaths) => Effect.gen(function* () {
|
|
1060
|
+
const cwd = process.cwd();
|
|
1061
|
+
const { ig, skipped: ignoreSkipped } = yield* loadGitignoreRules(ignoredPaths, cwd).pipe(Effect.mapError((cause) => new ScanFailed({
|
|
708
1062
|
message: `Failed to load gitignore rules: ${String(cause)}`,
|
|
709
1063
|
cause
|
|
710
1064
|
})));
|
|
711
|
-
const
|
|
712
|
-
const { files: paths, skipped: walkSkipped } = yield* walk(cwd, new Set(extensions));
|
|
713
|
-
const relativePaths = paths.map((p) => p.startsWith(cwd) ? p.slice(cwd.length + 1) : p);
|
|
1065
|
+
const { files, skipped: walkSkipped } = yield* walk(cwd, ig, cwd);
|
|
714
1066
|
return {
|
|
715
|
-
files
|
|
1067
|
+
files,
|
|
716
1068
|
skipped: [...ignoreSkipped, ...walkSkipped]
|
|
717
1069
|
};
|
|
718
1070
|
});
|
|
@@ -888,11 +1240,14 @@ const make = Effect.gen(function* () {
|
|
|
888
1240
|
const VectorStoreLive = Layer.effect(VectorStore, make);
|
|
889
1241
|
//#endregion
|
|
890
1242
|
//#region src/index.ts
|
|
891
|
-
const ServicesLayer = Layer.mergeAll(ConfigStoreLive, ScannerLive, OnnxEmbedderLive, VectorStoreLive);
|
|
1243
|
+
const ServicesLayer = Layer.mergeAll(ConfigStoreLive, ScannerLive, OnnxEmbedderLive, VectorStoreLive, ContentExtractorLive);
|
|
892
1244
|
const ChunkerLayer = ChunkerLive.pipe(Layer.provide(ServicesLayer));
|
|
893
1245
|
const InfraLayer = Layer.mergeAll(ServicesLayer, ChunkerLayer).pipe(Layer.provide(NodeContext.layer));
|
|
894
1246
|
const UseCaseLayer = Layer.mergeAll(InitProject.Default, GetStatus.Default, QueryProject.Default, IndexProject.Default, ResetIndex.Default);
|
|
895
1247
|
const AppLayer = Layer.merge(UseCaseLayer.pipe(Layer.provide(InfraLayer)), NodeContext.layer);
|
|
896
|
-
|
|
1248
|
+
const { effect, displayLayer } = cli(process.argv);
|
|
1249
|
+
const cliLayer = Layer.mergeAll(displayLayer, CliConfig.layer({ showTypes: false }));
|
|
1250
|
+
setupTerminalCleanup();
|
|
1251
|
+
effect.pipe(Effect.provide(AppLayer.pipe(Layer.provideMerge(cliLayer))), NodeRuntime.runMain({ disableErrorReporting: true }));
|
|
897
1252
|
//#endregion
|
|
898
1253
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucas-bur/pix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Lightweight local semantic project indexer",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"pix": ""
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
+
"@clack/prompts": "^1.4.0",
|
|
55
56
|
"@effect/cli": "^0.75.1",
|
|
56
57
|
"@effect/platform": "^0.96.1",
|
|
57
58
|
"@effect/platform-node": "^0.106.0",
|