dotmd-cli 0.45.1 → 0.45.2
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/lifecycle.mjs +29 -1
- package/src/prompts.mjs +6 -2
- package/src/query.mjs +5 -4
- package/src/util.mjs +10 -0
- package/src/validate.mjs +21 -0
package/package.json
CHANGED
package/src/lifecycle.mjs
CHANGED
|
@@ -552,12 +552,40 @@ export function runArchive(argv, config, opts = {}) {
|
|
|
552
552
|
|
|
553
553
|
const archiveFileRoot = findFileRoot(filePath, config);
|
|
554
554
|
const relFromRoot = path.relative(archiveFileRoot, filePath);
|
|
555
|
-
|
|
555
|
+
// Segment-membership covers both single-root (`<root>/archived/foo.md`) and
|
|
556
|
+
// multi-root (`<type-root>/archived/foo.md`) layouts. The older
|
|
557
|
+
// startsWith-only check missed nested cases where archived/ wasn't the first
|
|
558
|
+
// segment under the resolved root.
|
|
559
|
+
const inArchiveDir = relFromRoot.split(path.sep).includes(config.archiveDir);
|
|
556
560
|
|
|
557
561
|
const raw = readFileSync(filePath, 'utf8');
|
|
558
562
|
const { frontmatter, body } = extractFrontmatter(raw);
|
|
559
563
|
const parsed = parseSimpleFrontmatter(frontmatter);
|
|
560
564
|
const oldStatus = asString(parsed.status) ?? 'unknown';
|
|
565
|
+
|
|
566
|
+
// Heal stuck frontmatter (issue #13): file is under archiveDir/ but its
|
|
567
|
+
// status hasn't been flipped. Flip in place; don't try to move (it's already
|
|
568
|
+
// archived on disk) and don't refuse — refusal leaves the drift permanent.
|
|
569
|
+
if (inArchiveDir) {
|
|
570
|
+
if (oldStatus === 'archived') {
|
|
571
|
+
die(`Already archived: ${toRepoPath(filePath, config.repoRoot)}`);
|
|
572
|
+
}
|
|
573
|
+
const today = nowIso();
|
|
574
|
+
const repoPathHeal = toRepoPath(filePath, config.repoRoot);
|
|
575
|
+
if (dryRun) {
|
|
576
|
+
const prefix = dim('[dry-run]');
|
|
577
|
+
out.write(`${prefix} Would heal frontmatter in place: status: ${oldStatus} → archived, updated: ${today}\n`);
|
|
578
|
+
out.write(`${prefix} Would skip git mv (file already under \`${config.archiveDir}/\`)\n`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
updateFrontmatter(filePath, { status: 'archived', updated: today });
|
|
582
|
+
appendVersionHistory(filePath, `Archived (frontmatter healed in place from \`${oldStatus}\`).`);
|
|
583
|
+
if (!noIndex) regenIndex(config);
|
|
584
|
+
out.write(`${green('✓ Healed')}: ${repoPathHeal} (${oldStatus} → archived; file already under \`${config.archiveDir}/\`)\n`);
|
|
585
|
+
if (showFiles) emitFilesFooter([repoPathHeal, config.indexPath], config);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
561
589
|
const closeoutAction = closeoutTemplate ? planCloseoutInjection(body) : null;
|
|
562
590
|
|
|
563
591
|
const today = nowIso();
|
package/src/prompts.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
|
-
import { asString, toRepoPath, die, resolveDocPath } from './util.mjs';
|
|
4
|
+
import { asString, toRepoPath, die, resolveDocPath, isArchivedPath } from './util.mjs';
|
|
5
5
|
import { buildIndex } from './index.mjs';
|
|
6
6
|
import { runQuery } from './query.mjs';
|
|
7
7
|
import { runArchive, runStatus } from './lifecycle.mjs';
|
|
@@ -120,7 +120,11 @@ function renderPromptsVerbose(index, config, { hasStatusFlag, includeArchived })
|
|
|
120
120
|
|
|
121
121
|
export function pendingPromptsOldestFirst(config) {
|
|
122
122
|
const index = buildIndex(config);
|
|
123
|
-
const prompts = index.docs.filter(d =>
|
|
123
|
+
const prompts = index.docs.filter(d =>
|
|
124
|
+
d.type === 'prompt'
|
|
125
|
+
&& d.status === 'pending'
|
|
126
|
+
&& !isArchivedPath(d.path, config),
|
|
127
|
+
);
|
|
124
128
|
|
|
125
129
|
return prompts
|
|
126
130
|
.map(d => {
|
package/src/query.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { capitalize, toSlug, truncate, warn, suggestCandidates } from './util.mjs';
|
|
3
|
+
import { capitalize, toSlug, truncate, warn, suggestCandidates, isArchivedPath } from './util.mjs';
|
|
4
4
|
import { renderProgressBar, formatCurrentState } from './render.mjs';
|
|
5
5
|
import { computeDaysSinceUpdate, computeIsStale } from './validate.mjs';
|
|
6
6
|
import { getGitLastModifiedBatch } from './git.mjs';
|
|
@@ -176,14 +176,15 @@ export function filterDocs(docs, filters, config) {
|
|
|
176
176
|
|
|
177
177
|
if (filters.types?.length) result = result.filter(d => filters.types.includes(d.type));
|
|
178
178
|
if (filters.statuses?.length) result = result.filter(d => filters.statuses.includes(d.status));
|
|
179
|
-
// --exclude-archived strips terminal/archive statuses
|
|
180
|
-
//
|
|
179
|
+
// --exclude-archived strips terminal/archive statuses AND any file physically
|
|
180
|
+
// located under `archiveDir/` (issue #13: status can drift out of sync with
|
|
181
|
+
// the file's directory; the path is the source of truth for "is archived").
|
|
181
182
|
if (filters.excludeArchived && !filters.includeArchived) {
|
|
182
183
|
const archived = new Set([
|
|
183
184
|
...(config.lifecycle?.archiveStatuses ?? []),
|
|
184
185
|
...(config.lifecycle?.terminalStatuses ?? []),
|
|
185
186
|
]);
|
|
186
|
-
result = result.filter(d => !archived.has(d.status));
|
|
187
|
+
result = result.filter(d => !archived.has(d.status) && !isArchivedPath(d.path, config));
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
if (filters.keyword) {
|
package/src/util.mjs
CHANGED
|
@@ -55,6 +55,16 @@ export function toRepoPath(absolutePath, repoRoot) {
|
|
|
55
55
|
return path.relative(repoRoot, absolutePath).split(path.sep).join('/');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// True when any path segment equals `config.archiveDir`. Covers both
|
|
59
|
+
// `docs/plans/archived/foo.md` and `docs/prompts/archived/foo.md` regardless
|
|
60
|
+
// of whether the layout is single-root or flat-array. Read-side commands use
|
|
61
|
+
// this to skip files whose frontmatter `status:` has drifted out of sync with
|
|
62
|
+
// their archive location (issue #13).
|
|
63
|
+
export function isArchivedPath(repoPath, config) {
|
|
64
|
+
if (!repoPath || !config?.archiveDir) return false;
|
|
65
|
+
return repoPath.split('/').includes(config.archiveDir);
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
// Emit a `files: a b c` line to stderr listing every doc / index path
|
|
59
69
|
// the command touched (deduped, sorted, repo-relative). Lets agents do
|
|
60
70
|
// `git add` with the exact set instead of guessing. Opt-in via
|
package/src/validate.mjs
CHANGED
|
@@ -218,6 +218,27 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
// Inverse archive drift (issue #13): a doc whose path is under
|
|
222
|
+
// `<dir>/<archiveDir>/` but whose status is NOT an archive status. Without
|
|
223
|
+
// this check, the file is invisible to default queries (the path is below
|
|
224
|
+
// an archive bucket so it's filtered out by `--exclude-archived`), yet its
|
|
225
|
+
// `status: pending` (or similar) still makes it surface in pending-prompt
|
|
226
|
+
// scans. The heal is to flip the frontmatter via `dotmd <type-or-set>
|
|
227
|
+
// archive`, which now restores in place rather than failing.
|
|
228
|
+
if (doc.status && !config.lifecycle.archiveStatuses.has(doc.status)) {
|
|
229
|
+
const parentSegments = path.dirname(doc.path).split('/');
|
|
230
|
+
if (parentSegments.includes(config.archiveDir)) {
|
|
231
|
+
const heal = doc.type === 'prompt'
|
|
232
|
+
? `dotmd prompts archive ${doc.path}`
|
|
233
|
+
: `dotmd set archived ${doc.path}`;
|
|
234
|
+
doc.errors.push({
|
|
235
|
+
path: doc.path,
|
|
236
|
+
level: 'error',
|
|
237
|
+
message: `File is under \`${config.archiveDir}/\` but \`status: ${doc.status}\` is not an archive status. Run \`${heal}\` to heal the frontmatter in place, or move the file out of the archive directory.`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
221
242
|
// Validate reference fields resolve to existing files. Terminal statuses
|
|
222
243
|
// (archived, deprecated, etc.) document historical state — their refs may
|
|
223
244
|
// legitimately point at moved/deleted targets and shouldn't gate the
|