dotmd-cli 0.8.2 → 0.8.4
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 +91 -12
- package/package.json +1 -1
- package/src/ai.mjs +6 -1
- package/src/completions.mjs +9 -4
- package/src/config.mjs +1 -1
- package/src/diff.mjs +2 -2
- package/src/export.mjs +30 -13
- package/src/index-file.mjs +5 -4
- package/src/lifecycle.mjs +12 -3
- package/src/lint.mjs +28 -0
- package/src/new.mjs +12 -3
- package/src/prompt.mjs +25 -0
- package/src/query.mjs +7 -2
- package/src/rename.mjs +12 -5
- package/src/util.mjs +15 -0
package/bin/dotmd.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { runSummary } from '../src/summary.mjs';
|
|
|
25
25
|
import { runDeps } from '../src/deps.mjs';
|
|
26
26
|
import { runExport } from '../src/export.mjs';
|
|
27
27
|
import { runNotion } from '../src/notion.mjs';
|
|
28
|
-
import { die, warn } from '../src/util.mjs';
|
|
28
|
+
import { die, warn, levenshtein } from '../src/util.mjs';
|
|
29
29
|
|
|
30
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
31
|
const __dirname = path.dirname(__filename);
|
|
@@ -88,7 +88,10 @@ Filters:
|
|
|
88
88
|
--limit <n> Max results (default: 20)
|
|
89
89
|
--all Show all results (no limit)
|
|
90
90
|
--git Use git dates instead of frontmatter
|
|
91
|
-
--json Output as JSON
|
|
91
|
+
--json Output as JSON
|
|
92
|
+
--summarize Add AI summaries to results
|
|
93
|
+
--summarize-limit <n> Max docs to summarize (default: 5)
|
|
94
|
+
--model <name> MLX model for AI summaries`,
|
|
92
95
|
|
|
93
96
|
status: `dotmd status <file> <new-status> — transition document status
|
|
94
97
|
|
|
@@ -111,6 +114,25 @@ references in other docs, and regenerates the index.
|
|
|
111
114
|
|
|
112
115
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
113
116
|
|
|
117
|
+
coverage: `dotmd coverage — metadata coverage report
|
|
118
|
+
|
|
119
|
+
Shows which docs are missing surface, module, or audit metadata.
|
|
120
|
+
|
|
121
|
+
Options:
|
|
122
|
+
--json Machine-readable JSON output`,
|
|
123
|
+
|
|
124
|
+
focus: `dotmd focus [status] — detailed view for one status group
|
|
125
|
+
|
|
126
|
+
Shows detailed info for all docs matching the given status (default: active).`,
|
|
127
|
+
|
|
128
|
+
context: `dotmd context — compact briefing (LLM-oriented)
|
|
129
|
+
|
|
130
|
+
Generates a compact status briefing designed for AI/LLM consumption.
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
--summarize Add AI summaries for expanded docs
|
|
134
|
+
--model <name> MLX model for AI summaries`,
|
|
135
|
+
|
|
114
136
|
stats: `dotmd stats — doc health dashboard
|
|
115
137
|
|
|
116
138
|
Shows aggregated metrics: status counts, staleness, errors/warnings,
|
|
@@ -244,8 +266,14 @@ Options:
|
|
|
244
266
|
|
|
245
267
|
lint: `dotmd lint [--fix] — check and auto-fix frontmatter issues
|
|
246
268
|
|
|
247
|
-
Scans all docs for fixable problems:
|
|
248
|
-
|
|
269
|
+
Scans all docs for fixable problems:
|
|
270
|
+
- Missing status (inferred via local AI model when available)
|
|
271
|
+
- Missing updated date (set to today)
|
|
272
|
+
- Status casing (e.g. Active → active)
|
|
273
|
+
- camelCase key names (e.g. nextStep → next_step)
|
|
274
|
+
- Comma-separated surface values (converted to surfaces: array)
|
|
275
|
+
- Trailing whitespace in frontmatter values
|
|
276
|
+
- Missing newline at end of file
|
|
249
277
|
|
|
250
278
|
Without --fix, reports all issues. With --fix, applies fixes in place.
|
|
251
279
|
Use --dry-run (-n) with --fix to preview without writing anything.`,
|
|
@@ -320,7 +348,14 @@ async function main() {
|
|
|
320
348
|
const verbose = args.includes('--verbose');
|
|
321
349
|
|
|
322
350
|
const config = await resolveConfig(process.cwd(), explicitConfig);
|
|
323
|
-
|
|
351
|
+
|
|
352
|
+
// Strip global flags from restArgs so commands don't have to filter them
|
|
353
|
+
const restArgs = [];
|
|
354
|
+
for (let i = 1; i < args.length; i++) {
|
|
355
|
+
if (args[i] === '--config') { i++; continue; }
|
|
356
|
+
if (args[i] === '--dry-run' || args[i] === '-n' || args[i] === '--verbose') continue;
|
|
357
|
+
restArgs.push(args[i]);
|
|
358
|
+
}
|
|
324
359
|
|
|
325
360
|
if (!config.configFound && command !== 'init') {
|
|
326
361
|
warn('No dotmd config found — using defaults. Run `dotmd init` to create one.');
|
|
@@ -351,16 +386,16 @@ async function main() {
|
|
|
351
386
|
if (command === 'diff') { runDiff(restArgs, config); return; }
|
|
352
387
|
if (command === 'summary') { runSummary(restArgs, config); return; }
|
|
353
388
|
if (command === 'deps') { runDeps(restArgs, config); return; }
|
|
354
|
-
if (command === 'export') { runExport(restArgs, config); return; }
|
|
389
|
+
if (command === 'export') { runExport(restArgs, config, { dryRun }); return; }
|
|
355
390
|
if (command === 'notion') { await runNotion(restArgs, config, { dryRun }); return; }
|
|
356
391
|
|
|
357
392
|
// Lifecycle commands
|
|
358
|
-
if (command === 'status') { runStatus(restArgs, config, { dryRun }); return; }
|
|
393
|
+
if (command === 'status') { await runStatus(restArgs, config, { dryRun }); return; }
|
|
359
394
|
if (command === 'archive') { runArchive(restArgs, config, { dryRun }); return; }
|
|
360
395
|
if (command === 'touch') { runTouch(restArgs, config, { dryRun }); return; }
|
|
361
|
-
if (command === 'new') { runNew(restArgs, config, { dryRun }); return; }
|
|
396
|
+
if (command === 'new') { await runNew(restArgs, config, { dryRun }); return; }
|
|
362
397
|
if (command === 'lint') { runLint(restArgs, config, { dryRun }); return; }
|
|
363
|
-
if (command === 'rename') { runRename(restArgs, config, { dryRun }); return; }
|
|
398
|
+
if (command === 'rename') { await runRename(restArgs, config, { dryRun }); return; }
|
|
364
399
|
if (command === 'migrate') { runMigrate(restArgs, config, { dryRun }); return; }
|
|
365
400
|
if (command === 'fix-refs') { runFixRefs(restArgs, config, { dryRun }); return; }
|
|
366
401
|
if (command === 'doctor') { runDoctor(restArgs, config, { dryRun }); return; }
|
|
@@ -388,7 +423,9 @@ async function main() {
|
|
|
388
423
|
}
|
|
389
424
|
|
|
390
425
|
if (command === 'list') {
|
|
391
|
-
if (args.includes('--
|
|
426
|
+
if (args.includes('--json')) {
|
|
427
|
+
process.stdout.write(`${JSON.stringify(index, null, 2)}\n`);
|
|
428
|
+
} else if (args.includes('--verbose')) {
|
|
392
429
|
process.stdout.write(renderVerboseList(index, config));
|
|
393
430
|
} else {
|
|
394
431
|
process.stdout.write(renderCompactList(index, config));
|
|
@@ -419,6 +456,19 @@ async function main() {
|
|
|
419
456
|
return;
|
|
420
457
|
}
|
|
421
458
|
|
|
459
|
+
if (args.includes('--json')) {
|
|
460
|
+
process.stdout.write(JSON.stringify({
|
|
461
|
+
docsScanned: index.docs.length,
|
|
462
|
+
errors: index.errors,
|
|
463
|
+
warnings: errorsOnly ? [] : index.warnings,
|
|
464
|
+
errorCount: index.errors.length,
|
|
465
|
+
warningCount: index.warnings.length,
|
|
466
|
+
passed: index.errors.length === 0,
|
|
467
|
+
}, null, 2) + '\n');
|
|
468
|
+
if (index.errors.length > 0) process.exitCode = 1;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
422
472
|
process.stdout.write(renderCheck(index, config, { errorsOnly }));
|
|
423
473
|
if (index.errors.length > 0) process.exitCode = 1;
|
|
424
474
|
return;
|
|
@@ -463,6 +513,23 @@ async function main() {
|
|
|
463
513
|
if (command === 'focus') { runFocus(index, restArgs, config); return; }
|
|
464
514
|
if (command === 'query') { runQuery(index, restArgs, config); return; }
|
|
465
515
|
if (command === 'context') {
|
|
516
|
+
if (args.includes('--json')) {
|
|
517
|
+
const byStatus = {};
|
|
518
|
+
for (const s of config.statusOrder) {
|
|
519
|
+
const docs = index.docs.filter(d => d.status === s);
|
|
520
|
+
if (docs.length) byStatus[s] = docs;
|
|
521
|
+
}
|
|
522
|
+
const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status));
|
|
523
|
+
process.stdout.write(JSON.stringify({
|
|
524
|
+
generatedAt: new Date().toISOString(),
|
|
525
|
+
docsByStatus: byStatus,
|
|
526
|
+
countsByStatus: index.countsByStatus,
|
|
527
|
+
stale: stale.map(d => ({ path: d.path, title: d.title, daysSinceUpdate: d.daysSinceUpdate })),
|
|
528
|
+
errorCount: index.errors.length,
|
|
529
|
+
warningCount: index.warnings.length,
|
|
530
|
+
}, null, 2) + '\n');
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
466
533
|
const summarize = args.includes('--summarize');
|
|
467
534
|
const modelIdx = args.indexOf('--model');
|
|
468
535
|
const model = modelIdx !== -1 && args[modelIdx + 1] ? args[modelIdx + 1] : undefined;
|
|
@@ -489,8 +556,20 @@ async function main() {
|
|
|
489
556
|
return;
|
|
490
557
|
}
|
|
491
558
|
|
|
492
|
-
// Unknown command —
|
|
493
|
-
|
|
559
|
+
// Unknown command — suggest closest match
|
|
560
|
+
const allCommands = [
|
|
561
|
+
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'context',
|
|
562
|
+
'focus', 'query', 'index', 'status', 'archive', 'touch', 'doctor',
|
|
563
|
+
'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
|
|
564
|
+
'watch', 'diff', 'new', 'init', 'completions',
|
|
565
|
+
];
|
|
566
|
+
const matches = allCommands
|
|
567
|
+
.map(c => ({ cmd: c, dist: levenshtein(command, c) }))
|
|
568
|
+
.sort((a, b) => a.dist - b.dist);
|
|
569
|
+
if (matches[0] && matches[0].dist <= 3) {
|
|
570
|
+
die(`Unknown command: ${command}\n\nDid you mean \`dotmd ${matches[0].cmd}\`?`);
|
|
571
|
+
}
|
|
572
|
+
die(`Unknown command: ${command}\n\nRun \`dotmd --help\` for available commands.`);
|
|
494
573
|
}
|
|
495
574
|
|
|
496
575
|
main().catch(err => {
|
package/package.json
CHANGED
package/src/ai.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { warn } from './util.mjs';
|
|
3
|
+
import { dim } from './color.mjs';
|
|
3
4
|
|
|
4
5
|
let uvChecked = null;
|
|
5
6
|
|
|
@@ -11,10 +12,13 @@ export function checkUvAvailable() {
|
|
|
11
12
|
return uvChecked;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export const DEFAULT_MODEL = 'mlx-community/Llama-3.2-3B-Instruct-4bit';
|
|
16
|
+
|
|
14
17
|
export function runMLX(prompt, opts = {}) {
|
|
15
|
-
const { model =
|
|
18
|
+
const { model = DEFAULT_MODEL, maxTokens = 200, timeout = 120000 } = opts;
|
|
16
19
|
if (!checkUvAvailable()) return null;
|
|
17
20
|
|
|
21
|
+
process.stderr.write(dim(' generating...'));
|
|
18
22
|
const result = spawnSync('uv', [
|
|
19
23
|
'run', '--with', 'mlx-lm',
|
|
20
24
|
'python3', '-m', 'mlx_lm', 'generate',
|
|
@@ -24,6 +28,7 @@ export function runMLX(prompt, opts = {}) {
|
|
|
24
28
|
'--verbose', 'false',
|
|
25
29
|
], { encoding: 'utf8', timeout });
|
|
26
30
|
|
|
31
|
+
process.stderr.write('\r\x1b[K');
|
|
27
32
|
if (result.status !== 0) return null;
|
|
28
33
|
|
|
29
34
|
const output = result.stdout.trim();
|
package/src/completions.mjs
CHANGED
|
@@ -14,22 +14,27 @@ const COMMAND_FLAGS = {
|
|
|
14
14
|
'--checklist-open', '--sort', '--limit', '--all', '--git', '--json',
|
|
15
15
|
'--summarize', '--summarize-limit', '--model'],
|
|
16
16
|
index: ['--write'],
|
|
17
|
-
list: ['--verbose'],
|
|
17
|
+
list: ['--verbose', '--json'],
|
|
18
18
|
coverage: ['--json'],
|
|
19
19
|
new: ['--status', '--title', '--template', '--list-templates', '--root'],
|
|
20
|
-
diff: ['--stat', '--since', '--summarize', '--model'],
|
|
21
|
-
check: ['--errors-only', '--fix'],
|
|
20
|
+
diff: ['--stat', '--since', '--summarize', '--model', '--max-tokens'],
|
|
21
|
+
check: ['--errors-only', '--fix', '--json'],
|
|
22
22
|
stats: ['--json'],
|
|
23
23
|
graph: ['--dot', '--json', '--status', '--module', '--surface'],
|
|
24
24
|
deps: ['--json', '--depth'],
|
|
25
25
|
notion: ['import', 'export', 'sync', '--force', '--dry-run'],
|
|
26
26
|
export: ['--format', '--output', '--status', '--module', '--root'],
|
|
27
|
+
focus: ['--json'],
|
|
28
|
+
status: [],
|
|
29
|
+
archive: [],
|
|
30
|
+
doctor: [],
|
|
31
|
+
watch: [],
|
|
27
32
|
lint: ['--fix'],
|
|
28
33
|
rename: [],
|
|
29
34
|
migrate: [],
|
|
30
35
|
'fix-refs': [],
|
|
31
36
|
summary: ['--model', '--max-tokens', '--json'],
|
|
32
|
-
context: ['--summarize', '--model'],
|
|
37
|
+
context: ['--summarize', '--model', '--json'],
|
|
33
38
|
touch: ['--git'],
|
|
34
39
|
};
|
|
35
40
|
|
package/src/config.mjs
CHANGED
|
@@ -161,7 +161,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
161
161
|
try {
|
|
162
162
|
mod = await import(configUrl);
|
|
163
163
|
} catch (err) {
|
|
164
|
-
die('Failed to load config: ' + configPath + '\n' + err.message + '\
|
|
164
|
+
die('Failed to load config: ' + configPath + '\n' + err.message + '\nCheck for syntax errors in your config file.');
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
configDir = path.dirname(configPath);
|
package/src/diff.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
|
3
3
|
import { asString, toRepoPath, resolveDocPath, die, warn } from './util.mjs';
|
|
4
4
|
import { gitDiffSince } from './git.mjs';
|
|
5
5
|
import { buildIndex } from './index.mjs';
|
|
6
|
-
import { summarizeDiffText } from './ai.mjs';
|
|
6
|
+
import { summarizeDiffText, DEFAULT_MODEL } from './ai.mjs';
|
|
7
7
|
import { bold, dim, green } from './color.mjs';
|
|
8
8
|
|
|
9
9
|
export function runDiff(argv, config) {
|
|
@@ -12,7 +12,7 @@ export function runDiff(argv, config) {
|
|
|
12
12
|
let stat = false;
|
|
13
13
|
let sinceOverride = null;
|
|
14
14
|
let summarize = false;
|
|
15
|
-
let model =
|
|
15
|
+
let model = DEFAULT_MODEL;
|
|
16
16
|
|
|
17
17
|
for (let i = 0; i < argv.length; i++) {
|
|
18
18
|
if (argv[i] === '--stat') { stat = true; continue; }
|
package/src/export.mjs
CHANGED
|
@@ -5,13 +5,14 @@ import { buildIndex } from './index.mjs';
|
|
|
5
5
|
import { buildGraph } from './graph.mjs';
|
|
6
6
|
import { resolveDocPath, toRepoPath, capitalize, die } from './util.mjs';
|
|
7
7
|
|
|
8
|
-
export function runExport(argv, config) {
|
|
8
|
+
export function runExport(argv, config, opts = {}) {
|
|
9
9
|
const positional = [];
|
|
10
10
|
let format = 'md';
|
|
11
11
|
let output = null;
|
|
12
12
|
let statusFilter = null;
|
|
13
13
|
let moduleFilter = null;
|
|
14
14
|
let rootFilter = null;
|
|
15
|
+
const dryRun = opts.dryRun;
|
|
15
16
|
|
|
16
17
|
for (let i = 0; i < argv.length; i++) {
|
|
17
18
|
if (argv[i] === '--format' && argv[i + 1]) { format = argv[++i]; continue; }
|
|
@@ -62,26 +63,42 @@ export function runExport(argv, config) {
|
|
|
62
63
|
// Load bodies
|
|
63
64
|
const docsWithBody = docs.map(d => loadDocWithBody(d, config));
|
|
64
65
|
|
|
66
|
+
const prefix = dryRun ? '[dry-run] ' : '';
|
|
67
|
+
|
|
65
68
|
if (format === 'md') {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
process.stdout.write(`Exported ${docs.length} docs to ${output}\n`);
|
|
69
|
+
if (dryRun) {
|
|
70
|
+
process.stdout.write(`${prefix}Would export ${docs.length} docs as markdown`);
|
|
71
|
+
process.stdout.write(output ? ` to ${output}\n` : ' to stdout\n');
|
|
70
72
|
} else {
|
|
71
|
-
|
|
73
|
+
const content = exportMarkdown(docsWithBody, config);
|
|
74
|
+
if (output) {
|
|
75
|
+
writeFileSync(output, content, 'utf8');
|
|
76
|
+
process.stdout.write(`Exported ${docs.length} docs to ${output}\n`);
|
|
77
|
+
} else {
|
|
78
|
+
process.stdout.write(content);
|
|
79
|
+
}
|
|
72
80
|
}
|
|
73
81
|
} else if (format === 'json') {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
process.stdout.write(`Exported ${docs.length} docs to ${output}\n`);
|
|
82
|
+
if (dryRun) {
|
|
83
|
+
process.stdout.write(`${prefix}Would export ${docs.length} docs as JSON`);
|
|
84
|
+
process.stdout.write(output ? ` to ${output}\n` : ' to stdout\n');
|
|
78
85
|
} else {
|
|
79
|
-
|
|
86
|
+
const content = exportJson(docsWithBody);
|
|
87
|
+
if (output) {
|
|
88
|
+
writeFileSync(output, content, 'utf8');
|
|
89
|
+
process.stdout.write(`Exported ${docs.length} docs to ${output}\n`);
|
|
90
|
+
} else {
|
|
91
|
+
process.stdout.write(content);
|
|
92
|
+
}
|
|
80
93
|
}
|
|
81
94
|
} else if (format === 'html') {
|
|
82
95
|
const outDir = output ?? 'dotmd-export';
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
if (dryRun) {
|
|
97
|
+
process.stdout.write(`${prefix}Would export ${docs.length} docs as HTML to ${outDir}/\n`);
|
|
98
|
+
} else {
|
|
99
|
+
exportHtml(docsWithBody, config, outDir);
|
|
100
|
+
process.stdout.write(`Exported ${docs.length} docs to ${outDir}/\n`);
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
}
|
|
87
104
|
|
package/src/index-file.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { capitalize, escapeTable } from './util.mjs';
|
|
3
4
|
import { formatSnapshot } from './render.mjs';
|
|
4
5
|
|
|
@@ -19,7 +20,7 @@ export function renderIndexFile(index, config) {
|
|
|
19
20
|
|
|
20
21
|
function renderGeneratedBlock(index, config) {
|
|
21
22
|
const lines = [];
|
|
22
|
-
const
|
|
23
|
+
const indexDir = config.indexPath ? path.dirname(path.relative(config.repoRoot, config.indexPath)).split(path.sep).join('/') : '';
|
|
23
24
|
|
|
24
25
|
for (const status of config.statusOrder) {
|
|
25
26
|
const docs = index.docs.filter(doc => doc.status === status);
|
|
@@ -37,7 +38,7 @@ function renderGeneratedBlock(index, config) {
|
|
|
37
38
|
lines.push('|-----|-----------------|');
|
|
38
39
|
for (const doc of docs) {
|
|
39
40
|
const snapshot = formatSnapshot(doc, config);
|
|
40
|
-
const linkPath =
|
|
41
|
+
const linkPath = indexDir ? path.relative(indexDir, doc.path).split(path.sep).join('/') : doc.path;
|
|
41
42
|
lines.push(`| [${escapeTable(doc.title)}](${linkPath}) | ${escapeTable(snapshot)} |`);
|
|
42
43
|
}
|
|
43
44
|
lines.push('');
|
|
@@ -49,7 +50,7 @@ function renderGeneratedBlock(index, config) {
|
|
|
49
50
|
function renderArchivedSection(docs, config, status) {
|
|
50
51
|
const lines = [];
|
|
51
52
|
const limit = config.archivedHighlightLimit;
|
|
52
|
-
const
|
|
53
|
+
const indexDir = config.indexPath ? path.dirname(path.relative(config.repoRoot, config.indexPath)).split(path.sep).join('/') : '';
|
|
53
54
|
const highlights = docs
|
|
54
55
|
.filter(doc => doc.currentState && doc.currentState !== 'No current_state set')
|
|
55
56
|
.sort((a, b) => {
|
|
@@ -66,7 +67,7 @@ function renderArchivedSection(docs, config, status) {
|
|
|
66
67
|
lines.push('| Doc | Status Snapshot |');
|
|
67
68
|
lines.push('|-----|-----------------|');
|
|
68
69
|
for (const doc of highlights) {
|
|
69
|
-
const linkPath =
|
|
70
|
+
const linkPath = indexDir ? path.relative(indexDir, doc.path).split(path.sep).join('/') : doc.path;
|
|
70
71
|
lines.push(`| [${escapeTable(doc.title)}](${linkPath}) | ${escapeTable(formatSnapshot(doc, config))} |`);
|
|
71
72
|
}
|
|
72
73
|
lines.push('');
|
package/src/lifecycle.mjs
CHANGED
|
@@ -6,18 +6,27 @@ import { gitMv, getGitLastModified } 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';
|
|
9
|
+
import { isInteractive, promptChoice } from './prompt.mjs';
|
|
9
10
|
|
|
10
11
|
function findFileRoot(filePath, config) {
|
|
11
12
|
const roots = config.docsRoots || [config.docsRoot];
|
|
12
13
|
return roots.find(r => filePath.startsWith(r)) ?? config.docsRoot;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function runStatus(argv, config, opts = {}) {
|
|
16
|
+
export async function runStatus(argv, config, opts = {}) {
|
|
16
17
|
const { dryRun } = opts;
|
|
17
18
|
const input = argv[0];
|
|
18
|
-
|
|
19
|
+
let newStatus = argv[1];
|
|
19
20
|
|
|
20
|
-
if (!input
|
|
21
|
+
if (!input) { die('Usage: dotmd status <file> <new-status>'); }
|
|
22
|
+
if (!newStatus) {
|
|
23
|
+
if (isInteractive()) {
|
|
24
|
+
newStatus = await promptChoice('Which status?', config.statusOrder);
|
|
25
|
+
if (!newStatus) die('No status selected.');
|
|
26
|
+
} else {
|
|
27
|
+
die('Usage: dotmd status <file> <new-status>');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
21
30
|
if (!config.validStatuses.has(newStatus)) { die(`Invalid status: ${newStatus}\nValid: ${[...config.validStatuses].join(', ')}`); }
|
|
22
31
|
|
|
23
32
|
const filePath = resolveDocPath(input, config);
|
package/src/lint.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { extractFrontmatter, parseSimpleFrontmatter, replaceFrontmatter } from '
|
|
|
3
3
|
import { asString, toRepoPath, escapeRegex, warn } from './util.mjs';
|
|
4
4
|
import { buildIndex, collectDocFiles } from './index.mjs';
|
|
5
5
|
import { updateFrontmatter } from './lifecycle.mjs';
|
|
6
|
+
import { runMLX, checkUvAvailable } from './ai.mjs';
|
|
6
7
|
import { bold, green, yellow, dim } from './color.mjs';
|
|
7
8
|
|
|
8
9
|
const KEY_RENAMES = {
|
|
@@ -28,6 +29,11 @@ export function runLint(argv, config, opts = {}) {
|
|
|
28
29
|
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
29
30
|
const fixes = [];
|
|
30
31
|
|
|
32
|
+
// Missing status (fixable via AI inference)
|
|
33
|
+
if (!asString(parsed.status)) {
|
|
34
|
+
fixes.push({ field: 'status', oldValue: null, newValue: null, type: 'infer-status' });
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
// Missing updated
|
|
32
38
|
if (!asString(parsed.updated) && asString(parsed.status) && !config.lifecycle.skipWarningsFor.has(asString(parsed.status))) {
|
|
33
39
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -85,6 +91,8 @@ export function runLint(argv, config, opts = {}) {
|
|
|
85
91
|
for (const f of fixes) {
|
|
86
92
|
if (f.type === 'rename-key') {
|
|
87
93
|
process.stdout.write(dim(` ${f.oldValue} → ${f.newValue}\n`));
|
|
94
|
+
} else if (f.type === 'infer-status') {
|
|
95
|
+
process.stdout.write(dim(` missing status (fixable via AI)\n`));
|
|
88
96
|
} else if (f.type === 'split-to-array') {
|
|
89
97
|
process.stdout.write(dim(` ${f.field}: "${f.oldValue}" → surfaces: [${f.newValue.join(', ')}]\n`));
|
|
90
98
|
} else if (f.type === 'eof') {
|
|
@@ -138,6 +146,20 @@ export function runLint(argv, config, opts = {}) {
|
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
if (!dryRun) {
|
|
149
|
+
// Apply infer-status fixes via AI
|
|
150
|
+
for (const f of fixes.filter(f => f.type === 'infer-status')) {
|
|
151
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
152
|
+
const { body } = extractFrontmatter(raw);
|
|
153
|
+
const statusList = config.statusOrder.join(', ');
|
|
154
|
+
const prompt = `Given this markdown document, classify it into exactly one of these statuses: ${statusList}.\nReply with ONLY the status word, nothing else.\n\nFile: ${repoPath}\n\n${(body ?? '').slice(0, 4000)}`;
|
|
155
|
+
const result = runMLX(prompt, { maxTokens: 10 });
|
|
156
|
+
const suggested = result?.trim().toLowerCase().split(/\s+/)[0];
|
|
157
|
+
if (suggested && config.validStatuses.has(suggested)) {
|
|
158
|
+
updateFrontmatter(filePath, { status: suggested });
|
|
159
|
+
f.newValue = suggested;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
// Apply split-to-array fixes (surface: a, b → surfaces: array)
|
|
142
164
|
for (const sa of splitToArray) {
|
|
143
165
|
let raw = readFileSync(filePath, 'utf8');
|
|
@@ -199,6 +221,12 @@ export function runLint(argv, config, opts = {}) {
|
|
|
199
221
|
process.stdout.write(`${prefix} ${dim(`${f.oldValue} → ${f.newValue}`)}\n`);
|
|
200
222
|
} else if (f.type === 'eof') {
|
|
201
223
|
process.stdout.write(`${prefix} ${dim('added newline at EOF')}\n`);
|
|
224
|
+
} else if (f.type === 'infer-status') {
|
|
225
|
+
if (f.newValue) {
|
|
226
|
+
process.stdout.write(`${prefix} ${dim(`status: (missing) → ${f.newValue} (AI-inferred)`)}\n`);
|
|
227
|
+
} else {
|
|
228
|
+
process.stdout.write(`${prefix} ${dim('status: (missing) — AI inference unavailable')}\n`);
|
|
229
|
+
}
|
|
202
230
|
} else if (f.type === 'split-to-array') {
|
|
203
231
|
process.stdout.write(`${prefix} ${dim(`${f.field}: "${f.oldValue}" → surfaces: [${f.newValue.join(', ')}]`)}\n`);
|
|
204
232
|
} else if (f.type === 'add') {
|
package/src/new.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { toRepoPath, die, warn } from './util.mjs';
|
|
4
4
|
import { green, dim, bold } from './color.mjs';
|
|
5
|
+
import { isInteractive, promptText } from './prompt.mjs';
|
|
5
6
|
|
|
6
7
|
const BUILTIN_TEMPLATES = {
|
|
7
8
|
default: {
|
|
@@ -36,7 +37,7 @@ const BUILTIN_TEMPLATES = {
|
|
|
36
37
|
},
|
|
37
38
|
};
|
|
38
39
|
|
|
39
|
-
export function runNew(argv, config, opts = {}) {
|
|
40
|
+
export async function runNew(argv, config, opts = {}) {
|
|
40
41
|
const { dryRun } = opts;
|
|
41
42
|
|
|
42
43
|
// Parse args
|
|
@@ -50,6 +51,7 @@ export function runNew(argv, config, opts = {}) {
|
|
|
50
51
|
if (argv[i] === '--title' && argv[i + 1]) { title = argv[++i]; continue; }
|
|
51
52
|
if (argv[i] === '--template' && argv[i + 1]) { templateName = argv[++i]; continue; }
|
|
52
53
|
if (argv[i] === '--root' && argv[i + 1]) { rootName = argv[++i]; continue; }
|
|
54
|
+
if (argv[i] === '--config') { i++; continue; }
|
|
53
55
|
if (argv[i] === '--list-templates') {
|
|
54
56
|
listTemplates(config);
|
|
55
57
|
return;
|
|
@@ -57,8 +59,15 @@ export function runNew(argv, config, opts = {}) {
|
|
|
57
59
|
if (!argv[i].startsWith('-')) positional.push(argv[i]);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
if (!name) {
|
|
62
|
+
let name = positional[0];
|
|
63
|
+
if (!name) {
|
|
64
|
+
if (isInteractive()) {
|
|
65
|
+
name = await promptText('Document name: ');
|
|
66
|
+
if (!name) die('No name provided.');
|
|
67
|
+
} else {
|
|
68
|
+
die('Usage: dotmd new <name> [--template <t>] [--status <s>] [--title <t>]\n dotmd new --list-templates');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
62
71
|
|
|
63
72
|
// Validate status
|
|
64
73
|
if (!config.validStatuses.has(status)) {
|
package/src/prompt.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
|
|
3
|
+
export function isInteractive() {
|
|
4
|
+
return Boolean(process.stdin.isTTY);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function promptText(question) {
|
|
8
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
9
|
+
return new Promise(resolve => {
|
|
10
|
+
rl.question(question, answer => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer.trim());
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function promptChoice(question, options) {
|
|
18
|
+
process.stderr.write(question + '\n');
|
|
19
|
+
options.forEach((opt, i) => process.stderr.write(` ${i + 1}) ${opt}\n`));
|
|
20
|
+
const answer = await promptText('> ');
|
|
21
|
+
const idx = parseInt(answer, 10) - 1;
|
|
22
|
+
if (idx >= 0 && idx < options.length) return options[idx];
|
|
23
|
+
const match = options.find(o => o.toLowerCase() === answer.toLowerCase());
|
|
24
|
+
return match ?? null;
|
|
25
|
+
}
|
package/src/query.mjs
CHANGED
|
@@ -9,9 +9,14 @@ import { summarizeDocBody } from './ai.mjs';
|
|
|
9
9
|
import { dim } from './color.mjs';
|
|
10
10
|
|
|
11
11
|
export function runFocus(index, argv, config) {
|
|
12
|
-
const statusFilter = argv
|
|
12
|
+
const statusFilter = argv.find(a => !a.startsWith('-')) ?? 'active';
|
|
13
13
|
const docs = index.docs.filter(doc => doc.status === statusFilter);
|
|
14
14
|
|
|
15
|
+
if (argv.includes('--json')) {
|
|
16
|
+
process.stdout.write(JSON.stringify({ status: statusFilter, count: docs.length, docs }, null, 2) + '\n');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
if (docs.length === 0) {
|
|
16
21
|
process.stdout.write(`No docs found for status: ${statusFilter}\n`);
|
|
17
22
|
return;
|
|
@@ -139,7 +144,7 @@ function getDocSummary(doc, config) {
|
|
|
139
144
|
const meta = { title: doc.title, status: doc.status, path: doc.path };
|
|
140
145
|
return config.hooks.summarizeDoc
|
|
141
146
|
? config.hooks.summarizeDoc(body, meta)
|
|
142
|
-
: summarizeDocBody(body, meta
|
|
147
|
+
: summarizeDocBody(body, meta);
|
|
143
148
|
} catch { return null; }
|
|
144
149
|
}
|
|
145
150
|
|
package/src/rename.mjs
CHANGED
|
@@ -5,8 +5,9 @@ import { toRepoPath, resolveDocPath, die, warn } from './util.mjs';
|
|
|
5
5
|
import { collectDocFiles } from './index.mjs';
|
|
6
6
|
import { gitMv } from './git.mjs';
|
|
7
7
|
import { green, dim, yellow } from './color.mjs';
|
|
8
|
+
import { isInteractive, promptText } from './prompt.mjs';
|
|
8
9
|
|
|
9
|
-
export function runRename(argv, config, opts = {}) {
|
|
10
|
+
export async function runRename(argv, config, opts = {}) {
|
|
10
11
|
const { dryRun } = opts;
|
|
11
12
|
|
|
12
13
|
// Parse positional args (skip flags)
|
|
@@ -17,10 +18,16 @@ export function runRename(argv, config, opts = {}) {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const oldInput = positional[0];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!oldInput
|
|
23
|
-
|
|
21
|
+
let newInput = positional[1];
|
|
22
|
+
|
|
23
|
+
if (!oldInput) { die('Usage: dotmd rename <old> <new>'); }
|
|
24
|
+
if (!newInput) {
|
|
25
|
+
if (isInteractive()) {
|
|
26
|
+
newInput = await promptText('New name: ');
|
|
27
|
+
if (!newInput) die('No name provided.');
|
|
28
|
+
} else {
|
|
29
|
+
die('Usage: dotmd rename <old> <new>');
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
// Resolve old path
|
package/src/util.mjs
CHANGED
|
@@ -70,6 +70,21 @@ export function die(message) {
|
|
|
70
70
|
throw new DotmdError(message);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export function levenshtein(a, b) {
|
|
74
|
+
if (a.length === 0) return b.length;
|
|
75
|
+
if (b.length === 0) return a.length;
|
|
76
|
+
const matrix = [];
|
|
77
|
+
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
78
|
+
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
79
|
+
for (let i = 1; i <= b.length; i++) {
|
|
80
|
+
for (let j = 1; j <= a.length; j++) {
|
|
81
|
+
const cost = b[i - 1] === a[j - 1] ? 0 : 1;
|
|
82
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return matrix[b.length][a.length];
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
export function resolveDocPath(input, config) {
|
|
74
89
|
if (!input) return null;
|
|
75
90
|
if (path.isAbsolute(input)) return existsSync(input) ? input : null;
|