dotmd-cli 0.40.1 → 0.40.3
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/package.json +1 -1
- package/src/claude-commands.mjs +38 -10
- package/src/lifecycle.mjs +39 -19
package/package.json
CHANGED
package/src/claude-commands.mjs
CHANGED
|
@@ -14,21 +14,49 @@ const VERSION_MARKER = `<!-- dotmd-generated: ${pkg.version} -->`;
|
|
|
14
14
|
const VERSION_REGEX = /<!-- dotmd-generated: ([\d.]+) -->/;
|
|
15
15
|
|
|
16
16
|
// Trigger sentences surfaced by Claude Code's available-skills system reminder.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// Front-load the "when to reach for it" cue so Claude can route to the right
|
|
18
|
+
// slash command without the user having to type the slash. The plans entry
|
|
19
|
+
// gets a per-type status vocab appended at generation time so agents arrive
|
|
20
|
+
// with the valid `dotmd status` / `dotmd archive` values already in context.
|
|
20
21
|
const SLASH_DESCRIPTIONS = {
|
|
21
22
|
plans: "dotmd-managed plan briefing for this repo. Use when the user asks what's on the plate, references a plan slug, queues work, or wants to pick up / release / archive a plan.",
|
|
22
23
|
docs: "dotmd-managed docs briefing for this repo. Use when the user asks to list, scaffold, query, validate, archive, or rename non-plan docs (reference docs, ADRs, RFCs, design notes), or asks how the dotmd doc lifecycle works here.",
|
|
23
24
|
baton: "Save a resume prompt for the held plan and release the lease — the minimum handoff. Use when the user says hand off / save a resume / wrap up, or when context is getting tight.",
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const VOCAB_TRUNCATE_AT = 12;
|
|
28
|
+
|
|
29
|
+
// Per-type valid statuses, rendered as one clause per type. Appended to the
|
|
30
|
+
// plans description so it lands in Claude's available-skills listing at
|
|
31
|
+
// SessionStart — no discovery command needed before the first `dotmd status`
|
|
32
|
+
// / `dotmd archive` call. Types with no declared statuses are skipped (the
|
|
33
|
+
// generic global list applies); types with >VOCAB_TRUNCATE_AT statuses are
|
|
34
|
+
// truncated with an ellipsis so the description stays bounded.
|
|
35
|
+
function statusVocabClause(config) {
|
|
36
|
+
if (!config?.typeStatuses) return '';
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (const [type, statusesSet] of config.typeStatuses.entries()) {
|
|
39
|
+
if (!statusesSet || statusesSet.size === 0) continue;
|
|
40
|
+
let statuses = [...statusesSet];
|
|
41
|
+
if (statuses.length > VOCAB_TRUNCATE_AT) {
|
|
42
|
+
statuses = [...statuses.slice(0, VOCAB_TRUNCATE_AT), '…'];
|
|
43
|
+
}
|
|
44
|
+
parts.push(`Valid ${type} statuses: ${statuses.join(', ')}.`);
|
|
45
|
+
}
|
|
46
|
+
return parts.join(' ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function frontmatterFor(name, config) {
|
|
50
|
+
let description = SLASH_DESCRIPTIONS[name];
|
|
51
|
+
if (name === 'plans') {
|
|
52
|
+
const vocab = statusVocabClause(config);
|
|
53
|
+
if (vocab) description = `${description} ${vocab}`;
|
|
54
|
+
}
|
|
55
|
+
return ['---', `description: ${description}`, '---'];
|
|
28
56
|
}
|
|
29
57
|
|
|
30
58
|
function generatePlansCommand(config) {
|
|
31
|
-
const lines = [...frontmatterFor('plans'), VERSION_MARKER, ''];
|
|
59
|
+
const lines = [...frontmatterFor('plans', config), VERSION_MARKER, ''];
|
|
32
60
|
lines.push('Run `dotmd context` to get the current plans briefing, then use it to orient yourself.');
|
|
33
61
|
lines.push('');
|
|
34
62
|
lines.push(`Plans are managed by **dotmd** (v${pkg.version}). Config at \`dotmd.config.mjs\`. Always use \`dotmd\` directly.`);
|
|
@@ -65,8 +93,8 @@ function generatePlansCommand(config) {
|
|
|
65
93
|
return lines.join('\n');
|
|
66
94
|
}
|
|
67
95
|
|
|
68
|
-
function generateBatonCommand() {
|
|
69
|
-
const lines = [...frontmatterFor('baton'), VERSION_MARKER, ''];
|
|
96
|
+
function generateBatonCommand(config) {
|
|
97
|
+
const lines = [...frontmatterFor('baton', config), VERSION_MARKER, ''];
|
|
70
98
|
lines.push('Wrap this session. Minimum required (two commands):');
|
|
71
99
|
lines.push('');
|
|
72
100
|
lines.push('1. **Save the resume prompt.** `dotmd new prompt resume-<plan-slug>` with a 10-20 line body via heredoc: the next concrete decision plus any gotchas. NOT a recap of the plan body. The saved prompt IS the handoff — never print it into chat for copy-paste.');
|
|
@@ -89,7 +117,7 @@ function generateDocsCommand(config) {
|
|
|
89
117
|
const roots = Array.isArray(config.raw?.root) ? config.raw.root : [config.raw?.root ?? 'docs'];
|
|
90
118
|
const rootCount = roots.length;
|
|
91
119
|
|
|
92
|
-
const lines = [...frontmatterFor('docs'), VERSION_MARKER, ''];
|
|
120
|
+
const lines = [...frontmatterFor('docs', config), VERSION_MARKER, ''];
|
|
93
121
|
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\`.`);
|
|
94
122
|
lines.push('');
|
|
95
123
|
|
|
@@ -157,7 +185,7 @@ export function scaffoldClaudeCommands(cwd, config, opts = {}) {
|
|
|
157
185
|
const files = [
|
|
158
186
|
{ name: 'plans.md', generate: () => generatePlansCommand(config) },
|
|
159
187
|
{ name: 'docs.md', generate: () => generateDocsCommand(config) },
|
|
160
|
-
{ name: 'baton.md', generate: () => generateBatonCommand() },
|
|
188
|
+
{ name: 'baton.md', generate: () => generateBatonCommand(config) },
|
|
161
189
|
];
|
|
162
190
|
|
|
163
191
|
for (const { name, generate } of files) {
|
package/src/lifecycle.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { extractFrontmatter, parseSimpleFrontmatter
|
|
3
|
+
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
4
|
import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso, suggestCandidates, emitFilesFooter } from './util.mjs';
|
|
5
5
|
import { gitMv, getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
|
|
6
6
|
import { buildIndex, collectDocFiles } from './index.mjs';
|
|
@@ -829,30 +829,40 @@ function updateRefsAfterMove(oldPath, newPath, config) {
|
|
|
829
829
|
|
|
830
830
|
for (const docFile of allFiles) {
|
|
831
831
|
if (docFile === newPath) continue;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
832
|
+
const raw = readFileSync(docFile, 'utf8');
|
|
833
|
+
if (!raw.includes(basename)) continue;
|
|
834
|
+
const { frontmatter: fm, body } = extractFrontmatter(raw);
|
|
835
|
+
if (!fm) continue;
|
|
835
836
|
|
|
836
837
|
const docDir = path.dirname(docFile);
|
|
837
838
|
const oldRelPath = path.relative(docDir, oldPath).split(path.sep).join('/');
|
|
838
839
|
const newRelPath = path.relative(docDir, newPath).split(path.sep).join('/');
|
|
839
840
|
|
|
840
841
|
let newFm = fm;
|
|
841
|
-
|
|
842
|
-
// Replace exact relative path
|
|
843
842
|
if (newFm.includes(oldRelPath)) {
|
|
844
843
|
newFm = newFm.split(oldRelPath).join(newRelPath);
|
|
845
844
|
}
|
|
846
|
-
|
|
847
|
-
// Also handle ./ prefix variant
|
|
848
845
|
const dotSlashOld = './' + oldRelPath;
|
|
849
846
|
if (newFm.includes(dotSlashOld)) {
|
|
850
847
|
newFm = newFm.split(dotSlashOld).join(newRelPath);
|
|
851
848
|
}
|
|
852
849
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
850
|
+
// Body markdown links [text](path.md) or [text](path.md#anchor) pointing
|
|
851
|
+
// at oldPath. resolveRefPath can't be used here: oldPath no longer exists
|
|
852
|
+
// on disk (git mv already ran), so its existsSync probe would fail. Match
|
|
853
|
+
// by resolving the href manually and comparing absolute paths instead.
|
|
854
|
+
const linkRegex = /(\[[^\]]*\]\()([^)#]+\.md)(#[^)]*)?(\))/g;
|
|
855
|
+
const newBody = body.replace(linkRegex, (match, pre, href, frag, post) => {
|
|
856
|
+
if (/^https?:/i.test(href)) return match;
|
|
857
|
+
const docRelAbs = path.resolve(docDir, href);
|
|
858
|
+
const repoRelAbs = path.resolve(config.repoRoot, href);
|
|
859
|
+
if (docRelAbs !== oldPath && repoRelAbs !== oldPath) return match;
|
|
860
|
+
const newHref = path.relative(docDir, newPath).split(path.sep).join('/');
|
|
861
|
+
return `${pre}${newHref}${frag ?? ''}${post}`;
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
if (newFm !== fm || newBody !== body) {
|
|
865
|
+
writeFileSync(docFile, `---\n${newFm}\n---\n${newBody}`, 'utf8');
|
|
856
866
|
touched.push(docFile);
|
|
857
867
|
}
|
|
858
868
|
}
|
|
@@ -895,10 +905,7 @@ function updateRefsFromMovedFile(oldPath, newPath, config) {
|
|
|
895
905
|
});
|
|
896
906
|
|
|
897
907
|
if (newFm !== frontmatter || newBody !== body) {
|
|
898
|
-
|
|
899
|
-
// Replace body: rebuilt has updated frontmatter but old body
|
|
900
|
-
const { frontmatter: updatedFm } = extractFrontmatter(rebuilt);
|
|
901
|
-
writeFileSync(newPath, `---\n${updatedFm}\n---${newBody}`, 'utf8');
|
|
908
|
+
writeFileSync(newPath, `---\n${newFm}\n---\n${newBody}`, 'utf8');
|
|
902
909
|
return 1;
|
|
903
910
|
}
|
|
904
911
|
|
|
@@ -913,14 +920,27 @@ function countRefsToUpdate(oldPath, newPath, config) {
|
|
|
913
920
|
for (const docFile of allFiles) {
|
|
914
921
|
if (docFile === newPath) continue;
|
|
915
922
|
const raw = readFileSync(docFile, 'utf8');
|
|
916
|
-
|
|
917
|
-
|
|
923
|
+
if (!raw.includes(basename)) continue;
|
|
924
|
+
const { frontmatter: fm, body } = extractFrontmatter(raw);
|
|
925
|
+
if (!fm) continue;
|
|
918
926
|
|
|
919
927
|
const docDir = path.dirname(docFile);
|
|
920
928
|
const oldRelPath = path.relative(docDir, oldPath).split(path.sep).join('/');
|
|
921
|
-
|
|
922
|
-
|
|
929
|
+
const fmHit = fm.includes(oldRelPath) || fm.includes('./' + oldRelPath);
|
|
930
|
+
|
|
931
|
+
let bodyHit = false;
|
|
932
|
+
if (!fmHit) {
|
|
933
|
+
const linkRegex = /\[[^\]]*\]\(([^)#]+\.md)(?:#[^)]*)?\)/g;
|
|
934
|
+
for (const match of body.matchAll(linkRegex)) {
|
|
935
|
+
const href = match[1];
|
|
936
|
+
if (/^https?:/i.test(href)) continue;
|
|
937
|
+
const docRelAbs = path.resolve(docDir, href);
|
|
938
|
+
const repoRelAbs = path.resolve(config.repoRoot, href);
|
|
939
|
+
if (docRelAbs === oldPath || repoRelAbs === oldPath) { bodyHit = true; break; }
|
|
940
|
+
}
|
|
923
941
|
}
|
|
942
|
+
|
|
943
|
+
if (fmHit || bodyHit) count++;
|
|
924
944
|
}
|
|
925
945
|
|
|
926
946
|
return count;
|