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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/init.mjs +95 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.29.3",
3
+ "version": "0.30.0",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, Notion sync, AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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()) { walk(path.join(d, entry.name)); continue; }
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
- if (!frontmatter) continue;
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/';