@aladac/hu 0.1.0-a1

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 (70) hide show
  1. package/.tool-versions +1 -0
  2. package/CLAUDE.md +122 -0
  3. package/HOOKS-DATA-INTEGRATION.md +457 -0
  4. package/SAMPLE.md +378 -0
  5. package/TODO.md +25 -0
  6. package/biome.json +51 -0
  7. package/commands/bootstrap.md +13 -0
  8. package/commands/c.md +1 -0
  9. package/commands/check-name.md +62 -0
  10. package/commands/disk.md +141 -0
  11. package/commands/docs/archive.md +27 -0
  12. package/commands/docs/check-internal.md +53 -0
  13. package/commands/docs/cleanup.md +65 -0
  14. package/commands/docs/consolidate.md +72 -0
  15. package/commands/docs/get.md +101 -0
  16. package/commands/docs/list.md +61 -0
  17. package/commands/docs/sync.md +64 -0
  18. package/commands/docs/update.md +49 -0
  19. package/commands/plans/clear.md +23 -0
  20. package/commands/plans/create.md +71 -0
  21. package/commands/plans/list.md +21 -0
  22. package/commands/plans/sync.md +38 -0
  23. package/commands/reinstall.md +20 -0
  24. package/commands/replicate.md +303 -0
  25. package/commands/warp.md +0 -0
  26. package/doc/README.md +35 -0
  27. package/doc/claude-code/capabilities.md +202 -0
  28. package/doc/claude-code/directory-structure.md +246 -0
  29. package/doc/claude-code/hooks.md +348 -0
  30. package/doc/claude-code/overview.md +109 -0
  31. package/doc/claude-code/plugins.md +273 -0
  32. package/doc/claude-code/sdk-protocols.md +202 -0
  33. package/document-manifest.toml +29 -0
  34. package/justfile +39 -0
  35. package/package.json +33 -0
  36. package/plans/compiled-watching-feather.md +217 -0
  37. package/plans/crispy-crafting-pnueli.md +103 -0
  38. package/plans/greedy-booping-coral.md +146 -0
  39. package/plans/imperative-sleeping-flamingo.md +192 -0
  40. package/plans/jaunty-sprouting-marble.md +171 -0
  41. package/plans/jiggly-discovering-lake.md +68 -0
  42. package/plans/magical-nibbling-spark.md +144 -0
  43. package/plans/mellow-kindling-acorn.md +110 -0
  44. package/plans/recursive-questing-engelbart.md +65 -0
  45. package/plans/serialized-roaming-kernighan.md +227 -0
  46. package/plans/structured-wondering-wirth.md +230 -0
  47. package/plans/vectorized-dreaming-iverson.md +191 -0
  48. package/plans/velvety-enchanting-ocean.md +92 -0
  49. package/plans/wiggly-sparking-pixel.md +48 -0
  50. package/plans/zippy-shimmying-fox.md +188 -0
  51. package/plugins/installed_plugins.json +4 -0
  52. package/sample-hooks.json +298 -0
  53. package/settings.json +24 -0
  54. package/settings.local.json +7 -0
  55. package/src/commands/bump.ts +130 -0
  56. package/src/commands/disk.ts +419 -0
  57. package/src/commands/docs.ts +729 -0
  58. package/src/commands/plans.ts +259 -0
  59. package/src/commands/utils.ts +299 -0
  60. package/src/index.ts +26 -0
  61. package/src/lib/colors.ts +87 -0
  62. package/src/lib/exec.ts +25 -0
  63. package/src/lib/fs.ts +119 -0
  64. package/src/lib/html.ts +205 -0
  65. package/src/lib/spinner.ts +42 -0
  66. package/src/types/index.ts +61 -0
  67. package/tests/lib/colors.test.ts +69 -0
  68. package/tests/lib/fs.test.ts +65 -0
  69. package/tsconfig.json +20 -0
  70. package/vitest.config.ts +15 -0
