dotmd-cli 0.18.0 → 0.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
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/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, escapeRegex } from './util.mjs';
4
+ import { asString, toRepoPath, die, warn, resolveDocPath, escapeRegex, nowIso } 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';
@@ -71,7 +71,7 @@ export async function runStatus(argv, config, opts = {}) {
71
71
  return;
72
72
  }
73
73
 
74
- const today = new Date().toISOString().slice(0, 10);
74
+ const today = nowIso();
75
75
  const archiveDir = path.join(fileRoot, config.archiveDir);
76
76
  const relFromRoot = path.relative(fileRoot, filePath);
77
77
  const inArchive = relFromRoot.startsWith(config.archiveDir + '/') || relFromRoot.startsWith(config.archiveDir + path.sep);
@@ -185,7 +185,7 @@ export async function runPickup(argv, config, opts = {}) {
185
185
  const pickupable = new Set(['active', 'planned', 'in-session']);
186
186
  if (oldStatus && !pickupable.has(oldStatus) && !handoffQueued) die(`Cannot pick up a plan with status '${oldStatus}'. Must be active or planned.\n ${repoPath}`);
187
187
 
188
- const today = new Date().toISOString().slice(0, 10);
188
+ const today = nowIso();
189
189
  const leaseOldStatus = oldStatus === 'in-session' ? 'active' : (oldStatus ?? 'active');
190
190
  let leaseOutcome = 'acquired';
191
191
 
@@ -302,7 +302,7 @@ export async function runUnpickup(argv, config, opts = {}) {
302
302
  const parsedFm = parseSimpleFrontmatter(fmRaw);
303
303
  const cur = asString(parsedFm.status);
304
304
  if (cur === 'in-session') {
305
- const today = new Date().toISOString().slice(0, 10);
305
+ const today = nowIso();
306
306
  updateFrontmatter(filePath, { status: newStatus, updated: today });
307
307
  }
308
308
  // If frontmatter is no longer in-session (manual flip), leave it alone.
@@ -412,7 +412,7 @@ export async function runFinish(argv, config, opts = {}) {
412
412
 
413
413
  if (oldStatus !== 'in-session') die(`Plan is not in-session (current: ${oldStatus}).\n ${repoPath}`);
414
414
 
415
- const today = new Date().toISOString().slice(0, 10);
415
+ const today = nowIso();
416
416
 
417
417
  if (dryRun) {
418
418
  process.stderr.write(`${dim('[dry-run]')} Would update: status: in-session → ${targetStatus}, updated: ${today}\n`);
@@ -451,7 +451,7 @@ export function runArchive(argv, config, opts = {}) {
451
451
  const parsed = parseSimpleFrontmatter(frontmatter);
452
452
  const oldStatus = asString(parsed.status) ?? 'unknown';
453
453
 
454
- const today = new Date().toISOString().slice(0, 10);
454
+ const today = nowIso();
455
455
  const targetDir = path.join(archiveFileRoot, config.archiveDir);
456
456
  const targetPath = path.join(targetDir, path.basename(filePath));
457
457
  const oldRepoPath = toRepoPath(filePath, config.repoRoot);
@@ -608,7 +608,7 @@ export function runTouch(argv, config, opts = {}) {
608
608
  const filePath = resolveDocPath(input, config);
609
609
  if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); }
610
610
 
611
- const today = new Date().toISOString().slice(0, 10);
611
+ const today = nowIso();
612
612
 
613
613
  if (dryRun) {
614
614
  process.stdout.write(`${dim('[dry-run]')} Would touch: ${toRepoPath(filePath, config.repoRoot)} (updated → ${today})\n`);
@@ -790,7 +790,7 @@ export async function runHandoff(argv, config, opts = {}) {
790
790
  die(`Not held by this session: ${repoPath}\n Run \`dotmd pickup ${repoPath}\` first.`);
791
791
  }
792
792
 
793
- const today = new Date().toISOString().slice(0, 10);
793
+ const today = nowIso();
794
794
  const targetStatus = lease.oldStatus || 'active';
795
795
 
796
796
  if (dryRun) {
package/src/new.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { toRepoPath, die, warn } from './util.mjs';
3
+ import { toRepoPath, die, warn, nowIso } from './util.mjs';
4
4
  import { green, dim, bold } from './color.mjs';
5
5
  import { isInteractive, promptText } from './prompt.mjs';
6
6
 
@@ -11,9 +11,82 @@ const BUILTIN_TEMPLATES = {
11
11
  body: (t) => `\n# ${t}\n`,
12
12
  },
13
13
  plan: {
14
- description: 'Execution plan with module, surface, and cross-references',
15
- frontmatter: (s, d) => `type: plan\nstatus: ${s}\nupdated: ${d}\nsurface:\nmodule:\ncurrent_state:\nrelated_plans:`,
16
- body: (t) => `\n# ${t}\n\n## Overview\n\n\n\n## Implementation Plan\n\n- [ ] \n\n## Open Questions\n\n\n`,
14
+ description: 'Execution plan — build-up shape (Problem → Phases → Closeout) with phase status markers and Version History',
15
+ frontmatter: (s, d) => [
16
+ 'type: plan',
17
+ `status: ${s}`,
18
+ `created: ${d}`,
19
+ `updated: ${d}`,
20
+ 'surfaces: []',
21
+ 'modules: []',
22
+ 'domain:',
23
+ 'audience: internal',
24
+ 'parent_plan:',
25
+ 'related_plans: []',
26
+ 'related_docs: []',
27
+ 'current_state:',
28
+ 'next_step:',
29
+ ].join('\n'),
30
+ body: (t, ctx) => `
31
+ # ${t}
32
+
33
+ > One-paragraph problem statement: what this plan is for, why now.
34
+
35
+ ## Problem
36
+
37
+
38
+
39
+ ## Goals
40
+
41
+
42
+
43
+ ## Non-Goals
44
+
45
+
46
+
47
+ ## What Exists Today
48
+
49
+
50
+
51
+ ## Constraints
52
+
53
+
54
+
55
+ ## Decisions
56
+
57
+
58
+
59
+ ## Open Questions
60
+
61
+
62
+
63
+ ## Phases
64
+
65
+ <!--
66
+ Status markers (put in heading text):
67
+ ⬜ not started
68
+ 🟡 in progress (pickup targets this)
69
+ ✅ shipped (history; pickup skips)
70
+ ⏭ skipped (with reason in body)
71
+ 🚧 blocked (link to blocker)
72
+ -->
73
+
74
+ ### Phase 1 — <title> ⬜
75
+
76
+
77
+
78
+ ## Deferred
79
+
80
+
81
+
82
+ ## Version History
83
+
84
+ - **${ctx?.today ?? ''}** Created.
85
+
86
+ ## Closeout
87
+
88
+ <!-- Filled on archive: what shipped, key commits, deferrals dispositioned. -->
89
+ `,
17
90
  },
18
91
  adr: {
19
92
  description: 'Architecture Decision Record',
@@ -115,7 +188,7 @@ export async function runNew(argv, config, opts = {}) {
115
188
  die(`File already exists: ${repoPath}`);
116
189
  }
117
190
 
118
- const today = new Date().toISOString().slice(0, 10);
191
+ const today = nowIso();
119
192
 
120
193
  // Generate content
121
194
  let content;
@@ -123,7 +196,7 @@ export async function runNew(argv, config, opts = {}) {
123
196
  content = template(name, { status, title: docTitle, today });
124
197
  } else {
125
198
  const fm = template.frontmatter(status, today);
126
- const body = template.body(docTitle);
199
+ const body = template.body(docTitle, { today, status });
127
200
  content = `---\n${fm}\n---\n${body}`;
128
201
  }
129
202
 
package/src/util.mjs CHANGED
@@ -55,6 +55,10 @@ export function toRepoPath(absolutePath, repoRoot) {
55
55
  return path.relative(repoRoot, absolutePath).split(path.sep).join('/');
56
56
  }
57
57
 
58
+ export function nowIso() {
59
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
60
+ }
61
+
58
62
  export function warn(message) {
59
63
  process.stderr.write(`${dim(message)}\n`);
60
64
  }