@lucas-bur/pix 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +233 -93
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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";
|
|
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";
|
|
6
8
|
import crypto from "node:crypto";
|
|
7
9
|
import { FileSystem } from "@effect/platform";
|
|
8
10
|
import { env } from "@huggingface/transformers";
|
|
@@ -32,6 +34,104 @@ var GetStatus = class extends Effect.Service()("GetStatus", {
|
|
|
32
34
|
return { getStatus };
|
|
33
35
|
})
|
|
34
36
|
}) {};
|
|
37
|
+
Data.taggedEnum();
|
|
38
|
+
/** Display context tag — commands use `yield* Display` to produce output */
|
|
39
|
+
var Display = class extends Context.Tag("Display")() {};
|
|
40
|
+
/** Maps severity to the corresponding @clack/prompts log function */
|
|
41
|
+
const severityToClack = {
|
|
42
|
+
info: clack.log.info,
|
|
43
|
+
success: clack.log.success,
|
|
44
|
+
warn: clack.log.warning,
|
|
45
|
+
error: clack.log.error
|
|
46
|
+
};
|
|
47
|
+
/** Styling helpers using node:util styleText (zero-deps, Node 21+) */
|
|
48
|
+
const terminalStyle = {
|
|
49
|
+
status: (message) => styleText("bold", message),
|
|
50
|
+
dim: (message) => styleText("dim", message)
|
|
51
|
+
};
|
|
52
|
+
/** Extract the message text from an UpdateInteractivePayload */
|
|
53
|
+
const payloadText = (p) => typeof p === "string" ? p : p.message;
|
|
54
|
+
/**
|
|
55
|
+
* Compute the delta for a progress bar from the payload + current state. Returns 0 if there is no
|
|
56
|
+
* numeric payload or if the active element is a spinner.
|
|
57
|
+
*/
|
|
58
|
+
const computeDelta = (p, state) => {
|
|
59
|
+
if (typeof p === "string") return 0;
|
|
60
|
+
if ("advanceBy" in p && p.advanceBy !== void 0) return Math.max(-state.value, p.advanceBy);
|
|
61
|
+
if ("setTo" in p && p.setTo !== void 0) return Math.max(0, Math.min(state.max, p.setTo)) - state.value;
|
|
62
|
+
if ("setToPercent" in p && p.setToPercent !== void 0) {
|
|
63
|
+
const target = Math.floor(state.max * p.setToPercent / 100);
|
|
64
|
+
return Math.max(-state.value, Math.min(state.max - state.value, target - state.value));
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Extracts the "guarded interactive" pattern: skip if already active, otherwise
|
|
70
|
+
* acquire-use-release.
|
|
71
|
+
*/
|
|
72
|
+
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))))));
|
|
73
|
+
/** Display implementation using @clack/prompts for interactive terminal output */
|
|
74
|
+
const ClackDisplay = { layer: Layer.effect(Display, Effect.gen(function* () {
|
|
75
|
+
const activeRef = yield* Ref.make(null);
|
|
76
|
+
return {
|
|
77
|
+
intro: (title) => Effect.sync(() => clack.intro(styleText("inverse", ` ${title} `))),
|
|
78
|
+
outro: (message) => Effect.sync(() => clack.outro(message)),
|
|
79
|
+
log: (message, severity) => Effect.sync(() => severityToClack[severity](terminalStyle.status(message))),
|
|
80
|
+
note: (content, title) => Effect.sync(() => clack.note(content, title)),
|
|
81
|
+
text: (message) => Effect.sync(() => clack.log.message(message)),
|
|
82
|
+
spinner: (message, effect) => withInteractive(activeRef, Effect.sync(() => {
|
|
83
|
+
const s = clack.spinner();
|
|
84
|
+
s.start(message);
|
|
85
|
+
return s;
|
|
86
|
+
}), (s) => ({
|
|
87
|
+
type: "spinner",
|
|
88
|
+
handle: s
|
|
89
|
+
}), (s, exit) => Effect.sync(() => s.stop(exit._tag === "Success" ? message : `${message} (failed)`)), effect),
|
|
90
|
+
progress: (opts, effect) => withInteractive(activeRef, Effect.sync(() => {
|
|
91
|
+
const bar = clack.progress({
|
|
92
|
+
max: opts.max,
|
|
93
|
+
style: opts.style ?? "heavy",
|
|
94
|
+
size: opts.size ?? 40,
|
|
95
|
+
indicator: opts.indicator ?? "dots"
|
|
96
|
+
});
|
|
97
|
+
bar.start(opts.message);
|
|
98
|
+
return bar;
|
|
99
|
+
}), (bar) => ({
|
|
100
|
+
type: "progress",
|
|
101
|
+
handle: bar,
|
|
102
|
+
value: 0,
|
|
103
|
+
max: opts.max
|
|
104
|
+
}), (bar, exit) => Effect.sync(() => exit._tag === "Success" ? bar.stop(opts.message) : bar.error(opts.message)), effect),
|
|
105
|
+
updateInteractive: (payload) => Ref.get(activeRef).pipe(Effect.flatMap((active) => {
|
|
106
|
+
if (!active) return Effect.void;
|
|
107
|
+
if (active.type === "spinner") return Effect.sync(() => active.handle.message(payloadText(payload)));
|
|
108
|
+
const delta = computeDelta(payload, {
|
|
109
|
+
value: active.value,
|
|
110
|
+
max: active.max
|
|
111
|
+
});
|
|
112
|
+
const newValue = Math.max(0, Math.min(active.max, active.value + delta));
|
|
113
|
+
return Effect.sync(() => {
|
|
114
|
+
active.handle.advance(delta, payloadText(payload));
|
|
115
|
+
}).pipe(Effect.andThen(Ref.update(activeRef, (current) => current && current.type === "progress" ? {
|
|
116
|
+
...current,
|
|
117
|
+
value: newValue
|
|
118
|
+
} : current)));
|
|
119
|
+
})),
|
|
120
|
+
json: () => Effect.void
|
|
121
|
+
};
|
|
122
|
+
})) };
|
|
123
|
+
/** Display implementation for --json mode — no-ops interactive methods, writes JSON to stdout */
|
|
124
|
+
const JsonDisplay = { layer: Layer.succeed(Display, {
|
|
125
|
+
intro: () => Effect.void,
|
|
126
|
+
outro: () => Effect.void,
|
|
127
|
+
log: () => Effect.void,
|
|
128
|
+
note: () => Effect.void,
|
|
129
|
+
text: () => Effect.void,
|
|
130
|
+
spinner: (_message, effect) => effect,
|
|
131
|
+
progress: (_opts, effect) => effect,
|
|
132
|
+
updateInteractive: () => Effect.void,
|
|
133
|
+
json: (data) => Effect.sync(() => process.stdout.write(`${JSON.stringify(data)}\n`))
|
|
134
|
+
}) };
|
|
35
135
|
//#endregion
|
|
36
136
|
//#region src/domain/config.ts
|
|
37
137
|
var ConfigError = class extends Data.TaggedError("ConfigError") {};
|
|
@@ -51,7 +151,7 @@ const DEFAULT_CONFIG = {
|
|
|
51
151
|
//#region src/application/index-project.ts
|
|
52
152
|
/**
|
|
53
153
|
* Use case: index project files. Pipeline: scan → chunk → embed → store. Depends on ConfigStore,
|
|
54
|
-
* Scanner, Chunker, Embedder, VectorStore via Effect tags.
|
|
154
|
+
* Scanner, Chunker, Embedder, VectorStore, Display via Effect tags.
|
|
55
155
|
*/
|
|
56
156
|
var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
57
157
|
accessors: true,
|
|
@@ -61,6 +161,7 @@ var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
|
61
161
|
const chunker = yield* Chunker;
|
|
62
162
|
const embedder = yield* Embedder;
|
|
63
163
|
const vectorStore = yield* VectorStore;
|
|
164
|
+
const d = yield* Display;
|
|
64
165
|
const index = () => Effect.gen(function* () {
|
|
65
166
|
if (!(yield* configStore.configExists())) yield* configStore.writeConfig(DEFAULT_CONFIG);
|
|
66
167
|
const config = yield* configStore.readConfig();
|
|
@@ -70,23 +171,23 @@ var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
|
70
171
|
".js",
|
|
71
172
|
".jsx"
|
|
72
173
|
];
|
|
174
|
+
yield* d.updateInteractive("Scanning source files...");
|
|
73
175
|
const scanResult = yield* scanner.scanFiles(extensions);
|
|
176
|
+
yield* d.updateInteractive(`Chunking ${scanResult.files.length} files...`);
|
|
74
177
|
const allChunks = (yield* Effect.forEach(scanResult.files, (file) => chunker.chunkFile(file), { concurrency: Math.max(1, config.chunkConcurrency ?? 8) })).flat();
|
|
75
178
|
const totalChunks = allChunks.length;
|
|
76
179
|
const totalFiles = new Set(allChunks.map((c) => c.file)).size;
|
|
77
180
|
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
|
-
}
|
|
181
|
+
if (totalChunks === 0) return {
|
|
182
|
+
success: true,
|
|
183
|
+
status: {
|
|
184
|
+
chunks: 0,
|
|
185
|
+
files: 0,
|
|
186
|
+
totalLines: 0,
|
|
187
|
+
byteSize: 0
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
yield* d.updateInteractive(`Embedding ${totalChunks} chunks...`);
|
|
90
191
|
const texts = allChunks.map((c) => c.text);
|
|
91
192
|
const embeddings = yield* embedder.batch(texts);
|
|
92
193
|
yield* vectorStore.store(allChunks, embeddings);
|
|
@@ -183,41 +284,41 @@ const formatError = (error) => JSON.stringify({
|
|
|
183
284
|
message: messageFromError(error),
|
|
184
285
|
cause: causeFromError(error)
|
|
185
286
|
});
|
|
186
|
-
/** Log the error
|
|
187
|
-
const reportError = (error) =>
|
|
287
|
+
/** Log the error to Display in human + agent format, then re-fail to preserve non-zero exit code. */
|
|
288
|
+
const reportError = (error) => Effect.gen(function* () {
|
|
289
|
+
const d = yield* Display;
|
|
290
|
+
yield* d.log(`${codeFromError(error)}: ${messageFromError(error)}`, "error");
|
|
291
|
+
yield* d.json(JSON.parse(formatError(error)));
|
|
292
|
+
return yield* Effect.fail(error);
|
|
293
|
+
});
|
|
188
294
|
//#endregion
|
|
189
295
|
//#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
296
|
/** CLI command: pix index [--force] [--verbose] [--json] */
|
|
197
297
|
const indexCommand = Command.make("index", {
|
|
198
298
|
force: Options.boolean("force").pipe(Options.withDefault(false)),
|
|
199
299
|
verbose: Options.boolean("verbose").pipe(Options.withDefault(false)),
|
|
200
300
|
json: Options.boolean("json").pipe(Options.withDefault(false))
|
|
201
|
-
}, ({ force, verbose
|
|
202
|
-
yield*
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
301
|
+
}, ({ force, verbose }) => Effect.gen(function* () {
|
|
302
|
+
const d = yield* Display;
|
|
303
|
+
if (force) yield* d.log("--force is currently not implemented and only a placeholder.", "warn");
|
|
304
|
+
if (verbose) yield* d.log("--verbose is currently not implemented and only a placeholder.", "warn");
|
|
305
|
+
const result = yield* d.spinner("Indexing project...", IndexProject.index());
|
|
306
|
+
yield* d.json({
|
|
207
307
|
chunks: result.status.chunks,
|
|
208
|
-
files: result.status.files
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
yield*
|
|
308
|
+
files: result.status.files
|
|
309
|
+
});
|
|
310
|
+
if (result.status.chunks === 0) yield* d.log("No chunks to index.", "warn");
|
|
311
|
+
else yield* d.log(`Indexed ${result.status.chunks} chunks from ${result.status.files} files.`, "success");
|
|
212
312
|
}).pipe(Effect.catchAll(reportError)));
|
|
213
313
|
//#endregion
|
|
214
314
|
//#region src/commands/init.ts
|
|
215
315
|
/** 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*
|
|
316
|
+
const initCommand = Command.make("init", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, () => Effect.gen(function* () {
|
|
317
|
+
const d = yield* Display;
|
|
318
|
+
const result = yield* d.spinner("Initializing...", InitProject.init());
|
|
319
|
+
yield* d.json(result);
|
|
320
|
+
yield* d.log("Created .pix/config.json with default settings.", "success");
|
|
321
|
+
yield* d.note("Add `.pix` to your `.gitignore` file to avoid committing the index.", "Reminder");
|
|
221
322
|
}).pipe(Effect.catchTags({
|
|
222
323
|
ConfigError: reportError,
|
|
223
324
|
DiskFullError: reportError
|
|
@@ -258,30 +359,22 @@ const toJsonOutput = (results, ctxLines) => results.map((r) => ({
|
|
|
258
359
|
...ctxLines > 0 && r.contextBefore && { contextBefore: r.contextBefore },
|
|
259
360
|
...ctxLines > 0 && r.contextAfter && { contextAfter: r.contextAfter }
|
|
260
361
|
}));
|
|
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
362
|
/** CLI command: pix query "<text>" [--top N] [--json] [--context-lines N] */
|
|
272
363
|
const queryCommand = Command.make("query", {
|
|
273
364
|
queryText: Args.text({ name: "query" }),
|
|
274
365
|
top: Options.integer("top").pipe(Options.withDefault(DEFAULT_TOP_K), Options.optional),
|
|
275
366
|
json: Options.boolean("json").pipe(Options.withDefault(false)),
|
|
276
367
|
contextLines: Options.integer("context-lines").pipe(Options.withDefault(DEFAULT_CONTEXT_LINES), Options.optional)
|
|
277
|
-
}, ({ queryText, top,
|
|
368
|
+
}, ({ queryText, top, contextLines }) => Effect.gen(function* () {
|
|
369
|
+
const d = yield* Display;
|
|
278
370
|
const topK = Option.getOrElse(top, () => DEFAULT_TOP_K);
|
|
279
371
|
const ctxLines = Option.getOrElse(contextLines, () => DEFAULT_CONTEXT_LINES);
|
|
280
372
|
const clamped = clampTopK(topK);
|
|
281
|
-
if (clamped.clamped
|
|
282
|
-
const results = yield* QueryProject.queryProject(queryText, clamped.value);
|
|
283
|
-
|
|
284
|
-
yield*
|
|
373
|
+
if (clamped.clamped) yield* d.log(`topK clamped from ${topK} to ${clamped.value}`, "warn");
|
|
374
|
+
const results = yield* d.spinner("Searching...", QueryProject.queryProject(queryText, clamped.value));
|
|
375
|
+
yield* d.json(toJsonOutput(results, ctxLines));
|
|
376
|
+
if (results.length === 0) yield* d.log("No results found", "warn");
|
|
377
|
+
else for (const result of results) yield* d.text(formatResult(result));
|
|
285
378
|
}).pipe(Effect.catchTags({
|
|
286
379
|
ModelLoadError: reportError,
|
|
287
380
|
InferenceError: reportError,
|
|
@@ -305,30 +398,26 @@ const formatBytes = (bytes) => {
|
|
|
305
398
|
};
|
|
306
399
|
//#endregion
|
|
307
400
|
//#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
401
|
/** CLI command: pix reset [--json] */
|
|
326
|
-
const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, (
|
|
402
|
+
const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, () => Effect.gen(function* () {
|
|
403
|
+
const d = yield* Display;
|
|
327
404
|
const start = yield* Clock.currentTimeMillis;
|
|
328
|
-
const result = yield* ResetIndex.reset();
|
|
405
|
+
const result = yield* d.spinner("Resetting index...", ResetIndex.reset());
|
|
329
406
|
const elapsedMs = (yield* Clock.currentTimeMillis) - start;
|
|
330
|
-
|
|
331
|
-
|
|
407
|
+
yield* d.json({
|
|
408
|
+
status: "ok",
|
|
409
|
+
deletedChunks: result.deletedChunks,
|
|
410
|
+
deletedVectors: result.deletedVectors,
|
|
411
|
+
freedBytes: result.freedBytes,
|
|
412
|
+
elapsedMs
|
|
413
|
+
});
|
|
414
|
+
const deletedParts = [result.deletedChunks ? "chunks.jsonl" : null, result.deletedVectors ? "vectors.bin" : null].filter((part) => part !== null);
|
|
415
|
+
if (deletedParts.length === 0) yield* d.log("Nothing to reset.", "info");
|
|
416
|
+
else {
|
|
417
|
+
yield* d.log(`Deleted: ${deletedParts.join(", ")}`, "success");
|
|
418
|
+
yield* d.log(`Freed: ${formatBytes(result.freedBytes)}`, "info");
|
|
419
|
+
yield* d.log(`Time: ${elapsedMs}ms`, "info");
|
|
420
|
+
}
|
|
332
421
|
}).pipe(Effect.catchTags({
|
|
333
422
|
DiskFullError: reportError,
|
|
334
423
|
StoreError: reportError
|
|
@@ -336,22 +425,22 @@ const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(
|
|
|
336
425
|
//#endregion
|
|
337
426
|
//#region src/commands/status.ts
|
|
338
427
|
/** CLI command: pix status [--json] */
|
|
339
|
-
const statusCommand = Command.make("status", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, (
|
|
428
|
+
const statusCommand = Command.make("status", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, () => Effect.gen(function* () {
|
|
429
|
+
const d = yield* Display;
|
|
340
430
|
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*
|
|
431
|
+
yield* d.json(result);
|
|
432
|
+
const lastIndexStr = result.lastIndex > 0 ? new Date(result.lastIndex).toLocaleString() : "never";
|
|
433
|
+
yield* d.log(`Indexed: ${result.chunks} chunks across ${result.files} files`, "info");
|
|
434
|
+
yield* d.log(`Model: ${result.model || "none"}`, "info");
|
|
435
|
+
yield* d.log(`Total lines: ${result.totalLines.toLocaleString()}`, "info");
|
|
436
|
+
yield* d.log(`Index size: ${result.byteSize.toLocaleString()} bytes`, "info");
|
|
437
|
+
yield* d.log(`Last indexed: ${lastIndexStr}`, "info");
|
|
348
438
|
}).pipe(Effect.catchTags({ StoreError: reportError })));
|
|
349
439
|
//#endregion
|
|
350
440
|
//#region src/cli.ts
|
|
351
441
|
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
352
442
|
const pix = Command.make("pix", {}, () => Effect.gen(function* () {
|
|
353
|
-
yield*
|
|
354
|
-
yield* Effect.logInfo("Use `pix --help` to see available commands.");
|
|
443
|
+
yield* (yield* Display).log(`pix v${VERSION} - Lightweight local semantic project indexer`, "info");
|
|
355
444
|
})).pipe(Command.withSubcommands([
|
|
356
445
|
initCommand,
|
|
357
446
|
statusCommand,
|
|
@@ -359,10 +448,50 @@ const pix = Command.make("pix", {}, () => Effect.gen(function* () {
|
|
|
359
448
|
queryCommand,
|
|
360
449
|
resetCommand
|
|
361
450
|
]));
|
|
362
|
-
const cli = (args) =>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
451
|
+
const cli = (args) => {
|
|
452
|
+
const displayLayer = args.some((a) => a === "--json") ? JsonDisplay.layer : ClackDisplay.layer;
|
|
453
|
+
return {
|
|
454
|
+
effect: Command.run(pix, {
|
|
455
|
+
name: "pix",
|
|
456
|
+
version: VERSION
|
|
457
|
+
})(args),
|
|
458
|
+
displayLayer
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/display/terminalCleanup.ts
|
|
463
|
+
/**
|
|
464
|
+
* Terminal cleanup for abrupt exits.
|
|
465
|
+
*
|
|
466
|
+
* @clack/prompts' spinner and taskLog call stdin.setRawMode(true) and hide
|
|
467
|
+
* the cursor via escape sequences. When the process is killed by a signal
|
|
468
|
+
* handler that calls process.exit() directly, clack's own cleanup is
|
|
469
|
+
* bypassed and the terminal is left in raw mode with the cursor hidden.
|
|
470
|
+
*
|
|
471
|
+
* Registering a process 'exit' listener that restores these guarantees the
|
|
472
|
+
* terminal is always left in a usable state.
|
|
473
|
+
*/
|
|
474
|
+
/** Escape sequence to show the cursor (DECTCEM). */
|
|
475
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
476
|
+
/**
|
|
477
|
+
* Creates a synchronous exit handler that restores terminal state. Extracted as a pure function so
|
|
478
|
+
* it can be unit-tested without side effects.
|
|
479
|
+
*/
|
|
480
|
+
const makeTerminalCleanupHandler = (stdin, stdout) => () => {
|
|
481
|
+
if (stdin.isTTY && stdin.setRawMode) try {
|
|
482
|
+
stdin.setRawMode(false);
|
|
483
|
+
} catch {}
|
|
484
|
+
if (stdout.isTTY) try {
|
|
485
|
+
stdout.write(SHOW_CURSOR);
|
|
486
|
+
} catch {}
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* Registers the terminal cleanup handler on process 'exit'. Call once at program startup
|
|
490
|
+
* (index.ts).
|
|
491
|
+
*/
|
|
492
|
+
const setupTerminalCleanup = () => {
|
|
493
|
+
process.on("exit", makeTerminalCleanupHandler(process.stdin, process.stdout));
|
|
494
|
+
};
|
|
366
495
|
//#endregion
|
|
367
496
|
//#region src/domain/errors.ts
|
|
368
497
|
/** Config file or directory does not exist. Run pix init first. */
|
|
@@ -559,7 +688,13 @@ const createExtractor = (opts) => Effect.tryPromise(async () => {
|
|
|
559
688
|
const createExtractorWithFallback = (opts) => {
|
|
560
689
|
if (opts.device === "cpu") return createExtractor(opts);
|
|
561
690
|
return createExtractor(opts).pipe(Effect.catchAll((originalError) => Effect.gen(function* () {
|
|
562
|
-
|
|
691
|
+
const d = yield* Display;
|
|
692
|
+
yield* d.log(`GPU (${opts.device}) failed, falling back to CPU...`, "warn");
|
|
693
|
+
yield* d.json({
|
|
694
|
+
event: "embedder_fallback",
|
|
695
|
+
originalDevice: opts.device,
|
|
696
|
+
reason: originalError.message
|
|
697
|
+
});
|
|
563
698
|
return yield* createExtractor({
|
|
564
699
|
...opts,
|
|
565
700
|
device: "cpu"
|
|
@@ -567,7 +702,9 @@ const createExtractorWithFallback = (opts) => {
|
|
|
567
702
|
})));
|
|
568
703
|
};
|
|
569
704
|
const make$2 = Effect.gen(function* () {
|
|
570
|
-
const
|
|
705
|
+
const configStore = yield* ConfigStore;
|
|
706
|
+
const d = yield* Display;
|
|
707
|
+
const cfg = yield* resolveEmbedderConfig(configStore);
|
|
571
708
|
const getExtractor = yield* Effect.cached(createExtractorWithFallback(cfg));
|
|
572
709
|
const embed = (text) => Effect.gen(function* () {
|
|
573
710
|
const extractor = yield* getExtractor;
|
|
@@ -582,7 +719,7 @@ const make$2 = Effect.gen(function* () {
|
|
|
582
719
|
vector: normalize(data),
|
|
583
720
|
dims: cfg.dims
|
|
584
721
|
};
|
|
585
|
-
});
|
|
722
|
+
}).pipe(Effect.provideService(Display, d));
|
|
586
723
|
const batch = (texts) => Effect.gen(function* () {
|
|
587
724
|
const extractor = yield* getExtractor;
|
|
588
725
|
const results = [];
|
|
@@ -606,7 +743,7 @@ const make$2 = Effect.gen(function* () {
|
|
|
606
743
|
vector,
|
|
607
744
|
dims: cfg.dims
|
|
608
745
|
}));
|
|
609
|
-
});
|
|
746
|
+
}).pipe(Effect.provideService(Display, d));
|
|
610
747
|
return {
|
|
611
748
|
embed,
|
|
612
749
|
batch
|
|
@@ -893,6 +1030,9 @@ const ChunkerLayer = ChunkerLive.pipe(Layer.provide(ServicesLayer));
|
|
|
893
1030
|
const InfraLayer = Layer.mergeAll(ServicesLayer, ChunkerLayer).pipe(Layer.provide(NodeContext.layer));
|
|
894
1031
|
const UseCaseLayer = Layer.mergeAll(InitProject.Default, GetStatus.Default, QueryProject.Default, IndexProject.Default, ResetIndex.Default);
|
|
895
1032
|
const AppLayer = Layer.merge(UseCaseLayer.pipe(Layer.provide(InfraLayer)), NodeContext.layer);
|
|
896
|
-
|
|
1033
|
+
const { effect, displayLayer } = cli(process.argv);
|
|
1034
|
+
const cliLayer = Layer.mergeAll(displayLayer, CliConfig.layer({ showTypes: false }));
|
|
1035
|
+
setupTerminalCleanup();
|
|
1036
|
+
effect.pipe(Effect.provide(AppLayer.pipe(Layer.provideMerge(cliLayer))), NodeRuntime.runMain({ disableErrorReporting: true }));
|
|
897
1037
|
//#endregion
|
|
898
1038
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucas-bur/pix",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|