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 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');