dotmd-cli 0.1.0 → 0.2.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 +39 -2
- package/package.json +4 -1
- package/src/completions.mjs +97 -0
- package/src/config.mjs +27 -1
- package/src/lifecycle.mjs +3 -3
- package/src/new.mjs +54 -0
- package/src/util.mjs +5 -0
package/bin/dotmd.mjs
CHANGED
|
@@ -10,7 +10,9 @@ import { renderIndexFile, writeIndex } from '../src/index-file.mjs';
|
|
|
10
10
|
import { runFocus, runQuery } from '../src/query.mjs';
|
|
11
11
|
import { runStatus, runArchive, runTouch } from '../src/lifecycle.mjs';
|
|
12
12
|
import { runInit } from '../src/init.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import { runNew } from '../src/new.mjs';
|
|
14
|
+
import { runCompletions } from '../src/completions.mjs';
|
|
15
|
+
import { die, warn } from '../src/util.mjs';
|
|
14
16
|
|
|
15
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
18
|
const __dirname = path.dirname(__filename);
|
|
@@ -31,11 +33,14 @@ Commands:
|
|
|
31
33
|
status <file> <status> Transition document status
|
|
32
34
|
archive <file> Archive (status + move + index regen)
|
|
33
35
|
touch <file> Bump updated date
|
|
36
|
+
new <name> Create a new document with frontmatter
|
|
34
37
|
init Create starter config + docs directory
|
|
38
|
+
completions <shell> Output shell completion script (bash, zsh)
|
|
35
39
|
|
|
36
40
|
Options:
|
|
37
41
|
--config <path> Explicit config file path
|
|
38
42
|
--dry-run, -n Preview changes without writing anything
|
|
43
|
+
--verbose Show config details and doc count
|
|
39
44
|
--help, -h Show help
|
|
40
45
|
--version, -v Show version`,
|
|
41
46
|
|
|
@@ -81,6 +86,17 @@ With --write, updates the configured index file in place.
|
|
|
81
86
|
|
|
82
87
|
Use --dry-run (-n) with --write to preview without writing.`,
|
|
83
88
|
|
|
89
|
+
new: `dotmd new <name> — create a new document
|
|
90
|
+
|
|
91
|
+
Creates a new markdown document with frontmatter in the docs root.
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
--status <s> Set initial status (default: active)
|
|
95
|
+
--title <t> Override the document title
|
|
96
|
+
|
|
97
|
+
The filename is derived from <name> by slugifying it.
|
|
98
|
+
Use --dry-run (-n) to preview without creating the file.`,
|
|
99
|
+
|
|
84
100
|
init: `dotmd init — create starter config and docs directory
|
|
85
101
|
|
|
86
102
|
Creates dotmd.config.mjs, docs/, and docs/docs.md in the current
|
|
@@ -108,12 +124,17 @@ async function main() {
|
|
|
108
124
|
return;
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
// Init
|
|
127
|
+
// Init and completions don't need config
|
|
112
128
|
if (command === 'init') {
|
|
113
129
|
runInit(process.cwd());
|
|
114
130
|
return;
|
|
115
131
|
}
|
|
116
132
|
|
|
133
|
+
if (command === 'completions') {
|
|
134
|
+
runCompletions(args.slice(1));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
117
138
|
// Extract --config flag
|
|
118
139
|
let explicitConfig = null;
|
|
119
140
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -124,10 +145,21 @@ async function main() {
|
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
const dryRun = args.includes('--dry-run') || args.includes('-n');
|
|
148
|
+
const verbose = args.includes('--verbose');
|
|
127
149
|
|
|
128
150
|
const config = await resolveConfig(process.cwd(), explicitConfig);
|
|
129
151
|
const restArgs = args.slice(1);
|
|
130
152
|
|
|
153
|
+
if (!config.configFound && command !== 'init') {
|
|
154
|
+
warn('No dotmd config found — using defaults. Run `dotmd init` to create one.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (verbose) {
|
|
158
|
+
process.stderr.write(`Config: ${config.configPath ?? 'none'}\n`);
|
|
159
|
+
process.stderr.write(`Docs root: ${config.docsRoot}\n`);
|
|
160
|
+
process.stderr.write(`Repo root: ${config.repoRoot}\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
131
163
|
// Preset aliases
|
|
132
164
|
if (config.presets[command]) {
|
|
133
165
|
const index = buildIndex(config);
|
|
@@ -139,9 +171,14 @@ async function main() {
|
|
|
139
171
|
if (command === 'status') { runStatus(restArgs, config, { dryRun }); return; }
|
|
140
172
|
if (command === 'archive') { runArchive(restArgs, config, { dryRun }); return; }
|
|
141
173
|
if (command === 'touch') { runTouch(restArgs, config, { dryRun }); return; }
|
|
174
|
+
if (command === 'new') { runNew(restArgs, config, { dryRun }); return; }
|
|
142
175
|
|
|
143
176
|
const index = buildIndex(config);
|
|
144
177
|
|
|
178
|
+
if (verbose) {
|
|
179
|
+
process.stderr.write(`Docs found: ${index.docs.length}\n`);
|
|
180
|
+
}
|
|
181
|
+
|
|
145
182
|
if (command === 'json') {
|
|
146
183
|
process.stdout.write(`${JSON.stringify(index, null, 2)}\n`);
|
|
147
184
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotmd-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Zero-dependency CLI for managing markdown documents with YAML frontmatter — index, query, validate, lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
"url": "git+https://github.com/beyond-dev/platform.git",
|
|
32
32
|
"directory": "packages/dotmd"
|
|
33
33
|
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node --test test/*.test.mjs"
|
|
36
|
+
},
|
|
34
37
|
"engines": {
|
|
35
38
|
"node": ">=18"
|
|
36
39
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { die } from './util.mjs';
|
|
2
|
+
|
|
3
|
+
const COMMANDS = [
|
|
4
|
+
'list', 'json', 'check', 'coverage', 'context', 'focus', 'query',
|
|
5
|
+
'index', 'status', 'archive', 'touch', 'init', 'new', 'completions',
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const GLOBAL_FLAGS = ['--config', '--dry-run', '--verbose', '--help', '--version'];
|
|
9
|
+
|
|
10
|
+
const COMMAND_FLAGS = {
|
|
11
|
+
query: ['--status', '--keyword', '--module', '--surface', '--domain', '--owner',
|
|
12
|
+
'--updated-since', '--stale', '--has-next-step', '--has-blockers',
|
|
13
|
+
'--checklist-open', '--sort', '--limit', '--all', '--git', '--json'],
|
|
14
|
+
index: ['--write'],
|
|
15
|
+
list: ['--verbose'],
|
|
16
|
+
coverage: ['--json'],
|
|
17
|
+
new: ['--status', '--title'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function bashCompletion() {
|
|
21
|
+
return `# dotmd bash completion
|
|
22
|
+
# Add to ~/.bashrc: eval "$(dotmd completions bash)"
|
|
23
|
+
_dotmd() {
|
|
24
|
+
local cur prev cmd
|
|
25
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
26
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
27
|
+
|
|
28
|
+
# Find the subcommand
|
|
29
|
+
cmd=""
|
|
30
|
+
for ((i=1; i < COMP_CWORD; i++)); do
|
|
31
|
+
case "\${COMP_WORDS[i]}" in
|
|
32
|
+
-*) ;;
|
|
33
|
+
*) cmd="\${COMP_WORDS[i]}"; break ;;
|
|
34
|
+
esac
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
# Complete commands if no subcommand yet
|
|
38
|
+
if [[ -z "$cmd" ]]; then
|
|
39
|
+
COMPREPLY=( $(compgen -W "${COMMANDS.join(' ')} ${GLOBAL_FLAGS.join(' ')}" -- "$cur") )
|
|
40
|
+
return
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Per-command flag completion
|
|
44
|
+
case "$cmd" in
|
|
45
|
+
${Object.entries(COMMAND_FLAGS).map(([cmd, flags]) =>
|
|
46
|
+
` ${cmd}) COMPREPLY=( $(compgen -W "${flags.join(' ')} ${GLOBAL_FLAGS.join(' ')}" -- "$cur") ) ;;`
|
|
47
|
+
).join('\n')}
|
|
48
|
+
*) COMPREPLY=( $(compgen -W "${GLOBAL_FLAGS.join(' ')}" -- "$cur") ) ;;
|
|
49
|
+
esac
|
|
50
|
+
}
|
|
51
|
+
complete -F _dotmd dotmd`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function zshCompletion() {
|
|
55
|
+
return `# dotmd zsh completion
|
|
56
|
+
# Add to ~/.zshrc: eval "$(dotmd completions zsh)"
|
|
57
|
+
_dotmd() {
|
|
58
|
+
local -a commands global_flags
|
|
59
|
+
commands=(
|
|
60
|
+
${COMMANDS.map(c => ` '${c}'`).join('\n')}
|
|
61
|
+
)
|
|
62
|
+
global_flags=(
|
|
63
|
+
${GLOBAL_FLAGS.map(f => ` '${f}'`).join('\n')}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (( CURRENT == 2 )); then
|
|
67
|
+
_describe 'command' commands
|
|
68
|
+
_describe 'flag' global_flags
|
|
69
|
+
return
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
local cmd=\${words[2]}
|
|
73
|
+
case "$cmd" in
|
|
74
|
+
${Object.entries(COMMAND_FLAGS).map(([cmd, flags]) =>
|
|
75
|
+
` ${cmd}) _values 'flags' ${flags.map(f => `'${f}'`).join(' ')} ;;`
|
|
76
|
+
).join('\n')}
|
|
77
|
+
esac
|
|
78
|
+
|
|
79
|
+
_describe 'flag' global_flags
|
|
80
|
+
}
|
|
81
|
+
compdef _dotmd dotmd`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function runCompletions(argv) {
|
|
85
|
+
const shell = argv[0];
|
|
86
|
+
if (!shell) {
|
|
87
|
+
die('Usage: dotmd completions <bash|zsh>');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (shell === 'bash') {
|
|
91
|
+
process.stdout.write(bashCompletion() + '\n');
|
|
92
|
+
} else if (shell === 'zsh') {
|
|
93
|
+
process.stdout.write(zshCompletion() + '\n');
|
|
94
|
+
} else {
|
|
95
|
+
die(`Unsupported shell: ${shell}\nSupported: bash, zsh`);
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { die, warn } from './util.mjs';
|
|
4
5
|
|
|
5
6
|
const CONFIG_FILENAMES = ['dotmd.config.mjs', '.dotmd.config.mjs', 'dotmd.config.js'];
|
|
6
7
|
|
|
@@ -99,7 +100,27 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
99
100
|
|
|
100
101
|
if (configPath && existsSync(configPath)) {
|
|
101
102
|
const configUrl = pathToFileURL(configPath).href;
|
|
102
|
-
|
|
103
|
+
let mod;
|
|
104
|
+
try {
|
|
105
|
+
mod = await import(configUrl);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
die('Failed to load config: ' + configPath + '\n' + err.message + '\nRun `dotmd init` to create a starter config.');
|
|
108
|
+
// Return defaults so caller can still function
|
|
109
|
+
const defaults = deepMerge(DEFAULTS, {});
|
|
110
|
+
const defaultStatusOrder = defaults.statuses.order;
|
|
111
|
+
return {
|
|
112
|
+
raw: defaults, docsRoot: cwd, repoRoot: cwd, configDir: cwd,
|
|
113
|
+
configPath: configPath ?? null, configFound: Boolean(configPath),
|
|
114
|
+
archiveDir: defaults.archiveDir, excludeDirs: new Set(defaults.excludeDirs),
|
|
115
|
+
docsRootPrefix: '', statusOrder: defaultStatusOrder,
|
|
116
|
+
validStatuses: new Set(defaultStatusOrder), staleDaysByStatus: {},
|
|
117
|
+
lifecycle: { archiveStatuses: new Set(defaults.lifecycle.archiveStatuses), skipStaleFor: new Set(defaults.lifecycle.skipStaleFor), skipWarningsFor: new Set(defaults.lifecycle.skipWarningsFor) },
|
|
118
|
+
validSurfaces: null, moduleRequiredStatuses: new Set(),
|
|
119
|
+
indexPath: null, indexStartMarker: '<!-- GENERATED:dotmd:start -->', indexEndMarker: '<!-- GENERATED:dotmd:end -->', archivedHighlightLimit: 8,
|
|
120
|
+
context: defaults.context, display: defaults.display,
|
|
121
|
+
referenceFields: defaults.referenceFields, presets: defaults.presets, hooks: {},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
103
124
|
|
|
104
125
|
configDir = path.dirname(configPath);
|
|
105
126
|
|
|
@@ -123,6 +144,10 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
123
144
|
|
|
124
145
|
const docsRoot = path.resolve(configDir, config.root);
|
|
125
146
|
|
|
147
|
+
if (!existsSync(docsRoot)) {
|
|
148
|
+
warn('Docs root does not exist: ' + docsRoot);
|
|
149
|
+
}
|
|
150
|
+
|
|
126
151
|
// Find repo root by walking up looking for .git
|
|
127
152
|
let repoRoot = configDir;
|
|
128
153
|
{
|
|
@@ -170,6 +195,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
170
195
|
repoRoot,
|
|
171
196
|
configDir,
|
|
172
197
|
configPath: configPath ?? null,
|
|
198
|
+
configFound: Boolean(configPath),
|
|
173
199
|
archiveDir: config.archiveDir,
|
|
174
200
|
excludeDirs: new Set(config.excludeDirs),
|
|
175
201
|
docsRootPrefix,
|
package/src/lifecycle.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export function runStatus(argv, config, opts = {}) {
|
|
|
16
16
|
if (!config.validStatuses.has(newStatus)) { die(`Invalid status: ${newStatus}\nValid: ${[...config.validStatuses].join(', ')}`); return; }
|
|
17
17
|
|
|
18
18
|
const filePath = resolveDocPath(input, config);
|
|
19
|
-
if (!filePath) { die(`File not found: ${input}`); return; }
|
|
19
|
+
if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); return; }
|
|
20
20
|
|
|
21
21
|
const raw = readFileSync(filePath, 'utf8');
|
|
22
22
|
const { frontmatter } = extractFrontmatter(raw);
|
|
@@ -92,7 +92,7 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
92
92
|
if (!input) { die('Usage: dotmd archive <file>'); return; }
|
|
93
93
|
|
|
94
94
|
const filePath = resolveDocPath(input, config);
|
|
95
|
-
if (!filePath) { die(`File not found: ${input}`); return; }
|
|
95
|
+
if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); return; }
|
|
96
96
|
if (filePath.includes(`/${config.archiveDir}/`)) { die(`Already archived: ${toRepoPath(filePath, config.repoRoot)}`); return; }
|
|
97
97
|
|
|
98
98
|
const raw = readFileSync(filePath, 'utf8');
|
|
@@ -173,7 +173,7 @@ export function runTouch(argv, config, opts = {}) {
|
|
|
173
173
|
if (!input) { die('Usage: dotmd touch <file>'); return; }
|
|
174
174
|
|
|
175
175
|
const filePath = resolveDocPath(input, config);
|
|
176
|
-
if (!filePath) { die(`File not found: ${input}`); return; }
|
|
176
|
+
if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); return; }
|
|
177
177
|
|
|
178
178
|
const today = new Date().toISOString().slice(0, 10);
|
|
179
179
|
|
package/src/new.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { toRepoPath, die } from './util.mjs';
|
|
4
|
+
import { green, dim } from './color.mjs';
|
|
5
|
+
|
|
6
|
+
export function runNew(argv, config, opts = {}) {
|
|
7
|
+
const { dryRun } = opts;
|
|
8
|
+
|
|
9
|
+
// Parse args
|
|
10
|
+
const positional = [];
|
|
11
|
+
let status = 'active';
|
|
12
|
+
let title = null;
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
if (argv[i] === '--status' && argv[i + 1]) { status = argv[++i]; continue; }
|
|
15
|
+
if (argv[i] === '--title' && argv[i + 1]) { title = argv[++i]; continue; }
|
|
16
|
+
if (!argv[i].startsWith('-')) positional.push(argv[i]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const name = positional[0];
|
|
20
|
+
if (!name) { die('Usage: dotmd new <name> [--status <s>] [--title <t>]'); return; }
|
|
21
|
+
|
|
22
|
+
// Validate status
|
|
23
|
+
if (!config.validStatuses.has(status)) {
|
|
24
|
+
die(`Invalid status: ${status}\nValid: ${[...config.validStatuses].join(', ')}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Slugify
|
|
29
|
+
const slug = name.toLowerCase().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
30
|
+
if (!slug) { die('Name resolves to empty slug: ' + name); return; }
|
|
31
|
+
|
|
32
|
+
// Title
|
|
33
|
+
const docTitle = title ?? name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
34
|
+
|
|
35
|
+
// Path
|
|
36
|
+
const filePath = path.join(config.docsRoot, slug + '.md');
|
|
37
|
+
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
38
|
+
|
|
39
|
+
if (existsSync(filePath)) {
|
|
40
|
+
die(`File already exists: ${repoPath}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (dryRun) {
|
|
45
|
+
process.stdout.write(`${dim('[dry-run]')} Would create: ${repoPath}\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
50
|
+
const content = `---\nstatus: ${status}\nupdated: ${today}\n---\n\n# ${docTitle}\n`;
|
|
51
|
+
|
|
52
|
+
writeFileSync(filePath, content, 'utf8');
|
|
53
|
+
process.stdout.write(`${green('Created')}: ${repoPath}\n`);
|
|
54
|
+
}
|
package/src/util.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { dim } from './color.mjs';
|
|
2
3
|
|
|
3
4
|
export function escapeTable(value) {
|
|
4
5
|
return String(value).replace(/\|/g, '\\|');
|
|
@@ -49,6 +50,10 @@ export function toRepoPath(absolutePath, repoRoot) {
|
|
|
49
50
|
return path.relative(repoRoot, absolutePath).split(path.sep).join('/');
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export function warn(message) {
|
|
54
|
+
process.stderr.write(`${dim(message)}\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
export function die(message) {
|
|
53
58
|
process.stderr.write(`${message}\n`);
|
|
54
59
|
process.exitCode = 1;
|