runlist 0.95.2 → 0.95.3
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 +10 -3
- package/package.json +1 -1
- package/plugins/runlist-codex/.codex-plugin/plugin.json +1 -1
- package/src/codex-integration.mjs +22 -0
- package/src/install.mjs +7 -4
- package/src/update.mjs +53 -23
package/README.md
CHANGED
|
@@ -47,8 +47,8 @@ controls are experimental; live retrieval depends on a compatible external
|
|
|
47
47
|
provider. Local chat and push are deferred.
|
|
48
48
|
|
|
49
49
|
<!-- runlist:release:start -->
|
|
50
|
-
CLI and desktop release version: **0.95.
|
|
51
|
-
[Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.95.
|
|
50
|
+
CLI and desktop release version: **0.95.3**.
|
|
51
|
+
[Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.95.3).
|
|
52
52
|
Normal product releases publish both components; an installed Mac app is updated with rollback after it has quit.
|
|
53
53
|
<!-- runlist:release:end -->
|
|
54
54
|
|
|
@@ -219,7 +219,14 @@ and managed scopes are reported rather than silently updating another scope.
|
|
|
219
219
|
The release workflow uses the same scope selection and verifies every entry
|
|
220
220
|
against the released version.
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
Updates also refresh already installed OpenCode and Codex integrations. After
|
|
223
|
+
upgrading the CLI, host installers run from the updated package so their version
|
|
224
|
+
and assets match. Codex verification checks the host's installed copy separately
|
|
225
|
+
from its marketplace source. Releases use these same checks and remain resumable
|
|
226
|
+
if an installed host fails to update. Uninstalled hosts are not added.
|
|
227
|
+
|
|
228
|
+
Restart the affected host sessions to load updated integrations; Claude Code also
|
|
229
|
+
supports `/reload-plugins`.
|
|
223
230
|
|
|
224
231
|
## Quick Start
|
|
225
232
|
|
package/package.json
CHANGED
|
@@ -4,6 +4,8 @@ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, r
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { executableName } from './util.mjs';
|
|
7
9
|
|
|
8
10
|
const SOURCE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'plugins', 'runlist-codex');
|
|
9
11
|
const NAME = 'runlist-codex';
|
|
@@ -33,6 +35,26 @@ export function codexStatus({ version, homedir = os.homedir() } = {}) {
|
|
|
33
35
|
return { ...paths, exists, foreign: exists && !installed, version: installed, stale: Boolean(installed && version && installed !== version) };
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
// The marketplace source is not the plugin Codex loads. Ask the host about its
|
|
39
|
+
// installed copy rather than treating our generated source marker as proof.
|
|
40
|
+
export function installedCodexStatus({ homedir = os.homedir(), run = spawnSync } = {}) {
|
|
41
|
+
try {
|
|
42
|
+
const marketplace = marketplaceAt(codexPaths(homedir).marketplace).name;
|
|
43
|
+
const pluginId = `${NAME}@${marketplace}`;
|
|
44
|
+
const result = run(executableName('codex'), ['plugin', 'list', '--marketplace', marketplace, '--json'], {
|
|
45
|
+
encoding: 'utf8', shell: process.platform === 'win32', timeout: 30_000,
|
|
46
|
+
});
|
|
47
|
+
if (result.status !== 0) return { ok: false, reason: result.error?.message ?? `codex plugin list exited ${result.status ?? '?'}` };
|
|
48
|
+
const installed = JSON.parse(result.stdout).installed;
|
|
49
|
+
if (!Array.isArray(installed)) return { ok: false, reason: 'codex plugin list returned no installed plugin records' };
|
|
50
|
+
const plugin = installed.find(entry => entry.pluginId === pluginId && entry.installed === true);
|
|
51
|
+
return plugin?.version ? { ok: true, version: plugin.version, pluginId }
|
|
52
|
+
: { ok: false, reason: `${pluginId} is not installed in Codex` };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return { ok: false, reason: `cannot verify installed Codex plugin: ${err.message}` };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
export function installCodexPlugin({ version, homedir = os.homedir(), dryRun = false } = {}) {
|
|
37
59
|
const sourceVersion = JSON.parse(readFileSync(path.join(SOURCE, '.codex-plugin', 'plugin.json'), 'utf8')).version;
|
|
38
60
|
if (sourceVersion !== version) throw new Error(`Codex plugin source is ${sourceVersion}, CLI is ${version}`);
|
package/src/install.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from './host-integration.mjs';
|
|
10
10
|
import { LEGACY_PLUGIN_ID, readInstalledPlugin, readInstalledPluginRecords } from './update.mjs';
|
|
11
11
|
import { executableName, which } from './util.mjs';
|
|
12
|
-
import { codexStatus, installCodexPlugin } from './codex-integration.mjs';
|
|
12
|
+
import { codexStatus, installCodexPlugin, installedCodexStatus } from './codex-integration.mjs';
|
|
13
13
|
|
|
14
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
15
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -111,9 +111,12 @@ function installCodex(argv, dryRun, json) {
|
|
|
111
111
|
stdio: json ? 'pipe' : 'inherit', encoding: json ? 'utf8' : undefined,
|
|
112
112
|
shell: process.platform === 'win32',
|
|
113
113
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
const installed = added.status === 0 ? installedCodexStatus() : { ok: false, reason: added.error?.message ?? `codex plugin add exited ${added.status ?? '?'}` };
|
|
115
|
+
const verified = installed.ok && installed.version === pkg.version;
|
|
116
|
+
if (json) process.stdout.write(JSON.stringify({ ...result, pluginAdd: verified ? 'installed' : 'failed', pluginAddExit: added.status,
|
|
117
|
+
installedVersion: installed.version ?? null, ...(!verified ? { reason: installed.reason ?? `Codex has ${installed.version}, expected ${pkg.version}` } : {}) }, null, 2) + '\n');
|
|
118
|
+
if (!verified) {
|
|
119
|
+
process.stderr.write(`Codex plugin installation is unverified: ${installed.reason ?? `has ${installed.version}, expected ${pkg.version}`}. Retry: codex plugin add ${selector}\n`);
|
|
117
120
|
process.exitCode = 1;
|
|
118
121
|
} else if (!json) {
|
|
119
122
|
process.stdout.write('Start a new Codex thread and review/trust the plugin hooks to enable the primer and guard.\n');
|
package/src/update.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import os from 'node:os';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { green, dim, yellow } from './color.mjs';
|
|
7
7
|
import { executableName, which } from './util.mjs';
|
|
8
|
-
import { CLAUDE_MARKETPLACE, claudeMarketplaceRefusalHint,
|
|
9
|
-
import { codexStatus } from './codex-integration.mjs';
|
|
8
|
+
import { CLAUDE_MARKETPLACE, claudeMarketplaceRefusalHint, opencodeStatus } from './host-integration.mjs';
|
|
9
|
+
import { codexStatus, installedCodexStatus } from './codex-integration.mjs';
|
|
10
10
|
import { desktopLaunch } from './desktop.mjs';
|
|
11
11
|
import { compareDesktopVersions, installedDesktopVersion } from './desktop-install.mjs';
|
|
12
12
|
|
|
@@ -203,10 +203,10 @@ export function planUpdate(opts, ctx) {
|
|
|
203
203
|
// the moment the CLI moves on. Refresh it here — but only if it is already
|
|
204
204
|
// installed. `update` keeps hosts in lockstep; it never adopts a new one,
|
|
205
205
|
// which stays the job of the explicit `runlist install`.
|
|
206
|
-
if (ctx.opencode?.
|
|
207
|
-
steps.push({ kind: 'opencode', path: ctx.opencode.path });
|
|
208
|
-
} else if (ctx.opencode?.foreign) {
|
|
206
|
+
if (ctx.opencode?.foreign) {
|
|
209
207
|
steps.push({ kind: 'skip', reason: `${ctx.opencode.path} was not written by runlist — leaving it alone` });
|
|
208
|
+
} else if (ctx.opencode?.exists && (ctx.opencode.stale || (!opts.pluginOnly && ctx.hasNpm))) {
|
|
209
|
+
steps.push({ kind: 'opencode', cmd: ['runlist', 'install', 'opencode'] });
|
|
210
210
|
}
|
|
211
211
|
if (ctx.codex?.foreign) {
|
|
212
212
|
steps.push({ kind: 'skip', reason: `${ctx.codex.plugin} was not written by runlist — leaving it alone` });
|
|
@@ -220,6 +220,28 @@ export function planUpdate(opts, ctx) {
|
|
|
220
220
|
return steps;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
export function verifyManagedHostUpdates(before, after, expectedVersion) {
|
|
224
|
+
const failures = [];
|
|
225
|
+
for (const host of ['opencode', 'codex']) {
|
|
226
|
+
if (!before[host]?.exists || before[host].foreign) continue;
|
|
227
|
+
const current = after[host];
|
|
228
|
+
if (!current?.exists || current.foreign || current.version !== expectedVersion || current.stale) {
|
|
229
|
+
failures.push(`${host} integration has ${current?.version ?? 'no managed installation'}, expected ${expectedVersion}`);
|
|
230
|
+
}
|
|
231
|
+
if (host === 'codex' && (!current?.installed?.ok || current.installed.version !== expectedVersion)) {
|
|
232
|
+
failures.push(`Codex installed plugin ${current?.installed?.reason ?? `has ${current?.installed?.version ?? 'unknown'}, expected ${expectedVersion}`}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return failures;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function currentCodexStatus(version) {
|
|
239
|
+
const source = codexStatus({ version });
|
|
240
|
+
if (!source.exists || source.foreign) return source;
|
|
241
|
+
const installed = which('codex') ? installedCodexStatus() : { ok: false, reason: 'codex CLI not found; installed plugin is unverified' };
|
|
242
|
+
return { ...source, installed, stale: source.stale || !installed.ok || installed.version !== version };
|
|
243
|
+
}
|
|
244
|
+
|
|
223
245
|
export async function runUpdate(argv, _config, opts = {}) {
|
|
224
246
|
const check = argv.includes('--check');
|
|
225
247
|
const cliOnly = argv.includes('--cli-only');
|
|
@@ -252,10 +274,10 @@ export async function runUpdate(argv, _config, opts = {}) {
|
|
|
252
274
|
if (!oc.exists) process.stdout.write(dim('runlist opencode: not installed\n'));
|
|
253
275
|
else if (oc.foreign) process.stdout.write(`runlist opencode: ${yellow('unmanaged file — not written by runlist')}\n`);
|
|
254
276
|
else process.stdout.write(`runlist opencode: ${oc.version} ${oc.stale ? yellow('behind — run `runlist update`') : green('in sync')}\n`);
|
|
255
|
-
const cx =
|
|
277
|
+
const cx = currentCodexStatus(pkg.version);
|
|
256
278
|
if (!cx.exists) process.stdout.write(dim('runlist codex: not installed\n'));
|
|
257
279
|
else if (cx.foreign) process.stdout.write(`runlist codex: ${yellow('unmanaged plugin directory')}\n`);
|
|
258
|
-
else process.stdout.write(`runlist codex: ${cx.version} ${cx.stale ? yellow('
|
|
280
|
+
else process.stdout.write(`runlist codex: source ${cx.version}; installed ${cx.installed?.version ?? 'unverified'} ${cx.stale ? yellow('out of sync — run `runlist update`') : green('in sync')}\n`);
|
|
259
281
|
if (desktop) {
|
|
260
282
|
const version = await installedDesktopVersion(desktop);
|
|
261
283
|
process.stdout.write(`runlist desktop: ${version} ${compareDesktopVersions(version, pkg.version) < 0 ? yellow('behind — quit Runlist and run `runlist update`') : green('in sync')}\n`);
|
|
@@ -264,13 +286,12 @@ export async function runUpdate(argv, _config, opts = {}) {
|
|
|
264
286
|
}
|
|
265
287
|
|
|
266
288
|
const opencode = opencodeStatus({ version: pkg.version });
|
|
267
|
-
const codex =
|
|
289
|
+
const codex = currentCodexStatus(pkg.version);
|
|
268
290
|
const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, legacyPlugin, opencode, codex, desktop, hasClaude: which('claude'), hasNpm: which('npm') });
|
|
269
291
|
if (opts.dryRun) {
|
|
270
292
|
for (const step of steps) {
|
|
271
293
|
if (step.kind === 'skip') process.stdout.write(dim(`[dry-run] skip: ${step.reason}\n`));
|
|
272
294
|
else if (step.kind === 'manual') for (const line of step.lines) process.stdout.write(dim(`[dry-run] Run from a session: ${line}\n`));
|
|
273
|
-
else if (step.kind === 'opencode') process.stdout.write(dim(`[dry-run] Would refresh: ${step.path}\n`));
|
|
274
295
|
else if (step.kind === 'refusal') process.stdout.write(yellow(`[dry-run] Refused: ${step.reason}\n`));
|
|
275
296
|
else process.stdout.write(dim(`[dry-run] Would run: ${step.cmd.join(' ')}${step.cwd ? ` (in ${step.cwd})` : ''}\n`));
|
|
276
297
|
}
|
|
@@ -304,18 +325,17 @@ export async function runUpdate(argv, _config, opts = {}) {
|
|
|
304
325
|
process.stdout.write(dim(`skip: ${s.cmd.join(' ')} — the ${s.needs} step it depends on failed\n`));
|
|
305
326
|
continue;
|
|
306
327
|
}
|
|
307
|
-
if (s.kind === 'opencode') {
|
|
308
|
-
const result = installOpencodePlugin({ version: pkg.version });
|
|
309
|
-
process.stdout.write(dim(`refreshed opencode integration → ${pkg.version} ${result.path}\n`));
|
|
310
|
-
ran = true;
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
328
|
if (s.reason) process.stdout.write(dim(`${s.reason}\n`));
|
|
314
329
|
process.stdout.write(dim(`$ ${s.cmd.join(' ')}${s.cwd ? ` (in ${s.cwd})` : ''}\n`));
|
|
315
|
-
|
|
330
|
+
// npm can replace this package during the CLI step. A new Node process
|
|
331
|
+
// reads the new installers/assets; this process still holds the old modules.
|
|
332
|
+
// Use this installation's entry point, not another runlist earlier on PATH.
|
|
333
|
+
const ownCli = s.cmd[0] === 'runlist';
|
|
334
|
+
const r = spawnSync(ownCli ? process.execPath : executableName(s.cmd[0]),
|
|
335
|
+
ownCli ? [path.join(__dirname, '..', 'bin', 'runlist.mjs'), ...s.cmd.slice(1)] : s.cmd.slice(1), {
|
|
316
336
|
stdio: 'inherit',
|
|
317
337
|
...(s.cwd ? { cwd: s.cwd } : {}),
|
|
318
|
-
shell: process.platform === 'win32',
|
|
338
|
+
shell: !ownCli && process.platform === 'win32',
|
|
319
339
|
});
|
|
320
340
|
ran = true;
|
|
321
341
|
if (r.status !== 0) {
|
|
@@ -325,17 +345,27 @@ export async function runUpdate(argv, _config, opts = {}) {
|
|
|
325
345
|
if (s.kind === 'marketplace') for (const line of claudeMarketplaceRefusalHint(plugin?.marketplace)) process.stdout.write(yellow(`${line}\n`));
|
|
326
346
|
}
|
|
327
347
|
}
|
|
328
|
-
|
|
348
|
+
const currentVersion = opts.expectedVersion ?? JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
349
|
+
if (!cliOnly) {
|
|
350
|
+
const after = { opencode: opencodeStatus({ version: currentVersion }), codex: currentCodexStatus(currentVersion) };
|
|
351
|
+
for (const reason of verifyManagedHostUpdates({ opencode, codex }, after, currentVersion)) {
|
|
352
|
+
failures.push({ reason });
|
|
353
|
+
process.stdout.write(yellow(`${reason}\n`));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!cliOnly && plugin?.id === DEFAULT_PLUGIN_ID && !failedKinds.has('plugin') && !failedKinds.has('marketplace')) {
|
|
329
357
|
// npm may have replaced this package while this process was running.
|
|
330
|
-
const
|
|
331
|
-
|
|
358
|
+
const verified = verifyPluginScopeUpdates(plugin, readInstalledPluginRecords({ id: plugin.id }),
|
|
359
|
+
opts.expectedVersion ? { expectedVersion: currentVersion } : { minimumVersion: currentVersion });
|
|
332
360
|
if (!verified.ok) {
|
|
333
361
|
failures.push({ reason: verified.reason });
|
|
334
362
|
process.stdout.write(yellow(`${verified.reason}\n`));
|
|
335
363
|
}
|
|
336
364
|
}
|
|
365
|
+
if (!cliOnly && legacyPlugin && readInstalledPluginRecords({ id: LEGACY_PLUGIN_ID })) {
|
|
366
|
+
failures.push({ reason: `legacy Claude plugin ${LEGACY_PLUGIN_ID} remains installed; migration is incomplete` });
|
|
367
|
+
}
|
|
337
368
|
if (desktop && steps.some(step => step.kind === 'desktop') && !failedKinds.has('desktop')) {
|
|
338
|
-
const currentVersion = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
339
369
|
if (compareDesktopVersions(await installedDesktopVersion(desktop), currentVersion) < 0) {
|
|
340
370
|
failures.push({ reason: 'The matching desktop installer is not published yet; retry `runlist update` after the product release completes.' });
|
|
341
371
|
}
|
|
@@ -343,12 +373,12 @@ export async function runUpdate(argv, _config, opts = {}) {
|
|
|
343
373
|
if (failures.length) {
|
|
344
374
|
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`));
|
|
345
375
|
if (ran && failures.length < steps.filter(s => s.kind !== 'skip' && s.kind !== 'manual').length) {
|
|
346
|
-
process.stdout.write(dim('the other steps completed; restart
|
|
376
|
+
process.stdout.write(dim('the other steps completed; restart the affected host sessions to load the updated integrations.\n'));
|
|
347
377
|
}
|
|
348
378
|
process.exitCode = 1;
|
|
349
379
|
return;
|
|
350
380
|
}
|
|
351
381
|
if (ran) {
|
|
352
|
-
process.stdout.write(green('\n✓ restart
|
|
382
|
+
process.stdout.write(green('\n✓ restart the affected host sessions to load the updated integrations.\n'));
|
|
353
383
|
}
|
|
354
384
|
}
|