dotmd-cli 0.7.0 → 0.8.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 +121 -42
- package/bin/dotmd.mjs +99 -3
- package/package.json +15 -5
- package/src/ai.mjs +50 -0
- package/src/completions.mjs +12 -5
- package/src/config.mjs +10 -3
- package/src/deps.mjs +249 -0
- package/src/diff.mjs +2 -28
- package/src/export.mjs +344 -0
- package/src/fix-refs.mjs +102 -52
- package/src/index.mjs +15 -4
- package/src/init.mjs +88 -4
- package/src/lifecycle.mjs +11 -4
- package/src/new.mjs +15 -1
- package/src/notion.mjs +528 -0
- package/src/query.mjs +36 -4
- package/src/render.mjs +22 -4
- package/src/stats.mjs +161 -0
- package/src/summary.mjs +63 -0
- package/src/util.mjs +5 -2
- package/src/watch.mjs +12 -9
package/src/fix-refs.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, replaceFrontmatter } from './frontmatter.mjs';
|
|
4
|
-
import { toRepoPath, warn } from './util.mjs';
|
|
4
|
+
import { toRepoPath, escapeRegex, warn } from './util.mjs';
|
|
5
5
|
import { buildIndex, collectDocFiles } from './index.mjs';
|
|
6
6
|
import { green, dim, yellow } from './color.mjs';
|
|
7
7
|
|
|
8
8
|
export function runFixRefs(argv, config, opts = {}) {
|
|
9
9
|
const { dryRun } = opts;
|
|
10
10
|
const result = fixBrokenRefs(config, { dryRun });
|
|
11
|
-
if (result.totalFixed === 0 && result.unfixableCount === 0) {
|
|
11
|
+
if (result.totalFixed === 0 && result.bodyLinksFixed === 0 && result.unfixableCount === 0) {
|
|
12
12
|
process.stdout.write(green('No broken references found.') + '\n');
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -34,80 +34,130 @@ export function fixBrokenRefs(config, opts = {}) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
const prefix = dryRun ? dim('[dry-run] ') : '';
|
|
38
|
+
let totalFixed = 0;
|
|
39
|
+
let unfixableCount = 0;
|
|
40
|
+
let bodyLinksFixed = 0;
|
|
41
|
+
|
|
42
|
+
// ── Fix broken frontmatter references ────────────────────────────────
|
|
43
|
+
|
|
38
44
|
const brokenRefErrors = index.errors.filter(e =>
|
|
39
45
|
e.message.includes('does not resolve to an existing file')
|
|
40
46
|
);
|
|
41
47
|
|
|
42
|
-
if (brokenRefErrors.length
|
|
43
|
-
|
|
44
|
-
}
|
|
48
|
+
if (brokenRefErrors.length > 0) {
|
|
49
|
+
const fixesByDoc = new Map();
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
for (const err of brokenRefErrors) {
|
|
52
|
+
const match = err.message.match(/entry `([^`]+)` does not resolve/);
|
|
53
|
+
if (!match) { unfixableCount++; continue; }
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!match) { unfixableCount++; continue; }
|
|
55
|
+
const brokenRef = match[1];
|
|
56
|
+
const brokenBasename = path.basename(brokenRef);
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
const brokenBasename = path.basename(brokenRef);
|
|
58
|
+
if (duplicateBasenames.has(brokenBasename)) { unfixableCount++; continue; }
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
unfixableCount++;
|
|
59
|
-
|
|
60
|
+
const resolved = basenameMap.get(brokenBasename);
|
|
61
|
+
if (!resolved) { unfixableCount++; continue; }
|
|
62
|
+
|
|
63
|
+
const docAbsPath = path.join(config.repoRoot, err.path);
|
|
64
|
+
const docDir = path.dirname(docAbsPath);
|
|
65
|
+
const correctRelPath = path.relative(docDir, resolved).split(path.sep).join('/');
|
|
66
|
+
|
|
67
|
+
if (correctRelPath === brokenRef) { unfixableCount++; continue; }
|
|
68
|
+
|
|
69
|
+
if (!fixesByDoc.has(err.path)) fixesByDoc.set(err.path, []);
|
|
70
|
+
fixesByDoc.get(err.path).push({ brokenRef, correctRelPath });
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
const
|
|
63
|
-
|
|
73
|
+
for (const [docPath, fixes] of fixesByDoc) {
|
|
74
|
+
const absPath = path.join(config.repoRoot, docPath);
|
|
75
|
+
let raw = readFileSync(absPath, 'utf8');
|
|
76
|
+
const { frontmatter: fm } = extractFrontmatter(raw);
|
|
77
|
+
if (!fm) continue;
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
79
|
+
let newFm = fm;
|
|
80
|
+
for (const { brokenRef, correctRelPath } of fixes) {
|
|
81
|
+
newFm = newFm.split(brokenRef).join(correctRelPath);
|
|
82
|
+
}
|
|
68
83
|
|
|
69
|
-
|
|
84
|
+
if (newFm !== fm && !dryRun) {
|
|
85
|
+
raw = replaceFrontmatter(raw, newFm);
|
|
86
|
+
writeFileSync(absPath, raw, 'utf8');
|
|
87
|
+
}
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
if (!quiet) {
|
|
90
|
+
process.stdout.write(`${prefix}${green('Fixed')}: ${docPath} (${fixes.length} ref${fixes.length > 1 ? 's' : ''})\n`);
|
|
91
|
+
for (const { brokenRef, correctRelPath } of fixes) {
|
|
92
|
+
process.stdout.write(`${prefix} ${dim(`${brokenRef} → ${correctRelPath}`)}\n`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
totalFixed += fixes.length;
|
|
73
96
|
}
|
|
74
|
-
fixesByDoc.get(err.path).push({ brokenRef, correctRelPath });
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
let totalFixed = 0;
|
|
79
|
-
|
|
80
|
-
for (const [docPath, fixes] of fixesByDoc) {
|
|
81
|
-
const absPath = path.join(config.repoRoot, docPath);
|
|
82
|
-
let raw = readFileSync(absPath, 'utf8');
|
|
83
|
-
const { frontmatter: fm } = extractFrontmatter(raw);
|
|
84
|
-
if (!fm) continue;
|
|
99
|
+
// ── Fix broken body links ────────────────────────────────────────────
|
|
85
100
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
for (const doc of index.docs) {
|
|
102
|
+
const brokenBodyWarnings = doc.warnings.filter(w =>
|
|
103
|
+
w.message.startsWith('body link') && w.message.includes('does not resolve')
|
|
104
|
+
);
|
|
105
|
+
if (!brokenBodyWarnings.length) continue;
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
const absPath = path.join(config.repoRoot, doc.path);
|
|
108
|
+
let raw = readFileSync(absPath, 'utf8');
|
|
109
|
+
const { frontmatter: fm, body } = extractFrontmatter(raw);
|
|
110
|
+
let newBody = body;
|
|
111
|
+
const docDir = path.dirname(absPath);
|
|
112
|
+
const bodyFixes = [];
|
|
113
|
+
|
|
114
|
+
for (const warn of brokenBodyWarnings) {
|
|
115
|
+
const match = warn.message.match(/body link `([^`]+)` does not resolve/);
|
|
116
|
+
if (!match) continue;
|
|
117
|
+
|
|
118
|
+
const brokenHref = match[1];
|
|
119
|
+
const brokenBasename = path.basename(brokenHref);
|
|
120
|
+
|
|
121
|
+
if (duplicateBasenames.has(brokenBasename)) continue;
|
|
122
|
+
const resolved = basenameMap.get(brokenBasename);
|
|
123
|
+
if (!resolved) continue;
|
|
124
|
+
|
|
125
|
+
const correctHref = path.relative(docDir, resolved).split(path.sep).join('/');
|
|
126
|
+
if (correctHref === brokenHref) continue;
|
|
127
|
+
|
|
128
|
+
const linkRegex = new RegExp(
|
|
129
|
+
'(?<!!)\\[([^\\]]+)\\]\\(' + escapeRegex(brokenHref) + '(#[^)]*)?\\)',
|
|
130
|
+
'g'
|
|
131
|
+
);
|
|
132
|
+
newBody = newBody.replace(linkRegex, (_, text, anchor) =>
|
|
133
|
+
`[${text}](${correctHref}${anchor ?? ''})`
|
|
134
|
+
);
|
|
135
|
+
bodyFixes.push({ brokenHref, correctHref });
|
|
94
136
|
}
|
|
95
137
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
if (bodyFixes.length > 0 && newBody !== body) {
|
|
139
|
+
if (!dryRun) {
|
|
140
|
+
writeFileSync(absPath, `---\n${fm}\n---\n${newBody}`, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
if (!quiet) {
|
|
143
|
+
process.stdout.write(`${prefix}${green('Fixed')}: ${doc.path} (${bodyFixes.length} body link${bodyFixes.length > 1 ? 's' : ''})\n`);
|
|
144
|
+
for (const { brokenHref, correctHref } of bodyFixes) {
|
|
145
|
+
process.stdout.write(`${prefix} ${dim(`${brokenHref} → ${correctHref}`)}\n`);
|
|
146
|
+
}
|
|
100
147
|
}
|
|
148
|
+
bodyLinksFixed += bodyFixes.length;
|
|
101
149
|
}
|
|
102
|
-
totalFixed += fixes.length;
|
|
103
150
|
}
|
|
104
151
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
152
|
+
// ── Summary ──────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
const allFixed = totalFixed + bodyLinksFixed;
|
|
155
|
+
if (!quiet && allFixed > 0) {
|
|
156
|
+
process.stdout.write(`\n${prefix}${allFixed} fix${allFixed !== 1 ? 'es' : ''} applied.\n`);
|
|
157
|
+
}
|
|
158
|
+
if (!quiet && unfixableCount > 0) {
|
|
159
|
+
process.stdout.write(`${yellow(`${unfixableCount} broken reference(s) could not be auto-resolved`)} (file not found by basename).\n`);
|
|
110
160
|
}
|
|
111
161
|
|
|
112
|
-
return { totalFixed, unfixableCount };
|
|
162
|
+
return { totalFixed, unfixableCount, bodyLinksFixed };
|
|
113
163
|
}
|
package/src/index.mjs
CHANGED
|
@@ -77,11 +77,15 @@ export function collectDocFiles(config) {
|
|
|
77
77
|
const files = [];
|
|
78
78
|
const skipPaths = new Set();
|
|
79
79
|
if (config.indexPath) skipPaths.add(config.indexPath);
|
|
80
|
-
|
|
80
|
+
const roots = config.docsRoots || [config.docsRoot];
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
for (const root of roots) {
|
|
83
|
+
walkMarkdownFiles(root, files, config.excludeDirs, skipPaths, seen);
|
|
84
|
+
}
|
|
81
85
|
return files.sort((a, b) => a.localeCompare(b));
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
function walkMarkdownFiles(directory, files, excludedDirs, skipPaths) {
|
|
88
|
+
function walkMarkdownFiles(directory, files, excludedDirs, skipPaths, seen = new Set()) {
|
|
85
89
|
let entries;
|
|
86
90
|
try {
|
|
87
91
|
entries = readdirSync(directory, { withFileTypes: true });
|
|
@@ -91,11 +95,12 @@ function walkMarkdownFiles(directory, files, excludedDirs, skipPaths) {
|
|
|
91
95
|
for (const entry of entries) {
|
|
92
96
|
if (entry.isDirectory()) {
|
|
93
97
|
if (excludedDirs && excludedDirs.has(entry.name)) continue;
|
|
94
|
-
walkMarkdownFiles(path.join(directory, entry.name), files, excludedDirs, skipPaths);
|
|
98
|
+
walkMarkdownFiles(path.join(directory, entry.name), files, excludedDirs, skipPaths, seen);
|
|
95
99
|
continue;
|
|
96
100
|
}
|
|
97
101
|
const fullPath = path.join(directory, entry.name);
|
|
98
|
-
if (!entry.isFile() || !entry.name.endsWith('.md') || skipPaths.has(fullPath)) continue;
|
|
102
|
+
if (!entry.isFile() || !entry.name.endsWith('.md') || skipPaths.has(fullPath) || seen.has(fullPath)) continue;
|
|
103
|
+
seen.add(fullPath);
|
|
99
104
|
files.push(fullPath);
|
|
100
105
|
}
|
|
101
106
|
}
|
|
@@ -127,8 +132,14 @@ export function parseDocFile(filePath, config) {
|
|
|
127
132
|
refFields[field] = normalizeStringList(parsedFrontmatter[field]);
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
// Tag doc with its root
|
|
136
|
+
const roots = config.docsRoots || [config.docsRoot];
|
|
137
|
+
const docRoot = roots.find(r => filePath.startsWith(r)) ?? config.docsRoot;
|
|
138
|
+
const rootLabel = path.relative(config.repoRoot, docRoot).split(path.sep).join('/');
|
|
139
|
+
|
|
130
140
|
const doc = {
|
|
131
141
|
path: relativePath,
|
|
142
|
+
root: rootLabel,
|
|
132
143
|
status: asString(parsedFrontmatter.status) ?? null,
|
|
133
144
|
owner: asString(parsedFrontmatter.owner) ?? null,
|
|
134
145
|
surface,
|
package/src/init.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
3
4
|
import { green, dim } from './color.mjs';
|
|
4
5
|
|
|
5
6
|
const STARTER_CONFIG = `// dotmd.config.mjs — document management configuration
|
|
@@ -24,6 +25,84 @@ _No docs yet. Run \`dotmd list\` after creating your first document._
|
|
|
24
25
|
<!-- GENERATED:dotmd:end -->
|
|
25
26
|
`;
|
|
26
27
|
|
|
28
|
+
function scanExistingDocs(dir) {
|
|
29
|
+
const statuses = new Set();
|
|
30
|
+
const surfaces = new Set();
|
|
31
|
+
const modules = new Set();
|
|
32
|
+
const refFieldNames = new Set();
|
|
33
|
+
let docCount = 0;
|
|
34
|
+
|
|
35
|
+
function walk(d) {
|
|
36
|
+
let entries;
|
|
37
|
+
try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.isDirectory()) { walk(path.join(d, entry.name)); continue; }
|
|
40
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
41
|
+
let raw;
|
|
42
|
+
try { raw = readFileSync(path.join(d, entry.name), 'utf8'); } catch { continue; }
|
|
43
|
+
const { frontmatter } = extractFrontmatter(raw);
|
|
44
|
+
if (!frontmatter) continue;
|
|
45
|
+
const parsed = parseSimpleFrontmatter(frontmatter);
|
|
46
|
+
docCount++;
|
|
47
|
+
if (parsed.status) statuses.add(String(parsed.status).toLowerCase());
|
|
48
|
+
if (parsed.surface) surfaces.add(String(parsed.surface));
|
|
49
|
+
if (Array.isArray(parsed.surfaces)) parsed.surfaces.forEach(s => surfaces.add(String(s)));
|
|
50
|
+
if (parsed.module) modules.add(String(parsed.module));
|
|
51
|
+
if (Array.isArray(parsed.modules)) parsed.modules.forEach(m => modules.add(String(m)));
|
|
52
|
+
for (const [key, val] of Object.entries(parsed)) {
|
|
53
|
+
if (Array.isArray(val) && val.some(v => String(v).endsWith('.md'))) {
|
|
54
|
+
refFieldNames.add(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
walk(dir);
|
|
61
|
+
return { docCount, statuses, surfaces, modules, refFieldNames };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function generateDetectedConfig(scan, rootPath) {
|
|
65
|
+
const lines = [`// dotmd.config.mjs — auto-detected from ${scan.docCount} existing docs`, ''];
|
|
66
|
+
lines.push(`export const root = '${rootPath}';`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
|
|
69
|
+
const defaultOrder = ['active', 'ready', 'planned', 'research', 'blocked', 'reference', 'archived'];
|
|
70
|
+
const ordered = defaultOrder.filter(s => scan.statuses.has(s));
|
|
71
|
+
const extra = [...scan.statuses].filter(s => !defaultOrder.includes(s)).sort();
|
|
72
|
+
const allStatuses = [...ordered, ...extra];
|
|
73
|
+
if (allStatuses.length > 0) {
|
|
74
|
+
lines.push('export const statuses = {');
|
|
75
|
+
lines.push(` order: [${allStatuses.map(s => `'${s}'`).join(', ')}],`);
|
|
76
|
+
lines.push('};');
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (scan.surfaces.size > 0) {
|
|
81
|
+
lines.push('export const taxonomy = {');
|
|
82
|
+
lines.push(` surfaces: [${[...scan.surfaces].sort().map(s => `'${s}'`).join(', ')}],`);
|
|
83
|
+
lines.push('};');
|
|
84
|
+
lines.push('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (scan.refFieldNames.size > 0) {
|
|
88
|
+
const names = [...scan.refFieldNames].sort();
|
|
89
|
+
lines.push('export const referenceFields = {');
|
|
90
|
+
lines.push(` bidirectional: [${names.map(n => `'${n}'`).join(', ')}],`);
|
|
91
|
+
lines.push(' unidirectional: [],');
|
|
92
|
+
lines.push('};');
|
|
93
|
+
lines.push('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
lines.push('export const index = {');
|
|
97
|
+
lines.push(` path: '${rootPath}/docs.md',`);
|
|
98
|
+
lines.push(` startMarker: '<!-- GENERATED:dotmd:start -->',`);
|
|
99
|
+
lines.push(` endMarker: '<!-- GENERATED:dotmd:end -->',`);
|
|
100
|
+
lines.push('};');
|
|
101
|
+
lines.push('');
|
|
102
|
+
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
27
106
|
export function runInit(cwd) {
|
|
28
107
|
const configPath = path.join(cwd, 'dotmd.config.mjs');
|
|
29
108
|
const docsDir = path.join(cwd, 'docs');
|
|
@@ -34,8 +113,14 @@ export function runInit(cwd) {
|
|
|
34
113
|
if (existsSync(configPath)) {
|
|
35
114
|
process.stdout.write(` ${dim('exists')} dotmd.config.mjs\n`);
|
|
36
115
|
} else {
|
|
37
|
-
|
|
38
|
-
|
|
116
|
+
const scan = existsSync(docsDir) ? scanExistingDocs(docsDir) : null;
|
|
117
|
+
if (scan && scan.docCount > 0) {
|
|
118
|
+
writeFileSync(configPath, generateDetectedConfig(scan, 'docs'), 'utf8');
|
|
119
|
+
process.stdout.write(` ${green('create')} dotmd.config.mjs (detected ${scan.docCount} docs)\n`);
|
|
120
|
+
} else {
|
|
121
|
+
writeFileSync(configPath, STARTER_CONFIG, 'utf8');
|
|
122
|
+
process.stdout.write(` ${green('create')} dotmd.config.mjs\n`);
|
|
123
|
+
}
|
|
39
124
|
}
|
|
40
125
|
|
|
41
126
|
if (existsSync(docsDir)) {
|
|
@@ -57,4 +142,3 @@ export function runInit(cwd) {
|
|
|
57
142
|
process.stdout.write(` printf '---\\nstatus: active\\nupdated: ${today}\\n---\\n\\n# My Doc\\n' > docs/my-doc.md\n`);
|
|
58
143
|
process.stdout.write(` dotmd list\n\n`);
|
|
59
144
|
}
|
|
60
|
-
|
package/src/lifecycle.mjs
CHANGED
|
@@ -7,6 +7,11 @@ 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
9
|
|
|
10
|
+
function findFileRoot(filePath, config) {
|
|
11
|
+
const roots = config.docsRoots || [config.docsRoot];
|
|
12
|
+
return roots.find(r => filePath.startsWith(r)) ?? config.docsRoot;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function runStatus(argv, config, opts = {}) {
|
|
11
16
|
const { dryRun } = opts;
|
|
12
17
|
const input = argv[0];
|
|
@@ -29,7 +34,8 @@ export function runStatus(argv, config, opts = {}) {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
const today = new Date().toISOString().slice(0, 10);
|
|
32
|
-
const
|
|
37
|
+
const fileRoot = findFileRoot(filePath, config);
|
|
38
|
+
const archiveDir = path.join(fileRoot, config.archiveDir);
|
|
33
39
|
const isArchiving = config.lifecycle.archiveStatuses.has(newStatus) && !filePath.includes(`/${config.archiveDir}/`);
|
|
34
40
|
const isUnarchiving = !config.lifecycle.archiveStatuses.has(newStatus) && filePath.includes(`/${config.archiveDir}/`);
|
|
35
41
|
let finalPath = filePath;
|
|
@@ -43,7 +49,7 @@ export function runStatus(argv, config, opts = {}) {
|
|
|
43
49
|
finalPath = targetPath;
|
|
44
50
|
}
|
|
45
51
|
if (isUnarchiving) {
|
|
46
|
-
const targetPath = path.join(
|
|
52
|
+
const targetPath = path.join(fileRoot, path.basename(filePath));
|
|
47
53
|
process.stdout.write(`${prefix} Would move: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
48
54
|
finalPath = targetPath;
|
|
49
55
|
}
|
|
@@ -66,7 +72,7 @@ export function runStatus(argv, config, opts = {}) {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
if (isUnarchiving) {
|
|
69
|
-
const targetPath = path.join(
|
|
75
|
+
const targetPath = path.join(fileRoot, path.basename(filePath));
|
|
70
76
|
if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
|
|
71
77
|
const result = gitMv(filePath, targetPath, config.repoRoot);
|
|
72
78
|
if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
|
|
@@ -102,7 +108,8 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
102
108
|
const oldStatus = asString(parsed.status) ?? 'unknown';
|
|
103
109
|
|
|
104
110
|
const today = new Date().toISOString().slice(0, 10);
|
|
105
|
-
const
|
|
111
|
+
const archiveFileRoot = findFileRoot(filePath, config);
|
|
112
|
+
const targetDir = path.join(archiveFileRoot, config.archiveDir);
|
|
106
113
|
const targetPath = path.join(targetDir, path.basename(filePath));
|
|
107
114
|
const oldRepoPath = toRepoPath(filePath, config.repoRoot);
|
|
108
115
|
const newRepoPath = toRepoPath(targetPath, config.repoRoot);
|
package/src/new.mjs
CHANGED
|
@@ -44,10 +44,12 @@ export function runNew(argv, config, opts = {}) {
|
|
|
44
44
|
let status = 'active';
|
|
45
45
|
let title = null;
|
|
46
46
|
let templateName = null;
|
|
47
|
+
let rootName = null;
|
|
47
48
|
for (let i = 0; i < argv.length; i++) {
|
|
48
49
|
if (argv[i] === '--status' && argv[i + 1]) { status = argv[++i]; continue; }
|
|
49
50
|
if (argv[i] === '--title' && argv[i + 1]) { title = argv[++i]; continue; }
|
|
50
51
|
if (argv[i] === '--template' && argv[i + 1]) { templateName = argv[++i]; continue; }
|
|
52
|
+
if (argv[i] === '--root' && argv[i + 1]) { rootName = argv[++i]; continue; }
|
|
51
53
|
if (argv[i] === '--list-templates') {
|
|
52
54
|
listTemplates(config);
|
|
53
55
|
return;
|
|
@@ -73,8 +75,20 @@ export function runNew(argv, config, opts = {}) {
|
|
|
73
75
|
// Title
|
|
74
76
|
const docTitle = title ?? name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
75
77
|
|
|
78
|
+
// Resolve target root
|
|
79
|
+
let targetRoot = config.docsRoot;
|
|
80
|
+
if (rootName) {
|
|
81
|
+
const roots = config.docsRoots || [config.docsRoot];
|
|
82
|
+
const match = roots.find(r => r.endsWith(rootName) || path.basename(r) === rootName);
|
|
83
|
+
if (!match) {
|
|
84
|
+
const available = roots.map(r => path.basename(r)).join(', ');
|
|
85
|
+
die(`Unknown root: ${rootName}\nAvailable: ${available}`);
|
|
86
|
+
}
|
|
87
|
+
targetRoot = match;
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
// Path
|
|
77
|
-
const filePath = path.join(
|
|
91
|
+
const filePath = path.join(targetRoot, slug + '.md');
|
|
78
92
|
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
79
93
|
|
|
80
94
|
if (existsSync(filePath)) {
|