dotmd-cli 0.68.0 → 0.70.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 +144 -964
- package/bin/dotmd.mjs +241 -197
- package/dotmd.config.example.mjs +5 -8
- package/package.json +6 -10
- package/src/agent-context.mjs +132 -0
- package/src/atomic-mutation.mjs +1505 -0
- package/src/baton.mjs +109 -87
- package/src/bulk-tag.mjs +7 -7
- package/src/commands.mjs +326 -12
- package/src/completions.mjs +38 -98
- package/src/config.mjs +18 -3
- package/src/diff.mjs +7 -3
- package/src/doctor.mjs +12 -5
- package/src/export.mjs +154 -25
- package/src/fix-refs.mjs +2 -0
- package/src/frontmatter-fix.mjs +2 -0
- package/src/frontmatter.mjs +3 -2
- package/src/git.mjs +531 -14
- package/src/graph.mjs +53 -25
- package/src/guard.mjs +163 -60
- package/src/hud.mjs +65 -76
- package/src/index-file.mjs +28 -16
- package/src/index.mjs +17 -12
- package/src/init.mjs +1 -1
- package/src/journal.mjs +145 -12
- package/src/lifecycle.mjs +554 -282
- package/src/lint.mjs +57 -9
- package/src/managed-path.mjs +192 -0
- package/src/migrate-prompts.mjs +2 -0
- package/src/migrate-template.mjs +2 -0
- package/src/migrate.mjs +7 -1
- package/src/new.mjs +135 -54
- package/src/output-identity.mjs +106 -0
- package/src/pickup-card.mjs +24 -10
- package/src/pickup.mjs +457 -0
- package/src/prompts.mjs +138 -32
- package/src/query.mjs +22 -10
- package/src/reference-planner.mjs +292 -0
- package/src/rename.mjs +65 -73
- package/src/render.mjs +17 -8
- package/src/runlist.mjs +109 -71
- package/src/section.mjs +2 -1
- package/src/ship.mjs +39 -20
- package/src/stats.mjs +1 -1
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +11 -26
- package/src/summary.mjs +14 -3
- package/src/update.mjs +38 -10
- package/src/use.mjs +4 -1
- package/src/util.mjs +1 -0
- package/src/validate.mjs +14 -6
- package/src/watch.mjs +6 -1
- package/src/notion.mjs +0 -528
package/src/statuses.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { collectDocFiles } from './index.mjs';
|
|
|
14
14
|
import { asString, toRepoPath, die } from './util.mjs';
|
|
15
15
|
import { bold, dim, green, yellow } from './color.mjs';
|
|
16
16
|
import { isInteractive, promptText } from './prompt.mjs';
|
|
17
|
+
import { resolveStatusMetadata } from './status-metadata.mjs';
|
|
17
18
|
import {
|
|
18
19
|
parseStatusesBlock,
|
|
19
20
|
renderEntryLine,
|
|
@@ -95,34 +96,18 @@ function runListStatuses(args, config) {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function describeTypeStatuses(typeName, config) {
|
|
98
|
-
const typeDef = config.raw?.types?.[typeName];
|
|
99
|
-
if (!typeDef) return {};
|
|
100
99
|
const result = {};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const statusList = Array.isArray(typeDef.statuses) ? typeDef.statuses : Object.keys(typeDef.statuses ?? {});
|
|
104
|
-
const ctx = typeDef.context ?? {};
|
|
105
|
-
const ctxByStatus = {};
|
|
106
|
-
for (const [bucket, names] of Object.entries(ctx)) {
|
|
107
|
-
for (const n of names) ctxByStatus[n] = bucket;
|
|
108
|
-
}
|
|
109
|
-
const stale = typeDef.staleDays ?? {};
|
|
110
|
-
const lc = config.lifecycle;
|
|
111
|
-
const moduleReq = config.moduleRequiredStatuses;
|
|
112
|
-
|
|
113
|
-
for (const name of statusList) {
|
|
114
|
-
const skipStale = lc.skipStaleFor.has(name);
|
|
115
|
-
const skipWarnings = lc.skipWarningsFor.has(name);
|
|
116
|
-
const quiet = skipStale && skipWarnings;
|
|
100
|
+
for (const metadata of resolveStatusMetadata(config).byType[typeName] ?? []) {
|
|
101
|
+
const { name } = metadata;
|
|
117
102
|
result[name] = {
|
|
118
|
-
context:
|
|
119
|
-
staleDays:
|
|
120
|
-
requiresModule:
|
|
121
|
-
terminal:
|
|
122
|
-
archive:
|
|
123
|
-
skipStale,
|
|
124
|
-
skipWarnings,
|
|
125
|
-
quiet,
|
|
103
|
+
context: metadata.context,
|
|
104
|
+
staleDays: metadata.staleDays,
|
|
105
|
+
requiresModule: metadata.requiresModule,
|
|
106
|
+
terminal: metadata.terminal,
|
|
107
|
+
archive: metadata.archive,
|
|
108
|
+
skipStale: metadata.skipStale,
|
|
109
|
+
skipWarnings: metadata.skipWarnings,
|
|
110
|
+
quiet: metadata.quiet,
|
|
126
111
|
};
|
|
127
112
|
}
|
|
128
113
|
return result;
|
package/src/summary.mjs
CHANGED
|
@@ -37,10 +37,13 @@ export function runSummary(argv, config) {
|
|
|
37
37
|
const opts = {};
|
|
38
38
|
if (model) opts.model = model;
|
|
39
39
|
if (maxTokens) opts.maxTokens = maxTokens;
|
|
40
|
+
const previewSkipped = Boolean(config._execution?.suppressSideEffects);
|
|
40
41
|
|
|
41
42
|
let summary;
|
|
42
43
|
try {
|
|
43
|
-
summary =
|
|
44
|
+
summary = previewSkipped
|
|
45
|
+
? null
|
|
46
|
+
: config.hooks.summarizeDoc
|
|
44
47
|
? config.hooks.summarizeDoc(body, meta)
|
|
45
48
|
: summarizeDocBody(body, meta, opts);
|
|
46
49
|
} catch (err) {
|
|
@@ -49,13 +52,21 @@ export function runSummary(argv, config) {
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
if (json) {
|
|
52
|
-
process.stdout.write(JSON.stringify({
|
|
55
|
+
process.stdout.write(JSON.stringify({
|
|
56
|
+
path: repoPath,
|
|
57
|
+
title,
|
|
58
|
+
status,
|
|
59
|
+
summary: summary ?? null,
|
|
60
|
+
summaryStatus: previewSkipped ? 'skipped-preview' : summary ? 'generated' : 'unavailable',
|
|
61
|
+
}, null, 2) + '\n');
|
|
53
62
|
return;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
process.stdout.write(`${bold(title)} ${dim(`(${status})`)}\n`);
|
|
57
66
|
process.stdout.write(`${dim(repoPath)}\n\n`);
|
|
58
|
-
if (
|
|
67
|
+
if (previewSkipped) {
|
|
68
|
+
process.stdout.write(dim('[preview] Summary generation skipped; models and custom summarizeDoc hooks are not invoked.') + '\n');
|
|
69
|
+
} else if (summary) {
|
|
59
70
|
process.stdout.write(`${summary}\n`);
|
|
60
71
|
} else {
|
|
61
72
|
process.stdout.write(dim('Summary unavailable (model call failed or uv not installed).') + '\n');
|
package/src/update.mjs
CHANGED
|
@@ -29,23 +29,31 @@ export function compareVersions(a, b) {
|
|
|
29
29
|
// Read Claude Code's plugin install record to find the installed dotmd plugin's
|
|
30
30
|
// id + version. Network-free. `opts.home` is injectable for tests. Returns
|
|
31
31
|
// { id, version } or null when nothing is installed / the file is absent.
|
|
32
|
-
export function
|
|
32
|
+
export function readInstalledPluginRecords(opts = {}) {
|
|
33
33
|
const home = opts.home || os.homedir();
|
|
34
34
|
const file = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
|
|
35
35
|
try {
|
|
36
36
|
const j = JSON.parse(readFileSync(file, 'utf8'));
|
|
37
37
|
const plugins = j.plugins || {};
|
|
38
|
-
const id =
|
|
39
|
-
?
|
|
40
|
-
:
|
|
38
|
+
const id = opts.id
|
|
39
|
+
? (plugins[opts.id] ? opts.id : null)
|
|
40
|
+
: plugins[DEFAULT_PLUGIN_ID]
|
|
41
|
+
? DEFAULT_PLUGIN_ID
|
|
42
|
+
: Object.keys(plugins).find(k => /^dotmd@/.test(k));
|
|
41
43
|
if (!id) return null;
|
|
42
|
-
const
|
|
43
|
-
return { id,
|
|
44
|
+
const entries = Array.isArray(plugins[id]) ? plugins[id] : [plugins[id]];
|
|
45
|
+
return { id, entries: entries.filter(Boolean) };
|
|
44
46
|
} catch {
|
|
45
47
|
return null;
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export function readInstalledPlugin(opts = {}) {
|
|
52
|
+
const records = readInstalledPluginRecords(opts);
|
|
53
|
+
if (!records) return null;
|
|
54
|
+
return { id: records.id, version: records.entries[0]?.version ?? null };
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
// Decide which steps `dotmd update` should run. Pure — no side effects — so the
|
|
50
58
|
// orchestration is unit-testable. `opts` = { cliOnly, pluginOnly }; `ctx` =
|
|
51
59
|
// { plugin: {id,version}|null, hasClaude, hasNpm }.
|
|
@@ -77,7 +85,11 @@ function which(bin) {
|
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
function executableName(bin) {
|
|
89
|
+
return process.platform === 'win32' && !bin.endsWith('.cmd') ? `${bin}.cmd` : bin;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function runUpdate(argv, _config, opts = {}) {
|
|
81
93
|
const check = argv.includes('--check');
|
|
82
94
|
const cliOnly = argv.includes('--cli-only');
|
|
83
95
|
const pluginOnly = argv.includes('--plugin-only');
|
|
@@ -99,18 +111,34 @@ export function runUpdate(argv, _config) {
|
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, hasClaude: which('claude'), hasNpm: which('npm') });
|
|
114
|
+
if (opts.dryRun) {
|
|
115
|
+
for (const step of steps) {
|
|
116
|
+
if (step.kind === 'skip') process.stdout.write(dim(`[dry-run] skip: ${step.reason}\n`));
|
|
117
|
+
else process.stdout.write(dim(`[dry-run] Would run: ${step.cmd.join(' ')}\n`));
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
102
121
|
let ran = false;
|
|
122
|
+
let failed = false;
|
|
103
123
|
for (const s of steps) {
|
|
104
124
|
if (s.kind === 'skip') {
|
|
105
125
|
process.stdout.write(dim(`skip: ${s.reason}\n`));
|
|
106
126
|
continue;
|
|
107
127
|
}
|
|
108
128
|
process.stdout.write(dim(`$ ${s.cmd.join(' ')}\n`));
|
|
109
|
-
const r = spawnSync(s.cmd[0], s.cmd.slice(1), {
|
|
129
|
+
const r = spawnSync(executableName(s.cmd[0]), s.cmd.slice(1), {
|
|
130
|
+
stdio: 'inherit',
|
|
131
|
+
shell: process.platform === 'win32',
|
|
132
|
+
});
|
|
110
133
|
ran = true;
|
|
111
|
-
if (r.status !== 0)
|
|
134
|
+
if (r.status !== 0) {
|
|
135
|
+
failed = true;
|
|
136
|
+
process.stdout.write(yellow(`(${s.cmd[0]} exited ${r.status ?? '?'})\n`));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
112
139
|
}
|
|
113
|
-
if (ran) {
|
|
140
|
+
if (ran && !failed) {
|
|
114
141
|
process.stdout.write(green('\n✓ restart your Claude Code session (or /reload-plugins) to apply.\n'));
|
|
115
142
|
}
|
|
143
|
+
if (failed) process.exitCode = 1;
|
|
116
144
|
}
|
package/src/use.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
3
4
|
import { asString, die, resolveDocPath, toRepoPath } from './util.mjs';
|
|
4
5
|
import { consumePrompt, pendingPromptsOldestFirst, resolvePromptInput } from './prompts.mjs';
|
|
@@ -26,7 +27,9 @@ export async function runUse(argv, config, opts = {}) {
|
|
|
26
27
|
// Exact path first, then prompt slugs (they keep precedence on a slug
|
|
27
28
|
// collision — consuming a prompt is the more common intent), then the
|
|
28
29
|
// shared resolver for plan/doc slugs, which dies with did-you-mean on miss.
|
|
30
|
+
const cwdPath = path.resolve(process.cwd(), positional);
|
|
29
31
|
const filePath = resolveDocPath(positional, config)
|
|
32
|
+
?? (existsSync(cwdPath) ? cwdPath : null)
|
|
30
33
|
?? resolvePromptInput(positional, config, { dieOnMiss: false })
|
|
31
34
|
?? resolveDocArg(positional, config);
|
|
32
35
|
|
package/src/util.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { dim } from './color.mjs';
|
|
|
6
6
|
// Stable identifier for the current shell/agent session. Used for journal
|
|
7
7
|
// attribution and hint de-duplication — not for any plan locking.
|
|
8
8
|
export function currentSessionId() {
|
|
9
|
+
if (process.env.DOTMD_SESSION_ID) return process.env.DOTMD_SESSION_ID;
|
|
9
10
|
if (process.env.CLAUDE_CODE_SESSION_ID) return process.env.CLAUDE_CODE_SESSION_ID;
|
|
10
11
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
11
12
|
if (process.env.TERM_SESSION_ID) return `term:${process.env.TERM_SESSION_ID}`;
|
package/src/validate.mjs
CHANGED
|
@@ -519,9 +519,10 @@ export function checkRoadmapHubExecutionMode(docs, config) {
|
|
|
519
519
|
return warnings;
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
export function checkGitStaleness(docs, config) {
|
|
522
|
+
export function checkGitStaleness(docs, config, options = {}) {
|
|
523
523
|
const warnings = [];
|
|
524
|
-
const
|
|
524
|
+
const gitMetadata = getGitLastModifiedBatch(config.repoRoot, docs.map(doc => doc.path), options);
|
|
525
|
+
const gitDates = gitMetadata.dates;
|
|
525
526
|
for (const doc of docs) {
|
|
526
527
|
if (config.lifecycle.skipStaleFor.has(doc.status)) continue;
|
|
527
528
|
if (!doc.updated) continue;
|
|
@@ -540,6 +541,13 @@ export function checkGitStaleness(docs, config) {
|
|
|
540
541
|
});
|
|
541
542
|
}
|
|
542
543
|
}
|
|
544
|
+
if (!gitMetadata.complete) {
|
|
545
|
+
warnings.push({
|
|
546
|
+
path: toRepoPath(config.docsRoot, config.repoRoot) || '.',
|
|
547
|
+
level: 'warning',
|
|
548
|
+
message: `Git metadata is incomplete (${gitMetadata.reason}); staleness checks used known dates only.`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
543
551
|
return warnings;
|
|
544
552
|
}
|
|
545
553
|
|
|
@@ -582,9 +590,7 @@ export function validatePlanShape(doc, body, frontmatter, config) {
|
|
|
582
590
|
|
|
583
591
|
// 4. Heading drift: case + name variants
|
|
584
592
|
const headingDrift = [
|
|
585
|
-
{ wrong: /^##\s+Open questions\s*$/m, right: '## Open Questions' },
|
|
586
593
|
{ wrong: /^##\s+(Non-goals|Out of scope|Out of Scope|out of scope)\s*$/m, right: '## Non-Goals' },
|
|
587
|
-
{ wrong: /^##\s+open questions\s*$/m, right: '## Open Questions' },
|
|
588
594
|
];
|
|
589
595
|
for (const { wrong, right } of headingDrift) {
|
|
590
596
|
const m = body.match(wrong);
|
|
@@ -654,8 +660,10 @@ export function computeDaysSinceUpdate(updated) {
|
|
|
654
660
|
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
655
661
|
}
|
|
656
662
|
|
|
657
|
-
export function computeIsStale(status, updated, config) {
|
|
658
|
-
const
|
|
663
|
+
export function computeIsStale(status, updated, config, type = null) {
|
|
664
|
+
const typeStaleDays = type ? config.raw?.types?.[type]?.staleDays : null;
|
|
665
|
+
const hasTypeDays = Object.prototype.hasOwnProperty.call(typeStaleDays ?? {}, status);
|
|
666
|
+
const staleAfterDays = hasTypeDays ? typeStaleDays[status] : (config.staleDaysByStatus[status] ?? null);
|
|
659
667
|
if (staleAfterDays == null) return false;
|
|
660
668
|
|
|
661
669
|
const daysSinceUpdate = computeDaysSinceUpdate(updated);
|
package/src/watch.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { watch } from 'node:fs';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { dim } from './color.mjs';
|
|
5
6
|
|
|
7
|
+
export function watchCliPath(moduleUrl = import.meta.url) {
|
|
8
|
+
return path.resolve(path.dirname(fileURLToPath(moduleUrl)), '..', 'bin', 'dotmd.mjs');
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export function runWatch(argv, config) {
|
|
7
12
|
const subCommand = argv.length > 0 ? argv : ['list'];
|
|
8
|
-
const cliPath =
|
|
13
|
+
const cliPath = watchCliPath();
|
|
9
14
|
|
|
10
15
|
let lastRun = 0;
|
|
11
16
|
const DEBOUNCE = 300;
|