@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.
Files changed (2) hide show
  1. package/dist/index.mjs +525 -170
  2. 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 crypto from "node:crypto";
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
- files: {},
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 on ConfigStore,
54
- * Scanner, Chunker, Embedder, VectorStore via Effect tags.
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 extensions = Object.keys(config.files).length > 0 ? Object.keys(config.files) : [
68
- ".ts",
69
- ".tsx",
70
- ".js",
71
- ".jsx"
72
- ];
73
- const scanResult = yield* scanner.scanFiles(extensions);
74
- const allChunks = (yield* Effect.forEach(scanResult.files, (file) => chunker.chunkFile(file), { concurrency: Math.max(1, config.chunkConcurrency ?? 8) })).flat();
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
- yield* Effect.logInfo("No chunks to index.");
80
- return {
81
- success: true,
82
- status: {
83
- chunks: 0,
84
- files: 0,
85
- totalLines: 0,
86
- byteSize: 0
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 as JSON to stdout, then re-fail to preserve non-zero exit code. */
187
- const reportError = (error) => Console.log(formatError(error)).pipe(Effect.flatMap(() => Effect.fail(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, json }) => Effect.gen(function* () {
202
- yield* logFlagWarnings(force, verbose, json);
203
- const startTime = Date.now();
204
- const result = yield* IndexProject.index();
205
- const duration = `${((Date.now() - startTime) / 1e3).toFixed(1)}s`;
206
- if (json) return yield* Console.log(JSON.stringify({
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
- duration
210
- }));
211
- yield* logHumanOutput(result.status.chunks, result.status.files, duration);
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)) }, ({ json }) => Effect.gen(function* () {
217
- const result = yield* InitProject.init();
218
- if (json) return yield* Console.log(JSON.stringify(result, null, 2));
219
- yield* Effect.logInfo("Created .pix/config.json with default settings.");
220
- yield* Effect.logInfo("Reminder: Add `.pix` to your `.gitignore` file to avoid committing the index.");
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, json, contextLines }) => Effect.gen(function* () {
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 && !json) yield* Effect.logDebug(`topK clamped from ${topK} to ${clamped.value}`);
282
- const results = yield* QueryProject.queryProject(queryText, clamped.value);
283
- if (json) return yield* Console.log(JSON.stringify(toJsonOutput(results, ctxLines), null, 2));
284
- yield* renderResults(results);
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)) }, ({ json }) => Effect.gen(function* () {
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
- if (json) return yield* logJsonResult(result, elapsedMs);
331
- yield* logHumanResult(result, elapsedMs);
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)) }, ({ json }) => Effect.gen(function* () {
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
- if (json) return yield* Console.log(JSON.stringify(result, null, 2));
342
- const lastIndexStr = result.lastIndex > 0 ? new Date(result.lastIndex).toISOString() : "never";
343
- yield* Effect.logInfo(`Indexed: ${result.chunks} chunks across ${result.files} files`);
344
- yield* Effect.logInfo(`Model: ${result.model || "none"}`);
345
- yield* Effect.logInfo(`Total lines: ${result.totalLines.toLocaleString()}`);
346
- yield* Effect.logInfo(`Index size: ${formatBytes(result.byteSize)}`);
347
- yield* Effect.logInfo(`Last indexed: ${lastIndexStr}`);
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* Effect.logInfo("pix - Lightweight local semantic project indexer");
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) => Command.run(pix, {
363
- name: "pix",
364
- version: VERSION
365
- })(args).pipe(Effect.provide(CliConfig.layer({ showTypes: false })));
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/domain/errors.ts
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
- * Fatal scan failure gitignore loading failed entirely. Non-fatal per-entry skips are reported
386
- * via ScanResult.skipped.
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
- var ScanFailed = class extends Data.TaggedError("ScanFailed") {};
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$4 = Effect.gen(function* () {
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 { chunkFile };
721
+ return {
722
+ chunkFile,
723
+ chunkText
724
+ };
430
725
  });
431
- const ChunkerLive = Layer.effect(Chunker, make$4);
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$3 = Effect.gen(function* () {
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$3);
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
- yield* Effect.logWarning(`Embedding device "${opts.device}" failed, falling back to "cpu": ${originalError.message}`);
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 cfg = yield* resolveEmbedderConfig(yield* ConfigStore);
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 loadGitignoreRules = Effect.gen(function* () {
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
- const rootContent = yield* readFileWithSkip(`${cwd}/.gitignore`, (error) => `Could not read gitignore: ${String(error)}`);
663
- if (rootContent.skipped) skipped.push(rootContent.skipped);
664
- if (rootContent.content.trim()) ig.add(rootContent.content.split("\n"));
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 walk = (dir, extensions) => Effect.gen(function* () {
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
- if (ALWAYS_IGNORE.has(entry)) continue;
683
- const fullPath = `${dir}/${entry}`;
684
- const info = yield* statWithSkip(fullPath);
685
- if (info.skipped) {
686
- skipped.push(info.skipped);
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 = (extensions) => Effect.gen(function* () {
707
- const { ig, skipped: ignoreSkipped } = yield* loadGitignoreRules.pipe(Effect.mapError((cause) => new ScanFailed({
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 cwd = process.cwd();
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: ig.filter(relativePaths).map((p) => `${cwd}/${p}`),
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
- cli(process.argv).pipe(Effect.provide(AppLayer), NodeRuntime.runMain({ disableErrorReporting: true }));
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.9.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",