dotmd-cli 0.13.0 → 0.13.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/bin/dotmd.mjs CHANGED
@@ -4,29 +4,6 @@ import { readFileSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import path from 'node:path';
6
6
  import { resolveConfig } from '../src/config.mjs';
7
- import { buildIndex } from '../src/index.mjs';
8
- import { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage } from '../src/render.mjs';
9
- import { renderIndexFile, writeIndex } from '../src/index-file.mjs';
10
- import { runFocus, runQuery } from '../src/query.mjs';
11
- import { runStatus, runArchive, runTouch, runBulkArchive, runPickup, runFinish } from '../src/lifecycle.mjs';
12
- import { runInit } from '../src/init.mjs';
13
- import { runNew } from '../src/new.mjs';
14
- import { runCompletions } from '../src/completions.mjs';
15
- import { runWatch } from '../src/watch.mjs';
16
- import { runDiff } from '../src/diff.mjs';
17
- import { runLint } from '../src/lint.mjs';
18
- import { runRename } from '../src/rename.mjs';
19
- import { runMigrate } from '../src/migrate.mjs';
20
- import { runFixRefs, fixBrokenRefs } from '../src/fix-refs.mjs';
21
- import { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } from '../src/graph.mjs';
22
- import { runDoctor } from '../src/doctor.mjs';
23
- import { buildStats, renderStats, renderStatsJson } from '../src/stats.mjs';
24
- import { runSummary } from '../src/summary.mjs';
25
- import { runDeps, runUnblocks } from '../src/deps.mjs';
26
- import { runHealth } from '../src/health.mjs';
27
- import { runGlossary } from '../src/glossary.mjs';
28
- import { runExport } from '../src/export.mjs';
29
- import { runNotion } from '../src/notion.mjs';
30
7
  import { die, warn, levenshtein } from '../src/util.mjs';
31
8
 
32
9
  const __filename = fileURLToPath(import.meta.url);
@@ -38,23 +15,24 @@ const HELP = {
38
15
 
39
16
  View & Query:
40
17
  list [--verbose] [--json] List docs grouped by status (default command)
41
- json Full index as JSON
42
- briefing [--json] Compact summary for session start (5-10 lines)
18
+ briefing [--json] Compact summary for session start
43
19
  context [--summarize] [--json] Full briefing (LLM-oriented)
44
20
  focus [status] [--json] Detailed view for one status group
45
21
  query [filters] [--json] Filtered search (--status, --keyword, --stale, etc.)
46
- coverage [--json] Metadata coverage report
22
+ plans All plans (preset)
23
+ stale Stale docs (preset)
24
+ actionable Docs with next steps (preset)
25
+
26
+ Analyze:
47
27
  stats [--json] Doc health dashboard
28
+ health [--json] Plan velocity, aging, and pipeline health
29
+ coverage [--json] Metadata coverage report
48
30
  graph [--dot] [--json] Visualize document relationships
49
31
  deps [file] [--json] Dependency tree or overview
50
32
  unblocks <file> [--json] Show what completes when this doc ships
51
- health [--json] Plan velocity, aging, and pipeline health
52
- glossary <term> [--list] [--json] Look up domain terms + related docs
53
33
  diff [file] [--summarize] Show changes since last updated date
54
- plans List all plans (shortcut for query --type plan)
55
- stale List stale docs across all statuses
56
- actionable List docs with a next step ready to act on
57
34
  summary <file> [--json] AI summary of a document
35
+ glossary <term> [--list] [--json] Look up domain terms + related docs
58
36
 
59
37
  Validate & Fix:
60
38
  check [--fix] [--errors-only] [--json] Validate frontmatter and references
@@ -427,6 +405,7 @@ async function main() {
427
405
  }
428
406
 
429
407
  if (command === 'completions') {
408
+ const { runCompletions } = await import('../src/completions.mjs');
430
409
  runCompletions(args.slice(1));
431
410
  return;
432
411
  }
@@ -447,12 +426,13 @@ async function main() {
447
426
 
448
427
  // Init — now has access to config for Claude command generation
449
428
  if (command === 'init') {
429
+ const { runInit } = await import('../src/init.mjs');
450
430
  runInit(process.cwd(), config.configFound ? config : null);
451
431
  return;
452
432
  }
453
433
 
454
434
  // Watch is a pure proxy — pass raw args so the child process gets all flags
455
- if (command === 'watch') { runWatch(args.slice(1), config); return; }
435
+ if (command === 'watch') { const { runWatch } = await import('../src/watch.mjs'); runWatch(args.slice(1), config); return; }
456
436
 
457
437
  // Strip global flags from restArgs so commands don't have to filter them
458
438
  const restArgs = [];
@@ -485,35 +465,41 @@ async function main() {
485
465
 
486
466
  // Preset aliases
487
467
  if (config.presets[command]) {
468
+ const { buildIndex } = await import('../src/index.mjs');
469
+ const { runQuery } = await import('../src/query.mjs');
488
470
  const index = buildIndex(config);
489
471
  runQuery(index, [...config.presets[command], ...restArgs], config);
490
472
  return;
491
473
  }
492
474
 
493
475
  // Commands that handle their own index building
494
- if (command === 'diff') { runDiff(restArgs, config); return; }
495
- if (command === 'summary') { runSummary(restArgs, config); return; }
496
- if (command === 'deps') { runDeps(restArgs, config); return; }
497
- if (command === 'unblocks') { runUnblocks(restArgs, config); return; }
498
- if (command === 'health') { runHealth(restArgs, config); return; }
499
- if (command === 'glossary') { runGlossary(restArgs, config); return; }
500
- if (command === 'export') { runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
501
- if (command === 'notion') { await runNotion(restArgs, config, { dryRun }); return; }
476
+ if (command === 'diff') { const { runDiff } = await import('../src/diff.mjs'); runDiff(restArgs, config); return; }
477
+ if (command === 'summary') { const { runSummary } = await import('../src/summary.mjs'); runSummary(restArgs, config); return; }
478
+ if (command === 'deps') { const { runDeps } = await import('../src/deps.mjs'); runDeps(restArgs, config); return; }
479
+ if (command === 'unblocks') { const { runUnblocks } = await import('../src/deps.mjs'); runUnblocks(restArgs, config); return; }
480
+ if (command === 'health') { const { runHealth } = await import('../src/health.mjs'); runHealth(restArgs, config); return; }
481
+ if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
482
+ if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
483
+ if (command === 'notion') { const { runNotion } = await import('../src/notion.mjs'); await runNotion(restArgs, config, { dryRun }); return; }
502
484
 
503
485
  // Lifecycle commands
504
- if (command === 'pickup') { await runPickup(restArgs, config, { dryRun }); return; }
505
- if (command === 'finish') { await runFinish(restArgs, config, { dryRun }); return; }
506
- if (command === 'status') { await runStatus(restArgs, config, { dryRun }); return; }
507
- if (command === 'archive') { runArchive(restArgs, config, { dryRun }); return; }
508
- if (command === 'bulk' && restArgs[0] === 'archive') { runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
509
- if (command === 'touch') { runTouch(restArgs, config, { dryRun }); return; }
510
- if (command === 'new') { await runNew(restArgs, config, { dryRun, root: rootArg }); return; }
511
- if (command === 'lint') { runLint(restArgs, config, { dryRun }); return; }
512
- if (command === 'rename') { await runRename(restArgs, config, { dryRun }); return; }
513
- if (command === 'migrate') { runMigrate(restArgs, config, { dryRun }); return; }
514
- if (command === 'fix-refs') { runFixRefs(restArgs, config, { dryRun }); return; }
515
- if (command === 'doctor') { runDoctor(restArgs, config, { dryRun }); return; }
516
-
486
+ if (command === 'pickup') { const { runPickup } = await import('../src/lifecycle.mjs'); await runPickup(restArgs, config, { dryRun }); return; }
487
+ if (command === 'finish') { const { runFinish } = await import('../src/lifecycle.mjs'); await runFinish(restArgs, config, { dryRun }); return; }
488
+ if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
489
+ if (command === 'archive') { const { runArchive } = await import('../src/lifecycle.mjs'); runArchive(restArgs, config, { dryRun }); return; }
490
+ if (command === 'bulk' && restArgs[0] === 'archive') { const { runBulkArchive } = await import('../src/lifecycle.mjs'); runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
491
+ if (command === 'touch') { const { runTouch } = await import('../src/lifecycle.mjs'); runTouch(restArgs, config, { dryRun }); return; }
492
+ if (command === 'new') { const { runNew } = await import('../src/new.mjs'); await runNew(restArgs, config, { dryRun, root: rootArg }); return; }
493
+ if (command === 'lint') { const { runLint } = await import('../src/lint.mjs'); runLint(restArgs, config, { dryRun }); return; }
494
+ if (command === 'rename') { const { runRename } = await import('../src/rename.mjs'); await runRename(restArgs, config, { dryRun }); return; }
495
+ if (command === 'migrate') { const { runMigrate } = await import('../src/migrate.mjs'); runMigrate(restArgs, config, { dryRun }); return; }
496
+ if (command === 'fix-refs') { const { runFixRefs } = await import('../src/fix-refs.mjs'); runFixRefs(restArgs, config, { dryRun }); return; }
497
+ if (command === 'doctor') { const { runDoctor } = await import('../src/doctor.mjs'); runDoctor(restArgs, config, { dryRun }); return; }
498
+
499
+ // All remaining commands need the index + render modules
500
+ const { buildIndex } = await import('../src/index.mjs');
501
+ const { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage } = await import('../src/render.mjs');
502
+ const { runFocus, runQuery } = await import('../src/query.mjs');
517
503
  const index = buildIndex(config);
518
504
 
519
505
  // Apply --root and --type filters
@@ -570,6 +556,8 @@ async function main() {
570
556
 
571
557
  if (fix) {
572
558
  // Auto-fix: broken refs, then lint, then rebuild index
559
+ const { fixBrokenRefs } = await import('../src/fix-refs.mjs');
560
+ const { runLint } = await import('../src/lint.mjs');
573
561
  fixBrokenRefs(config, { dryRun, quiet: false });
574
562
  runLint(['--fix'], config, { dryRun });
575
563
  if (config.indexPath) {
@@ -629,6 +617,7 @@ async function main() {
629
617
  }
630
618
 
631
619
  if (command === 'stats') {
620
+ const { buildStats, renderStats, renderStatsJson } = await import('../src/stats.mjs');
632
621
  const stats = buildStats(index, config);
633
622
  if (args.includes('--json')) {
634
623
  process.stdout.write(renderStatsJson(stats));
@@ -643,6 +632,7 @@ async function main() {
643
632
  die('Index generation is not configured. Add an `index` section to your dotmd.config.mjs.');
644
633
  }
645
634
  const write = args.includes('--write');
635
+ const { renderIndexFile, writeIndex } = await import('../src/index-file.mjs');
646
636
  const rendered = renderIndexFile(index, config);
647
637
  if (write && !dryRun) {
648
638
  writeIndex(rendered, config);
@@ -730,6 +720,7 @@ async function main() {
730
720
  }
731
721
 
732
722
  if (command === 'graph') {
723
+ const { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } = await import('../src/graph.mjs');
733
724
  const statusFilter = (() => { const i = args.indexOf('--status'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
734
725
  const moduleFilter = (() => { const i = args.indexOf('--module'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
735
726
  const surfaceFilter = (() => { const i = args.indexOf('--surface'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.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/git.mjs CHANGED
@@ -19,6 +19,24 @@ export function getGitLastModified(relPath, repoRoot) {
19
19
  return result.stdout.trim();
20
20
  }
21
21
 
22
+ export function getGitLastModifiedBatch(repoRoot) {
23
+ const result = spawnSync('git', [
24
+ 'log', '--format=commit %aI', '--name-only', '--diff-filter=ACDMR', 'HEAD',
25
+ ], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
26
+ if (result.error || result.status !== 0) return new Map();
27
+
28
+ const map = new Map();
29
+ let currentDate = null;
30
+ for (const line of result.stdout.split('\n')) {
31
+ if (line.startsWith('commit ')) {
32
+ currentDate = line.slice(7).trim();
33
+ } else if (line && currentDate && !map.has(line)) {
34
+ map.set(line, currentDate);
35
+ }
36
+ }
37
+ return map;
38
+ }
39
+
22
40
  export function gitMv(source, target, repoRoot) {
23
41
  ensureGit();
24
42
  const result = spawnSync('git', ['mv', source, target], {
package/src/lifecycle.mjs CHANGED
@@ -2,7 +2,7 @@ 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
4
  import { asString, toRepoPath, die, warn, resolveDocPath, escapeRegex } from './util.mjs';
5
- import { gitMv, getGitLastModified } from './git.mjs';
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';
8
8
  import { green, dim, yellow } from './color.mjs';
@@ -356,6 +356,7 @@ export function runTouch(argv, config, opts = {}) {
356
356
 
357
357
  const prefix = dryRun ? dim('[dry-run] ') : '';
358
358
  let synced = 0;
359
+ const gitDates = getGitLastModifiedBatch(config.repoRoot);
359
360
 
360
361
  for (const filePath of allFiles) {
361
362
  const repoPath = toRepoPath(filePath, config.repoRoot);
@@ -368,7 +369,7 @@ export function runTouch(argv, config, opts = {}) {
368
369
  if (config.lifecycle.skipStaleFor.has(status)) continue;
369
370
 
370
371
  const fmUpdated = asString(parsed.updated);
371
- const gitDate = getGitLastModified(repoPath, config.repoRoot);
372
+ const gitDate = gitDates.get(repoPath) ?? null;
372
373
  if (!gitDate) continue;
373
374
 
374
375
  const gitDay = gitDate.slice(0, 10);
package/src/query.mjs CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { capitalize, toSlug, warn } from './util.mjs';
4
4
  import { renderProgressBar } from './render.mjs';
5
5
  import { computeDaysSinceUpdate, computeIsStale } from './validate.mjs';
6
- import { getGitLastModified } from './git.mjs';
6
+ import { getGitLastModifiedBatch } from './git.mjs';
7
7
  import { extractFrontmatter } from './frontmatter.mjs';
8
8
  import { summarizeDocBody } from './ai.mjs';
9
9
  import { dim } from './color.mjs';
@@ -128,8 +128,9 @@ export function filterDocs(docs, filters, config) {
128
128
  if (filters.updatedSince) result = result.filter(d => d.updated && d.updated >= filters.updatedSince);
129
129
 
130
130
  if (filters.git) {
131
+ const gitDates = getGitLastModifiedBatch(config.repoRoot);
131
132
  for (const doc of result) {
132
- const gitDate = getGitLastModified(doc.path, config.repoRoot);
133
+ const gitDate = gitDates.get(doc.path) ?? null;
133
134
  if (gitDate) {
134
135
  doc.daysSinceUpdate = computeDaysSinceUpdate(gitDate);
135
136
  doc.isStale = computeIsStale(doc.status, gitDate, config);
package/src/validate.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { asString } from './util.mjs';
4
- import { getGitLastModified } from './git.mjs';
4
+ import { getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
5
5
  import { toRepoPath } from './util.mjs';
6
6
 
7
7
  const NOW = new Date();
@@ -150,11 +150,12 @@ export function checkBidirectionalReferences(docs, config) {
150
150
 
151
151
  export function checkGitStaleness(docs, config) {
152
152
  const warnings = [];
153
+ const gitDates = getGitLastModifiedBatch(config.repoRoot);
153
154
  for (const doc of docs) {
154
155
  if (config.lifecycle.skipStaleFor.has(doc.status)) continue;
155
156
  if (!doc.updated) continue;
156
157
 
157
- const gitDate = getGitLastModified(doc.path, config.repoRoot);
158
+ const gitDate = gitDates.get(doc.path) ?? null;
158
159
  if (!gitDate) continue;
159
160
 
160
161
  const gitDay = gitDate.slice(0, 10);