dotmd-cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dotmd.mjs +53 -5
- package/dotmd.config.example.mjs +1 -1
- package/package.json +1 -1
- package/src/completions.mjs +4 -1
- package/src/fix-refs.mjs +113 -0
- package/src/lifecycle.mjs +128 -36
- package/src/render.mjs +5 -4
package/bin/dotmd.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { runDiff } from '../src/diff.mjs';
|
|
|
17
17
|
import { runLint } from '../src/lint.mjs';
|
|
18
18
|
import { runRename } from '../src/rename.mjs';
|
|
19
19
|
import { runMigrate } from '../src/migrate.mjs';
|
|
20
|
+
import { runFixRefs, fixBrokenRefs } from '../src/fix-refs.mjs';
|
|
20
21
|
import { die, warn } from '../src/util.mjs';
|
|
21
22
|
|
|
22
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -29,15 +30,16 @@ const HELP = {
|
|
|
29
30
|
Commands:
|
|
30
31
|
list [--verbose] List docs grouped by status (default)
|
|
31
32
|
json Full index as JSON
|
|
32
|
-
check
|
|
33
|
+
check [flags] Validate frontmatter and references
|
|
33
34
|
coverage [--json] Metadata coverage report
|
|
34
35
|
context Compact briefing (LLM-oriented)
|
|
35
36
|
focus [status] Detailed view for one status group
|
|
36
37
|
query [filters] Filtered search
|
|
37
38
|
index [--write] Generate/update docs.md index block
|
|
38
39
|
status <file> <status> Transition document status
|
|
39
|
-
archive <file> Archive (status + move +
|
|
40
|
+
archive <file> Archive (status + move + update refs)
|
|
40
41
|
touch <file> Bump updated date
|
|
42
|
+
fix-refs Auto-fix broken reference paths
|
|
41
43
|
lint [--fix] Check and auto-fix frontmatter issues
|
|
42
44
|
rename <old> <new> Rename doc and update references
|
|
43
45
|
migrate <f> <old> <new> Batch update a frontmatter field
|
|
@@ -82,10 +84,33 @@ regenerates the index (if configured).
|
|
|
82
84
|
|
|
83
85
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
84
86
|
|
|
87
|
+
check: `dotmd check — validate frontmatter and references
|
|
88
|
+
|
|
89
|
+
Options:
|
|
90
|
+
--errors-only Show only errors, suppress warnings
|
|
91
|
+
--fix Auto-fix broken references and regenerate index`,
|
|
92
|
+
|
|
85
93
|
archive: `dotmd archive <file> — archive a document
|
|
86
94
|
|
|
87
|
-
Sets status to 'archived', moves to the archive directory,
|
|
88
|
-
|
|
95
|
+
Sets status to 'archived', moves to the archive directory, auto-updates
|
|
96
|
+
references in other docs, and regenerates the index.
|
|
97
|
+
|
|
98
|
+
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
99
|
+
|
|
100
|
+
'fix-refs': `dotmd fix-refs — auto-fix broken reference paths
|
|
101
|
+
|
|
102
|
+
Scans all docs for reference fields that point to non-existent files,
|
|
103
|
+
then attempts to resolve them by matching the basename against all known
|
|
104
|
+
docs. Fixes are applied by rewriting the frontmatter path.
|
|
105
|
+
|
|
106
|
+
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
107
|
+
|
|
108
|
+
touch: `dotmd touch <file> — bump updated date
|
|
109
|
+
dotmd touch --git — bulk-sync dates from git history
|
|
110
|
+
|
|
111
|
+
Without --git, updates a single file's frontmatter updated date to today.
|
|
112
|
+
With --git, scans all docs (or a specific file) and syncs their updated
|
|
113
|
+
date to match the last git commit date, fixing date drift warnings.
|
|
89
114
|
|
|
90
115
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
91
116
|
|
|
@@ -243,6 +268,7 @@ async function main() {
|
|
|
243
268
|
if (command === 'lint') { runLint(restArgs, config, { dryRun }); return; }
|
|
244
269
|
if (command === 'rename') { runRename(restArgs, config, { dryRun }); return; }
|
|
245
270
|
if (command === 'migrate') { runMigrate(restArgs, config, { dryRun }); return; }
|
|
271
|
+
if (command === 'fix-refs') { runFixRefs(restArgs, config, { dryRun }); return; }
|
|
246
272
|
|
|
247
273
|
const index = buildIndex(config);
|
|
248
274
|
|
|
@@ -265,7 +291,29 @@ async function main() {
|
|
|
265
291
|
}
|
|
266
292
|
|
|
267
293
|
if (command === 'check') {
|
|
268
|
-
|
|
294
|
+
const fix = args.includes('--fix');
|
|
295
|
+
const errorsOnly = args.includes('--errors-only');
|
|
296
|
+
|
|
297
|
+
if (fix) {
|
|
298
|
+
// Auto-fix: broken refs, then lint, then rebuild index
|
|
299
|
+
const refResult = fixBrokenRefs(config, { dryRun, quiet: false });
|
|
300
|
+
if (!dryRun) {
|
|
301
|
+
runLint(['--fix'], config, { dryRun });
|
|
302
|
+
}
|
|
303
|
+
if (!dryRun && config.indexPath) {
|
|
304
|
+
const { renderIndexFile: rif, writeIndex: wi } = await import('../src/index-file.mjs');
|
|
305
|
+
const freshIndex = buildIndex(config);
|
|
306
|
+
wi(rif(freshIndex, config), config);
|
|
307
|
+
process.stdout.write('Index regenerated.\n');
|
|
308
|
+
}
|
|
309
|
+
// Show remaining issues
|
|
310
|
+
const freshIndex = buildIndex(config);
|
|
311
|
+
process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly }));
|
|
312
|
+
if (freshIndex.errors.length > 0) process.exitCode = 1;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
process.stdout.write(renderCheck(index, config, { errorsOnly }));
|
|
269
317
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
270
318
|
return;
|
|
271
319
|
}
|
package/dotmd.config.example.mjs
CHANGED
|
@@ -35,7 +35,7 @@ export const lifecycle = {
|
|
|
35
35
|
|
|
36
36
|
// Taxonomy validation — set fields to null to skip validation
|
|
37
37
|
export const taxonomy = {
|
|
38
|
-
surfaces: ['frontend', 'backend', '
|
|
38
|
+
surfaces: ['web', 'ios', 'android', 'mobile', 'full-stack', 'frontend', 'backend', 'api', 'docs', 'ops', 'platform', 'infra', 'design'],
|
|
39
39
|
moduleRequiredFor: ['active', 'ready', 'planned', 'blocked'],
|
|
40
40
|
};
|
|
41
41
|
|
package/package.json
CHANGED
package/src/completions.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { die } from './util.mjs';
|
|
|
3
3
|
const COMMANDS = [
|
|
4
4
|
'list', 'json', 'check', 'coverage', 'context', 'focus', 'query',
|
|
5
5
|
'index', 'status', 'archive', 'touch', 'lint', 'rename', 'migrate',
|
|
6
|
-
'watch', 'diff', 'init', 'new', 'completions',
|
|
6
|
+
'fix-refs', 'watch', 'diff', 'init', 'new', 'completions',
|
|
7
7
|
];
|
|
8
8
|
|
|
9
9
|
const GLOBAL_FLAGS = ['--config', '--dry-run', '--verbose', '--help', '--version'];
|
|
@@ -17,9 +17,12 @@ const COMMAND_FLAGS = {
|
|
|
17
17
|
coverage: ['--json'],
|
|
18
18
|
new: ['--status', '--title'],
|
|
19
19
|
diff: ['--stat', '--since', '--summarize', '--model'],
|
|
20
|
+
check: ['--errors-only', '--fix'],
|
|
20
21
|
lint: ['--fix'],
|
|
21
22
|
rename: [],
|
|
22
23
|
migrate: [],
|
|
24
|
+
'fix-refs': [],
|
|
25
|
+
touch: ['--git'],
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
function bashCompletion() {
|
package/src/fix-refs.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { extractFrontmatter, replaceFrontmatter } from './frontmatter.mjs';
|
|
4
|
+
import { toRepoPath, warn } from './util.mjs';
|
|
5
|
+
import { buildIndex, collectDocFiles } from './index.mjs';
|
|
6
|
+
import { green, dim, yellow } from './color.mjs';
|
|
7
|
+
|
|
8
|
+
export function runFixRefs(argv, config, opts = {}) {
|
|
9
|
+
const { dryRun } = opts;
|
|
10
|
+
const result = fixBrokenRefs(config, { dryRun });
|
|
11
|
+
if (result.totalFixed === 0 && result.unfixableCount === 0) {
|
|
12
|
+
process.stdout.write(green('No broken references found.') + '\n');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Core logic for fixing broken references. Returns { totalFixed, unfixableCount }.
|
|
18
|
+
* Shared by `dotmd fix-refs` and `dotmd check --fix`.
|
|
19
|
+
*/
|
|
20
|
+
export function fixBrokenRefs(config, opts = {}) {
|
|
21
|
+
const { dryRun, quiet } = opts;
|
|
22
|
+
const index = buildIndex(config);
|
|
23
|
+
const allFiles = collectDocFiles(config);
|
|
24
|
+
|
|
25
|
+
// Build a map of basename → absolute path for all docs
|
|
26
|
+
const basenameMap = new Map();
|
|
27
|
+
const duplicateBasenames = new Set();
|
|
28
|
+
for (const filePath of allFiles) {
|
|
29
|
+
const basename = path.basename(filePath);
|
|
30
|
+
if (basenameMap.has(basename)) {
|
|
31
|
+
duplicateBasenames.add(basename);
|
|
32
|
+
} else {
|
|
33
|
+
basenameMap.set(basename, filePath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Find broken ref errors
|
|
38
|
+
const brokenRefErrors = index.errors.filter(e =>
|
|
39
|
+
e.message.includes('does not resolve to an existing file')
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (brokenRefErrors.length === 0) {
|
|
43
|
+
return { totalFixed: 0, unfixableCount: 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Group fixes by doc path
|
|
47
|
+
const fixesByDoc = new Map();
|
|
48
|
+
let unfixableCount = 0;
|
|
49
|
+
|
|
50
|
+
for (const err of brokenRefErrors) {
|
|
51
|
+
const match = err.message.match(/entry `([^`]+)` does not resolve/);
|
|
52
|
+
if (!match) { unfixableCount++; continue; }
|
|
53
|
+
|
|
54
|
+
const brokenRef = match[1];
|
|
55
|
+
const brokenBasename = path.basename(brokenRef);
|
|
56
|
+
|
|
57
|
+
if (duplicateBasenames.has(brokenBasename)) {
|
|
58
|
+
unfixableCount++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const resolved = basenameMap.get(brokenBasename);
|
|
63
|
+
if (!resolved) { unfixableCount++; continue; }
|
|
64
|
+
|
|
65
|
+
const docAbsPath = path.join(config.repoRoot, err.path);
|
|
66
|
+
const docDir = path.dirname(docAbsPath);
|
|
67
|
+
const correctRelPath = path.relative(docDir, resolved).split(path.sep).join('/');
|
|
68
|
+
|
|
69
|
+
if (correctRelPath === brokenRef) { unfixableCount++; continue; }
|
|
70
|
+
|
|
71
|
+
if (!fixesByDoc.has(err.path)) {
|
|
72
|
+
fixesByDoc.set(err.path, []);
|
|
73
|
+
}
|
|
74
|
+
fixesByDoc.get(err.path).push({ brokenRef, correctRelPath });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const prefix = dryRun ? dim('[dry-run] ') : '';
|
|
78
|
+
let totalFixed = 0;
|
|
79
|
+
|
|
80
|
+
for (const [docPath, fixes] of fixesByDoc) {
|
|
81
|
+
const absPath = path.join(config.repoRoot, docPath);
|
|
82
|
+
let raw = readFileSync(absPath, 'utf8');
|
|
83
|
+
const { frontmatter: fm } = extractFrontmatter(raw);
|
|
84
|
+
if (!fm) continue;
|
|
85
|
+
|
|
86
|
+
let newFm = fm;
|
|
87
|
+
for (const { brokenRef, correctRelPath } of fixes) {
|
|
88
|
+
newFm = newFm.split(brokenRef).join(correctRelPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (newFm !== fm && !dryRun) {
|
|
92
|
+
raw = replaceFrontmatter(raw, newFm);
|
|
93
|
+
writeFileSync(absPath, raw, 'utf8');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!quiet) {
|
|
97
|
+
process.stdout.write(`${prefix}${green('Fixed')}: ${docPath} (${fixes.length} ref${fixes.length > 1 ? 's' : ''})\n`);
|
|
98
|
+
for (const { brokenRef, correctRelPath } of fixes) {
|
|
99
|
+
process.stdout.write(`${prefix} ${dim(`${brokenRef} → ${correctRelPath}`)}\n`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
totalFixed += fixes.length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!quiet) {
|
|
106
|
+
process.stdout.write(`\n${prefix}${totalFixed} reference${totalFixed !== 1 ? 's' : ''} fixed across ${fixesByDoc.size} file(s).\n`);
|
|
107
|
+
if (unfixableCount > 0) {
|
|
108
|
+
process.stdout.write(`${yellow(`${unfixableCount} broken reference(s) could not be auto-resolved`)} (file not found by basename).\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { totalFixed, unfixableCount };
|
|
113
|
+
}
|
package/src/lifecycle.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
3
|
+
import { extractFrontmatter, parseSimpleFrontmatter, replaceFrontmatter } from './frontmatter.mjs';
|
|
4
4
|
import { asString, toRepoPath, die, warn, resolveDocPath, escapeRegex } from './util.mjs';
|
|
5
|
-
import { gitMv } from './git.mjs';
|
|
5
|
+
import { gitMv, getGitLastModified } from './git.mjs';
|
|
6
6
|
import { buildIndex, collectDocFiles } from './index.mjs';
|
|
7
7
|
import { renderIndexFile, writeIndex } from './index-file.mjs';
|
|
8
|
-
import { green, dim } from './color.mjs';
|
|
8
|
+
import { green, dim, yellow } from './color.mjs';
|
|
9
9
|
|
|
10
10
|
export function runStatus(argv, config, opts = {}) {
|
|
11
11
|
const { dryRun } = opts;
|
|
@@ -114,20 +114,10 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
114
114
|
process.stdout.write(`${prefix} Would move: ${oldRepoPath} → ${newRepoPath}\n`);
|
|
115
115
|
if (config.indexPath) process.stdout.write(`${prefix} Would regenerate index\n`);
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (docFile === targetPath) continue;
|
|
122
|
-
const docRaw = readFileSync(docFile, 'utf8');
|
|
123
|
-
const { frontmatter: docFm } = extractFrontmatter(docRaw);
|
|
124
|
-
if (docFm.includes(basename)) {
|
|
125
|
-
references.push(toRepoPath(docFile, config.repoRoot));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (references.length > 0) {
|
|
129
|
-
process.stdout.write('\nThese docs reference the old path — would need updating:\n');
|
|
130
|
-
for (const ref of references) process.stdout.write(`- ${ref}\n`);
|
|
117
|
+
// Preview reference updates
|
|
118
|
+
const refCount = countRefsToUpdate(filePath, targetPath, config);
|
|
119
|
+
if (refCount > 0) {
|
|
120
|
+
process.stdout.write(`${prefix} Would update references in ${refCount} file(s)\n`);
|
|
131
121
|
}
|
|
132
122
|
return;
|
|
133
123
|
}
|
|
@@ -140,39 +130,78 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
140
130
|
const result = gitMv(filePath, targetPath, config.repoRoot);
|
|
141
131
|
if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
|
|
142
132
|
|
|
133
|
+
// Auto-update references in other docs
|
|
134
|
+
const updatedRefCount = updateRefsAfterMove(filePath, targetPath, config);
|
|
135
|
+
|
|
143
136
|
if (config.indexPath) {
|
|
144
137
|
const index = buildIndex(config);
|
|
145
138
|
writeIndex(renderIndexFile(index, config), config);
|
|
146
139
|
}
|
|
147
140
|
|
|
148
141
|
process.stdout.write(`${green('Archived')}: ${oldRepoPath} → ${newRepoPath}\n`);
|
|
142
|
+
if (updatedRefCount > 0) process.stdout.write(`Updated references in ${updatedRefCount} file(s).\n`);
|
|
149
143
|
if (config.indexPath) process.stdout.write('Index regenerated.\n');
|
|
150
144
|
|
|
151
|
-
const basename = path.basename(filePath);
|
|
152
|
-
const references = [];
|
|
153
|
-
for (const docFile of collectDocFiles(config)) {
|
|
154
|
-
if (docFile === targetPath) continue;
|
|
155
|
-
const docRaw = readFileSync(docFile, 'utf8');
|
|
156
|
-
const { frontmatter: docFm } = extractFrontmatter(docRaw);
|
|
157
|
-
if (docFm.includes(basename)) {
|
|
158
|
-
references.push(toRepoPath(docFile, config.repoRoot));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (references.length > 0) {
|
|
163
|
-
process.stdout.write('\nThese docs reference the old path — update reference entries:\n');
|
|
164
|
-
for (const ref of references) process.stdout.write(`- ${ref}\n`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
process.stdout.write('\nNext: commit, then update references if needed.\n');
|
|
168
145
|
try { config.hooks.onArchive?.({ path: newRepoPath, oldStatus }, { oldPath: oldRepoPath, newPath: newRepoPath }); } catch (err) { warn(`Hook 'onArchive' threw: ${err.message}`); }
|
|
169
146
|
}
|
|
170
147
|
|
|
171
148
|
export function runTouch(argv, config, opts = {}) {
|
|
172
149
|
const { dryRun } = opts;
|
|
173
|
-
const
|
|
150
|
+
const useGit = argv.includes('--git');
|
|
151
|
+
const positional = [];
|
|
152
|
+
for (let i = 0; i < argv.length; i++) {
|
|
153
|
+
if (argv[i] === '--config') { i++; continue; }
|
|
154
|
+
if (argv[i].startsWith('-')) continue;
|
|
155
|
+
positional.push(argv[i]);
|
|
156
|
+
}
|
|
157
|
+
const input = positional[0];
|
|
174
158
|
|
|
175
|
-
|
|
159
|
+
// --git mode: bulk-sync frontmatter dates from git history
|
|
160
|
+
if (useGit) {
|
|
161
|
+
const allFiles = input ? [resolveDocPath(input, config)].filter(Boolean) : collectDocFiles(config);
|
|
162
|
+
if (input && allFiles.length === 0) { die(`File not found: ${input}`); }
|
|
163
|
+
|
|
164
|
+
const prefix = dryRun ? dim('[dry-run] ') : '';
|
|
165
|
+
let synced = 0;
|
|
166
|
+
|
|
167
|
+
for (const filePath of allFiles) {
|
|
168
|
+
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
169
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
170
|
+
const { frontmatter } = extractFrontmatter(raw);
|
|
171
|
+
if (!frontmatter) continue;
|
|
172
|
+
|
|
173
|
+
const parsed = parseSimpleFrontmatter(frontmatter);
|
|
174
|
+
const status = asString(parsed.status);
|
|
175
|
+
if (config.lifecycle.skipStaleFor.has(status)) continue;
|
|
176
|
+
|
|
177
|
+
const fmUpdated = asString(parsed.updated);
|
|
178
|
+
const gitDate = getGitLastModified(repoPath, config.repoRoot);
|
|
179
|
+
if (!gitDate) continue;
|
|
180
|
+
|
|
181
|
+
const gitDay = gitDate.slice(0, 10);
|
|
182
|
+
if (fmUpdated === gitDay) continue;
|
|
183
|
+
|
|
184
|
+
// Only sync if git is newer than frontmatter
|
|
185
|
+
const gitMs = new Date(gitDate).getTime();
|
|
186
|
+
const fmMs = fmUpdated ? new Date(fmUpdated).getTime() : 0;
|
|
187
|
+
if (fmMs >= gitMs) continue;
|
|
188
|
+
|
|
189
|
+
if (!dryRun) {
|
|
190
|
+
updateFrontmatter(filePath, { updated: gitDay });
|
|
191
|
+
}
|
|
192
|
+
process.stdout.write(`${prefix}${green('Synced')}: ${repoPath} (updated → ${gitDay})\n`);
|
|
193
|
+
synced++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (synced === 0) {
|
|
197
|
+
process.stdout.write(green('All frontmatter dates are in sync with git.') + '\n');
|
|
198
|
+
} else {
|
|
199
|
+
process.stdout.write(`\n${prefix}${synced} file(s) synced.\n`);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!input) { die('Usage: dotmd touch <file>\n dotmd touch --git Bulk-sync dates from git history'); }
|
|
176
205
|
|
|
177
206
|
const filePath = resolveDocPath(input, config);
|
|
178
207
|
if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); }
|
|
@@ -190,6 +219,69 @@ export function runTouch(argv, config, opts = {}) {
|
|
|
190
219
|
try { config.hooks.onTouch?.({ path: toRepoPath(filePath, config.repoRoot) }, { path: toRepoPath(filePath, config.repoRoot), date: today }); } catch (err) { warn(`Hook 'onTouch' threw: ${err.message}`); }
|
|
191
220
|
}
|
|
192
221
|
|
|
222
|
+
/**
|
|
223
|
+
* After a file moves (archive/unarchive), update frontmatter references in all
|
|
224
|
+
* docs that pointed to the old location so they point to the new one.
|
|
225
|
+
*/
|
|
226
|
+
function updateRefsAfterMove(oldPath, newPath, config) {
|
|
227
|
+
const basename = path.basename(oldPath);
|
|
228
|
+
const allFiles = collectDocFiles(config);
|
|
229
|
+
let updatedCount = 0;
|
|
230
|
+
|
|
231
|
+
for (const docFile of allFiles) {
|
|
232
|
+
if (docFile === newPath) continue;
|
|
233
|
+
let raw = readFileSync(docFile, 'utf8');
|
|
234
|
+
const { frontmatter: fm } = extractFrontmatter(raw);
|
|
235
|
+
if (!fm || !fm.includes(basename)) continue;
|
|
236
|
+
|
|
237
|
+
const docDir = path.dirname(docFile);
|
|
238
|
+
const oldRelPath = path.relative(docDir, oldPath).split(path.sep).join('/');
|
|
239
|
+
const newRelPath = path.relative(docDir, newPath).split(path.sep).join('/');
|
|
240
|
+
|
|
241
|
+
let newFm = fm;
|
|
242
|
+
|
|
243
|
+
// Replace exact relative path
|
|
244
|
+
if (newFm.includes(oldRelPath)) {
|
|
245
|
+
newFm = newFm.split(oldRelPath).join(newRelPath);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Also handle ./ prefix variant
|
|
249
|
+
const dotSlashOld = './' + oldRelPath;
|
|
250
|
+
if (newFm.includes(dotSlashOld)) {
|
|
251
|
+
newFm = newFm.split(dotSlashOld).join(newRelPath);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (newFm !== fm) {
|
|
255
|
+
raw = replaceFrontmatter(raw, newFm);
|
|
256
|
+
writeFileSync(docFile, raw, 'utf8');
|
|
257
|
+
updatedCount++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return updatedCount;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function countRefsToUpdate(oldPath, newPath, config) {
|
|
265
|
+
const basename = path.basename(oldPath);
|
|
266
|
+
const allFiles = collectDocFiles(config);
|
|
267
|
+
let count = 0;
|
|
268
|
+
|
|
269
|
+
for (const docFile of allFiles) {
|
|
270
|
+
if (docFile === newPath) continue;
|
|
271
|
+
const raw = readFileSync(docFile, 'utf8');
|
|
272
|
+
const { frontmatter: fm } = extractFrontmatter(raw);
|
|
273
|
+
if (!fm || !fm.includes(basename)) continue;
|
|
274
|
+
|
|
275
|
+
const docDir = path.dirname(docFile);
|
|
276
|
+
const oldRelPath = path.relative(docDir, oldPath).split(path.sep).join('/');
|
|
277
|
+
if (fm.includes(oldRelPath) || fm.includes('./' + oldRelPath)) {
|
|
278
|
+
count++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return count;
|
|
283
|
+
}
|
|
284
|
+
|
|
193
285
|
export function updateFrontmatter(filePath, updates) {
|
|
194
286
|
const raw = readFileSync(filePath, 'utf8');
|
|
195
287
|
if (!raw.startsWith('---\n')) throw new Error(`${filePath} has no frontmatter block.`);
|
package/src/render.mjs
CHANGED
|
@@ -143,8 +143,8 @@ function _renderContext(index, config) {
|
|
|
143
143
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
export function renderCheck(index, config) {
|
|
147
|
-
const defaultRenderer = (idx) => _renderCheck(idx);
|
|
146
|
+
export function renderCheck(index, config, opts = {}) {
|
|
147
|
+
const defaultRenderer = (idx) => _renderCheck(idx, opts);
|
|
148
148
|
if (config.hooks.renderCheck) {
|
|
149
149
|
try { return config.hooks.renderCheck(index, defaultRenderer); }
|
|
150
150
|
catch (err) { warn(`Hook 'renderCheck' threw: ${err.message}`); }
|
|
@@ -152,7 +152,8 @@ export function renderCheck(index, config) {
|
|
|
152
152
|
return defaultRenderer(index);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
function _renderCheck(index) {
|
|
155
|
+
function _renderCheck(index, opts = {}) {
|
|
156
|
+
const { errorsOnly } = opts;
|
|
156
157
|
const lines = ['Check', ''];
|
|
157
158
|
lines.push(`- docs scanned: ${index.docs.length}`);
|
|
158
159
|
lines.push(`- errors: ${index.errors.length}`);
|
|
@@ -167,7 +168,7 @@ function _renderCheck(index) {
|
|
|
167
168
|
lines.push('');
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
if (index.warnings.length > 0) {
|
|
171
|
+
if (!errorsOnly && index.warnings.length > 0) {
|
|
171
172
|
lines.push(yellow('Warnings'));
|
|
172
173
|
for (const issue of index.warnings) {
|
|
173
174
|
lines.push(`- ${issue.path}: ${issue.message}`);
|