dotmd-cli 0.87.2 → 0.89.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/src/init.mjs CHANGED
@@ -53,8 +53,8 @@ export const root = 'docs';
53
53
 
54
54
  export const index = {
55
55
  path: 'docs/docs.md',
56
- startMarker: '<!-- GENERATED:dotmd:start -->',
57
- endMarker: '<!-- GENERATED:dotmd:end -->',
56
+ startMarker: '<!-- GENERATED:runlist:start -->',
57
+ endMarker: '<!-- GENERATED:runlist:end -->',
58
58
  archivedLimit: 8,
59
59
  };
60
60
 
@@ -118,11 +118,11 @@ against the type, and runs the lifecycle hooks.
118
118
 
119
119
  const STARTER_INDEX = `# Docs
120
120
 
121
- <!-- GENERATED:dotmd:start -->
121
+ <!-- GENERATED:runlist:start -->
122
122
 
123
123
  _No docs yet. Run \`runlist list\` after creating your first document._
124
124
 
125
- <!-- GENERATED:dotmd:end -->
125
+ <!-- GENERATED:runlist:end -->
126
126
  `;
127
127
 
128
128
  function scanExistingDocs(dir) {
@@ -263,8 +263,8 @@ function generateDetectedConfig(scan, rootPath) {
263
263
 
264
264
  lines.push('export const index = {');
265
265
  lines.push(` path: '${rootPath}/docs.md',`);
266
- lines.push(` startMarker: '<!-- GENERATED:dotmd:start -->',`);
267
- lines.push(` endMarker: '<!-- GENERATED:dotmd:end -->',`);
266
+ lines.push(` startMarker: '<!-- GENERATED:runlist:start -->',`);
267
+ lines.push(` endMarker: '<!-- GENERATED:runlist:end -->',`);
268
268
  lines.push(` snapshot: 'status',`);
269
269
  lines.push('};');
270
270
  lines.push('');
package/src/install.mjs CHANGED
@@ -9,11 +9,12 @@ import {
9
9
  } from './host-integration.mjs';
10
10
  import { readInstalledPlugin } from './update.mjs';
11
11
  import { executableName, which } from './util.mjs';
12
+ import { codexStatus, installCodexPlugin } from './codex-integration.mjs';
12
13
 
13
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
15
  const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
15
16
 
16
- const HOSTS = ['claude', 'opencode'];
17
+ const HOSTS = ['claude', 'codex', 'opencode'];
17
18
  const ALIASES = { 'claude-code': 'claude', claudecode: 'claude' };
18
19
 
19
20
  function flagValue(argv, name) {
@@ -33,6 +34,7 @@ function hostStates() {
33
34
  marketplaceRegistered: plugin ? plugin.marketplaceRegistered : null,
34
35
  },
35
36
  opencode: { ...opencodeStatus({ version: pkg.version }), detected: opencodeDetected() },
37
+ codex: codexStatus({ version: pkg.version }),
36
38
  };
37
39
  }
38
40
 
@@ -42,7 +44,7 @@ function reportStatus(json) {
42
44
  process.stdout.write(JSON.stringify({ cli: pkg.version, hosts: states }, null, 2) + '\n');
43
45
  return;
44
46
  }
45
- const { claude, opencode } = states;
47
+ const { claude, codex, opencode } = states;
46
48
  process.stdout.write(`${bold('runlist host integrations')} ${dim(`CLI ${pkg.version}`)}\n\n`);
47
49
 
48
50
  const claudeBroken = claude.installed && claude.marketplaceRegistered === false;
@@ -52,6 +54,13 @@ function reportStatus(json) {
52
54
  process.stdout.write(` claude ${claudeState}\n`);
53
55
  process.stdout.write(` ${dim(claude.installed ? claude.id : 'plugin: SessionStart primer, PreToolUse guard, workflow skill')}\n`);
54
56
 
57
+ const codexState = codex.foreign ? yellow('unmanaged directory present')
58
+ : !codex.exists ? yellow('not installed')
59
+ : codex.stale ? yellow(`${codex.version} — behind CLI ${pkg.version}`)
60
+ : green(codex.version);
61
+ process.stdout.write(` codex ${codexState}\n`);
62
+ process.stdout.write(` ${dim(codex.plugin)}\n`);
63
+
55
64
  const ocState = opencode.foreign ? yellow('unmanaged file present')
56
65
  : !opencode.exists ? yellow('not installed')
57
66
  : opencode.stale ? yellow(`${opencode.version} — behind CLI ${pkg.version}`)
@@ -61,6 +70,7 @@ function reportStatus(json) {
61
70
 
62
71
  const todo = [];
63
72
  if (!claude.installed || claudeBroken) todo.push('runlist install claude');
73
+ if (!codex.exists || codex.stale) todo.push('runlist install codex');
64
74
  if (!opencode.exists || opencode.stale) todo.push('runlist install opencode');
65
75
  if (todo.length) {
66
76
  process.stdout.write('\n');
@@ -73,6 +83,43 @@ function reportStatus(json) {
73
83
  }
74
84
  }
75
85
 
86
+ function installCodex(argv, dryRun, json) {
87
+ if (argv.includes('--remove')) {
88
+ process.stderr.write('Codex removal is managed by `codex plugin remove`; the personal marketplace source is left available.\n');
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ const result = installCodexPlugin({ version: pkg.version, dryRun });
93
+ if (result.action === 'refused') {
94
+ if (json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
95
+ else process.stderr.write(`refused: ${result.reason}\n`);
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ if (!json) process.stdout.write(`${dryRun ? '[dry-run] ' : ''}${result.action} codex integration (${pkg.version}) at ${result.plugin}\n`);
100
+ if (dryRun) {
101
+ if (json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
102
+ return;
103
+ }
104
+ const selector = `runlist-codex@${result.marketplaceName}`;
105
+ if (!which('codex')) {
106
+ if (json) process.stdout.write(JSON.stringify({ ...result, pluginAdd: 'codex-cli-unavailable' }, null, 2) + '\n');
107
+ else process.stdout.write(`Install in Codex with: codex plugin add ${selector}\n`);
108
+ return;
109
+ }
110
+ const added = spawnSync(executableName('codex'), ['plugin', 'add', selector], {
111
+ stdio: json ? 'pipe' : 'inherit', encoding: json ? 'utf8' : undefined,
112
+ shell: process.platform === 'win32',
113
+ });
114
+ if (json) process.stdout.write(JSON.stringify({ ...result, pluginAdd: added.status === 0 ? 'installed' : 'failed', pluginAddExit: added.status }, null, 2) + '\n');
115
+ if (added.status !== 0) {
116
+ process.stderr.write(`Codex plugin source is ready; finish installation with: codex plugin add ${selector}\n`);
117
+ process.exitCode = 1;
118
+ } else if (!json) {
119
+ process.stdout.write('Start a new Codex thread and review/trust the plugin hooks to enable the primer and guard.\n');
120
+ }
121
+ }
122
+
76
123
  function installClaude(argv, dryRun, json) {
77
124
  const remove = argv.includes('--remove');
78
125
  const steps = planClaudeInstall({ installed: readInstalledPlugin(), hasClaude: which('claude'), remove });
@@ -164,5 +211,6 @@ export function runInstall(argv, _config, opts = {}) {
164
211
 
165
212
  const dryRun = Boolean(opts.dryRun);
166
213
  if (host === 'claude') { installClaude(argv, dryRun, json); return; }
214
+ if (host === 'codex') { installCodex(argv, dryRun, json); return; }
167
215
  installOpencode(argv, dryRun, json);
168
216
  }
package/src/lifecycle.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { extractFrontmatter, parseSimpleFrontmatter, normalizeEol } from './frontmatter.mjs';
4
- import { asString, toRepoPath, die, warn, resolveDocPath, escapeRegex, nowIso, suggestCandidates, emitFilesFooter, isArchivedPath, currentSessionId } from './util.mjs';
4
+ import { asString, toRepoPath, die, warn, resolveDocPath, resolveRefPath, escapeRegex, nowIso, suggestCandidates, emitFilesFooter, isArchivedPath, currentSessionId } from './util.mjs';
5
5
  import { readJournalEntries } from './journal.mjs';
6
6
  import { captureGitIndexGeneration, getGitLastModifiedBatch, getGitLastSubstantiveModifiedBatch, isTracked } from './git.mjs';
7
7
  import { buildIndex, collectDocFiles, resolveDocArg } from './index.mjs';
@@ -78,6 +78,22 @@ function commitLifecycleMutation(filePath, targetPath, config, updates, historyF
78
78
  };
79
79
  };
80
80
  if (targetPath) {
81
+ // A companion prompt created or refreshed by baton links this plan before
82
+ // its status transition decides whether to file it elsewhere. Stamp the
83
+ // destination into that companion before publishing any transaction path.
84
+ const newRepoPath = toRepoPath(targetPath, config.repoRoot);
85
+ const retargetPrompt = item => {
86
+ if (typeof item.content !== 'string') return item;
87
+ const { frontmatter, body } = extractFrontmatter(item.content);
88
+ if (!frontmatter) return item;
89
+ const fm = parseSimpleFrontmatter(frontmatter);
90
+ const linked = asString(fm.plan);
91
+ const resolved = linked ? resolveRefPath(linked, path.dirname(item.path), config.repoRoot) : null;
92
+ if (asString(fm.type) !== 'prompt' || !resolved || path.resolve(resolved) !== path.resolve(filePath)) return item;
93
+ const rewritten = frontmatter.replace(/^plan:[ \t]*\S.*$/m, `plan: ${newRepoPath}`);
94
+ return { ...item, content: `---\n${rewritten}\n---\n${body}` };
95
+ };
96
+ const creations = (options.creations ?? []).map(retargetPrompt);
81
97
  let result;
82
98
  const tracked = isTracked(filePath, config.repoRoot);
83
99
  const gitIndex = tracked ? captureGitIndexGeneration(config.repoRoot) : null;
@@ -89,8 +105,8 @@ function commitLifecycleMutation(filePath, targetPath, config, updates, historyF
89
105
  const identities = createReferenceIdentitySet([filePath, ...allFiles]);
90
106
  const referenceFields = configuredReferenceFields(config);
91
107
  const additionalUpdates = options.skipInboundRefs
92
- ? (options.additionalUpdates ?? [])
93
- : (options.additionalUpdates ?? []).map(item => ({
108
+ ? (options.additionalUpdates ?? []).map(retargetPrompt)
109
+ : (options.additionalUpdates ?? []).map(item => retargetPrompt({
94
110
  ...item,
95
111
  content: item.content === undefined ? undefined : rewriteDocumentReferences(item.content, {
96
112
  sourcePath: item.path, repoRoot: config.repoRoot, identities, oldPath: filePath, newPath: targetPath, referenceFields,
@@ -108,7 +124,7 @@ function commitLifecycleMutation(filePath, targetPath, config, updates, historyF
108
124
  sourcePath: docFile, repoRoot: config.repoRoot, identities, oldPath: filePath, newPath: targetPath, referenceFields,
109
125
  }),
110
126
  })), ...additionalUpdates],
111
- creations: options.creations ?? [],
127
+ creations,
112
128
  guards: options.guards ?? [],
113
129
  gitMove: tracked,
114
130
  gitIndex,
@@ -506,7 +522,7 @@ export async function runStatus(argv, config, opts = {}) {
506
522
  process.stdout.write(`${prefix} Would append Version History: - **${today}** Status: ${oldStatus ?? 'unknown'} → ${newStatus} — ${note}\n`);
507
523
  }
508
524
  process.stdout.write(`${prefix} ${toRepoPath(finalPath, config.repoRoot)}: ${oldStatus ?? 'unknown'} → ${newStatus}\n`);
509
- return;
525
+ return { dryRun: true, oldRepoPath: toRepoPath(filePath, config.repoRoot), newRepoPath: toRepoPath(finalPath, config.repoRoot), touched: [] };
510
526
  }
511
527
 
512
528
  const mutationResult = commitLifecycleMutation(filePath, targetPath, config, { status: newStatus, updated: today }, currentOld => {
@@ -516,6 +532,7 @@ export async function runStatus(argv, config, opts = {}) {
516
532
  createSection: Boolean(note),
517
533
  additionalUpdates: opts.additionalUpdates,
518
534
  creations: opts.creations,
535
+ guards: opts.guards,
519
536
  testHooks: opts.testHooks,
520
537
  });
521
538
 
@@ -845,7 +862,7 @@ export function runArchive(argv, config, opts = {}) {
845
862
  if (config.hooks?.onArchive) {
846
863
  out.write(`${prefix} Would fire hook: onArchive\n`);
847
864
  }
848
- return;
865
+ return { dryRun: true, oldRepoPath, newRepoPath, touched: [] };
849
866
  }
850
867
 
851
868
  const mutationResult = commitLifecycleMutation(filePath, targetPath, config, { status: targetStatus, updated: today },
@@ -1008,6 +1025,7 @@ export async function runSet(argv, config, opts = {}) {
1008
1025
  dryRun, note, archiveStatus: newStatus, force, testHooks: opts.testHooks, deferIndex: opts.deferIndex,
1009
1026
  additionalUpdates: opts.additionalUpdates,
1010
1027
  creations: opts.creations,
1028
+ guards: opts.guards,
1011
1029
  });
1012
1030
  }
1013
1031
 
@@ -1059,8 +1077,9 @@ export async function runSet(argv, config, opts = {}) {
1059
1077
  dryRun,
1060
1078
  suppressDeprecation: true,
1061
1079
  note,
1062
- additionalUpdates: releaseUpdate ? [releaseUpdate] : [],
1080
+ additionalUpdates: [...(opts.additionalUpdates ?? []), ...(releaseUpdate ? [releaseUpdate] : [])],
1063
1081
  creations: opts.creations,
1082
+ guards: opts.guards,
1064
1083
  testHooks: opts.testHooks,
1065
1084
  deferIndex: opts.deferIndex,
1066
1085
  });
@@ -64,7 +64,7 @@ export function migrateOnePrompt(raw, opts = {}) {
64
64
  'type: prompt',
65
65
  'status: pending',
66
66
  `created: ${created}`,
67
- `dotmd_version: ${pkg.version}`,
67
+ `runlist_version: ${pkg.version}`,
68
68
  `context: "${context.replace(/"/g, '\\"')}"`,
69
69
  'related_plans: []',
70
70
  ].join('\n');
@@ -86,7 +86,7 @@ export function migrateOnePrompt(raw, opts = {}) {
86
86
  if (!parsed.type) needed.push(['type', 'prompt']);
87
87
  if (!parsed.status) needed.push(['status', 'pending']);
88
88
  if (!parsed.created) needed.push(['created', created]);
89
- if (!parsed.dotmd_version) needed.push(['dotmd_version', pkg.version]);
89
+ if (!parsed.runlist_version && !parsed.dotmd_version) needed.push(['runlist_version', pkg.version]);
90
90
  if (!parsed.context) {
91
91
  // Prefer existing `title:` if present, else derive from body/filename.
92
92
  const ctx = typeof parsed.title === 'string' ? parsed.title : deriveContext(rawBody || raw, opts.filePath || 'unknown');
package/src/new.mjs CHANGED
@@ -237,7 +237,7 @@ Status markers (put in heading text):
237
237
  `status: ${s}`,
238
238
  `created: ${d}`,
239
239
  `updated: ${d}`,
240
- `dotmd_version: ${pkg.version}`,
240
+ `runlist_version: ${pkg.version}`,
241
241
  `context: ${ctx?.title ? `"${ctx.title.replace(/"/g, '\\"')}"` : ''}`,
242
242
  'related_plans:',
243
243
  ].join('\n'),
package/src/update.mjs CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { green, dim, yellow } from './color.mjs';
7
7
  import { executableName, which } from './util.mjs';
8
8
  import { CLAUDE_MARKETPLACE, claudeMarketplaceRefusalHint, installOpencodePlugin, opencodeStatus } from './host-integration.mjs';
9
+ import { codexStatus } from './codex-integration.mjs';
9
10
 
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
12
  const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
@@ -129,6 +130,11 @@ export function planUpdate(opts, ctx) {
129
130
  } else if (ctx.opencode?.foreign) {
130
131
  steps.push({ kind: 'skip', reason: `${ctx.opencode.path} was not written by runlist — leaving it alone` });
131
132
  }
133
+ if (ctx.codex?.foreign) {
134
+ steps.push({ kind: 'skip', reason: `${ctx.codex.plugin} was not written by runlist — leaving it alone` });
135
+ } else if (ctx.codex?.exists && (ctx.codex.stale || (!opts.pluginOnly && ctx.hasNpm))) {
136
+ steps.push({ kind: 'codex', cmd: ['runlist', 'install', 'codex'] });
137
+ }
132
138
  }
133
139
  return steps;
134
140
  }
@@ -158,11 +164,16 @@ export function runUpdate(argv, _config, opts = {}) {
158
164
  if (!oc.exists) process.stdout.write(dim('runlist opencode: not installed\n'));
159
165
  else if (oc.foreign) process.stdout.write(`runlist opencode: ${yellow('unmanaged file — not written by runlist')}\n`);
160
166
  else process.stdout.write(`runlist opencode: ${oc.version} ${oc.stale ? yellow('behind — run `runlist update`') : green('in sync')}\n`);
167
+ const cx = codexStatus({ version: pkg.version });
168
+ if (!cx.exists) process.stdout.write(dim('runlist codex: not installed\n'));
169
+ else if (cx.foreign) process.stdout.write(`runlist codex: ${yellow('unmanaged plugin directory')}\n`);
170
+ else process.stdout.write(`runlist codex: ${cx.version} ${cx.stale ? yellow('behind — run `runlist update`') : green('in sync')}\n`);
161
171
  return;
162
172
  }
163
173
 
164
174
  const opencode = opencodeStatus({ version: pkg.version });
165
- const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, opencode, hasClaude: which('claude'), hasNpm: which('npm') });
175
+ const codex = codexStatus({ version: pkg.version });
176
+ const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, opencode, codex, hasClaude: which('claude'), hasNpm: which('npm') });
166
177
  if (opts.dryRun) {
167
178
  for (const step of steps) {
168
179
  if (step.kind === 'skip') process.stdout.write(dim(`[dry-run] skip: ${step.reason}\n`));