@olorehq/olore 0.1.2 → 0.1.4

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/cli.js +854 -386
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3,107 +3,25 @@
3
3
  // src/cli.ts
4
4
  import { createRequire } from "module";
5
5
  import { Command } from "commander";
6
- import pc7 from "picocolors";
6
+ import pc10 from "picocolors";
7
7
 
8
- // src/commands/init.ts
9
- import fs from "fs";
10
- import path from "path";
11
- import readline from "readline";
8
+ // src/commands/doctor.ts
12
9
  import pc from "picocolors";
13
- async function prompt(question, defaultValue) {
14
- const rl = readline.createInterface({
15
- input: process.stdin,
16
- output: process.stdout
17
- });
18
- return new Promise((resolve) => {
19
- const q = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
20
- rl.question(q, (answer) => {
21
- rl.close();
22
- resolve(answer || defaultValue || "");
23
- });
24
- });
25
- }
26
- async function init(options) {
27
- const cwd = process.cwd();
28
- const folderName = path.basename(cwd);
29
- if (fs.existsSync(path.join(cwd, "olore.config.json"))) {
30
- throw new Error("olore.config.json already exists. This folder is already initialized.");
31
- }
32
- let name;
33
- let version2;
34
- let description;
35
- if (options.yes) {
36
- name = options.name || folderName;
37
- version2 = options.version || "1.0.0";
38
- description = `Documentation for ${name}`;
39
- } else {
40
- console.log(pc.bold("\nInitialize olore documentation package\n"));
41
- name = await prompt("Package name", options.name || folderName);
42
- version2 = await prompt("Version", options.version || "1.0.0");
43
- description = await prompt("Description", `Documentation for ${name}`);
44
- }
45
- const fullName = `olore-${name}-${version2}`;
46
- const configContent = {
47
- name,
48
- version: version2,
49
- description,
50
- contentPath: "./docs",
51
- outputPath: "./olore-package",
52
- extensions: [".md", ".mdx"],
53
- exclude: []
54
- };
55
- fs.writeFileSync(
56
- path.join(cwd, "olore.config.json"),
57
- JSON.stringify(configContent, null, 2) + "\n"
58
- );
59
- const docsDir = path.join(cwd, "docs");
60
- if (!fs.existsSync(docsDir)) {
61
- fs.mkdirSync(docsDir);
62
- fs.writeFileSync(
63
- path.join(docsDir, "getting-started.md"),
64
- `# Getting Started with ${name}
65
10
 
66
- Add your documentation content here.
67
-
68
- ## Overview
69
-
70
- Describe what this documentation covers.
71
-
72
- ## Topics
73
-
74
- - Topic 1
75
- - Topic 2
76
- `
77
- );
78
- }
79
- console.log(pc.bold(`
80
- Initialized olore package: `) + pc.cyan(fullName));
81
- console.log("");
82
- console.log(pc.gray("Created:"));
83
- console.log(pc.green(" \u2713 olore.config.json"));
84
- console.log(pc.green(" \u2713 docs/") + pc.gray(" (add your documentation here)"));
85
- console.log("");
86
- console.log(pc.gray("Next steps:"));
87
- console.log(" 1. Add your .md files to the " + pc.cyan("docs/") + " folder");
88
- console.log(" 2. Run " + pc.cyan("/olore-docs-packager-1.0.0") + " in Claude Code to build");
89
- console.log(" 3. Run " + pc.cyan("olore install ./olore-package") + " to install");
90
- console.log("");
91
- }
92
-
93
- // src/commands/install.ts
94
- import path5 from "path";
95
- import fs5 from "fs-extra";
96
- import ora from "ora";
97
- import pc2 from "picocolors";
11
+ // src/core/doctor.ts
12
+ import fs4 from "fs";
13
+ import os4 from "os";
14
+ import path4 from "path";
15
+ import fsExtra from "fs-extra";
98
16
 
99
17
  // src/core/download.ts
100
18
  import { createHash } from "crypto";
101
19
  import { createWriteStream } from "fs";
102
20
  import os from "os";
103
- import path2 from "path";
21
+ import path from "path";
104
22
  import { Readable } from "stream";
105
23
  import { pipeline } from "stream/promises";
106
- import fs2 from "fs-extra";
24
+ import fs from "fs-extra";
107
25
  import * as tar from "tar";
108
26
 
109
27
  // src/core/constants.ts
