@lucas-bur/pix 0.8.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 +249 -108
- 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,11 +34,124 @@ 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
|
+
}) };
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/domain/config.ts
|
|
137
|
+
var ConfigError = class extends Data.TaggedError("ConfigError") {};
|
|
138
|
+
const DEFAULT_CONFIG = {
|
|
139
|
+
schema: "1",
|
|
140
|
+
chunkLines: 60,
|
|
141
|
+
overlapLines: 10,
|
|
142
|
+
chunkConcurrency: 8,
|
|
143
|
+
files: {},
|
|
144
|
+
embedder: {
|
|
145
|
+
model: "Xenova/all-MiniLM-L6-v2",
|
|
146
|
+
device: "auto",
|
|
147
|
+
dtype: "fp32"
|
|
148
|
+
}
|
|
149
|
+
};
|
|
35
150
|
//#endregion
|
|
36
151
|
//#region src/application/index-project.ts
|
|
37
152
|
/**
|
|
38
153
|
* Use case: index project files. Pipeline: scan → chunk → embed → store. Depends on ConfigStore,
|
|
39
|
-
* Scanner, Chunker, Embedder, VectorStore via Effect tags.
|
|
154
|
+
* Scanner, Chunker, Embedder, VectorStore, Display via Effect tags.
|
|
40
155
|
*/
|
|
41
156
|
var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
42
157
|
accessors: true,
|
|
@@ -46,7 +161,9 @@ var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
|
46
161
|
const chunker = yield* Chunker;
|
|
47
162
|
const embedder = yield* Embedder;
|
|
48
163
|
const vectorStore = yield* VectorStore;
|
|
164
|
+
const d = yield* Display;
|
|
49
165
|
const index = () => Effect.gen(function* () {
|
|
166
|
+
if (!(yield* configStore.configExists())) yield* configStore.writeConfig(DEFAULT_CONFIG);
|
|
50
167
|
const config = yield* configStore.readConfig();
|
|
51
168
|
const extensions = Object.keys(config.files).length > 0 ? Object.keys(config.files) : [
|
|
52
169
|
".ts",
|
|
@@ -54,23 +171,23 @@ var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
|
54
171
|
".js",
|
|
55
172
|
".jsx"
|
|
56
173
|
];
|
|
174
|
+
yield* d.updateInteractive("Scanning source files...");
|
|
57
175
|
const scanResult = yield* scanner.scanFiles(extensions);
|
|
176
|
+
yield* d.updateInteractive(`Chunking ${scanResult.files.length} files...`);
|
|
58
177
|
const allChunks = (yield* Effect.forEach(scanResult.files, (file) => chunker.chunkFile(file), { concurrency: Math.max(1, config.chunkConcurrency ?? 8) })).flat();
|
|
59
178
|
const totalChunks = allChunks.length;
|
|
60
179
|
const totalFiles = new Set(allChunks.map((c) => c.file)).size;
|
|
61
180
|
const totalLines = allChunks.reduce((sum, c) => sum + (c.endLine - c.startLine + 1), 0);
|
|
62
|
-
if (totalChunks === 0) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
73
|
-
}
|
|
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...`);
|
|
74
191
|
const texts = allChunks.map((c) => c.text);
|
|
75
192
|
const embeddings = yield* embedder.batch(texts);
|
|
76
193
|
yield* vectorStore.store(allChunks, embeddings);
|
|
@@ -89,21 +206,6 @@ var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
|
89
206
|
})
|
|
90
207
|
}) {};
|
|
91
208
|
//#endregion
|
|
92
|
-
//#region src/domain/config.ts
|
|
93
|
-
var ConfigError = class extends Data.TaggedError("ConfigError") {};
|
|
94
|
-
const DEFAULT_CONFIG = {
|
|
95
|
-
schema: "1",
|
|
96
|
-
chunkLines: 60,
|
|
97
|
-
overlapLines: 10,
|
|
98
|
-
chunkConcurrency: 8,
|
|
99
|
-
files: {},
|
|
100
|
-
embedder: {
|
|
101
|
-
model: "Xenova/all-MiniLM-L6-v2",
|
|
102
|
-
device: "auto",
|
|
103
|
-
dtype: "fp32"
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
//#endregion
|
|
107
209
|
//#region src/application/init-project.ts
|
|
108
210
|
/**
|
|
109
211
|
* Use case: initialize a pix project by writing default config. Depends on ConfigStore via Effect
|
|
@@ -182,41 +284,41 @@ const formatError = (error) => JSON.stringify({
|
|
|
182
284
|
message: messageFromError(error),
|
|
183
285
|
cause: causeFromError(error)
|
|
184
286
|
});
|
|
185
|
-
/** Log the error
|
|
186
|
-
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
|
+
});
|
|
187
294
|
//#endregion
|
|
188
295
|
//#region src/commands/index-cmd.ts
|
|
189
|
-
const logFlagWarnings = (force, verbose, json) => {
|
|
190
|
-
if (json) return Effect.void;
|
|
191
|
-
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);
|
|
192
|
-
return Effect.forEach(warnings, (msg) => Effect.logInfo(msg), { discard: true });
|
|
193
|
-
};
|
|
194
|
-
const logHumanOutput = (chunks, files, duration) => Effect.logInfo(`Indexed ${chunks} chunks from ${files} files in ${duration}.`);
|
|
195
296
|
/** CLI command: pix index [--force] [--verbose] [--json] */
|
|
196
297
|
const indexCommand = Command.make("index", {
|
|
197
298
|
force: Options.boolean("force").pipe(Options.withDefault(false)),
|
|
198
299
|
verbose: Options.boolean("verbose").pipe(Options.withDefault(false)),
|
|
199
300
|
json: Options.boolean("json").pipe(Options.withDefault(false))
|
|
200
|
-
}, ({ force, verbose
|
|
201
|
-
yield*
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
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({
|
|
206
307
|
chunks: result.status.chunks,
|
|
207
|
-
files: result.status.files
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
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");
|
|
211
312
|
}).pipe(Effect.catchAll(reportError)));
|
|
212
313
|
//#endregion
|
|
213
314
|
//#region src/commands/init.ts
|
|
214
315
|
/** CLI command: pix init [--json] */
|
|
215
|
-
const initCommand = Command.make("init", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, (
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
yield*
|
|
219
|
-
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");
|
|
220
322
|
}).pipe(Effect.catchTags({
|
|
221
323
|
ConfigError: reportError,
|
|
222
324
|
DiskFullError: reportError
|
|
@@ -257,30 +359,22 @@ const toJsonOutput = (results, ctxLines) => results.map((r) => ({
|
|
|
257
359
|
...ctxLines > 0 && r.contextBefore && { contextBefore: r.contextBefore },
|
|
258
360
|
...ctxLines > 0 && r.contextAfter && { contextAfter: r.contextAfter }
|
|
259
361
|
}));
|
|
260
|
-
const renderResults = (results) => Effect.gen(function* () {
|
|
261
|
-
if (results.length === 0) {
|
|
262
|
-
yield* Effect.logInfo("No results found");
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
for (const result of results) {
|
|
266
|
-
yield* Console.log(formatResult(result));
|
|
267
|
-
yield* Console.log("---");
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
362
|
/** CLI command: pix query "<text>" [--top N] [--json] [--context-lines N] */
|
|
271
363
|
const queryCommand = Command.make("query", {
|
|
272
364
|
queryText: Args.text({ name: "query" }),
|
|
273
365
|
top: Options.integer("top").pipe(Options.withDefault(DEFAULT_TOP_K), Options.optional),
|
|
274
366
|
json: Options.boolean("json").pipe(Options.withDefault(false)),
|
|
275
367
|
contextLines: Options.integer("context-lines").pipe(Options.withDefault(DEFAULT_CONTEXT_LINES), Options.optional)
|
|
276
|
-
}, ({ queryText, top,
|
|
368
|
+
}, ({ queryText, top, contextLines }) => Effect.gen(function* () {
|
|
369
|
+
const d = yield* Display;
|
|
277
370
|
const topK = Option.getOrElse(top, () => DEFAULT_TOP_K);
|
|
278
371
|
const ctxLines = Option.getOrElse(contextLines, () => DEFAULT_CONTEXT_LINES);
|
|
279
372
|
const clamped = clampTopK(topK);
|
|
280
|
-
if (clamped.clamped
|
|
281
|
-
const results = yield* QueryProject.queryProject(queryText, clamped.value);
|
|
282
|
-
|
|
283
|
-
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));
|
|
284
378
|
}).pipe(Effect.catchTags({
|
|
285
379
|
ModelLoadError: reportError,
|
|
286
380
|
InferenceError: reportError,
|
|
@@ -304,30 +398,26 @@ const formatBytes = (bytes) => {
|
|
|
304
398
|
};
|
|
305
399
|
//#endregion
|
|
306
400
|
//#region src/commands/reset.ts
|
|
307
|
-
const logJsonResult = (result, elapsedMs) => Console.log(JSON.stringify({
|
|
308
|
-
status: "ok",
|
|
309
|
-
deletedChunks: result.deletedChunks,
|
|
310
|
-
deletedVectors: result.deletedVectors,
|
|
311
|
-
freedBytes: result.freedBytes,
|
|
312
|
-
elapsedMs
|
|
313
|
-
}));
|
|
314
|
-
const logHumanResult = (result, elapsedMs) => Effect.gen(function* () {
|
|
315
|
-
const deletedParts = [result.deletedChunks ? "chunks.jsonl" : null, result.deletedVectors ? "vectors.bin" : null].filter((part) => part !== null);
|
|
316
|
-
if (deletedParts.length === 0) {
|
|
317
|
-
yield* Effect.logInfo("Nothing to reset.");
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
yield* Effect.logInfo(`Deleted: ${deletedParts.join(", ")}`);
|
|
321
|
-
yield* Effect.logInfo(`Freed: ${formatBytes(result.freedBytes)}`);
|
|
322
|
-
yield* Effect.logInfo(`Time: ${elapsedMs}ms`);
|
|
323
|
-
});
|
|
324
401
|
/** CLI command: pix reset [--json] */
|
|
325
|
-
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;
|
|
326
404
|
const start = yield* Clock.currentTimeMillis;
|
|
327
|
-
const result = yield* ResetIndex.reset();
|
|
405
|
+
const result = yield* d.spinner("Resetting index...", ResetIndex.reset());
|
|
328
406
|
const elapsedMs = (yield* Clock.currentTimeMillis) - start;
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|
|
331
421
|
}).pipe(Effect.catchTags({
|
|
332
422
|
DiskFullError: reportError,
|
|
333
423
|
StoreError: reportError
|
|
@@ -335,22 +425,22 @@ const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(
|
|
|
335
425
|
//#endregion
|
|
336
426
|
//#region src/commands/status.ts
|
|
337
427
|
/** CLI command: pix status [--json] */
|
|
338
|
-
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;
|
|
339
430
|
const result = yield* GetStatus.getStatus();
|
|
340
|
-
|
|
341
|
-
const lastIndexStr = result.lastIndex > 0 ? new Date(result.lastIndex).
|
|
342
|
-
yield*
|
|
343
|
-
yield*
|
|
344
|
-
yield*
|
|
345
|
-
yield*
|
|
346
|
-
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");
|
|
347
438
|
}).pipe(Effect.catchTags({ StoreError: reportError })));
|
|
348
439
|
//#endregion
|
|
349
440
|
//#region src/cli.ts
|
|
350
441
|
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
351
442
|
const pix = Command.make("pix", {}, () => Effect.gen(function* () {
|
|
352
|
-
yield*
|
|
353
|
-
yield* Effect.logInfo("Use `pix --help` to see available commands.");
|
|
443
|
+
yield* (yield* Display).log(`pix v${VERSION} - Lightweight local semantic project indexer`, "info");
|
|
354
444
|
})).pipe(Command.withSubcommands([
|
|
355
445
|
initCommand,
|
|
356
446
|
statusCommand,
|
|
@@ -358,10 +448,50 @@ const pix = Command.make("pix", {}, () => Effect.gen(function* () {
|
|
|
358
448
|
queryCommand,
|
|
359
449
|
resetCommand
|
|
360
450
|
]));
|
|
361
|
-
const cli = (args) =>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
};
|
|
365
495
|
//#endregion
|
|
366
496
|
//#region src/domain/errors.ts
|
|
367
497
|
/** Config file or directory does not exist. Run pix init first. */
|
|
@@ -558,7 +688,13 @@ const createExtractor = (opts) => Effect.tryPromise(async () => {
|
|
|
558
688
|
const createExtractorWithFallback = (opts) => {
|
|
559
689
|
if (opts.device === "cpu") return createExtractor(opts);
|
|
560
690
|
return createExtractor(opts).pipe(Effect.catchAll((originalError) => Effect.gen(function* () {
|
|
561
|
-
|
|
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
|
+
});
|
|
562
698
|
return yield* createExtractor({
|
|
563
699
|
...opts,
|
|
564
700
|
device: "cpu"
|
|
@@ -566,7 +702,9 @@ const createExtractorWithFallback = (opts) => {
|
|
|
566
702
|
})));
|
|
567
703
|
};
|
|
568
704
|
const make$2 = Effect.gen(function* () {
|
|
569
|
-
const
|
|
705
|
+
const configStore = yield* ConfigStore;
|
|
706
|
+
const d = yield* Display;
|
|
707
|
+
const cfg = yield* resolveEmbedderConfig(configStore);
|
|
570
708
|
const getExtractor = yield* Effect.cached(createExtractorWithFallback(cfg));
|
|
571
709
|
const embed = (text) => Effect.gen(function* () {
|
|
572
710
|
const extractor = yield* getExtractor;
|
|
@@ -581,7 +719,7 @@ const make$2 = Effect.gen(function* () {
|
|
|
581
719
|
vector: normalize(data),
|
|
582
720
|
dims: cfg.dims
|
|
583
721
|
};
|
|
584
|
-
});
|
|
722
|
+
}).pipe(Effect.provideService(Display, d));
|
|
585
723
|
const batch = (texts) => Effect.gen(function* () {
|
|
586
724
|
const extractor = yield* getExtractor;
|
|
587
725
|
const results = [];
|
|
@@ -605,7 +743,7 @@ const make$2 = Effect.gen(function* () {
|
|
|
605
743
|
vector,
|
|
606
744
|
dims: cfg.dims
|
|
607
745
|
}));
|
|
608
|
-
});
|
|
746
|
+
}).pipe(Effect.provideService(Display, d));
|
|
609
747
|
return {
|
|
610
748
|
embed,
|
|
611
749
|
batch
|
|
@@ -892,6 +1030,9 @@ const ChunkerLayer = ChunkerLive.pipe(Layer.provide(ServicesLayer));
|
|
|
892
1030
|
const InfraLayer = Layer.mergeAll(ServicesLayer, ChunkerLayer).pipe(Layer.provide(NodeContext.layer));
|
|
893
1031
|
const UseCaseLayer = Layer.mergeAll(InitProject.Default, GetStatus.Default, QueryProject.Default, IndexProject.Default, ResetIndex.Default);
|
|
894
1032
|
const AppLayer = Layer.merge(UseCaseLayer.pipe(Layer.provide(InfraLayer)), NodeContext.layer);
|
|
895
|
-
|
|
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 }));
|
|
896
1037
|
//#endregion
|
|
897
1038
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucas-bur/pix",
|
|
3
|
-
"version": "0.
|
|
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",
|