dotmd-cli 0.11.0 → 0.12.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 +2 -0
- package/bin/dotmd.mjs +34 -8
- package/dotmd.config.example.mjs +2 -0
- package/package.json +1 -1
- package/src/claude-commands.mjs +151 -0
- package/src/completions.mjs +3 -1
- package/src/doctor.mjs +17 -3
- package/src/index.mjs +4 -0
- package/src/init.mjs +14 -1
- package/src/lifecycle.mjs +112 -0
package/README.md
CHANGED
|
@@ -120,6 +120,8 @@ dotmd plans List all plans
|
|
|
120
120
|
dotmd stale List stale docs
|
|
121
121
|
dotmd actionable List docs with next steps
|
|
122
122
|
dotmd index [--write] Generate/update docs.md index block
|
|
123
|
+
dotmd pickup <file> Pick up a plan (in-session + print)
|
|
124
|
+
dotmd finish <file> Finish a plan (done or active)
|
|
123
125
|
dotmd status <file> <status> Transition document status
|
|
124
126
|
dotmd archive <file> Archive (status + move + update refs)
|
|
125
127
|
dotmd touch <file> Bump updated date
|
package/bin/dotmd.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { buildIndex } from '../src/index.mjs';
|
|
|
8
8
|
import { renderCompactList, renderVerboseList, renderContext, renderCheck, renderCoverage, buildCoverage } from '../src/render.mjs';
|
|
9
9
|
import { renderIndexFile, writeIndex } from '../src/index-file.mjs';
|
|
10
10
|
import { runFocus, runQuery } from '../src/query.mjs';
|
|
11
|
-
import { runStatus, runArchive, runTouch, runBulkArchive } from '../src/lifecycle.mjs';
|
|
11
|
+
import { runStatus, runArchive, runTouch, runBulkArchive, runPickup, runFinish } from '../src/lifecycle.mjs';
|
|
12
12
|
import { runInit } from '../src/init.mjs';
|
|
13
13
|
import { runNew } from '../src/new.mjs';
|
|
14
14
|
import { runCompletions } from '../src/completions.mjs';
|
|
@@ -62,6 +62,8 @@ Validate & Fix:
|
|
|
62
62
|
fix-refs [--dry-run] Auto-fix broken reference paths + body links
|
|
63
63
|
|
|
64
64
|
Lifecycle:
|
|
65
|
+
pickup <file> Pick up a plan (set in-session + print)
|
|
66
|
+
finish <file> [done|active] Finish a plan (set done or active)
|
|
65
67
|
status <file> <status> Transition document status
|
|
66
68
|
archive <file> Archive (status + move + update refs)
|
|
67
69
|
bulk archive <f1> <f2> ... Archive multiple files at once
|
|
@@ -130,6 +132,28 @@ Filters:
|
|
|
130
132
|
--summarize-limit <n> Max docs to summarize (default: 5)
|
|
131
133
|
--model <name> Model for AI summaries`,
|
|
132
134
|
|
|
135
|
+
pickup: `dotmd pickup <file> — pick up a plan and start working
|
|
136
|
+
|
|
137
|
+
Sets the plan to in-session and prints its content.
|
|
138
|
+
Fails if the plan is already in-session, blocked, done, or archived.
|
|
139
|
+
|
|
140
|
+
Options:
|
|
141
|
+
--json Output as JSON
|
|
142
|
+
--dry-run, -n Preview without writing
|
|
143
|
+
|
|
144
|
+
If no file is given, prompts with a list of active plans.`,
|
|
145
|
+
|
|
146
|
+
finish: `dotmd finish <file> [done|active] — finish working on a plan
|
|
147
|
+
|
|
148
|
+
Sets the plan status to done (default) or back to active.
|
|
149
|
+
Only works on plans currently in-session.
|
|
150
|
+
|
|
151
|
+
Options:
|
|
152
|
+
--json Output as JSON
|
|
153
|
+
--dry-run, -n Preview without writing
|
|
154
|
+
|
|
155
|
+
If no file is given, prompts with a list of in-session plans.`,
|
|
156
|
+
|
|
133
157
|
status: `dotmd status <file> <new-status> — transition document status
|
|
134
158
|
|
|
135
159
|
Moves the document to the new status. If transitioning to an archive
|
|
@@ -393,12 +417,6 @@ async function main() {
|
|
|
393
417
|
return;
|
|
394
418
|
}
|
|
395
419
|
|
|
396
|
-
// Init and completions don't need config
|
|
397
|
-
if (command === 'init') {
|
|
398
|
-
runInit(process.cwd());
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
420
|
if (command === 'completions') {
|
|
403
421
|
runCompletions(args.slice(1));
|
|
404
422
|
return;
|
|
@@ -418,6 +436,12 @@ async function main() {
|
|
|
418
436
|
|
|
419
437
|
const config = await resolveConfig(process.cwd(), explicitConfig);
|
|
420
438
|
|
|
439
|
+
// Init — now has access to config for Claude command generation
|
|
440
|
+
if (command === 'init') {
|
|
441
|
+
runInit(process.cwd(), config.configFound ? config : null);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
421
445
|
// Watch is a pure proxy — pass raw args so the child process gets all flags
|
|
422
446
|
if (command === 'watch') { runWatch(args.slice(1), config); return; }
|
|
423
447
|
|
|
@@ -468,6 +492,8 @@ async function main() {
|
|
|
468
492
|
if (command === 'notion') { await runNotion(restArgs, config, { dryRun }); return; }
|
|
469
493
|
|
|
470
494
|
// Lifecycle commands
|
|
495
|
+
if (command === 'pickup') { await runPickup(restArgs, config, { dryRun }); return; }
|
|
496
|
+
if (command === 'finish') { await runFinish(restArgs, config, { dryRun }); return; }
|
|
471
497
|
if (command === 'status') { await runStatus(restArgs, config, { dryRun }); return; }
|
|
472
498
|
if (command === 'archive') { runArchive(restArgs, config, { dryRun }); return; }
|
|
473
499
|
if (command === 'bulk' && restArgs[0] === 'archive') { runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
|
|
@@ -698,7 +724,7 @@ async function main() {
|
|
|
698
724
|
// Unknown command — suggest closest match
|
|
699
725
|
const allCommands = [
|
|
700
726
|
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'context',
|
|
701
|
-
'focus', 'query', 'plans', 'stale', 'actionable', 'index', 'status', 'archive', 'touch', 'doctor',
|
|
727
|
+
'focus', 'query', 'plans', 'stale', 'actionable', 'index', 'pickup', 'finish', 'status', 'archive', 'touch', 'doctor',
|
|
702
728
|
'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
|
|
703
729
|
'watch', 'diff', 'new', 'init', 'completions',
|
|
704
730
|
];
|
package/dotmd.config.example.mjs
CHANGED
|
@@ -147,6 +147,8 @@ export const presets = {
|
|
|
147
147
|
// export function onNew({ path, status, title, template }) {}
|
|
148
148
|
// export function onRename({ oldPath, newPath, referencesUpdated }) {}
|
|
149
149
|
// export function onLint({ path, fixes }) {}
|
|
150
|
+
// export function onPickup({ path, oldStatus, newStatus }) {}
|
|
151
|
+
// export function onFinish({ path, oldStatus, newStatus }) {}
|
|
150
152
|
|
|
151
153
|
// AI hooks — override summarization (replaces local MLX model).
|
|
152
154
|
// export function summarizeDoc(body, meta) { return 'Custom summary'; }
|
package/package.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { green, dim, yellow } from './color.mjs';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
8
|
+
const VERSION_MARKER = `<!-- dotmd-generated: ${pkg.version} -->`;
|
|
9
|
+
const VERSION_REGEX = /^<!-- dotmd-generated: ([\d.]+) -->/;
|
|
10
|
+
|
|
11
|
+
function generatePlansCommand(config) {
|
|
12
|
+
const lines = [VERSION_MARKER, ''];
|
|
13
|
+
lines.push('Run `dotmd context` to get the current plans briefing, then use it to orient yourself.');
|
|
14
|
+
lines.push('');
|
|
15
|
+
lines.push(`Plans are managed by **dotmd** (v${pkg.version}). Config at \`dotmd.config.mjs\`. Always use \`dotmd\` directly.`);
|
|
16
|
+
lines.push('');
|
|
17
|
+
lines.push('Plan-specific commands:');
|
|
18
|
+
lines.push('- `dotmd context` — briefing with active/paused/ready plans, age tags, next steps');
|
|
19
|
+
lines.push('- `dotmd health` — plan velocity, aging, checklist progress, pipeline view');
|
|
20
|
+
lines.push('- `dotmd unblocks <file>` — what depends on / is blocked by a plan');
|
|
21
|
+
lines.push('- `dotmd next` — ready plans with next steps (what to promote)');
|
|
22
|
+
lines.push('- `dotmd new <name> --template plan` — scaffold with full phase structure');
|
|
23
|
+
lines.push('- `dotmd archive <file>` — archive with auto ref-fixing (both directions)');
|
|
24
|
+
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
25
|
+
lines.push('- `dotmd status <file> <status>` — transition status');
|
|
26
|
+
lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
|
|
27
|
+
|
|
28
|
+
if (config.raw?.glossary) {
|
|
29
|
+
lines.push('- `dotmd glossary <term>` — domain term lookup with related plans');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push('If the user asks about a specific plan, read its file directly (path is in the briefing or findable via `dotmd query --keyword <term>`).');
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push('If the user asks to change a plan\'s status, use `dotmd status <file> <status>`.');
|
|
36
|
+
lines.push('If the user asks to archive a plan, use `dotmd archive <file>`.');
|
|
37
|
+
lines.push('');
|
|
38
|
+
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function generateDocsCommand(config) {
|
|
43
|
+
const roots = Array.isArray(config.raw?.root) ? config.raw.root : [config.raw?.root ?? 'docs'];
|
|
44
|
+
const rootCount = roots.length;
|
|
45
|
+
|
|
46
|
+
const lines = [VERSION_MARKER, ''];
|
|
47
|
+
lines.push(`All documentation in this repo is managed by **dotmd** (v${pkg.version}). Docs across ${rootCount} root${rootCount > 1 ? 's' : ''}: ${roots.join(', ')}. Config at \`dotmd.config.mjs\`.`);
|
|
48
|
+
lines.push('');
|
|
49
|
+
|
|
50
|
+
// Document types from config
|
|
51
|
+
const types = config.raw?.types ? Object.keys(config.raw.types) : [];
|
|
52
|
+
if (types.length > 0) {
|
|
53
|
+
lines.push(`Document types: ${types.map(t => '`' + t + '`').join(', ')}.`);
|
|
54
|
+
lines.push('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lines.push('Commands for working with docs:');
|
|
58
|
+
lines.push('- `dotmd context` — LLM-oriented briefing across all types');
|
|
59
|
+
lines.push('- `dotmd check` — validate frontmatter, refs, body links (target: 0 errors)');
|
|
60
|
+
lines.push('- `dotmd doctor` — auto-fix everything in one pass (refs, lint, dates, index)');
|
|
61
|
+
lines.push('- `dotmd query [filters]` — search by status, keyword, module, surface, type, staleness');
|
|
62
|
+
lines.push('- `dotmd health` — plan pipeline, velocity, aging');
|
|
63
|
+
lines.push('- `dotmd stats` — doc health dashboard (completeness, checklists, audit coverage)');
|
|
64
|
+
lines.push('- `dotmd graph [--dot]` — visualize document relationships');
|
|
65
|
+
lines.push('- `dotmd deps [file]` — dependency tree');
|
|
66
|
+
lines.push('- `dotmd unblocks <file>` — impact analysis for a doc');
|
|
67
|
+
lines.push('- `dotmd diff [file]` — git changes since last updated date');
|
|
68
|
+
lines.push('- `dotmd list` — all docs grouped by status');
|
|
69
|
+
lines.push('- `dotmd focus <status>` — detailed view for one status group');
|
|
70
|
+
|
|
71
|
+
if (config.raw?.glossary) {
|
|
72
|
+
lines.push('- `dotmd glossary <term>` — domain term lookup with related docs and plans');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push('Lifecycle:');
|
|
77
|
+
lines.push('- `dotmd new <name> --template plan` — scaffold new plan');
|
|
78
|
+
lines.push('- `dotmd status <file> <status>` — transition status');
|
|
79
|
+
lines.push('- `dotmd archive <file>` — archive with auto ref-fixing');
|
|
80
|
+
lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
|
|
81
|
+
lines.push('- `dotmd touch --git` — bulk-sync updated dates from git history');
|
|
82
|
+
lines.push('- `dotmd lint --fix` — auto-fix frontmatter issues');
|
|
83
|
+
lines.push('- `dotmd fix-refs` — repair broken references and body links');
|
|
84
|
+
lines.push('- `dotmd rename <old> <new>` — rename doc + update all references');
|
|
85
|
+
lines.push('');
|
|
86
|
+
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getInstalledVersion(filePath) {
|
|
91
|
+
if (!existsSync(filePath)) return null;
|
|
92
|
+
const content = readFileSync(filePath, 'utf8');
|
|
93
|
+
const match = content.match(VERSION_REGEX);
|
|
94
|
+
return match ? match[1] : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function scaffoldClaudeCommands(cwd, config) {
|
|
98
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
99
|
+
if (!existsSync(claudeDir)) return [];
|
|
100
|
+
|
|
101
|
+
const commandsDir = path.join(claudeDir, 'commands');
|
|
102
|
+
const results = [];
|
|
103
|
+
|
|
104
|
+
const files = [
|
|
105
|
+
{ name: 'plans.md', generate: () => generatePlansCommand(config) },
|
|
106
|
+
{ name: 'docs.md', generate: () => generateDocsCommand(config) },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const { name, generate } of files) {
|
|
110
|
+
const filePath = path.join(commandsDir, name);
|
|
111
|
+
const installedVersion = getInstalledVersion(filePath);
|
|
112
|
+
|
|
113
|
+
if (installedVersion === pkg.version) {
|
|
114
|
+
results.push({ name, action: 'current' });
|
|
115
|
+
} else if (installedVersion) {
|
|
116
|
+
// Outdated — regenerate
|
|
117
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
118
|
+
writeFileSync(filePath, generate(), 'utf8');
|
|
119
|
+
results.push({ name, action: 'updated', from: installedVersion, to: pkg.version });
|
|
120
|
+
} else if (!existsSync(filePath)) {
|
|
121
|
+
// New — create
|
|
122
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
123
|
+
writeFileSync(filePath, generate(), 'utf8');
|
|
124
|
+
results.push({ name, action: 'created' });
|
|
125
|
+
} else {
|
|
126
|
+
// File exists but no version marker — user-managed, don't touch
|
|
127
|
+
results.push({ name, action: 'skipped' });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function checkClaudeCommands(cwd) {
|
|
135
|
+
const commandsDir = path.join(cwd, '.claude', 'commands');
|
|
136
|
+
if (!existsSync(commandsDir)) return [];
|
|
137
|
+
|
|
138
|
+
const warnings = [];
|
|
139
|
+
for (const name of ['plans.md', 'docs.md']) {
|
|
140
|
+
const filePath = path.join(commandsDir, name);
|
|
141
|
+
const installedVersion = getInstalledVersion(filePath);
|
|
142
|
+
if (installedVersion && installedVersion !== pkg.version) {
|
|
143
|
+
warnings.push({
|
|
144
|
+
path: `.claude/commands/${name}`,
|
|
145
|
+
level: 'warning',
|
|
146
|
+
message: `Claude command outdated (v${installedVersion} → v${pkg.version}). Run \`dotmd doctor\` to update.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return warnings;
|
|
151
|
+
}
|
package/src/completions.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { die } from './util.mjs';
|
|
|
2
2
|
|
|
3
3
|
const COMMANDS = [
|
|
4
4
|
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'unblocks', 'health', 'glossary', 'context', 'focus', 'query',
|
|
5
|
-
'plans', 'stale', 'actionable', 'index', 'status', 'archive', 'bulk', 'touch', 'doctor', 'lint', 'rename', 'migrate',
|
|
5
|
+
'plans', 'stale', 'actionable', 'index', 'pickup', 'finish', 'status', 'archive', 'bulk', 'touch', 'doctor', 'lint', 'rename', 'migrate',
|
|
6
6
|
'fix-refs', 'notion', 'export', 'summary', 'watch', 'diff', 'init', 'new', 'completions',
|
|
7
7
|
];
|
|
8
8
|
|
|
@@ -32,6 +32,8 @@ const COMMAND_FLAGS = {
|
|
|
32
32
|
plans: ['--status', '--json', '--sort', '--limit', '--all', '--stale', '--has-next-step'],
|
|
33
33
|
stale: ['--json', '--sort', '--limit', '--all'],
|
|
34
34
|
actionable: ['--json', '--sort', '--limit', '--all'],
|
|
35
|
+
pickup: ['--json'],
|
|
36
|
+
finish: ['--json'],
|
|
35
37
|
status: [],
|
|
36
38
|
archive: [],
|
|
37
39
|
doctor: [],
|
package/src/doctor.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { runTouch } from './lifecycle.mjs';
|
|
|
4
4
|
import { buildIndex } from './index.mjs';
|
|
5
5
|
import { renderIndexFile, writeIndex } from './index-file.mjs';
|
|
6
6
|
import { renderCheck } from './render.mjs';
|
|
7
|
-
import { bold } from './color.mjs';
|
|
7
|
+
import { bold, dim, green, yellow } from './color.mjs';
|
|
8
|
+
import { scaffoldClaudeCommands } from './claude-commands.mjs';
|
|
8
9
|
|
|
9
10
|
export function runDoctor(argv, config, opts = {}) {
|
|
10
11
|
const { dryRun } = opts;
|
|
@@ -34,8 +35,21 @@ export function runDoctor(argv, config, opts = {}) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
// Step 5:
|
|
38
|
-
|
|
38
|
+
// Step 5: Refresh Claude Code commands
|
|
39
|
+
const claudeResults = dryRun ? [] : scaffoldClaudeCommands(config.repoRoot, config);
|
|
40
|
+
if (claudeResults.some(r => r.action !== 'current' && r.action !== 'skipped')) {
|
|
41
|
+
process.stdout.write('\n' + bold('5. Claude Code commands:') + '\n');
|
|
42
|
+
for (const r of claudeResults) {
|
|
43
|
+
if (r.action === 'updated') {
|
|
44
|
+
process.stdout.write(`${green('Updated')} .claude/commands/${r.name} (v${r.from} → v${r.to})\n`);
|
|
45
|
+
} else if (r.action === 'created') {
|
|
46
|
+
process.stdout.write(`${green('Created')} .claude/commands/${r.name}\n`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Step 6: Show remaining check
|
|
52
|
+
process.stdout.write('\n' + bold('6. Remaining issues:') + '\n');
|
|
39
53
|
const freshIndex = buildIndex(config);
|
|
40
54
|
process.stdout.write(renderCheck(freshIndex, config));
|
|
41
55
|
}
|
package/src/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { extractFirstHeading, extractSummary, extractStatusSnapshot, extractNext
|
|
|
5
5
|
import { asString, normalizeStringList, normalizeBlockers, mergeUniqueStrings, toRepoPath, warn } from './util.mjs';
|
|
6
6
|
import { validateDoc, checkBidirectionalReferences, checkGitStaleness, computeDaysSinceUpdate, computeIsStale, computeChecklistCompletionRate } from './validate.mjs';
|
|
7
7
|
import { checkIndex } from './index-file.mjs';
|
|
8
|
+
import { checkClaudeCommands } from './claude-commands.mjs';
|
|
8
9
|
|
|
9
10
|
export function buildIndex(config) {
|
|
10
11
|
const docs = collectDocFiles(config).map(f => parseDocFile(f, config));
|
|
@@ -70,6 +71,9 @@ export function buildIndex(config) {
|
|
|
70
71
|
const gitWarnings = checkGitStaleness(transformedDocs, config);
|
|
71
72
|
warnings.push(...gitWarnings);
|
|
72
73
|
|
|
74
|
+
const claudeWarnings = checkClaudeCommands(config.repoRoot);
|
|
75
|
+
warnings.push(...claudeWarnings);
|
|
76
|
+
|
|
73
77
|
return {
|
|
74
78
|
generatedAt: new Date().toISOString(),
|
|
75
79
|
docs: transformedDocs,
|
package/src/init.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
4
|
import { green, dim } from './color.mjs';
|
|
5
|
+
import { scaffoldClaudeCommands } from './claude-commands.mjs';
|
|
5
6
|
|
|
6
7
|
const STARTER_CONFIG = `// dotmd.config.mjs — document management configuration
|
|
7
8
|
// All exports are optional. See dotmd.config.example.mjs for full reference.
|
|
@@ -103,7 +104,7 @@ function generateDetectedConfig(scan, rootPath) {
|
|
|
103
104
|
return lines.join('\n');
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
export function runInit(cwd) {
|
|
107
|
+
export function runInit(cwd, config) {
|
|
107
108
|
const configPath = path.join(cwd, 'dotmd.config.mjs');
|
|
108
109
|
const docsDir = path.join(cwd, 'docs');
|
|
109
110
|
const indexPath = path.join(docsDir, 'docs.md');
|
|
@@ -137,6 +138,18 @@ export function runInit(cwd) {
|
|
|
137
138
|
process.stdout.write(` ${green('create')} docs/docs.md\n`);
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
// Claude Code integration — auto-detect .claude/ directory
|
|
142
|
+
if (config) {
|
|
143
|
+
const results = scaffoldClaudeCommands(cwd, config);
|
|
144
|
+
for (const r of results) {
|
|
145
|
+
if (r.action === 'created') {
|
|
146
|
+
process.stdout.write(` ${green('create')} .claude/commands/${r.name}\n`);
|
|
147
|
+
} else if (r.action === 'current') {
|
|
148
|
+
process.stdout.write(` ${dim('current')} .claude/commands/${r.name}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
process.stdout.write(`\nReady. Create your first doc:\n`);
|
|
141
154
|
process.stdout.write(` dotmd new my-doc\n`);
|
|
142
155
|
process.stdout.write(` dotmd list\n\n`);
|
package/src/lifecycle.mjs
CHANGED
|
@@ -119,6 +119,118 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
119
119
|
}); } catch (err) { warn(`Hook 'onStatusChange' threw: ${err.message}`); }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
export async function runPickup(argv, config, opts = {}) {
|
|
123
|
+
const { dryRun } = opts;
|
|
124
|
+
const json = argv.includes('--json');
|
|
125
|
+
let input = argv.find(a => !a.startsWith('-'));
|
|
126
|
+
|
|
127
|
+
// Interactive: pick from active plans
|
|
128
|
+
if (!input) {
|
|
129
|
+
if (!isInteractive()) die('Usage: dotmd pickup <file>');
|
|
130
|
+
const index = buildIndex(config);
|
|
131
|
+
const active = index.docs.filter(d => d.type === 'plan' && (d.status === 'active' || d.status === 'planned'));
|
|
132
|
+
if (active.length === 0) die('No active or planned plans to pick up.');
|
|
133
|
+
const choice = await promptChoice('Pick a plan:', active.map(d => `${d.title} (${d.status}) — ${d.path}`));
|
|
134
|
+
if (!choice) die('No plan selected.');
|
|
135
|
+
const idx = active.findIndex((_, i) => choice === `${active[i].title} (${active[i].status}) — ${active[i].path}`);
|
|
136
|
+
if (idx === -1) die('No plan selected.');
|
|
137
|
+
input = active[idx].path;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const filePath = resolveDocPath(input, config);
|
|
141
|
+
if (!filePath) die(`File not found: ${input}`);
|
|
142
|
+
|
|
143
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
144
|
+
const { frontmatter: fmRaw, body } = extractFrontmatter(raw);
|
|
145
|
+
const parsedFm = parseSimpleFrontmatter(fmRaw);
|
|
146
|
+
const docType = asString(parsedFm.type) ?? null;
|
|
147
|
+
const oldStatus = asString(parsedFm.status);
|
|
148
|
+
const title = asString(parsedFm.title) ?? path.basename(filePath, '.md');
|
|
149
|
+
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
150
|
+
|
|
151
|
+
if (docType && docType !== 'plan') warn(`${repoPath} has type '${docType}', not 'plan'.`);
|
|
152
|
+
|
|
153
|
+
if (oldStatus === 'in-session') die(`Already in-session — another Claude instance may be working on this.\n ${repoPath}`);
|
|
154
|
+
if (oldStatus === 'blocked') {
|
|
155
|
+
const blockers = parsedFm.blockers ? (Array.isArray(parsedFm.blockers) ? parsedFm.blockers.join(', ') : String(parsedFm.blockers)) : 'unknown';
|
|
156
|
+
die(`Plan is blocked: ${blockers}\n ${repoPath}`);
|
|
157
|
+
}
|
|
158
|
+
const pickupable = new Set(['active', 'planned']);
|
|
159
|
+
if (oldStatus && !pickupable.has(oldStatus)) die(`Cannot pick up a plan with status '${oldStatus}'. Must be active or planned.\n ${repoPath}`);
|
|
160
|
+
|
|
161
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
162
|
+
|
|
163
|
+
if (dryRun) {
|
|
164
|
+
process.stderr.write(`${dim('[dry-run]')} Would update: status: ${oldStatus} → in-session, updated: ${today}\n`);
|
|
165
|
+
} else {
|
|
166
|
+
updateFrontmatter(filePath, { status: 'in-session', updated: today });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (json) {
|
|
170
|
+
process.stdout.write(JSON.stringify({ path: repoPath, oldStatus, newStatus: 'in-session', title, body: body?.trim() ?? '' }, null, 2) + '\n');
|
|
171
|
+
} else {
|
|
172
|
+
process.stderr.write(`${green('▶ Picked up')}: ${repoPath} (${oldStatus} → in-session)\n\n`);
|
|
173
|
+
if (body?.trim()) process.stdout.write(body.trim() + '\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try { config.hooks.onPickup?.({ path: repoPath, oldStatus, newStatus: 'in-session' }); } catch (err) { warn(`Hook 'onPickup' threw: ${err.message}`); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function runFinish(argv, config, opts = {}) {
|
|
180
|
+
const { dryRun } = opts;
|
|
181
|
+
const json = argv.includes('--json');
|
|
182
|
+
const positional = argv.filter(a => !a.startsWith('-'));
|
|
183
|
+
let input = positional[0];
|
|
184
|
+
const targetStatus = positional[1] ?? 'done';
|
|
185
|
+
|
|
186
|
+
if (!['done', 'active'].includes(targetStatus)) die(`Invalid finish status: ${targetStatus}. Use 'done' or 'active'.`);
|
|
187
|
+
|
|
188
|
+
// Interactive: pick from in-session plans
|
|
189
|
+
if (!input) {
|
|
190
|
+
if (!isInteractive()) die('Usage: dotmd finish <file> [done|active]');
|
|
191
|
+
const index = buildIndex(config);
|
|
192
|
+
const inSession = index.docs.filter(d => d.status === 'in-session');
|
|
193
|
+
if (inSession.length === 0) die('No plans currently in-session.');
|
|
194
|
+
if (inSession.length === 1) {
|
|
195
|
+
input = inSession[0].path;
|
|
196
|
+
process.stderr.write(`${dim(`Auto-selected: ${input}`)}\n`);
|
|
197
|
+
} else {
|
|
198
|
+
const choice = await promptChoice('Finish which plan:', inSession.map(d => `${d.title} — ${d.path}`));
|
|
199
|
+
if (!choice) die('No plan selected.');
|
|
200
|
+
const idx = inSession.findIndex((_, i) => choice === `${inSession[i].title} — ${inSession[i].path}`);
|
|
201
|
+
if (idx === -1) die('No plan selected.');
|
|
202
|
+
input = inSession[idx].path;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const filePath = resolveDocPath(input, config);
|
|
207
|
+
if (!filePath) die(`File not found: ${input}`);
|
|
208
|
+
|
|
209
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
210
|
+
const { frontmatter: fmRaw } = extractFrontmatter(raw);
|
|
211
|
+
const parsedFm = parseSimpleFrontmatter(fmRaw);
|
|
212
|
+
const oldStatus = asString(parsedFm.status);
|
|
213
|
+
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
214
|
+
|
|
215
|
+
if (oldStatus !== 'in-session') die(`Plan is not in-session (current: ${oldStatus}).\n ${repoPath}`);
|
|
216
|
+
|
|
217
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
218
|
+
|
|
219
|
+
if (dryRun) {
|
|
220
|
+
process.stderr.write(`${dim('[dry-run]')} Would update: status: in-session → ${targetStatus}, updated: ${today}\n`);
|
|
221
|
+
} else {
|
|
222
|
+
updateFrontmatter(filePath, { status: targetStatus, updated: today });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (json) {
|
|
226
|
+
process.stdout.write(JSON.stringify({ path: repoPath, oldStatus, newStatus: targetStatus }, null, 2) + '\n');
|
|
227
|
+
} else {
|
|
228
|
+
process.stdout.write(`${green('✓ Finished')}: ${repoPath} (in-session → ${targetStatus})\n`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try { config.hooks.onFinish?.({ path: repoPath, oldStatus, newStatus: targetStatus }); } catch (err) { warn(`Hook 'onFinish' threw: ${err.message}`); }
|
|
232
|
+
}
|
|
233
|
+
|
|
122
234
|
export function runArchive(argv, config, opts = {}) {
|
|
123
235
|
const { dryRun } = opts;
|
|
124
236
|
const input = argv[0];
|