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/README.md +30 -4
- package/assets/opencode/plugin.js +29 -14
- package/bin/dotmd.mjs +23 -6
- package/package.json +2 -1
- package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
- package/plugins/runlist-codex/bin/runlist-hook +86 -0
- package/plugins/runlist-codex/hooks/hooks.json +16 -0
- package/plugins/runlist-codex/skills/runlist/SKILL.md +21 -0
- package/runlist.config.example.mjs +5 -0
- package/src/atomic-mutation.mjs +10 -2
- package/src/baton.mjs +140 -49
- package/src/codex-integration.mjs +81 -0
- package/src/commands.mjs +1 -1
- package/src/config.mjs +9 -6
- package/src/export.mjs +1 -1
- package/src/guard.mjs +60 -7
- package/src/hub-membership.mjs +1 -1
- package/src/index-file.mjs +20 -5
- package/src/init.mjs +6 -6
- package/src/install.mjs +50 -2
- package/src/lifecycle.mjs +26 -7
- package/src/migrate-prompts.mjs +2 -2
- package/src/new.mjs +1 -1
- package/src/update.mjs +12 -1
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:
|
|
57
|
-
endMarker: '<!-- GENERATED:
|
|
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:
|
|
121
|
+
<!-- GENERATED:runlist:start -->
|
|
122
122
|
|
|
123
123
|
_No docs yet. Run \`runlist list\` after creating your first document._
|
|
124
124
|
|
|
125
|
-
<!-- GENERATED:
|
|
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:
|
|
267
|
-
lines.push(` endMarker: '<!-- GENERATED:
|
|
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
|
|
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
|
});
|
package/src/migrate-prompts.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export function migrateOnePrompt(raw, opts = {}) {
|
|
|
64
64
|
'type: prompt',
|
|
65
65
|
'status: pending',
|
|
66
66
|
`created: ${created}`,
|
|
67
|
-
`
|
|
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(['
|
|
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
|
-
`
|
|
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
|
|
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`));
|