dotmd-cli 0.14.4 → 0.14.6

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
@@ -500,7 +500,7 @@ async function main() {
500
500
  const { buildIndex } = await import('../src/index.mjs');
501
501
  const { runQuery } = await import('../src/query.mjs');
502
502
  const index = buildIndex(config);
503
- runQuery(index, [...config.presets[command], ...restArgs], config);
503
+ runQuery(index, [...config.presets[command], ...restArgs], config, { preset: command });
504
504
  return;
505
505
  }
506
506
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
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/query.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { capitalize, toSlug, warn } from './util.mjs';
3
+ import { capitalize, toSlug, truncate, warn } from './util.mjs';
4
4
  import { renderProgressBar } from './render.mjs';
5
5
  import { computeDaysSinceUpdate, computeIsStale } from './validate.mjs';
6
6
  import { getGitLastModifiedBatch } from './git.mjs';
7
7
  import { extractFrontmatter } from './frontmatter.mjs';
8
8
  import { summarizeDocBody } from './ai.mjs';
9
- import { dim } from './color.mjs';
9
+ import { bold, dim, yellow, red } from './color.mjs';
10
10
 
11
11
  export function runFocus(index, argv, config) {
12
12
  // Find first positional arg, skipping flag-value pairs like --root <name>
@@ -50,7 +50,7 @@ export function runFocus(index, argv, config) {
50
50
  }
51
51
  }
52
52
 
