dotmd-cli 0.29.3 → 0.30.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/package.json +1 -1
- package/src/init.mjs +95 -7
package/package.json
CHANGED
package/src/init.mjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
|
-
import { green, dim } from './color.mjs';
|
|
4
|
+
import { green, dim, yellow } from './color.mjs';
|
|
5
5
|
import { warn } from './util.mjs';
|
|
6
6
|
import { scaffoldClaudeCommands } from './claude-commands.mjs';
|
|
7
7
|
|
|
8
|
+
// Subdirectories scaffolded under docsRoot and tracked separately during scans.
|
|
9
|
+
// Each maps to a builtin type (plan, prompt). New types added here should also
|
|
10
|
+
// have a matching builtin template so `dotmd new <type>` lands files correctly.
|
|
11
|
+
const TYPE_SUBDIRS = ['plans', 'prompts'];
|
|
12
|
+
|
|
8
13
|
const STARTER_CONFIG = `// dotmd.config.mjs — document management configuration
|
|
9
14
|
// All exports are optional. See dotmd.config.example.mjs for full reference.
|
|
10
15
|
|
|
@@ -33,17 +38,33 @@ function scanExistingDocs(dir) {
|
|
|
33
38
|
const modules = new Set();
|
|
34
39
|
const refFieldNames = new Set();
|
|
35
40
|
let docCount = 0;
|
|
41
|
+
// Track files per top-level subdir under `dir` (e.g. plans/, prompts/, "")
|
|
42
|
+
// so callers can report what's already there — including files without frontmatter,
|
|
43
|
+
// which are otherwise invisible to detection.
|
|
44
|
+
const subdirCounts = {};
|
|
45
|
+
|
|
46
|
+
function bump(subdir, hasFrontmatter) {
|
|
47
|
+
if (!subdirCounts[subdir]) subdirCounts[subdir] = { withFrontmatter: 0, withoutFrontmatter: 0 };
|
|
48
|
+
if (hasFrontmatter) subdirCounts[subdir].withFrontmatter++;
|
|
49
|
+
else subdirCounts[subdir].withoutFrontmatter++;
|
|
50
|
+
}
|
|
36
51
|
|
|
37
|
-
function walk(d) {
|
|
52
|
+
function walk(d, topSubdir) {
|
|
38
53
|
let entries;
|
|
39
54
|
try { entries = readdirSync(d, { withFileTypes: true }); } catch (err) { warn(`Could not read ${d}: ${err.message}`); return; }
|
|
40
55
|
for (const entry of entries) {
|
|
41
|
-
if (entry.isDirectory()) {
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
const nextTop = topSubdir === null ? entry.name : topSubdir;
|
|
58
|
+
walk(path.join(d, entry.name), nextTop);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
42
61
|
if (!entry.name.endsWith('.md')) continue;
|
|
43
62
|
let raw;
|
|
44
63
|
try { raw = readFileSync(path.join(d, entry.name), 'utf8'); } catch (err) { warn(`Could not read ${entry.name}: ${err.message}`); continue; }
|
|
45
64
|
const { frontmatter } = extractFrontmatter(raw);
|
|
46
|
-
|
|
65
|
+
const subdir = topSubdir ?? '';
|
|
66
|
+
if (!frontmatter) { bump(subdir, false); continue; }
|
|
67
|
+
bump(subdir, true);
|
|
47
68
|
const parsed = parseSimpleFrontmatter(frontmatter);
|
|
48
69
|
docCount++;
|
|
49
70
|
if (parsed.status) statuses.add(String(parsed.status).toLowerCase());
|
|
@@ -59,8 +80,25 @@ function scanExistingDocs(dir) {
|
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
|
|
62
|
-
walk(dir);
|
|
63
|
-
return { docCount, statuses, surfaces, modules, refFieldNames };
|
|
83
|
+
walk(dir, null);
|
|
84
|
+
return { docCount, statuses, surfaces, modules, refFieldNames, subdirCounts };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Count .md files (regardless of frontmatter) directly inside a single directory.
|
|
88
|
+
// Used to detect root-level plans/ or prompts/ siblings that aren't under docsRoot.
|
|
89
|
+
function countMarkdownFiles(dir) {
|
|
90
|
+
let withFrontmatter = 0;
|
|
91
|
+
let withoutFrontmatter = 0;
|
|
92
|
+
let entries;
|
|
93
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return { withFrontmatter, withoutFrontmatter }; }
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
96
|
+
let raw;
|
|
97
|
+
try { raw = readFileSync(path.join(dir, entry.name), 'utf8'); } catch { continue; }
|
|
98
|
+
const { frontmatter } = extractFrontmatter(raw);
|
|
99
|
+
if (frontmatter) withFrontmatter++; else withoutFrontmatter++;
|
|
100
|
+
}
|
|
101
|
+
return { withFrontmatter, withoutFrontmatter };
|
|
64
102
|
}
|
|
65
103
|
|
|
66
104
|
function generateDetectedConfig(scan, rootPath) {
|
|
@@ -112,10 +150,11 @@ export function runInit(cwd, config) {
|
|
|
112
150
|
|
|
113
151
|
process.stdout.write('\n');
|
|
114
152
|
|
|
153
|
+
const scan = existsSync(docsDir) ? scanExistingDocs(docsDir) : null;
|
|
154
|
+
|
|
115
155
|
if (existsSync(configPath)) {
|
|
116
156
|
process.stdout.write(` ${dim('exists')} dotmd.config.mjs\n`);
|
|
117
157
|
} else {
|
|
118
|
-
const scan = existsSync(docsDir) ? scanExistingDocs(docsDir) : null;
|
|
119
158
|
if (scan && scan.docCount > 0) {
|
|
120
159
|
writeFileSync(configPath, generateDetectedConfig(scan, 'docs'), 'utf8');
|
|
121
160
|
process.stdout.write(` ${green('create')} dotmd.config.mjs (detected ${scan.docCount} docs)\n`);
|
|
@@ -132,6 +171,41 @@ export function runInit(cwd, config) {
|
|
|
132
171
|
process.stdout.write(` ${green('create')} docs/\n`);
|
|
133
172
|
}
|
|
134
173
|
|
|
174
|
+
// Inspect root-level siblings (e.g. ./plans/, ./prompts/) before scaffolding.
|
|
175
|
+
// If a sibling already holds content, skip creating the matching docs/<sub>/
|
|
176
|
+
// so we don't quietly create a parallel dir the user has to reconcile.
|
|
177
|
+
const siblingsWithContent = [];
|
|
178
|
+
for (const sub of TYPE_SUBDIRS) {
|
|
179
|
+
const siblingPath = path.join(cwd, sub);
|
|
180
|
+
if (!existsSync(siblingPath)) continue;
|
|
181
|
+
const c = countMarkdownFiles(siblingPath);
|
|
182
|
+
const total = c.withFrontmatter + c.withoutFrontmatter;
|
|
183
|
+
if (total > 0) siblingsWithContent.push({ sub, total });
|
|
184
|
+
}
|
|
185
|
+
const siblingSet = new Set(siblingsWithContent.map(s => s.sub));
|
|
186
|
+
|
|
187
|
+
// Scaffold the canonical type subdirs (docs/plans/, docs/prompts/) so the
|
|
188
|
+
// builtin `dotmd new plan` / `dotmd new prompt` templates land somewhere
|
|
189
|
+
// sensible without extra config.
|
|
190
|
+
for (const sub of TYPE_SUBDIRS) {
|
|
191
|
+
const subPath = path.join(docsDir, sub);
|
|
192
|
+
const counts = scan?.subdirCounts?.[sub];
|
|
193
|
+
const total = counts ? counts.withFrontmatter + counts.withoutFrontmatter : 0;
|
|
194
|
+
if (siblingSet.has(sub) && !existsSync(subPath)) {
|
|
195
|
+
process.stdout.write(` ${yellow('skip')} docs/${sub}/ (root-level ./${sub}/ already holds content)\n`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (existsSync(subPath)) {
|
|
199
|
+
const detail = total > 0
|
|
200
|
+
? ` (${counts.withFrontmatter} dotmd-tracked, ${counts.withoutFrontmatter} plain .md)`
|
|
201
|
+
: '';
|
|
202
|
+
process.stdout.write(` ${dim('exists')} docs/${sub}/${detail}\n`);
|
|
203
|
+
} else {
|
|
204
|
+
mkdirSync(subPath, { recursive: true });
|
|
205
|
+
process.stdout.write(` ${green('create')} docs/${sub}/\n`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
135
209
|
if (existsSync(indexPath)) {
|
|
136
210
|
process.stdout.write(` ${dim('exists')} docs/docs.md\n`);
|
|
137
211
|
} else {
|
|
@@ -139,6 +213,20 @@ export function runInit(cwd, config) {
|
|
|
139
213
|
process.stdout.write(` ${green('create')} docs/docs.md\n`);
|
|
140
214
|
}
|
|
141
215
|
|
|
216
|
+
if (siblingsWithContent.length > 0) {
|
|
217
|
+
const list = siblingsWithContent
|
|
218
|
+
.map(({ sub, total }) => `${sub}/ (${total} .md file${total === 1 ? '' : 's'})`)
|
|
219
|
+
.join(', ');
|
|
220
|
+
const subs = siblingsWithContent.map(s => s.sub);
|
|
221
|
+
process.stdout.write(`\n ${yellow('notice')} found at repo root: ${list}\n`);
|
|
222
|
+
process.stdout.write(` these are NOT under docs/ and won't be tracked by the default config. Either:\n`);
|
|
223
|
+
for (const sub of subs) {
|
|
224
|
+
process.stdout.write(` • move into docs/: mv ./${sub}/* docs/${sub}/ && rmdir ./${sub}\n`);
|
|
225
|
+
}
|
|
226
|
+
process.stdout.write(` • or use a flat layout — set in dotmd.config.mjs:\n`);
|
|
227
|
+
process.stdout.write(` export const root = [${subs.map(s => `'${s}'`).join(', ')}];\n`);
|
|
228
|
+
}
|
|
229
|
+
|
|
142
230
|
// .gitignore: ensure .dotmd/ is ignored (session leases live there)
|
|
143
231
|
const gitignorePath = path.join(cwd, '.gitignore');
|
|
144
232
|
const ignoreLine = '.dotmd/';
|