@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.
- package/.tool-versions +1 -0
- package/CLAUDE.md +122 -0
- package/HOOKS-DATA-INTEGRATION.md +457 -0
- package/SAMPLE.md +378 -0
- package/TODO.md +25 -0
- package/biome.json +51 -0
- package/commands/bootstrap.md +13 -0
- package/commands/c.md +1 -0
- package/commands/check-name.md +62 -0
- package/commands/disk.md +141 -0
- package/commands/docs/archive.md +27 -0
- package/commands/docs/check-internal.md +53 -0
- package/commands/docs/cleanup.md +65 -0
- package/commands/docs/consolidate.md +72 -0
- package/commands/docs/get.md +101 -0
- package/commands/docs/list.md +61 -0
- package/commands/docs/sync.md +64 -0
- package/commands/docs/update.md +49 -0
- package/commands/plans/clear.md +23 -0
- package/commands/plans/create.md +71 -0
- package/commands/plans/list.md +21 -0
- package/commands/plans/sync.md +38 -0
- package/commands/reinstall.md +20 -0
- package/commands/replicate.md +303 -0
- package/commands/warp.md +0 -0
- package/doc/README.md +35 -0
- package/doc/claude-code/capabilities.md +202 -0
- package/doc/claude-code/directory-structure.md +246 -0
- package/doc/claude-code/hooks.md +348 -0
- package/doc/claude-code/overview.md +109 -0
- package/doc/claude-code/plugins.md +273 -0
- package/doc/claude-code/sdk-protocols.md +202 -0
- package/document-manifest.toml +29 -0
- package/justfile +39 -0
- package/package.json +33 -0
- package/plans/compiled-watching-feather.md +217 -0
- package/plans/crispy-crafting-pnueli.md +103 -0
- package/plans/greedy-booping-coral.md +146 -0
- package/plans/imperative-sleeping-flamingo.md +192 -0
- package/plans/jaunty-sprouting-marble.md +171 -0
- package/plans/jiggly-discovering-lake.md +68 -0
- package/plans/magical-nibbling-spark.md +144 -0
- package/plans/mellow-kindling-acorn.md +110 -0
- package/plans/recursive-questing-engelbart.md +65 -0
- package/plans/serialized-roaming-kernighan.md +227 -0
- package/plans/structured-wondering-wirth.md +230 -0
- package/plans/vectorized-dreaming-iverson.md +191 -0
- package/plans/velvety-enchanting-ocean.md +92 -0
- package/plans/wiggly-sparking-pixel.md +48 -0
- package/plans/zippy-shimmying-fox.md +188 -0
- package/plugins/installed_plugins.json +4 -0
- package/sample-hooks.json +298 -0
- package/settings.json +24 -0
- package/settings.local.json +7 -0
- package/src/commands/bump.ts +130 -0
- package/src/commands/disk.ts +419 -0
- package/src/commands/docs.ts +729 -0
- package/src/commands/plans.ts +259 -0
- package/src/commands/utils.ts +299 -0
- package/src/index.ts +26 -0
- package/src/lib/colors.ts +87 -0
- package/src/lib/exec.ts +25 -0
- package/src/lib/fs.ts +119 -0
- package/src/lib/html.ts +205 -0
- package/src/lib/spinner.ts +42 -0
- package/src/types/index.ts +61 -0
- package/tests/lib/colors.test.ts +69 -0
- package/tests/lib/fs.test.ts +65 -0
- package/tsconfig.json +20 -0
- 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
|
+
});
|