dotmd-cli 0.36.0 → 0.36.2
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/config.mjs +10 -0
- package/src/glossary.mjs +13 -6
- package/src/lifecycle.mjs +6 -2
- package/src/new.mjs +17 -3
- package/src/query.mjs +14 -7
- package/src/render.mjs +7 -1
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -158,6 +158,16 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
158
158
|
const quietImpliesSkipStale = p.quiet && p.skipStale !== false;
|
|
159
159
|
const quietImpliesSkipWarnings = p.quiet && p.skipWarnings !== false;
|
|
160
160
|
|
|
161
|
+
// Contradiction diagnostics — emit at load so dead config doesn't fail silently.
|
|
162
|
+
const skipStaleEffective = p.skipStale === true || quietImpliesSkipStale;
|
|
163
|
+
const skipWarningsEffective = p.skipWarnings === true || quietImpliesSkipWarnings;
|
|
164
|
+
if (skipStaleEffective && p.staleDays != null) {
|
|
165
|
+
warn(`dotmd config: status "${typeName}.${name}" has skipStale: true and staleDays: ${p.staleDays} — staleDays is ignored. Drop one to silence this warning.`);
|
|
166
|
+
}
|
|
167
|
+
if (skipWarningsEffective && p.requiresModule) {
|
|
168
|
+
warn(`dotmd config: status "${typeName}.${name}" has skipWarnings: true and requiresModule: true — the module requirement can never fire. Drop one to silence this warning.`);
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
|
|
162
172
|
if ((p.skipStale || quietImpliesSkipStale) && !derived.skipStaleFor.includes(name)) derived.skipStaleFor.push(name);
|
|
163
173
|
if ((p.skipWarnings || quietImpliesSkipWarnings) && !derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
|
package/src/glossary.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { bold, dim, green, yellow } from './color.mjs';
|
|
|
7
7
|
function parseGlossaryTable(content, sectionHeading) {
|
|
8
8
|
const headingRegex = new RegExp(`^##\\s+${sectionHeading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'm');
|
|
9
9
|
const match = content.match(headingRegex);
|
|
10
|
-
if (!match) return [];
|
|
10
|
+
if (!match) return { found: false, entries: [] };
|
|
11
11
|
|
|
12
12
|
const sectionStart = match.index + match[0].length;
|
|
13
13
|
const nextHeading = content.indexOf('\n## ', sectionStart);
|
|
@@ -47,7 +47,7 @@ function parseGlossaryTable(content, sectionHeading) {
|
|
|
47
47
|
entries.push({ term: m[1], meaning: `UI label: "${m[2]}"${m[3] ? ` (${m[3]})` : ''}`, tiers: 'schema→UI' });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
return entries;
|
|
50
|
+
return { found: true, entries };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function loadGlossary(config) {
|
|
@@ -62,7 +62,8 @@ function loadGlossary(config) {
|
|
|
62
62
|
|
|
63
63
|
const content = readFileSync(filePath, 'utf8');
|
|
64
64
|
const section = glossaryConfig.section ?? 'Terminology';
|
|
65
|
-
|
|
65
|
+
const result = parseGlossaryTable(content, section);
|
|
66
|
+
return { ...result, path: glossaryConfig.path, section };
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
function matchTerm(query, entries) {
|
|
@@ -177,9 +178,15 @@ export function runGlossary(argv, config) {
|
|
|
177
178
|
const listAll = argv.includes('--list');
|
|
178
179
|
const term = argv.find(a => !a.startsWith('-'));
|
|
179
180
|
|
|
180
|
-
const
|
|
181
|
-
if (!
|
|
182
|
-
if (
|
|
181
|
+
const result = loadGlossary(config);
|
|
182
|
+
if (!result) die('No glossary configured. Add glossary: { path, section } to your dotmd config.');
|
|
183
|
+
if (!result.found) {
|
|
184
|
+
die(`Glossary section "## ${result.section}" not found in ${result.path}. Add the section, or update glossary.section in dotmd.config.mjs.`);
|
|
185
|
+
}
|
|
186
|
+
if (result.entries.length === 0) {
|
|
187
|
+
die(`Glossary section "## ${result.section}" found in ${result.path} but contains no recognizable entries (expected markdown table or schema→UI bullets).`);
|
|
188
|
+
}
|
|
189
|
+
const entries = result.entries;
|
|
183
190
|
|
|
184
191
|
if (json && listAll) {
|
|
185
192
|
const index = buildIndex(config);
|
package/src/lifecycle.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter, replaceFrontmatter } from './frontmatter.mjs';
|
|
4
|
-
import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso } from './util.mjs';
|
|
4
|
+
import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso, suggestCandidates } from './util.mjs';
|
|
5
5
|
import { gitMv, getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
|
|
6
6
|
import { buildIndex, collectDocFiles } from './index.mjs';
|
|
7
7
|
import { renderIndexFile, writeIndex } from './index-file.mjs';
|
|
@@ -101,7 +101,11 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
if (!effectiveValid.has(newStatus)) {
|
|
104
|
+
if (!effectiveValid.has(newStatus)) {
|
|
105
|
+
const suggestions = suggestCandidates(newStatus, [...effectiveValid]);
|
|
106
|
+
const hint = suggestions.length ? `\nDid you mean: ${suggestions.join(', ')}?` : '';
|
|
107
|
+
die(`Invalid status: ${newStatus}\nValid: ${[...effectiveValid].join(', ')}${hint}`);
|
|
108
|
+
}
|
|
105
109
|
|
|
106
110
|
const oldStatus = asString(parsedFm.status);
|
|
107
111
|
|
package/src/new.mjs
CHANGED
|
@@ -69,14 +69,27 @@ ${ctx?.bodyInput?.trim() ?? ''}
|
|
|
69
69
|
'current_state:',
|
|
70
70
|
'next_step:',
|
|
71
71
|
].join('\n'),
|
|
72
|
-
body: (t, ctx) =>
|
|
72
|
+
body: (t, ctx) => {
|
|
73
|
+
const bodyInput = ctx?.bodyInput?.trim() ?? '';
|
|
74
|
+
// Full-body shortcut: if the input already authors `## Section` headings,
|
|
75
|
+
// it's a complete plan body the user/agent wrote start-to-finish. Drop
|
|
76
|
+
// the scaffold's later sections to avoid duplicate empty `## Goals`,
|
|
77
|
+
// `## Phases`, etc. below the user's already-filled versions. A bare title
|
|
78
|
+
// (`# X`) at the head of the body is honored — we don't double-print the
|
|
79
|
+
// scaffold's title. Otherwise emit the scaffold and slot the body into
|
|
80
|
+
// `## Problem` as before (section-content mode).
|
|
81
|
+
if (/^##\s+\S/m.test(bodyInput)) {
|
|
82
|
+
const hasOwnTitle = /^#\s+\S/.test(bodyInput);
|
|
83
|
+
return hasOwnTitle ? `\n${bodyInput}\n` : `\n# ${t}\n\n${bodyInput}\n`;
|
|
84
|
+
}
|
|
85
|
+
return `
|
|
73
86
|
# ${t}
|
|
74
87
|
|
|
75
88
|
> One-paragraph problem statement: what this plan is for, why now.
|
|
76
89
|
|
|
77
90
|
## Problem
|
|
78
91
|
|
|
79
|
-
${
|
|
92
|
+
${bodyInput}
|
|
80
93
|
|
|
81
94
|
## Goals
|
|
82
95
|
|
|
@@ -128,7 +141,8 @@ Status markers (put in heading text):
|
|
|
128
141
|
## Closeout
|
|
129
142
|
|
|
130
143
|
<!-- Filled on archive: what shipped, key commits, deferrals dispositioned. -->
|
|
131
|
-
|
|
144
|
+
`;
|
|
145
|
+
},
|
|
132
146
|
},
|
|
133
147
|
prompt: {
|
|
134
148
|
description: 'Saved prompt to seed a future Claude session — body is required',
|
package/src/query.mjs
CHANGED
|
@@ -251,7 +251,13 @@ function getDocSummary(doc, config) {
|
|
|
251
251
|
|
|
252
252
|
function renderQueryResults(docs, filters, config) {
|
|
253
253
|
process.stdout.write('Query\n\n');
|
|
254
|
-
|
|
254
|
+
const total = filters._totalBeforeLimit ?? docs.length;
|
|
255
|
+
const truncated = !filters.all && total > docs.length;
|
|
256
|
+
if (truncated) {
|
|
257
|
+
process.stdout.write(`- results: ${docs.length} of ${total} ${dim('(use --all to see all)')}\n`);
|
|
258
|
+
} else {
|
|
259
|
+
process.stdout.write(`- results: ${docs.length}\n`);
|
|
260
|
+
}
|
|
255
261
|
if (filters.types?.length) process.stdout.write(`- type: ${filters.types.join(', ')}\n`);
|
|
256
262
|
if (filters.statuses?.length) process.stdout.write(`- status: ${filters.statuses.join(', ')}\n`);
|
|
257
263
|
if (filters.keyword) process.stdout.write(`- keyword: ${filters.keyword}\n`);
|
|
@@ -381,12 +387,13 @@ function renderPlansOutput(docs, filters, config, opts = {}) {
|
|
|
381
387
|
// Triage view: flat, sorted by recency, tag on right.
|
|
382
388
|
process.stdout.write('\n');
|
|
383
389
|
renderPlanRows(docs, filters, maxWidth, { showTag: true });
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Footer when the result was capped — emit for every view shape.
|
|
393
|
+
const hidden = totalAll - totalShown;
|
|
394
|
+
if (hidden > 0) {
|
|
395
|
+
process.stdout.write('\n');
|
|
396
|
+
process.stdout.write(dim(` ${hidden} more ${noun} · dotmd ${noun} --all · dotmd ${noun} status\n`));
|
|
390
397
|
}
|
|
391
398
|
|
|
392
399
|
process.stdout.write('\n');
|
package/src/render.mjs
CHANGED
|
@@ -275,7 +275,13 @@ function _renderContext(index, config, opts = {}) {
|
|
|
275
275
|
|
|
276
276
|
const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status));
|
|
277
277
|
if (stale.length) {
|
|
278
|
-
|
|
278
|
+
const cap = config.context?.staleTailLimit ?? 8;
|
|
279
|
+
const shown = stale.slice(0, cap).map(d => `${toSlug(d)} (${d.daysSinceUpdate}d)`).join(', ');
|
|
280
|
+
const overflow = stale.length - cap;
|
|
281
|
+
const tail = overflow > 0
|
|
282
|
+
? `${shown}, …and ${overflow} more (run \`dotmd stale\` for the full list)`
|
|
283
|
+
: shown;
|
|
284
|
+
lines.push(`Stale: ${tail}`);
|
|
279
285
|
} else {
|
|
280
286
|
lines.push('Stale: none');
|
|
281
287
|
}
|