dotmd-cli 0.8.6 → 0.9.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Beyond
3
+ Copyright (c) 2026 Robert Owens
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/bin/dotmd.mjs CHANGED
File without changes
@@ -16,6 +16,12 @@ export const excludeDirs = ['evidence'];
16
16
  // Status workflow — order determines display grouping
17
17
  export const statuses = {
18
18
  order: ['active', 'ready', 'planned', 'research', 'blocked', 'reference', 'archived'],
19
+ // Additional statuses valid only in specific roots (merged with order)
20
+ // Useful when different doc areas track different things (e.g. plans vs module docs)
21
+ // rootStatuses: {
22
+ // 'docs/modules': ['implemented', 'partial', 'draft', 'deprecated'],
23
+ // 'docs/core': ['implemented', 'partial'],
24
+ // },
19
25
  // Days after which a doc is considered stale (null = never stale)
20
26
  staleDays: {
21
27
  active: 14,
@@ -77,6 +83,13 @@ export const presets = {
77
83
  actionable: ['--status', 'active,ready', '--has-next-step', '--sort', 'updated', '--all'],
78
84
  };
79
85
 
86
+ // ─── Notion ──────────────────────────────────────────────────────────────────
87
+ // IMPORTANT: Use environment variables for tokens — never hardcode secrets in config files.
88
+ // export const notion = {
89
+ // token: process.env.NOTION_TOKEN,
90
+ // databaseId: process.env.NOTION_DATABASE_ID,
91
+ // };
92
+
80
93
  // ─── Function Hooks ──────────────────────────────────────────────────────────
81
94
  // Hooks are optional. Each receives a default implementation it can wrap or replace.
82
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.8.6",
3
+ "version": "0.9.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/config.mjs CHANGED
@@ -216,6 +216,16 @@ export async function resolveConfig(cwd, explicitConfigPath) {
216
216
  staleDaysByStatus[status] = config.statuses.staleDays?.[status] ?? null;
217
217
  }
218
218
 
219
+ // Per-root additional statuses (merged with global validStatuses)
220
+ const rootStatusesRaw = config.statuses.rootStatuses ?? {};
221
+ const rootLabels = new Set(rootPaths.map(r => path.relative(configDir, path.resolve(configDir, r)).split(path.sep).join('/')));
222
+ const rootValidStatuses = new Map();
223
+ for (const [rootKey, extraStatuses] of Object.entries(rootStatusesRaw)) {
224
+ const merged = new Set(validStatuses);
225
+ for (const s of extraStatuses) merged.add(s);
226
+ rootValidStatuses.set(rootKey, merged);
227
+ }
228
+
219
229
  const validSurfaces = config.taxonomy.surfaces
220
230
  ? new Set(config.taxonomy.surfaces)
221
231
  : null;
@@ -235,6 +245,13 @@ export async function resolveConfig(cwd, explicitConfigPath) {
235
245
  const skipStaleFor = new Set(lifecycle.skipStaleFor);
236
246
  const skipWarningsFor = new Set(lifecycle.skipWarningsFor);
237
247
 
248
+ // Warn if rootStatuses keys don't match any configured root
249
+ for (const rootKey of Object.keys(rootStatusesRaw)) {
250
+ if (!rootLabels.has(rootKey)) {
251
+ earlyWarnings.push(`Config: statuses.rootStatuses key '${rootKey}' does not match any configured root.`);
252
+ }
253
+ }
254
+
238
255
  const configWarnings = [...earlyWarnings, ...validateConfig(userConfig, config, validStatuses, indexPath)];
239
256
 
240
257
  return {
@@ -252,6 +269,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
252
269
 
253
270
  statusOrder,
254
271
  validStatuses,
272
+ rootValidStatuses,
255
273
  staleDaysByStatus,
256
274
 
257
275
  lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor },
package/src/lifecycle.mjs CHANGED
@@ -27,11 +27,16 @@ export async function runStatus(argv, config, opts = {}) {
27
27
  die('Usage: dotmd status <file> <new-status>');
28
28
  }
29
29
  }
30
- if (!config.validStatuses.has(newStatus)) { die(`Invalid status: ${newStatus}\nValid: ${[...config.validStatuses].join(', ')}`); }
31
-
32
30
  const filePath = resolveDocPath(input, config);
33
31
  if (!filePath) { die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`); }
34
32
 
33
+ // Validate status against root-specific vocabulary
34
+ const fileRoot = findFileRoot(filePath, config);
35
+ const rootLabel = path.relative(config.repoRoot, fileRoot).split(path.sep).join('/');
36
+ const rootSet = config.rootValidStatuses?.get(rootLabel);
37
+ const effectiveValid = rootSet ?? config.validStatuses;
38
+ if (!effectiveValid.has(newStatus)) { die(`Invalid status: ${newStatus}\nValid: ${[...effectiveValid].join(', ')}`); }
39
+
35
40
  const raw = readFileSync(filePath, 'utf8');
36
41
  const { frontmatter } = extractFrontmatter(raw);
37
42
  const parsed = parseSimpleFrontmatter(frontmatter);
@@ -43,7 +48,6 @@ export async function runStatus(argv, config, opts = {}) {
43
48
  }
44
49
 
45
50
  const today = new Date().toISOString().slice(0, 10);
46
- const fileRoot = findFileRoot(filePath, config);
47
51
  const archiveDir = path.join(fileRoot, config.archiveDir);
48
52
  const isArchiving = config.lifecycle.archiveStatuses.has(newStatus) && !filePath.includes(`/${config.archiveDir}/`);
49
53
  const isUnarchiving = !config.lifecycle.archiveStatuses.has(newStatus) && filePath.includes(`/${config.archiveDir}/`);
package/src/validate.mjs CHANGED
@@ -6,15 +6,20 @@ import { toRepoPath } from './util.mjs';
6
6
 
7
7
  const NOW = new Date();
8
8
 
9
+ function isValidStatus(status, root, config) {
10
+ const rootSet = config.rootValidStatuses?.get(root);
11
+ if (rootSet) return rootSet.has(status);
12
+ return config.validStatuses.has(status);
13
+ }
14
+
9
15
  export function validateDoc(doc, frontmatter, headingTitle, config) {
10
16
  if (!doc.status) {
11
17
  doc.errors.push({ path: doc.path, level: 'error', message: 'Missing frontmatter `status`.' });
12
- } else if (!config.validStatuses.has(doc.status)) {
18
+ } else if (!isValidStatus(doc.status, doc.root, config)) {
13
19
  doc.warnings.push({ path: doc.path, level: 'warning', message: `Unknown status \`${doc.status}\`; not in statuses.order.` });
14
20
  }
15
21
 
16
- // Only enforce lifecycle fields for known statuses (skip for unknown like implemented, partial, etc.)
17
- const knownStatus = config.validStatuses.has(doc.status);
22
+ const knownStatus = isValidStatus(doc.status, doc.root, config);
18
23
 
19
24
  if (knownStatus && !config.lifecycle.skipWarningsFor.has(doc.status) && !doc.updated) {
20
25
  doc.errors.push({ path: doc.path, level: 'error', message: 'Missing frontmatter `updated` for non-archived doc.' });