runlist 0.95.1 → 0.95.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.
- package/README.md +24 -21
- package/bin/runlist.mjs +3 -3
- package/package.json +1 -1
- package/plugins/runlist-codex/.codex-plugin/plugin.json +1 -1
- package/src/desktop-install.mjs +48 -6
- package/src/update.mjs +18 -2
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.2**.
|
|
51
|
+
[Mac installer and release notes](https://github.com/reowens/runlist/releases/tag/desktop-v0.95.2).
|
|
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
|
|
@@ -280,13 +283,13 @@ runlist desktop install --from /path/to/Runlist.dmg # install a local signed DM
|
|
|
280
283
|
|
|
281
284
|
The installer currently supports Apple Silicon Macs. It verifies the published
|
|
282
285
|
download checksum, Runlist developer signature and Apple approval, and installs
|
|
283
|
-
without opening the app. An
|
|
284
|
-
replacement
|
|
285
|
-
changes no files. The
|
|
286
|
+
without opening the app. An older installed app is updated after it has quit;
|
|
287
|
+
replacement keeps a rollback copy. `--dry-run` uses no network and
|
|
288
|
+
changes no files. The current Mac download replaces the withdrawn
|
|
286
289
|
installers. These commands ship
|
|
287
290
|
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
|
-
|
|
291
|
+
be used from this checkout as `node bin/runlist.mjs desktop …`. Normal product releases keep GUI and CLI versions together; an npm install
|
|
292
|
+
does not add a GUI to a machine without one.
|
|
290
293
|
|
|
291
294
|
The optional browser document app is available from this source checkout (unreleased):
|
|
292
295
|
|
|
@@ -828,13 +831,13 @@ local model or server. GUI controls are implemented in the source checkout:
|
|
|
828
831
|
Product goal shows the saved statement, delivery and assessment, with reviewed
|
|
829
832
|
Serves/Clear/Close/Fold actions and retained recovery. Structured plans display
|
|
830
833
|
the comparison with an explanation when assessment changes are unavailable.
|
|
831
|
-
These controls are included in the public desktop package
|
|
832
|
-
|
|
834
|
+
These controls are included in the public desktop package.
|
|
835
|
+
`runlist update` updates an existing Mac app alongside the CLI and integrations.
|
|
833
836
|
|
|
834
837
|
### Plan Homes And Filing Checks
|
|
835
838
|
|
|
836
839
|
This policy is available in CLI 0.93.0. From a source checkout, invoke the
|
|
837
|
-
commands below as `node bin/runlist.mjs …`. The
|
|
840
|
+
commands below as `node bin/runlist.mjs …`. The bundled GUI
|
|
838
841
|
The desktop app also includes a read-only Filing view. The correction
|
|
839
842
|
for category inheritance from status-less hub indexes is included in CLI
|
|
840
843
|
0.93.1.
|
|
@@ -880,7 +883,7 @@ is labeled and suppresses absence/uniqueness findings. Path/type-scoped output
|
|
|
880
883
|
keeps the full hub/ancestor evidence while reporting only selected plans.
|
|
881
884
|
The source checkout also includes a read-only Filing view: open it to scan,
|
|
882
885
|
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
|
|
886
|
+
edits invalidate the report. This view is included in the public desktop package.
|
|
884
887
|
The policy is off when `filing` is unset, false, or `{ enabled: false }`.
|
|
885
888
|
It can replace a repository's older “any link counts” coverage script once
|
|
886
889
|
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
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/update.mjs
CHANGED
|
@@ -7,6 +7,8 @@ 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
9
|
import { codexStatus } 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'));
|
|
@@ -212,15 +214,19 @@ 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 runUpdate(argv, _config, opts = {}) {
|
|
223
|
+
export async function runUpdate(argv, _config, opts = {}) {
|
|
219
224
|
const check = argv.includes('--check');
|
|
220
225
|
const cliOnly = argv.includes('--cli-only');
|
|
221
226
|
const pluginOnly = argv.includes('--plugin-only');
|
|
222
227
|
const plugin = readInstalledPlugin();
|
|
223
228
|
const legacyPlugin = readInstalledPluginRecords({ id: LEGACY_PLUGIN_ID });
|
|
229
|
+
const desktop = process.platform === 'darwin' && process.arch === 'arm64' ? desktopLaunch([]).app : null;
|
|
224
230
|
|
|
225
231
|
if (check) {
|
|
226
232
|
process.stdout.write(`runlist CLI: ${pkg.version}\n`);
|
|
@@ -250,12 +256,16 @@ export function runUpdate(argv, _config, opts = {}) {
|
|
|
250
256
|
if (!cx.exists) process.stdout.write(dim('runlist codex: not installed\n'));
|
|
251
257
|
else if (cx.foreign) process.stdout.write(`runlist codex: ${yellow('unmanaged plugin directory')}\n`);
|
|
252
258
|
else process.stdout.write(`runlist codex: ${cx.version} ${cx.stale ? yellow('behind — run `runlist update`') : green('in sync')}\n`);
|
|
259
|
+
if (desktop) {
|
|
260
|
+
const version = await installedDesktopVersion(desktop);
|
|
261
|
+
process.stdout.write(`runlist desktop: ${version} ${compareDesktopVersions(version, pkg.version) < 0 ? yellow('behind — quit Runlist and run `runlist update`') : green('in sync')}\n`);
|
|
262
|
+
}
|
|
253
263
|
return;
|
|
254
264
|
}
|
|
255
265
|
|
|
256
266
|
const opencode = opencodeStatus({ version: pkg.version });
|
|
257
267
|
const codex = codexStatus({ version: pkg.version });
|
|
258
|
-
const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, legacyPlugin, opencode, codex, hasClaude: which('claude'), hasNpm: which('npm') });
|
|
268
|
+
const steps = planUpdate({ cliOnly, pluginOnly }, { plugin, legacyPlugin, opencode, codex, desktop, hasClaude: which('claude'), hasNpm: which('npm') });
|
|
259
269
|
if (opts.dryRun) {
|
|
260
270
|
for (const step of steps) {
|
|
261
271
|
if (step.kind === 'skip') process.stdout.write(dim(`[dry-run] skip: ${step.reason}\n`));
|
|
@@ -324,6 +334,12 @@ export function runUpdate(argv, _config, opts = {}) {
|
|
|
324
334
|
process.stdout.write(yellow(`${verified.reason}\n`));
|
|
325
335
|
}
|
|
326
336
|
}
|
|
337
|
+
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
|
+
if (compareDesktopVersions(await installedDesktopVersion(desktop), currentVersion) < 0) {
|
|
340
|
+
failures.push({ reason: 'The matching desktop installer is not published yet; retry `runlist update` after the product release completes.' });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
327
343
|
if (failures.length) {
|
|
328
344
|
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
345
|
if (ran && failures.length < steps.filter(s => s.kind !== 'skip' && s.kind !== 'manual').length) {
|