runlist 0.95.1 → 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 +32 -22
- package/bin/runlist.mjs +3 -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/desktop-install.mjs +48 -6
- package/src/install.mjs +7 -4
- package/src/update.mjs +70 -24
package/README.md
CHANGED
|
@@ -46,13 +46,16 @@ Stage filters, grouping and editing are included. Optional grepmax semantic
|
|
|
46
46
|
controls are experimental; live retrieval depends on a compatible external
|
|
47
47
|
provider. Local chat and push are deferred.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
<!-- runlist:release:start -->
|
|
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
|
+
Normal product releases publish both components; an installed Mac app is updated with rollback after it has quit.
|
|
53
|
+
<!-- runlist:release:end -->
|
|
54
|
+
|
|
55
|
+
The desktop app bundles the matching engine and runtime. Publishing a product
|
|
56
|
+
release now includes building, qualifying, signing/notarizing and publishing the
|
|
57
|
+
Mac installer. `runlist update` also updates an existing Mac app; installing the
|
|
58
|
+
npm package alone does not download an app onto a machine without one.
|
|
56
59
|
Earlier development packages and their qualification results are historical;
|
|
57
60
|
see the [desktop validation record](desktop/VALIDATION.md).
|
|
58
61
|
|
|
@@ -84,8 +87,8 @@ Deferred work awaiting selection:
|
|
|
84
87
|
older distributions, real-version upgrades, fresh/minimum-version and Intel
|
|
85
88
|
Macs, plus native rendering, focus, accessibility and whole-window memory.
|
|
86
89
|
|
|
87
|
-
Mac
|
|
88
|
-
CLI
|
|
90
|
+
The current Mac installer is linked in the release-version section above.
|
|
91
|
+
CLI and desktop include the optional product yardstick.
|
|
89
92
|
The supported desktop
|
|
90
93
|
release is Apple Silicon macOS 13.5 or newer. Broader platform work and
|
|
91
94
|
experimental semantic search remain separate follow-up work.
|
|
@@ -109,10 +112,10 @@ without opening it. The app bundles its runtime and reads local files through
|
|
|
109
112
|
private pipes, with no listening server. Choose your checkout folder in the app.
|
|
110
113
|
No checkout configuration is loaded by these CLI commands.
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
`runlist desktop install` selects the newest published Mac installer and
|
|
116
|
+
updates an older installed app after you quit it.
|
|
114
117
|
Windows, Linux and Intel Mac GUI downloads are not yet available through this
|
|
115
|
-
command.
|
|
118
|
+
command. Current or newer installed apps are preserved. To use a downloaded signed
|
|
116
119
|
DMG, quit Runlist and run:
|
|
117
120
|
|
|
118
121
|
```bash
|
|
@@ -216,7 +219,14 @@ and managed scopes are reported rather than silently updating another scope.
|
|
|
216
219
|
The release workflow uses the same scope selection and verifies every entry
|
|
217
220
|
against the released version.
|
|
218
221
|
|
|
219
|
-
|
|
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`.
|
|
220
230
|
|
|
221
231
|
## Quick Start
|
|
222
232
|
|
|
@@ -280,13 +290,13 @@ runlist desktop install --from /path/to/Runlist.dmg # install a local signed DM
|
|
|
280
290
|
|
|
281
291
|
The installer currently supports Apple Silicon Macs. It verifies the published
|
|
282
292
|
download checksum, Runlist developer signature and Apple approval, and installs
|
|
283
|
-
without opening the app. An
|
|
284
|
-
replacement
|
|
285
|
-
changes no files. The
|
|
293
|
+
without opening the app. An older installed app is updated after it has quit;
|
|
294
|
+
replacement keeps a rollback copy. `--dry-run` uses no network and
|
|
295
|
+
changes no files. The current Mac download replaces the withdrawn
|
|
286
296
|
installers. These commands ship
|
|
287
297
|
in [CLI 0.92.0](https://github.com/reowens/runlist/releases/tag/v0.92.0) and can also
|
|
288
|
-
be used from this checkout as `node bin/runlist.mjs desktop …`.
|
|
289
|
-
|
|
298
|
+
be used from this checkout as `node bin/runlist.mjs desktop …`. Normal product releases keep GUI and CLI versions together; an npm install
|
|
299
|
+
does not add a GUI to a machine without one.
|
|
290
300
|
|
|
291
301
|
The optional browser document app is available from this source checkout (unreleased):
|
|
292
302
|
|
|
@@ -828,13 +838,13 @@ local model or server. GUI controls are implemented in the source checkout:
|
|
|
828
838
|
Product goal shows the saved statement, delivery and assessment, with reviewed
|
|
829
839
|
Serves/Clear/Close/Fold actions and retained recovery. Structured plans display
|
|
830
840
|
the comparison with an explanation when assessment changes are unavailable.
|
|
831
|
-
These controls are included in the public desktop package
|
|
832
|
-
|
|
841
|
+
These controls are included in the public desktop package.
|
|
842
|
+
`runlist update` updates an existing Mac app alongside the CLI and integrations.
|
|
833
843
|
|
|
834
844
|
### Plan Homes And Filing Checks
|
|
835
845
|
|
|
836
846
|
This policy is available in CLI 0.93.0. From a source checkout, invoke the
|
|
837
|
-
commands below as `node bin/runlist.mjs …`. The
|
|
847
|
+
commands below as `node bin/runlist.mjs …`. The bundled GUI
|
|
838
848
|
The desktop app also includes a read-only Filing view. The correction
|
|
839
849
|
for category inheritance from status-less hub indexes is included in CLI
|
|
840
850
|
0.93.1.
|
|
@@ -880,7 +890,7 @@ is labeled and suppresses absence/uniqueness findings. Path/type-scoped output
|
|
|
880
890
|
keeps the full hub/ancestor evidence while reporting only selected plans.
|
|
881
891
|
The source checkout also includes a read-only Filing view: open it to scan,
|
|
882
892
|
filter 50-plan pages, and inspect exact hub rows in 20-row evidence pages. Saved
|
|
883
|
-
edits invalidate the report. This view is included in public desktop package
|
|
893
|
+
edits invalidate the report. This view is included in the public desktop package.
|
|
884
894
|
The policy is off when `filing` is unset, false, or `{ enabled: false }`.
|
|
885
895
|
It can replace a repository's older “any link counts” coverage script once
|
|
886
896
|
that repository enables it and retires its custom check.
|
package/bin/runlist.mjs
CHANGED
|
@@ -1509,8 +1509,8 @@ runlist desktop install [--from /path/to/installer.dmg] — install the optional
|
|
|
1509
1509
|
|
|
1510
1510
|
Install downloads the latest Mac Apple Silicon GUI from Runlist's GitHub
|
|
1511
1511
|
releases, or uses a local signed DMG with --from. It verifies the Runlist
|
|
1512
|
-
developer signature and Apple approval before installing.
|
|
1513
|
-
|
|
1512
|
+
developer signature and Apple approval before installing. Older installed apps
|
|
1513
|
+
are updated after you quit Runlist; replacements retain a rollback copy.
|
|
1514
1514
|
Installation does not open the app. Windows/Linux installers are not yet
|
|
1515
1515
|
available through this command. --dry-run uses no network and changes no files.
|
|
1516
1516
|
|
|
@@ -2206,7 +2206,7 @@ async function main() {
|
|
|
2206
2206
|
}
|
|
2207
2207
|
if (command === 'hud') { const { runHud } = await import('../src/hud.mjs'); runHud(restArgs, config); return; }
|
|
2208
2208
|
if (command === 'guard') { const { runGuard } = await import('../src/guard.mjs'); await runGuard(restArgs, config, { dryRun }); return; }
|
|
2209
|
-
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); runUpdate(restArgs, config, { dryRun }); return; }
|
|
2209
|
+
if (command === 'update') { const { runUpdate } = await import('../src/update.mjs'); await runUpdate(restArgs, config, { dryRun }); return; }
|
|
2210
2210
|
if (command === 'install') { const { runInstall } = await import('../src/install.mjs'); runInstall(restArgs, config, { dryRun }); return; }
|
|
2211
2211
|
if (command === 'errors') { const { runErrors } = await import('../src/errors-read.mjs'); runErrors(restArgs); return; }
|
|
2212
2212
|
if (command === 'misuse') { const { runMisuse } = await import('../src/misuse-read.mjs'); runMisuse(restArgs, config); return; }
|
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/desktop-install.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {createHash,randomUUID} from 'node:crypto';
|
|
2
2
|
import {execFileSync} from 'node:child_process';
|
|
3
3
|
import {createWriteStream} from 'node:fs';
|
|
4
|
-
import {lstat,mkdir,mkdtemp,rename,rm} from 'node:fs/promises';
|
|
4
|
+
import {lstat,mkdir,mkdtemp,readFile,rename,rm} from 'node:fs/promises';
|
|
5
5
|
import {homedir,tmpdir} from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import {Readable,Transform} from 'node:stream';
|
|
@@ -12,6 +12,35 @@ const RELEASES='https://api.github.com/repos/reowens/runlist/releases';
|
|
|
12
12
|
const DOWNLOADS='https://github.com/reowens/runlist/releases/download/';
|
|
13
13
|
const TEAM='7GSPYYN5X8';
|
|
14
14
|
const MAX_DOWNLOAD=512*1024*1024;
|
|
15
|
+
|
|
16
|
+
export function compareDesktopVersions(a,b) {
|
|
17
|
+
const parse=value=>/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\w.-]+))?$/.exec(value??'');
|
|
18
|
+
const left=parse(a),right=parse(b);
|
|
19
|
+
if(!left||!right)throw new Error('Cannot compare the installed and published desktop versions. Nothing was replaced.');
|
|
20
|
+
for(let i=1;i<=3;i++)if(Number(left[i])!==Number(right[i]))return Number(left[i])<Number(right[i])?-1:1;
|
|
21
|
+
if(left[4]===right[4])return 0;
|
|
22
|
+
if(!left[4]||!right[4])return left[4]?-1:1;
|
|
23
|
+
const x=left[4].split('.'),y=right[4].split('.');
|
|
24
|
+
for(let i=0;i<Math.max(x.length,y.length);i++) {
|
|
25
|
+
if(x[i]===y[i])continue;
|
|
26
|
+
if(x[i]===undefined||y[i]===undefined)return x[i]===undefined?-1:1;
|
|
27
|
+
const xn=/^\d+$/.test(x[i]),yn=/^\d+$/.test(y[i]);
|
|
28
|
+
if(xn!==yn)return xn?-1:1;
|
|
29
|
+
return xn?(Number(x[i])<Number(y[i])?-1:1):(x[i]<y[i]?-1:1);
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function installedDesktopVersion(app) {
|
|
35
|
+
try {
|
|
36
|
+
const engine=path.join(app,'Contents/Resources/engine');
|
|
37
|
+
const manifest=JSON.parse(await readFile(path.join(engine,'bundle-manifest.json'),'utf8'));
|
|
38
|
+
const pkg=JSON.parse(await readFile(path.join(engine,'package.json'),'utf8'));
|
|
39
|
+
if(manifest.version!==pkg.version||manifest.protocol!==1)throw new Error('Bundle version mismatch.');
|
|
40
|
+
compareDesktopVersions(pkg.version,pkg.version);
|
|
41
|
+
return pkg.version;
|
|
42
|
+
} catch(error) {throw new Error('Cannot verify the installed desktop version. Use a reviewed local installer with --from to repair it.',{cause:error});}
|
|
43
|
+
}
|
|
15
44
|
const nativeExec=(exe,args)=>{
|
|
16
45
|
try {return {stdout:execFileSync(exe,args,{encoding:'utf8',timeout:60000,stdio:['ignore','pipe','pipe']})};}
|
|
17
46
|
catch(error) {throw new Error(`${path.basename(exe)} failed: ${(error.stderr?.toString()||error.message).trim()}`,{cause:error});}
|
|
@@ -19,6 +48,7 @@ const nativeExec=(exe,args)=>{
|
|
|
19
48
|
|
|
20
49
|
export function selectDesktopRelease(releases) {
|
|
21
50
|
if(!Array.isArray(releases))throw new Error('Runlist release information is unavailable.');
|
|
51
|
+
let selected=null;
|
|
22
52
|
for(const release of releases) {
|
|
23
53
|
if(release.draft)continue;
|
|
24
54
|
for(const asset of release.assets??[]) {
|
|
@@ -29,10 +59,11 @@ export function selectDesktopRelease(releases) {
|
|
|
29
59
|
if(!Number.isSafeInteger(asset.size)||asset.size<=0||asset.size>MAX_DOWNLOAD)throw new Error('The Runlist installer size is invalid.');
|
|
30
60
|
const checksum=/^sha256:([a-f0-9]{64})$/i.exec(asset.digest??'')?.[1].toLowerCase();
|
|
31
61
|
if(!checksum)throw new Error('The published Runlist installer is missing its SHA-256 digest.');
|
|
32
|
-
|
|
62
|
+
const candidate={url:url.href,version:match[1],size:asset.size,sha256:checksum};
|
|
63
|
+
if(!selected||compareDesktopVersions(candidate.version,selected.version)>0)selected=candidate;
|
|
33
64
|
}
|
|
34
65
|
}
|
|
35
|
-
return
|
|
66
|
+
return selected;
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
async function latestInstaller(fetcher) {
|
|
@@ -93,7 +124,16 @@ export async function runDesktopInstall(argv,{dryRun=false,platform=process.plat
|
|
|
93
124
|
if(fromAt>=0&&(!from||from.startsWith('--')))throw new Error('--from requires a local Runlist installer.dmg path.');
|
|
94
125
|
if(dryRun){write(`Would install the optional Runlist GUI from ${from?path.resolve(from):'Runlist GitHub releases'} without opening it. No download or files changed.\n`);return {dryRun:true};}
|
|
95
126
|
const installed=desktopLaunch([],{platform,home}).app;
|
|
96
|
-
|
|
127
|
+
let installer;
|
|
128
|
+
if(installed&&!from) {
|
|
129
|
+
const version=await installedDesktopVersion(installed);
|
|
130
|
+
installer=await latestInstaller(fetcher);
|
|
131
|
+
if(compareDesktopVersions(version,installer.version)>=0) {
|
|
132
|
+
write(`Runlist desktop ${version} is current at ${installed}.\n`);
|
|
133
|
+
return {installed,version,alreadyInstalled:true};
|
|
134
|
+
}
|
|
135
|
+
write(`Updating Runlist desktop ${version} → ${installer.version}…\n`);
|
|
136
|
+
}
|
|
97
137
|
if(!exec) {
|
|
98
138
|
// Imported only for this explicit installer; no subprocess or download at import time.
|
|
99
139
|
const {spawnSync}=await import('node:child_process');
|
|
@@ -119,7 +159,7 @@ export async function runDesktopInstall(argv,{dryRun=false,platform=process.plat
|
|
|
119
159
|
image=path.resolve(from);const info=await entry(image);
|
|
120
160
|
if(!image.toLowerCase().endsWith('.dmg')||!info?.isFile()||info.isSymbolicLink()||info.size>MAX_DOWNLOAD)throw new Error('--from must name a regular signed Runlist .dmg installer.');
|
|
121
161
|
} else {
|
|
122
|
-
|
|
162
|
+
installer??=await latestInstaller(fetcher);write(`Downloading Runlist ${installer.version} for Mac Apple Silicon…\n`);
|
|
123
163
|
image=path.join(work,'Runlist.dmg');await download(installer,image,fetcher);
|
|
124
164
|
}
|
|
125
165
|
verifySigned(image,exec);
|
|
@@ -128,6 +168,8 @@ export async function runDesktopInstall(argv,{dryRun=false,platform=process.plat
|
|
|
128
168
|
const source=path.join(mount,'Runlist.app');const info=await entry(source);
|
|
129
169
|
if(!info?.isDirectory()||info.isSymbolicLink())throw new Error('The installer does not contain a regular Runlist.app.');
|
|
130
170
|
verifySigned(source,exec,'dev.reowens.runlist');
|
|
171
|
+
const version=await installedDesktopVersion(source);
|
|
172
|
+
if(installer&&version!==installer.version)throw new Error('The downloaded app version differs from the published installer. Nothing was replaced.');
|
|
131
173
|
stage=path.join(parent,`.Runlist-install-${randomUUID()}.app`);
|
|
132
174
|
exec('/usr/bin/ditto',[source,stage]);
|
|
133
175
|
// Keep normal Mac download identification instead of stripping quarantine.
|
|
@@ -145,7 +187,7 @@ export async function runDesktopInstall(argv,{dryRun=false,platform=process.plat
|
|
|
145
187
|
catch(error){if(backup)await rename(backup,target);throw error;}
|
|
146
188
|
write(`Installed Runlist at ${target}. Open it with runlist desktop.\n`);
|
|
147
189
|
if(backup)write(`Previous app kept at ${backup}.\n`);
|
|
148
|
-
return {installed:target,backup:backup??null};
|
|
190
|
+
return {installed:target,version,backup:backup??null};
|
|
149
191
|
} finally {
|
|
150
192
|
if(stage)await rm(stage,{recursive:true,force:true});
|
|
151
193
|
if(attached) {
|
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,10 @@ 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
|
+
import { desktopLaunch } from './desktop.mjs';
|
|
11
|
+
import { compareDesktopVersions, installedDesktopVersion } from './desktop-install.mjs';
|
|
10
12
|
|
|
11
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
14
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -201,10 +203,10 @@ export function planUpdate(opts, ctx) {
|
|
|
201
203
|
// the moment the CLI moves on. Refresh it here — but only if it is already
|
|
202
204
|
// installed. `update` keeps hosts in lockstep; it never adopts a new one,
|
|
203
205
|
// which stays the job of the explicit `runlist install`.
|
|
204
|
-
if (ctx.opencode?.
|
|
205
|
-
steps.push({ kind: 'opencode', path: ctx.opencode.path });
|
|
206
|
-
} else if (ctx.opencode?.foreign) {
|
|
206
|
+
if (ctx.opencode?.foreign) {
|
|
207
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'] });
|
|
208
210
|
}
|
|
209
211
|
if (ctx.codex?.foreign) {
|
|
210
212
|
steps.push({ kind: 'skip', reason: `${ctx.codex.plugin} was not written by runlist — leaving it alone` });
|
|
@@ -212,15 +214,41 @@ export function planUpdate(opts, ctx) {
|
|
|
212
214
|
steps.push({ kind: 'codex', cmd: ['runlist', 'install', 'codex'] });
|
|
213
215
|
}
|
|
214
216
|
}
|
|
217
|
+
if (!opts.cliOnly && !opts.pluginOnly && ctx.desktop) {
|
|
218
|
+
steps.push({ kind: 'desktop', cmd: ['runlist', 'desktop', 'install'] });
|
|
219
|
+
}
|
|
215
220
|
return steps;
|
|
216
221
|
}
|
|
217
222
|
|
|
218
|
-
export function
|
|
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
|
+
|
|
245
|
+
export async function runUpdate(argv, _config, opts = {}) {
|
|
219
246
|
const check = argv.includes('--check');
|
|
220
247
|
const cliOnly = argv.includes('--cli-only');
|
|
221
248
|
const pluginOnly = argv.includes('--plugin-only');
|
|
222
249
|
const plugin = readInstalledPlugin();
|
|
223
250
|
const legacyPlugin = readInstalledPluginRecords({ id: LEGACY_PLUGIN_ID });
|
|
251
|
+
const desktop = process.platform === 'darwin' && process.arch === 'arm64' ? desktopLaunch([]).app : null;
|
|
224
252
|
|
|
225
253
|
if (check) {
|
|
226
254
|
process.stdout.write(`runlist CLI: ${pkg.version}\n`);
|
|
@@ -246,21 +274,24 @@ export function runUpdate(argv, _config, opts = {}) {
|
|
|
246
274
|
if (!oc.exists) process.stdout.write(dim('runlist opencode: not installed\n'));
|
|
247
275
|
else if (oc.foreign) process.stdout.write(`runlist opencode: ${yellow('unmanaged file — not written by runlist')}\n`);
|
|
248
276
|
else process.stdout.write(`runlist opencode: ${oc.version} ${oc.stale ? yellow('behind — run `runlist update`') : green('in sync')}\n`);
|
|
249
|
-
const cx =
|
|
277
|
+
const cx = currentCodexStatus(pkg.version);
|
|
250
278
|
if (!cx.exists) process.stdout.write(dim('runlist codex: not installed\n'));
|
|
251
279
|
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('
|
|
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`);
|
|
281
|
+
if (desktop) {
|
|
282
|
+
const version = await installedDesktopVersion(desktop);
|
|
283
|
+
process.stdout.write(`runlist desktop: ${version} ${compareDesktopVersions(version, pkg.version) < 0 ? yellow('behind — quit Runlist and run `runlist update`') : green('in sync')}\n`);
|
|
284
|
+
}
|
|
253
285
|
return;
|
|
254
286
|
}
|
|
255
287
|
|
|
256
288
|
const opencode = opencodeStatus({ version: pkg.version });
|
|
257
|
-
const codex =
|
|
258
|
-
const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, legacyPlugin, opencode, codex, hasClaude: which('claude'), hasNpm: which('npm') });
|
|
289
|
+
const codex = currentCodexStatus(pkg.version);
|
|
290
|
+
const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, legacyPlugin, opencode, codex, desktop, hasClaude: which('claude'), hasNpm: which('npm') });
|
|
259
291
|
if (opts.dryRun) {
|
|
260
292
|
for (const step of steps) {
|
|
261
293
|
if (step.kind === 'skip') process.stdout.write(dim(`[dry-run] skip: ${step.reason}\n`));
|
|
262
294
|
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
295
|
else if (step.kind === 'refusal') process.stdout.write(yellow(`[dry-run] Refused: ${step.reason}\n`));
|
|
265
296
|
else process.stdout.write(dim(`[dry-run] Would run: ${step.cmd.join(' ')}${step.cwd ? ` (in ${step.cwd})` : ''}\n`));
|
|
266
297
|
}
|
|
@@ -294,18 +325,17 @@ export function runUpdate(argv, _config, opts = {}) {
|
|
|
294
325
|
process.stdout.write(dim(`skip: ${s.cmd.join(' ')} — the ${s.needs} step it depends on failed\n`));
|
|
295
326
|
continue;
|
|
296
327
|
}
|
|
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
328
|
if (s.reason) process.stdout.write(dim(`${s.reason}\n`));
|
|
304
329
|
process.stdout.write(dim(`$ ${s.cmd.join(' ')}${s.cwd ? ` (in ${s.cwd})` : ''}\n`));
|
|
305
|
-
|
|
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), {
|
|
306
336
|
stdio: 'inherit',
|
|
307
337
|
...(s.cwd ? { cwd: s.cwd } : {}),
|
|
308
|
-
shell: process.platform === 'win32',
|
|
338
|
+
shell: !ownCli && process.platform === 'win32',
|
|
309
339
|
});
|
|
310
340
|
ran = true;
|
|
311
341
|
if (r.status !== 0) {
|
|
@@ -315,24 +345,40 @@ export function runUpdate(argv, _config, opts = {}) {
|
|
|
315
345
|
if (s.kind === 'marketplace') for (const line of claudeMarketplaceRefusalHint(plugin?.marketplace)) process.stdout.write(yellow(`${line}\n`));
|
|
316
346
|
}
|
|
317
347
|
}
|
|
318
|
-
|
|
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')) {
|
|
319
357
|
// npm may have replaced this package while this process was running.
|
|
320
|
-
const
|
|
321
|
-
|
|
358
|
+
const verified = verifyPluginScopeUpdates(plugin, readInstalledPluginRecords({ id: plugin.id }),
|
|
359
|
+
opts.expectedVersion ? { expectedVersion: currentVersion } : { minimumVersion: currentVersion });
|
|
322
360
|
if (!verified.ok) {
|
|
323
361
|
failures.push({ reason: verified.reason });
|
|
324
362
|
process.stdout.write(yellow(`${verified.reason}\n`));
|
|
325
363
|
}
|
|
326
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
|
+
}
|
|
368
|
+
if (desktop && steps.some(step => step.kind === 'desktop') && !failedKinds.has('desktop')) {
|
|
369
|
+
if (compareDesktopVersions(await installedDesktopVersion(desktop), currentVersion) < 0) {
|
|
370
|
+
failures.push({ reason: 'The matching desktop installer is not published yet; retry `runlist update` after the product release completes.' });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
327
373
|
if (failures.length) {
|
|
328
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`));
|
|
329
375
|
if (ran && failures.length < steps.filter(s => s.kind !== 'skip' && s.kind !== 'manual').length) {
|
|
330
|
-
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'));
|
|
331
377
|
}
|
|
332
378
|
process.exitCode = 1;
|
|
333
379
|
return;
|
|
334
380
|
}
|
|
335
381
|
if (ran) {
|
|
336
|
-
process.stdout.write(green('\n✓ restart
|
|
382
|
+
process.stdout.write(green('\n✓ restart the affected host sessions to load the updated integrations.\n'));
|
|
337
383
|
}
|
|
338
384
|
}
|