dotmd-cli 0.72.0 → 0.72.1
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/init.mjs +112 -7
- package/src/prompts.mjs +12 -4
package/package.json
CHANGED
package/src/init.mjs
CHANGED
|
@@ -67,6 +67,55 @@ export const referenceFields = {
|
|
|
67
67
|
};
|
|
68
68
|
`;
|
|
69
69
|
|
|
70
|
+
// A git repository cannot hold an empty directory, so scaffolding `docs/plans/`
|
|
71
|
+
// and `docs/prompts/` and stopping there means both vanish for the next clone —
|
|
72
|
+
// `dotmd init` produced three committable files and two directories that existed
|
|
73
|
+
// only on the machine that ran it.
|
|
74
|
+
//
|
|
75
|
+
// plans/ gets a real sample rather than a keepfile: it is the one place a new user
|
|
76
|
+
// benefits from seeing the frontmatter shape before running `dotmd new`. It is
|
|
77
|
+
// written `status: planned` so it sits quietly in the pipeline instead of posing
|
|
78
|
+
// as live work, and it says how to delete itself.
|
|
79
|
+
//
|
|
80
|
+
// prompts/ gets a keepfile instead, and cannot get a sample: the live queue is
|
|
81
|
+
// gitignored by the rule this same command writes, and a committed prompt at
|
|
82
|
+
// `status: pending` would be silently consumed by the next no-arg `dotmd use`.
|
|
83
|
+
const SAMPLE_PLAN_NAME = 'example-plan.md';
|
|
84
|
+
const samplePlan = (today) => `---
|
|
85
|
+
type: plan
|
|
86
|
+
status: planned
|
|
87
|
+
created: ${today}
|
|
88
|
+
updated: ${today}
|
|
89
|
+
title: Example Plan
|
|
90
|
+
summary: A scaffolded sample showing what a dotmd plan looks like — safe to delete.
|
|
91
|
+
current_state: "Scaffolded by \`dotmd init\` as a shape reference. Nothing here is real work."
|
|
92
|
+
next_step: "Delete this file, or edit it into your first real plan."
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
# Example Plan
|
|
96
|
+
|
|
97
|
+
> A scaffolded sample showing what a dotmd plan looks like — safe to delete.
|
|
98
|
+
|
|
99
|
+
## Problem
|
|
100
|
+
|
|
101
|
+
\`dotmd init\` leaves this file behind so \`docs/plans/\` survives a clone (git cannot
|
|
102
|
+
track an empty directory) and so the frontmatter above has something to point at.
|
|
103
|
+
|
|
104
|
+
The fields that matter: \`status\` drives every listing, \`current_state\` and
|
|
105
|
+
\`next_step\` are what \`dotmd briefing\` reads out, and \`updated\` drives staleness.
|
|
106
|
+
Never hand-edit \`status:\` — \`dotmd set <status> <file>\` writes it, validates it
|
|
107
|
+
against the type, and runs the lifecycle hooks.
|
|
108
|
+
|
|
109
|
+
## Phases
|
|
110
|
+
|
|
111
|
+
- [ ] Delete this file: \`dotmd archive docs/plans/${SAMPLE_PLAN_NAME}\`
|
|
112
|
+
- [ ] Write a real one: \`dotmd new plan <name>\`
|
|
113
|
+
|
|
114
|
+
## Version History
|
|
115
|
+
|
|
116
|
+
- Scaffolded by \`dotmd init\`.
|
|
117
|
+
`;
|
|
118
|
+
|
|
70
119
|
const STARTER_INDEX = `# Docs
|
|
71
120
|
|
|
72
121
|
<!-- GENERATED:dotmd:start -->
|
|
@@ -290,6 +339,21 @@ export async function runInit(cwd, config, opts = {}) {
|
|
|
290
339
|
if (!dryRun) mkdirSync(subPath, { recursive: true });
|
|
291
340
|
process.stdout.write(` ${dryTag}${green('create')} docs/${sub}/\n`);
|
|
292
341
|
}
|
|
342
|
+
|
|
343
|
+
// Give the directory something git can carry — but only when it is genuinely
|
|
344
|
+
// empty. Re-running init over a populated tree must never drop a sample plan
|
|
345
|
+
// into someone's real estate.
|
|
346
|
+
const keeper = sub === 'plans'
|
|
347
|
+
? { file: SAMPLE_PLAN_NAME, body: samplePlan(new Date().toISOString().slice(0, 10)) }
|
|
348
|
+
: { file: '.gitkeep', body: '' };
|
|
349
|
+
const keeperPath = path.join(subPath, keeper.file);
|
|
350
|
+
const dirIsEmpty = !existsSync(subPath) || readdirSync(subPath).length === 0;
|
|
351
|
+
if (existsSync(keeperPath)) {
|
|
352
|
+
process.stdout.write(` ${dryTag}${dim('exists')} docs/${sub}/${keeper.file}\n`);
|
|
353
|
+
} else if (dirIsEmpty || dryRun) {
|
|
354
|
+
if (!dryRun) writeFileSync(keeperPath, keeper.body, 'utf8');
|
|
355
|
+
process.stdout.write(` ${dryTag}${green('create')} docs/${sub}/${keeper.file}\n`);
|
|
356
|
+
}
|
|
293
357
|
}
|
|
294
358
|
|
|
295
359
|
if (existsSync(indexPath)) {
|
|
@@ -313,21 +377,38 @@ export async function runInit(cwd, config, opts = {}) {
|
|
|
313
377
|
process.stdout.write(` export const root = [${subs.map(s => `'${s}'`).join(', ')}];\n`);
|
|
314
378
|
}
|
|
315
379
|
|
|
316
|
-
// .gitignore:
|
|
380
|
+
// .gitignore: two rules.
|
|
381
|
+
//
|
|
382
|
+
// .dotmd/ — session ownership records
|
|
383
|
+
// <docs>/prompts/*.md — the LIVE saved-prompt queue
|
|
384
|
+
//
|
|
385
|
+
// The second one is load-bearing. Saved prompts are session-local by design and
|
|
386
|
+
// the workflow docs say never to commit them, but nothing enforced that: a plain
|
|
387
|
+
// `git add -A` from any session swept the queue into the repo. Scoped with a
|
|
388
|
+
// single `*` so it stops at the directory — `prompts/archived/` is the committed
|
|
389
|
+
// historical record and must stay tracked. Anchored with a leading `/` so a
|
|
390
|
+
// `prompts/` directory elsewhere in the tree is unaffected.
|
|
317
391
|
const gitignorePath = path.join(cwd, '.gitignore');
|
|
318
|
-
const
|
|
392
|
+
const docsRel = path.relative(cwd, docsDir).split(path.sep).join('/');
|
|
393
|
+
const promptsIgnore = `/${docsRel ? `${docsRel}/` : ''}prompts/*.md`;
|
|
394
|
+
const ignoreRules = [
|
|
395
|
+
{ line: '.dotmd/', accepts: (l) => l === '.dotmd/' || l === '.dotmd' },
|
|
396
|
+
{ line: promptsIgnore, accepts: (l) => l === promptsIgnore || l === promptsIgnore.slice(1) },
|
|
397
|
+
];
|
|
319
398
|
if (existsSync(gitignorePath)) {
|
|
320
399
|
const current = readFileSync(gitignorePath, 'utf8');
|
|
321
|
-
const
|
|
322
|
-
|
|
400
|
+
const present = new Set(current.split('\n').map(l => l.trim()));
|
|
401
|
+
const missing = ignoreRules.filter(r => ![...present].some(l => r.accepts(l)));
|
|
402
|
+
if (missing.length) {
|
|
323
403
|
const sep = current.endsWith('\n') ? '' : '\n';
|
|
324
|
-
|
|
325
|
-
|
|
404
|
+
const added = missing.map(r => `${r.line}\n`).join('');
|
|
405
|
+
if (!dryRun) writeFileSync(gitignorePath, `${current}${sep}${added}`, 'utf8');
|
|
406
|
+
process.stdout.write(` ${dryTag}${green('update')} .gitignore (+${missing.map(r => r.line).join(', ')})\n`);
|
|
326
407
|
} else {
|
|
327
408
|
process.stdout.write(` ${dryTag}${dim('exists')} .gitignore\n`);
|
|
328
409
|
}
|
|
329
410
|
} else {
|
|
330
|
-
if (!dryRun) writeFileSync(gitignorePath, `${
|
|
411
|
+
if (!dryRun) writeFileSync(gitignorePath, ignoreRules.map(r => `${r.line}\n`).join(''), 'utf8');
|
|
331
412
|
process.stdout.write(` ${dryTag}${green('create')} .gitignore\n`);
|
|
332
413
|
}
|
|
333
414
|
|
|
@@ -397,6 +478,30 @@ export async function runInit(cwd, config, opts = {}) {
|
|
|
397
478
|
}
|
|
398
479
|
}
|
|
399
480
|
|
|
481
|
+
// Render the index against what we just scaffolded.
|
|
482
|
+
//
|
|
483
|
+
// `docs.md` ships a "no docs yet" placeholder, which stopped being true the
|
|
484
|
+
// moment this command also started writing a sample plan — so a fresh init
|
|
485
|
+
// committed an index that contradicted the tree beside it, and the user's first
|
|
486
|
+
// `dotmd check` opened with a stale-index warning. The config passed into
|
|
487
|
+
// runInit predates the file we just wrote, so re-resolve before rendering.
|
|
488
|
+
//
|
|
489
|
+
// Best-effort: a repo that scaffolds correctly but cannot render its index is
|
|
490
|
+
// still a successful init, and the next command regenerates it anyway.
|
|
491
|
+
if (!dryRun) {
|
|
492
|
+
try {
|
|
493
|
+
const { resolveConfig } = await import('./config.mjs');
|
|
494
|
+
const freshConfig = await resolveConfig(cwd);
|
|
495
|
+
if (freshConfig.indexPath) {
|
|
496
|
+
const { buildIndex } = await import('./index.mjs');
|
|
497
|
+
const { writeRenderedIndex } = await import('./index-file.mjs');
|
|
498
|
+
writeRenderedIndex(() => buildIndex(freshConfig, { fast: true }), freshConfig);
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
// Leave the placeholder; `dotmd check` self-heals it on first run.
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
400
505
|
process.stdout.write(`\nReady. A few starting points:\n`);
|
|
401
506
|
process.stdout.write(` dotmd new doc my-doc # scaffold a reference doc\n`);
|
|
402
507
|
process.stdout.write(` dotmd new plan my-plan # scaffold an execution plan\n`);
|
package/src/prompts.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
|
|
4
|
-
import { asString, toRepoPath, die, resolveDocPath, isArchivedPath } from './util.mjs';
|
|
4
|
+
import { asString, toRepoPath, die, resolveDocPath, resolveRefPath, isArchivedPath } from './util.mjs';
|
|
5
5
|
import { buildIndex, resolveDocArg } from './index.mjs';
|
|
6
6
|
import { runQuery } from './query.mjs';
|
|
7
7
|
import { completePlanClaim, regenIndex, renderLifecycleMutation, runArchive, runStatus } from './lifecycle.mjs';
|
|
@@ -258,7 +258,7 @@ export async function consumePrompt(filePath, config, opts) {
|
|
|
258
258
|
|
|
259
259
|
const planRef = asString(parsed.plan);
|
|
260
260
|
let linkedClaim = null;
|
|
261
|
-
if (planRef) linkedClaim = prepareLinkedPromptClaim(planRef, config);
|
|
261
|
+
if (planRef) linkedClaim = prepareLinkedPromptClaim(planRef, config, path.dirname(filePath));
|
|
262
262
|
|
|
263
263
|
if (dryRun) {
|
|
264
264
|
const prefix = dim('[dry-run]');
|
|
@@ -353,8 +353,16 @@ export async function writeConsumedBody(body, archivedPath, write = null, linked
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
// `planRef` is a reference written in the prompt's own frontmatter, so it obeys
|
|
357
|
+
// the same convention every other ref field does: doc-relative first (baton's
|
|
358
|
+
// `../plans/x.md` from `docs/prompts/`), repo-root-relative second. Resolving it
|
|
359
|
+
// with `resolveDocPath` alone read only the repo-root form, so a doc-relative
|
|
360
|
+
// link — the form nothing validates, since `plan` is not a `referenceFields`
|
|
361
|
+
// entry — died as "missing" while pointing at a file that was plainly there.
|
|
362
|
+
function prepareLinkedPromptClaim(planRef, config, promptDir) {
|
|
363
|
+
let planPath = resolveRefPath(planRef, promptDir, config.repoRoot)
|
|
364
|
+
?? resolveDocPath(planRef, config)
|
|
365
|
+
?? resolveDocArg(planRef, config, { dieOnMiss: false });
|
|
358
366
|
if (!planPath || !existsSync(planPath)) die(`Linked plan is missing; prompt was not consumed: ${planRef}`);
|
|
359
367
|
planPath = authorizeManagedSource(planPath, config, { kind: 'Prompt linked plan source' }).path;
|
|
360
368
|
const raw = readFileSync(planPath, 'utf8');
|