runlist 0.0.0 → 0.94.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.
Files changed (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1112 -6
  3. package/assets/app/app.css +283 -0
  4. package/assets/app/app.mjs +381 -0
  5. package/assets/app/block-editor.mjs +182 -0
  6. package/assets/app/block-model.mjs +52 -0
  7. package/assets/app/document-create.mjs +86 -0
  8. package/assets/app/document-lifecycle.mjs +36 -0
  9. package/assets/app/document-stages.mjs +22 -0
  10. package/assets/app/document-views.mjs +33 -0
  11. package/assets/app/document-yardstick.mjs +258 -0
  12. package/assets/app/editor-navigation.mjs +51 -0
  13. package/assets/app/filing-navigation.mjs +138 -0
  14. package/assets/app/git-changes.mjs +80 -0
  15. package/assets/app/git-commit.mjs +46 -0
  16. package/assets/app/index.html +70 -0
  17. package/assets/app/library-navigation.mjs +93 -0
  18. package/assets/app/outline.mjs +18 -0
  19. package/assets/app/quick-navigation.mjs +52 -0
  20. package/assets/app/record-navigation.mjs +78 -0
  21. package/assets/app/recovery-center.mjs +53 -0
  22. package/assets/app/semantic-search.mjs +14 -0
  23. package/assets/app/settings.mjs +15 -0
  24. package/assets/app/shared.mjs +104 -0
  25. package/assets/app/stage-source.mjs +75 -0
  26. package/assets/app/template-editor.mjs +47 -0
  27. package/assets/app/theme.mjs +25 -0
  28. package/assets/app/transport.mjs +43 -0
  29. package/assets/contracts/source-editing-v1.md +232 -0
  30. package/assets/opencode/plugin.js +162 -0
  31. package/assets/schemas/runlist-record-v1.schema.json +912 -0
  32. package/bin/dotmd.mjs +5 -0
  33. package/bin/runlist.mjs +2714 -0
  34. package/package.json +53 -6
  35. package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
  36. package/plugins/runlist-codex/bin/runlist-hook +86 -0
  37. package/plugins/runlist-codex/hooks/hooks.json +16 -0
  38. package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
  39. package/runlist.config.example.mjs +370 -0
  40. package/scripts/postinstall.mjs +56 -0
  41. package/src/agent-context.mjs +201 -0
  42. package/src/ai.mjs +30 -0
  43. package/src/app-create.mjs +169 -0
  44. package/src/app-filing.mjs +139 -0
  45. package/src/app-git-commits.mjs +48 -0
  46. package/src/app-git.mjs +180 -0
  47. package/src/app-library.mjs +152 -0
  48. package/src/app-lifecycle-worker.mjs +12 -0
  49. package/src/app-lifecycle.mjs +79 -0
  50. package/src/app-records.mjs +134 -0
  51. package/src/app-search.mjs +80 -0
  52. package/src/app-semantic-search.mjs +105 -0
  53. package/src/app-service.mjs +208 -0
  54. package/src/app-yardstick.mjs +269 -0
  55. package/src/app.mjs +102 -0
  56. package/src/atomic-mutation.mjs +1736 -0
  57. package/src/baton.mjs +435 -0
  58. package/src/body-link.mjs +48 -0
  59. package/src/bulk-tag.mjs +174 -0
  60. package/src/check-collapse.mjs +75 -0
  61. package/src/claude-commands.mjs +75 -0
  62. package/src/cli-editor.mjs +8 -0
  63. package/src/code-refs.mjs +545 -0
  64. package/src/codex-integration.mjs +81 -0
  65. package/src/color.mjs +14 -0
  66. package/src/commands.mjs +374 -0
  67. package/src/completions.mjs +93 -0
  68. package/src/config-edit.mjs +606 -0
  69. package/src/config.mjs +705 -0
  70. package/src/decision-action.mjs +58 -0
  71. package/src/decision.mjs +220 -0
  72. package/src/decisions.mjs +787 -0
  73. package/src/deps.mjs +317 -0
  74. package/src/desktop-install.mjs +158 -0
  75. package/src/desktop-runtime.mjs +4 -0
  76. package/src/desktop.mjs +25 -0
  77. package/src/diff.mjs +101 -0
  78. package/src/doctor.mjs +591 -0
  79. package/src/durable-rename.mjs +55 -0
  80. package/src/errors-read.mjs +44 -0
  81. package/src/export.mjs +496 -0
  82. package/src/extractors.mjs +101 -0
  83. package/src/filing.mjs +255 -0
  84. package/src/fix-membership.mjs +134 -0
  85. package/src/fix-refs.mjs +174 -0
  86. package/src/flags.mjs +318 -0
  87. package/src/frontmatter-fix.mjs +195 -0
  88. package/src/frontmatter.mjs +252 -0
  89. package/src/git-commit-filters.mjs +32 -0
  90. package/src/git-commit-protocol.mjs +198 -0
  91. package/src/git-commit-signing.mjs +75 -0
  92. package/src/git-commit-store.mjs +26 -0
  93. package/src/git-commit-support.mjs +16 -0
  94. package/src/git-commit-worker.mjs +32 -0
  95. package/src/git.mjs +909 -0
  96. package/src/glossary-check.mjs +35 -0
  97. package/src/glossary.mjs +243 -0
  98. package/src/graph.mjs +298 -0
  99. package/src/guard.mjs +488 -0
  100. package/src/health.mjs +212 -0
  101. package/src/hints.mjs +129 -0
  102. package/src/host-integration.mjs +339 -0
  103. package/src/hub-membership.mjs +284 -0
  104. package/src/hub.mjs +296 -0
  105. package/src/hud.mjs +436 -0
  106. package/src/index-file.mjs +163 -0
  107. package/src/index.mjs +478 -0
  108. package/src/init.mjs +512 -0
  109. package/src/install.mjs +222 -0
  110. package/src/journal-read.mjs +116 -0
  111. package/src/journal.mjs +472 -0
  112. package/src/lifecycle.mjs +1444 -0
  113. package/src/lint.mjs +386 -0
  114. package/src/managed-path.mjs +192 -0
  115. package/src/markdown-code-spans.mjs +58 -0
  116. package/src/migrate-prompts.mjs +171 -0
  117. package/src/migrate-template.mjs +170 -0
  118. package/src/migrate.mjs +90 -0
  119. package/src/misuse-read.mjs +61 -0
  120. package/src/model-request.mjs +33 -0
  121. package/src/model.mjs +523 -0
  122. package/src/modules.mjs +355 -0
  123. package/src/naming.mjs +55 -0
  124. package/src/native-action.mjs +59 -0
  125. package/src/native-record.mjs +244 -0
  126. package/src/new.mjs +1321 -0
  127. package/src/output-identity.mjs +111 -0
  128. package/src/parent-plan.mjs +58 -0
  129. package/src/parse-cache.mjs +99 -0
  130. package/src/path-containment.mjs +15 -0
  131. package/src/pickup-card.mjs +293 -0
  132. package/src/pickup.mjs +671 -0
  133. package/src/prompt.mjs +25 -0
  134. package/src/prompts.mjs +585 -0
  135. package/src/public-inspection.mjs +19 -0
  136. package/src/query.mjs +977 -0
  137. package/src/record-action.mjs +11 -0
  138. package/src/record-create.mjs +82 -0
  139. package/src/reference-planner.mjs +388 -0
  140. package/src/reference-values.mjs +11 -0
  141. package/src/rename.mjs +107 -0
  142. package/src/render.mjs +678 -0
  143. package/src/roadmap.mjs +184 -0
  144. package/src/runlist.mjs +944 -0
  145. package/src/section.mjs +225 -0
  146. package/src/semantic-search-client.mjs +58 -0
  147. package/src/ship.mjs +166 -0
  148. package/src/show.mjs +101 -0
  149. package/src/skill-drift.mjs +98 -0
  150. package/src/source-editor.mjs +491 -0
  151. package/src/stages.mjs +73 -0
  152. package/src/state-migration.mjs +123 -0
  153. package/src/stats.mjs +200 -0
  154. package/src/status-metadata.mjs +87 -0
  155. package/src/statuses.mjs +721 -0
  156. package/src/summary.mjs +74 -0
  157. package/src/surfaces.mjs +28 -0
  158. package/src/sync-status.mjs +316 -0
  159. package/src/template-store.mjs +62 -0
  160. package/src/update.mjs +338 -0
  161. package/src/use.mjs +58 -0
  162. package/src/util.mjs +337 -0
  163. package/src/validate.mjs +795 -0
  164. package/src/watch.mjs +56 -0
  165. package/src/xref.mjs +406 -0
  166. package/src/yardstick-command.mjs +178 -0
  167. package/src/yardstick.mjs +112 -0
package/src/update.mjs ADDED
@@ -0,0 +1,338 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { green, dim, yellow } from './color.mjs';
7
+ import { executableName, which } from './util.mjs';
8
+ import { CLAUDE_MARKETPLACE, claudeMarketplaceRefusalHint, installOpencodePlugin, opencodeStatus } from './host-integration.mjs';
9
+ import { codexStatus } from './codex-integration.mjs';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
13
+
14
+ const NPM_PKG = 'runlist';
15
+ const DEFAULT_PLUGIN_ID = 'runlist@runlist';
16
+ export const LEGACY_PLUGIN_ID = 'dotmd@dotmd';
17
+
18
+ // Parse an x.y.z prefix; returns [major, minor, patch] or null.
19
+ function parseVer(v) {
20
+ if (typeof v !== 'string') return null;
21
+ const m = v.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
22
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
23
+ }
24
+
25
+ // -1 if a<b, 0 if equal, 1 if a>b, null if either is unparseable.
26
+ export function compareVersions(a, b) {
27
+ const pa = parseVer(a), pb = parseVer(b);
28
+ if (!pa || !pb) return null;
29
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
30
+ return 0;
31
+ }
32
+
33
+ // Read Claude Code's plugin install record to find the installed runlist plugin's
34
+ // id + version. Network-free. `opts.home` is injectable for tests. Returns
35
+ // { id, version } or null when nothing is installed / the file is absent.
36
+ export function readInstalledPluginRecords(opts = {}) {
37
+ const home = opts.home || os.homedir();
38
+ const file = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
39
+ try {
40
+ const j = JSON.parse(readFileSync(file, 'utf8'));
41
+ const plugins = j.plugins || {};
42
+ const id = opts.id
43
+ ? (plugins[opts.id] ? opts.id : null)
44
+ : plugins[DEFAULT_PLUGIN_ID]
45
+ ? DEFAULT_PLUGIN_ID
46
+ : plugins[LEGACY_PLUGIN_ID]
47
+ ? LEGACY_PLUGIN_ID
48
+ : Object.keys(plugins).find(k => /^(?:runlist|dotmd)@/.test(k));
49
+ if (!id) return null;
50
+ const entries = Array.isArray(plugins[id]) ? plugins[id] : [plugins[id]];
51
+ return { id, entries: entries.filter(Boolean) };
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ // Claude Code's marketplace registry, keyed by marketplace name. A plugin id
58
+ // is `<plugin>@<marketplace>`, and the plugin only loads while its marketplace
59
+ // is registered here — an install record alone is not an installed plugin.
60
+ export function readKnownMarketplaces(opts = {}) {
61
+ const home = opts.home || os.homedir();
62
+ const file = path.join(home, '.claude', 'plugins', 'known_marketplaces.json');
63
+ try {
64
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
65
+ return parsed && typeof parsed === 'object' ? parsed : {};
66
+ } catch {
67
+ return {};
68
+ }
69
+ }
70
+
71
+ // `marketplaceRegistered: false` is the state `claude plugin list` shows as
72
+ // "failed to load: Marketplace runlist not found": the install record survived,
73
+ // the marketplace registration did not (a settings.json declaration that no
74
+ // longer matches, a wiped registry). Reading only the install record called
75
+ // that "installed", so `runlist install claude` skipped the one repair it owns
76
+ // and `runlist update` ran a `plugin update` that could only fail.
77
+ export function readInstalledPlugin(opts = {}) {
78
+ const records = readInstalledPluginRecords(opts);
79
+ if (!records) return null;
80
+ const marketplace = records.id.split('@')[1] || null;
81
+ const known = readKnownMarketplaces(opts);
82
+ return {
83
+ id: records.id,
84
+ entries: records.entries,
85
+ version: records.entries[0]?.version ?? null,
86
+ marketplace,
87
+ marketplaceRegistered: Boolean(marketplace && known[marketplace]),
88
+ };
89
+ }
90
+
91
+ export function pluginScopeLabel(entry) {
92
+ return `${entry.scope ?? 'unknown scope'}${entry.projectPath ? ` (${entry.projectPath})` : ''}`;
93
+ }
94
+
95
+ const scopeKey = entry => JSON.stringify([entry.scope, entry.projectPath ?? null]);
96
+
97
+ // Scope comes from the install registry, not the updater's current directory.
98
+ // Never turn a missing project or unsupported scope into a user-wide update.
99
+ export function planPluginUpdates(plugin) {
100
+ if (!plugin) return [];
101
+ if (!plugin?.entries) return [{ kind: 'plugin', cmd: ['claude', 'plugin', 'update', plugin.id] }];
102
+ const steps = [];
103
+ const seen = new Set();
104
+ for (const entry of plugin.entries) {
105
+ const key = scopeKey(entry);
106
+ if (seen.has(key)) continue;
107
+ seen.add(key);
108
+ const scoped = entry.scope === 'project' || entry.scope === 'local';
109
+ if (!['user', 'project', 'local'].includes(entry.scope) ||
110
+ (scoped && (typeof entry.projectPath !== 'string' || !path.isAbsolute(entry.projectPath)))) {
111
+ steps.push({ kind: 'refusal', reason: `cannot update ${plugin.id} at ${pluginScopeLabel(entry)}: ${scoped ? 'an absolute project path is required' : 'scope is not writable by the Claude CLI'}` });
112
+ continue;
113
+ }
114
+ steps.push({ kind: 'plugin', scope: entry.scope, ...(scoped ? { cwd: entry.projectPath } : {}),
115
+ cmd: ['claude', 'plugin', 'update', plugin.id, '--scope', entry.scope] });
116
+ }
117
+ return steps;
118
+ }
119
+
120
+ // A zero command exit is insufficient: retain every original registration and
121
+ // verify the fresh registry rather than trusting the first installation.
122
+ export function verifyPluginScopeUpdates(before, after, { expectedVersion, minimumVersion } = {}) {
123
+ if (!before?.entries?.length) return { ok: false, reason: `${before?.id ?? DEFAULT_PLUGIN_ID} plugin is not installed` };
124
+ const failures = [];
125
+ const versions = new Set();
126
+ const targets = new Map([...before.entries, ...(after?.id === before.id ? after.entries : [])].map(entry => [scopeKey(entry), entry]));
127
+ for (const entry of targets.values()) {
128
+ const current = after?.id === before.id ? after.entries.filter(item => scopeKey(item) === scopeKey(entry)) : [];
129
+ if (!current.length) {
130
+ failures.push(`${pluginScopeLabel(entry)} registration disappeared`);
131
+ continue;
132
+ }
133
+ for (const item of current) {
134
+ versions.add(item.version);
135
+ if (parseVer(item.version) === null || (expectedVersion && item.version !== expectedVersion) ||
136
+ (minimumVersion && compareVersions(item.version, minimumVersion) < 0)) {
137
+ failures.push(`${pluginScopeLabel(entry)} has ${item.version ?? 'unknown'}, expected ${expectedVersion ?? (minimumVersion ? `at least ${minimumVersion}` : 'a valid version')}`);
138
+ }
139
+ }
140
+ }
141
+ if (!failures.length && versions.size > 1) failures.push('installed scopes have different versions');
142
+ return failures.length ? { ok: false, reason: `${before.id}: ${[...new Set(failures)].join('; ')}` } : { ok: true };
143
+ }
144
+
145
+ // The steps that put a plugin whose marketplace registration is gone back on
146
+ // its feet. Only the marketplace runlist publishes has a source runlist knows; a
147
+ // plugin installed from some other marketplace names a source we cannot guess.
148
+ export function planMarketplaceRepair(plugin, { hasClaude, verb }) {
149
+ const reason = `marketplace "${plugin.marketplace}" is not registered, so the installed plugin cannot load`;
150
+ if (plugin.marketplace !== 'runlist') {
151
+ return [{ kind: 'skip', reason: `${reason} — re-add that marketplace (runlist does not know its source), then rerun` }];
152
+ }
153
+ if (!hasClaude) {
154
+ return [{ kind: 'manual', reason, lines: [`/plugin marketplace add ${CLAUDE_MARKETPLACE}`, `/plugin ${verb} ${plugin.id}`] }];
155
+ }
156
+ return [
157
+ { kind: 'marketplace', reason: `${reason} — re-adding it first`, cmd: ['claude', 'plugin', 'marketplace', 'add', CLAUDE_MARKETPLACE] },
158
+ ...(verb === 'update' ? planPluginUpdates(plugin).map(step => ({ ...step, needs: 'marketplace' }))
159
+ : [{ kind: 'plugin', needs: 'marketplace', cmd: ['claude', 'plugin', verb, plugin.id] }]),
160
+ ];
161
+ }
162
+
163
+ // Decide which steps `runlist update` should run. Pure — no side effects — so the
164
+ // orchestration is unit-testable. `opts` = { cliOnly, pluginOnly }; `ctx` =
165
+ // { plugin: {id,version}|null, hasClaude, hasNpm }.
166
+ export function planUpdate(opts, ctx) {
167
+ const steps = [];
168
+ if (!opts.pluginOnly) {
169
+ steps.push(ctx.hasNpm
170
+ ? { kind: 'cli', cmd: ['npm', 'i', '-g', `${NPM_PKG}@latest`] }
171
+ : { kind: 'skip', reason: 'npm not found on PATH — skipping CLI update' });
172
+ }
173
+ if (!opts.cliOnly) {
174
+ if (!ctx.plugin) {
175
+ steps.push({ kind: 'skip', reason: 'runlist plugin not installed — skipping plugin update' });
176
+ } else if (ctx.plugin.id === LEGACY_PLUGIN_ID) {
177
+ if (ctx.hasClaude) {
178
+ steps.push({ kind: 'marketplace', cmd: ['claude', 'plugin', 'marketplace', 'add', CLAUDE_MARKETPLACE] });
179
+ steps.push({ kind: 'plugin', needs: 'marketplace', cmd: ['claude', 'plugin', 'install', DEFAULT_PLUGIN_ID] });
180
+ steps.push({ kind: 'legacy', needs: 'plugin', cmd: ['claude', 'plugin', 'uninstall', LEGACY_PLUGIN_ID] });
181
+ } else {
182
+ steps.push({ kind: 'manual', reason: 'Claude CLI not found', lines: [
183
+ `/plugin marketplace add ${CLAUDE_MARKETPLACE}`,
184
+ `/plugin install ${DEFAULT_PLUGIN_ID}`,
185
+ `/plugin uninstall ${LEGACY_PLUGIN_ID}`,
186
+ ] });
187
+ }
188
+ } else if (ctx.plugin.marketplaceRegistered === false) {
189
+ steps.push(...planMarketplaceRepair(ctx.plugin, { hasClaude: ctx.hasClaude, verb: 'update' }));
190
+ } else if (!ctx.hasClaude) {
191
+ steps.push({ kind: 'skip', reason: `claude CLI not found — run \`/plugin update ${ctx.plugin.id}\` from a session instead` });
192
+ } else {
193
+ steps.push(...planPluginUpdates(ctx.plugin));
194
+ }
195
+ if (ctx.legacyPlugin && ctx.plugin?.id !== LEGACY_PLUGIN_ID) {
196
+ steps.push(ctx.hasClaude
197
+ ? { kind: 'legacy', needs: 'plugin', cmd: ['claude', 'plugin', 'uninstall', LEGACY_PLUGIN_ID] }
198
+ : { kind: 'manual', reason: 'Legacy Claude plugin also installed', lines: [`/plugin uninstall ${LEGACY_PLUGIN_ID}`] });
199
+ }
200
+ // The OpenCode integration is a file runlist wrote, so it goes stale silently
201
+ // the moment the CLI moves on. Refresh it here — but only if it is already
202
+ // installed. `update` keeps hosts in lockstep; it never adopts a new one,
203
+ // which stays the job of the explicit `runlist install`.
204
+ if (ctx.opencode?.exists && ctx.opencode.stale) {
205
+ steps.push({ kind: 'opencode', path: ctx.opencode.path });
206
+ } else if (ctx.opencode?.foreign) {
207
+ steps.push({ kind: 'skip', reason: `${ctx.opencode.path} was not written by runlist — leaving it alone` });
208
+ }
209
+ if (ctx.codex?.foreign) {
210
+ steps.push({ kind: 'skip', reason: `${ctx.codex.plugin} was not written by runlist — leaving it alone` });
211
+ } else if (ctx.codex?.exists && (ctx.codex.stale || (!opts.pluginOnly && ctx.hasNpm))) {
212
+ steps.push({ kind: 'codex', cmd: ['runlist', 'install', 'codex'] });
213
+ }
214
+ }
215
+ return steps;
216
+ }
217
+
218
+ export function runUpdate(argv, _config, opts = {}) {
219
+ const check = argv.includes('--check');
220
+ const cliOnly = argv.includes('--cli-only');
221
+ const pluginOnly = argv.includes('--plugin-only');
222
+ const plugin = readInstalledPlugin();
223
+ const legacyPlugin = readInstalledPluginRecords({ id: LEGACY_PLUGIN_ID });
224
+
225
+ if (check) {
226
+ process.stdout.write(`runlist CLI: ${pkg.version}\n`);
227
+ if (plugin) {
228
+ if (plugin.id === LEGACY_PLUGIN_ID || (legacyPlugin && plugin.id !== LEGACY_PLUGIN_ID)) {
229
+ process.stdout.write(yellow(` legacy Claude plugin ${LEGACY_PLUGIN_ID} is installed — run \`runlist update --plugin-only\` to migrate.\n`));
230
+ }
231
+ for (const entry of plugin.entries) {
232
+ const cmp = compareVersions(entry.version, pkg.version);
233
+ const tag = cmp === 0 ? green('in sync')
234
+ : cmp === null ? dim('(unknown)')
235
+ : cmp < 0 ? yellow('behind — run `runlist update`')
236
+ : yellow('ahead — CLI is behind');
237
+ process.stdout.write(`runlist plugin: ${entry.version ?? '?'} (${plugin.id}; ${pluginScopeLabel(entry)}) ${tag}\n`);
238
+ }
239
+ if (plugin.marketplaceRegistered === false) {
240
+ process.stdout.write(yellow(` marketplace "${plugin.marketplace}" is not registered — the plugin fails to load; run \`runlist install claude\`\n`));
241
+ }
242
+ } else {
243
+ process.stdout.write(dim('runlist plugin: not installed — `runlist install claude`\n'));
244
+ }
245
+ const oc = opencodeStatus({ version: pkg.version });
246
+ if (!oc.exists) process.stdout.write(dim('runlist opencode: not installed\n'));
247
+ else if (oc.foreign) process.stdout.write(`runlist opencode: ${yellow('unmanaged file — not written by runlist')}\n`);
248
+ else process.stdout.write(`runlist opencode: ${oc.version} ${oc.stale ? yellow('behind — run `runlist update`') : green('in sync')}\n`);
249
+ const cx = codexStatus({ version: pkg.version });
250
+ if (!cx.exists) process.stdout.write(dim('runlist codex: not installed\n'));
251
+ else if (cx.foreign) process.stdout.write(`runlist codex: ${yellow('unmanaged plugin directory')}\n`);
252
+ else process.stdout.write(`runlist codex: ${cx.version} ${cx.stale ? yellow('behind — run `runlist update`') : green('in sync')}\n`);
253
+ return;
254
+ }
255
+
256
+ const opencode = opencodeStatus({ version: pkg.version });
257
+ const codex = codexStatus({ version: pkg.version });
258
+ const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, legacyPlugin, opencode, codex, hasClaude: which('claude'), hasNpm: which('npm') });
259
+ if (opts.dryRun) {
260
+ for (const step of steps) {
261
+ if (step.kind === 'skip') process.stdout.write(dim(`[dry-run] skip: ${step.reason}\n`));
262
+ else if (step.kind === 'manual') for (const line of step.lines) process.stdout.write(dim(`[dry-run] Run from a session: ${line}\n`));
263
+ else if (step.kind === 'opencode') process.stdout.write(dim(`[dry-run] Would refresh: ${step.path}\n`));
264
+ else if (step.kind === 'refusal') process.stdout.write(yellow(`[dry-run] Refused: ${step.reason}\n`));
265
+ else process.stdout.write(dim(`[dry-run] Would run: ${step.cmd.join(' ')}${step.cwd ? ` (in ${step.cwd})` : ''}\n`));
266
+ }
267
+ return;
268
+ }
269
+ // The hosts are independent: a failed `claude plugin update` says nothing
270
+ // about the OpenCode file, so every step runs and the failures are reported
271
+ // together at the end. Stopping at the first one left OpenCode stale behind a
272
+ // Claude registry problem — silently, since the abort said nothing about the
273
+ // steps it never reached.
274
+ let ran = false;
275
+ const failures = [];
276
+ const failedKinds = new Set();
277
+ for (const s of steps) {
278
+ if (s.kind === 'refusal') {
279
+ failures.push(s);
280
+ failedKinds.add('plugin');
281
+ process.stdout.write(yellow(`${s.reason}\n`));
282
+ continue;
283
+ }
284
+ if (s.kind === 'skip') {
285
+ process.stdout.write(dim(`skip: ${s.reason}\n`));
286
+ continue;
287
+ }
288
+ if (s.kind === 'manual') {
289
+ process.stdout.write(`${yellow(s.reason)} — run these from a Claude Code session:\n`);
290
+ for (const line of s.lines) process.stdout.write(` ${line}\n`);
291
+ continue;
292
+ }
293
+ if (s.needs && failedKinds.has(s.needs)) {
294
+ process.stdout.write(dim(`skip: ${s.cmd.join(' ')} — the ${s.needs} step it depends on failed\n`));
295
+ continue;
296
+ }
297
+ if (s.kind === 'opencode') {
298
+ const result = installOpencodePlugin({ version: pkg.version });
299
+ process.stdout.write(dim(`refreshed opencode integration → ${pkg.version} ${result.path}\n`));
300
+ ran = true;
301
+ continue;
302
+ }
303
+ if (s.reason) process.stdout.write(dim(`${s.reason}\n`));
304
+ process.stdout.write(dim(`$ ${s.cmd.join(' ')}${s.cwd ? ` (in ${s.cwd})` : ''}\n`));
305
+ const r = spawnSync(executableName(s.cmd[0]), s.cmd.slice(1), {
306
+ stdio: 'inherit',
307
+ ...(s.cwd ? { cwd: s.cwd } : {}),
308
+ shell: process.platform === 'win32',
309
+ });
310
+ ran = true;
311
+ if (r.status !== 0) {
312
+ failedKinds.add(s.kind);
313
+ failures.push(s);
314
+ process.stdout.write(yellow(`(${s.cmd[0]} ${r.error ? `failed: ${r.error.message}` : `exited ${r.status ?? '?'}`})\n`));
315
+ if (s.kind === 'marketplace') for (const line of claudeMarketplaceRefusalHint(plugin?.marketplace)) process.stdout.write(yellow(`${line}\n`));
316
+ }
317
+ }
318
+ if (plugin?.id === DEFAULT_PLUGIN_ID && steps.some(step => step.kind === 'plugin') && !failedKinds.has('plugin') && !failedKinds.has('marketplace')) {
319
+ // npm may have replaced this package while this process was running.
320
+ const currentVersion = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
321
+ const verified = verifyPluginScopeUpdates(plugin, readInstalledPluginRecords({ id: plugin.id }), { minimumVersion: currentVersion });
322
+ if (!verified.ok) {
323
+ failures.push({ reason: verified.reason });
324
+ process.stdout.write(yellow(`${verified.reason}\n`));
325
+ }
326
+ }
327
+ if (failures.length) {
328
+ process.stdout.write(yellow(`\n${failures.length === 1 ? '1 step' : `${failures.length} steps`} failed: ${failures.map(s => s.cmd ? `${s.cmd.join(' ')}${s.cwd ? ` (in ${s.cwd})` : ''}` : s.reason).join('; ')}\n`));
329
+ if (ran && failures.length < steps.filter(s => s.kind !== 'skip' && s.kind !== 'manual').length) {
330
+ process.stdout.write(dim('the other steps completed; restart your Claude Code session (or /reload-plugins) to apply them.\n'));
331
+ }
332
+ process.exitCode = 1;
333
+ return;
334
+ }
335
+ if (ran) {
336
+ process.stdout.write(green('\n✓ restart your Claude Code session (or /reload-plugins) to apply.\n'));
337
+ }
338
+ }
package/src/use.mjs ADDED
@@ -0,0 +1,58 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
4
+ import { asString, die, resolveDocPath, toRepoPath } from './util.mjs';
5
+ import { consumePrompt, pendingPromptsOldestFirst, resolvePromptInput } from './prompts.mjs';
6
+ import { startPlan } from './lifecycle.mjs';
7
+ import { resolveDocArg } from './index.mjs';
8
+
9
+ // Top-level `runlist use [file]` — the single "start engaging with this doc"
10
+ // verb. Dispatches by the target doc's `type:` so agents don't have to know
11
+ // the verb-per-type rule:
12
+ // - prompt → print body + archive (one-shot consume)
13
+ // - plan → set status in-session + print the plan card
14
+ // - doc → print body (read-only)
15
+ // With no argument, consumes the oldest pending prompt.
16
+ export async function runUse(argv, config, opts = {}) {
17
+ const positional = argv.find(a => !a.startsWith('-'));
18
+ // `--no-claim` reads and archives a prompt without starting the plan it links.
19
+ const noClaim = argv.includes('--no-claim');
20
+ const promptOpts = { ...opts, noClaim };
21
+
22
+ if (!positional) {
23
+ const queue = pendingPromptsOldestFirst(config);
24
+ if (queue.length === 0) die('No pending prompts. Pass a file to use a plan or doc.');
25
+ const head = queue[0];
26
+ if (!head.abs) die(`Could not resolve path: ${head.doc.path}`);
27
+ return consumePrompt(head.abs, config, promptOpts);
28
+ }
29
+
30
+ // Exact path first, then prompt slugs (they keep precedence on a slug
31
+ // collision — consuming a prompt is the more common intent), then the
32
+ // shared resolver for plan/doc slugs, which dies with did-you-mean on miss.
33
+ const cwdPath = path.resolve(process.cwd(), positional);
34
+ const filePath = resolveDocPath(positional, config)
35
+ ?? (existsSync(cwdPath) ? cwdPath : null)
36
+ ?? resolvePromptInput(positional, config, { dieOnMiss: false })
37
+ ?? resolveDocArg(positional, config);
38
+
39
+ const raw = readFileSync(filePath, 'utf8');
40
+ const { frontmatter } = extractFrontmatter(raw);
41
+ const parsed = parseSimpleFrontmatter(frontmatter);
42
+ const type = asString(parsed.type);
43
+
44
+ if (type === 'prompt') {
45
+ return consumePrompt(filePath, config, promptOpts);
46
+ }
47
+ if (noClaim) die('--no-claim applies to prompts only; a plan opened with `use` is always claimed.');
48
+ if (type === 'plan') {
49
+ // Mark in-session (pure frontmatter) and print the plan card.
50
+ return startPlan([filePath, ...argv.filter(a => a !== positional)], config, opts);
51
+ }
52
+ // Anything else (doc, untyped, custom): print the body. The frontmatter
53
+ // already names everything an agent needs to know about lifecycle for that
54
+ // type, so the verb stays a no-op read for non-actionable types.
55
+ process.stdout.write(raw);
56
+ if (!raw.endsWith('\n')) process.stdout.write('\n');
57
+ process.stderr.write(`${toRepoPath(filePath, config.repoRoot)} (${type ?? 'untyped'})\n`);
58
+ }