dotmd-cli 0.39.1 → 0.39.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/new.mjs +59 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.39.1",
3
+ "version": "0.39.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
@@ -265,17 +265,37 @@ export async function runNew(argv, config, opts = {}) {
265
265
 
266
266
  // Fail-fast when the user passes body input to a template that doesn't
267
267
  // consume it — silently discarding heredoc content is the worst UX.
268
- // Templates opt in via `acceptsBody: true` or `requiresBody: true`. All
269
- // built-in templates (doc, plan, prompt) accept body; this guard fires
270
- // only for custom templates that opt out.
268
+ // Templates opt in via `acceptsBody: true` or `requiresBody: true`.
271
269
  if (bodyInput !== null && !template.acceptsBody && !template.requiresBody) {
272
- const accepting = Object.entries(BUILTIN_TEMPLATES)
273
- .filter(([, t]) => t.acceptsBody || t.requiresBody)
274
- .map(([n]) => n);
270
+ const configTemplates = config.raw?.templates ?? {};
271
+ // Compute the accepting list from the RESOLVED set (config merged over
272
+ // built-ins) so the hint doesn't contradict the rejection.
273
+ const resolvedNames = new Set([...Object.keys(BUILTIN_TEMPLATES), ...Object.keys(configTemplates)]);
274
+ const accepting = [...resolvedNames]
275
+ .filter(n => n !== typeName)
276
+ .filter(n => {
277
+ const t = resolveTemplate(n, config);
278
+ return t.acceptsBody || t.requiresBody;
279
+ });
275
280
  const hint = accepting.length > 0
276
281
  ? ` Templates that accept body input: ${accepting.join(', ')}.`
277
282
  : '';
278
- 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.`);
283
+
284
+ // Override-of-builtin diagnosis: the most common cause is a project
285
+ // dotmd.config.mjs that copy-pasted a stripped-down `plan` template
286
+ // and dropped the body-acceptance contract. Name that explicitly so
287
+ // an agent can self-fix without spelunking the config.
288
+ const builtin = BUILTIN_TEMPLATES[typeName];
289
+ const isOverride = Boolean(configTemplates[typeName] && builtin);
290
+ const builtinAccepts = Boolean(builtin && (builtin.acceptsBody || builtin.requiresBody));
291
+ let cause;
292
+ if (isOverride && builtinAccepts) {
293
+ const where = config.configPath ? toRepoPath(config.configPath, config.repoRoot) : 'dotmd.config.mjs';
294
+ cause = `Your config (${where}) overrides the built-in \`${typeName}\` template, and the override drops body acceptance.\nFix: in that override, add \`acceptsBody: true\` AND interpolate \`\${ctx?.bodyInput?.trim() ?? ''}\` into your \`body\` fn (e.g., inside \`## Problem\`). Or drop the override to use the built-in.`;
295
+ } else {
296
+ cause = `Either drop the body, switch to a template that accepts it, or set \`acceptsBody: true\` on your custom \`${typeName}\` template in dotmd.config.mjs.`;
297
+ }
298
+ die(`\`${typeName}\` template does not accept body input, but body was passed via ${bodyInputSource}.${hint}\n${cause}`);
279
299
  }
280
300
 
281
301
  // If name contains path separators, split into directory prefix and basename
@@ -379,10 +399,39 @@ export async function runNew(argv, config, opts = {}) {
379
399
  }
380
400
 
381
401
  function resolveTemplate(name, config) {
382
- // Config templates take priority
383
402
  const configTemplates = config.raw?.templates ?? {};
384
- if (configTemplates[name]) return configTemplates[name];
385
- if (BUILTIN_TEMPLATES[name]) return BUILTIN_TEMPLATES[name];
403
+ const override = configTemplates[name];
404
+ const builtin = BUILTIN_TEMPLATES[name];
405
+
406
+ if (override) {
407
+ if (!builtin) return { ...override, _overridesBuiltin: false };
408
+ // Partial-override DX: shallow-merge built-in under override so missing
409
+ // fields (description, dir, targetRoot, defaultStatus, frontmatter, body,
410
+ // acceptsBody, requiresBody) fall back to the built-in. Anything the
411
+ // override explicitly declares wins.
412
+ const merged = { ...builtin, ...override, _overridesBuiltin: true };
413
+
414
+ // Body-loss guard: if the override supplies its OWN body fn but doesn't
415
+ // explicitly opt in to body acceptance, the inherited built-in
416
+ // `acceptsBody`/`requiresBody` could let body input flow into a custom
417
+ // body fn that doesn't honor `ctx.bodyInput` — silently discarding the
418
+ // heredoc, the worst-UX bug fix #9 was added to prevent. Strip the
419
+ // inherited flags so the fail-fast guard fires. EXCEPT when the custom
420
+ // body fn references `bodyInput` itself, in which case it's clearly
421
+ // body-aware and inheriting acceptsBody is the agent-first move.
422
+ const overrodeBody = typeof override.body === 'function';
423
+ const declaredAcceptance = override.acceptsBody !== undefined || override.requiresBody !== undefined;
424
+ if (overrodeBody && !declaredAcceptance) {
425
+ const bodyAware = /bodyInput/.test(override.body.toString());
426
+ if (!bodyAware) {
427
+ merged.acceptsBody = undefined;
428
+ merged.requiresBody = undefined;
429
+ }
430
+ }
431
+ return merged;
432
+ }
433
+
434
+ if (builtin) return builtin;
386
435
 
387
436
  const available = [...new Set([...Object.keys(BUILTIN_TEMPLATES), ...Object.keys(configTemplates)])];
388
437
  die(`Unknown type: ${name}\nAvailable: ${available.join(', ')}`);