dotmd-cli 0.64.2 → 0.64.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/doctor.mjs +2 -1
- package/src/frontmatter.mjs +35 -9
- package/src/guard.mjs +17 -6
- package/src/lifecycle.mjs +4 -4
- package/src/new.mjs +2 -2
package/package.json
CHANGED
package/src/doctor.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { checkClaudeCommands, removeGeneratedSlashCommands } from './claude-comm
|
|
|
11
11
|
import { runMigrateTemplate } from './migrate-template.mjs';
|
|
12
12
|
import { runMigratePrompts } from './migrate-prompts.mjs';
|
|
13
13
|
import { runFrontmatterFix } from './frontmatter-fix.mjs';
|
|
14
|
+
import { normalizeEol } from './frontmatter.mjs';
|
|
14
15
|
import { toRepoPath } from './util.mjs';
|
|
15
16
|
|
|
16
17
|
// Tunable thresholds for `dotmd doctor --statuses` conflation detection.
|
|
@@ -165,7 +166,7 @@ function findWorkflowDrift(config) {
|
|
|
165
166
|
for (const filePath of collectDocFiles(config)) {
|
|
166
167
|
let raw = '';
|
|
167
168
|
try { raw = readFileSync(filePath, 'utf8'); } catch { continue; }
|
|
168
|
-
if (!raw.startsWith('---\n')) docsWithoutFrontmatter.push(toRepoPath(filePath, config.repoRoot));
|
|
169
|
+
if (!normalizeEol(raw).startsWith('---\n')) docsWithoutFrontmatter.push(toRepoPath(filePath, config.repoRoot));
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
const planStatusGaps = [];
|
package/src/frontmatter.mjs
CHANGED
|
@@ -1,24 +1,40 @@
|
|
|
1
|
+
// Windows-authored (CRLF) docs otherwise slip past the LF-only fence detection
|
|
2
|
+
// below and read as having NO frontmatter — silently dropping them from the
|
|
3
|
+
// managed set (no type, no status). Normalizing CRLF→LF at every parse/rewrite
|
|
4
|
+
// boundary is the fix; the per-line value parser already strips a trailing \r,
|
|
5
|
+
// so only the fence scan was blind. A managed doc settles to LF the first time a
|
|
6
|
+
// dotmd verb rewrites it — content-preserving line-ending normalization, not
|
|
7
|
+
// corruption. For LF docs this is a no-op (the `\r` guard skips the replace), so
|
|
8
|
+
// existing behavior is byte-identical.
|
|
9
|
+
export function normalizeEol(text) {
|
|
10
|
+
return typeof text === 'string' && text.includes('\r') ? text.replace(/\r\n/g, '\n') : text;
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
export function extractFrontmatter(raw) {
|
|
2
|
-
|
|
3
|
-
|
|
14
|
+
const text = normalizeEol(raw);
|
|
15
|
+
if (!text.startsWith('---\n')) {
|
|
16
|
+
return { frontmatter: '', body: text };
|
|
4
17
|
}
|
|
5
18
|
|
|
6
|
-
const endMarker =
|
|
19
|
+
const endMarker = text.indexOf('\n---\n', 4);
|
|
7
20
|
if (endMarker === -1) {
|
|
8
|
-
return { frontmatter: '', body:
|
|
21
|
+
return { frontmatter: '', body: text };
|
|
9
22
|
}
|
|
10
23
|
|
|
11
24
|
return {
|
|
12
|
-
frontmatter:
|
|
13
|
-
body:
|
|
25
|
+
frontmatter: text.slice(4, endMarker),
|
|
26
|
+
body: text.slice(endMarker + 5),
|
|
14
27
|
};
|
|
15
28
|
}
|
|
16
29
|
|
|
17
30
|
export function replaceFrontmatter(raw, newFrontmatter) {
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
const text = normalizeEol(raw);
|
|
32
|
+
// No frontmatter to replace: return the ORIGINAL bytes untouched (a no-op
|
|
33
|
+
// rewrite must not normalize a file the caller didn't intend to change).
|
|
34
|
+
if (!text.startsWith('---\n')) return raw;
|
|
35
|
+
const endMarker = text.indexOf('\n---\n', 4);
|
|
20
36
|
if (endMarker === -1) return raw;
|
|
21
|
-
const body =
|
|
37
|
+
const body = text.slice(endMarker + 5);
|
|
22
38
|
return `---\n${newFrontmatter}\n---\n${body}`;
|
|
23
39
|
}
|
|
24
40
|
|
|
@@ -34,6 +50,16 @@ export function replaceFrontmatter(raw, newFrontmatter) {
|
|
|
34
50
|
// folded block scalar `key: >\n one line\n continues` → "one line continues"
|
|
35
51
|
// literal block scalar `key: |\n one\n two` → "one\ntwo"
|
|
36
52
|
// chomping indicators `>-`, `|-` (strip), `>+`, `|+` (keep), default (clip to one trailing \n)
|
|
53
|
+
//
|
|
54
|
+
// Supported-subset boundary (deliberately NOT a full YAML parser):
|
|
55
|
+
// - Scalars stay strings except literal `true`/`false`. Numbers, null, and
|
|
56
|
+
// dates are kept verbatim as strings; callers coerce where they need a type
|
|
57
|
+
// (so `1.0`, `2025-01-01`, version strings, and numeric-looking ids survive
|
|
58
|
+
// intact rather than silently changing type).
|
|
59
|
+
// - Duplicate keys keep the FIRST occurrence; later ones are ignored (and
|
|
60
|
+
// reported via the optional `warnings` array).
|
|
61
|
+
// - Nested maps / multi-level indentation are not parsed — only top-level
|
|
62
|
+
// keys, with one level of `- ` items under an array key.
|
|
37
63
|
export function parseSimpleFrontmatter(text, warnings) {
|
|
38
64
|
const data = {};
|
|
39
65
|
const seenDupKeys = new Set();
|
package/src/guard.mjs
CHANGED
|
@@ -27,6 +27,14 @@ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'),
|
|
|
27
27
|
|
|
28
28
|
const SHELL_READERS = new Set(['cat', 'less', 'more', 'head', 'tail', 'bat', 'view', 'open']);
|
|
29
29
|
|
|
30
|
+
// Normalize path separators so the guard's `/`-based matching also fires on
|
|
31
|
+
// Windows backslash paths (`docs\prompts\foo.md`). Without this the PreToolUse
|
|
32
|
+
// status-edit/commit-prompt guards silently no-op on Windows — protection
|
|
33
|
+
// absent exactly where it's needed. On POSIX paths this is a no-op.
|
|
34
|
+
function toSlash(p) {
|
|
35
|
+
return typeof p === 'string' ? p.replace(/\\/g, '/') : p;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
// A path that ends in .md and sits under a `prompts/` directory is a
|
|
31
39
|
// session-local saved prompt regardless of which doc root it belongs to —
|
|
32
40
|
// robust across repos without needing the resolved config. Archived prompts
|
|
@@ -34,20 +42,23 @@ const SHELL_READERS = new Set(['cat', 'less', 'more', 'head', 'tail', 'bat', 'vi
|
|
|
34
42
|
// committable history, NOT session-local, so they're explicitly excluded — the
|
|
35
43
|
// guard must not block committing or reading them.
|
|
36
44
|
function isPromptPath(p) {
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
45
|
+
const s = toSlash(p);
|
|
46
|
+
if (typeof s !== 'string' || !s.endsWith('.md')) return false;
|
|
47
|
+
if (!/(^|\/)prompts\//.test(s)) return false;
|
|
48
|
+
if (/(^|\/)archived\//.test(s)) return false;
|
|
40
49
|
return true;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
// Loose "is this a dotmd-managed doc" test: a .md file under one of the
|
|
44
53
|
// configured doc roots (default `docs/`). Used for the status-edit guard.
|
|
45
54
|
function isManagedDoc(p, config) {
|
|
46
|
-
|
|
55
|
+
const s = toSlash(p);
|
|
56
|
+
if (typeof s !== 'string' || !s.endsWith('.md')) return false;
|
|
47
57
|
const roots = config?.docsRoots || (config?.docsRoot ? [config.docsRoot] : ['docs']);
|
|
48
58
|
return roots.some(r => {
|
|
49
|
-
const
|
|
50
|
-
|
|
59
|
+
const rNorm = toSlash(r).replace(/\/+$/, '');
|
|
60
|
+
const base = rNorm.split('/').pop();
|
|
61
|
+
return s.includes(`/${base}/`) || s.startsWith(`${base}/`) || s.includes(rNorm);
|
|
51
62
|
});
|
|
52
63
|
}
|
|
53
64
|
|
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 } from './frontmatter.mjs';
|
|
3
|
+
import { extractFrontmatter, parseSimpleFrontmatter, normalizeEol } from './frontmatter.mjs';
|
|
4
4
|
import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso, suggestCandidates, emitFilesFooter, isArchivedPath } from './util.mjs';
|
|
5
5
|
import { gitMv, getGitLastModifiedBatch } from './git.mjs';
|
|
6
6
|
import { buildIndex, collectDocFiles, resolveDocArg } from './index.mjs';
|
|
@@ -935,7 +935,7 @@ function countRefsToUpdate(oldPath, newPath, config) {
|
|
|
935
935
|
// — never auto-creates the section (don't surprise users on old plans/docs).
|
|
936
936
|
export function appendVersionHistory(filePath, entry, { createSection = false } = {}) {
|
|
937
937
|
let raw;
|
|
938
|
-
try { raw = readFileSync(filePath, 'utf8'); } catch { return false; }
|
|
938
|
+
try { raw = normalizeEol(readFileSync(filePath, 'utf8')); } catch { return false; }
|
|
939
939
|
if (!raw.startsWith('---\n')) return false;
|
|
940
940
|
|
|
941
941
|
const endMarker = raw.indexOf('\n---\n', 4);
|
|
@@ -979,7 +979,7 @@ export function appendVersionHistory(filePath, entry, { createSection = false }
|
|
|
979
979
|
}
|
|
980
980
|
|
|
981
981
|
export function updateFrontmatter(filePath, updates) {
|
|
982
|
-
const raw = readFileSync(filePath, 'utf8');
|
|
982
|
+
const raw = normalizeEol(readFileSync(filePath, 'utf8'));
|
|
983
983
|
// Name the remedy in the error: this is where every status verb lands when a
|
|
984
984
|
// doc was created outside dotmd, and "no frontmatter block" alone left
|
|
985
985
|
// sessions retrying other verbs instead of fixing the doc.
|
|
@@ -1009,7 +1009,7 @@ export function updateFrontmatter(filePath, updates) {
|
|
|
1009
1009
|
// to updateFrontmatter when a block already exists so callers can hand it any
|
|
1010
1010
|
// file without pre-checking — the result is the same shape either way.
|
|
1011
1011
|
export function writeFrontmatter(filePath, fields) {
|
|
1012
|
-
const raw = readFileSync(filePath, 'utf8');
|
|
1012
|
+
const raw = normalizeEol(readFileSync(filePath, 'utf8'));
|
|
1013
1013
|
if (raw.startsWith('---\n')) {
|
|
1014
1014
|
updateFrontmatter(filePath, fields);
|
|
1015
1015
|
return;
|
package/src/new.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { toRepoPath, die, warn, nowIso, emitFilesFooter } from './util.mjs';
|
|
|
5
5
|
import { green, dim, bold } from './color.mjs';
|
|
6
6
|
import { isInteractive, promptText } from './prompt.mjs';
|
|
7
7
|
import { regenIndex } from './lifecycle.mjs';
|
|
8
|
-
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
8
|
+
import { extractFrontmatter, parseSimpleFrontmatter, normalizeEol } from './frontmatter.mjs';
|
|
9
9
|
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -203,7 +203,7 @@ Status markers (put in heading text):
|
|
|
203
203
|
// trap 4. Returns `{ frontmatter: object|null, body: string }`.
|
|
204
204
|
function splitBodyFrontmatter(rawBody) {
|
|
205
205
|
if (!rawBody || typeof rawBody !== 'string') return { frontmatter: null, body: rawBody };
|
|
206
|
-
if (!rawBody.startsWith('---\n')) return { frontmatter: null, body: rawBody };
|
|
206
|
+
if (!normalizeEol(rawBody).startsWith('---\n')) return { frontmatter: null, body: rawBody };
|
|
207
207
|
const { frontmatter: fmText, body } = extractFrontmatter(rawBody);
|
|
208
208
|
if (!fmText) return { frontmatter: null, body: rawBody };
|
|
209
209
|
const parsed = parseSimpleFrontmatter(fmText);
|