@olorehq/olore 0.1.2 → 0.1.3

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 +723 -371
  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 pc9 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
-
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
10
 
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,37 +119,12 @@ 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(() => {
209
- });
210
- }
211
- }
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(() => {
126
+ await fs.remove(tempDir).catch(() => {
233
127
  });
234
- throw error;
235
128
  }
236
129
  }
237
130
  function formatBytes(bytes) {
@@ -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;
@@ -389,14 +286,14 @@ async function getDirectoryStats(dir) {
389
286
  let files = 0;
390
287
  let size = 0;
391
288
  function walk(currentDir) {
392
- const entries = fs4.readdirSync(currentDir, { withFileTypes: true });
289
+ const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
393
290
  for (const entry of entries) {
394
- const fullPath = path4.join(currentDir, entry.name);
291
+ const fullPath = path3.join(currentDir, entry.name);
395
292
  if (entry.isDirectory()) {
396
293
  walk(fullPath);
397
294
  } else if (entry.isFile()) {
398
295
  files++;
399
- size += fs4.statSync(fullPath).size;
296
+ size += fs3.statSync(fullPath).size;
400
297
  }
401
298
  }
402
299
  }
@@ -404,6 +301,416 @@ async function getDirectoryStats(dir) {
404
301
  return { files, size };
405
302
  }
406
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
+ );
693
+ }
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("");
706
+ }
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) {
@@ -468,22 +775,22 @@ async function resolveVersion(name, version2) {
468
775
  // src/commands/install.ts
469
776
  async function installFromLocal(localPath) {
470
777
  const fullPath = expandPath(localPath);
471
- if (!await fs5.pathExists(fullPath)) {
778
+ if (!await fs6.pathExists(fullPath)) {
472
779
  throw new Error(`Path not found: ${fullPath}`);
473
780
  }
474
- const stat = await fs5.stat(fullPath);
781
+ const stat = await fs6.stat(fullPath);
475
782
  if (!stat.isDirectory()) {
476
783
  throw new Error(`Not a directory: ${fullPath}`);
477
784
  }
478
- const lockPath = path5.join(fullPath, "olore-lock.json");
479
- if (!await fs5.pathExists(lockPath)) {
785
+ const lockPath = path6.join(fullPath, "olore-lock.json");
786
+ if (!await fs6.pathExists(lockPath)) {
480
787
  throw new Error(`Missing olore-lock.json in ${fullPath}`);
481
788
  }
482
- const skillPath = path5.join(fullPath, "SKILL.md");
483
- if (!await fs5.pathExists(skillPath)) {
789
+ const skillPath = path6.join(fullPath, "SKILL.md");
790
+ if (!await fs6.pathExists(skillPath)) {
484
791
  throw new Error(`Missing SKILL.md in ${fullPath}. Run /generate-agent-skills first.`);
485
792
  }
486
- const lock = await fs5.readJson(lockPath);
793
+ const lock = await fs6.readJson(lockPath);
487
794
  const packageName = lock.name;
488
795
  const packageVersion = lock.version;
489
796
  if (!packageName) {
@@ -493,72 +800,57 @@ async function installFromLocal(localPath) {
493
800
  throw new Error(`Invalid olore-lock.json: missing "version" field`);
494
801
  }
495
802
  const skillName = `olore-${packageName}-${packageVersion}`;
496
- const skillContent = await fs5.readFile(skillPath, "utf-8");
803
+ const skillContent = await fs6.readFile(skillPath, "utf-8");
497
804
  const nameMatch = skillContent.match(/^name:\s*(.+)$/m);
498
805
  const skillMdName = nameMatch ? nameMatch[1].trim() : null;
499
806
  if (skillMdName !== skillName) {
500
- console.log(pc2.yellow(`
807
+ console.log(pc3.yellow(`
501
808
  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...`));
809
+ console.log(pc3.gray(` Expected: ${skillName}`));
810
+ console.log(pc3.gray(` Found: ${skillMdName || "(none)"}`));
811
+ console.log(pc3.yellow(` Updating SKILL.md to fix...`));
505
812
  const updatedContent = skillMdName ? skillContent.replace(/^name:\s*.+$/m, `name: ${skillName}`) : skillContent.replace(/^---\n/, `---
506
813
  name: ${skillName}
507
814
  `);
508
- await fs5.writeFile(skillPath, updatedContent);
815
+ await fs6.writeFile(skillPath, updatedContent);
509
816
  }
510
- console.log(pc2.bold(`
817
+ console.log(pc3.bold(`
511
818
  Installing ${packageName}@${packageVersion} from local path...
512
819
  `));
513
820
  const agents = detectAgents();
514
821
  if (agents.length === 0) {
515
- console.log(pc2.yellow("No agents detected. Creating directories anyway."));
822
+ console.log(pc3.yellow("No agents detected. Creating directories anyway."));
516
823
  }
517
824
  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}`));
825
+ const olorePath = getOlorePackagePath(packageName, packageVersion);
826
+ const spinner = ora("Copying to ~/.olore...").start();
827
+ await fs6.ensureDir(path6.dirname(olorePath));
828
+ await fs6.remove(olorePath);
829
+ await fs6.copy(fullPath, olorePath);
830
+ spinner.succeed(`Copied to ${pc3.gray(olorePath)}`);
831
+ const linkSpinner = ora("Linking to agent directories...").start();
832
+ const linked = [];
833
+ for (const [agent, skillsDir] of Object.entries(agentPaths)) {
834
+ const targetDir = path6.join(skillsDir, skillName);
835
+ await fs6.ensureDir(skillsDir);
836
+ await fs6.remove(targetDir);
837
+ await linkOrCopy(olorePath, targetDir);
838
+ linked.push(`${pc3.green("\u2713")} ${agent} ${pc3.gray("\u2192")} ${pc3.gray(targetDir)}`);
550
839
  }
840
+ linkSpinner.stop();
841
+ linked.forEach((line) => console.log(` ${line}`));
842
+ console.log(pc3.gray(` \u2514\u2500 all linked to ${olorePath}`));
551
843
  console.log("");
552
- console.log(pc2.green("Installation complete!"));
844
+ console.log(pc3.green("Installation complete!"));
553
845
  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)"));
846
+ console.log(pc3.gray("Skill is now available as:"));
847
+ console.log(pc3.cyan(` /${skillName}`) + pc3.gray(" (Claude Code)"));
848
+ console.log(pc3.cyan(` $${skillName}`) + pc3.gray(" (Codex)"));
849
+ console.log(pc3.cyan(` ${skillName}`) + pc3.gray(" (OpenCode)"));
558
850
  }
559
851
  async function install(pkg, options) {
560
852
  if (options.force) {
561
- console.log(pc2.cyan("\n\u2728 May the Skill be with you.\n"));
853
+ console.log(pc3.cyan("\n\u2728 May the Skill be with you.\n"));
562
854
  }
563
855
  if (isLocalPath(pkg)) {
564
856
  await installFromLocal(pkg);
@@ -579,7 +871,7 @@ function parsePackageSpec(spec) {
579
871
  async function installFromRemote(pkg, optionsVersion) {
580
872
  const { name, version: specVersion } = parsePackageSpec(pkg);
581
873
  const requestedVersion = optionsVersion || specVersion || "latest";
582
- console.log(pc2.bold(`
874
+ console.log(pc3.bold(`
583
875
  Installing ${name}@${requestedVersion} from registry...
584
876
  `));
585
877
  const spinner = ora("Fetching package info...").start();
@@ -591,18 +883,18 @@ Installing ${name}@${requestedVersion} from registry...
591
883
  spinner.fail("Failed to resolve package");
592
884
  if (error instanceof RegistryError) {
593
885
  if (error.code === "NOT_FOUND") {
594
- console.log(pc2.yellow(`
886
+ console.log(pc3.yellow(`
595
887
  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>`));
888
+ console.log(pc3.gray("\nFor local packages, use a path:"));
889
+ console.log(pc3.cyan(` olore install ./vault/packages/${name}/<version>`));
598
890
  console.log("");
599
891
  } else if (error.code === "NETWORK_ERROR" || error.code === "TIMEOUT") {
600
- console.log(pc2.red(`
892
+ console.log(pc3.red(`
601
893
  Network error: ${error.message}`));
602
- console.log(pc2.gray("Please check your internet connection and try again."));
894
+ console.log(pc3.gray("Please check your internet connection and try again."));
603
895
  }
604
896
  } else {
605
- console.log(pc2.red(`
897
+ console.log(pc3.red(`
606
898
  Error: ${error instanceof Error ? error.message : "Unknown error"}`));
607
899
  }
608
900
  process.exit(1);
@@ -610,114 +902,74 @@ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
610
902
  const skillName = `olore-${name}-${versionInfo.version}`;
611
903
  const agents = detectAgents();
612
904
  if (agents.length === 0) {
613
- console.log(pc2.yellow("No agents detected. Creating directories anyway."));
905
+ console.log(pc3.yellow("No agents detected. Creating directories anyway."));
614
906
  }
615
907
  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
- }
908
+ const olorePath = getOlorePackagePath(name, versionInfo.version);
909
+ const downloadSpinner = ora("Downloading package...").start();
910
+ try {
911
+ await downloadAndInstall(versionInfo.downloadUrl, olorePath, versionInfo.integrity);
912
+ downloadSpinner.succeed(`Downloaded to ${pc3.gray(olorePath)}`);
913
+ } catch (error) {
914
+ downloadSpinner.fail("Download failed");
915
+ if (error instanceof DownloadError) {
916
+ if (error.code === "CHECKSUM_MISMATCH") {
917
+ console.log(pc3.red("\nChecksum verification failed!"));
918
+ console.log(pc3.gray("The downloaded package may be corrupted or tampered with."));
632
919
  } 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(`
920
+ console.log(pc3.red(`
668
921
  Download error: ${error.message}`));
669
- }
670
- } else {
671
- console.log(pc2.red(`
672
- Error: ${error instanceof Error ? error.message : "Unknown error"}`));
673
922
  }
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)}`);
923
+ } else {
924
+ console.log(pc3.red(`
925
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
684
926
  }
685
- linkSpinner.stop();
686
- linked.forEach((line) => console.log(` ${line}`));
687
- console.log(pc2.gray(` \u2514\u2500 all symlinked to ${olorePath}`));
927
+ process.exit(1);
688
928
  }
929
+ const linkSpinner = ora("Linking to agent directories...").start();
930
+ const linked = [];
931
+ for (const [agent, skillsDir] of Object.entries(agentPaths)) {
932
+ const targetDir = path6.join(skillsDir, skillName);
933
+ await fs6.ensureDir(skillsDir);
934
+ await fs6.remove(targetDir);
935
+ await linkOrCopy(olorePath, targetDir);
936
+ linked.push(`${pc3.green("\u2713")} ${agent} ${pc3.gray("\u2192")} ${pc3.gray(targetDir)}`);
937
+ }
938
+ linkSpinner.stop();
939
+ linked.forEach((line) => console.log(` ${line}`));
940
+ console.log(pc3.gray(` \u2514\u2500 all linked to ${olorePath}`));
689
941
  console.log("");
690
- console.log(pc2.green("Installation complete!"));
942
+ console.log(pc3.green("Installation complete!"));
691
943
  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)"));
944
+ console.log(pc3.gray("Skill is now available as:"));
945
+ console.log(pc3.cyan(` /${skillName}`) + pc3.gray(" (Claude Code)"));
946
+ console.log(pc3.cyan(` $${skillName}`) + pc3.gray(" (Codex)"));
947
+ console.log(pc3.cyan(` ${skillName}`) + pc3.gray(" (OpenCode)"));
696
948
  }
697
949
 
698
950
  // src/commands/link.ts
699
- import path6 from "path";
700
- import fs6 from "fs-extra";
951
+ import path7 from "path";
952
+ import fs7 from "fs-extra";
701
953
  import ora2 from "ora";
702
- import pc3 from "picocolors";
954
+ import pc4 from "picocolors";
703
955
  async function link(localPath) {
704
956
  const fullPath = expandPath(localPath);
705
- if (!await fs6.pathExists(fullPath)) {
957
+ if (!await fs7.pathExists(fullPath)) {
706
958
  throw new Error(`Path not found: ${fullPath}`);
707
959
  }
708
- const stat = await fs6.stat(fullPath);
960
+ const stat = await fs7.stat(fullPath);
709
961
  if (!stat.isDirectory()) {
710
962
  throw new Error(`Not a directory: ${fullPath}`);
711
963
  }
712
- const lockPath = path6.join(fullPath, "olore-lock.json");
713
- if (!await fs6.pathExists(lockPath)) {
964
+ const lockPath = path7.join(fullPath, "olore-lock.json");
965
+ if (!await fs7.pathExists(lockPath)) {
714
966
  throw new Error(`Missing olore-lock.json in ${fullPath}`);
715
967
  }
716
- const skillPath = path6.join(fullPath, "SKILL.md");
717
- if (!await fs6.pathExists(skillPath)) {
968
+ const skillPath = path7.join(fullPath, "SKILL.md");
969
+ if (!await fs7.pathExists(skillPath)) {
718
970
  throw new Error(`Missing SKILL.md in ${fullPath}. Run /build-docs first.`);
719
971
  }
720
- const lock = await fs6.readJson(lockPath);
972
+ const lock = await fs7.readJson(lockPath);
721
973
  const packageName = lock.name;
722
974
  const packageVersion = lock.version;
723
975
  if (!packageName) {
@@ -727,87 +979,96 @@ async function link(localPath) {
727
979
  throw new Error(`Invalid olore-lock.json: missing "version" field`);
728
980
  }
729
981
  const skillName = `olore-${packageName}-${packageVersion}`;
730
- const skillContent = await fs6.readFile(skillPath, "utf-8");
982
+ const skillContent = await fs7.readFile(skillPath, "utf-8");
731
983
  const nameMatch = skillContent.match(/^name:\s*(.+)$/m);
732
984
  const skillMdName = nameMatch ? nameMatch[1].trim() : null;
733
985
  if (skillMdName !== skillName) {
734
- console.log(pc3.yellow(`
986
+ console.log(pc4.yellow(`
735
987
  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...`));
988
+ console.log(pc4.gray(` Expected: ${skillName}`));
989
+ console.log(pc4.gray(` Found: ${skillMdName || "(none)"}`));
990
+ console.log(pc4.yellow(` Updating SKILL.md to fix...`));
739
991
  const updatedContent = skillMdName ? skillContent.replace(/^name:\s*.+$/m, `name: ${skillName}`) : skillContent.replace(/^---\n/, `---
740
992
  name: ${skillName}
741
993
  `);
742
- await fs6.writeFile(skillPath, updatedContent);
994
+ await fs7.writeFile(skillPath, updatedContent);
743
995
  }
744
- console.log(pc3.bold(`
996
+ console.log(pc4.bold(`
745
997
  Linking ${packageName}@${packageVersion}...
746
998
  `));
747
999
  const agents = detectAgents();
748
1000
  if (agents.length === 0) {
749
- console.log(pc3.yellow("No agents detected. Creating directories anyway."));
1001
+ console.log(pc4.yellow("No agents detected. Creating directories anyway."));
750
1002
  }
751
1003
  const agentPaths = getAgentPaths();
752
1004
  const spinner = ora2(`${getLinkActionText()}...`).start();
753
1005
  const linked = [];
754
1006
  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);
1007
+ const targetDir = path7.join(skillsDir, skillName);
1008
+ await fs7.ensureDir(skillsDir);
1009
+ await fs7.remove(targetDir);
758
1010
  await linkOrCopy(fullPath, targetDir);
759
- linked.push(`${pc3.blue("\u26D3")} ${agent} ${pc3.gray("\u2192")} ${pc3.gray(targetDir)}`);
1011
+ linked.push(`${pc4.blue("\u26D3")} ${agent} ${pc4.gray("\u2192")} ${pc4.gray(targetDir)}`);
760
1012
  }
761
1013
  spinner.stop();
762
1014
  linked.forEach((line) => console.log(` ${line}`));
763
- console.log(pc3.gray(` \u2514\u2500 ${getLinkTypeText()} ${fullPath}`));
1015
+ console.log(pc4.gray(` \u2514\u2500 ${getLinkTypeText()} ${fullPath}`));
764
1016
  console.log("");
765
- console.log(pc3.blue("Link complete!"));
1017
+ console.log(pc4.blue("Link complete!"));
766
1018
  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)"));
1019
+ console.log(pc4.gray("Skill is now available as:"));
1020
+ console.log(pc4.cyan(` /${skillName}`) + pc4.gray(" (Claude Code)"));
1021
+ console.log(pc4.cyan(` $${skillName}`) + pc4.gray(" (Codex)"));
1022
+ console.log(pc4.cyan(` ${skillName}`) + pc4.gray(" (OpenCode)"));
771
1023
  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."));
1024
+ console.log(pc4.gray(`Development mode: ${getLinkTypeText()} source (bypasses ~/.olore).`));
1025
+ console.log(pc4.gray("Changes to source are immediately visible."));
775
1026
  console.log(
776
- pc3.gray("Use ") + pc3.cyan("olore install") + pc3.gray(" for a stable copy in ~/.olore.")
1027
+ pc4.gray("Use ") + pc4.cyan("olore install") + pc4.gray(" for a stable copy in ~/.olore.")
777
1028
  );
778
1029
  }
779
1030
 
780
1031
  // src/commands/list.ts
781
- import pc4 from "picocolors";
1032
+ import pc5 from "picocolors";
782
1033
  async function list(options) {
783
1034
  const packages = await getInstalledPackages();
784
1035
  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.`);
1036
+ console.log(pc5.yellow("\nI find your lack of skills disturbing.\n"));
1037
+ console.log(pc5.gray("No packages installed."));
1038
+ console.log(`Run ${pc5.cyan("olore install <package>")} to install documentation.`);
788
1039
  return;
789
1040
  }
790
1041
  if (options.json) {
791
- console.log(JSON.stringify(packages, null, 2));
1042
+ const { issues: issues2 } = await diagnose();
1043
+ console.log(JSON.stringify({ packages, issueCount: issues2.length }, null, 2));
792
1044
  return;
793
1045
  }
794
- console.log(pc4.bold("\nInstalled packages:\n"));
1046
+ console.log(pc5.bold("\nInstalled packages:\n"));
795
1047
  console.log(
796
- pc4.gray(
1048
+ pc5.gray(
797
1049
  "PACKAGE".padEnd(25) + "VERSION".padEnd(12) + "TYPE".padEnd(10) + "FILES".padStart(8) + "SIZE".padStart(12)
798
1050
  )
799
1051
  );
800
- console.log(pc4.gray("-".repeat(67)));
1052
+ console.log(pc5.gray("-".repeat(67)));
801
1053
  for (const pkg of packages) {
802
- const typeLabel = pkg.installType === "linked" ? pc4.blue("linked") : pkg.installType;
1054
+ const typeLabel = pkg.installType === "linked" ? pc5.blue("linked") : pkg.installType;
803
1055
  console.log(
804
1056
  pkg.name.padEnd(25) + pkg.version.padEnd(12) + typeLabel.padEnd(10) + String(pkg.files).padStart(8) + formatSize(pkg.size).padStart(12)
805
1057
  );
806
1058
  }
807
- console.log(pc4.gray("-".repeat(67)));
808
- console.log(pc4.gray(`Total: ${packages.length} packages`));
1059
+ console.log(pc5.gray("-".repeat(67)));
1060
+ console.log(pc5.gray(`Total: ${packages.length} packages`));
809
1061
  console.log("");
810
- console.log(pc4.gray("Types: installed = in ~/.olore, linked = dev symlink, copied = legacy"));
1062
+ console.log(pc5.gray("Types: installed = in ~/.olore, linked = dev symlink, copied = legacy"));
1063
+ const { issues } = await diagnose();
1064
+ if (issues.length > 0) {
1065
+ console.log("");
1066
+ console.log(
1067
+ pc5.yellow(
1068
+ `\u26A0 ${issues.length} issue${issues.length === 1 ? "" : "s"} detected. Run ${pc5.cyan("olore doctor")} for details.`
1069
+ )
1070
+ );
1071
+ }
811
1072
  }
812
1073
  function formatSize(bytes) {
813
1074
  if (bytes < 1024) return `${bytes} B`;
@@ -816,54 +1077,129 @@ function formatSize(bytes) {
816
1077
  }
817
1078
 
818
1079
  // src/commands/order66.ts
819
- import path7 from "path";
820
- import fs7 from "fs-extra";
1080
+ import path8 from "path";
1081
+ import fs8 from "fs-extra";
821
1082
  import ora3 from "ora";
822
- import pc5 from "picocolors";
1083
+ import pc6 from "picocolors";
823
1084
  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."));
1085
+ console.log(pc6.red("\n\u26A0\uFE0F Execute Order 66?\n"));
1086
+ console.log(pc6.yellow("This will remove ALL installed documentation packages."));
826
1087
  const packages = await getInstalledPackages();
827
1088
  if (packages.length === 0) {
828
- console.log(pc5.gray("\nNo packages to remove. The Jedi are already gone."));
1089
+ console.log(pc6.gray("\nNo packages to remove. The Jedi are already gone."));
829
1090
  return;
830
1091
  }
831
- console.log(pc5.gray(`
1092
+ console.log(pc6.gray(`
832
1093
  Packages to be removed: ${packages.length}`));
833
1094
  for (const pkg of packages) {
834
- console.log(pc5.gray(` - ${pkg.name}@${pkg.version}`));
1095
+ console.log(pc6.gray(` - ${pkg.name}@${pkg.version}`));
835
1096
  }
836
- console.log(pc5.red('\n"It will be done, my lord."\n'));
1097
+ console.log(pc6.red('\n"It will be done, my lord."\n'));
837
1098
  const spinner = ora3("Executing Order 66...").start();
838
1099
  let removedCount = 0;
839
1100
  const agentPaths = getAgentPaths();
840
1101
  for (const pkg of packages) {
841
1102
  const skillName = `olore-${pkg.name}-${pkg.version}`;
842
1103
  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);
1104
+ const skillPath = path8.join(basePath, skillName);
1105
+ if (await fs8.pathExists(skillPath)) {
1106
+ await fs8.remove(skillPath);
846
1107
  }
847
1108
  }
848
1109
  if (pkg.installType === "installed") {
849
1110
  const olorePath = getOlorePackagePath(pkg.name, pkg.version);
850
- if (await fs7.pathExists(olorePath)) {
851
- await fs7.remove(olorePath);
1111
+ if (await fs8.pathExists(olorePath)) {
1112
+ await fs8.remove(olorePath);
852
1113
  }
853
1114
  }
854
1115
  removedCount++;
855
1116
  }
856
1117
  spinner.succeed(`Removed ${removedCount} packages`);
857
- console.log(pc5.gray("\nThe Jedi have been eliminated."));
1118
+ console.log(pc6.gray("\nThe Jedi have been eliminated."));
1119
+ }
1120
+
1121
+ // src/commands/prune.ts
1122
+ import pc7 from "picocolors";
1123
+ async function prune(options) {
1124
+ if (!options.json) {
1125
+ console.log(pc7.bold("\nScanning for issues...\n"));
1126
+ }
1127
+ const { issues } = await diagnose();
1128
+ if (issues.length === 0) {
1129
+ if (options.json) {
1130
+ console.log(JSON.stringify({ removed: [], failed: [] }, null, 2));
1131
+ } else {
1132
+ console.log(pc7.green("Nothing to prune. Everything is clean."));
1133
+ }
1134
+ return;
1135
+ }
1136
+ if (options.dryRun) {
1137
+ if (options.json) {
1138
+ console.log(JSON.stringify({ dryRun: true, wouldRemove: issues }, null, 2));
1139
+ return;
1140
+ }
1141
+ console.log(
1142
+ pc7.yellow(`Would remove ${issues.length} item${issues.length === 1 ? "" : "s"}:
1143
+ `)
1144
+ );
1145
+ for (const issue of issues) {
1146
+ console.log(` ${issueLabel(issue.type)} ${displayPath(issue.path)}`);
1147
+ }
1148
+ console.log(`
1149
+ Run ${pc7.cyan("olore prune")} (without --dry-run) to remove them.`);
1150
+ return;
1151
+ }
1152
+ const result = await pruneIssues(issues);
1153
+ if (options.json) {
1154
+ console.log(JSON.stringify(result, null, 2));
1155
+ return;
1156
+ }
1157
+ if (result.removed.length > 0) {
1158
+ let totalFreed = 0;
1159
+ console.log(`Removed ${result.removed.length} item${result.removed.length === 1 ? "" : "s"}:`);
1160
+ for (const entry of result.removed) {
1161
+ totalFreed += entry.freedBytes;
1162
+ console.log(
1163
+ ` ${pc7.green("\u2713")} ${displayPath(entry.issue.path)} (${issueLabel(entry.issue.type)})`
1164
+ );
1165
+ }
1166
+ if (totalFreed > 0) {
1167
+ console.log(`
1168
+ ${formatBytes(totalFreed)} freed.`);
1169
+ }
1170
+ }
1171
+ if (result.failed.length > 0) {
1172
+ console.log("");
1173
+ console.log(
1174
+ pc7.red(
1175
+ `Failed to remove ${result.failed.length} item${result.failed.length === 1 ? "" : "s"}:`
1176
+ )
1177
+ );
1178
+ for (const entry of result.failed) {
1179
+ console.log(` ${pc7.red("\u2717")} ${displayPath(entry.issue.path)}: ${entry.error}`);
1180
+ }
1181
+ }
1182
+ }
1183
+ function issueLabel(type) {
1184
+ switch (type) {
1185
+ case "dangling-symlink":
1186
+ return "dangling symlink";
1187
+ case "orphaned":
1188
+ return "orphaned package";
1189
+ case "partial-install":
1190
+ return "partial install";
1191
+ default:
1192
+ return type;
1193
+ }
858
1194
  }
859
1195
 
860
1196
  // src/commands/remove.ts
861
- import path8 from "path";
862
- import fs8 from "fs-extra";
1197
+ import path9 from "path";
1198
+ import fs9 from "fs-extra";
863
1199
  import ora4 from "ora";
864
- import pc6 from "picocolors";
1200
+ import pc8 from "picocolors";
865
1201
  async function remove(pkg) {
866
- console.log(pc6.bold(`
1202
+ console.log(pc8.bold(`
867
1203
  Removing ${pkg}...
868
1204
  `));
869
1205
  const packages = await getInstalledPackages();
@@ -879,20 +1215,20 @@ Removing ${pkg}...
879
1215
  return p.name === name;
880
1216
  });
881
1217
  if (matches.length === 0) {
882
- console.error(pc6.red(`Package not found: ${pkg}`));
1218
+ console.error(pc8.red(`Package not found: ${pkg}`));
883
1219
  console.log("\nInstalled packages:");
884
1220
  for (const p of packages) {
885
- console.log(` ${pc6.cyan(p.name)}@${p.version}`);
1221
+ console.log(` ${pc8.cyan(p.name)}@${p.version}`);
886
1222
  }
887
1223
  process.exit(1);
888
1224
  }
889
1225
  if (matches.length > 1 && !version2) {
890
- console.error(pc6.red(`Multiple versions found for ${name}:`));
1226
+ console.error(pc8.red(`Multiple versions found for ${name}:`));
891
1227
  for (const p of matches) {
892
- console.log(` ${pc6.cyan(p.name)}@${p.version} (${p.installType})`);
1228
+ console.log(` ${pc8.cyan(p.name)}@${p.version} (${p.installType})`);
893
1229
  }
894
1230
  console.log(`
895
- Specify version: ${pc6.cyan(`olore remove ${name}@<version>`)}`);
1231
+ Specify version: ${pc8.cyan(`olore remove ${name}@<version>`)}`);
896
1232
  process.exit(1);
897
1233
  }
898
1234
  const found = matches[0];
@@ -901,26 +1237,26 @@ Specify version: ${pc6.cyan(`olore remove ${name}@<version>`)}`);
901
1237
  const removed = [];
902
1238
  const agentPaths = getAgentPaths();
903
1239
  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}`);
1240
+ const skillPath = path9.join(basePath, skillName);
1241
+ if (await fs9.pathExists(skillPath)) {
1242
+ await fs9.remove(skillPath);
1243
+ removed.push(`${pc8.green("\u2713")} ${agent}`);
908
1244
  }
909
1245
  }
910
1246
  if (found.installType === "installed") {
911
1247
  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`);
1248
+ if (await fs9.pathExists(olorePath)) {
1249
+ await fs9.remove(olorePath);
1250
+ removed.push(`${pc8.green("\u2713")} ~/.olore`);
915
1251
  }
916
1252
  }
917
1253
  spinner.stop();
918
1254
  if (removed.length === 0) {
919
- console.log(pc6.yellow("No files found to remove."));
1255
+ console.log(pc8.yellow("No files found to remove."));
920
1256
  } else {
921
1257
  removed.forEach((line) => console.log(` ${line}`));
922
1258
  console.log("");
923
- console.log(pc6.green(`Removed ${found.name}@${found.version}`));
1259
+ console.log(pc8.green(`Removed ${found.name}@${found.version}`));
924
1260
  }
925
1261
  }
926
1262
 
@@ -929,12 +1265,12 @@ var require2 = createRequire(import.meta.url);
929
1265
  var { version } = require2("../package.json");
930
1266
  var program = new Command();
931
1267
  program.name("olore").description("Universal documentation for any AI coding agent").version(version).addHelpText("after", `
932
- ${pc7.gray("May the Skill be with you.")}`);
1268
+ ${pc9.gray("May the Skill be with you.")}`);
933
1269
  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
1270
  try {
935
1271
  await init(options);
936
1272
  } catch (error) {
937
- console.error(pc7.red(`Error: ${error.message}`));
1273
+ console.error(pc9.red(`Error: ${error.message}`));
938
1274
  process.exit(1);
939
1275
  }
940
1276
  });
@@ -942,7 +1278,7 @@ program.command("install <package>").alias("i").description("Install a documenta
942
1278
  try {
943
1279
  await install(pkg, options);
944
1280
  } catch (error) {
945
- console.error(pc7.red(`Error: ${error.message}`));
1281
+ console.error(pc9.red(`Error: ${error.message}`));
946
1282
  process.exit(1);
947
1283
  }
948
1284
  });
@@ -950,7 +1286,7 @@ program.command("link <path>").description("Link a local package for development
950
1286
  try {
951
1287
  await link(localPath);
952
1288
  } catch (error) {
953
- console.error(pc7.red(`Error: ${error.message}`));
1289
+ console.error(pc9.red(`Error: ${error.message}`));
954
1290
  process.exit(1);
955
1291
  }
956
1292
  });
@@ -958,7 +1294,7 @@ program.command("list").alias("ls").description("List installed documentation pa
958
1294
  try {
959
1295
  await list(options);
960
1296
  } catch (error) {
961
- console.error(pc7.red(`Error: ${error.message}`));
1297
+ console.error(pc9.red(`Error: ${error.message}`));
962
1298
  process.exit(1);
963
1299
  }
964
1300
  });
@@ -966,7 +1302,23 @@ program.command("remove <package>").alias("rm").description("Remove an installed
966
1302
  try {
967
1303
  await remove(pkg);
968
1304
  } catch (error) {
969
- console.error(pc7.red(`Error: ${error.message}`));
1305
+ console.error(pc9.red(`Error: ${error.message}`));
1306
+ process.exit(1);
1307
+ }
1308
+ });
1309
+ program.command("doctor").description("Check for issues with installed packages").option("--json", "Output as JSON").action(async (options) => {
1310
+ try {
1311
+ await doctor(options);
1312
+ } catch (error) {
1313
+ console.error(pc9.red(`Error: ${error.message}`));
1314
+ process.exit(1);
1315
+ }
1316
+ });
1317
+ 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) => {
1318
+ try {
1319
+ await prune(options);
1320
+ } catch (error) {
1321
+ console.error(pc9.red(`Error: ${error.message}`));
970
1322
  process.exit(1);
971
1323
  }
972
1324
  });
@@ -974,7 +1326,7 @@ program.command("order66").description(false).action(async () => {
974
1326
  try {
975
1327
  await order66();
976
1328
  } catch (error) {
977
- console.error(pc7.red(`Error: ${error.message}`));
1329
+ console.error(pc9.red(`Error: ${error.message}`));
978
1330
  process.exit(1);
979
1331
  }
980
1332
  });