dotmd-cli 0.22.0 → 0.23.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 +31 -11
- package/package.json +1 -1
- package/src/claude-commands.mjs +6 -2
- package/src/config.mjs +5 -0
- package/src/migrate-template.mjs +1 -1
- package/src/new.mjs +119 -45
package/bin/dotmd.mjs
CHANGED
|
@@ -55,7 +55,7 @@ Lifecycle:
|
|
|
55
55
|
migrate <field> <old> <new> [f...]Batch update a frontmatter field value (optional file filter)
|
|
56
56
|
|
|
57
57
|
Create & Export:
|
|
58
|
-
new <name> [
|
|
58
|
+
new <type> <name> [body] Create doc of given type (plan, doc, prompt, research)
|
|
59
59
|
index [--write] Generate/update docs.md index block
|
|
60
60
|
export [--format md|html|json] Export docs as markdown, HTML, or JSON
|
|
61
61
|
notion import|export|sync [db-id] Notion database integration
|
|
@@ -386,22 +386,42 @@ With --write, updates the configured index file in place.
|
|
|
386
386
|
|
|
387
387
|
Use --dry-run (-n) with --write to preview without writing.`,
|
|
388
388
|
|
|
389
|
-
new: `dotmd new <name> — create a new document
|
|
389
|
+
new: `dotmd new <type> <name> [body] — create a new document
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
Types and their default destinations:
|
|
392
|
+
plan docs/plans/<slug>.md (build-up template: Problem → Phases → Closeout)
|
|
393
|
+
doc docs/<slug>.md (minimal reference doc)
|
|
394
|
+
prompt docs/prompts/<slug>.md (saved prompt to seed a future session — body required)
|
|
395
|
+
research docs/<slug>.md (audit / investigation)
|
|
392
396
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
+
\`<type>\` can be omitted; defaults to \`doc\`.
|
|
398
|
+
\`<name>\` is slugified for the filename.
|
|
399
|
+
|
|
400
|
+
Body input (prompt type only):
|
|
401
|
+
<text> Inline body as 3rd positional
|
|
402
|
+
--message "<text>" Explicit inline body
|
|
403
|
+
- Read body from stdin (heredoc-friendly for agents)
|
|
404
|
+
@path Read body from a file
|
|
405
|
+
|
|
406
|
+
Examples:
|
|
407
|
+
dotmd new plan auth-revamp
|
|
408
|
+
dotmd new prompt cleanup-tomorrow "look at remaining lint warnings"
|
|
409
|
+
dotmd new prompt resume-foo - <<'EOF'
|
|
410
|
+
multi-line
|
|
411
|
+
prompt body
|
|
412
|
+
EOF
|
|
413
|
+
dotmd new prompt from-file @/tmp/draft.md
|
|
414
|
+
|
|
415
|
+
Other options:
|
|
416
|
+
--status <s> Set initial status (defaults to first valid status for the type)
|
|
417
|
+
--title <t> Override the auto-derived title
|
|
397
418
|
--root <name> Create in a specific docs root
|
|
398
|
-
--list-
|
|
419
|
+
--list-types Show registered types (alias: --list-templates)
|
|
399
420
|
|
|
400
421
|
For plans, the default status vocabulary is: in-session, active, planned,
|
|
401
|
-
blocked, partial, paused, awaiting, queued-after, archived.
|
|
402
|
-
|
|
422
|
+
blocked, partial, paused, awaiting, queued-after, archived.
|
|
423
|
+
For prompts: pending (default), claimed, archived.
|
|
403
424
|
|
|
404
|
-
The filename is derived from <name> by slugifying it.
|
|
405
425
|
Use --dry-run (-n) to preview without creating the file.`,
|
|
406
426
|
|
|
407
427
|
watch: `dotmd watch [command] — re-run a command on file changes
|
package/package.json
CHANGED
package/src/claude-commands.mjs
CHANGED
|
@@ -22,7 +22,8 @@ function generatePlansCommand(config) {
|
|
|
22
22
|
lines.push('- `dotmd health` — plan velocity, aging, checklist progress, pipeline view');
|
|
23
23
|
lines.push('- `dotmd unblocks <file>` — what depends on / is blocked by a plan');
|
|
24
24
|
lines.push('- `dotmd next` — ready plans with next steps (what to promote)');
|
|
25
|
-
lines.push('- `dotmd new <name
|
|
25
|
+
lines.push('- `dotmd new plan <name>` — scaffold with full phase structure');
|
|
26
|
+
lines.push('- `dotmd new prompt <name> "<body>"` — save a resume-prompt to docs/prompts/');
|
|
26
27
|
lines.push('- `dotmd archive <file>` — archive with auto ref-fixing (both directions)');
|
|
27
28
|
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
28
29
|
lines.push('- `dotmd status <file> <status>` — transition status');
|
|
@@ -115,7 +116,10 @@ function generateDocsCommand(config) {
|
|
|
115
116
|
|
|
116
117
|
lines.push('');
|
|
117
118
|
lines.push('Lifecycle:');
|
|
118
|
-
lines.push('- `dotmd new <name
|
|
119
|
+
lines.push('- `dotmd new plan <name>` — scaffold new plan');
|
|
120
|
+
lines.push('- `dotmd new doc <name>` — scaffold reference doc');
|
|
121
|
+
lines.push('- `dotmd new prompt <name> "<body>"` — save a resume-prompt');
|
|
122
|
+
lines.push('- `dotmd new research <name>` — scaffold an audit/investigation');
|
|
119
123
|
lines.push('- `dotmd status <file> <status>` — transition status');
|
|
120
124
|
lines.push('- `dotmd archive <file>` — archive with auto ref-fixing');
|
|
121
125
|
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
package/src/config.mjs
CHANGED
|
@@ -40,6 +40,11 @@ const DEFAULTS = {
|
|
|
40
40
|
context: { expanded: ['active'], listed: [], counted: ['reference', 'archived'] },
|
|
41
41
|
staleDays: { active: 30 },
|
|
42
42
|
},
|
|
43
|
+
prompt: {
|
|
44
|
+
statuses: ['pending', 'claimed', 'archived'],
|
|
45
|
+
context: { expanded: ['pending'], listed: [], counted: ['claimed', 'archived'] },
|
|
46
|
+
staleDays: { pending: 30 },
|
|
47
|
+
},
|
|
43
48
|
},
|
|
44
49
|
|
|
45
50
|
statuses: {
|
package/src/migrate-template.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { bold, green, yellow, dim } from './color.mjs';
|
|
|
8
8
|
const HEADING_RENAMES = [
|
|
9
9
|
{ from: /^##\s+Open questions\s*$/gm, to: '## Open Questions' },
|
|
10
10
|
{ from: /^##\s+open questions\s*$/gm, to: '## Open Questions' },
|
|
11
|
-
{ from: /^##\s+Out of
|
|
11
|
+
{ from: /^##\s+Out of [Ss]cope\s*$/gm, to: '## Non-Goals' },
|
|
12
12
|
{ from: /^##\s+out of scope\s*$/gm, to: '## Non-Goals' },
|
|
13
13
|
{ from: /^##\s+Non-goals\s*$/gm, to: '## Non-Goals' },
|
|
14
14
|
];
|
package/src/new.mjs
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { existsSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { toRepoPath, die, warn, nowIso } from './util.mjs';
|
|
4
5
|
import { green, dim, bold } from './color.mjs';
|
|
5
6
|
import { isInteractive, promptText } from './prompt.mjs';
|
|
6
7
|
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
10
|
+
|
|
7
11
|
const BUILTIN_TEMPLATES = {
|
|
8
|
-
|
|
9
|
-
description: '
|
|
10
|
-
|
|
12
|
+
doc: {
|
|
13
|
+
description: 'Reference doc, design note, glossary entry, etc.',
|
|
14
|
+
defaultStatus: 'active',
|
|
15
|
+
frontmatter: (s, d) => `type: doc\nstatus: ${s}\ncreated: ${d}\nupdated: ${d}`,
|
|
11
16
|
body: (t) => `\n# ${t}\n`,
|
|
12
17
|
},
|
|
13
18
|
plan: {
|
|
14
19
|
description: 'Execution plan — build-up shape (Problem → Phases → Closeout) with phase status markers and Version History',
|
|
20
|
+
dir: 'plans',
|
|
21
|
+
defaultStatus: 'active',
|
|
15
22
|
frontmatter: (s, d) => [
|
|
16
23
|
'type: plan',
|
|
17
24
|
`status: ${s}`,
|
|
@@ -88,67 +95,127 @@ Status markers (put in heading text):
|
|
|
88
95
|
<!-- Filled on archive: what shipped, key commits, deferrals dispositioned. -->
|
|
89
96
|
`,
|
|
90
97
|
},
|
|
91
|
-
|
|
92
|
-
description: 'Architecture Decision Record',
|
|
93
|
-
frontmatter: (s, d) => `type: doc\nstatus: ${s}\nupdated: ${d}\ndecision_date:\ndeciders:`,
|
|
94
|
-
body: (t) => `\n# ${t}\n\n## Context\n\n\n\n## Decision\n\n\n\n## Consequences\n\n\n`,
|
|
95
|
-
},
|
|
96
|
-
rfc: {
|
|
97
|
-
description: 'Request for Comments',
|
|
98
|
-
frontmatter: (s, d) => `type: doc\nstatus: ${s}\nupdated: ${d}\nowner:\nreviewers:`,
|
|
99
|
-
body: (t) => `\n# ${t}\n\n## Summary\n\n\n\n## Motivation\n\n\n\n## Detailed Design\n\n\n\n## Alternatives\n\n\n\n## Open Questions\n\n\n`,
|
|
100
|
-
},
|
|
101
|
-
audit: {
|
|
98
|
+
research: {
|
|
102
99
|
description: 'Codebase audit or research investigation',
|
|
103
|
-
|
|
100
|
+
defaultStatus: 'active',
|
|
101
|
+
frontmatter: (s, d) => [
|
|
102
|
+
'type: research',
|
|
103
|
+
`status: ${s}`,
|
|
104
|
+
`created: ${d}`,
|
|
105
|
+
`updated: ${d}`,
|
|
106
|
+
`audited: ${d}`,
|
|
107
|
+
'audit_level: pass1',
|
|
108
|
+
'module:',
|
|
109
|
+
'source_of_truth: code',
|
|
110
|
+
'supports_plans: []',
|
|
111
|
+
].join('\n'),
|
|
104
112
|
body: (t) => `\n# ${t}\n\n## Scope\n\n\n\n## Findings\n\n\n\n## Recommendations\n\n\n`,
|
|
105
113
|
},
|
|
106
|
-
|
|
107
|
-
description: '
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
prompt: {
|
|
115
|
+
description: 'Saved prompt to seed a future Claude session — body is required',
|
|
116
|
+
dir: 'prompts',
|
|
117
|
+
defaultStatus: 'pending',
|
|
118
|
+
requiresBody: true,
|
|
119
|
+
frontmatter: (s, d, ctx) => [
|
|
120
|
+
'type: prompt',
|
|
121
|
+
`status: ${s}`,
|
|
122
|
+
`created: ${d}`,
|
|
123
|
+
`dotmd_version: ${pkg.version}`,
|
|
124
|
+
`context: ${ctx?.title ? `"${ctx.title.replace(/"/g, '\\"')}"` : ''}`,
|
|
125
|
+
'related_plans: []',
|
|
126
|
+
].join('\n'),
|
|
127
|
+
body: (t, ctx) => `\n${ctx?.bodyInput ?? '<!-- prompt body -->'}\n`,
|
|
110
128
|
},
|
|
111
129
|
};
|
|
112
130
|
|
|
131
|
+
function readBodyInput(source) {
|
|
132
|
+
if (source === '-') {
|
|
133
|
+
try { return readFileSync(0, 'utf8'); } catch (err) { die(`Could not read body from stdin: ${err.message}`); }
|
|
134
|
+
}
|
|
135
|
+
if (typeof source === 'string' && source.startsWith('@')) {
|
|
136
|
+
const file = source.slice(1);
|
|
137
|
+
if (!existsSync(file)) die(`Body file not found: ${file}`);
|
|
138
|
+
return readFileSync(file, 'utf8');
|
|
139
|
+
}
|
|
140
|
+
return source;
|
|
141
|
+
}
|
|
142
|
+
|
|
113
143
|
export async function runNew(argv, config, opts = {}) {
|
|
114
144
|
const { dryRun } = opts;
|
|
115
145
|
|
|
116
|
-
// Parse args
|
|
146
|
+
// Parse args. Pull out flags first.
|
|
117
147
|
const positional = [];
|
|
118
|
-
let status =
|
|
148
|
+
let status = null;
|
|
119
149
|
let title = null;
|
|
120
|
-
let templateName = null;
|
|
121
150
|
let rootName = opts.root ?? null;
|
|
151
|
+
let messageFlag = null;
|
|
122
152
|
for (let i = 0; i < argv.length; i++) {
|
|
123
153
|
if (argv[i] === '--status' && argv[i + 1]) { status = argv[++i]; continue; }
|
|
124
154
|
if (argv[i] === '--title' && argv[i + 1]) { title = argv[++i]; continue; }
|
|
125
|
-
if (argv[i] === '--
|
|
155
|
+
if (argv[i] === '--message' && argv[i + 1]) { messageFlag = argv[++i]; continue; }
|
|
126
156
|
if (argv[i] === '--root' && argv[i + 1]) { rootName = argv[++i]; continue; }
|
|
127
157
|
if (argv[i] === '--config') { i++; continue; }
|
|
128
|
-
if (argv[i] === '--list-templates') {
|
|
158
|
+
if (argv[i] === '--list-templates' || argv[i] === '--list-types') {
|
|
129
159
|
listTemplates(config);
|
|
130
160
|
return;
|
|
131
161
|
}
|
|
132
|
-
|
|
162
|
+
// Treat `-` alone (stdin marker) as a positional, not a flag.
|
|
163
|
+
if (!argv[i].startsWith('-') || argv[i] === '-') positional.push(argv[i]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Resolve type vs name:
|
|
167
|
+
// `dotmd new plan auth-revamp` → type=plan, name=auth-revamp
|
|
168
|
+
// `dotmd new auth-revamp` → type=doc (default), name=auth-revamp
|
|
169
|
+
// `dotmd new prompt foo "body"` → type=prompt, name=foo, bodyArg="body"
|
|
170
|
+
const knownTypes = new Set(Object.keys(BUILTIN_TEMPLATES));
|
|
171
|
+
// Also include any custom templates from config
|
|
172
|
+
for (const k of Object.keys(config.raw?.templates ?? {})) knownTypes.add(k);
|
|
173
|
+
|
|
174
|
+
let typeName, name, bodyArg = null;
|
|
175
|
+
if (positional.length >= 1 && knownTypes.has(positional[0])) {
|
|
176
|
+
typeName = positional[0];
|
|
177
|
+
name = positional[1];
|
|
178
|
+
if (positional.length > 2) bodyArg = positional.slice(2).join(' ');
|
|
179
|
+
} else {
|
|
180
|
+
typeName = 'doc';
|
|
181
|
+
name = positional[0];
|
|
182
|
+
if (positional.length > 1) bodyArg = positional.slice(1).join(' ');
|
|
133
183
|
}
|
|
134
184
|
|
|
135
|
-
let name = positional[0];
|
|
136
185
|
if (!name) {
|
|
137
186
|
if (isInteractive()) {
|
|
138
|
-
name = await promptText(
|
|
187
|
+
name = await promptText(`${typeName} name: `);
|
|
139
188
|
if (!name) die('No name provided.');
|
|
140
189
|
} else {
|
|
141
|
-
die(
|
|
190
|
+
die(`Usage: dotmd new <type> <name> [body]\n types: ${[...knownTypes].join(', ')}\n body: inline text | "-" (stdin) | "@path" (file) | --message "..."`);
|
|
142
191
|
}
|
|
143
192
|
}
|
|
144
193
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
194
|
+
// Resolve template (by type name, falls back to lookup)
|
|
195
|
+
const template = resolveTemplate(typeName, config);
|
|
196
|
+
|
|
197
|
+
// Validate status (template default first, then per-type list, then 'active')
|
|
198
|
+
if (!status) {
|
|
199
|
+
if (typeof template === 'object' && template.defaultStatus) {
|
|
200
|
+
status = template.defaultStatus;
|
|
201
|
+
} else {
|
|
202
|
+
const typeStatuses = config.typeStatuses?.get(typeName);
|
|
203
|
+
status = typeStatuses && typeStatuses.size > 0 ? [...typeStatuses][0] : 'active';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const effective = config.typeStatuses?.get(typeName) ?? config.validStatuses;
|
|
207
|
+
if (!effective.has(status)) {
|
|
208
|
+
die(`Invalid status \`${status}\` for type \`${typeName}\`\nValid: ${[...effective].join(', ')}`);
|
|
148
209
|
}
|
|
149
210
|
|
|
150
|
-
//
|
|
151
|
-
|
|
211
|
+
// Body input resolution: messageFlag > bodyArg > nothing
|
|
212
|
+
let bodyInput = null;
|
|
213
|
+
if (messageFlag !== null) bodyInput = readBodyInput(messageFlag);
|
|
214
|
+
else if (bodyArg !== null) bodyInput = readBodyInput(bodyArg);
|
|
215
|
+
|
|
216
|
+
if (template.requiresBody && (!bodyInput || !bodyInput.trim())) {
|
|
217
|
+
die(`\`${typeName}\` template requires a body. Pass inline, --message "...", - for stdin, or @path for a file.`);
|
|
218
|
+
}
|
|
152
219
|
|
|
153
220
|
// If name contains path separators, split into directory prefix and basename
|
|
154
221
|
let nameDir = null;
|
|
@@ -179,7 +246,12 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
179
246
|
targetRoot = match;
|
|
180
247
|
}
|
|
181
248
|
|
|
182
|
-
//
|
|
249
|
+
// Template-declared subdirectory (e.g., prompt → 'prompts')
|
|
250
|
+
if (typeof template === 'object' && template.dir && !nameDir) {
|
|
251
|
+
nameDir = path.join(path.relative(config.repoRoot, targetRoot), template.dir);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Path — if user provided a directory prefix OR template declared one, resolve relative to repoRoot
|
|
183
255
|
const baseDir = nameDir ? path.resolve(config.repoRoot, nameDir) : targetRoot;
|
|
184
256
|
const filePath = path.join(baseDir, slug + '.md');
|
|
185
257
|
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
@@ -192,26 +264,28 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
192
264
|
|
|
193
265
|
// Generate content
|
|
194
266
|
let content;
|
|
267
|
+
const tmplCtx = { status, title: docTitle, today, bodyInput };
|
|
195
268
|
if (typeof template === 'function') {
|
|
196
|
-
content = template(name,
|
|
269
|
+
content = template(name, tmplCtx);
|
|
197
270
|
} else {
|
|
198
|
-
const fm = template.frontmatter(status, today);
|
|
199
|
-
const body = template.body(docTitle,
|
|
271
|
+
const fm = template.frontmatter(status, today, tmplCtx);
|
|
272
|
+
const body = template.body(docTitle, tmplCtx);
|
|
200
273
|
content = `---\n${fm}\n---\n${body}`;
|
|
201
274
|
}
|
|
202
275
|
|
|
203
276
|
if (dryRun) {
|
|
204
277
|
process.stdout.write(`${dim('[dry-run]')} Would create: ${repoPath}\n`);
|
|
205
|
-
|
|
278
|
+
process.stdout.write(`${dim('[dry-run]')} Type: ${typeName}\n`);
|
|
206
279
|
return;
|
|
207
280
|
}
|
|
208
281
|
|
|
282
|
+
// Ensure parent dir exists (templates with `dir:` may target a new subdirectory)
|
|
283
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
284
|
+
|
|
209
285
|
writeFileSync(filePath, content, 'utf8');
|
|
210
|
-
process.stdout.write(`${green('Created')}: ${repoPath}`);
|
|
211
|
-
if (templateName) process.stdout.write(` ${dim(`(template: ${templateName})`)}`);
|
|
212
|
-
process.stdout.write('\n');
|
|
286
|
+
process.stdout.write(`${green('Created')}: ${repoPath} ${dim(`(${typeName})`)}\n`);
|
|
213
287
|
|
|
214
|
-
try { config.hooks.onNew?.({ path: repoPath, status, title: docTitle,
|
|
288
|
+
try { config.hooks.onNew?.({ path: repoPath, status, title: docTitle, type: typeName }); } catch (err) { warn(`Hook 'onNew' threw: ${err.message}`); }
|
|
215
289
|
}
|
|
216
290
|
|
|
217
291
|
function resolveTemplate(name, config) {
|
|
@@ -221,7 +295,7 @@ function resolveTemplate(name, config) {
|
|
|
221
295
|
if (BUILTIN_TEMPLATES[name]) return BUILTIN_TEMPLATES[name];
|
|
222
296
|
|
|
223
297
|
const available = [...new Set([...Object.keys(BUILTIN_TEMPLATES), ...Object.keys(configTemplates)])];
|
|
224
|
-
die(`Unknown
|
|
298
|
+
die(`Unknown type: ${name}\nAvailable: ${available.join(', ')}`);
|
|
225
299
|
}
|
|
226
300
|
|
|
227
301
|
function listTemplates(config) {
|
|
@@ -231,7 +305,7 @@ function listTemplates(config) {
|
|
|
231
305
|
all[k] = v;
|
|
232
306
|
}
|
|
233
307
|
|
|
234
|
-
process.stdout.write(bold('Available
|
|
308
|
+
process.stdout.write(bold('Available types') + '\n\n');
|
|
235
309
|
for (const [name, tmpl] of Object.entries(all)) {
|
|
236
310
|
const desc = typeof tmpl === 'function'
|
|
237
311
|
? '(custom function)'
|