dotmd-cli 0.57.0 → 0.59.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 +7 -0
- package/bin/dotmd.mjs +6 -1
- package/dotmd.config.example.mjs +3 -0
- package/package.json +1 -1
- package/src/config.mjs +10 -1
- package/src/guard.mjs +10 -4
- package/src/lifecycle.mjs +44 -5
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ If you drive dotmd from Claude Code, install the **dotmd plugin**. It teaches ev
|
|
|
23
23
|
|
|
24
24
|
The plugin bundles the hooks (`SessionStart`/`SubagentStart` priming, a `PreToolUse` guard) and a canonical workflow skill, so guidance travels to **every** repo automatically — no per-repo setup. It calls the `dotmd` CLI, so keep `npm install -g dotmd-cli` installed too. (Source: `plugins/dotmd/` in this repo.)
|
|
25
25
|
|
|
26
|
+
> **Upgrading to 0.57.0+:** per-repo `.claude/commands/{plans,docs,baton}.md` scaffolding is retired — that guidance now ships via the plugin's workflow skill and `/plans`, `/docs`, `/prompts`, `/baton` commands. On the next `dotmd hud` (SessionStart), dotmd removes those generated files (only banner-stamped `<!-- dotmd-generated -->` ones — your hand-authored command files are never touched). If you'd committed them, you'll see deletions to commit — that's expected. Run `claude plugin update dotmd@dotmd` to pick up `/baton`.
|
|
27
|
+
|
|
26
28
|
## Quick Start
|
|
27
29
|
|
|
28
30
|
```bash
|
|
@@ -803,6 +805,11 @@ export const lifecycle = {
|
|
|
803
805
|
skipStaleFor: ['archived'],
|
|
804
806
|
skipWarningsFor: ['archived'],
|
|
805
807
|
terminalStatuses: ['archived'],
|
|
808
|
+
// Types that archive into their own <typeDir>/<archiveDir> (e.g.
|
|
809
|
+
// docs/prompts/archived/) instead of the shared <root>/<archiveDir>.
|
|
810
|
+
// Defaults to ['prompt'] so session-local prompt churn doesn't bury
|
|
811
|
+
// plans and docs in the shared archive. Set to [] to disable.
|
|
812
|
+
archiveNestedTypes: ['prompt'],
|
|
806
813
|
};
|
|
807
814
|
|
|
808
815
|
export const taxonomy = {
|
package/bin/dotmd.mjs
CHANGED
|
@@ -507,11 +507,16 @@ Options:
|
|
|
507
507
|
--json Output errors and warnings as JSON (always full detail)
|
|
508
508
|
--dry-run, -n Preview fixes without writing (with --fix)`,
|
|
509
509
|
|
|
510
|
-
archive: `dotmd archive <file> — archive a document
|
|
510
|
+
archive: `dotmd archive <file-or-slug> — archive a document
|
|
511
511
|
|
|
512
512
|
Sets status to 'archived', moves to the archive directory, auto-updates
|
|
513
513
|
references in other docs, and regenerates the index.
|
|
514
514
|
|
|
515
|
+
<file-or-slug> resolves like \`dotmd use\`: an exact path wins, but a bare
|
|
516
|
+
slug / basename (e.g. \`archive resume-foo\`) falls back to a recursive
|
|
517
|
+
basename match under the doc roots. An ambiguous basename (the same name in
|
|
518
|
+
two places) errors with the candidate list instead of guessing.
|
|
519
|
+
|
|
515
520
|
Options:
|
|
516
521
|
--no-index Skip index regen. Use when multiple sessions are
|
|
517
522
|
working concurrently and you want a path-limited
|
package/dotmd.config.example.mjs
CHANGED
|
@@ -130,6 +130,9 @@ export const statuses = {
|
|
|
130
130
|
// skipStaleFor: ['archived'], // skip staleness checks
|
|
131
131
|
// skipWarningsFor: ['archived'], // skip validation warnings (summary, etc.)
|
|
132
132
|
// terminalStatuses: ['archived', 'deprecated', 'reference'], // skip current_state/next_step warnings, exclude from stats scope
|
|
133
|
+
// archiveNestedTypes: ['prompt'], // types that archive under <typeDir>/<archiveDir>
|
|
134
|
+
// // (e.g. docs/prompts/archived/) instead of the
|
|
135
|
+
// // shared <root>/<archiveDir>; set [] to disable
|
|
133
136
|
// };
|
|
134
137
|
|
|
135
138
|
// Taxonomy validation — set fields to null to skip validation.
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -63,6 +63,12 @@ const DEFAULTS = {
|
|
|
63
63
|
// statuses file under the owning type folder; archive remains a separate
|
|
64
64
|
// primitive untouched.
|
|
65
65
|
filedStatuses: { held: 'held', shelved: 'held', paused: 'held' },
|
|
66
|
+
// Types whose archive nests under their own type dir (<typeDir>/<archiveDir>,
|
|
67
|
+
// e.g. docs/prompts/archived/) instead of the shared <root>/<archiveDir>.
|
|
68
|
+
// Prompts are session-local churn — keeping their archive out of the shared
|
|
69
|
+
// docs/archived/ stops them from burying plans/docs there. Set to [] to send
|
|
70
|
+
// every type to the shared archive.
|
|
71
|
+
archiveNestedTypes: ['prompt'],
|
|
66
72
|
},
|
|
67
73
|
|
|
68
74
|
taxonomy: {
|
|
@@ -476,6 +482,9 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
476
482
|
// F15: filedStatuses keyed by status name, value = directory name. Empty
|
|
477
483
|
// object when no status opts in via `filed: true` (or `filed: '<dirname>'`).
|
|
478
484
|
const filedStatuses = new Map(Object.entries(lifecycle.filedStatuses ?? {}));
|
|
485
|
+
// Types that archive into their own <typeDir>/<archiveDir> rather than the
|
|
486
|
+
// shared <root>/<archiveDir> (default: prompt).
|
|
487
|
+
const archiveNestedTypes = new Set(lifecycle.archiveNestedTypes ?? []);
|
|
479
488
|
|
|
480
489
|
// Warn if rootStatuses keys don't match any configured root
|
|
481
490
|
for (const rootKey of Object.keys(rootStatusesRaw)) {
|
|
@@ -507,7 +516,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
507
516
|
rootValidStatuses,
|
|
508
517
|
staleDaysByStatus,
|
|
509
518
|
|
|
510
|
-
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses, filedStatuses },
|
|
519
|
+
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses, filedStatuses, archiveNestedTypes },
|
|
511
520
|
|
|
512
521
|
validSurfaces,
|
|
513
522
|
validModules,
|
package/src/guard.mjs
CHANGED
|
@@ -25,11 +25,17 @@ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'),
|
|
|
25
25
|
|
|
26
26
|
const SHELL_READERS = new Set(['cat', 'less', 'more', 'head', 'tail', 'bat', 'view', 'open']);
|
|
27
27
|
|
|
28
|
-
// A path that ends in .md and sits under a `prompts/` directory is a
|
|
29
|
-
// prompt regardless of which doc root it belongs to —
|
|
30
|
-
// without needing the resolved config.
|
|
28
|
+
// A path that ends in .md and sits under a `prompts/` directory is a
|
|
29
|
+
// session-local saved prompt regardless of which doc root it belongs to —
|
|
30
|
+
// robust across repos without needing the resolved config. Archived prompts
|
|
31
|
+
// (`…/prompts/archived/…`, the default nested archive for the prompt type) are
|
|
32
|
+
// committable history, NOT session-local, so they're explicitly excluded — the
|
|
33
|
+
// guard must not block committing or reading them.
|
|
31
34
|
function isPromptPath(p) {
|
|
32
|
-
|
|
35
|
+
if (typeof p !== 'string' || !p.endsWith('.md')) return false;
|
|
36
|
+
if (!/(^|\/)prompts\//.test(p)) return false;
|
|
37
|
+
if (/(^|\/)archived\//.test(p)) return false;
|
|
38
|
+
return true;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
// Loose "is this a dotmd-managed doc" test: a .md file under one of the
|
package/src/lifecycle.mjs
CHANGED
|
@@ -15,6 +15,29 @@ function findFileRoot(filePath, config) {
|
|
|
15
15
|
return roots.find(r => filePath.startsWith(r + '/')) ?? config.docsRoot;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Resolve an archive target like `dotmd use` resolves a pickup target: an exact
|
|
19
|
+
// path wins (the resolveDocPath fast path), but a bare slug / basename falls
|
|
20
|
+
// back to a recursive basename match under the doc roots. Mirrors
|
|
21
|
+
// resolvePromptInput's basename pass so the closure verb is as forgiving about
|
|
22
|
+
// naming as `use`/`prompts archive`. A basename shared by two files errors with
|
|
23
|
+
// the candidate list rather than guessing which one to move.
|
|
24
|
+
function resolveArchiveTarget(input, config) {
|
|
25
|
+
const direct = resolveDocPath(input, config);
|
|
26
|
+
if (direct) return direct;
|
|
27
|
+
if (!input.endsWith('.md')) {
|
|
28
|
+
const withExt = resolveDocPath(input + '.md', config);
|
|
29
|
+
if (withExt) return withExt;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const slug = input.replace(/\.md$/, '');
|
|
33
|
+
const byBasename = collectDocFiles(config).filter(f => path.basename(f, '.md') === slug);
|
|
34
|
+
if (byBasename.length === 1) return byBasename[0];
|
|
35
|
+
if (byBasename.length > 1) {
|
|
36
|
+
die(`Multiple docs match "${input}" by basename:\n${byBasename.map(f => ' ' + toRepoPath(f, config.repoRoot)).join('\n')}`);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
function defaultTypeDir(docType, config) {
|
|
19
42
|
if (docType === 'plan') return 'plans';
|
|
20
43
|
if (docType === 'prompt') return 'prompts';
|
|
@@ -32,6 +55,17 @@ function findFilingRoot(filePath, fileRoot, docType, config) {
|
|
|
32
55
|
return fileRoot;
|
|
33
56
|
}
|
|
34
57
|
|
|
58
|
+
// Base directory a doc archives under. Types in lifecycle.archiveNestedTypes
|
|
59
|
+
// (default: prompt) archive into their own <typeDir>/ — yielding
|
|
60
|
+
// <typeDir>/<archiveDir> (e.g. docs/prompts/archived/) — so session-local
|
|
61
|
+
// prompt churn doesn't bury plans/docs in the shared <root>/<archiveDir>.
|
|
62
|
+
// Everything else archives under fileRoot. Used by both runStatus (set
|
|
63
|
+
// archived) and runArchive so the two paths stay in lockstep.
|
|
64
|
+
function archiveBaseFor(filePath, fileRoot, docType, config) {
|
|
65
|
+
const nest = config.lifecycle?.archiveNestedTypes?.has(docType) ?? false;
|
|
66
|
+
return nest ? findFilingRoot(filePath, fileRoot, docType, config) : fileRoot;
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
// Best-effort index regen for any doc-set or doc-status mutation. The
|
|
36
70
|
// generated block groups by status and embeds per-doc snapshots, so any
|
|
37
71
|
// change that affects what would render leaves the index stale. Wrapped
|
|
@@ -172,8 +206,11 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
172
206
|
}
|
|
173
207
|
|
|
174
208
|
const today = nowIso();
|
|
175
|
-
const archiveDir = path.join(fileRoot, config.archiveDir);
|
|
176
209
|
const filingRoot = findFilingRoot(filePath, fileRoot, docType, config);
|
|
210
|
+
// Type-aware archive base (prompts nest under their type dir by default);
|
|
211
|
+
// unarchive reuses archiveBase so a prompt restores to its type dir.
|
|
212
|
+
const archiveBase = archiveBaseFor(filePath, fileRoot, docType, config);
|
|
213
|
+
const archiveDir = path.join(archiveBase, config.archiveDir);
|
|
177
214
|
const relFromFilingRoot = path.relative(filingRoot, filePath);
|
|
178
215
|
const relSegments = relFromFilingRoot.split(path.sep);
|
|
179
216
|
const inArchive = isArchivedPath(toRepoPath(filePath, config.repoRoot), config);
|
|
@@ -202,7 +239,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
202
239
|
finalPath = targetPath;
|
|
203
240
|
}
|
|
204
241
|
if (isUnarchiving) {
|
|
205
|
-
const targetPath = path.join(
|
|
242
|
+
const targetPath = path.join(archiveBase, path.basename(filePath));
|
|
206
243
|
process.stdout.write(`${prefix} Would move: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
207
244
|
finalPath = targetPath;
|
|
208
245
|
}
|
|
@@ -235,7 +272,7 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
235
272
|
}
|
|
236
273
|
|
|
237
274
|
if (isUnarchiving) {
|
|
238
|
-
const targetPath = path.join(
|
|
275
|
+
const targetPath = path.join(archiveBase, path.basename(filePath));
|
|
239
276
|
if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
|
|
240
277
|
const result = gitMv(filePath, targetPath, config.repoRoot);
|
|
241
278
|
if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
|
|
@@ -387,7 +424,7 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
387
424
|
|
|
388
425
|
if (!input) { die('Usage: dotmd archive <file>'); }
|
|
389
426
|
|
|
390
|
-
const filePath =
|
|
427
|
+
const filePath = resolveArchiveTarget(input, config);
|
|
391
428
|
if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); }
|
|
392
429
|
|
|
393
430
|
const archiveFileRoot = findFileRoot(filePath, config);
|
|
@@ -436,7 +473,9 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
436
473
|
const closeoutAction = closeoutTemplate ? planCloseoutInjection(body) : null;
|
|
437
474
|
|
|
438
475
|
const today = nowIso();
|
|
439
|
-
|
|
476
|
+
// Type-aware: prompts archive under docs/prompts/archived/ by default (see
|
|
477
|
+
// archiveBaseFor); plans/docs keep the shared <root>/archived/.
|
|
478
|
+
const targetDir = path.join(archiveBaseFor(filePath, archiveFileRoot, asString(parsed.type), config), config.archiveDir);
|
|
440
479
|
const targetPath = uniqueArchiveTarget(targetDir, path.basename(filePath));
|
|
441
480
|
const oldRepoPath = toRepoPath(filePath, config.repoRoot);
|
|
442
481
|
const newRepoPath = toRepoPath(targetPath, config.repoRoot);
|