dotmd-cli 0.14.1 → 0.14.3
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/config.mjs +131 -3
- package/src/index.mjs +2 -1
- package/src/init.mjs +3 -2
- package/src/notion.mjs +2 -2
- package/src/query.mjs +1 -1
- package/src/render.mjs +1 -1
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -5,6 +5,16 @@ import { die, warn } from './util.mjs';
|
|
|
5
5
|
|
|
6
6
|
const CONFIG_FILENAMES = ['dotmd.config.mjs', '.dotmd.config.mjs', 'dotmd.config.js'];
|
|
7
7
|
|
|
8
|
+
// Keys where user config replaces defaults entirely (not deep-merged).
|
|
9
|
+
// These are flat maps or config sections where the user's version is authoritative —
|
|
10
|
+
// default keys that the user omitted should NOT survive the merge.
|
|
11
|
+
const REPLACE_KEYS = new Set([
|
|
12
|
+
'statuses.staleDays',
|
|
13
|
+
'statuses.rootStatuses',
|
|
14
|
+
'presets',
|
|
15
|
+
'context',
|
|
16
|
+
]);
|
|
17
|
+
|
|
8
18
|
const DEFAULTS = {
|
|
9
19
|
root: '.',
|
|
10
20
|
archiveDir: 'archived',
|
|
@@ -87,6 +97,118 @@ const DEFAULTS = {
|
|
|
87
97
|
},
|
|
88
98
|
};
|
|
89
99
|
|
|
100
|
+
const VALID_CONTEXT_VALUES = new Set(['expanded', 'listed', 'counted']);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Normalize rich status definitions (object form) into array form + derived config.
|
|
104
|
+
* When types.<type>.statuses is an object like:
|
|
105
|
+
* { 'active': { context: 'expanded', staleDays: 14, requiresModule: true }, ... }
|
|
106
|
+
* this extracts all behavioral properties, converts statuses to an array for
|
|
107
|
+
* downstream processing, and returns derived values for lifecycle/taxonomy/context.
|
|
108
|
+
*
|
|
109
|
+
* Returns null if no types use object-form statuses.
|
|
110
|
+
*/
|
|
111
|
+
function normalizeRichStatuses(config, userConfig) {
|
|
112
|
+
const derived = {
|
|
113
|
+
archiveStatuses: [],
|
|
114
|
+
skipStaleFor: [],
|
|
115
|
+
skipWarningsFor: [],
|
|
116
|
+
terminalStatuses: [],
|
|
117
|
+
moduleRequiredFor: [],
|
|
118
|
+
staleDays: {},
|
|
119
|
+
statusOrder: [],
|
|
120
|
+
context: { expanded: [], listed: [], counted: [] },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let hasRich = false;
|
|
124
|
+
|
|
125
|
+
for (const [typeName, typeDef] of Object.entries(config.types ?? {})) {
|
|
126
|
+
if (!typeDef.statuses || Array.isArray(typeDef.statuses)) continue;
|
|
127
|
+
if (typeof typeDef.statuses !== 'object') continue;
|
|
128
|
+
|
|
129
|
+
hasRich = true;
|
|
130
|
+
const statusNames = [];
|
|
131
|
+
const typeContext = { expanded: [], listed: [], counted: [] };
|
|
132
|
+
const typeStaleDays = {};
|
|
133
|
+
|
|
134
|
+
for (const [name, props] of Object.entries(typeDef.statuses)) {
|
|
135
|
+
const p = props ?? {};
|
|
136
|
+
statusNames.push(name);
|
|
137
|
+
|
|
138
|
+
const ctx = p.context ?? 'counted';
|
|
139
|
+
if (typeContext[ctx]) typeContext[ctx].push(name);
|
|
140
|
+
// Global context: only add if not already in any bucket (first type wins)
|
|
141
|
+
const inGlobal = derived.context.expanded.includes(name) ||
|
|
142
|
+
derived.context.listed.includes(name) || derived.context.counted.includes(name);
|
|
143
|
+
if (!inGlobal && derived.context[ctx]) derived.context[ctx].push(name);
|
|
144
|
+
|
|
145
|
+
if (p.staleDays != null) {
|
|
146
|
+
typeStaleDays[name] = p.staleDays;
|
|
147
|
+
derived.staleDays[name] = p.staleDays;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
|
|
151
|
+
if (p.skipStale && !derived.skipStaleFor.includes(name)) derived.skipStaleFor.push(name);
|
|
152
|
+
if (p.skipWarnings && !derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
|
|
153
|
+
if (p.terminal && !derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
|
|
154
|
+
if (p.requiresModule && !derived.moduleRequiredFor.includes(name)) derived.moduleRequiredFor.push(name);
|
|
155
|
+
|
|
156
|
+
if (!derived.statusOrder.includes(name)) derived.statusOrder.push(name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Convert to array form for downstream pipeline
|
|
160
|
+
typeDef.statuses = statusNames;
|
|
161
|
+
|
|
162
|
+
// Derive type-level context/staleDays unless user explicitly provided them
|
|
163
|
+
const userTypeDef = userConfig.types?.[typeName];
|
|
164
|
+
if (!userTypeDef?.context) typeDef.context = typeContext;
|
|
165
|
+
if (!userTypeDef?.staleDays) typeDef.staleDays = typeStaleDays;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!hasRich) return null;
|
|
169
|
+
return derived;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Apply derived values from rich status definitions into the merged config.
|
|
174
|
+
* Explicit user config always wins over derived values.
|
|
175
|
+
*/
|
|
176
|
+
function applyDerivedConfig(config, userConfig, derived) {
|
|
177
|
+
// statuses.order — derive from types if user didn't explicitly set
|
|
178
|
+
if (!userConfig.statuses?.order && derived.statusOrder.length) {
|
|
179
|
+
config.statuses.order = derived.statusOrder;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// statuses.staleDays — merge derived as base, user overrides
|
|
183
|
+
if (!userConfig.statuses?.staleDays && Object.keys(derived.staleDays).length) {
|
|
184
|
+
config.statuses.staleDays = derived.staleDays;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// lifecycle — each sub-key independently
|
|
188
|
+
if (!userConfig.lifecycle?.archiveStatuses && derived.archiveStatuses.length) {
|
|
189
|
+
config.lifecycle.archiveStatuses = derived.archiveStatuses;
|
|
190
|
+
}
|
|
191
|
+
if (!userConfig.lifecycle?.skipStaleFor && derived.skipStaleFor.length) {
|
|
192
|
+
config.lifecycle.skipStaleFor = derived.skipStaleFor;
|
|
193
|
+
}
|
|
194
|
+
if (!userConfig.lifecycle?.skipWarningsFor && derived.skipWarningsFor.length) {
|
|
195
|
+
config.lifecycle.skipWarningsFor = derived.skipWarningsFor;
|
|
196
|
+
}
|
|
197
|
+
if (!userConfig.lifecycle?.terminalStatuses && derived.terminalStatuses.length) {
|
|
198
|
+
config.lifecycle.terminalStatuses = derived.terminalStatuses;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// taxonomy.moduleRequiredFor
|
|
202
|
+
if (!userConfig.taxonomy?.moduleRequiredFor && derived.moduleRequiredFor.length) {
|
|
203
|
+
config.taxonomy.moduleRequiredFor = derived.moduleRequiredFor;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// context — only the status-related arrays, not recentDays/recentLimit/etc
|
|
207
|
+
if (!userConfig.context?.expanded) config.context.expanded = derived.context.expanded;
|
|
208
|
+
if (!userConfig.context?.listed) config.context.listed = derived.context.listed;
|
|
209
|
+
if (!userConfig.context?.counted) config.context.counted = derived.context.counted;
|
|
210
|
+
}
|
|
211
|
+
|
|
90
212
|
function findConfigFile(startDir) {
|
|
91
213
|
let dir = path.resolve(startDir);
|
|
92
214
|
const root = path.parse(dir).root;
|
|
@@ -155,12 +277,14 @@ function validateConfig(userConfig, config, validStatuses, indexPath) {
|
|
|
155
277
|
return warnings;
|
|
156
278
|
}
|
|
157
279
|
|
|
158
|
-
function deepMerge(defaults, overrides) {
|
|
280
|
+
function deepMerge(defaults, overrides, parentPath = '') {
|
|
159
281
|
const result = { ...defaults };
|
|
160
282
|
for (const [key, value] of Object.entries(overrides)) {
|
|
283
|
+
const keyPath = parentPath ? `${parentPath}.${key}` : key;
|
|
161
284
|
if (value != null && typeof value === 'object' && !Array.isArray(value) &&
|
|
162
|
-
result[key] != null && typeof result[key] === 'object' && !Array.isArray(result[key])
|
|
163
|
-
|
|
285
|
+
result[key] != null && typeof result[key] === 'object' && !Array.isArray(result[key]) &&
|
|
286
|
+
!REPLACE_KEYS.has(keyPath)) {
|
|
287
|
+
result[key] = deepMerge(result[key], value, keyPath);
|
|
164
288
|
} else {
|
|
165
289
|
result[key] = value;
|
|
166
290
|
}
|
|
@@ -206,6 +330,10 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
206
330
|
|
|
207
331
|
const config = deepMerge(DEFAULTS, userConfig);
|
|
208
332
|
|
|
333
|
+
// Normalize rich status definitions (object form → array + derived config)
|
|
334
|
+
const derived = normalizeRichStatuses(config, userConfig);
|
|
335
|
+
if (derived) applyDerivedConfig(config, userConfig, derived);
|
|
336
|
+
|
|
209
337
|
const rootPaths = Array.isArray(config.root) ? config.root : [config.root];
|
|
210
338
|
const docsRoots = rootPaths.map(r => path.resolve(configDir, r));
|
|
211
339
|
const docsRoot = docsRoots[0]; // primary root for backwards compat
|
package/src/index.mjs
CHANGED
|
@@ -99,7 +99,8 @@ function walkMarkdownFiles(directory, files, excludedDirs, skipPaths, seen = new
|
|
|
99
99
|
let entries;
|
|
100
100
|
try {
|
|
101
101
|
entries = readdirSync(directory, { withFileTypes: true });
|
|
102
|
-
} catch {
|
|
102
|
+
} catch (err) {
|
|
103
|
+
warn(`Could not read directory ${directory}: ${err.message}`);
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
106
|
for (const entry of entries) {
|
package/src/init.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
4
|
import { green, dim } from './color.mjs';
|
|
5
|
+
import { warn } from './util.mjs';
|
|
5
6
|
import { scaffoldClaudeCommands } from './claude-commands.mjs';
|
|
6
7
|
|
|
7
8
|
const STARTER_CONFIG = `// dotmd.config.mjs — document management configuration
|
|
@@ -35,12 +36,12 @@ function scanExistingDocs(dir) {
|
|
|
35
36
|
|
|
36
37
|
function walk(d) {
|
|
37
38
|
let entries;
|
|
38
|
-
try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
39
|
+
try { entries = readdirSync(d, { withFileTypes: true }); } catch (err) { warn(`Could not read ${d}: ${err.message}`); return; }
|
|
39
40
|
for (const entry of entries) {
|
|
40
41
|
if (entry.isDirectory()) { walk(path.join(d, entry.name)); continue; }
|
|
41
42
|
if (!entry.name.endsWith('.md')) continue;
|
|
42
43
|
let raw;
|
|
43
|
-
try { raw = readFileSync(path.join(d, entry.name), 'utf8'); } catch { continue; }
|
|
44
|
+
try { raw = readFileSync(path.join(d, entry.name), 'utf8'); } catch (err) { warn(`Could not read ${entry.name}: ${err.message}`); continue; }
|
|
44
45
|
const { frontmatter } = extractFrontmatter(raw);
|
|
45
46
|
if (!frontmatter) continue;
|
|
46
47
|
const parsed = parseSimpleFrontmatter(frontmatter);
|
package/src/notion.mjs
CHANGED
|
@@ -402,7 +402,7 @@ export async function runNotionSync(argv, config, opts = {}) {
|
|
|
402
402
|
try {
|
|
403
403
|
const mdBlocks = await n2m.pageToMarkdown(remote.page.id);
|
|
404
404
|
body = n2m.toMarkdownString(mdBlocks).parent ?? '';
|
|
405
|
-
} catch {
|
|
405
|
+
} catch (err) { warn(`Could not convert Notion page body for "${title}": ${err.message}`); }
|
|
406
406
|
|
|
407
407
|
const content = `---\n${serializeFrontmatter(fm)}\n---\n\n# ${title}\n\n${body}`;
|
|
408
408
|
const filePath = path.join(config.docsRoot, slug + '.md');
|
|
@@ -456,7 +456,7 @@ export async function runNotionSync(argv, config, opts = {}) {
|
|
|
456
456
|
try {
|
|
457
457
|
const mdBlocks = await n2m.pageToMarkdown(remote.page.id);
|
|
458
458
|
body = n2m.toMarkdownString(mdBlocks).parent ?? '';
|
|
459
|
-
} catch {
|
|
459
|
+
} catch (err) { warn(`Could not convert Notion page body for "${title}": ${err.message}`); }
|
|
460
460
|
|
|
461
461
|
const content = `---\n${serializeFrontmatter(fm)}\n---\n\n# ${title}\n\n${body}`;
|
|
462
462
|
const filePath = path.join(config.repoRoot, local.path);
|
package/src/query.mjs
CHANGED
|
@@ -157,7 +157,7 @@ function getDocSummary(doc, config) {
|
|
|
157
157
|
return config.hooks.summarizeDoc
|
|
158
158
|
? config.hooks.summarizeDoc(body, meta)
|
|
159
159
|
: summarizeDocBody(body, meta);
|
|
160
|
-
} catch { return null; }
|
|
160
|
+
} catch (err) { warn(`Could not summarize ${doc.path}: ${err.message}`); return null; }
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
function renderQueryResults(docs, filters, config) {
|
package/src/render.mjs
CHANGED
|
@@ -151,7 +151,7 @@ function _renderContextSection(docs, ctx, opts, config, lines) {
|
|
|
151
151
|
if (summary) {
|
|
152
152
|
lines.push(` ${''.padEnd(maxSlug)} ${dim('ai: ' + truncate(summary, 120))}`);
|
|
153
153
|
}
|
|
154
|
-
} catch {
|
|
154
|
+
} catch (err) { warn(`AI summary failed for ${doc.path}: ${err.message}`); }
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
lines.push('');
|