dotmd-cli 0.47.0 → 0.48.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/config.mjs +22 -1
- package/src/lifecycle.mjs +42 -1
- package/src/validate.mjs +10 -0
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -58,6 +58,10 @@ const DEFAULTS = {
|
|
|
58
58
|
skipStaleFor: ['archived', 'reference', 'partial', 'queued-after'],
|
|
59
59
|
skipWarningsFor: ['archived', 'partial', 'queued-after'],
|
|
60
60
|
terminalStatuses: ['archived', 'deprecated', 'reference'],
|
|
61
|
+
// F15: opt-in per-status `{ filed: true }` in `types.<type>.statuses`
|
|
62
|
+
// populates this map (status → dirName). Empty by default — archive: true
|
|
63
|
+
// remains a separate primitive untouched.
|
|
64
|
+
filedStatuses: {},
|
|
61
65
|
},
|
|
62
66
|
|
|
63
67
|
taxonomy: {
|
|
@@ -126,6 +130,12 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
126
130
|
skipWarningsFor: [],
|
|
127
131
|
terminalStatuses: [],
|
|
128
132
|
moduleRequiredFor: [],
|
|
133
|
+
// F15: status-name → directory-name (defaults to the status name verbatim).
|
|
134
|
+
// Filed statuses move docs into <root>/<dirName>/ on transition INTO the
|
|
135
|
+
// status, and back to flat <root>/ on transition OUT. Separate from
|
|
136
|
+
// archive: true to avoid touching that long-standing primitive's
|
|
137
|
+
// semantics.
|
|
138
|
+
filedStatuses: {},
|
|
129
139
|
staleDays: {},
|
|
130
140
|
statusOrder: [],
|
|
131
141
|
context: { expanded: [], listed: [], counted: [] },
|
|
@@ -177,6 +187,11 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
177
187
|
if ((p.skipWarnings || quietImpliesSkipWarnings) && !derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
|
|
178
188
|
if (p.terminal && !derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
|
|
179
189
|
if (p.requiresModule && !derived.moduleRequiredFor.includes(name)) derived.moduleRequiredFor.push(name);
|
|
190
|
+
if (p.filed && !derived.filedStatuses[name]) {
|
|
191
|
+
// dirName defaults to the status name; users can override with
|
|
192
|
+
// `filed: 'custom-dir'` (string form) instead of `filed: true`.
|
|
193
|
+
derived.filedStatuses[name] = typeof p.filed === 'string' ? p.filed : name;
|
|
194
|
+
}
|
|
180
195
|
|
|
181
196
|
if (!derived.statusOrder.includes(name)) derived.statusOrder.push(name);
|
|
182
197
|
}
|
|
@@ -222,6 +237,9 @@ function applyDerivedConfig(config, userConfig, derived) {
|
|
|
222
237
|
if (!userConfig.lifecycle?.terminalStatuses && derived.terminalStatuses.length) {
|
|
223
238
|
config.lifecycle.terminalStatuses = derived.terminalStatuses;
|
|
224
239
|
}
|
|
240
|
+
if (!userConfig.lifecycle?.filedStatuses && Object.keys(derived.filedStatuses).length) {
|
|
241
|
+
config.lifecycle.filedStatuses = derived.filedStatuses;
|
|
242
|
+
}
|
|
225
243
|
|
|
226
244
|
// taxonomy.moduleRequiredFor
|
|
227
245
|
if (!userConfig.taxonomy?.moduleRequiredFor && derived.moduleRequiredFor.length) {
|
|
@@ -442,6 +460,9 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
442
460
|
const skipStaleFor = new Set(lifecycle.skipStaleFor);
|
|
443
461
|
const skipWarningsFor = new Set(lifecycle.skipWarningsFor);
|
|
444
462
|
const terminalStatuses = new Set(lifecycle.terminalStatuses);
|
|
463
|
+
// F15: filedStatuses keyed by status name, value = directory name. Empty
|
|
464
|
+
// object when no status opts in via `filed: true` (or `filed: '<dirname>'`).
|
|
465
|
+
const filedStatuses = new Map(Object.entries(lifecycle.filedStatuses ?? {}));
|
|
445
466
|
|
|
446
467
|
// Warn if rootStatuses keys don't match any configured root
|
|
447
468
|
for (const rootKey of Object.keys(rootStatusesRaw)) {
|
|
@@ -473,7 +494,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
473
494
|
rootValidStatuses,
|
|
474
495
|
staleDaysByStatus,
|
|
475
496
|
|
|
476
|
-
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses },
|
|
497
|
+
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses, filedStatuses },
|
|
477
498
|
|
|
478
499
|
validSurfaces,
|
|
479
500
|
moduleRequiredStatuses,
|
package/src/lifecycle.mjs
CHANGED
|
@@ -168,9 +168,22 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
168
168
|
const today = nowIso();
|
|
169
169
|
const archiveDir = path.join(fileRoot, config.archiveDir);
|
|
170
170
|
const relFromRoot = path.relative(fileRoot, filePath);
|
|
171
|
+
const relSegments = relFromRoot.split(path.sep);
|
|
171
172
|
const inArchive = relFromRoot.startsWith(config.archiveDir + '/') || relFromRoot.startsWith(config.archiveDir + path.sep);
|
|
172
173
|
const isArchiving = config.lifecycle.archiveStatuses.has(newStatus) && !inArchive;
|
|
173
174
|
const isUnarchiving = !config.lifecycle.archiveStatuses.has(newStatus) && inArchive;
|
|
175
|
+
|
|
176
|
+
// F15 filing: a status with `filed: true` lives in `<root>/<dirName>/`. The
|
|
177
|
+
// current parent dir under root tells us whether the file is in some
|
|
178
|
+
// "bucket" right now. Archiving keeps its own path; filing is a separate
|
|
179
|
+
// primitive that fires only when the new status is filed (and isn't an
|
|
180
|
+
// archive transition — archive wins by being earlier in the conditional).
|
|
181
|
+
const filedStatuses = config.lifecycle.filedStatuses ?? new Map();
|
|
182
|
+
const newFiledDir = filedStatuses.get(newStatus) ?? null;
|
|
183
|
+
const oldFiledDir = oldStatus ? (filedStatuses.get(oldStatus) ?? null) : null;
|
|
184
|
+
const currentBucket = relSegments.length > 1 ? relSegments[0] : null;
|
|
185
|
+
const isFiling = !isArchiving && !isUnarchiving && newFiledDir && currentBucket !== newFiledDir;
|
|
186
|
+
const isUnfiling = !isArchiving && !isUnarchiving && !newFiledDir && oldFiledDir && currentBucket === oldFiledDir;
|
|
174
187
|
let finalPath = filePath;
|
|
175
188
|
|
|
176
189
|
if (dryRun) {
|
|
@@ -186,7 +199,17 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
186
199
|
process.stdout.write(`${prefix} Would move: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
187
200
|
finalPath = targetPath;
|
|
188
201
|
}
|
|
189
|
-
if (
|
|
202
|
+
if (isFiling) {
|
|
203
|
+
const targetPath = path.join(fileRoot, newFiledDir, path.basename(filePath));
|
|
204
|
+
process.stdout.write(`${prefix} Would file: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
205
|
+
finalPath = targetPath;
|
|
206
|
+
}
|
|
207
|
+
if (isUnfiling) {
|
|
208
|
+
const targetPath = path.join(fileRoot, path.basename(filePath));
|
|
209
|
+
process.stdout.write(`${prefix} Would unfile: ${toRepoPath(filePath, config.repoRoot)} → ${toRepoPath(targetPath, config.repoRoot)}\n`);
|
|
210
|
+
finalPath = targetPath;
|
|
211
|
+
}
|
|
212
|
+
if ((isArchiving || isUnarchiving || isFiling || isUnfiling) && config.indexPath) {
|
|
190
213
|
process.stdout.write(`${prefix} Would regenerate index\n`);
|
|
191
214
|
}
|
|
192
215
|
process.stdout.write(`${prefix} ${toRepoPath(finalPath, config.repoRoot)}: ${oldStatus ?? 'unknown'} → ${newStatus}\n`);
|
|
@@ -212,6 +235,24 @@ export async function runStatus(argv, config, opts = {}) {
|
|
|
212
235
|
finalPath = targetPath;
|
|
213
236
|
}
|
|
214
237
|
|
|
238
|
+
if (isFiling) {
|
|
239
|
+
const targetDir = path.join(fileRoot, newFiledDir);
|
|
240
|
+
mkdirSync(targetDir, { recursive: true });
|
|
241
|
+
const targetPath = path.join(targetDir, path.basename(filePath));
|
|
242
|
+
if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
|
|
243
|
+
const result = gitMv(filePath, targetPath, config.repoRoot);
|
|
244
|
+
if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
|
|
245
|
+
finalPath = targetPath;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (isUnfiling) {
|
|
249
|
+
const targetPath = path.join(fileRoot, path.basename(filePath));
|
|
250
|
+
if (existsSync(targetPath)) { die(`Target already exists: ${toRepoPath(targetPath, config.repoRoot)}`); }
|
|
251
|
+
const result = gitMv(filePath, targetPath, config.repoRoot);
|
|
252
|
+
if (result.status !== 0) { die(result.stderr || 'git mv failed.'); }
|
|
253
|
+
finalPath = targetPath;
|
|
254
|
+
}
|
|
255
|
+
|
|
215
256
|
// Regen the index on every status change — `active → planned` etc. drift
|
|
216
257
|
// the per-status sections just as much as archive crossings. Archive paths
|
|
217
258
|
// also benefit (replaces the previously-gated regen). `--no-index` skips
|
package/src/validate.mjs
CHANGED
|
@@ -19,6 +19,11 @@ const BUILTIN_TYPE_DIR_NAMES = ['plans', 'prompts'];
|
|
|
19
19
|
function liveTypeDirsForRoots(config) {
|
|
20
20
|
const set = new Set();
|
|
21
21
|
const roots = config.docsRoots || (config.docsRoot ? [config.docsRoot] : []);
|
|
22
|
+
// F15: filed-status bucket dirs are "live" too — a doc whose status is
|
|
23
|
+
// an archive status but whose parent dir is a filed bucket should still
|
|
24
|
+
// trigger the archive-drift warning (the file needs to move into the
|
|
25
|
+
// archive bucket).
|
|
26
|
+
const filedDirs = [...((config.lifecycle?.filedStatuses?.values?.()) ?? [])];
|
|
22
27
|
for (const root of roots) {
|
|
23
28
|
const rootRel = path.relative(config.repoRoot, root).split(path.sep).join('/');
|
|
24
29
|
// The root itself is a live dir (covers flat-array layouts where the
|
|
@@ -40,6 +45,11 @@ function liveTypeDirsForRoots(config) {
|
|
|
40
45
|
set.add(rootRel ? `${rootRel}/${tmpl.dir}` : tmpl.dir);
|
|
41
46
|
}
|
|
42
47
|
}
|
|
48
|
+
// F15: filed bucket dirs joined to each root.
|
|
49
|
+
for (const dirName of filedDirs) {
|
|
50
|
+
if (path.basename(rootRel) === dirName) continue;
|
|
51
|
+
set.add(rootRel ? `${rootRel}/${dirName}` : dirName);
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
54
|
return set;
|
|
45
55
|
}
|