53
- export function runQuery(index, argv, config) {
53
+ export function runQuery(index, argv, config, opts = {}) {
54
54
  const filters = parseQueryArgs(argv);
55
55
  const docs = filterDocs(index.docs, filters, config);
56
56
 
@@ -64,6 +64,11 @@ export function runQuery(index, argv, config) {
64
64
  return;
65
65
  }
66
66
 
67
+ if (opts.preset === 'plans') {
68
+ renderPlansOutput(docs, filters, config);
69
+ return;
70
+ }
71
+
67
72
  renderQueryResults(docs, filters, config);
68
73
  }
69
74
 
@@ -227,3 +232,66 @@ function compareUpdatedDesc(a, b) {
227
232
  const au = a.updated ?? ''; const bu = b.updated ?? '';
228
233
  return au !== bu ? bu.localeCompare(au) : 0;
229
234
  }
235
+
236
+ function renderPlansOutput(docs, filters, config) {
237
+ if (docs.length === 0) {
238
+ process.stdout.write('No plans found.\n');
239
+ return;
240
+ }
241
+
242
+ // Summary line
243
+ const bySt = {};
244
+ for (const d of docs) { bySt[d.status] = (bySt[d.status] ?? 0) + 1; }
245
+ const counts = Object.entries(bySt).map(([s, n]) => `${n} ${s}`).join(', ');
246
+ process.stdout.write(`${bold('Plans')} ${dim(`(${docs.length})`)} ${counts}\n`);
247
+
248
+ // Active filter note (only if user applied extra filters beyond the preset defaults)
249
+ const activeFilters = [];
250
+ if (filters.statuses?.length) activeFilters.push(`status: ${filters.statuses.join(', ')}`);
251
+ if (filters.keyword) activeFilters.push(`keyword: ${filters.keyword}`);
252
+ if (filters.stale) activeFilters.push('stale only');
253
+ if (filters.hasNextStep) activeFilters.push('has next step');
254
+ if (filters.hasBlockers) activeFilters.push('has blockers');
255
+ if (activeFilters.length) process.stdout.write(dim(` filtered: ${activeFilters.join(' | ')}`) + '\n');
256
+
257
+ // Group by status, ordered by config.statusOrder
258
+ const statusGroups = new Map();
259
+ for (const d of docs) {
260
+ const s = d.status ?? 'unknown';
261
+ if (!statusGroups.has(s)) statusGroups.set(s, []);
262
+ statusGroups.get(s).push(d);
263
+ }
264
+
265
+ const orderedStatuses = [...config.statusOrder.filter(s => statusGroups.has(s)), ...([...statusGroups.keys()].filter(s => !config.statusOrder.includes(s)))];
266
+ const maxWidth = process.stdout.columns || 100;
267
+
268
+ for (const status of orderedStatuses) {
269
+ const group = statusGroups.get(status);
270
+ process.stdout.write(`\n${bold(`${capitalize(status)} (${group.length})`)}\n`);
271
+
272
+ const maxSlug = Math.min(30, Math.max(...group.map(d => toSlug(d).length)));
273
+
274
+ for (const doc of group) {
275
+ const slug = toSlug(doc).padEnd(maxSlug);
276
+ const age = doc.daysSinceUpdate != null ? `${doc.daysSinceUpdate}d` : ' —';
277
+ const ageStr = doc.daysSinceUpdate != null && doc.isStale ? red(age.padStart(4)) : dim(age.padStart(4));
278
+ const progress = renderProgressBar(doc.checklist);
279
+
280
+ const parts = [` ${slug} ${ageStr}`];
281
+ if (progress) parts.push(progress);
282
+
283
+ if (doc.blockers?.length && (status === 'blocked')) {
284
+ parts.push(yellow(`blockers: ${doc.blockers.join('; ')}`));
285
+ } else if (doc.nextStep) {
286
+ parts.push(`next: ${doc.nextStep}`);
287
+ } else {
288
+ parts.push(dim('(no next step)'));
289
+ }
290
+
291
+ const line = parts.join(' ');
292
+ process.stdout.write((line.length > maxWidth ? line.slice(0, maxWidth - 3) + '...' : line) + '\n');
293
+ }
294
+ }
295
+
296
+ process.stdout.write('\n');
297
+ }
package/src/render.mjs CHANGED
@@ -17,8 +17,10 @@ export function renderCompactList(index, config) {
17
17
  function _renderCompactList(index, config) {
18
18
  const lines = ['Index', ''];
19
19
  const maxWidth = config.display.lineWidth || process.stdout.columns || 120;
20
+ const hidden = config.lifecycle.archiveStatuses;
20
21
 
21
22
  for (const status of config.statusOrder) {
23
+ if (hidden.has(status)) continue;
22
24
  const docs = index.docs.filter(d => d.status === status);
23
25
  if (!docs.length) continue;
24
26
 
@@ -43,7 +45,7 @@ function _renderCompactList(index, config) {
43
45
 
44
46
  // Render docs with statuses not in statusOrder
45
47
  const knownStatuses = new Set(config.statusOrder);
46
- const otherStatuses = [...new Set(index.docs.filter(d => d.status && !knownStatuses.has(d.status)).map(d => d.status))].sort();
48
+ const otherStatuses = [...new Set(index.docs.filter(d => d.status && !knownStatuses.has(d.status) && !hidden.has(d.status)).map(d => d.status))].sort();
47
49
  for (const status of otherStatuses) {
48
50
  const docs = index.docs.filter(d => d.status === status);
49
51
  if (!docs.length) continue;
@@ -72,8 +74,10 @@ function _renderCompactList(index, config) {
72
74
 
73
75
  export function renderVerboseList(index, config) {
74
76
  const lines = ['Index', ''];
77
+ const hidden = config.lifecycle.archiveStatuses;
75
78
 
76
79
  for (const status of config.statusOrder) {
80
+ if (hidden.has(status)) continue;
77
81
  const docs = index.docs.filter(doc => doc.status === status);
78
82
  if (docs.length === 0) continue;
79
83
 
@@ -90,7 +94,7 @@ export function renderVerboseList(index, config) {
90
94
 
91
95
  // Render docs with statuses not in statusOrder
92
96
  const knownStatuses = new Set(config.statusOrder);
93
- const otherStatuses = [...new Set(index.docs.filter(d => d.status && !knownStatuses.has(d.status)).map(d => d.status))].sort();
97
+ const otherStatuses = [...new Set(index.docs.filter(d => d.status && !knownStatuses.has(d.status) && !hidden.has(d.status)).map(d => d.status))].sort();
94
98
  for (const status of otherStatuses) {
95
99
  const docs = index.docs.filter(doc => doc.status === status);
96
100
  if (docs.length === 0) continue;