@lucas-bur/pix 0.3.0 → 0.5.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 (3) hide show
  1. package/README.md +36 -20
  2. package/dist/index.mjs +102 -86
  3. package/package.json +9 -4
package/README.md CHANGED
@@ -1,18 +1,11 @@
1
1
  # @lucas-bur/pix
2
2
 
3
- [![CI](https://github.com/lucas-bur/pix/actions/workflows/ci.yml/badge.svg)](https://github.com/lucas-bur/pix/actions/workflows/ci.yml)
4
- [![codecov](https://codecov.io/gh/lucas-bur/pix/branch/main/graph/badge.svg)](https://codecov.io/gh/lucas-bur/pix)
3
+ [![CI](https://github.com/Lucas-Bur/pix/actions/workflows/ci.yml/badge.svg)](https://github.com/Lucas-Bur/pix/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/Lucas-Bur/pix/branch/main/graph/badge.svg)](https://codecov.io/gh/Lucas-Bur/pix)
5
5
  [![npm version](https://img.shields.io/npm/v/@lucas-bur/pix)](https://www.npmjs.com/package/@lucas-bur/pix)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@lucas-bur/pix)](https://www.npmjs.com/package/@lucas-bur/pix)
7
- [![Code Quality](https://img.shields.io/badge/code%20quality-fallow-blue)](https://github.com/fallow-rs/fallow)
8
7
 
9
- Lightweight local semantic project indexer (short pix)
10
-
11
- Zero external services, 100% local + offline. Installs as a devDependency and provides agent-ready structured JSON output.
12
-
13
- ## Status
14
-
15
- MVP in development. See [CONTEXT.md](./CONTEXT.md) for architecture decisions and [.scratch/pix-mvp/PRD.md](./.scratch/pix-mvp/PRD.md) for the product requirements.
8
+ Lightweight local semantic project indexer. Zero external services, 100% local and offline. Installs as a devDependency and provides agent-ready structured JSON output.
16
9
 
17
10
  ## Quick Start
18
11
 
@@ -23,23 +16,46 @@ pix index
23
16
  pix query "authentication middleware"
24
17
  ```
25
18
 
26
- ## Quality Gates
19
+ ## Commands
20
+
21
+ | Command | Description | JSON flag |
22
+ | -------------------- | ------------------------------------------- | --------- |
23
+ | `pix init` | Create `.pix/config.json` with defaults | `--json` |
24
+ | `pix index` | Scan, chunk, embed, and store project files | `--json` |
25
+ | `pix query "<text>"` | Semantic search via cosine similarity | `--json` |
26
+ | `pix status` | Show index statistics | `--json` |
27
+ | `pix reset` | Delete index files (chunks + vectors) | `--json` |
28
+
29
+ All commands support `--json` for structured output on stdout — ideal for piping to AI agents.
30
+
31
+ ## Agent-Ready Output
27
32
 
28
- This project uses [fallow](https://github.com/fallow-rs/fallow) for static analysis (dead code, duplication, complexity).
33
+ ```bash
34
+ $ pix status --json
35
+ {"chunks":59,"files":37,"model":"Xenova/all-MiniLM-L6-v2","lastIndex":1715030400000,"totalLines":1260,"byteSize":16128}
36
+ ```
37
+
38
+ Errors use the same structured format:
39
+
40
+ ```json
41
+ { "error": true, "code": "CONFIG_MISSING", "message": "No .pix/config.json found" }
42
+ ```
29
43
 
30
- ### Commands
44
+ ## Architecture
31
45
 
32
- - `vp run lint:fallow` Run fallow with JSON output (used in CI)
33
- - `fallow audit --summary` — Check only changed files (used in pre-commit hook)
46
+ pix follows hexagonal architecture (ports and adapters) with three layers:
34
47
 
35
- ### Pre-commit Hook
48
+ - **Domain** (`src/domain/`) — Pure types, entities, port declarations
49
+ - **Application** (`src/application/`) — Use cases orchestrating business logic
50
+ - **Infrastructure** (`src/services/`) — Concrete adapters (ONNX, filesystem, ffmpeg scanning)
36
51
 
37
- The pre-commit hook is managed by vite-plus and runs:
52
+ See [CONTEXT.md](./CONTEXT.md) for architecture decisions and [docs/adr/](./docs/adr/) for decision records.
38
53
 
39
- 1. `vp staged` — Formats, lints, and type-checks staged files
40
- 2. `fallow audit --summary` — Audits changed files for quality issues
54
+ ## Quality
41
55
 
42
- To set up hooks after cloning: `vp config`
56
+ - `vp check` Format, lint, type-check
57
+ - `vp test` — Unit and integration tests
58
+ - `vp run lint:fallow` — Dead code, duplication, complexity analysis
43
59
 
44
60
  ## License
45
61
 
package/dist/index.mjs CHANGED
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
- import { Clock, Context, Data, Effect, Layer, Option } from "effect";
5
- import { Args, Command, Options } from "@effect/cli";
4
+ import { Clock, Console, Context, Data, Effect, Layer, Option } from "effect";
5
+ import { Args, CliConfig, Command, Options } from "@effect/cli";
6
6
  import crypto from "node:crypto";
7
7
  import { FileSystem } from "@effect/platform";
8
8
  import { env } from "@huggingface/transformers";
9
- import fg from "fast-glob";
10
9
  import ignore from "ignore";
11
10
  //#region src/domain/ports.ts
12
11
  var ConfigStore = class extends Context.Tag("ConfigStore")() {};
@@ -68,15 +67,13 @@ var IndexProject = class extends Effect.Service()("IndexProject", {
68
67
  const embeddings = yield* embedder.batch(texts);
69
68
  yield* vectorStore.store(allChunks, embeddings);
70
69
  const dims = embeddings[0]?.dims ?? 384;
71
- const byteSize = embeddings.length * dims * 4;
72
- yield* Effect.logInfo(`Indexed ${totalChunks} chunks from ${totalFiles} files.`);
73
70
  return {
74
71
  success: true,
75
72
  stats: {
76
73
  chunks: totalChunks,
77
74
  files: totalFiles,
78
75
  totalLines,
79
- byteSize
76
+ byteSize: embeddings.length * dims * 4
80
77
  }
81
78
  };
82
79
  });
@@ -135,6 +132,26 @@ var ResetIndex = class extends Effect.Service()("ResetIndex", {
135
132
  })
136
133
  }) {};
137
134
  //#endregion
135
+ //#region src/lib/error-format.ts
136
+ const errorCodes = {
137
+ ConfigError: "CONFIG_MISSING",
138
+ PlatformError: "PLATFORM_ERROR"
139
+ };
140
+ const messageFromError = (error) => {
141
+ if (typeof error === "string") return error;
142
+ if (error && typeof error === "object" && "message" in error) return String(error.message);
143
+ return "Unknown error";
144
+ };
145
+ const codeFromError = (error) => {
146
+ if (error && typeof error === "object" && "_tag" in error) return errorCodes[String(error._tag)] ?? "UNKNOWN";
147
+ return "UNKNOWN";
148
+ };
149
+ const formatError = (error) => JSON.stringify({
150
+ error: true,
151
+ code: codeFromError(error),
152
+ message: messageFromError(error)
153
+ });
154
+ //#endregion
138
155
  //#region src/commands/index-cmd.ts
139
156
  /** CLI command: pix index [--force] [--verbose] [--json] */
140
157
  const indexCommand = Command.make("index", {
@@ -142,39 +159,28 @@ const indexCommand = Command.make("index", {
142
159
  verbose: Options.boolean("verbose").pipe(Options.withDefault(false)),
143
160
  json: Options.boolean("json").pipe(Options.withDefault(false))
144
161
  }, ({ force, verbose, json }) => Effect.gen(function* () {
145
- if (force) yield* Effect.logInfo(`--force is currently not implemented and only a placeholder.`);
146
- if (verbose) yield* Effect.logInfo(`--verbose is currently not implemented and only a placeholder.`);
162
+ if (force && !json) yield* Effect.logInfo("--force is currently not implemented and only a placeholder.");
163
+ if (verbose && !json) yield* Effect.logInfo("--verbose is currently not implemented and only a placeholder.");
147
164
  const startTime = Date.now();
148
165
  const result = yield* IndexProject.index().pipe(Effect.either);
149
- if (result._tag === "Left") {
150
- const error = result.left;
151
- const message = error.message ?? String(error);
152
- yield* Effect.sync(() => {
153
- console.log(JSON.stringify({ error: message }));
154
- });
155
- return yield* Effect.fail(error);
156
- }
166
+ if (result._tag === "Left") return yield* Effect.fail(result.left);
157
167
  const duration = `${((Date.now() - startTime) / 1e3).toFixed(1)}s`;
158
- if (json) return yield* Effect.sync(() => {
159
- console.log(JSON.stringify({
160
- chunks: result.right.stats.chunks,
161
- files: result.right.stats.files,
162
- duration
163
- }));
164
- });
168
+ if (json) return yield* Console.log(JSON.stringify({
169
+ chunks: result.right.stats.chunks,
170
+ files: result.right.stats.files,
171
+ duration
172
+ }));
165
173
  yield* Effect.logInfo(`Indexed ${result.right.stats.chunks} chunks from ${result.right.stats.files} files in ${duration}.`);
166
- }));
174
+ }).pipe(Effect.tapError((error) => Console.log(formatError(error)))));
167
175
  //#endregion
168
176
  //#region src/commands/init.ts
169
177
  /** CLI command: pix init [--json] */
170
178
  const initCommand = Command.make("init", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, ({ json }) => Effect.gen(function* () {
171
179
  const result = yield* InitProject.init();
172
- if (json) return yield* Effect.sync(() => {
173
- console.log(JSON.stringify(result, null, 2));
174
- });
180
+ if (json) return yield* Console.log(JSON.stringify(result, null, 2));
175
181
  yield* Effect.logInfo("Created .pix/config.json with default settings.");
176
182
  yield* Effect.logInfo("Reminder: Add `.pix` to your `.gitignore` file to avoid committing the index.");
177
- }));
183
+ }).pipe(Effect.tapError((error) => Console.log(formatError(error)))));
178
184
  //#endregion
179
185
  //#region src/commands/query.ts
180
186
  const DEFAULT_TOP_K = 5;
@@ -214,7 +220,7 @@ const queryCommand = Command.make("query", {
214
220
  const clamped = clampTopK(topK);
215
221
  if (clamped.clamped) yield* Effect.logDebug(`topK clamped from ${topK} to ${clamped.value}`);
216
222
  const results = yield* QueryProject.queryProject(queryText, clamped.value);
217
- if (json) return yield* Effect.sync(() => {
223
+ if (json) {
218
224
  const output = results.map((r) => ({
219
225
  score: r.score,
220
226
  file: r.file,
@@ -224,17 +230,31 @@ const queryCommand = Command.make("query", {
224
230
  ...ctxLines > 0 && r.contextBefore && { contextBefore: r.contextBefore },
225
231
  ...ctxLines > 0 && r.contextAfter && { contextAfter: r.contextAfter }
226
232
  }));
227
- console.log(JSON.stringify(output, null, 2));
228
- });
233
+ return yield* Console.log(JSON.stringify(output, null, 2));
234
+ }
229
235
  if (results.length === 0) {
230
236
  yield* Effect.logInfo("No results found");
231
237
  return;
232
238
  }
233
- for (const result of results) yield* Effect.sync(() => {
234
- console.log(formatResult(result));
235
- console.log("---");
236
- });
237
- }));
239
+ for (const result of results) {
240
+ yield* Console.log(formatResult(result));
241
+ yield* Console.log("---");
242
+ }
243
+ }).pipe(Effect.tapError((error) => Console.log(formatError(error)))));
244
+ //#endregion
245
+ //#region src/lib/format.ts
246
+ /** Format byte count as human-readable string (e.g. "1.5 MB") */
247
+ const formatBytes = (bytes) => {
248
+ if (bytes === 0) return "0 B";
249
+ const units = [
250
+ "B",
251
+ "KB",
252
+ "MB",
253
+ "GB"
254
+ ];
255
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
256
+ return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
257
+ };
238
258
  //#endregion
239
259
  //#region src/commands/reset.ts
240
260
  /** CLI command: pix reset [--json] */
@@ -242,15 +262,13 @@ const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(
242
262
  const start = yield* Clock.currentTimeMillis;
243
263
  const result = yield* ResetIndex.reset();
244
264
  const elapsedMs = (yield* Clock.currentTimeMillis) - start;
245
- if (json) return yield* Effect.sync(() => {
246
- console.log(JSON.stringify({
247
- status: "ok",
248
- deletedChunks: result.deletedChunks,
249
- deletedVectors: result.deletedVectors,
250
- freedBytes: result.freedBytes,
251
- elapsedMs
252
- }));
253
- });
265
+ if (json) return yield* Console.log(JSON.stringify({
266
+ status: "ok",
267
+ deletedChunks: result.deletedChunks,
268
+ deletedVectors: result.deletedVectors,
269
+ freedBytes: result.freedBytes,
270
+ elapsedMs
271
+ }));
254
272
  if (!result.deletedChunks && !result.deletedVectors) {
255
273
  yield* Effect.logInfo("Nothing to reset.");
256
274
  return;
@@ -259,48 +277,22 @@ const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(
259
277
  if (result.deletedChunks) parts.push("chunks.jsonl");
260
278
  if (result.deletedVectors) parts.push("vectors.bin");
261
279
  yield* Effect.logInfo(`Deleted: ${parts.join(", ")}`);
262
- yield* Effect.logInfo(`Freed: ${formatBytes$1(result.freedBytes)}`);
280
+ yield* Effect.logInfo(`Freed: ${formatBytes(result.freedBytes)}`);
263
281
  yield* Effect.logInfo(`Time: ${elapsedMs}ms`);
264
- }));
265
- /** Format byte count as human-readable string (e.g. "1.5 MB") */
266
- const formatBytes$1 = (bytes) => {
267
- if (bytes === 0) return "0 B";
268
- const units = [
269
- "B",
270
- "KB",
271
- "MB",
272
- "GB"
273
- ];
274
- const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
275
- return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
276
- };
282
+ }).pipe(Effect.tapError((error) => Console.log(formatError(error)))));
277
283
  //#endregion
278
284
  //#region src/commands/status.ts
279
285
  /** CLI command: pix status [--json] */
280
286
  const statusCommand = Command.make("status", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, ({ json }) => Effect.gen(function* () {
281
287
  const result = yield* GetStatus.getStatus();
282
- if (json) return yield* Effect.sync(() => {
283
- console.log(JSON.stringify(result, null, 2));
284
- });
288
+ if (json) return yield* Console.log(JSON.stringify(result, null, 2));
285
289
  const lastIndexStr = result.lastIndex > 0 ? new Date(result.lastIndex).toISOString() : "never";
286
290
  yield* Effect.logInfo(`Indexed: ${result.chunks} chunks across ${result.files} files`);
287
291
  yield* Effect.logInfo(`Model: ${result.model || "none"}`);
288
292
  yield* Effect.logInfo(`Total lines: ${result.totalLines.toLocaleString()}`);
289
293
  yield* Effect.logInfo(`Index size: ${formatBytes(result.byteSize)}`);
290
294
  yield* Effect.logInfo(`Last indexed: ${lastIndexStr}`);
291
- }));
292
- /** Format byte count as human-readable string (e.g. "1.5 MB") */
293
- const formatBytes = (bytes) => {
294
- if (bytes === 0) return "0 B";
295
- const units = [
296
- "B",
297
- "KB",
298
- "MB",
299
- "GB"
300
- ];
301
- const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
302
- return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
303
- };
295
+ }).pipe(Effect.tapError((error) => Console.log(formatError(error)))));
304
296
  //#endregion
305
297
  //#region src/cli.ts
306
298
  const VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -314,10 +306,10 @@ const pix = Command.make("pix", {}, () => Effect.gen(function* () {
314
306
  queryCommand,
315
307
  resetCommand
316
308
  ]));
317
- const cli = Command.run(pix, {
309
+ const cli = (args) => Command.run(pix, {
318
310
  name: "pix",
319
311
  version: VERSION
320
- });
312
+ })(args).pipe(Effect.provide(CliConfig.layer({ showTypes: false })));
321
313
  //#endregion
322
314
  //#region src/services/chunker.ts
323
315
  const MIN_CHUNK_CHARS = 20;
@@ -451,9 +443,16 @@ const make$2 = Effect.gen(function* () {
451
443
  const OnnxEmbedderLive = Layer.effect(Embedder, make$2);
452
444
  //#endregion
453
445
  //#region src/services/scanner.ts
446
+ const ALWAYS_IGNORE = new Set([
447
+ ".pix",
448
+ "node_modules",
449
+ ".git",
450
+ "dist",
451
+ "build",
452
+ ".next"
453
+ ]);
454
454
  const make$1 = Effect.gen(function* () {
455
455
  const fs = yield* FileSystem.FileSystem;
456
- /** Loads all gitignore patterns from .gitignore files in the repo. */
457
456
  const loadGitignoreRules = Effect.gen(function* () {
458
457
  const ig = ignore();
459
458
  const cwd = process.cwd();
@@ -466,13 +465,30 @@ const make$1 = Effect.gen(function* () {
466
465
  }
467
466
  return ig;
468
467
  }).pipe(Effect.catchAll(() => Effect.succeed(ignore())));
468
+ const walk = (dir, extensions) => Effect.gen(function* () {
469
+ const entries = yield* fs.readDirectory(dir).pipe(Effect.catchAll(() => Effect.succeed([])));
470
+ let results = [];
471
+ for (const entry of entries) {
472
+ if (ALWAYS_IGNORE.has(entry)) continue;
473
+ const fullPath = `${dir}/${entry}`;
474
+ const info = yield* fs.stat(fullPath).pipe(Effect.catchAll(() => Effect.succeed(null)));
475
+ if (!info) continue;
476
+ if (info.type === "Directory") {
477
+ const subResults = yield* walk(fullPath, extensions);
478
+ results.push(...subResults);
479
+ } else if (info.type === "File") {
480
+ const dotIndex = entry.lastIndexOf(".");
481
+ if (dotIndex === -1) continue;
482
+ const ext = entry.slice(dotIndex);
483
+ if (extensions.has(ext)) results.push(fullPath);
484
+ }
485
+ }
486
+ return results;
487
+ });
469
488
  const scanFiles = (extensions) => Effect.gen(function* () {
470
489
  const ig = yield* loadGitignoreRules;
471
490
  const cwd = process.cwd();
472
- const pattern = extensions.map((ext) => `**/*${ext}`);
473
- const relativePaths = (yield* Effect.tryPromise(() => fg(pattern, { dot: false })).pipe(Effect.catchAll(() => Effect.succeed([])))).map((p) => {
474
- return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
475
- });
491
+ const relativePaths = (yield* walk(cwd, new Set(extensions))).map((p) => p.startsWith(cwd) ? p.slice(cwd.length + 1) : p);
476
492
  return ig.filter(relativePaths).map((p) => `${cwd}/${p}`);
477
493
  });
478
494
  return { scanFiles };
@@ -587,7 +603,7 @@ const make = Effect.gen(function* () {
587
603
  const model = readModelFromChunks(lines);
588
604
  const totalLines = countTotalLines(lines);
589
605
  const vectorsStat = yield* fs.stat(VECTORS_FILE).pipe(Effect.catchAll(() => Effect.succeed(null)));
590
- const byteSize = vectorsStat && "size" in vectorsStat ? vectorsStat.size : 0;
606
+ const byteSize = vectorsStat && "size" in vectorsStat ? Number(vectorsStat.size) : 0;
591
607
  return {
592
608
  chunks,
593
609
  files,
@@ -603,13 +619,13 @@ const make = Effect.gen(function* () {
603
619
  let freedBytes = 0;
604
620
  if (yield* fs.exists(CHUNKS_FILE)) {
605
621
  const stat = yield* fs.stat(CHUNKS_FILE);
606
- freedBytes += stat && "size" in stat ? stat.size : 0;
622
+ freedBytes += stat && "size" in stat ? Number(stat.size) : 0;
607
623
  yield* fs.remove(CHUNKS_FILE);
608
624
  deletedChunks = true;
609
625
  }
610
626
  if (yield* fs.exists(VECTORS_FILE)) {
611
627
  const stat = yield* fs.stat(VECTORS_FILE);
612
- freedBytes += stat && "size" in stat ? stat.size : 0;
628
+ freedBytes += stat && "size" in stat ? Number(stat.size) : 0;
613
629
  yield* fs.remove(VECTORS_FILE);
614
630
  deletedVectors = true;
615
631
  }
@@ -634,6 +650,6 @@ const ChunkerLayer = ChunkerLive.pipe(Layer.provide(ServicesLayer));
634
650
  const InfraLayer = Layer.mergeAll(ServicesLayer, ChunkerLayer).pipe(Layer.provide(NodeContext.layer));
635
651
  const UseCaseLayer = Layer.mergeAll(InitProject.Default, GetStatus.Default, QueryProject.Default, IndexProject.Default, ResetIndex.Default);
636
652
  const AppLayer = Layer.merge(UseCaseLayer.pipe(Layer.provide(InfraLayer)), NodeContext.layer);
637
- cli(process.argv).pipe(Effect.provide(AppLayer), NodeRuntime.runMain);
653
+ cli(process.argv).pipe(Effect.provide(AppLayer), NodeRuntime.runMain({ disableErrorReporting: true }));
638
654
  //#endregion
639
655
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucas-bur/pix",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Lightweight local semantic project indexer",
5
5
  "keywords": [
6
6
  "cli",
@@ -39,8 +39,13 @@
39
39
  "test": "vp test",
40
40
  "test:coverage": "vp test --coverage",
41
41
  "check": "vp check",
42
- "ci": "vp check && vp test --coverage && vp run build && fallow audit",
43
- "lint:fallow": "fallow --format json",
42
+ "ci": "vp run check && vp run test:coverage && vp run build && vp run lint:effect && vp run lint:fallow",
43
+ "lint:effect": "effect-language-service diagnostics --project tsconfig.json --format pretty --strict",
44
+ "lint:effect:ci": "effect-language-service diagnostics --project tsconfig.json --format github-actions --strict",
45
+ "lint:effect:agent": "effect-language-service diagnostics --project tsconfig.json --format json --strict",
46
+ "lint:fallow": "fallow",
47
+ "lint:fallow:ci": "vpx fallow audit --base main --format badge",
48
+ "lint:fallow:agent": "fallow --format json",
44
49
  "fallow": "fallow",
45
50
  "prepublishOnly": "vp run build",
46
51
  "pix": ""
@@ -51,7 +56,6 @@
51
56
  "@effect/platform-node": "^0.106.0",
52
57
  "@huggingface/transformers": "^4.2.0",
53
58
  "effect": "^3.21.2",
54
- "fast-glob": "^3.3.3",
55
59
  "ignore": "^7.0.5"
56
60
  },
57
61
  "devDependencies": {
@@ -59,6 +63,7 @@
59
63
  "@types/node": "^25.5.0",
60
64
  "@typescript/native-preview": "7.0.0-dev.20260328.1",
61
65
  "@vitest/coverage-v8": "^4.1.6",
66
+ "effect-memfs": "^0.8.0",
62
67
  "fallow": "^2.65.0",
63
68
  "typescript": "^6.0.2",
64
69
  "vite-plus": "^0.1.14"