dotmd-cli 0.29.0 → 0.29.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.29.0",
3
+ "version": "0.29.2",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/new.mjs CHANGED
@@ -128,6 +128,7 @@ Status markers (put in heading text):
128
128
  targetRoot: 'prompts',
129
129
  defaultStatus: 'pending',
130
130
  requiresBody: true,
131
+ acceptsBody: true,
131
132
  frontmatter: (s, d, ctx) => [
132
133
  'type: prompt',
133
134
  `status: ${s}`,
@@ -222,13 +223,31 @@ export async function runNew(argv, config, opts = {}) {
222
223
 
223
224
  // Body input resolution: messageFlag > bodyArg > nothing
224
225
  let bodyInput = null;
225
- if (messageFlag !== null) bodyInput = readBodyInput(messageFlag);
226
- else if (bodyArg !== null) bodyInput = readBodyInput(bodyArg);
226
+ let bodyInputSource = null;
227
+ if (messageFlag !== null) { bodyInput = readBodyInput(messageFlag); bodyInputSource = '--message'; }
228
+ else if (bodyArg !== null) {
229
+ bodyInput = readBodyInput(bodyArg);
230
+ bodyInputSource = bodyArg === '-' ? 'stdin (`-`)' : (bodyArg.startsWith('@') ? `file (\`${bodyArg}\`)` : 'inline body argument');
231
+ }
227
232
 
228
233
  if (template.requiresBody && (!bodyInput || !bodyInput.trim())) {
229
234
  die(`\`${typeName}\` template requires a body. Pass inline, --message "...", - for stdin, or @path for a file.`);
230
235
  }
231
236
 
237
+ // Fail-fast when the user passes body input to a template that doesn't
238
+ // consume it — silently discarding heredoc content is the worst UX.
239
+ // Templates opt in via `acceptsBody: true` or `requiresBody: true`. Built-in
240
+ // `prompt` is the only template that consumes body by default.
241
+ if (bodyInput !== null && !template.acceptsBody && !template.requiresBody) {
242
+ const accepting = Object.entries(BUILTIN_TEMPLATES)
243
+ .filter(([, t]) => t.acceptsBody || t.requiresBody)
244
+ .map(([n]) => n);
245
+ const hint = accepting.length > 0
246
+ ? ` Templates that accept body input: ${accepting.join(', ')}.`
247
+ : '';
248
+ die(`\`${typeName}\` template does not accept body input, but body was passed via ${bodyInputSource}.${hint}\nEither drop the body, switch to a template that accepts it, or set \`acceptsBody: true\` on your custom \`${typeName}\` template in dotmd.config.mjs.`);
249
+ }
250
+
232
251
  // If name contains path separators, split into directory prefix and basename
233
252
  let nameDir = null;
234
253
  let namePart = name;
package/src/validate.mjs CHANGED
@@ -5,6 +5,44 @@ import { toRepoPath } from './util.mjs';
5
5
 
6
6
  const NOW = new Date();
7
7
 
8
+ // Type-conventional dirs are the directories where `dotmd new <type>` lands
9
+ // live (non-archive) docs of that type. Built-ins use `dir` ('plans'/'prompts')
10
+ // and `targetRoot`. In flat-array root configs (e.g. root: ['docs/plans',
11
+ // 'docs/prompts']), the root itself is a type-conventional dir; in default
12
+ // single-root configs (root: 'docs'), the type-conventional dirs are
13
+ // '<root>/plans' and '<root>/prompts'. This helper builds the union so the
14
+ // archive-drift check works for both layouts. Custom user templates with
15
+ // their own `dir` would extend this; we hard-code the built-in dir names.
16
+ const BUILTIN_TYPE_DIR_NAMES = ['plans', 'prompts'];
17
+
18
+ function liveTypeDirsForRoots(config) {
19
+ const set = new Set();
20
+ const roots = config.docsRoots || (config.docsRoot ? [config.docsRoot] : []);
21
+ for (const root of roots) {
22
+ const rootRel = path.relative(config.repoRoot, root).split(path.sep).join('/');
23
+ // The root itself is a live dir (covers flat-array layouts where the
24
+ // root IS the type-container).
25
+ set.add(rootRel);
26
+ // Each builtin type-dir joined to the root (covers single-root layouts
27
+ // where 'docs' contains 'docs/plans' and 'docs/prompts' subdirs).
28
+ for (const dirName of BUILTIN_TYPE_DIR_NAMES) {
29
+ // Skip if root already ends in this name (no double-nesting like
30
+ // 'docs/prompts/prompts').
31
+ if (path.basename(rootRel) === dirName) continue;
32
+ set.add(rootRel ? `${rootRel}/${dirName}` : dirName);
33
+ }
34
+ // User template dirs from config (extend the set with whatever live
35
+ // dirs custom types declare).
36
+ for (const tmpl of Object.values(config.raw?.templates ?? {})) {
37
+ if (!tmpl || typeof tmpl !== 'object') continue;
38
+ if (tmpl.dir && path.basename(rootRel) !== tmpl.dir) {
39
+ set.add(rootRel ? `${rootRel}/${tmpl.dir}` : tmpl.dir);
40
+ }
41
+ }
42
+ }
43
+ return set;
44
+ }
45
+
8
46
  function isValidStatus(status, root, config, type) {
9
47
  // When a doc declares a known type, that type's status set is authoritative.
10
48
  // Falling through to the global union (across all types) would allow a
@@ -101,6 +139,26 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
101
139
  doc.warnings.push({ path: doc.path, level: 'warning', message: 'Archived plan missing `## Closeout` section.' });
102
140
  }
103
141
 
142
+ // Archive drift: a doc with an archive-flagged status (`status: archived` by
143
+ // default) whose parent dir is a "live" type-conventional location is
144
+ // misplaced — `dotmd archive` would have moved it under `<that>/archiveDir/`.
145
+ // Without this check, default `dotmd plans` / `dotmd prompts` views silently
146
+ // drop the file (because they exclude archived paths), and the user gets no
147
+ // signal it exists but is invisible. Nested intentional content (e.g.,
148
+ // `docs/plans/audit/<file>.md`) is in a non-conventional subdir and exempt.
149
+ if (config.lifecycle.archiveStatuses.has(doc.status)) {
150
+ const parentDir = path.dirname(doc.path);
151
+ const liveDirs = liveTypeDirsForRoots(config);
152
+ if (liveDirs.has(parentDir)) {
153
+ const expected = `${parentDir}/${config.archiveDir}/${path.basename(doc.path)}`;
154
+ doc.errors.push({
155
+ path: doc.path,
156
+ level: 'error',
157
+ message: `\`status: ${doc.status}\` but file is a direct child of \`${parentDir}/\`, not \`${parentDir}/${config.archiveDir}/\`. Run \`dotmd archive ${doc.path}\` to relocate to \`${expected}\`, or change the status.`,
158
+ });
159
+ }
160
+ }
161
+
104
162
  // Validate reference fields resolve to existing files
105
163
  const docDir = path.dirname(path.join(config.repoRoot, doc.path));
106
164
  const allRefFields = [...(config.referenceFields.bidirectional || []), ...(config.referenceFields.unidirectional || [])];