@@ -0,0 +1,729 @@
1
+ /**
2
+ * Documentation management commands
3
+ */
4
+
5
+ import { execSync } from 'node:child_process';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import { defineCommand } from 'citty';
9
+ import { c, formatSize } from '../lib/colors.ts';
10
+ import {
11
+ GLOBAL_DOCS,
12
+ SYSTEM_DOCS,
13
+ countCheckboxes,
14
+ exists,
15
+ findMarkdownFiles,
16
+ findRepoRoot,
17
+ getStats,
18
+ parseFrontmatter,
19
+ readFile,
20
+ writeFile,
21
+ } from '../lib/fs.ts';
22
+ import type { DocInfo, SystemDocInfo } from '../types/index.ts';
23
+
24
+ // ─────────────────────────────────────────────────────────────
25
+ // List Docs
26
+ // ─────────────────────────────────────────────────────────────
27
+
28
+ function getFileInfo(filePath: string): DocInfo | null {
29
+ const stats = getStats(filePath);
30
+ const content = readFile(filePath);
31
+ if (!stats || !content) return null;
32
+
33
+ const frontmatter = parseFrontmatter(content);
34
+ const lines = content.split('\n').length;
35
+
36
+ // Get title from first # heading
37
+ const titleMatch = content.match(/^#\s+(.+)$/m);
38
+ const title = titleMatch ? titleMatch[1].trim() : path.basename(filePath, '.md');
39
+
40
+ return {
41
+ path: filePath,
42
+ name: path.basename(filePath),
43
+ title,
44
+ size: stats.size,
45
+ sizeHuman: formatSize(stats.size),
46
+ lines,
47
+ modified: stats.mtime.toISOString().split('T')[0],
48
+ source: frontmatter.source || null,
49
+ fetched: frontmatter.fetched || null,
50
+ };
51
+ }
52
+
53
+ function listDocs(location: string, format: string): void {
54
+ const cwd = process.cwd();
55
+ let files: string[] = [];
56
+
57
+ if (location === 'project' || location === 'all') {
58
+ const docDir = path.join(cwd, 'doc');
59
+ files.push(...findMarkdownFiles(docDir).filter((f) => !f.endsWith('README.md')));
60
+ }
61
+
62
+ if (location === 'global' || location === 'all') {
63
+ files.push(...findMarkdownFiles(GLOBAL_DOCS));
64
+ }
65
+
66
+ if (location === 'repo') {
67
+ const repoRoot = findRepoRoot();
68
+ const allMd = findMarkdownFiles(repoRoot);
69
+ files = allMd.filter((f) => {
70
+ const name = path.basename(f);
71
+ const rel = path.relative(repoRoot, f);
72
+ if (SYSTEM_DOCS.includes(name)) return false;
73
+ if (rel.startsWith('.claude/commands')) return false;
74
+ return true;
75
+ });
76
+ }
77
+
78
+ const docs = files.map(getFileInfo).filter((doc): doc is DocInfo => doc !== null);
79
+ docs.sort((a, b) => a.path.localeCompare(b.path));
80
+
81
+ if (format === 'json') {
82
+ console.log(JSON.stringify(docs, null, 2));
83
+ } else if (format === 'simple') {
84
+ for (const doc of docs) {
85
+ console.log(doc.path);
86
+ }
87
+ } else {
88
+ if (docs.length === 0) {
89
+ console.log('No documentation files found.');
90
+ return;
91
+ }
92
+
93
+ console.log('| File | Title | Size | Lines | Modified | Source |');
94
+ console.log('|------|-------|------|-------|----------|--------|');
95
+ for (const doc of docs) {
96
+ const relPath = path.relative(cwd, doc.path);
97
+ const source = doc.source ? `[link](${doc.source})` : '-';
98
+ console.log(
99
+ `| ${relPath} | ${doc.title} | ${doc.sizeHuman} | ${doc.lines} | ${doc.modified} | ${source} |`,
100
+ );
101
+ }
102
+ console.log(`\nTotal: ${docs.length} files`);
103
+ }
104
+ }
105
+
106
+ // ─────────────────────────────────────────────────────────────
107
+ // Check System Docs
108
+ // ─────────────────────────────────────────────────────────────
109
+
110
+ const INTERNAL_DOCS = ['MODELS.md', 'PLAN.md', 'TODO.md', 'TEST.md', 'REFACTOR.md'];
111
+ const STANDARD_DOCS = ['README.md', 'CLAUDE.md', 'LICENSE.md', 'CHANGELOG.md', 'CONTRIBUTING.md'];
112
+
113
+ function getSystemDocInfo(repoRoot: string, filename: string): SystemDocInfo {
114
+ const filePath = path.join(repoRoot, filename);
115
+ const filePathLower = path.join(repoRoot, filename.toLowerCase());
116
+
117
+ let actualPath: string | null = null;
118
+ if (exists(filePath)) actualPath = filePath;
119
+ else if (exists(filePathLower)) actualPath = filePathLower;
120
+
121
+ if (!actualPath) {
122
+ return {
123
+ name: filename,
124
+ found: false,
125
+ path: null,
126
+ size: null,
127
+ sizeHuman: null,
128
+ lines: null,
129
+ modified: null,
130
+ pending: null,
131
+ completed: null,
132
+ };
133
+ }
134
+
135
+ const stats = getStats(actualPath);
136
+ const content = readFile(actualPath);
137
+ if (!stats || !content) {
138
+ return {
139
+ name: filename,
140
+ found: false,
141
+ path: null,
142
+ size: null,
143
+ sizeHuman: null,
144
+ lines: null,
145
+ modified: null,
146
+ pending: null,
147
+ completed: null,
148
+ };
149
+ }
150
+
151
+ const lines = content.split('\n').length;
152
+ const info: SystemDocInfo = {
153
+ name: filename,
154
+ found: true,
155
+ path: actualPath,
156
+ size: stats.size,
157
+ sizeHuman: formatSize(stats.size),
158
+ lines,
159
+ modified: stats.mtime.toISOString().split('T')[0],
160
+ pending: null,
161
+ completed: null,
162
+ };
163
+
164
+ if (['TODO.md', 'PLAN.md'].includes(filename)) {
165
+ const { pending, completed } = countCheckboxes(content);
166
+ info.pending = pending;
167
+ info.completed = completed;
168
+ }
169
+
170
+ return info;
171
+ }
172
+
173
+ function checkSystemDocs(format: string): void {
174
+ const repoRoot = findRepoRoot();
175
+ const internalDocs = INTERNAL_DOCS.map((name) => getSystemDocInfo(repoRoot, name));
176
+ const standardDocs = STANDARD_DOCS.map((name) => getSystemDocInfo(repoRoot, name));
177
+
178
+ const allDocs = [...internalDocs, ...standardDocs];
179
+ const foundCount = allDocs.filter((d) => d.found).length;
180
+ const pendingFiles = allDocs.filter((d) => d.pending !== null && d.pending > 0);
181
+
182
+ if (format === 'json') {
183
+ console.log(
184
+ JSON.stringify(
185
+ {
186
+ repoRoot,
187
+ internal: internalDocs,
188
+ standard: standardDocs,
189
+ summary: {
190
+ found: foundCount,
191
+ total: allDocs.length,
192
+ filesWithPending: pendingFiles.map((d) => d.name),
193
+ },
194
+ },
195
+ null,
196
+ 2,
197
+ ),
198
+ );
199
+ } else {
200
+ console.log('## System Documents Status\n');
201
+ console.log(`Repository: ${repoRoot}\n`);
202
+
203
+ console.log('### Internal Project Files');
204
+ console.log('| File | Status | Size | Lines | Notes |');
205
+ console.log('|------|--------|------|-------|-------|');
206
+ for (const doc of internalDocs) {
207
+ const status = doc.found ? '✓ Found' : '✗ Not found';
208
+ const size = doc.sizeHuman || '-';
209
+ const lines = doc.lines?.toString() || '-';
210
+ let notes = '-';
211
+ if (doc.pending !== null) {
212
+ notes = `${doc.pending} pending, ${doc.completed} done`;
213
+ }
214
+ console.log(`| ${doc.name} | ${status} | ${size} | ${lines} | ${notes} |`);
215
+ }
216
+
217
+ console.log('\n### Standard Repo Files');
218
+ console.log('| File | Status | Size | Lines |');
219
+ console.log('|------|--------|------|-------|');
220
+ for (const doc of standardDocs) {
221
+ const status = doc.found ? '✓ Found' : '✗ Not found';
222
+ const size = doc.sizeHuman || '-';
223
+ const lines = doc.lines?.toString() || '-';
224
+ console.log(`| ${doc.name} | ${status} | ${size} | ${lines} |`);
225
+ }
226
+
227
+ console.log('\n### Summary');
228
+ console.log(`- Total system documents: ${foundCount} of ${allDocs.length}`);
229
+ if (pendingFiles.length > 0) {
230
+ console.log(`- Files with pending items: ${pendingFiles.map((d) => d.name).join(', ')}`);
231
+ }
232
+ }
233
+ }
234
+
235
+ // ─────────────────────────────────────────────────────────────
236
+ // Scan Docs (for sync/update)
237
+ // ─────────────────────────────────────────────────────────────
238
+
239
+ interface DocScanResult {
240
+ path: string;
241
+ relativePath: string;
242
+ source: string | null;
243
+ additionalSources: string[];
244
+ fetched: string | null;
245
+ size: number;
246
+ lines: number;
247
+ title: string;
248
+ location: 'project' | 'global';
249
+ }
250
+
251
+ interface ScanSummary {
252
+ targets: DocScanResult[];
253
+ withSource: number;
254
+ withoutSource: number;
255
+ stale: number; // fetched > 30 days ago
256
+ location: string;
257
+ }
258
+
259
+ /**
260
+ * Parse additional_sources from frontmatter (can be array or single value)
261
+ */
262
+ function parseAdditionalSources(content: string): string[] {
263
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
264
+ if (!match) return [];
265
+
266
+ const fmContent = match[1];
267
+ const sources: string[] = [];
268
+
269
+ // Check for array format
270
+ const arrayMatch = fmContent.match(/additional_sources:\s*\n((?:\s+-\s+.+\n?)+)/);
271
+ if (arrayMatch) {
272
+ const lines = arrayMatch[1].split('\n');
273
+ for (const line of lines) {
274
+ const urlMatch = line.match(/^\s+-\s+(.+)/);
275
+ if (urlMatch) sources.push(urlMatch[1].trim());
276
+ }
277
+ }
278
+
279
+ return sources;
280
+ }
281
+
282
+ /**
283
+ * Check if a date string is stale (> 30 days old)
284
+ */
285
+ function isStale(dateStr: string | null): boolean {
286
+ if (!dateStr) return true;
287
+ const fetched = new Date(dateStr);
288
+ const now = new Date();
289
+ const diffDays = (now.getTime() - fetched.getTime()) / (1000 * 60 * 60 * 24);
290
+ return diffDays > 30;
291
+ }
292
+
293
+ /**
294
+ * Scan a single doc file for sync/update info
295
+ */
296
+ function scanDocFile(filePath: string, location: 'project' | 'global'): DocScanResult | null {
297
+ const content = readFile(filePath);
298
+ const stats = getStats(filePath);
299
+ if (!content || !stats) return null;
300
+
301
+ const frontmatter = parseFrontmatter(content);
302
+ const additionalSources = parseAdditionalSources(content);
303
+
304
+ // Get title from first # heading
305
+ const titleMatch = content.match(/^#\s+(.+)$/m);
306
+ const title = titleMatch ? titleMatch[1].trim() : path.basename(filePath, '.md');
307
+
308
+ const repoRoot = findRepoRoot();
309
+ const relativePath =
310
+ location === 'project'
311
+ ? path.relative(repoRoot, filePath)
312
+ : path.relative(GLOBAL_DOCS, filePath);
313
+
314
+ return {
315
+ path: filePath,
316
+ relativePath,
317
+ source: frontmatter.source || null,
318
+ additionalSources,
319
+ fetched: frontmatter.fetched || null,
320
+ size: stats.size,
321
+ lines: content.split('\n').length,
322
+ title,
323
+ location,
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Identify doc files to scan based on target argument
329
+ */
330
+ function identifyTargets(target: string): { files: string[]; location: string } {
331
+ const cwd = process.cwd();
332
+ const repoRoot = findRepoRoot();
333
+
334
+ if (target === 'global') {
335
+ const files = findMarkdownFiles(GLOBAL_DOCS).filter((f) => !f.endsWith('README.md'));
336
+ return { files, location: 'global' };
337
+ }
338
+
339
+ if (target === 'all') {
340
+ const projectFiles = findMarkdownFiles(path.join(repoRoot, 'doc')).filter(
341
+ (f) => !f.endsWith('README.md'),
342
+ );
343
+ const globalFiles = findMarkdownFiles(GLOBAL_DOCS).filter((f) => !f.endsWith('README.md'));
344
+ return { files: [...projectFiles, ...globalFiles], location: 'all' };
345
+ }
346
+
347
+ // Check if it's a specific file path
348
+ const asPath = path.isAbsolute(target) ? target : path.join(cwd, target);
349
+ if (exists(asPath) && asPath.endsWith('.md')) {
350
+ return { files: [asPath], location: 'file' };
351
+ }
352
+
353
+ // Check if it's a category (subdirectory of doc/)
354
+ const categoryPath = path.join(repoRoot, 'doc', target);
355
+ if (exists(categoryPath) && getStats(categoryPath)?.isDirectory()) {
356
+ const files = findMarkdownFiles(categoryPath).filter((f) => !f.endsWith('README.md'));
357
+ return { files, location: `category:${target}` };
358
+ }
359
+
360
+ // Default: project docs
361
+ const docDir = path.join(repoRoot, 'doc');
362
+ const files = findMarkdownFiles(docDir).filter((f) => !f.endsWith('README.md'));
363
+ return { files, location: 'project' };
364
+ }
365
+
366
+ function scanDocs(target: string, format: string): void {
367
+ const { files, location } = identifyTargets(target);
368
+
369
+ const targets: DocScanResult[] = [];
370
+ for (const file of files) {
371
+ const loc = file.startsWith(GLOBAL_DOCS) ? 'global' : 'project';
372
+ const result = scanDocFile(file, loc);
373
+ if (result) targets.push(result);
374
+ }
375
+
376
+ const withSource = targets.filter((t) => t.source).length;
377
+ const withoutSource = targets.filter((t) => !t.source).length;
378
+ const stale = targets.filter((t) => t.source && isStale(t.fetched)).length;
379
+
380
+ const summary: ScanSummary = {
381
+ targets,
382
+ withSource,
383
+ withoutSource,
384
+ stale,
385
+ location,
386
+ };
387
+
388
+ if (format === 'json') {
389
+ console.log(JSON.stringify(summary, null, 2));
390
+ } else {
391
+ console.log('## Documentation Scan\n');
392
+ console.log(`Location: ${location}`);
393
+ console.log(`Total: ${targets.length} | With source: ${withSource} | Stale: ${stale}\n`);
394
+
395
+ if (targets.length === 0) {
396
+ console.log('No documentation files found.');
397
+ return;
398
+ }
399
+
400
+ console.log('| File | Source | Fetched | Status |');
401
+ console.log('|------|--------|---------|--------|');
402
+ for (const t of targets) {
403
+ const source = t.source ? 'Yes' : 'No';
404
+ const fetched = t.fetched || '-';
405
+ let status = '-';
406
+ if (t.source) {
407
+ status = isStale(t.fetched) ? `${c.yellow}stale${c.reset}` : `${c.green}current${c.reset}`;
408
+ }
409
+ console.log(`| ${t.relativePath} | ${source} | ${fetched} | ${status} |`);
410
+ }
411
+
412
+ if (stale > 0) {
413
+ console.log(`\n${c.yellow}${stale} doc(s) need updating (fetched > 30 days ago)${c.reset}`);
414
+ }
415
+ }
416
+ }
417
+
418
+ // ─────────────────────────────────────────────────────────────
419
+ // Archive Docs
420
+ // ─────────────────────────────────────────────────────────────
421
+
422
+ interface ArchiveResult {
423
+ file: string;
424
+ success: boolean;
425
+ archived?: string;
426
+ deleted?: boolean;
427
+ reason?: string;
428
+ error?: string;
429
+ }
430
+
431
+ /**
432
+ * Get project name from git repo or directory
433
+ */
434
+ function getProjectName(): string {
435
+ const repoRoot = findRepoRoot();
436
+ try {
437
+ const remoteUrl = execSync('git remote get-url origin 2>/dev/null', {
438
+ cwd: repoRoot,
439
+ encoding: 'utf-8',
440
+ }).trim();
441
+ // Extract repo name from URL (handles https and ssh)
442
+ const match = remoteUrl.match(/\/([^/]+?)(\.git)?$/);
443
+ if (match) return match[1];
444
+ } catch {
445
+ // No git remote, use directory name
446
+ }
447
+ return path.basename(repoRoot);
448
+ }
449
+
450
+ /**
451
+ * Generate timestamp in YYYYMMDD-HHMMSS format
452
+ */
453
+ function generateTimestamp(): string {
454
+ const now = new Date();
455
+ const pad = (n: number) => n.toString().padStart(2, '0');
456
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
457
+ }
458
+
459
+ /**
460
+ * Remove completed items from checkbox content
461
+ * Returns null if all items are completed (file should be deleted)
462
+ */
463
+ function removeCompletedItems(content: string): string | null {
464
+ const lines = content.split('\n');
465
+ const filtered = lines.filter((line) => !line.match(/^(\s*)-\s*\[x\]/i));
466
+
467
+ // Check if any pending items remain
468
+ const hasPending = filtered.some((line) => line.match(/^(\s*)-\s*\[ \]/));
469
+ if (!hasPending) return null;
470
+
471
+ return filtered.join('\n');
472
+ }
473
+
474
+ /**
475
+ * Archive a single file
476
+ */
477
+ function archiveFile(
478
+ filePath: string,
479
+ projectName: string,
480
+ timestamp: string,
481
+ dryRun: boolean,
482
+ ): ArchiveResult {
483
+ const filename = path.basename(filePath);
484
+ const repoRoot = findRepoRoot();
485
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(repoRoot, filePath);
486
+
487
+ if (!exists(absPath)) {
488
+ return { file: filename, success: false, error: 'File not found' };
489
+ }
490
+
491
+ const content = readFile(absPath);
492
+ if (!content) {
493
+ return { file: filename, success: false, error: 'Could not read file' };
494
+ }
495
+
496
+ // Special handling for TODO.md and PLAN.md
497
+ const isProgressTracker = ['TODO.md', 'PLAN.md'].includes(filename.toUpperCase());
498
+ let archiveContent = content;
499
+
500
+ if (isProgressTracker) {
501
+ const processed = removeCompletedItems(content);
502
+ if (processed === null) {
503
+ // All items completed - delete without archiving
504
+ if (!dryRun) {
505
+ fs.unlinkSync(absPath);
506
+ try {
507
+ execSync(`git add "${absPath}" && git commit -m "docs: remove completed ${filename}"`, {
508
+ cwd: repoRoot,
509
+ stdio: 'pipe',
510
+ });
511
+ } catch {
512
+ // Not in git or commit failed
513
+ }
514
+ }
515
+ return {
516
+ file: filename,
517
+ success: true,
518
+ deleted: true,
519
+ reason: 'All items completed',
520
+ };
521
+ }
522
+ archiveContent = processed;
523
+ }
524
+
525
+ // Create archive destination
526
+ const archiveDir = path.join(GLOBAL_DOCS, projectName);
527
+ const archiveName = `${timestamp}-${filename}`;
528
+ const archivePath = path.join(archiveDir, archiveName);
529
+
530
+ if (!dryRun) {
531
+ // Ensure archive directory exists
532
+ fs.mkdirSync(archiveDir, { recursive: true });
533
+
534
+ // Write to archive
535
+ writeFile(archivePath, archiveContent);
536
+
537
+ // Delete from repo
538
+ fs.unlinkSync(absPath);
539
+
540
+ // Commit the deletion
541
+ try {
542
+ execSync(`git add "${absPath}" && git commit -m "docs: archive ${filename}"`, {
543
+ cwd: repoRoot,
544
+ stdio: 'pipe',
545
+ });
546
+ } catch {
547
+ // Not in git or commit failed
548
+ }
549
+
550
+ // Update manifest if exists
551
+ const manifestPath = path.join(repoRoot, 'document-manifest.toml');
552
+ if (exists(manifestPath)) {
553
+ const manifest = readFile(manifestPath);
554
+ if (manifest) {
555
+ // Remove entry for this file (simple approach)
556
+ const lines = manifest.split('\n');
557
+ const filtered = lines.filter((line) => !line.includes(filename));
558
+ if (filtered.length !== lines.length) {
559
+ writeFile(manifestPath, filtered.join('\n'));
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ return {
566
+ file: filename,
567
+ success: true,
568
+ archived: archivePath,
569
+ };
570
+ }
571
+
572
+ function archiveDocs(files: string[], dryRun: boolean, format: string): void {
573
+ if (files.length === 0) {
574
+ console.log('No files specified. Usage: hu docs archive <file...>');
575
+ return;
576
+ }
577
+
578
+ const projectName = getProjectName();
579
+ const timestamp = generateTimestamp();
580
+ const results: ArchiveResult[] = [];
581
+
582
+ for (const file of files) {
583
+ const result = archiveFile(file, projectName, timestamp, dryRun);
584
+ results.push(result);
585
+ }
586
+
587
+ if (format === 'json') {
588
+ console.log(JSON.stringify({ projectName, timestamp, dryRun, results }, null, 2));
589
+ } else {
590
+ console.log('## Archive Results\n');
591
+ console.log(`Project: ${projectName}`);
592
+ console.log(`Timestamp: ${timestamp}`);
593
+ if (dryRun) console.log(`Mode: ${c.yellow}DRY RUN${c.reset}\n`);
594
+ else console.log('');
595
+
596
+ for (const r of results) {
597
+ if (r.success) {
598
+ if (r.deleted) {
599
+ console.log(`${c.green}✓${c.reset} ${r.file} - deleted (${r.reason})`);
600
+ } else {
601
+ console.log(`${c.green}✓${c.reset} ${r.file} → ${r.archived}`);
602
+ }
603
+ } else {
604
+ console.log(`${c.red}✗${c.reset} ${r.file} - ${r.error}`);
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ // ─────────────────────────────────────────────────────────────
611
+ // Subcommands
612
+ // ─────────────────────────────────────────────────────────────
613
+
614
+ const listCommand = defineCommand({
615
+ meta: {
616
+ name: 'list',
617
+ description: 'List documentation files with metadata',
618
+ },
619
+ args: {
620
+ location: {
621
+ type: 'positional',
622
+ description: 'Location to scan: project (default), global, all, repo',
623
+ required: false,
624
+ },
625
+ json: {
626
+ type: 'boolean',
627
+ alias: 'j',
628
+ description: 'Output as JSON',
629
+ },
630
+ simple: {
631
+ type: 'boolean',
632
+ alias: 's',
633
+ description: 'Output as simple list',
634
+ },
635
+ },
636
+ run: ({ args }) => {
637
+ const location = (args.location as string) || 'project';
638
+ const format = args.json ? 'json' : args.simple ? 'simple' : 'table';
639
+ listDocs(location, format);
640
+ },
641
+ });
642
+
643
+ const checkCommand = defineCommand({
644
+ meta: {
645
+ name: 'check',
646
+ description: 'Check status of system documents in repo',
647
+ },
648
+ args: {
649
+ json: {
650
+ type: 'boolean',
651
+ alias: 'j',
652
+ description: 'Output as JSON',
653
+ },
654
+ },
655
+ run: ({ args }) => {
656
+ const format = args.json ? 'json' : 'table';
657
+ checkSystemDocs(format);
658
+ },
659
+ });
660
+
661
+ const scanCommand = defineCommand({
662
+ meta: {
663
+ name: 'scan',
664
+ description: 'Scan docs for sync/update (shows sources and staleness)',
665
+ },
666
+ args: {
667
+ target: {
668
+ type: 'positional',
669
+ description: 'Target: file path, category, "global", "all", or empty for project',
670
+ required: false,
671
+ },
672
+ json: {
673
+ type: 'boolean',
674
+ alias: 'j',
675
+ description: 'Output as JSON',
676
+ },
677
+ },
678
+ run: ({ args }) => {
679
+ const target = (args.target as string) || '';
680
+ const format = args.json ? 'json' : 'table';
681
+ scanDocs(target, format);
682
+ },
683
+ });
684
+
685
+ const archiveCommand = defineCommand({
686
+ meta: {
687
+ name: 'archive',
688
+ description: 'Archive docs from repo to global store',
689
+ },
690
+ args: {
691
+ files: {
692
+ type: 'positional',
693
+ description: 'Files to archive',
694
+ required: false,
695
+ },
696
+ dryRun: {
697
+ type: 'boolean',
698
+ alias: 'd',
699
+ description: 'Show what would be done without making changes',
700
+ },
701
+ json: {
702
+ type: 'boolean',
703
+ alias: 'j',
704
+ description: 'Output as JSON',
705
+ },
706
+ },
707
+ run: ({ args }) => {
708
+ const files = (args._ || []) as string[];
709
+ const format = args.json ? 'json' : 'table';
710
+ archiveDocs(files, !!args.dryRun, format);
711
+ },
712
+ });
713
+
714
+ // ─────────────────────────────────────────────────────────────
715
+ // Main Command
716
+ // ─────────────────────────────────────────────────────────────
717
+
718
+ export const docsCommand = defineCommand({
719
+ meta: {
720
+ name: 'docs',
721
+ description: 'Documentation management tools',
722
+ },
723
+ subCommands: {
724
+ list: listCommand,
725
+ check: checkCommand,
726
+ scan: scanCommand,
727
+ archive: archiveCommand,
728
+ },
729
+ });