dotmd-cli 0.5.0 → 0.7.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/README.md +115 -41
- package/bin/dotmd.mjs +98 -5
- package/dotmd.config.example.mjs +1 -1
- package/package.json +1 -1
- package/src/completions.mjs +8 -4
- package/src/config.mjs +2 -0
- package/src/doctor.mjs +41 -0
- package/src/extractors.mjs +19 -0
- package/src/fix-refs.mjs +113 -0
- package/src/graph.mjs +268 -0
- package/src/index.mjs +3 -1
- package/src/lifecycle.mjs +128 -36
- package/src/new.mjs +90 -7
- package/src/render.mjs +5 -4
- package/src/validate.mjs +8 -0
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ dotmd new my-feature # scaffold a new doc with frontmatter
|
|
|
19
19
|
dotmd list # index all docs grouped by status
|
|
20
20
|
dotmd check # validate frontmatter and references
|
|
21
21
|
dotmd context # compact briefing (great for LLM context)
|
|
22
|
+
dotmd doctor # auto-fix everything in one pass
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
### Shell Completion
|
|
@@ -37,9 +38,11 @@ dotmd scans a directory of markdown files, parses their YAML frontmatter, and gi
|
|
|
37
38
|
|
|
38
39
|
- **Index** — group docs by status, show progress bars, next steps
|
|
39
40
|
- **Query** — filter by status, keyword, module, surface, owner, staleness
|
|
40
|
-
- **Validate** — check for missing fields, broken references, stale dates
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
41
|
+
- **Validate** — check for missing fields, broken references, stale dates, broken body links
|
|
42
|
+
- **Graph** — visualize document relationships as text, Graphviz DOT, or JSON
|
|
43
|
+
- **Lifecycle** — transition statuses, auto-archive with `git mv` and reference updates
|
|
44
|
+
- **Doctor** — auto-fix broken refs, lint issues, date drift, and stale indexes in one pass
|
|
45
|
+
- **Scaffold** — create new docs from templates (plan, ADR, RFC, audit, design)
|
|
43
46
|
- **Index generation** — auto-generate a `docs.md` index block
|
|
44
47
|
- **Context briefing** — compact summary designed for AI/LLM consumption
|
|
45
48
|
- **Dry-run** — preview any mutation with `--dry-run` before committing
|
|
@@ -56,6 +59,8 @@ module: auth
|
|
|
56
59
|
surface: backend
|
|
57
60
|
next_step: implement token refresh
|
|
58
61
|
current_state: initial scaffolding complete
|
|
62
|
+
related_plans:
|
|
63
|
+
- ./design-doc.md
|
|
59
64
|
---
|
|
60
65
|
|
|
61
66
|
# Auth Token Refresh
|
|
@@ -67,28 +72,32 @@ Design doc content here...
|
|
|
67
72
|
- [ ] Add tests
|
|
68
73
|
```
|
|
69
74
|
|
|
70
|
-
The only required field is `status`. Everything else is optional but unlocks more features (staleness detection, filtering, coverage reports).
|
|
75
|
+
The only required field is `status`. Everything else is optional but unlocks more features (staleness detection, filtering, coverage reports, graph visualization).
|
|
71
76
|
|
|
72
77
|
## Commands
|
|
73
78
|
|
|
74
79
|
```
|
|
75
80
|
dotmd list [--verbose] List docs grouped by status (default)
|
|
76
81
|
dotmd json Full index as JSON
|
|
77
|
-
dotmd check
|
|
82
|
+
dotmd check [--errors-only] [--fix] Validate frontmatter and references
|
|
78
83
|
dotmd coverage [--json] Metadata coverage report
|
|
84
|
+
dotmd graph [--dot|--json] Visualize document relationships
|
|
79
85
|
dotmd context Compact briefing (LLM-oriented)
|
|
80
86
|
dotmd focus [status] Detailed view for one status group
|
|
81
87
|
dotmd query [filters] Filtered search
|
|
82
88
|
dotmd index [--write] Generate/update docs.md index block
|
|
83
89
|
dotmd status <file> <status> Transition document status
|
|
84
|
-
dotmd archive <file> Archive (status + move +
|
|
90
|
+
dotmd archive <file> Archive (status + move + update refs)
|
|
85
91
|
dotmd touch <file> Bump updated date
|
|
92
|
+
dotmd touch --git Bulk-sync dates from git history
|
|
93
|
+
dotmd doctor Auto-fix everything in one pass
|
|
94
|
+
dotmd fix-refs Auto-fix broken reference paths
|
|
86
95
|
dotmd lint [--fix] Check and auto-fix frontmatter issues
|
|
87
96
|
dotmd rename <old> <new> Rename doc and update references
|
|
88
97
|
dotmd migrate <f> <old> <new> Batch update a frontmatter field
|
|
89
98
|
dotmd watch [command] Re-run a command on file changes
|
|
90
99
|
dotmd diff [file] Show changes since last updated date
|
|
91
|
-
dotmd new <name> Create a new document
|
|
100
|
+
dotmd new <name> Create a new document from template
|
|
92
101
|
dotmd init Create starter config + docs directory
|
|
93
102
|
dotmd completions <shell> Output shell completion script (bash, zsh)
|
|
94
103
|
```
|
|
@@ -114,31 +123,91 @@ dotmd query --surface backend --checklist-open
|
|
|
114
123
|
|
|
115
124
|
Flags: `--status`, `--keyword`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`.
|
|
116
125
|
|
|
117
|
-
### Scaffold
|
|
126
|
+
### Scaffold with Templates
|
|
118
127
|
|
|
119
128
|
```bash
|
|
120
|
-
dotmd new my-feature
|
|
121
|
-
dotmd new
|
|
122
|
-
dotmd new
|
|
123
|
-
dotmd new
|
|
129
|
+
dotmd new my-feature # default (status + title)
|
|
130
|
+
dotmd new my-plan --template plan # plan with module, surface, refs
|
|
131
|
+
dotmd new my-decision --template adr # ADR: Context, Decision, Consequences
|
|
132
|
+
dotmd new my-proposal --template rfc # RFC: Summary, Motivation, Design
|
|
133
|
+
dotmd new my-audit --template audit # Audit: Scope, Findings, Recommendations
|
|
134
|
+
dotmd new my-design --template design # Design: Goals, Non-Goals, Design
|
|
135
|
+
dotmd new my-feature --status planned --title "Title" # custom status and title
|
|
136
|
+
dotmd new --list-templates # show all available templates
|
|
124
137
|
```
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Define custom query presets in your config:
|
|
139
|
+
Built-in templates: `default`, `plan`, `adr`, `rfc`, `audit`, `design`. Add custom templates in your config:
|
|
129
140
|
|
|
130
141
|
```js
|
|
131
|
-
export const
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
export const templates = {
|
|
143
|
+
spike: {
|
|
144
|
+
description: 'Timeboxed investigation',
|
|
145
|
+
frontmatter: (status, today) => `status: ${status}\nupdated: ${today}\ntimebox: 2d`,
|
|
146
|
+
body: (title) => `\n# ${title}\n\n## Hypothesis\n\n\n\n## Findings\n\n\n`,
|
|
147
|
+
},
|
|
134
148
|
};
|
|
135
149
|
```
|
|
136
150
|
|
|
137
|
-
|
|
151
|
+
### Check & Fix
|
|
138
152
|
|
|
139
|
-
|
|
153
|
+
```bash
|
|
154
|
+
dotmd check # validate everything
|
|
155
|
+
dotmd check --errors-only # suppress warnings, show only errors
|
|
156
|
+
dotmd check --fix # auto-fix broken refs + lint + regen index
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Validates: required fields, status values, broken reference paths, broken body links (`[text](path.md)`), bidirectional reference symmetry, git date drift, taxonomy mismatches.
|
|
160
|
+
|
|
161
|
+
### Doctor
|
|
162
|
+
|
|
163
|
+
One command to fix everything fixable:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
dotmd doctor # fix refs → lint → sync git dates → regen index
|
|
167
|
+
dotmd doctor --dry-run # preview all changes
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Runs in sequence: `fix-refs` → `lint --fix` → `touch --git` → `index --write` → shows remaining issues.
|
|
171
|
+
|
|
172
|
+
### Graph
|
|
173
|
+
|
|
174
|
+
Visualize how documents reference each other:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
dotmd graph # text adjacency list
|
|
178
|
+
dotmd graph --dot | dot -Tpng -o g.png # Graphviz PNG
|
|
179
|
+
dotmd graph --json # machine-readable
|
|
180
|
+
dotmd graph --status active,ready # filter by status
|
|
181
|
+
dotmd graph --module auth # filter by module
|
|
182
|
+
```
|
|
140
183
|
|
|
141
|
-
|
|
184
|
+
### Archive
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
dotmd archive docs/old-plan.md # move + update refs + regen index
|
|
188
|
+
dotmd archive docs/old-plan.md -n # preview
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Archives a document: sets status to `archived`, moves to archive directory via `git mv`, auto-updates references in other docs, and regenerates the index.
|
|
192
|
+
|
|
193
|
+
### Touch
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
dotmd touch docs/my-doc.md # set updated to today
|
|
197
|
+
dotmd touch --git # bulk-sync all docs from git history
|
|
198
|
+
dotmd touch --git docs/my-doc.md # sync one file from git
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Fix References
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
dotmd fix-refs # find and fix broken ref paths
|
|
205
|
+
dotmd fix-refs --dry-run # preview fixes
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Scans all reference fields for broken paths, resolves by basename matching, and rewrites frontmatter.
|
|
209
|
+
|
|
210
|
+
### Lint
|
|
142
211
|
|
|
143
212
|
```bash
|
|
144
213
|
dotmd lint # report issues
|
|
@@ -146,34 +215,35 @@ dotmd lint --fix # fix all issues
|
|
|
146
215
|
dotmd lint --fix --dry-run # preview fixes without writing
|
|
147
216
|
```
|
|
148
217
|
|
|
149
|
-
Detected issues:
|
|
150
|
-
- Missing `updated` date on non-archived docs
|
|
151
|
-
- Status casing mismatch (e.g., `Active` → `active`)
|
|
152
|
-
- camelCase frontmatter keys (e.g., `nextStep` → `next_step`)
|
|
153
|
-
- Trailing whitespace in frontmatter values
|
|
154
|
-
- Missing newline at end of file
|
|
218
|
+
Detected issues: missing `updated`, status casing, camelCase keys, trailing whitespace, missing EOF newline.
|
|
155
219
|
|
|
156
220
|
### Rename
|
|
157
221
|
|
|
158
|
-
Rename a document and update all frontmatter references across your docs:
|
|
159
|
-
|
|
160
222
|
```bash
|
|
161
223
|
dotmd rename old-name.md new-name # renames + updates refs
|
|
162
224
|
dotmd rename old-name.md new-name -n # preview without writing
|
|
163
225
|
```
|
|
164
226
|
|
|
165
|
-
Uses `git mv`
|
|
227
|
+
Uses `git mv` and updates all frontmatter references. Body markdown links are warned about but not auto-fixed.
|
|
166
228
|
|
|
167
229
|
### Migrate
|
|
168
230
|
|
|
169
|
-
Batch update a frontmatter field value across all docs:
|
|
170
|
-
|
|
171
231
|
```bash
|
|
172
232
|
dotmd migrate status research exploration # rename a status
|
|
173
233
|
dotmd migrate module auth identity # rename a module
|
|
174
|
-
dotmd migrate module auth identity -n # preview
|
|
175
234
|
```
|
|
176
235
|
|
|
236
|
+
### Preset Aliases
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
export const presets = {
|
|
240
|
+
stale: ['--status', 'active,ready', '--stale', '--sort', 'updated', '--all'],
|
|
241
|
+
mine: ['--owner', 'robert', '--status', 'active', '--all'],
|
|
242
|
+
};
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Then run `dotmd stale` or `dotmd mine` as shorthand.
|
|
246
|
+
|
|
177
247
|
### Watch Mode
|
|
178
248
|
|
|
179
249
|
```bash
|
|
@@ -184,19 +254,13 @@ dotmd watch context # live briefing
|
|
|
184
254
|
|
|
185
255
|
### Diff & Summarize
|
|
186
256
|
|
|
187
|
-
Show git changes since each document's `updated` frontmatter date:
|
|
188
|
-
|
|
189
257
|
```bash
|
|
190
258
|
dotmd diff # all drifted docs
|
|
191
259
|
dotmd diff docs/plans/auth.md # single file
|
|
192
260
|
dotmd diff --stat # summary stats only
|
|
193
|
-
dotmd diff --since 2026-01-01 # override date
|
|
194
261
|
dotmd diff --summarize # AI summary via local MLX model
|
|
195
|
-
dotmd diff --summarize --model mlx-community/Mistral-7B-Instruct-v0.3-4bit
|
|
196
262
|
```
|
|
197
263
|
|
|
198
|
-
The `--summarize` flag requires `uv` and a local MLX-compatible model. No JS dependencies are added.
|
|
199
|
-
|
|
200
264
|
## Configuration
|
|
201
265
|
|
|
202
266
|
Create `dotmd.config.mjs` at your project root (or run `dotmd init`):
|
|
@@ -216,6 +280,16 @@ export const lifecycle = {
|
|
|
216
280
|
skipWarningsFor: ['archived'],
|
|
217
281
|
};
|
|
218
282
|
|
|
283
|
+
export const taxonomy = {
|
|
284
|
+
surfaces: ['web', 'ios', 'backend', 'api', 'platform'],
|
|
285
|
+
moduleRequiredFor: ['active', 'ready', 'planned', 'blocked'],
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const referenceFields = {
|
|
289
|
+
bidirectional: ['related_plans'], // warn if A→B but B↛A
|
|
290
|
+
unidirectional: ['supports_plans'], // one-way, no symmetry check
|
|
291
|
+
};
|
|
292
|
+
|
|
219
293
|
export const index = {
|
|
220
294
|
path: 'docs/docs.md',
|
|
221
295
|
startMarker: '<!-- GENERATED:dotmd:start -->',
|
|
@@ -258,7 +332,7 @@ export function renderContext(index, defaultRenderer) {
|
|
|
258
332
|
}
|
|
259
333
|
```
|
|
260
334
|
|
|
261
|
-
Available: `renderContext`, `renderCompactList`, `renderCheck`, `formatSnapshot`.
|
|
335
|
+
Available: `renderContext`, `renderCompactList`, `renderCheck`, `renderGraph`, `formatSnapshot`.
|
|
262
336
|
|
|
263
337
|
### Lifecycle Hooks
|
|
264
338
|
|
|
@@ -291,7 +365,7 @@ export function summarizeDiff(diffOutput, filePath) {
|
|
|
291
365
|
- **No build step** — ships as plain ESM, runs directly
|
|
292
366
|
- **Git-aware** — detects frontmatter date drift vs git history, uses `git mv` for archives
|
|
293
367
|
- **Dry-run everything** — preview any mutation with `--dry-run` / `-n`
|
|
294
|
-
- **Configurable everything** — statuses, taxonomy, lifecycle, validation rules, display
|
|
368
|
+
- **Configurable everything** — statuses, taxonomy, lifecycle, validation rules, display, templates
|
|
295
369
|
- **Hook system** — extend with JS functions, no plugin framework to learn
|
|
296
370
|
- **LLM-friendly** — `dotmd context` generates compact briefings for AI assistants
|
|
297
371
|
- **Shell completion** — bash and zsh via `dotmd completions`
|
package/bin/dotmd.mjs
CHANGED
|
@@ -17,6 +17,9 @@ 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';
|
|
21
|
+
import { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } from '../src/graph.mjs';
|
|
22
|
+
import { runDoctor } from '../src/doctor.mjs';
|
|
20
23
|
import { die, warn } from '../src/util.mjs';
|
|
21
24
|
|
|
22
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -29,15 +32,18 @@ const HELP = {
|
|
|
29
32
|
Commands:
|
|
30
33
|
list [--verbose] List docs grouped by status (default)
|
|
31
34
|
json Full index as JSON
|
|
32
|
-
check
|
|
35
|
+
check [flags] Validate frontmatter and references
|
|
33
36
|
coverage [--json] Metadata coverage report
|
|
37
|
+
graph [--dot|--json] Visualize document relationships
|
|
34
38
|
context Compact briefing (LLM-oriented)
|
|
35
39
|
focus [status] Detailed view for one status group
|
|
36
40
|
query [filters] Filtered search
|
|
37
41
|
index [--write] Generate/update docs.md index block
|
|
38
42
|
status <file> <status> Transition document status
|
|
39
|
-
archive <file> Archive (status + move +
|
|
43
|
+
archive <file> Archive (status + move + update refs)
|
|
40
44
|
touch <file> Bump updated date
|
|
45
|
+
doctor Auto-fix everything: refs, lint, dates, index
|
|
46
|
+
fix-refs Auto-fix broken reference paths
|
|
41
47
|
lint [--fix] Check and auto-fix frontmatter issues
|
|
42
48
|
rename <old> <new> Rename doc and update references
|
|
43
49
|
migrate <f> <old> <new> Batch update a frontmatter field
|
|
@@ -82,10 +88,52 @@ regenerates the index (if configured).
|
|
|
82
88
|
|
|
83
89
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
84
90
|
|
|
91
|
+
check: `dotmd check — validate frontmatter and references
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
--errors-only Show only errors, suppress warnings
|
|
95
|
+
--fix Auto-fix broken references and regenerate index`,
|
|
96
|
+
|
|
85
97
|
archive: `dotmd archive <file> — archive a document
|
|
86
98
|
|
|
87
|
-
Sets status to 'archived', moves to the archive directory,
|
|
88
|
-
|
|
99
|
+
Sets status to 'archived', moves to the archive directory, auto-updates
|
|
100
|
+
references in other docs, and regenerates the index.
|
|
101
|
+
|
|
102
|
+
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
103
|
+
|
|
104
|
+
graph: `dotmd graph — visualize document relationships
|
|
105
|
+
|
|
106
|
+
Output formats:
|
|
107
|
+
(default) Text adjacency list
|
|
108
|
+
--dot Graphviz DOT format (pipe to dot -Tpng)
|
|
109
|
+
--json Machine-readable JSON
|
|
110
|
+
|
|
111
|
+
Filters:
|
|
112
|
+
--status <s1,s2> Show only docs with these statuses
|
|
113
|
+
--module <name> Show only docs with this module
|
|
114
|
+
--surface <name> Show only docs with this surface`,
|
|
115
|
+
|
|
116
|
+
doctor: `dotmd doctor — auto-fix everything in one pass
|
|
117
|
+
|
|
118
|
+
Runs in sequence: fix broken references, lint --fix, sync dates from
|
|
119
|
+
git, regenerate index, then show remaining issues.
|
|
120
|
+
|
|
121
|
+
Use --dry-run (-n) to preview all changes without writing anything.`,
|
|
122
|
+
|
|
123
|
+
'fix-refs': `dotmd fix-refs — auto-fix broken reference paths
|
|
124
|
+
|
|
125
|
+
Scans all docs for reference fields that point to non-existent files,
|
|
126
|
+
then attempts to resolve them by matching the basename against all known
|
|
127
|
+
docs. Fixes are applied by rewriting the frontmatter path.
|
|
128
|
+
|
|
129
|
+
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
130
|
+
|
|
131
|
+
touch: `dotmd touch <file> — bump updated date
|
|
132
|
+
dotmd touch --git — bulk-sync dates from git history
|
|
133
|
+
|
|
134
|
+
Without --git, updates a single file's frontmatter updated date to today.
|
|
135
|
+
With --git, scans all docs (or a specific file) and syncs their updated
|
|
136
|
+
date to match the last git commit date, fixing date drift warnings.
|
|
89
137
|
|
|
90
138
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
91
139
|
|
|
@@ -101,8 +149,10 @@ Use --dry-run (-n) with --write to preview without writing.`,
|
|
|
101
149
|
Creates a new markdown document with frontmatter in the docs root.
|
|
102
150
|
|
|
103
151
|
Options:
|
|
152
|
+
--template <name> Use a template (default, plan, adr, rfc, audit, design)
|
|
104
153
|
--status <s> Set initial status (default: active)
|
|
105
154
|
--title <t> Override the document title
|
|
155
|
+
--list-templates Show available templates
|
|
106
156
|
|
|
107
157
|
The filename is derived from <name> by slugifying it.
|
|
108
158
|
Use --dry-run (-n) to preview without creating the file.`,
|
|
@@ -243,6 +293,8 @@ async function main() {
|
|
|
243
293
|
if (command === 'lint') { runLint(restArgs, config, { dryRun }); return; }
|
|
244
294
|
if (command === 'rename') { runRename(restArgs, config, { dryRun }); return; }
|
|
245
295
|
if (command === 'migrate') { runMigrate(restArgs, config, { dryRun }); return; }
|
|
296
|
+
if (command === 'fix-refs') { runFixRefs(restArgs, config, { dryRun }); return; }
|
|
297
|
+
if (command === 'doctor') { runDoctor(restArgs, config, { dryRun }); return; }
|
|
246
298
|
|
|
247
299
|
const index = buildIndex(config);
|
|
248
300
|
|
|
@@ -265,7 +317,29 @@ async function main() {
|
|
|
265
317
|
}
|
|
266
318
|
|
|
267
319
|
if (command === 'check') {
|
|
268
|
-
|
|
320
|
+
const fix = args.includes('--fix');
|
|
321
|
+
const errorsOnly = args.includes('--errors-only');
|
|
322
|
+
|
|
323
|
+
if (fix) {
|
|
324
|
+
// Auto-fix: broken refs, then lint, then rebuild index
|
|
325
|
+
const refResult = fixBrokenRefs(config, { dryRun, quiet: false });
|
|
326
|
+
if (!dryRun) {
|
|
327
|
+
runLint(['--fix'], config, { dryRun });
|
|
328
|
+
}
|
|
329
|
+
if (!dryRun && config.indexPath) {
|
|
330
|
+
const { renderIndexFile: rif, writeIndex: wi } = await import('../src/index-file.mjs');
|
|
331
|
+
const freshIndex = buildIndex(config);
|
|
332
|
+
wi(rif(freshIndex, config), config);
|
|
333
|
+
process.stdout.write('Index regenerated.\n');
|
|
334
|
+
}
|
|
335
|
+
// Show remaining issues
|
|
336
|
+
const freshIndex = buildIndex(config);
|
|
337
|
+
process.stdout.write('\n' + renderCheck(freshIndex, config, { errorsOnly }));
|
|
338
|
+
if (freshIndex.errors.length > 0) process.exitCode = 1;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
process.stdout.write(renderCheck(index, config, { errorsOnly }));
|
|
269
343
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
270
344
|
return;
|
|
271
345
|
}
|
|
@@ -300,6 +374,25 @@ async function main() {
|
|
|
300
374
|
if (command === 'query') { runQuery(index, restArgs, config); return; }
|
|
301
375
|
if (command === 'context') { process.stdout.write(renderContext(index, config)); return; }
|
|
302
376
|
|
|
377
|
+
if (command === 'graph') {
|
|
378
|
+
const statusFilter = (() => { const i = args.indexOf('--status'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
|
|
379
|
+
const moduleFilter = (() => { const i = args.indexOf('--module'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
|
|
380
|
+
const surfaceFilter = (() => { const i = args.indexOf('--surface'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
|
|
381
|
+
const graph = buildGraph(index, config, {
|
|
382
|
+
statuses: statusFilter?.split(',') ?? null,
|
|
383
|
+
module: moduleFilter,
|
|
384
|
+
surface: surfaceFilter,
|
|
385
|
+
});
|
|
386
|
+
if (args.includes('--dot')) {
|
|
387
|
+
process.stdout.write(renderGraphDot(graph, config));
|
|
388
|
+
} else if (args.includes('--json')) {
|
|
389
|
+
process.stdout.write(renderGraphJson(graph));
|
|
390
|
+
} else {
|
|
391
|
+
process.stdout.write(renderGraphText(graph, config));
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
303
396
|
// Unknown command — show help
|
|
304
397
|
die(`Unknown command: ${command}\n\n${HELP._main}`);
|
|
305
398
|
}
|
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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { die } from './util.mjs';
|
|
2
2
|
|
|
3
3
|
const COMMANDS = [
|
|
4
|
-
'list', 'json', 'check', 'coverage', 'context', 'focus', 'query',
|
|
5
|
-
'index', 'status', 'archive', 'touch', 'lint', 'rename', 'migrate',
|
|
6
|
-
'watch', 'diff', 'init', 'new', 'completions',
|
|
4
|
+
'list', 'json', 'check', 'coverage', 'graph', 'context', 'focus', 'query',
|
|
5
|
+
'index', 'status', 'archive', 'touch', 'doctor', 'lint', 'rename', 'migrate',
|
|
6
|
+
'fix-refs', 'watch', 'diff', 'init', 'new', 'completions',
|
|
7
7
|
];
|
|
8
8
|
|
|
9
9
|
const GLOBAL_FLAGS = ['--config', '--dry-run', '--verbose', '--help', '--version'];
|
|
@@ -15,11 +15,15 @@ const COMMAND_FLAGS = {
|
|
|
15
15
|
index: ['--write'],
|
|
16
16
|
list: ['--verbose'],
|
|
17
17
|
coverage: ['--json'],
|
|
18
|
-
new: ['--status', '--title'],
|
|
18
|
+
new: ['--status', '--title', '--template', '--list-templates'],
|
|
19
19
|
diff: ['--stat', '--since', '--summarize', '--model'],
|
|
20
|
+
check: ['--errors-only', '--fix'],
|
|
21
|
+
graph: ['--dot', '--json', '--status', '--module', '--surface'],
|
|
20
22
|
lint: ['--fix'],
|
|
21
23
|
rename: [],
|
|
22
24
|
migrate: [],
|
|
25
|
+
'fix-refs': [],
|
|
26
|
+
touch: ['--git'],
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
function bashCompletion() {
|
package/src/config.mjs
CHANGED
|
@@ -55,6 +55,8 @@ const DEFAULTS = {
|
|
|
55
55
|
unidirectional: [],
|
|
56
56
|
},
|
|
57
57
|
|
|
58
|
+
templates: {},
|
|
59
|
+
|
|
58
60
|
presets: {
|
|
59
61
|
stale: ['--status', 'active,ready,planned,blocked,research', '--stale', '--sort', 'updated', '--all'],
|
|
60
62
|
actionable: ['--status', 'active,ready', '--has-next-step', '--sort', 'updated', '--all'],
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { fixBrokenRefs } from './fix-refs.mjs';
|
|
2
|
+
import { runLint } from './lint.mjs';
|
|
3
|
+
import { runTouch } from './lifecycle.mjs';
|
|
4
|
+
import { buildIndex } from './index.mjs';
|
|
5
|
+
import { renderIndexFile, writeIndex } from './index-file.mjs';
|
|
6
|
+
import { renderCheck } from './render.mjs';
|
|
7
|
+
import { bold } from './color.mjs';
|
|
8
|
+
|
|
9
|
+
export function runDoctor(argv, config, opts = {}) {
|
|
10
|
+
const { dryRun } = opts;
|
|
11
|
+
process.stdout.write(bold('dotmd doctor') + '\n\n');
|
|
12
|
+
|
|
13
|
+
// Step 1: Fix broken references
|
|
14
|
+
process.stdout.write(bold('1. Fixing broken references...') + '\n');
|
|
15
|
+
fixBrokenRefs(config, { dryRun });
|
|
16
|
+
|
|
17
|
+
// Step 2: Lint --fix
|
|
18
|
+
process.stdout.write('\n' + bold('2. Fixing frontmatter issues...') + '\n');
|
|
19
|
+
runLint(['--fix'], config, { dryRun });
|
|
20
|
+
|
|
21
|
+
// Step 3: Sync dates from git
|
|
22
|
+
process.stdout.write('\n' + bold('3. Syncing dates from git...') + '\n');
|
|
23
|
+
runTouch(['--git'], config, { dryRun });
|
|
24
|
+
|
|
25
|
+
// Step 4: Regenerate index
|
|
26
|
+
if (config.indexPath) {
|
|
27
|
+
process.stdout.write('\n' + bold('4. Regenerating index...') + '\n');
|
|
28
|
+
if (!dryRun) {
|
|
29
|
+
const index = buildIndex(config);
|
|
30
|
+
writeIndex(renderIndexFile(index, config), config);
|
|
31
|
+
process.stdout.write('Index updated.\n');
|
|
32
|
+
} else {
|
|
33
|
+
process.stdout.write('[dry-run] Would regenerate index.\n');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Step 5: Show remaining check
|
|
38
|
+
process.stdout.write('\n' + bold('5. Remaining issues:') + '\n');
|
|
39
|
+
const freshIndex = buildIndex(config);
|
|
40
|
+
process.stdout.write(renderCheck(freshIndex, config));
|
|
41
|
+
}
|
package/src/extractors.mjs
CHANGED
|
@@ -39,6 +39,25 @@ export function extractNextStep(body) {
|
|
|
39
39
|
return lines[0] ?? null;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export function extractBodyLinks(body) {
|
|
43
|
+
if (!body) return [];
|
|
44
|
+
// Strip fenced code blocks to avoid false positives
|
|
45
|
+
const stripped = body.replace(/^```[\s\S]*?^```/gm, '');
|
|
46
|
+
const links = [];
|
|
47
|
+
// Match [text](path.md) or [text](path.md#anchor), skip images (preceded by !)
|
|
48
|
+
const regex = /(?<!!)\[([^\]]+)\]\(([^)]+\.md(?:#[^)]*)?)\)/g;
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = regex.exec(stripped)) !== null) {
|
|
51
|
+
const href = match[2];
|
|
52
|
+
// Skip external URLs
|
|
53
|
+
if (/^https?:\/\//i.test(href)) continue;
|
|
54
|
+
// Strip anchor fragment for path resolution
|
|
55
|
+
const cleanHref = href.replace(/#.*$/, '');
|
|
56
|
+
links.push({ text: match[1], href: cleanHref });
|
|
57
|
+
}
|
|
58
|
+
return links;
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
export function extractChecklistCounts(body) {
|
|
43
62
|
const matches = [...body.matchAll(/^\s*[-*]\s+\[([ xX])\]\s+/gm)];
|
|
44
63
|
let completed = 0;
|
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
|
+
}
|