dotmd-cli 0.14.4 → 0.14.5
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 +1 -1
- package/package.json +1 -1
- package/src/query.mjs +71 -3
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
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
|
+
}
|