dotmd-cli 0.40.3 → 0.41.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/bin/dotmd.mjs CHANGED
@@ -53,6 +53,7 @@ Lifecycle:
53
53
  runlist <hub> [next] Show or walk an ordered group of plans (see \`dotmd help runlist\`)
54
54
  finish <file> [done|active] Finish a plan (set done or active)
55
55
  status <file> <status> Transition document status
56
+ set <status> [<file>] Unified transition: archive/release/transition in one verb
56
57
  archive <file> Archive (status + move + update refs)
57
58
  bulk archive <f1> <f2> ... Archive multiple files at once
58
59
  bulk-tag [files...] Tag pre-existing untagged .md files
@@ -306,6 +307,30 @@ Options:
306
307
 
307
308
  If no file is given, prompts with a list of in-session plans.`,
308
309
 
310
+ set: `dotmd set <status> [<file>] — unified status-transition verb
311
+
312
+ Routes to the right plumbing based on the target status:
313
+ - target is an archive status → archive the file (move + ref update)
314
+ - source is in-session → also releases the held lease
315
+ - everything else → plain frontmatter status bump
316
+
317
+ When <file> is omitted, dotmd infers it from the calling session's held
318
+ lease. With zero or multiple leases, you must pass <file> explicitly.
319
+
320
+ To acquire an in-session lease, use \`dotmd pickup <file>\` instead —
321
+ \`dotmd set in-session\` is refused so the asymmetric lease-acquisition
322
+ path is never skipped silently.
323
+
324
+ Options:
325
+ --no-index Skip index regen (see \`dotmd archive --help\`).
326
+ --show-files Append \`files: …\` footer.
327
+ --dry-run, -n Preview without writing.
328
+
329
+ Examples:
330
+ dotmd set partial # release current lease, mark partial
331
+ dotmd set archived docs/plans/x # archive a specific plan
332
+ dotmd set active # finish a held in-session plan`,
333
+
309
334
  status: `dotmd status <file> <new-status> — transition document status
310
335
 
311
336
  Moves the document to the new status. If transitioning to an archive
@@ -1137,6 +1162,7 @@ async function main() {
1137
1162
  if (command === 'handoff') { die('`dotmd handoff` was removed in 0.31.0. Use `dotmd prompts new <name>` to create a saved prompt instead. The .dotmd/handoffs/ sidecar mechanism no longer exists; see CHANGELOG.'); }
1138
1163
  if (command === 'finish') { const { runFinish } = await import('../src/lifecycle.mjs'); await runFinish(restArgs, config, { dryRun }); return; }
1139
1164
  if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
1165
+ if (command === 'set') { const { runSet } = await import('../src/lifecycle.mjs'); await runSet(restArgs, config, { dryRun }); return; }
1140
1166
  if (command === 'archive') { const { runArchive } = await import('../src/lifecycle.mjs'); runArchive(restArgs, config, { dryRun }); return; }
1141
1167
  if (command === 'bulk' && restArgs[0] === 'archive') { const { runBulkArchive } = await import('../src/lifecycle.mjs'); runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
1142
1168
  if (command === 'bulk' && restArgs[0] === 'tag') { const { runBulkTag } = await import('../src/bulk-tag.mjs'); runBulkTag(restArgs.slice(1), config, { dryRun }); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.40.3",
3
+ "version": "0.41.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",
@@ -74,7 +74,8 @@ function generatePlansCommand(config) {
74
74
  lines.push('- `dotmd prompts use <file>` — consume a specific prompt (prints body, auto-archives)');
75
75
  lines.push('- `dotmd archive <file>` — archive with auto ref-fixing (both directions)');
76
76
  lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
77
- lines.push('- `dotmd status <file> <status>` — transition status');
77
+ lines.push('- `dotmd set <status> [<file>]`unified transition (archive / release / status bump in one verb; infers path from held lease)');
78
+ lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
78
79
  lines.push('- `dotmd query --keyword <term>` — find plans by keyword');
79
80
 
80
81
  if (config.raw?.glossary) {
@@ -153,7 +154,8 @@ function generateDocsCommand(config) {
153
154
  lines.push('- `dotmd prompts new <name> "<body>"` — save a resume-prompt');
154
155
  lines.push('- `dotmd prompts next` — consume oldest pending prompt (prints body, auto-archives)');
155
156
  lines.push('- `dotmd prompts use <file>` — consume a specific prompt (prints body, auto-archives)');
156
- lines.push('- `dotmd status <file> <status>` — transition status');
157
+ lines.push('- `dotmd set <status> [<file>]`unified transition (archive / release / status bump; infers path from held lease)');
158
+ lines.push('- `dotmd status <file> <status>` — transition status (legacy; `set` is preferred)');
157
159
  lines.push('- `dotmd archive <file>` — archive with auto ref-fixing');
158
160
  lines.push('- `dotmd bulk archive <files>` — archive multiple at once');
159
161
  lines.push('- `dotmd touch --git` — bulk-sync updated dates from git history');
package/src/commands.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // templates points at a real command.
5
5
  export const KNOWN_COMMANDS = [
6
6
  'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'briefing', 'context', 'hud',
7
- 'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'finish', 'status', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
7
+ 'focus', 'query', 'plans', 'prompts', 'stale', 'actionable', 'index', 'pickup', 'release', 'finish', 'status', 'set', 'archive', 'bulk', 'bulk-tag', 'touch', 'doctor',
8
8
  'unblocks', 'health', 'glossary', 'modules', 'module',
9
9
  'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
10
10
  'watch', 'diff', 'new', 'init', 'completions', 'statuses', 'journal',
package/src/lifecycle.mjs CHANGED
@@ -680,6 +680,82 @@ export function runArchive(argv, config, opts = {}) {
680
680
  return { touched };
681
681
  }
682
682
 
683
+ // Unified status-transition verb. Collapses status/archive/release into one
684
+ // signature — `dotmd set <status> [<path>]` — and dispatches to the right
685
+ // plumbing based on the *target* status:
686
+ // - target in archiveStatuses (and file not already archived) → runArchive
687
+ // (gets us ref-fixing + auto lease release + closeout-template offer)
688
+ // - source = in-session, target != in-session → runStatus +
689
+ // auto-release of the held lease (so users don't have to chain `release`)
690
+ // - everything else (incl. unarchive, plain transitions) → runStatus
691
+ //
692
+ // Path is inferred from the calling session's held lease when omitted. With
693
+ // zero leases or >1 leases, we refuse and ask for explicit `<path>` instead
694
+ // of guessing.
695
+ //
696
+ // `dotmd set in-session <path>` is refused — acquiring a lease is asymmetric
697
+ // enough to deserve its own verb (`dotmd pickup`), and silently routing here
698
+ // would skip the lease-acquisition path entirely.
699
+ export async function runSet(argv, config, opts = {}) {
700
+ const { dryRun } = opts;
701
+ const noIndex = argv.includes('--no-index');
702
+ const showFiles = argv.includes('--show-files');
703
+ argv = argv.filter(a => a !== '--no-index' && a !== '--show-files');
704
+
705
+ const newStatus = argv[0];
706
+ let input = argv[1];
707
+
708
+ if (!newStatus) die('Usage: dotmd set <status> [<path>]');
709
+
710
+ if (newStatus === 'in-session') {
711
+ die('To acquire an in-session lease, use `dotmd pickup <file>`. `dotmd set` is for releasing/transitioning a held lease.');
712
+ }
713
+
714
+ if (!input) {
715
+ const leases = readLeases(config);
716
+ const sid = currentSessionId();
717
+ const owned = Object.entries(leases).filter(([_, l]) => l.session === sid);
718
+ if (owned.length === 0) {
719
+ die('No <path> given and no held lease to infer from.\nUsage: dotmd set <status> <path>');
720
+ }
721
+ if (owned.length > 1) {
722
+ const paths = owned.map(([p]) => ` - ${p}`).join('\n');
723
+ die(`No <path> given; you hold ${owned.length} leases:\n${paths}\nSpecify <path> explicitly.`);
724
+ }
725
+ input = owned[0][0];
726
+ }
727
+
728
+ const filePath = resolveDocPath(input, config);
729
+ if (!filePath) die(`File not found: ${input}\nSearched: ${toRepoPath(config.repoRoot, config.repoRoot) || '.'}, ${toRepoPath(config.docsRoot, config.repoRoot)}`);
730
+
731
+ const raw = readFileSync(filePath, 'utf8');
732
+ const { frontmatter: fmRaw } = extractFrontmatter(raw);
733
+ const parsedFm = parseSimpleFrontmatter(fmRaw);
734
+ const oldStatus = asString(parsedFm.status);
735
+
736
+ const fileRoot = findFileRoot(filePath, config);
737
+ const relFromRoot = path.relative(fileRoot, filePath);
738
+ const inArchive = relFromRoot.startsWith(config.archiveDir + '/') || relFromRoot.startsWith(config.archiveDir + path.sep);
739
+
740
+ if (config.lifecycle.archiveStatuses.has(newStatus) && !inArchive) {
741
+ const archiveArgs = [filePath];
742
+ if (noIndex) archiveArgs.push('--no-index');
743
+ if (showFiles) archiveArgs.push('--show-files');
744
+ return runArchive(archiveArgs, config, { dryRun });
745
+ }
746
+
747
+ const statusArgs = [filePath, newStatus];
748
+ if (noIndex) statusArgs.push('--no-index');
749
+ if (showFiles) statusArgs.push('--show-files');
750
+ await runStatus(statusArgs, config, { dryRun });
751
+
752
+ if (oldStatus === 'in-session' && newStatus !== 'in-session' && !dryRun) {
753
+ const repoPath = toRepoPath(filePath, config.repoRoot);
754
+ try { releaseLease(config, repoPath, { force: false }); }
755
+ catch (err) { warn(`Could not release lease for ${repoPath}: ${err.message}`); }
756
+ }
757
+ }
758
+
683
759
  export function runBulkArchive(argv, config, opts = {}) {
684
760
  const { dryRun } = opts;
685
761
  const noIndex = argv.includes('--no-index') || opts.noIndex;