@@ -138,7 +56,7 @@ async function downloadFile(url, dest) {
138
56
  if (!response.body) {
139
57
  throw new DownloadError("No response body", "NETWORK_ERROR");
140
58
  }
141
- await fs2.ensureDir(path2.dirname(dest));
59
+ await fs.ensureDir(path.dirname(dest));
142
60
  const nodeReadable = Readable.fromWeb(response.body);
143
61
  const writeStream = createWriteStream(dest);
144
62
  await pipeline(nodeReadable, writeStream);
@@ -159,7 +77,7 @@ async function downloadFile(url, dest) {
159
77
  }
160
78
  async function calculateChecksum(filePath) {
161
79
  const hash = createHash("sha256");
162
- const stream = fs2.createReadStream(filePath);
80
+ const stream = fs.createReadStream(filePath);
163
81
  return new Promise((resolve, reject) => {
164
82
  stream.on("data", (data) => hash.update(data));
165
83
  stream.on("end", () => resolve(`sha256-${hash.digest("base64")}`));
@@ -171,7 +89,7 @@ async function verifyChecksum(filePath, expected) {
171
89
  return actual === expected;
172
90
  }
173
91
  async function extractTarball(tarball, dest) {
174
- await fs2.ensureDir(dest);
92
+ await fs.ensureDir(dest);
175
93
  try {
176
94
  await tar.extract({
177
95
  file: tarball,
@@ -187,8 +105,8 @@ async function extractTarball(tarball, dest) {
187
105
  }
188
106
  }
189
107
  async function downloadAndInstall(url, dest, integrity) {
190
- const tempDir = await fs2.mkdtemp(path2.join(os.tmpdir(), "olore-"));
191
- const tarballPath = path2.join(tempDir, "package.tar.gz");
108
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "olore-"));
109
+ const tarballPath = path.join(tempDir, "package.tar.gz");
192
110
  try {
193
111
  await downloadFile(url, tarballPath);
194
112
  const checksumValid = await verifyChecksum(tarballPath, integrity);
@@ -201,39 +119,14 @@ Actual: ${actual}`,
201
119
  "CHECKSUM_MISMATCH"
202
120
  );
203
121
  }
204
- await fs2.remove(dest);
122
+ await fs.remove(dest);
205
123
  await extractTarball(tarballPath, dest);
206
124
  return dest;
207
125
  } finally {
208
- await fs2.remove(tempDir).catch(() => {
126
+ await fs.remove(tempDir).catch(() => {
209
127
  });
210
128
  }
211
129
  }
212
- async function downloadAndExtractToTemp(url, integrity) {
213
- const tempDir = await fs2.mkdtemp(path2.join(os.tmpdir(), "olore-"));
214
- const tarballPath = path2.join(tempDir, "package.tar.gz");
215
- const extractDir = path2.join(tempDir, "package");
216
- try {
217
- await downloadFile(url, tarballPath);
218
- const checksumValid = await verifyChecksum(tarballPath, integrity);
219
- if (!checksumValid) {
220
- const actual = await calculateChecksum(tarballPath);
221
- throw new DownloadError(
222
- `Checksum verification failed.
223
- Expected: ${integrity}
224
- Actual: ${actual}`,
225
- "CHECKSUM_MISMATCH"
226
- );
227
- }
228
- await extractTarball(tarballPath, extractDir);
229
- await fs2.remove(tarballPath);
230
- return extractDir;
231
- } catch (error) {
232
- await fs2.remove(tempDir).catch(() => {
233
- });
234
- throw error;
235
- }
236
- }
237
130
  function formatBytes(bytes) {
238
131
  if (bytes === 0) return "0 B";
239
132
  const k = 1024;
@@ -243,28 +136,32 @@ function formatBytes(bytes) {
243
136
  }
244
137
 
245
138
  // src/core/paths.ts
246
- import fs4 from "fs";
139
+ import fs3 from "fs";
247
140
  import os3 from "os";
248
- import path4 from "path";
141
+ import path3 from "path";
249
142
  import { fileURLToPath } from "url";
250
143
 
251
144
  // src/core/platform.ts
252
145
  import os2 from "os";
253
- import path3 from "path";
254
- import fs3 from "fs-extra";
146
+ import path2 from "path";
147
+ import fs2 from "fs-extra";
255
148
  var isWindows = process.platform === "win32";
256
149
  async function linkOrCopy(source, target) {
257
150
  if (isWindows) {
258
- await fs3.copy(source, target);
151
+ try {
152
+ await fs2.symlink(source, target, "junction");
153
+ } catch {
154
+ await fs2.copy(source, target);
155
+ }
259
156
  } else {
260
- await fs3.symlink(source, target, "dir");
157
+ await fs2.symlink(source, target, "dir");
261
158
  }
262
159
  }
263
160
  function expandPath(p) {
264
161
  if (p.startsWith("~")) {
265
- return path3.join(os2.homedir(), p.slice(1));
162
+ return path2.join(os2.homedir(), p.slice(1));
266
163
  }
267
- return path3.resolve(p);
164
+ return path2.resolve(p);
268
165
  }
269
166
  function isLocalPath(p) {
270
167
  if (p.startsWith(".") || p.startsWith("/") || p.startsWith("~")) {
@@ -285,33 +182,33 @@ function pathStartsWith(child, parent) {
285
182
  return child.startsWith(parent);
286
183
  }
287
184
  function getLinkActionText() {
288
- return isWindows ? "Copying" : "Creating symlinks";
185
+ return "Linking";
289
186
  }
290
187
  function getLinkTypeText() {
291
- return isWindows ? "copied to" : "symlinked to";
188
+ return "linked to";
292
189
  }
293
190
 
294
191
  // src/core/paths.ts
295
192
  var __filename2 = fileURLToPath(import.meta.url);
296
- var __dirname2 = path4.dirname(__filename2);
193
+ var __dirname2 = path3.dirname(__filename2);
297
194
  function getOloreHome() {
298
- return path4.join(os3.homedir(), ".olore");
195
+ return path3.join(os3.homedir(), ".olore");
299
196
  }
300
197
  function getOlorePackagePath(name, version2) {
301
- return path4.join(getOloreHome(), "packages", name, version2);
198
+ return path3.join(getOloreHome(), "packages", name, version2);
302
199
  }
303
200
  function isInstalledPackage(symlinkPath) {
304
201
  try {
305
- const target = fs4.readlinkSync(symlinkPath);
202
+ const target = fs3.readlinkSync(symlinkPath);
306
203
  const oloreHome = getOloreHome();
307
204
  return pathStartsWith(target, oloreHome);
308
205
  } catch {
309
- return fs4.existsSync(symlinkPath) && fs4.statSync(symlinkPath).isDirectory();
206
+ return fs3.existsSync(symlinkPath) && fs3.statSync(symlinkPath).isDirectory();
310
207
  }
311
208
  }
312
209
  function getSymlinkTarget(symlinkPath) {
313
210
  try {
314
- return fs4.readlinkSync(symlinkPath);
211
+ return fs3.readlinkSync(symlinkPath);
315
212
  } catch {
316
213
  return null;
317
214
  }
@@ -319,17 +216,17 @@ function getSymlinkTarget(symlinkPath) {
319
216
  function getAgentPaths() {
320
217
  const home = os3.homedir();
321
218
  return {
322
- claude: path4.join(home, ".claude", "skills"),
323
- codex: path4.join(home, ".codex", "skills"),
324
- opencode: path4.join(home, ".config", "opencode", "skills")
219
+ claude: path3.join(home, ".claude", "skills"),
220
+ codex: path3.join(home, ".codex", "skills"),
221
+ opencode: path3.join(home, ".config", "opencode", "skills")
325
222
  };
326
223
  }
327
224
  function detectAgents() {
328
225
  const agents = [];
329
226
  const paths = getAgentPaths();
330
227
  for (const [agent, agentPath] of Object.entries(paths)) {
331
- const parentDir = path4.dirname(agentPath);
332
- if (fs4.existsSync(parentDir)) {
228
+ const parentDir = path3.dirname(agentPath);
229
+ if (fs3.existsSync(parentDir)) {
333
230
  agents.push(agent);
334
231
  }
335
232
  }
@@ -340,18 +237,18 @@ async function getInstalledPackages() {
340
237
  const packages = [];
341
238
  const seen = /* @__PURE__ */ new Set();
342
239
  for (const [agent, skillsDir] of Object.entries(agentPaths)) {
343
- if (!fs4.existsSync(skillsDir)) {
240
+ if (!fs3.existsSync(skillsDir)) {
344
241
  continue;
345
242
  }
346
- const entries = fs4.readdirSync(skillsDir, { withFileTypes: true });
243
+ const entries = fs3.readdirSync(skillsDir, { withFileTypes: true });
347
244
  for (const entry of entries) {
348
245
  const isValidEntry = entry.isDirectory() || entry.isSymbolicLink();
349
246
  if (!isValidEntry || !entry.name.startsWith("olore-")) {
350
247
  continue;
351
248
  }
352
- const pkgPath = path4.join(skillsDir, entry.name);
353
- const lockPath = path4.join(pkgPath, "olore-lock.json");
354
- if (!fs4.existsSync(lockPath)) {
249
+ const pkgPath = path3.join(skillsDir, entry.name);
250
+ const lockPath = path3.join(pkgPath, "olore-lock.json");
251
+ if (!fs3.existsSync(lockPath)) {
355
252
  continue;
356
253
  }
357
254
  const pkgName = entry.name;
@@ -360,7 +257,7 @@ async function getInstalledPackages() {
360
257
  }
361
258
  seen.add(pkgName);
362
259
  try {
363
- const lock = JSON.parse(fs4.readFileSync(lockPath, "utf-8"));
260
+ const lock = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
364
261
  const stats = await getDirectoryStats(pkgPath);
365
262
  const symlinkTarget = getSymlinkTarget(pkgPath);
366
263
  let installType;
@@ -383,27 +280,437 @@ async function getInstalledPackages() {
383
280
  }
384
281
  }
385
282
  }
386
- return packages.sort((a, b) => a.name.localeCompare(b.name));
387
- }
388
- async function getDirectoryStats(dir) {
389
- let files = 0;
390
- let size = 0;
391
- function walk(currentDir) {
392
- const entries = fs4.readdirSync(currentDir, { withFileTypes: true });
393
- for (const entry of entries) {
394
- const fullPath = path4.join(currentDir, entry.name);
395
- if (entry.isDirectory()) {
396
- walk(fullPath);
397
- } else if (entry.isFile()) {
398
- files++;
399
- size += fs4.statSync(fullPath).size;
400
- }
401
- }
283
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
284
+ }
285
+ async function getDirectoryStats(dir) {
286
+ let files = 0;
287
+ let size = 0;
288
+ function walk(currentDir) {
289
+ const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
290
+ for (const entry of entries) {
291
+ const fullPath = path3.join(currentDir, entry.name);
292
+ if (entry.isDirectory()) {
293
+ walk(fullPath);
294
+ } else if (entry.isFile()) {
295
+ files++;
296
+ size += fs3.statSync(fullPath).size;
297
+ }
298
+ }
299
+ }
300
+ walk(dir);
301
+ return { files, size };
302
+ }
303
+
304
+ // src/core/doctor.ts
305
+ async function diagnose() {
306
+ const issues = [];
307
+ const [dangling, orphaned, partial] = await Promise.all([
308
+ scanAgentDirs(),
309
+ scanOrphanedPackages(),
310
+ scanPartialInstalls()
311
+ ]);
312
+ issues.push(...dangling, ...orphaned, ...partial);
313
+ return {
314
+ ok: issues.length === 0,
315
+ issues
316
+ };
317
+ }
318
+ async function scanAgentDirs() {
319
+ const issues = [];
320
+ const agentPaths = getAgentPaths();
321
+ for (const [agent, skillsDir] of Object.entries(agentPaths)) {
322
+ if (!fs4.existsSync(skillsDir)) {
323
+ continue;
324
+ }
325
+ let entries;
326
+ try {
327
+ entries = fs4.readdirSync(skillsDir, { withFileTypes: true });
328
+ } catch {
329
+ continue;
330
+ }
331
+ for (const entry of entries) {
332
+ if (!entry.name.startsWith("olore-")) {
333
+ continue;
334
+ }
335
+ if (!entry.isSymbolicLink()) {
336
+ continue;
337
+ }
338
+ const fullPath = path4.join(skillsDir, entry.name);
339
+ const target = readSymlinkTarget(fullPath);
340
+ if (target === null) {
341
+ continue;
342
+ }
343
+ const resolvedTarget = path4.resolve(skillsDir, target);
344
+ if (!fs4.existsSync(resolvedTarget)) {
345
+ issues.push({
346
+ type: "dangling-symlink",
347
+ path: fullPath,
348
+ target: resolvedTarget,
349
+ agent
350
+ });
351
+ }
352
+ }
353
+ }
354
+ return issues;
355
+ }
356
+ async function scanOrphanedPackages() {
357
+ const issues = [];
358
+ const packagesDir = path4.join(getOloreHome(), "packages");
359
+ if (!fs4.existsSync(packagesDir)) {
360
+ return issues;
361
+ }
362
+ const activeTargets = await collectActiveSymlinkTargets();
363
+ let nameEntries;
364
+ try {
365
+ nameEntries = fs4.readdirSync(packagesDir, { withFileTypes: true });
366
+ } catch {
367
+ return issues;
368
+ }
369
+ for (const nameEntry of nameEntries) {
370
+ if (!nameEntry.isDirectory()) {
371
+ continue;
372
+ }
373
+ const nameDir = path4.join(packagesDir, nameEntry.name);
374
+ let versionEntries;
375
+ try {
376
+ versionEntries = fs4.readdirSync(nameDir, { withFileTypes: true });
377
+ } catch {
378
+ continue;
379
+ }
380
+ for (const versionEntry of versionEntries) {
381
+ if (!versionEntry.isDirectory()) {
382
+ continue;
383
+ }
384
+ const versionDir = path4.join(nameDir, versionEntry.name);
385
+ const lockPath = path4.join(versionDir, "olore-lock.json");
386
+ if (!fs4.existsSync(lockPath)) {
387
+ continue;
388
+ }
389
+ const realPath = fs4.realpathSync(versionDir);
390
+ if (activeTargets.has(realPath)) {
391
+ continue;
392
+ }
393
+ const stats = getDirectoryStats2(versionDir);
394
+ issues.push({
395
+ type: "orphaned",
396
+ path: versionDir,
397
+ name: nameEntry.name,
398
+ version: versionEntry.name,
399
+ files: stats.files,
400
+ size: stats.size
401
+ });
402
+ }
403
+ }
404
+ return issues;
405
+ }
406
+ async function scanPartialInstalls() {
407
+ const issues = [];
408
+ const packagesDir = path4.join(getOloreHome(), "packages");
409
+ if (!fs4.existsSync(packagesDir)) {
410
+ return issues;
411
+ }
412
+ let nameEntries;
413
+ try {
414
+ nameEntries = fs4.readdirSync(packagesDir, { withFileTypes: true });
415
+ } catch {
416
+ return issues;
417
+ }
418
+ for (const nameEntry of nameEntries) {
419
+ if (!nameEntry.isDirectory()) {
420
+ continue;
421
+ }
422
+ const nameDir = path4.join(packagesDir, nameEntry.name);
423
+ let versionEntries;
424
+ try {
425
+ versionEntries = fs4.readdirSync(nameDir, { withFileTypes: true });
426
+ } catch {
427
+ continue;
428
+ }
429
+ for (const versionEntry of versionEntries) {
430
+ if (!versionEntry.isDirectory()) {
431
+ continue;
432
+ }
433
+ const versionDir = path4.join(nameDir, versionEntry.name);
434
+ const lockPath = path4.join(versionDir, "olore-lock.json");
435
+ if (!fs4.existsSync(lockPath)) {
436
+ issues.push({
437
+ type: "partial-install",
438
+ path: versionDir,
439
+ name: nameEntry.name,
440
+ version: versionEntry.name
441
+ });
442
+ }
443
+ }
444
+ }
445
+ return issues;
446
+ }
447
+ async function collectActiveSymlinkTargets() {
448
+ const targets = /* @__PURE__ */ new Set();
449
+ const agentPaths = getAgentPaths();
450
+ for (const skillsDir of Object.values(agentPaths)) {
451
+ if (!fs4.existsSync(skillsDir)) {
452
+ continue;
453
+ }
454
+ let entries;
455
+ try {
456
+ entries = fs4.readdirSync(skillsDir, { withFileTypes: true });
457
+ } catch {
458
+ continue;
459
+ }
460
+ for (const entry of entries) {
461
+ if (!entry.name.startsWith("olore-") || !entry.isSymbolicLink()) {
462
+ continue;
463
+ }
464
+ const fullPath = path4.join(skillsDir, entry.name);
465
+ try {
466
+ const realTarget = fs4.realpathSync(fullPath);
467
+ targets.add(realTarget);
468
+ } catch {
469
+ }
470
+ }
471
+ }
472
+ return targets;
473
+ }
474
+ async function pruneIssues(issues) {
475
+ const removed = [];
476
+ const failed = [];
477
+ for (const issue of issues) {
478
+ if (!pathExists(issue.path)) {
479
+ continue;
480
+ }
481
+ try {
482
+ let freedBytes = 0;
483
+ if (issue.type === "dangling-symlink") {
484
+ await fsExtra.remove(issue.path);
485
+ } else if (issue.type === "orphaned") {
486
+ freedBytes = issue.size;
487
+ await fsExtra.remove(issue.path);
488
+ await cleanEmptyParentDir(issue.path);
489
+ } else if (issue.type === "partial-install") {
490
+ const stats = getDirectoryStats2(issue.path);
491
+ freedBytes = stats.size;
492
+ await fsExtra.remove(issue.path);
493
+ await cleanEmptyParentDir(issue.path);
494
+ }
495
+ removed.push({ issue, freedBytes });
496
+ } catch (error) {
497
+ failed.push({
498
+ issue,
499
+ error: error instanceof Error ? error.message : "Unknown error"
500
+ });
501
+ }
502
+ }
503
+ return { removed, failed };
504
+ }
505
+ function pathExists(p) {
506
+ try {
507
+ fs4.lstatSync(p);
508
+ return true;
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+ function readSymlinkTarget(symlinkPath) {
514
+ try {
515
+ return fs4.readlinkSync(symlinkPath);
516
+ } catch {
517
+ return null;
518
+ }
519
+ }
520
+ async function cleanEmptyParentDir(removedPath) {
521
+ const parentDir = path4.dirname(removedPath);
522
+ try {
523
+ const remaining = fs4.readdirSync(parentDir);
524
+ if (remaining.length === 0) {
525
+ await fsExtra.remove(parentDir);
526
+ }
527
+ } catch {
528
+ }
529
+ }
530
+ function getDirectoryStats2(dir) {
531
+ let files = 0;
532
+ let size = 0;
533
+ function walk(currentDir) {
534
+ let entries;
535
+ try {
536
+ entries = fs4.readdirSync(currentDir, { withFileTypes: true });
537
+ } catch {
538
+ return;
539
+ }
540
+ for (const entry of entries) {
541
+ const fullPath = path4.join(currentDir, entry.name);
542
+ if (entry.isDirectory()) {
543
+ walk(fullPath);
544
+ } else if (entry.isFile()) {
545
+ try {
546
+ files++;
547
+ size += fs4.statSync(fullPath).size;
548
+ } catch {
549
+ }
550
+ }
551
+ }
552
+ }
553
+ walk(dir);
554
+ return { files, size };
555
+ }
556
+ function displayPath(p) {
557
+ const home = os4.homedir();
558
+ if (p.startsWith(home)) {
559
+ return "~" + p.slice(home.length);
560
+ }
561
+ return p;
562
+ }
563
+ function formatIssue(issue) {
564
+ switch (issue.type) {
565
+ case "dangling-symlink":
566
+ return `${displayPath(issue.path)} -> ${displayPath(issue.target)} (missing)`;
567
+ case "orphaned":
568
+ return `${displayPath(issue.path)} (no agent links, ${issue.files} files, ${formatBytes(issue.size)})`;
569
+ case "partial-install":
570
+ return `${displayPath(issue.path)} (missing olore-lock.json)`;
571
+ }
572
+ }
573
+
574
+ // src/commands/doctor.ts
575
+ async function doctor(options) {
576
+ if (!options.json) {
577
+ console.log(pc.bold("\nChecking installed packages...\n"));
578
+ }
579
+ const result = await diagnose();
580
+ if (options.json) {
581
+ console.log(JSON.stringify(result, null, 2));
582
+ if (!result.ok) {
583
+ process.exit(1);
584
+ }
585
+ return;
586
+ }
587
+ if (result.ok) {
588
+ console.log(pc.green("No issues found. Everything looks good."));
589
+ return;
590
+ }
591
+ const dangling = result.issues.filter((i) => i.type === "dangling-symlink");
592
+ const orphaned = result.issues.filter((i) => i.type === "orphaned");
593
+ const partial = result.issues.filter((i) => i.type === "partial-install");
594
+ console.log(
595
+ `Found ${pc.yellow(String(result.issues.length))} issue${result.issues.length === 1 ? "" : "s"}:
596
+ `
597
+ );
598
+ if (dangling.length > 0) {
599
+ console.log(pc.bold(`Dangling symlinks (${dangling.length}):`));
600
+ for (const issue of dangling) {
601
+ console.log(` ${formatIssue(issue)}`);
602
+ }
603
+ console.log("");
604
+ }
605
+ if (orphaned.length > 0) {
606
+ console.log(pc.bold(`Orphaned packages (${orphaned.length}):`));
607
+ for (const issue of orphaned) {
608
+ console.log(` ${formatIssue(issue)}`);
609
+ }
610
+ console.log("");
611
+ }
612
+ if (partial.length > 0) {
613
+ console.log(pc.bold(`Partial installs (${partial.length}):`));
614
+ for (const issue of partial) {
615
+ console.log(` ${formatIssue(issue)}`);
616
+ }
617
+ console.log("");
618
+ }
619
+ console.log(`Run ${pc.cyan("olore prune")} to fix these issues.`);
620
+ process.exit(1);
621
+ }
622
+
623
+ // src/commands/init.ts
624
+ import fs5 from "fs";
625
+ import path5 from "path";
626
+ import readline from "readline";
627
+ import pc2 from "picocolors";
628
+ async function prompt(question, defaultValue) {
629
+ const rl = readline.createInterface({
630
+ input: process.stdin,
631
+ output: process.stdout
632
+ });
633
+ return new Promise((resolve) => {
634
+ const q = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
635
+ rl.question(q, (answer) => {
636
+ rl.close();
637
+ resolve(answer || defaultValue || "");
638
+ });
639
+ });
640
+ }
641
+ async function init(options) {
642
+ const cwd = process.cwd();
643
+ const folderName = path5.basename(cwd);
644
+ if (fs5.existsSync(path5.join(cwd, "olore.config.json"))) {
645
+ throw new Error("olore.config.json already exists. This folder is already initialized.");
646
+ }
647
+ let name;
648
+ let version2;
649
+ let description;
650
+ if (options.yes) {
651
+ name = options.name || folderName;
652
+ version2 = options.version || "1.0.0";
653
+ description = `Documentation for ${name}`;
654
+ } else {
655
+ console.log(pc2.bold("\nInitialize olore documentation package\n"));
656
+ name = await prompt("Package name", options.name || folderName);
657
+ version2 = await prompt("Version", options.version || "1.0.0");
658
+ description = await prompt("Description", `Documentation for ${name}`);
659
+ }
660
+ const fullName = `olore-${name}-${version2}`;
661
+ const configContent = {
662
+ name,
663
+ version: version2,
664
+ description,
665
+ contentPath: "./docs",
666
+ outputPath: "./olore-package",
667
+ extensions: [".md", ".mdx"],
668
+ exclude: []
669
+ };
670
+ fs5.writeFileSync(
671
+ path5.join(cwd, "olore.config.json"),
672
+ JSON.stringify(configContent, null, 2) + "\n"
673
+ );
674
+ const docsDir = path5.join(cwd, "docs");
675
+ if (!fs5.existsSync(docsDir)) {
676
+ fs5.mkdirSync(docsDir);
677
+ fs5.writeFileSync(
678
+ path5.join(docsDir, "getting-started.md"),
679
+ `# Getting Started with ${name}
680
+
681
+ Add your documentation content here.
682
+
683
+ ## Overview
684
+
685
+ Describe what this documentation covers.
686
+
687
+ ## Topics
688
+
689
+ - Topic 1
690
+ - Topic 2
691
+ `
692
+ );
402
693
  }
403
- walk(dir);
404
- return { files, size };
694
+ console.log(pc2.bold(`
695
+ Initialized olore package: `) + pc2.cyan(fullName));
696
+ console.log("");
697
+ console.log(pc2.gray("Created:"));
698
+ console.log(pc2.green(" \u2713 olore.config.json"));
699
+ console.log(pc2.green(" \u2713 docs/") + pc2.gray(" (add your documentation here)"));
700
+ console.log("");
701
+ console.log(pc2.gray("Next steps:"));
702
+ console.log(" 1. Add your .md files to the " + pc2.cyan("docs/") + " folder");
703
+ console.log(" 2. Run " + pc2.cyan("/olore-docs-packager-1.0.0") + " in Claude Code to build");
704
+ console.log(" 3. Run " + pc2.cyan("olore install ./olore-package") + " to install");
705
+ console.log("");
405
706
  }
406
707
 
708
+ // src/commands/install.ts
709
+ import path6 from "path";
710
+ import fs6 from "fs-extra";
711
+ import ora from "ora";
712
+ import pc3 from "picocolors";
713
+
407
714
  // src/core/registry.ts
408
715
  var RegistryError = class extends Error {
409
716
  constructor(message, code) {
@@ -435,6 +742,22 @@ async function fetchWithTimeout(url, timeout = DOWNLOAD_TIMEOUT) {
435
742
  clearTimeout(timeoutId);
436
743
  }
437
744
  }
745
+ async function fetchPackageIndex() {
746
+ const url = `${REGISTRY_URL}/index.json`;
747
+ const response = await fetchWithTimeout(url);
748
+ if (response.status === 404) {
749
+ throw new RegistryError("Registry not found", "NOT_FOUND");
750
+ }
751
+ if (!response.ok) {
752
+ throw new RegistryError(`Failed to fetch registry: ${response.status}`, "NETWORK_ERROR");
753
+ }
754
+ try {
755
+ const data = await response.json();
756
+ return data;
757
+ } catch {
758
+ throw new RegistryError("Invalid registry response", "INVALID_RESPONSE");
759
+ }
760
+ }
438
761
  async function fetchPackageVersions(name) {
439
762
  const url = `${REGISTRY_URL}/packages/${name}.json`;
440
763
  const response = await fetchWithTimeout(url);
@@ -468,22 +791,22 @@ async function resolveVersion(name, version2) {
468
791
  // src/commands/install.ts
469
792
  async function installFromLocal(localPath) {
470
793
  const fullPath = expandPath(localPath);
471
- if (!await fs5.pathExists(fullPath)) {
794
+ if (!await fs6.pathExists(fullPath)) {
472
795
  throw new Error(`Path not found: ${fullPath}`);
473
796
  }
474
- const stat = await fs5.stat(fullPath);
797
+ const stat = await fs6.stat(fullPath);
475
798
  if (!stat.isDirectory()) {
476
799
  throw new Error(`Not a directory: ${fullPath}`);
477
800
  }
478
- const lockPath = path5.join(fullPath, "olore-lock.json");
479
- if (!await fs5.pathExists(lockPath)) {
801
+ const lockPath = path6.join(fullPath, "olore-lock.json");
802
+ if (!await fs6.pathExists(lockPath)) {
480
803
  throw new Error(`Missing olore-lock.json in ${fullPath}`);
481
804
  }
482
- const skillPath = path5.join(fullPath, "SKILL.md");
483
- if (!await fs5.pathExists(skillPath)) {
805
+ const skillPath = path6.join(fullPath, "SKILL.md");
806
+ if (!await fs6.pathExists(skillPath)) {
484
807
  throw new Error(`Missing SKILL.md in ${fullPath}. Run /generate-agent-skills first.`);
485
808
  }
486
- const lock = await fs5.readJson(lockPath);
809
+ const lock = await fs6.readJson(lockPath);
487
810
  const packageName = lock.name;
488
811
  const packageVersion = lock.version;
489
812
  if (!packageName) {
@@ -493,72 +816,57 @@ async function installFromLocal(localPath) {
493
816
  throw new Error(`Invalid olore-lock.json: missing "version" field`);
494
817
  }
495
818
  const skillName = `olore-${packageName}-${packageVersion}`;
496
- const skillContent = await fs5.readFile(skillPath, "utf-8");
819
+ const skillContent = await fs6.readFile(skillPath, "utf-8");
497
820
  const nameMatch = skillContent.match(/^name:\s*(.+)$/m);
498
821
  const skillMdName = nameMatch ? nameMatch[1].trim() : null;
499
822
  if (skillMdName !== skillName) {
500
- console.log(pc2.yellow(`
823
+ console.log(pc3.yellow(`
501
824
  Warning: SKILL.md name mismatch`));
502
- console.log(pc2.gray(` Expected: ${skillName}`));
503
- console.log(pc2.gray(` Found: ${skillMdName || "(none)"}`));
504
- console.log(pc2.yellow(` Updating SKILL.md to fix...`));
825
+ console.log(pc3.gray(` Expected: ${skillName}`));
826
+ console.log(pc3.gray(` Found: ${skillMdName || "(none)"}`));
827
+ console.log(pc3.yellow(` Updating SKILL.md to fix...`));
505
828
  const updatedContent = skillMdName ? skillContent.replace(/^name:\s*.+$/m, `name: ${skillName}`) : skillContent.replace(/^---\n/, `---
506
829
  name: ${skillName}
507
830
  `);
508
- await fs5.writeFile(skillPath, updatedContent);
831
+ await fs6.writeFile(skillPath, updatedContent);
509
832
  }
510
- console.log(pc2.bold(`
833
+ console.log(pc3.bold(`
511
834
  Installing ${packageName}@${packageVersion} from local path...
512
835
  `));
513
836
  const agents = detectAgents();
514
837
  if (agents.length === 0) {
515
- console.log(pc2.yellow("No agents detected. Creating directories anyway."));
838
+ console.log(pc3.yellow("No agents detected. Creating directories anyway."));
516
839
  }
517
840
  const agentPaths = getAgentPaths();
518
- if (isWindows) {
519
- const spinner = ora("Copying to agent directories...").start();
520
- const copied = [];
521
- for (const [agent, skillsDir] of Object.entries(agentPaths)) {
522
- const targetDir = path5.join(skillsDir, skillName);
523
- await fs5.ensureDir(skillsDir);
524
- await fs5.remove(targetDir);
525
- await fs5.copy(fullPath, targetDir);
526
- copied.push(`${pc2.green("\u2713")} ${agent} ${pc2.gray("\u2192")} ${pc2.gray(targetDir)}`);
527
- }
528
- spinner.stop();
529
- copied.forEach((line) => console.log(` ${line}`));
530
- console.log(pc2.gray(` \u2514\u2500 copied from ${fullPath}`));
531
- } else {
532
- const olorePath = getOlorePackagePath(packageName, packageVersion);
533
- const spinner = ora("Copying to ~/.olore...").start();
534
- await fs5.ensureDir(path5.dirname(olorePath));
535
- await fs5.remove(olorePath);
536
- await fs5.copy(fullPath, olorePath);
537
- spinner.succeed(`Copied to ${pc2.gray(olorePath)}`);
538
- const linkSpinner = ora("Creating symlinks...").start();
539
- const linked = [];
540
- for (const [agent, skillsDir] of Object.entries(agentPaths)) {
541
- const targetDir = path5.join(skillsDir, skillName);
542
- await fs5.ensureDir(skillsDir);
543
- await fs5.remove(targetDir);
544
- await linkOrCopy(olorePath, targetDir);
545
- linked.push(`${pc2.green("\u2713")} ${agent} ${pc2.gray("\u2192")} ${pc2.gray(targetDir)}`);
546
- }
547
- linkSpinner.stop();
548
- linked.forEach((line) => console.log(` ${line}`));
549
- console.log(pc2.gray(` \u2514\u2500 all symlinked to ${olorePath}`));
841
+ const olorePath = getOlorePackagePath(packageName, packageVersion);
842
+ const spinner = ora("Copying to ~/.olore...").start();
843
+ await fs6.ensureDir(path6.dirname(olorePath));
844
+ await fs6.remove(olorePath);
845
+ await fs6.copy(fullPath, olorePath);
846
+ spinner.succeed(`Copied to ${pc3.gray(olorePath)}`);
847
+ const linkSpinner = ora("Linking to agent directories...").start();
848
+ const linked = [];
849
+ for (const [agent, skillsDir] of Object.entries(agentPaths)) {
850
+ const targetDir = path6.join(skillsDir, skillName);
851
+ await fs6.ensureDir(skillsDir);
852
+ await fs6.remove(targetDir);
853
+ await linkOrCopy(olorePath, targetDir);
854
+ linked.push(`${pc3.green("\u2713")} ${agent} ${pc3.gray("\u2192")} ${pc3.gray(targetDir)}`);
550
855
  }
856
+ linkSpinner.stop();
857
+ linked.forEach((line) => console.log(` ${line}`));
858
+ console.log(pc3.gray(` \u2514\u2500 all linked to ${olorePath}`));
551
859
  console.log("");
552
- console.log(pc2.green("Installation complete!"));
860
+ console.log(pc3.green("Installation complete!"));
553
861
  console.log("");
554
- console.log(pc2.gray("Skill is now available as:"));
555
- console.log(pc2.cyan(` /${skillName}`) + pc2.gray(" (Claude Code)"));
556
- console.log(pc2.cyan(` $${skillName}`) + pc2.gray(" (Codex)"));
557
- console.log(pc2.cyan(` ${skillName}`) + pc2.gray(" (OpenCode)"));
862
+ console.log(pc3.gray("Skill is now available as:"));
863
+ console.log(pc3.cyan(` /${skillName}`) + pc3.gray(" (Claude Code)"));
864
+ console.log(pc3.cyan(` $${skillName}`) + pc3.gray(" (Codex)"));
865
+ console.log(pc3.cyan(` ${skillName}`) + pc3.gray(" (OpenCode)"));
558
866
  }
559
867
  async function install(pkg, options) {
560
868
  if (options.force) {
561
- console.log(pc2.cyan("\n\u2728 May the Skill be with you.\n"));
869
+ console.log(pc3.cyan("\n\u2728 May the Skill be with you.\n"));
562
870
  }
563
871
  if (isLocalPath(pkg)) {
564
872
  await installFromLocal(pkg);
@@ -579,7 +887,7 @@ function parsePackageSpec(spec) {
579
887
  async function installFromRemote(pkg, optionsVersion) {
580
888
  const { name, version: specVersion } = parsePackageSpec(pkg);
581
889
  const requestedVersion = optionsVersion || specVersion || "latest";
582
- console.log(pc2.bold(`
890
+ console.log(pc3.bold(`
583
891
  Installing ${name}@${requestedVersion} from registry...
584
892
  `));
585
893
  const spinner = ora("Fetching package info...").start();
@@ -591,18 +899,18 @@ Installing ${name}@${requestedVersion} from registry...
591
899
  spinner.fail("Failed to resolve package");
592
900
  if (error instanceof RegistryError) {
593
901
  if (error.code === "NOT_FOUND") {
594
- console.log(pc2.yellow(`
902
+ console.log(pc3.yellow(`
595
903
  Package "${name}" not found in registry.`));
596
- console.log(pc2.gray("\nFor local packages, use a path:"));
597
- console.log(pc2.cyan(` olore install ./vault/packages/${name}/<version>`));
904
+ console.log(pc3.gray("\nFor local packages, use a path:"));
905
+ console.log(pc3.cyan(` olore install ./vault/packages/${name}/<version>`));
598
906
  console.log("");
599
907
  } else if (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT") {
600
- console.log(pc2.red(`
908
+ console.log(pc3.red(`
601
909
  Network error: ${error.message}`));
602
- console.log(pc2.gray("Please check your internet connection and try again."));
910
+ console.log(pc3.gray("Please check your internet connection and try again."));
603
911
  }
604
912
  } else {
605
- console.log(pc2.red(`
913
+ console.log(pc3.red(`
606
914
  Error: ${error instanceof Error ? error.message : "Unknown error"}`));
607
915
  }
608
916
  process.exit(1);
@@ -610,114 +918,74 @@ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
610
918
  const skillName = `olore-${name}-${versionInfo.version}`;
611
919
  const agents = detectAgents();
612
920
  if (agents.length === 0) {
613
- console.log(pc2.yellow("No agents detected. Creating directories anyway."));
921
+ console.log(pc3.yellow("No agents detected. Creating directories anyway."));
614
922
  }
615
923
  const agentPaths = getAgentPaths();
616
- if (isWindows) {
617
- const downloadSpinner = ora("Downloading package...").start();
618
- let tempDir;
619
- try {
620
- tempDir = await downloadAndExtractToTemp(versionInfo.downloadUrl, versionInfo.integrity);
621
- downloadSpinner.succeed("Downloaded and verified");
622
- } catch (error) {
623
- downloadSpinner.fail("Download failed");
624
- if (error instanceof DownloadError) {
625
- if (error.code === "CHECKSUM_MISMATCH") {
626
- console.log(pc2.red("\nChecksum verification failed!"));
627
- console.log(pc2.gray("The downloaded package may be corrupted or tampered with."));
628
- } else {
629
- console.log(pc2.red(`
630
- Download error: ${error.message}`));
631
- }
924
+ const olorePath = getOlorePackagePath(name, versionInfo.version);
925
+ const downloadSpinner = ora("Downloading package...").start();
926
+ try {
927
+ await downloadAndInstall(versionInfo.downloadUrl, olorePath, versionInfo.integrity);
928
+ downloadSpinner.succeed(`Downloaded to ${pc3.gray(olorePath)}`);
929
+ } catch (error) {
930
+ downloadSpinner.fail("Download failed");
931
+ if (error instanceof DownloadError) {
932
+ if (error.code === "CHECKSUM_MISMATCH") {
933
+ console.log(pc3.red("\nChecksum verification failed!"));
934
+ console.log(pc3.gray("The downloaded package may be corrupted or tampered with."));
632
935
  } else {
633
- console.log(pc2.red(`
634
- Error: ${error instanceof Error ? error.message : "Unknown error"}`));
635
- }
636
- process.exit(1);
637
- }
638
- try {
639
- const copySpinner = ora("Copying to agent directories...").start();
640
- const copied = [];
641
- for (const [agent, skillsDir] of Object.entries(agentPaths)) {
642
- const targetDir = path5.join(skillsDir, skillName);
643
- await fs5.ensureDir(skillsDir);
644
- await fs5.remove(targetDir);
645
- await fs5.copy(tempDir, targetDir);
646
- copied.push(`${pc2.green("\u2713")} ${agent} ${pc2.gray("\u2192")} ${pc2.gray(targetDir)}`);
647
- }
648
- copySpinner.stop();
649
- copied.forEach((line) => console.log(` ${line}`));
650
- } finally {
651
- await fs5.remove(path5.dirname(tempDir)).catch(() => {
652
- });
653
- }
654
- } else {
655
- const olorePath = getOlorePackagePath(name, versionInfo.version);
656
- const downloadSpinner = ora("Downloading package...").start();
657
- try {
658
- await downloadAndInstall(versionInfo.downloadUrl, olorePath, versionInfo.integrity);
659
- downloadSpinner.succeed(`Downloaded to ${pc2.gray(olorePath)}`);
660
- } catch (error) {
661
- downloadSpinner.fail("Download failed");
662
- if (error instanceof DownloadError) {
663
- if (error.code === "CHECKSUM_MISMATCH") {
664
- console.log(pc2.red("\nChecksum verification failed!"));
665
- console.log(pc2.gray("The downloaded package may be corrupted or tampered with."));
666
- } else {
667
- console.log(pc2.red(`
936
+ console.log(pc3.red(`
668
937
  Download error: ${error.message}`));
669
- }
670
- } else {
671
- console.log(pc2.red(`
672
- Error: ${error instanceof Error ? error.message : "Unknown error"}`));
673
938
  }
674
- process.exit(1);
675
- }
676
- const linkSpinner = ora("Creating symlinks...").start();
677
- const linked = [];
678
- for (const [agent, skillsDir] of Object.entries(agentPaths)) {
679
- const targetDir = path5.join(skillsDir, skillName);
680
- await fs5.ensureDir(skillsDir);
681
- await fs5.remove(targetDir);
682
- await linkOrCopy(olorePath, targetDir);
683
- linked.push(`${pc2.green("\u2713")} ${agent} ${pc2.gray("\u2192")} ${pc2.gray(targetDir)}`);
939
+ } else {
940
+ console.log(pc3.red(`
941
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
684
942
  }
685
- linkSpinner.stop();
686
- linked.forEach((line) => console.log(` ${line}`));
687
- console.log(pc2.gray(` \u2514\u2500 all symlinked to ${olorePath}`));
943
+ process.exit(1);
944
+ }
945
+ const linkSpinner = ora("Linking to agent directories...").start();
946
+ const linked = [];
947
+ for (const [agent, skillsDir] of Object.entries(agentPaths)) {
948
+ const targetDir = path6.join(skillsDir, skillName);
949
+ await fs6.ensureDir(skillsDir);
950
+ await fs6.remove(targetDir);
951
+ await linkOrCopy(olorePath, targetDir);
952
+ linked.push(`${pc3.green("\u2713")} ${agent} ${pc3.gray("\u2192")} ${pc3.gray(targetDir)}`);
688
953
  }
954
+ linkSpinner.stop();
955
+ linked.forEach((line) => console.log(` ${line}`));
956
+ console.log(pc3.gray(` \u2514\u2500 all linked to ${olorePath}`));
689
957
  console.log("");
690
- console.log(pc2.green("Installation complete!"));
958
+ console.log(pc3.green("Installation complete!"));
691
959
  console.log("");
692
- console.log(pc2.gray("Skill is now available as:"));
693
- console.log(pc2.cyan(` /${skillName}`) + pc2.gray(" (Claude Code)"));
694
- console.log(pc2.cyan(` $${skillName}`) + pc2.gray(" (Codex)"));
695
- console.log(pc2.cyan(` ${skillName}`) + pc2.gray(" (OpenCode)"));
960
+ console.log(pc3.gray("Skill is now available as:"));
961
+ console.log(pc3.cyan(` /${skillName}`) + pc3.gray(" (Claude Code)"));
962
+ console.log(pc3.cyan(` $${skillName}`) + pc3.gray(" (Codex)"));
963
+ console.log(pc3.cyan(` ${skillName}`) + pc3.gray(" (OpenCode)"));
696
964
  }
697
965
 
698
966
  // src/commands/link.ts
699
- import path6 from "path";
700
- import fs6 from "fs-extra";
967
+ import path7 from "path";
968
+ import fs7 from "fs-extra";
701
969
  import ora2 from "ora";
702
- import pc3 from "picocolors";
970
+ import pc4 from "picocolors";
703
971
  async function link(localPath) {
704
972
  const fullPath = expandPath(localPath);
705
- if (!await fs6.pathExists(fullPath)) {
973
+ if (!await fs7.pathExists(fullPath)) {
706
974
  throw new Error(`Path not found: ${fullPath}`);
707
975
  }
708
- const stat = await fs6.stat(fullPath);
976
+ const stat = await fs7.stat(fullPath);
709
977
  if (!stat.isDirectory()) {
710
978
  throw new Error(`Not a directory: ${fullPath}`);
711
979
  }
712
- const lockPath = path6.join(fullPath, "olore-lock.json");
713
- if (!await fs6.pathExists(lockPath)) {
980
+ const lockPath = path7.join(fullPath, "olore-lock.json");
981
+ if (!await fs7.pathExists(lockPath)) {
714
982
  throw new Error(`Missing olore-lock.json in ${fullPath}`);
715
983
  }
716
- const skillPath = path6.join(fullPath, "SKILL.md");
717
- if (!await fs6.pathExists(skillPath)) {
984
+ const skillPath = path7.join(fullPath, "SKILL.md");
985
+ if (!await fs7.pathExists(skillPath)) {
718
986
  throw new Error(`Missing SKILL.md in ${fullPath}. Run /build-docs first.`);
719
987
  }
720
- const lock = await fs6.readJson(lockPath);
988
+ const lock = await fs7.readJson(lockPath);
721
989
  const packageName = lock.name;
722
990
  const packageVersion = lock.version;
723
991
  if (!packageName) {
@@ -727,87 +995,96 @@ async function link(localPath) {
727
995
  throw new Error(`Invalid olore-lock.json: missing "version" field`);
728
996
  }
729
997
  const skillName = `olore-${packageName}-${packageVersion}`;
730
- const skillContent = await fs6.readFile(skillPath, "utf-8");
998
+ const skillContent = await fs7.readFile(skillPath, "utf-8");
731
999
  const nameMatch = skillContent.match(/^name:\s*(.+)$/m);
732
1000
  const skillMdName = nameMatch ? nameMatch[1].trim() : null;
733
1001
  if (skillMdName !== skillName) {
734
- console.log(pc3.yellow(`
1002
+ console.log(pc4.yellow(`
735
1003
  Warning: SKILL.md name mismatch`));
736
- console.log(pc3.gray(` Expected: ${skillName}`));
737
- console.log(pc3.gray(` Found: ${skillMdName || "(none)"}`));
738
- console.log(pc3.yellow(` Updating SKILL.md to fix...`));
1004
+ console.log(pc4.gray(` Expected: ${skillName}`));
1005
+ console.log(pc4.gray(` Found: ${skillMdName || "(none)"}`));
1006
+ console.log(pc4.yellow(` Updating SKILL.md to fix...`));
739
1007
  const updatedContent = skillMdName ? skillContent.replace(/^name:\s*.+$/m, `name: ${skillName}`) : skillContent.replace(/^---\n/, `---
740
1008
  name: ${skillName}
741
1009
  `);
742
- await fs6.writeFile(skillPath, updatedContent);
1010
+ await fs7.writeFile(skillPath, updatedContent);
743
1011
  }
744
- console.log(pc3.bold(`
1012
+ console.log(pc4.bold(`
745
1013
  Linking ${packageName}@${packageVersion}...
746
1014
  `));
747
1015
  const agents = detectAgents();
748
1016
  if (agents.length === 0) {
749
- console.log(pc3.yellow("No agents detected. Creating directories anyway."));
1017
+ console.log(pc4.yellow("No agents detected. Creating directories anyway."));
750
1018
  }
751
1019
  const agentPaths = getAgentPaths();
752
1020
  const spinner = ora2(`${getLinkActionText()}...`).start();
753
1021
  const linked = [];
754
1022
  for (const [agent, skillsDir] of Object.entries(agentPaths)) {
755
- const targetDir = path6.join(skillsDir, skillName);
756
- await fs6.ensureDir(skillsDir);
757
- await fs6.remove(targetDir);
1023
+ const targetDir = path7.join(skillsDir, skillName);
1024
+ await fs7.ensureDir(skillsDir);
1025
+ await fs7.remove(targetDir);
758
1026
  await linkOrCopy(fullPath, targetDir);
759
- linked.push(`${pc3.blue("\u26D3")} ${agent} ${pc3.gray("\u2192")} ${pc3.gray(targetDir)}`);
1027
+ linked.push(`${pc4.blue("\u26D3")} ${agent} ${pc4.gray("\u2192")} ${pc4.gray(targetDir)}`);
760
1028
  }
761
1029
  spinner.stop();
762
1030
  linked.forEach((line) => console.log(` ${line}`));
763
- console.log(pc3.gray(` \u2514\u2500 ${getLinkTypeText()} ${fullPath}`));
1031
+ console.log(pc4.gray(` \u2514\u2500 ${getLinkTypeText()} ${fullPath}`));
764
1032
  console.log("");
765
- console.log(pc3.blue("Link complete!"));
1033
+ console.log(pc4.blue("Link complete!"));
766
1034
  console.log("");
767
- console.log(pc3.gray("Skill is now available as:"));
768
- console.log(pc3.cyan(` /${skillName}`) + pc3.gray(" (Claude Code)"));
769
- console.log(pc3.cyan(` $${skillName}`) + pc3.gray(" (Codex)"));
770
- console.log(pc3.cyan(` ${skillName}`) + pc3.gray(" (OpenCode)"));
1035
+ console.log(pc4.gray("Skill is now available as:"));
1036
+ console.log(pc4.cyan(` /${skillName}`) + pc4.gray(" (Claude Code)"));
1037
+ console.log(pc4.cyan(` $${skillName}`) + pc4.gray(" (Codex)"));
1038
+ console.log(pc4.cyan(` ${skillName}`) + pc4.gray(" (OpenCode)"));
771
1039
  console.log("");
772
- console.log(pc3.gray(`Development mode: ${getLinkTypeText()} source (bypasses ~/.olore).`));
773
- console.log(pc3.gray("On Unix, changes to source are immediately visible."));
774
- console.log(pc3.gray("On Windows, re-run link after changes."));
1040
+ console.log(pc4.gray(`Development mode: ${getLinkTypeText()} source (bypasses ~/.olore).`));
1041
+ console.log(pc4.gray("Changes to source are immediately visible."));
775
1042
  console.log(
776
- pc3.gray("Use ") + pc3.cyan("olore install") + pc3.gray(" for a stable copy in ~/.olore.")
1043
+ pc4.gray("Use ") + pc4.cyan("olore install") + pc4.gray(" for a stable copy in ~/.olore.")
777
1044
  );
778
1045
  }
779
1046
 
780
1047
  // src/commands/list.ts
781
- import pc4 from "picocolors";
1048
+ import pc5 from "picocolors";
782
1049
  async function list(options) {
783
1050
  const packages = await getInstalledPackages();
784
1051
  if (packages.length === 0) {
785
- console.log(pc4.yellow("\nI find your lack of skills disturbing.\n"));
786
- console.log(pc4.gray("No packages installed."));
787
- console.log(`Run ${pc4.cyan("olore install <package>")} to install documentation.`);
1052
+ console.log(pc5.yellow("\nI find your lack of skills disturbing.\n"));
1053
+ console.log(pc5.gray("No packages installed."));
1054
+ console.log(`Run ${pc5.cyan("olore install <package>")} to install documentation.`);
788
1055
  return;
789
1056
  }
790
1057
  if (options.json) {
791
- console.log(JSON.stringify(packages, null, 2));
1058
+ const { issues: issues2 } = await diagnose();
1059
+ console.log(JSON.stringify({ packages, issueCount: issues2.length }, null, 2));
792
1060
  return;
793
1061
  }
794
- console.log(pc4.bold("\nInstalled packages:\n"));
1062
+ console.log(pc5.bold("\nInstalled packages:\n"));
795
1063
  console.log(
796
- pc4.gray(
1064
+ pc5.gray(
797
1065
  "PACKAGE".padEnd(25) + "VERSION".padEnd(12) + "TYPE".padEnd(10) + "FILES".padStart(8) + "SIZE".padStart(12)
798
1066
  )
799
1067
  );
800
- console.log(pc4.gray("-".repeat(67)));
1068
+ console.log(pc5.gray("-".repeat(67)));
801
1069
  for (const pkg of packages) {
802
- const typeLabel = pkg.installType === "linked" ? pc4.blue("linked") : pkg.installType;
1070
+ const typeLabel = pkg.installType === "linked" ? pc5.blue("linked") : pkg.installType;
803
1071
  console.log(
804
1072
  pkg.name.padEnd(25) + pkg.version.padEnd(12) + typeLabel.padEnd(10) + String(pkg.files).padStart(8) + formatSize(pkg.size).padStart(12)
805
1073
  );
806
1074
  }
807
- console.log(pc4.gray("-".repeat(67)));
808
- console.log(pc4.gray(`Total: ${packages.length} packages`));
1075
+ console.log(pc5.gray("-".repeat(67)));
1076
+ console.log(pc5.gray(`Total: ${packages.length} packages`));
809
1077
  console.log("");
810
- console.log(pc4.gray("Types: installed = in ~/.olore, linked = dev symlink, copied = legacy"));
1078
+ console.log(pc5.gray("Types: installed = in ~/.olore, linked = dev symlink, copied = legacy"));
1079
+ const { issues } = await diagnose();
1080
+ if (issues.length > 0) {
1081
+ console.log("");
1082
+ console.log(
1083
+ pc5.yellow(
1084
+ `\u26A0 ${issues.length} issue${issues.length === 1 ? "" : "s"} detected. Run ${pc5.cyan("olore doctor")} for details.`
1085
+ )
1086
+ );
1087
+ }
811
1088
  }
812
1089
  function formatSize(bytes) {
813
1090
  if (bytes < 1024) return `${bytes} B`;
@@ -816,54 +1093,129 @@ function formatSize(bytes) {
816
1093
  }
817
1094
 
818
1095
  // src/commands/order66.ts
819
- import path7 from "path";
820
- import fs7 from "fs-extra";
1096
+ import path8 from "path";
1097
+ import fs8 from "fs-extra";
821
1098
  import ora3 from "ora";
822
- import pc5 from "picocolors";
1099
+ import pc6 from "picocolors";
823
1100
  async function order66() {
824
- console.log(pc5.red("\n\u26A0\uFE0F Execute Order 66?\n"));
825
- console.log(pc5.yellow("This will remove ALL installed documentation packages."));
1101
+ console.log(pc6.red("\n\u26A0\uFE0F Execute Order 66?\n"));
1102
+ console.log(pc6.yellow("This will remove ALL installed documentation packages."));
826
1103
  const packages = await getInstalledPackages();
827
1104
  if (packages.length === 0) {
828
- console.log(pc5.gray("\nNo packages to remove. The Jedi are already gone."));
1105
+ console.log(pc6.gray("\nNo packages to remove. The Jedi are already gone."));
829
1106
  return;
830
1107
  }
831
- console.log(pc5.gray(`
1108
+ console.log(pc6.gray(`
832
1109
  Packages to be removed: ${packages.length}`));
833
1110
  for (const pkg of packages) {
834
- console.log(pc5.gray(` - ${pkg.name}@${pkg.version}`));
1111
+ console.log(pc6.gray(` - ${pkg.name}@${pkg.version}`));
835
1112
  }
836
- console.log(pc5.red('\n"It will be done, my lord."\n'));
1113
+ console.log(pc6.red('\n"It will be done, my lord."\n'));
837
1114
  const spinner = ora3("Executing Order 66...").start();
838
1115
  let removedCount = 0;
839
1116
  const agentPaths = getAgentPaths();
840
1117
  for (const pkg of packages) {
841
1118
  const skillName = `olore-${pkg.name}-${pkg.version}`;
842
1119
  for (const [, basePath] of Object.entries(agentPaths)) {
843
- const skillPath = path7.join(basePath, skillName);
844
- if (await fs7.pathExists(skillPath)) {
845
- await fs7.remove(skillPath);
1120
+ const skillPath = path8.join(basePath, skillName);
1121
+ if (await fs8.pathExists(skillPath)) {
1122
+ await fs8.remove(skillPath);
846
1123
  }
847
1124
  }
848
1125
  if (pkg.installType === "installed") {
849
1126
  const olorePath = getOlorePackagePath(pkg.name, pkg.version);
850
- if (await fs7.pathExists(olorePath)) {
851
- await fs7.remove(olorePath);
1127
+ if (await fs8.pathExists(olorePath)) {
1128
+ await fs8.remove(olorePath);
852
1129
  }
853
1130
  }
854
1131
  removedCount++;
855
1132
  }
856
1133
  spinner.succeed(`Removed ${removedCount} packages`);
857
- console.log(pc5.gray("\nThe Jedi have been eliminated."));
1134
+ console.log(pc6.gray("\nThe Jedi have been eliminated."));
1135
+ }
1136
+
1137
+ // src/commands/prune.ts
1138
+ import pc7 from "picocolors";
1139
+ async function prune(options) {
1140
+ if (!options.json) {
1141
+ console.log(pc7.bold("\nScanning for issues...\n"));
1142
+ }
1143
+ const { issues } = await diagnose();
1144
+ if (issues.length === 0) {
1145
+ if (options.json) {
1146
+ console.log(JSON.stringify({ removed: [], failed: [] }, null, 2));
1147
+ } else {
1148
+ console.log(pc7.green("Nothing to prune. Everything is clean."));
1149
+ }
1150
+ return;
1151
+ }
1152
+ if (options.dryRun) {
1153
+ if (options.json) {
1154
+ console.log(JSON.stringify({ dryRun: true, wouldRemove: issues }, null, 2));
1155
+ return;
1156
+ }
1157
+ console.log(
1158
+ pc7.yellow(`Would remove ${issues.length} item${issues.length === 1 ? "" : "s"}:
1159
+ `)
1160
+ );
1161
+ for (const issue of issues) {
1162
+ console.log(` ${issueLabel(issue.type)} ${displayPath(issue.path)}`);
1163
+ }
1164
+ console.log(`
1165
+ Run ${pc7.cyan("olore prune")} (without --dry-run) to remove them.`);
1166
+ return;
1167
+ }
1168
+ const result = await pruneIssues(issues);
1169
+ if (options.json) {
1170
+ console.log(JSON.stringify(result, null, 2));
1171
+ return;
1172
+ }
1173
+ if (result.removed.length > 0) {
1174
+ let totalFreed = 0;
1175
+ console.log(`Removed ${result.removed.length} item${result.removed.length === 1 ? "" : "s"}:`);
1176
+ for (const entry of result.removed) {
1177
+ totalFreed += entry.freedBytes;
1178
+ console.log(
1179
+ ` ${pc7.green("\u2713")} ${displayPath(entry.issue.path)} (${issueLabel(entry.issue.type)})`
1180
+ );
1181
+ }
1182
+ if (totalFreed > 0) {
1183
+ console.log(`
1184
+ ${formatBytes(totalFreed)} freed.`);
1185
+ }
1186
+ }
1187
+ if (result.failed.length > 0) {
1188
+ console.log("");
1189
+ console.log(
1190
+ pc7.red(
1191
+ `Failed to remove ${result.failed.length} item${result.failed.length === 1 ? "" : "s"}:`
1192
+ )
1193
+ );
1194
+ for (const entry of result.failed) {
1195
+ console.log(` ${pc7.red("\u2717")} ${displayPath(entry.issue.path)}: ${entry.error}`);
1196
+ }
1197
+ }
1198
+ }
1199
+ function issueLabel(type) {
1200
+ switch (type) {
1201
+ case "dangling-symlink":
1202
+ return "dangling symlink";
1203
+ case "orphaned":
1204
+ return "orphaned package";
1205
+ case "partial-install":
1206
+ return "partial install";
1207
+ default:
1208
+ return type;
1209
+ }
858
1210
  }
859
1211
 
860
1212
  // src/commands/remove.ts
861
- import path8 from "path";
862
- import fs8 from "fs-extra";
1213
+ import path9 from "path";
1214
+ import fs9 from "fs-extra";
863
1215
  import ora4 from "ora";
864
- import pc6 from "picocolors";
1216
+ import pc8 from "picocolors";
865
1217
  async function remove(pkg) {
866
- console.log(pc6.bold(`
1218
+ console.log(pc8.bold(`
867
1219
  Removing ${pkg}...
868
1220
  `));
869
1221
  const packages = await getInstalledPackages();
@@ -879,20 +1231,20 @@ Removing ${pkg}...
879
1231
  return p.name === name;
880
1232
  });
881
1233
  if (matches.length === 0) {
882
- console.error(pc6.red(`Package not found: ${pkg}`));
1234
+ console.error(pc8.red(`Package not found: ${pkg}`));
883
1235
  console.log("\nInstalled packages:");
884
1236
  for (const p of packages) {
885
- console.log(` ${pc6.cyan(p.name)}@${p.version}`);
1237
+ console.log(` ${pc8.cyan(p.name)}@${p.version}`);
886
1238
  }
887
1239
  process.exit(1);
888
1240
  }
889
1241
  if (matches.length > 1 && !version2) {
890
- console.error(pc6.red(`Multiple versions found for ${name}:`));
1242
+ console.error(pc8.red(`Multiple versions found for ${name}:`));
891
1243
  for (const p of matches) {
892
- console.log(` ${pc6.cyan(p.name)}@${p.version} (${p.installType})`);
1244
+ console.log(` ${pc8.cyan(p.name)}@${p.version} (${p.installType})`);
893
1245
  }
894
1246
  console.log(`
895
- Specify version: ${pc6.cyan(`olore remove ${name}@<version>`)}`);
1247
+ Specify version: ${pc8.cyan(`olore remove ${name}@<version>`)}`);
896
1248
  process.exit(1);
897
1249
  }
898
1250
  const found = matches[0];
@@ -901,27 +1253,119 @@ Specify version: ${pc6.cyan(`olore remove ${name}@<version>`)}`);
901
1253
  const removed = [];
902
1254
  const agentPaths = getAgentPaths();
903
1255
  for (const [agent, basePath] of Object.entries(agentPaths)) {
904
- const skillPath = path8.join(basePath, skillName);
905
- if (await fs8.pathExists(skillPath)) {
906
- await fs8.remove(skillPath);
907
- removed.push(`${pc6.green("\u2713")} ${agent}`);
1256
+ const skillPath = path9.join(basePath, skillName);
1257
+ if (await fs9.pathExists(skillPath)) {
1258
+ await fs9.remove(skillPath);
1259
+ removed.push(`${pc8.green("\u2713")} ${agent}`);
908
1260
  }
909
1261
  }
910
1262
  if (found.installType === "installed") {
911
1263
  const olorePath = getOlorePackagePath(found.name, found.version);
912
- if (await fs8.pathExists(olorePath)) {
913
- await fs8.remove(olorePath);
914
- removed.push(`${pc6.green("\u2713")} ~/.olore`);
1264
+ if (await fs9.pathExists(olorePath)) {
1265
+ await fs9.remove(olorePath);
1266
+ removed.push(`${pc8.green("\u2713")} ~/.olore`);
915
1267
  }
916
1268
  }
917
1269
  spinner.stop();
918
1270
  if (removed.length === 0) {
919
- console.log(pc6.yellow("No files found to remove."));
1271
+ console.log(pc8.yellow("No files found to remove."));
920
1272
  } else {
921
1273
  removed.forEach((line) => console.log(` ${line}`));
922
1274
  console.log("");
923
- console.log(pc6.green(`Removed ${found.name}@${found.version}`));
1275
+ console.log(pc8.green(`Removed ${found.name}@${found.version}`));
1276
+ }
1277
+ }
1278
+
1279
+ // src/commands/search.ts
1280
+ import ora5 from "ora";
1281
+ import pc9 from "picocolors";
1282
+ async function search(query, options) {
1283
+ const spinner = ora5("Fetching package registry...").start();
1284
+ let packages;
1285
+ try {
1286
+ const index = await fetchPackageIndex();
1287
+ packages = index.packages;
1288
+ spinner.stop();
1289
+ } catch (error) {
1290
+ spinner.fail("Failed to fetch registry");
1291
+ if (error instanceof RegistryError) {
1292
+ if (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT") {
1293
+ console.log(pc9.red(`
1294
+ Network error: ${error.message}`));
1295
+ console.log(pc9.gray("Please check your internet connection and try again."));
1296
+ } else {
1297
+ console.log(pc9.red(`
1298
+ Error: ${error.message}`));
1299
+ }
1300
+ } else {
1301
+ console.log(pc9.red(`
1302
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1303
+ }
1304
+ process.exit(1);
1305
+ }
1306
+ let entries = Object.entries(packages);
1307
+ if (query) {
1308
+ const q = query.toLowerCase();
1309
+ entries = entries.filter(
1310
+ ([name, info]) => name.toLowerCase().includes(q) || info.description.toLowerCase().includes(q)
1311
+ );
1312
+ }
1313
+ if (entries.length === 0) {
1314
+ if (query) {
1315
+ console.log(pc9.yellow(`
1316
+ No packages matching '${query}'`));
1317
+ } else {
1318
+ console.log(pc9.yellow("\nNo packages available in the registry"));
1319
+ }
1320
+ return;
1321
+ }
1322
+ const installed = await getInstalledPackages();
1323
+ const installedNames = new Set(installed.map((p) => p.name));
1324
+ const installedVersions = /* @__PURE__ */ new Map();
1325
+ for (const pkg of installed) {
1326
+ installedVersions.set(pkg.name, pkg.version);
1327
+ }
1328
+ if (options.json) {
1329
+ const result = {
1330
+ packages: entries.map(([name, info]) => ({
1331
+ name,
1332
+ description: info.description,
1333
+ versions: info.versions,
1334
+ installed: installedNames.has(name),
1335
+ installedVersion: installedVersions.get(name) || null
1336
+ })),
1337
+ ...query ? { query } : {}
1338
+ };
1339
+ console.log(JSON.stringify(result, null, 2));
1340
+ return;
1341
+ }
1342
+ const colName = 20;
1343
+ const colDesc = 44;
1344
+ const colVersions = 12;
1345
+ console.log(pc9.bold("\nAvailable packages:\n"));
1346
+ console.log(
1347
+ pc9.gray(
1348
+ "PACKAGE".padEnd(colName) + "DESCRIPTION".padEnd(colDesc) + "VERSIONS".padEnd(colVersions) + "INSTALLED"
1349
+ )
1350
+ );
1351
+ console.log(pc9.gray("\u2500".repeat(colName + colDesc + colVersions + 12)));
1352
+ for (const [name, info] of entries.sort(([a], [b]) => a.localeCompare(b))) {
1353
+ const desc = truncate(info.description, colDesc - 2);
1354
+ const versions = info.versions.join(", ");
1355
+ const installedVersion = installedVersions.get(name);
1356
+ const status = installedVersion ? pc9.green(`\u2713 ${installedVersion}`) : "";
1357
+ console.log(
1358
+ name.padEnd(colName) + desc.padEnd(colDesc) + versions.padEnd(colVersions) + status
1359
+ );
924
1360
  }
1361
+ console.log(pc9.gray("\u2500".repeat(colName + colDesc + colVersions + 12)));
1362
+ console.log(pc9.gray(`${entries.length} package${entries.length === 1 ? "" : "s"} available`));
1363
+ console.log(`
1364
+ Install with: ${pc9.cyan("olore install <package>")}`);
1365
+ }
1366
+ function truncate(str, maxLen) {
1367
+ if (str.length <= maxLen) return str;
1368
+ return str.slice(0, maxLen - 1) + "\u2026";
925
1369
  }
926
1370
 
927
1371
  // src/cli.ts
@@ -929,12 +1373,12 @@ var require2 = createRequire(import.meta.url);
929
1373
  var { version } = require2("../package.json");
930
1374
  var program = new Command();
931
1375
  program.name("olore").description("Universal documentation for any AI coding agent").version(version).addHelpText("after", `
932
- ${pc7.gray("May the Skill be with you.")}`);
1376
+ ${pc10.gray("May the Skill be with you.")}`);
933
1377
  program.command("init").description("Initialize a documentation package in the current directory").option("-n, --name <name>", "Package name (default: folder name)").option("-v, --version <version>", "Package version (default: latest)").option("-y, --yes", "Skip prompts, use defaults").action(async (options) => {
934
1378
  try {
935
1379
  await init(options);
936
1380
  } catch (error) {
937
- console.error(pc7.red(`Error: ${error.message}`));
1381
+ console.error(pc10.red(`Error: ${error.message}`));
938
1382
  process.exit(1);
939
1383
  }
940
1384
  });
@@ -942,7 +1386,7 @@ program.command("install <package>").alias("i").description("Install a documenta
942
1386
  try {
943
1387
  await install(pkg, options);
944
1388
  } catch (error) {
945
- console.error(pc7.red(`Error: ${error.message}`));
1389
+ console.error(pc10.red(`Error: ${error.message}`));
946
1390
  process.exit(1);
947
1391
  }
948
1392
  });
@@ -950,7 +1394,7 @@ program.command("link <path>").description("Link a local package for development
950
1394
  try {
951
1395
  await link(localPath);
952
1396
  } catch (error) {
953
- console.error(pc7.red(`Error: ${error.message}`));
1397
+ console.error(pc10.red(`Error: ${error.message}`));
954
1398
  process.exit(1);
955
1399
  }
956
1400
  });
@@ -958,7 +1402,15 @@ program.command("list").alias("ls").description("List installed documentation pa
958
1402
  try {
959
1403
  await list(options);
960
1404
  } catch (error) {
961
- console.error(pc7.red(`Error: ${error.message}`));
1405
+ console.error(pc10.red(`Error: ${error.message}`));
1406
+ process.exit(1);
1407
+ }
1408
+ });
1409
+ program.command("search [query]").description("Search available packages in the registry").option("--json", "Output as JSON").action(async (query, options) => {
1410
+ try {
1411
+ await search(query, options);
1412
+ } catch (error) {
1413
+ console.error(pc10.red(`Error: ${error.message}`));
962
1414
  process.exit(1);
963
1415
  }
964
1416
  });
@@ -966,7 +1418,23 @@ program.command("remove <package>").alias("rm").description("Remove an installed
966
1418
  try {
967
1419
  await remove(pkg);
968
1420
  } catch (error) {
969
- console.error(pc7.red(`Error: ${error.message}`));
1421
+ console.error(pc10.red(`Error: ${error.message}`));
1422
+ process.exit(1);
1423
+ }
1424
+ });
1425
+ program.command("doctor").description("Check for issues with installed packages").option("--json", "Output as JSON").action(async (options) => {
1426
+ try {
1427
+ await doctor(options);
1428
+ } catch (error) {
1429
+ console.error(pc10.red(`Error: ${error.message}`));
1430
+ process.exit(1);
1431
+ }
1432
+ });
1433
+ program.command("prune").description("Remove dangling symlinks, orphaned packages, and partial installs").option("--dry-run", "Show what would be removed without acting").option("--json", "Output as JSON").action(async (options) => {
1434
+ try {
1435
+ await prune(options);
1436
+ } catch (error) {
1437
+ console.error(pc10.red(`Error: ${error.message}`));
970
1438
  process.exit(1);
971
1439
  }
972
1440
  });
@@ -974,7 +1442,7 @@ program.command("order66").description(false).action(async () => {
974
1442
  try {
975
1443
  await order66();
976
1444
  } catch (error) {
977
- console.error(pc7.red(`Error: ${error.message}`));
1445
+ console.error(pc10.red(`Error: ${error.message}`));
978
1446
  process.exit(1);
979
1447
  }
980
1448
  });