dotmd-cli 0.68.0 → 0.70.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/README.md +144 -964
- package/bin/dotmd.mjs +241 -197
- package/dotmd.config.example.mjs +5 -8
- package/package.json +6 -10
- package/src/agent-context.mjs +132 -0
- package/src/atomic-mutation.mjs +1505 -0
- package/src/baton.mjs +109 -87
- package/src/bulk-tag.mjs +7 -7
- package/src/commands.mjs +326 -12
- package/src/completions.mjs +38 -98
- package/src/config.mjs +18 -3
- package/src/diff.mjs +7 -3
- package/src/doctor.mjs +12 -5
- package/src/export.mjs +154 -25
- package/src/fix-refs.mjs +2 -0
- package/src/frontmatter-fix.mjs +2 -0
- package/src/frontmatter.mjs +3 -2
- package/src/git.mjs +531 -14
- package/src/graph.mjs +53 -25
- package/src/guard.mjs +163 -60
- package/src/hud.mjs +65 -76
- package/src/index-file.mjs +28 -16
- package/src/index.mjs +17 -12
- package/src/init.mjs +1 -1
- package/src/journal.mjs +145 -12
- package/src/lifecycle.mjs +554 -282
- package/src/lint.mjs +57 -9
- package/src/managed-path.mjs +192 -0
- package/src/migrate-prompts.mjs +2 -0
- package/src/migrate-template.mjs +2 -0
- package/src/migrate.mjs +7 -1
- package/src/new.mjs +135 -54
- package/src/output-identity.mjs +106 -0
- package/src/pickup-card.mjs +24 -10
- package/src/pickup.mjs +457 -0
- package/src/prompts.mjs +138 -32
- package/src/query.mjs +22 -10
- package/src/reference-planner.mjs +292 -0
- package/src/rename.mjs +65 -73
- package/src/render.mjs +17 -8
- package/src/runlist.mjs +109 -71
- package/src/section.mjs +2 -1
- package/src/ship.mjs +39 -20
- package/src/stats.mjs +1 -1
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +11 -26
- package/src/summary.mjs +14 -3
- package/src/update.mjs +38 -10
- package/src/use.mjs +4 -1
- package/src/util.mjs +1 -0
- package/src/validate.mjs +14 -6
- package/src/watch.mjs +6 -1
- package/src/notion.mjs +0 -528
package/src/rename.mjs
CHANGED
|
@@ -1,107 +1,99 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { toRepoPath, die, warn } from './util.mjs';
|
|
4
4
|
import { collectDocFiles, resolveDocArg } from './index.mjs';
|
|
5
5
|
import { regenIndex } from './lifecycle.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { captureGitIndexGeneration, isTracked } from './git.mjs';
|
|
7
7
|
import { green, dim } from './color.mjs';
|
|
8
8
|
import { isInteractive, promptText } from './prompt.mjs';
|
|
9
|
+
import { authorizeManagedDestination, authorizeManagedSource, authorizeManagedSweep } from './managed-path.mjs';
|
|
10
|
+
import { availableSessionId, prepareOwnershipMigration } from './pickup.mjs';
|
|
11
|
+
import { moveFileAtomic } from './atomic-mutation.mjs';
|
|
12
|
+
import { configuredReferenceFields, createReferenceIdentitySet, planReferenceMove, rewriteDocumentReferences } from './reference-planner.mjs';
|
|
9
13
|
|
|
10
14
|
export async function runRename(argv, config, opts = {}) {
|
|
11
15
|
const { dryRun } = opts;
|
|
12
|
-
|
|
13
|
-
// Parse positional args (skip flags)
|
|
14
|
-
const positional = [];
|
|
15
|
-
for (let i = 0; i < argv.length; i++) {
|
|
16
|
-
if (argv[i].startsWith('-')) continue;
|
|
17
|
-
positional.push(argv[i]);
|
|
18
|
-
}
|
|
19
|
-
|
|
16
|
+
const positional = argv.filter(arg => !arg.startsWith('-'));
|
|
20
17
|
const oldInput = positional[0];
|
|
21
18
|
let newInput = positional[1];
|
|
22
|
-
|
|
23
|
-
if (!oldInput) { die('Usage: dotmd rename <old> <new>'); }
|
|
19
|
+
if (!oldInput) die('Usage: dotmd rename <old> <new>');
|
|
24
20
|
if (!newInput) {
|
|
25
|
-
if (isInteractive())
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} else {
|
|
29
|
-
die('Usage: dotmd rename <old> <new>');
|
|
30
|
-
}
|
|
21
|
+
if (!isInteractive()) die('Usage: dotmd rename <old> <new>');
|
|
22
|
+
newInput = await promptText('New name: ');
|
|
23
|
+
if (!newInput) die('No name provided.');
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
// Compute new path — cross-directory if input has slashes, same directory otherwise
|
|
26
|
+
let oldPath = resolveDocArg(oldInput, config);
|
|
27
|
+
const sourceAuthorization = authorizeManagedSource(oldPath, config, { kind: 'Rename source' });
|
|
28
|
+
oldPath = sourceAuthorization.path;
|
|
37
29
|
let newPath;
|
|
38
30
|
if (newInput.includes('/') || newInput.includes(path.sep)) {
|
|
39
|
-
|
|
40
|
-
if (!resolved.endsWith('.md')) resolved += '.md';
|
|
41
|
-
newPath = path.resolve(config.repoRoot, resolved);
|
|
31
|
+
newPath = path.resolve(config.repoRoot, newInput.endsWith('.md') ? newInput : `${newInput}.md`);
|
|
42
32
|
} else {
|
|
43
|
-
|
|
44
|
-
if (!newBasename.endsWith('.md')) newBasename += '.md';
|
|
45
|
-
newPath = path.join(path.dirname(oldPath), newBasename);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (existsSync(newPath)) {
|
|
49
|
-
die(`Target already exists: ${toRepoPath(newPath, config.repoRoot)}`);
|
|
50
|
-
return;
|
|
33
|
+
newPath = path.join(path.dirname(oldPath), newInput.endsWith('.md') ? newInput : `${newInput}.md`);
|
|
51
34
|
}
|
|
35
|
+
if (existsSync(newPath)) die(`Target already exists: ${toRepoPath(newPath, config.repoRoot)}`);
|
|
36
|
+
newPath = authorizeManagedDestination(newPath, config, { root: sourceAuthorization.root, kind: 'Rename destination' }).path;
|
|
52
37
|
|
|
53
38
|
const oldRepoPath = toRepoPath(oldPath, config.repoRoot);
|
|
54
39
|
const newRepoPath = toRepoPath(newPath, config.repoRoot);
|
|
55
|
-
const oldBasename = path.basename(oldPath);
|
|
56
|
-
const newBasename = path.basename(newPath);
|
|
57
|
-
|
|
58
|
-
// Scan for references in other docs
|
|
59
40
|
const allFiles = collectDocFiles(config);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
filesToUpdate.push(filePath);
|
|
67
|
-
}
|
|
41
|
+
authorizeManagedSweep(allFiles, config, { kind: 'Rename reference rewrite source' });
|
|
42
|
+
const documents = allFiles.map(filePath => ({ path: filePath, content: readFileSync(filePath, 'utf8') }));
|
|
43
|
+
let sourceDocument = documents.find(document => document.path === oldPath);
|
|
44
|
+
if (!sourceDocument) {
|
|
45
|
+
sourceDocument = { path: oldPath, content: readFileSync(oldPath, 'utf8') };
|
|
46
|
+
documents.push(sourceDocument);
|
|
68
47
|
}
|
|
48
|
+
const sourceContent = sourceDocument.content;
|
|
49
|
+
const referenceFields = configuredReferenceFields(config);
|
|
50
|
+
const referencePlan = planReferenceMove({ documents, oldPath, newPath, repoRoot: config.repoRoot, referenceFields });
|
|
51
|
+
const ownership = prepareOwnershipMigration(oldRepoPath, newPath, config, { sessionId: availableSessionId() });
|
|
69
52
|
|
|
70
53
|
if (dryRun) {
|
|
71
54
|
const prefix = dim('[dry-run]');
|
|
72
55
|
process.stdout.write(`${prefix} Would rename: ${oldRepoPath} → ${newRepoPath}\n`);
|
|
73
|
-
if (
|
|
74
|
-
process.stdout.write(`${prefix} Would update references in ${
|
|
75
|
-
for (const
|
|
76
|
-
process.stdout.write(`${prefix} ${toRepoPath(f, config.repoRoot)}\n`);
|
|
77
|
-
}
|
|
56
|
+
if (referencePlan.updates.length) {
|
|
57
|
+
process.stdout.write(`${prefix} Would update references in ${referencePlan.updates.length} file(s):\n`);
|
|
58
|
+
for (const item of referencePlan.updates) process.stdout.write(`${prefix} ${toRepoPath(item.path, config.repoRoot)}\n`);
|
|
78
59
|
}
|
|
60
|
+
if (ownership) process.stdout.write(`${prefix} Would migrate this session's ownership record.\n`);
|
|
79
61
|
return;
|
|
80
62
|
}
|
|
81
63
|
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
64
|
+
const tracked = isTracked(oldPath, config.repoRoot);
|
|
65
|
+
const gitIndex = tracked ? captureGitIndexGeneration(config.repoRoot) : null;
|
|
66
|
+
const identities = createReferenceIdentitySet(allFiles);
|
|
67
|
+
const result = moveFileAtomic(oldPath, newPath, current => rewriteDocumentReferences(current, {
|
|
68
|
+
sourcePath: oldPath,
|
|
69
|
+
outputPath: newPath,
|
|
70
|
+
repoRoot: config.repoRoot,
|
|
71
|
+
identities,
|
|
72
|
+
oldPath,
|
|
73
|
+
newPath,
|
|
74
|
+
referenceFields,
|
|
75
|
+
rebaseAll: true,
|
|
76
|
+
}), {
|
|
77
|
+
repoRoot: config.repoRoot,
|
|
78
|
+
config,
|
|
79
|
+
updates: allFiles.filter(filePath => filePath !== oldPath).map(filePath => ({
|
|
80
|
+
path: filePath,
|
|
81
|
+
render: current => rewriteDocumentReferences(current, {
|
|
82
|
+
sourcePath: filePath, repoRoot: config.repoRoot, identities, oldPath, newPath, referenceFields,
|
|
83
|
+
}),
|
|
84
|
+
})),
|
|
85
|
+
creations: ownership ? [{ path: ownership.newRecordPath, content: ownership.newContent, label: 'ownership' }] : [],
|
|
86
|
+
deletions: ownership ? [{ path: ownership.oldRecordPath, expectedContent: ownership.oldContent, label: 'ownership' }] : [],
|
|
87
|
+
gitMove: tracked,
|
|
88
|
+
gitIndex,
|
|
89
|
+
operation: 'rename',
|
|
90
|
+
sessionId: availableSessionId(),
|
|
91
|
+
testHooks: opts.testHooks,
|
|
92
|
+
});
|
|
99
93
|
regenIndex(config);
|
|
100
|
-
|
|
101
94
|
process.stdout.write(`${green('Renamed')}: ${oldRepoPath} → ${newRepoPath}\n`);
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
try { config.hooks.onRename?.({ oldPath: oldRepoPath, newPath: newRepoPath, referencesUpdated: updatedCount }); } catch (err) { warn(`Hook 'onRename' threw: ${err.message}`); }
|
|
95
|
+
if (result.updatedPaths.length) process.stdout.write(`Updated references in ${result.updatedPaths.length} file(s).\n`);
|
|
96
|
+
try { config.hooks.onRename?.({ oldPath: oldRepoPath, newPath: newRepoPath, referencesUpdated: result.updatedPaths.length }); }
|
|
97
|
+
catch (err) { warn(`Hook 'onRename' threw: ${err.message}`); }
|
|
98
|
+
return { oldRepoPath, newRepoPath, referencePaths: result.updatedPaths.map(item => toRepoPath(item, config.repoRoot)) };
|
|
107
99
|
}
|
package/src/render.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { summarizeDocBody } from './ai.mjs';
|
|
|
6
6
|
import { bold, red, yellow, green, dim } from './color.mjs';
|
|
7
7
|
import { categorizeWarnings } from './check-collapse.mjs';
|
|
8
8
|
import { buildCoordinationIndex, isRoadmapHub } from './runlist.mjs';
|
|
9
|
+
import { resolveStatusMetadata, statusMetadataFor } from './status-metadata.mjs';
|
|
9
10
|
|
|
10
11
|
// Render `currentState` with an `(auto)` prefix when the value was body-scraped
|
|
11
12
|
// rather than read from frontmatter. Lets a reader see at a glance which docs
|
|
@@ -20,7 +21,7 @@ export function formatCurrentState(doc) {
|
|
|
20
21
|
|
|
21
22
|
export function renderCompactList(index, config) {
|
|
22
23
|
const defaultRenderer = (idx) => _renderCompactList(idx, config);
|
|
23
|
-
if (config.hooks.renderCompactList) {
|
|
24
|
+
if (!config._execution?.suppressSideEffects && config.hooks.renderCompactList) {
|
|
24
25
|
try { return config.hooks.renderCompactList(index, defaultRenderer); }
|
|
25
26
|
catch (err) { warn(`Hook 'renderCompactList' threw: ${err.message}`); }
|
|
26
27
|
}
|
|
@@ -162,7 +163,7 @@ export function renderVerboseList(index, config) {
|
|
|
162
163
|
|
|
163
164
|
export function renderContext(index, config, opts = {}) {
|
|
164
165
|
const defaultRenderer = (idx) => _renderContext(idx, config, opts);
|
|
165
|
-
if (config.hooks.renderContext) {
|
|
166
|
+
if (!config._execution?.suppressSideEffects && config.hooks.renderContext) {
|
|
166
167
|
try { return config.hooks.renderContext(index, defaultRenderer); }
|
|
167
168
|
catch (err) { warn(`Hook 'renderContext' threw: ${err.message}`); }
|
|
168
169
|
}
|
|
@@ -190,7 +191,7 @@ function _renderContextSection(docs, ctx, opts, config, lines) {
|
|
|
190
191
|
? truncate(doc.nextStep, ctx.truncateNextStep || 80)
|
|
191
192
|
: '(no next step)';
|
|
192
193
|
lines.push(` ${slug}${ageTag} next: ${next}`);
|
|
193
|
-
if (opts.summarize) {
|
|
194
|
+
if (opts.summarize && !config._execution?.suppressSideEffects) {
|
|
194
195
|
try {
|
|
195
196
|
const absPath = path.resolve(config.repoRoot, doc.path);
|
|
196
197
|
const raw = readFileSync(absPath, 'utf8');
|
|
@@ -225,6 +226,9 @@ function _renderContextSection(docs, ctx, opts, config, lines) {
|
|
|
225
226
|
function _renderContext(index, config, opts = {}) {
|
|
226
227
|
const today = new Date().toISOString().slice(0, 10);
|
|
227
228
|
const lines = [`BRIEFING (${today})`, ''];
|
|
229
|
+
if (opts.summarize && config._execution?.suppressSideEffects) {
|
|
230
|
+
lines.push(dim('[preview] AI summaries skipped; models and custom summarizeDoc hooks are not invoked.'), '');
|
|
231
|
+
}
|
|
228
232
|
|
|
229
233
|
// Group docs by type
|
|
230
234
|
const typeOrder = ['plan', 'doc', 'research'];
|
|
@@ -352,11 +356,16 @@ export function renderBriefing(index, config) {
|
|
|
352
356
|
// headline plan count entirely (they get their own `N runlists · dotmd
|
|
353
357
|
// runlists` pointer line below). The breakdown is leaf statuses only and sums
|
|
354
358
|
// back to the leaf count, so "N live plans" means N things to actually work on.
|
|
355
|
-
const
|
|
359
|
+
const planStatusOrder = (resolveStatusMetadata(config).byType.plan ?? []).map(item => item.name);
|
|
360
|
+
const orderedStatuses = [
|
|
361
|
+
...planStatusOrder.filter(status => bySt[status]),
|
|
362
|
+
...Object.keys(bySt).filter(status => !planStatusOrder.includes(status)).sort(),
|
|
363
|
+
];
|
|
364
|
+
const counts = orderedStatuses.map(status => `${bySt[status]} ${status}`).join(', ');
|
|
356
365
|
const closedCount = plans.length - live.length;
|
|
357
366
|
const closedPart = closedCount ? ` (${closedCount} archived)` : '';
|
|
358
367
|
lines.push(liveLeaves ? `${liveLeaves} live plans${closedPart}: ${counts}` : `0 live plans${closedPart}`);
|
|
359
|
-
const show = plans.filter(p => (
|
|
368
|
+
const show = plans.filter(p => statusMetadataFor(config, 'plan', p.status)?.context === 'expanded' && !isHub(p));
|
|
360
369
|
for (const p of show) {
|
|
361
370
|
const next = p.nextStep ? `next: ${p.nextStep}` : '(no next step)';
|
|
362
371
|
lines.push(` > ${path.basename(p.path, '.md')} (${p.status}) ${next}`);
|
|
@@ -382,7 +391,7 @@ export function renderBriefing(index, config) {
|
|
|
382
391
|
if (untyped.length) parts.push(`${untyped.length} untyped`);
|
|
383
392
|
if (parts.length) lines.push(parts.join(' | '));
|
|
384
393
|
|
|
385
|
-
const stale = index.docs.filter(d => d.isStale && !config.
|
|
394
|
+
const stale = index.docs.filter(d => d.isStale && !statusMetadataFor(config, d.type, d.status)?.skipStale).length;
|
|
386
395
|
// Append a hint when errors are present — otherwise the user sees `Errors: 1`
|
|
387
396
|
// with no clue what or where. `dotmd check` is the canonical detail view.
|
|
388
397
|
const errorCount = index.errors.length;
|
|
@@ -396,7 +405,7 @@ export function renderBriefing(index, config) {
|
|
|
396
405
|
|
|
397
406
|
export function renderCheck(index, config, opts = {}) {
|
|
398
407
|
const defaultRenderer = (idx) => _renderCheck(idx, opts);
|
|
399
|
-
if (config.hooks.renderCheck) {
|
|
408
|
+
if (!config._execution?.suppressSideEffects && config.hooks.renderCheck) {
|
|
400
409
|
try { return config.hooks.renderCheck(index, defaultRenderer); }
|
|
401
410
|
catch (err) { warn(`Hook 'renderCheck' threw: ${err.message}`); }
|
|
402
411
|
}
|
|
@@ -586,7 +595,7 @@ export function renderProgressBar(checklist) {
|
|
|
586
595
|
|
|
587
596
|
export function formatSnapshot(doc, config) {
|
|
588
597
|
const defaultFormatter = (d) => _formatSnapshot(d, config);
|
|
589
|
-
if (config.hooks.formatSnapshot) {
|
|
598
|
+
if (!config._execution?.suppressSideEffects && config.hooks.formatSnapshot) {
|
|
590
599
|
try { return config.hooks.formatSnapshot(doc, defaultFormatter); }
|
|
591
600
|
catch (err) { warn(`Hook 'formatSnapshot' threw: ${err.message}`); }
|
|
592
601
|
}
|