dotmd-cli 0.1.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/LICENSE +21 -0
- package/README.md +187 -0
- package/bin/dotmd.mjs +202 -0
- package/dotmd.config.example.mjs +116 -0
- package/package.json +37 -0
- package/src/color.mjs +10 -0
- package/src/config.mjs +197 -0
- package/src/extractors.mjs +60 -0
- package/src/frontmatter.mjs +55 -0
- package/src/git.mjs +18 -0
- package/src/index-file.mjs +104 -0
- package/src/index.mjs +151 -0
- package/src/init.mjs +60 -0
- package/src/lifecycle.mjs +224 -0
- package/src/query.mjs +178 -0
- package/src/render.mjs +248 -0
- package/src/util.mjs +55 -0
- package/src/validate.mjs +157 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Beyond
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# dotmd
|
|
2
|
+
|
|
3
|
+
Zero-dependency CLI for managing markdown documents with YAML frontmatter.
|
|
4
|
+
|
|
5
|
+
Index, query, validate, and lifecycle-manage any collection of `.md` files — plans, ADRs, RFCs, design docs, meeting notes. Built for AI-assisted development workflows where structured docs need to stay current.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g dotmd-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
dotmd init # creates dotmd.config.mjs + docs/ + docs/README.md
|
|
17
|
+
dotmd list # index all docs grouped by status
|
|
18
|
+
dotmd check # validate frontmatter and references
|
|
19
|
+
dotmd context # compact briefing (great for LLM context)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What It Does
|
|
23
|
+
|
|
24
|
+
dotmd scans a directory of markdown files, parses their YAML frontmatter, and gives you tools to work with them:
|
|
25
|
+
|
|
26
|
+
- **Index** — group docs by status, show progress bars, next steps
|
|
27
|
+
- **Query** — filter by status, keyword, module, surface, owner, staleness
|
|
28
|
+
- **Validate** — check for missing fields, broken references, stale dates
|
|
29
|
+
- **Lifecycle** — transition statuses, auto-archive with `git mv`, bump dates
|
|
30
|
+
- **README generation** — auto-generate an index block in your README
|
|
31
|
+
- **Context briefing** — compact summary designed for AI/LLM consumption
|
|
32
|
+
|
|
33
|
+
## Document Format
|
|
34
|
+
|
|
35
|
+
Any `.md` file with YAML frontmatter:
|
|
36
|
+
|
|
37
|
+
```markdown
|
|
38
|
+
---
|
|
39
|
+
status: active
|
|
40
|
+
updated: 2026-03-14
|
|
41
|
+
module: auth
|
|
42
|
+
surface: backend
|
|
43
|
+
next_step: implement token refresh
|
|
44
|
+
current_state: initial scaffolding complete
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
# Auth Token Refresh
|
|
48
|
+
|
|
49
|
+
Design doc content here...
|
|
50
|
+
|
|
51
|
+
- [x] Research existing patterns
|
|
52
|
+
- [ ] Implement refresh logic
|
|
53
|
+
- [ ] Add tests
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The only required field is `status`. Everything else is optional but unlocks more features (staleness detection, filtering, coverage reports).
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
dotmd list [--verbose] List docs grouped by status
|
|
62
|
+
dotmd json Full index as JSON
|
|
63
|
+
dotmd check Validate frontmatter and references
|
|
64
|
+
dotmd coverage [--json] Metadata coverage report
|
|
65
|
+
dotmd context Compact briefing (LLM-oriented)
|
|
66
|
+
dotmd focus [status] Detailed view for one status group
|
|
67
|
+
dotmd query [filters] Filtered search
|
|
68
|
+
dotmd index [--write] Generate/update docs.md index block
|
|
69
|
+
dotmd status <file> <status> Transition document status
|
|
70
|
+
dotmd archive <file> Archive (status + move + index regen)
|
|
71
|
+
dotmd touch <file> Bump updated date
|
|
72
|
+
dotmd init Create starter config + docs directory
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Query Filters
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dotmd query --status active,ready --module auth
|
|
79
|
+
dotmd query --keyword "token" --has-next-step
|
|
80
|
+
dotmd query --stale --sort updated --all
|
|
81
|
+
dotmd query --surface backend --checklist-open
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Flags: `--status`, `--keyword`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`.
|
|
85
|
+
|
|
86
|
+
### Preset Aliases
|
|
87
|
+
|
|
88
|
+
Define custom query presets in your config:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
export const presets = {
|
|
92
|
+
stale: ['--status', 'active,ready', '--stale', '--sort', 'updated', '--all'],
|
|
93
|
+
mine: ['--owner', 'robert', '--status', 'active', '--all'],
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then run `dotmd stale` or `dotmd mine` as shorthand.
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
Create `dotmd.config.mjs` at your project root (or run `dotmd init`):
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
export const root = 'docs/plans'; // where your .md files live
|
|
105
|
+
export const archiveDir = 'archived'; // subdirectory for archived docs
|
|
106
|
+
|
|
107
|
+
export const statuses = {
|
|
108
|
+
order: ['draft', 'active', 'approved', 'superseded', 'archived'],
|
|
109
|
+
staleDays: { draft: 7, active: 14, approved: 30 },
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const lifecycle = {
|
|
113
|
+
archiveStatuses: ['archived'], // auto-move to archiveDir
|
|
114
|
+
skipStaleFor: ['archived'],
|
|
115
|
+
skipWarningsFor: ['archived'],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const readme = {
|
|
119
|
+
path: 'docs/plans/README.md',
|
|
120
|
+
startMarker: '<!-- GENERATED:dotmd:start -->',
|
|
121
|
+
endMarker: '<!-- GENERATED:dotmd:end -->',
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
All exports are optional. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs) for the full reference.
|
|
126
|
+
|
|
127
|
+
Config discovery walks up from cwd looking for `dotmd.config.mjs` or `.dotmd.config.mjs`.
|
|
128
|
+
|
|
129
|
+
## Hooks
|
|
130
|
+
|
|
131
|
+
Hooks are function exports in your config file. They let you extend validation, customize rendering, and react to lifecycle events.
|
|
132
|
+
|
|
133
|
+
### Custom Validation
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
export function validate(doc, ctx) {
|
|
137
|
+
const warnings = [];
|
|
138
|
+
if (doc.status === 'active' && !doc.owner) {
|
|
139
|
+
warnings.push({
|
|
140
|
+
path: doc.path,
|
|
141
|
+
level: 'warning',
|
|
142
|
+
message: 'Active docs should have an owner.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { errors: [], warnings };
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Render Hooks
|
|
150
|
+
|
|
151
|
+
Override any renderer by exporting a function that receives the default:
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
export function renderContext(index, defaultRenderer) {
|
|
155
|
+
let output = defaultRenderer(index);
|
|
156
|
+
return `# My Project\n\n${output}`;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Available: `renderContext`, `renderCompactList`, `renderCheck`, `formatSnapshot`.
|
|
161
|
+
|
|
162
|
+
### Lifecycle Hooks
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
export function onArchive(doc, { oldPath, newPath }) {
|
|
166
|
+
console.log(`Archived: ${oldPath} → ${newPath}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function onStatusChange(doc, { oldStatus, newStatus }) {
|
|
170
|
+
// notify, log, trigger CI, etc.
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Available: `onArchive`, `onStatusChange`, `onTouch`.
|
|
175
|
+
|
|
176
|
+
## Features
|
|
177
|
+
|
|
178
|
+
- **Zero dependencies** — pure Node.js builtins (`fs`, `path`, `child_process`)
|
|
179
|
+
- **No build step** — ships as plain ESM, runs directly
|
|
180
|
+
- **Git-aware** — detects frontmatter date drift vs git history, uses `git mv` for archives
|
|
181
|
+
- **Configurable everything** — statuses, taxonomy, lifecycle, validation rules, display
|
|
182
|
+
- **Hook system** — extend with JS functions, no plugin framework to learn
|
|
183
|
+
- **LLM-friendly** — `dotmd context` generates compact briefings for AI assistants
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
package/bin/dotmd.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { resolveConfig } from '../src/config.mjs';
|
|
7
|
+
import { buildIndex } from '../src/index.mjs';
|
|
8
|
+
import { renderCompactList, renderVerboseList, renderContext, renderCheck, renderCoverage, buildCoverage } from '../src/render.mjs';
|
|
9
|
+
import { renderIndexFile, writeIndex } from '../src/index-file.mjs';
|
|
10
|
+
import { runFocus, runQuery } from '../src/query.mjs';
|
|
11
|
+
import { runStatus, runArchive, runTouch } from '../src/lifecycle.mjs';
|
|
12
|
+
import { runInit } from '../src/init.mjs';
|
|
13
|
+
import { die } from '../src/util.mjs';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
18
|
+
|
|
19
|
+
const HELP = {
|
|
20
|
+
_main: `dotmd v${pkg.version} — frontmatter markdown document manager
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
list [--verbose] List docs grouped by status (default)
|
|
24
|
+
json Full index as JSON
|
|
25
|
+
check Validate frontmatter and references
|
|
26
|
+
coverage [--json] Metadata coverage report
|
|
27
|
+
context Compact briefing (LLM-oriented)
|
|
28
|
+
focus [status] Detailed view for one status group
|
|
29
|
+
query [filters] Filtered search
|
|
30
|
+
index [--write] Generate/update docs.md index block
|
|
31
|
+
status <file> <status> Transition document status
|
|
32
|
+
archive <file> Archive (status + move + index regen)
|
|
33
|
+
touch <file> Bump updated date
|
|
34
|
+
init Create starter config + docs directory
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--config <path> Explicit config file path
|
|
38
|
+
--dry-run, -n Preview changes without writing anything
|
|
39
|
+
--help, -h Show help
|
|
40
|
+
--version, -v Show version`,
|
|
41
|
+
|
|
42
|
+
query: `dotmd query — filtered document search
|
|
43
|
+
|
|
44
|
+
Filters:
|
|
45
|
+
--status <s1,s2> Filter by status (comma-separated)
|
|
46
|
+
--keyword <term> Search title, summary, state, path
|
|
47
|
+
--module <name> Filter by module
|
|
48
|
+
--surface <name> Filter by surface
|
|
49
|
+
--domain <name> Filter by domain
|
|
50
|
+
--owner <name> Filter by owner
|
|
51
|
+
--updated-since <date> Only docs updated after date
|
|
52
|
+
--stale Only stale docs
|
|
53
|
+
--has-next-step Only docs with a next step
|
|
54
|
+
--has-blockers Only docs with blockers
|
|
55
|
+
--checklist-open Only docs with open checklist items
|
|
56
|
+
--sort <field> Sort by: updated (default), title, status
|
|
57
|
+
--limit <n> Max results (default: 20)
|
|
58
|
+
--all Show all results (no limit)
|
|
59
|
+
--git Use git dates instead of frontmatter
|
|
60
|
+
--json Output as JSON`,
|
|
61
|
+
|
|
62
|
+
status: `dotmd status <file> <new-status> — transition document status
|
|
63
|
+
|
|
64
|
+
Moves the document to the new status. If transitioning to an archive
|
|
65
|
+
status, automatically moves the file to the archive directory and
|
|
66
|
+
regenerates the index (if configured).
|
|
67
|
+
|
|
68
|
+
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
69
|
+
|
|
70
|
+
archive: `dotmd archive <file> — archive a document
|
|
71
|
+
|
|
72
|
+
Sets status to 'archived', moves to the archive directory, regenerates
|
|
73
|
+
the index, and scans for stale references.
|
|
74
|
+
|
|
75
|
+
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
76
|
+
|
|
77
|
+
index: `dotmd index [--write] — generate/update docs.md index
|
|
78
|
+
|
|
79
|
+
Without --write, prints the generated content to stdout.
|
|
80
|
+
With --write, updates the configured index file in place.
|
|
81
|
+
|
|
82
|
+
Use --dry-run (-n) with --write to preview without writing.`,
|
|
83
|
+
|
|
84
|
+
init: `dotmd init — create starter config and docs directory
|
|
85
|
+
|
|
86
|
+
Creates dotmd.config.mjs, docs/, and docs/docs.md in the current
|
|
87
|
+
directory. Skips any files that already exist.`,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
const args = process.argv.slice(2);
|
|
92
|
+
const command = args[0] ?? 'list';
|
|
93
|
+
|
|
94
|
+
// Pre-config flags
|
|
95
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
96
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (command === '--help' || command === '-h') {
|
|
101
|
+
process.stdout.write(`${HELP._main}\n`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Per-command help
|
|
106
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
107
|
+
process.stdout.write(`${HELP[command] ?? HELP._main}\n`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Init doesn't need config
|
|
112
|
+
if (command === 'init') {
|
|
113
|
+
runInit(process.cwd());
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract --config flag
|
|
118
|
+
let explicitConfig = null;
|
|
119
|
+
for (let i = 0; i < args.length; i++) {
|
|
120
|
+
if (args[i] === '--config' && args[i + 1]) {
|
|
121
|
+
explicitConfig = args[i + 1];
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const dryRun = args.includes('--dry-run') || args.includes('-n');
|
|
127
|
+
|
|
128
|
+
const config = await resolveConfig(process.cwd(), explicitConfig);
|
|
129
|
+
const restArgs = args.slice(1);
|
|
130
|
+
|
|
131
|
+
// Preset aliases
|
|
132
|
+
if (config.presets[command]) {
|
|
133
|
+
const index = buildIndex(config);
|
|
134
|
+
runQuery(index, [...config.presets[command], ...restArgs], config);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Lifecycle commands
|
|
139
|
+
if (command === 'status') { runStatus(restArgs, config, { dryRun }); return; }
|
|
140
|
+
if (command === 'archive') { runArchive(restArgs, config, { dryRun }); return; }
|
|
141
|
+
if (command === 'touch') { runTouch(restArgs, config, { dryRun }); return; }
|
|
142
|
+
|
|
143
|
+
const index = buildIndex(config);
|
|
144
|
+
|
|
145
|
+
if (command === 'json') {
|
|
146
|
+
process.stdout.write(`${JSON.stringify(index, null, 2)}\n`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (command === 'list') {
|
|
151
|
+
if (args.includes('--verbose')) {
|
|
152
|
+
process.stdout.write(renderVerboseList(index, config));
|
|
153
|
+
} else {
|
|
154
|
+
process.stdout.write(renderCompactList(index, config));
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (command === 'check') {
|
|
160
|
+
process.stdout.write(renderCheck(index, config));
|
|
161
|
+
if (index.errors.length > 0) process.exitCode = 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (command === 'coverage') {
|
|
166
|
+
if (args.includes('--json')) {
|
|
167
|
+
process.stdout.write(`${JSON.stringify(buildCoverage(index, config), null, 2)}\n`);
|
|
168
|
+
} else {
|
|
169
|
+
process.stdout.write(renderCoverage(index, config));
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (command === 'index') {
|
|
175
|
+
if (!config.indexPath) {
|
|
176
|
+
die('Index generation is not configured. Add an `index` section to your dotmd.config.mjs.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const write = args.includes('--write');
|
|
180
|
+
const rendered = renderIndexFile(index, config);
|
|
181
|
+
if (write && !dryRun) {
|
|
182
|
+
writeIndex(rendered, config);
|
|
183
|
+
process.stdout.write(`Updated ${config.indexPath}\n`);
|
|
184
|
+
} else if (write && dryRun) {
|
|
185
|
+
process.stdout.write(`[dry-run] Would update ${config.indexPath}\n`);
|
|
186
|
+
} else {
|
|
187
|
+
process.stdout.write(rendered);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (command === 'focus') { runFocus(index, restArgs, config); return; }
|
|
193
|
+
if (command === 'query') { runQuery(index, restArgs, config); return; }
|
|
194
|
+
if (command === 'context') { process.stdout.write(renderContext(index, config)); return; }
|
|
195
|
+
|
|
196
|
+
// Unknown command — show help
|
|
197
|
+
die(`Unknown command: ${command}\n\n${HELP._main}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main().catch(err => {
|
|
201
|
+
die(err.message);
|
|
202
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// dotmd.config.mjs — document management configuration
|
|
2
|
+
// All exports are optional. Omitted values use built-in defaults.
|
|
3
|
+
// Place this file at the root of your project.
|
|
4
|
+
|
|
5
|
+
// ─── Static Config ───────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
// Directory containing your markdown docs (relative to this config file)
|
|
8
|
+
export const root = 'docs';
|
|
9
|
+
|
|
10
|
+
// Subdirectory for archived docs (used by `dotmd archive` and `dotmd status`)
|
|
11
|
+
export const archiveDir = 'archived';
|
|
12
|
+
|
|
13
|
+
// Directories to skip when scanning
|
|
14
|
+
export const excludeDirs = ['evidence'];
|
|
15
|
+
|
|
16
|
+
// Status workflow — order determines display grouping
|
|
17
|
+
export const statuses = {
|
|
18
|
+
order: ['active', 'ready', 'planned', 'research', 'blocked', 'reference', 'archived'],
|
|
19
|
+
// Days after which a doc is considered stale (null = never stale)
|
|
20
|
+
staleDays: {
|
|
21
|
+
active: 14,
|
|
22
|
+
ready: 14,
|
|
23
|
+
planned: 30,
|
|
24
|
+
blocked: 30,
|
|
25
|
+
research: 30,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Lifecycle behavior — which statuses trigger special handling
|
|
30
|
+
export const lifecycle = {
|
|
31
|
+
archiveStatuses: ['archived'], // auto-move to archiveDir on transition
|
|
32
|
+
skipStaleFor: ['archived'], // skip staleness checks
|
|
33
|
+
skipWarningsFor: ['archived'], // skip validation warnings (summary, etc.)
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Taxonomy validation — set fields to null to skip validation
|
|
37
|
+
export const taxonomy = {
|
|
38
|
+
surfaces: ['frontend', 'backend', 'mobile', 'docs', 'ops', 'platform'],
|
|
39
|
+
moduleRequiredFor: ['active', 'ready', 'planned', 'blocked'],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Index file generation — remove this section to disable
|
|
43
|
+
export const index = {
|
|
44
|
+
path: 'docs/docs.md',
|
|
45
|
+
startMarker: '<!-- GENERATED:dotmd:start -->',
|
|
46
|
+
endMarker: '<!-- GENERATED:dotmd:end -->',
|
|
47
|
+
archivedLimit: 8,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Context briefing layout (`dotmd context`)
|
|
51
|
+
export const context = {
|
|
52
|
+
expanded: ['active'],
|
|
53
|
+
listed: ['ready', 'planned'],
|
|
54
|
+
counted: ['blocked', 'research', 'reference', 'archived'],
|
|
55
|
+
recentDays: 3,
|
|
56
|
+
recentStatuses: ['active', 'ready', 'planned'],
|
|
57
|
+
recentLimit: 10,
|
|
58
|
+
truncateNextStep: 80,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Display settings
|
|
62
|
+
export const display = {
|
|
63
|
+
lineWidth: 0, // 0 = auto-detect terminal width
|
|
64
|
+
truncateTitle: 30,
|
|
65
|
+
truncateNextStep: 80,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Reference fields for bidirectional link checking
|
|
69
|
+
export const referenceFields = {
|
|
70
|
+
bidirectional: ['related_docs'], // warn if A→B but B↛A
|
|
71
|
+
unidirectional: ['supports'], // one-way, no symmetry check
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Query presets — expand to filter args when used as commands
|
|
75
|
+
export const presets = {
|
|
76
|
+
stale: ['--status', 'active,ready,planned,blocked,research', '--stale', '--sort', 'updated', '--all'],
|
|
77
|
+
actionable: ['--status', 'active,ready', '--has-next-step', '--sort', 'updated', '--all'],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ─── Function Hooks ──────────────────────────────────────────────────────────
|
|
81
|
+
// Hooks are optional. Each receives a default implementation it can wrap or replace.
|
|
82
|
+
|
|
83
|
+
// Custom validation — called after built-in validation for each doc.
|
|
84
|
+
// Return { errors: [], warnings: [] } to add issues.
|
|
85
|
+
// export function validate(doc, ctx) {
|
|
86
|
+
// const warnings = [];
|
|
87
|
+
// if (doc.status === 'active' && !doc.owner) {
|
|
88
|
+
// warnings.push({ path: doc.path, level: 'warning', message: 'Active docs should have an owner.' });
|
|
89
|
+
// }
|
|
90
|
+
// return { errors: [], warnings };
|
|
91
|
+
// }
|
|
92
|
+
|
|
93
|
+
// Override the context briefing output.
|
|
94
|
+
// export function renderContext(index, defaultRenderer) {
|
|
95
|
+
// return defaultRenderer(index);
|
|
96
|
+
// }
|
|
97
|
+
|
|
98
|
+
// Override the index file rendering.
|
|
99
|
+
// export function renderIndex(index, defaultRenderer) {
|
|
100
|
+
// return defaultRenderer(index);
|
|
101
|
+
// }
|
|
102
|
+
|
|
103
|
+
// Override the status snapshot display format.
|
|
104
|
+
// export function formatSnapshot(doc, defaultFormatter) {
|
|
105
|
+
// return defaultFormatter(doc);
|
|
106
|
+
// }
|
|
107
|
+
|
|
108
|
+
// Post-parse doc transformation — add computed fields.
|
|
109
|
+
// export function transformDoc(doc) {
|
|
110
|
+
// return doc;
|
|
111
|
+
// }
|
|
112
|
+
|
|
113
|
+
// Lifecycle callbacks — fire after the operation completes.
|
|
114
|
+
// export function onArchive(doc, { oldPath, newPath }) {}
|
|
115
|
+
// export function onStatusChange(doc, { oldStatus, newStatus, path }) {}
|
|
116
|
+
// export function onTouch(doc, { path, date }) {}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dotmd-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency CLI for managing markdown documents with YAML frontmatter — index, query, validate, lifecycle.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dotmd": "bin/dotmd.mjs"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"src/",
|
|
16
|
+
"dotmd.config.example.mjs"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"markdown",
|
|
20
|
+
"frontmatter",
|
|
21
|
+
"cli",
|
|
22
|
+
"docs",
|
|
23
|
+
"plans",
|
|
24
|
+
"adr",
|
|
25
|
+
"rfc",
|
|
26
|
+
"document-management",
|
|
27
|
+
"yaml"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/beyond-dev/platform.git",
|
|
32
|
+
"directory": "packages/dotmd"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/color.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const enabled = (process.env.FORCE_COLOR === '1')
|
|
2
|
+
|| (process.stdout.isTTY && !process.env.NO_COLOR);
|
|
3
|
+
|
|
4
|
+
const wrap = (code) => enabled ? (s) => `\x1b[${code}m${s}\x1b[0m` : (s) => s;
|
|
5
|
+
|
|
6
|
+
export const bold = wrap('1');
|
|
7
|
+
export const dim = wrap('2');
|
|
8
|
+
export const red = wrap('31');
|
|
9
|
+
export const yellow = wrap('33');
|
|
10
|
+
export const green = wrap('32');
|