@petercjl/codex-own 0.5.1 → 0.6.0
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 +6 -0
- package/bin/codex-own.mjs +6 -4
- package/capabilities.json +14 -0
- package/lib/app-update.mjs +25 -13
- package/lib/binding.mjs +39 -0
- package/lib/manager.mjs +27 -8
- package/lib/tool-runtime.mjs +18 -5
- package/package.json +790 -612
- package/release.json +789 -611
- package/skills/codex-own/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -22,6 +22,12 @@ Mac 任务使用运行时目录内的私有 zsh 启动文件和 Bash `BASH_ENV`
|
|
|
22
22
|
|
|
23
23
|
从 0.4.x 的应用内更新按钮升级时,新应用首次打开会显示“正在准备 codex-own 工作环境”,自动从 npm 补齐工具并验证后再进入桌面。下载失败或取消后可重新打开重试,已验证分片会保留。
|
|
24
24
|
|
|
25
|
+
## 独立组件版本
|
|
26
|
+
|
|
27
|
+
组件绑定合同 2 分别记录 npm 管理器版本、桌面成品版本和 Node/npm/Python 工具版本。管理器升级可以继续使用已验证的旧桌面和工具;版本号不同是正常的。更新检查按绑定的实际组件判断,相同桌面无需退出、重新下载或切换程序。工具更新保留桌面,重启后采用新工具路径。
|
|
28
|
+
|
|
29
|
+
现有 0.5.1 客户端首次切换需要一个支持此合同的桥接桌面版本。之后的 CLI、Skill 和说明文档小更新可以走轻量发布。开发和发布流程见仓库中的 `ARTIFACT-LIFECYCLE.md`;公开 npm 当前版本以注册表为准。
|
|
30
|
+
|
|
25
31
|
## 应用内更新
|
|
26
32
|
|
|
27
33
|
带有 npm 更新组件的桌面版本会在启动 15 秒后、以及运行期间每 6 小时检查稳定版。也可以打开菜单“更新 → 检查 codex-own 更新…”。发现新版后点击“下载更新”,下载期间可以继续使用 App。下载完成后结束正在运行的任务,点击“重启并安装”;更新进程会等待 App 退出,备份程序及独立数据,再安装并重新打开。
|
package/bin/codex-own.mjs
CHANGED
|
@@ -5,28 +5,30 @@ import os from 'node:os';
|
|
|
5
5
|
import {pkg,packageRoot,read,fail,paths,doctor,latest,state,install,launch,rollback,hash,rejectLinks,configure} from '../lib/manager.mjs';
|
|
6
6
|
import {manifestFor} from '../lib/manager.mjs';
|
|
7
7
|
import {installRuntime,runtimeDoctor,rollbackRuntime,runRuntime,toolEnv} from '../lib/tool-runtime.mjs';
|
|
8
|
+
import {components} from '../lib/binding.mjs';
|
|
8
9
|
const args=process.argv.slice(2),[command,sub]=args;
|
|
9
10
|
const flag=n=>args.includes('--'+n),value=n=>{const i=args.indexOf('--'+n);return i<0?undefined:args[i+1];};
|
|
10
11
|
const yes=()=>{if(!flag('yes'))fail('CONFIRMATION_REQUIRED','Mutating commands require --yes');};
|
|
12
|
+
function toolSelectionGate(){const p=paths();if(!state(p))return;const file=path.join(p.target,process.platform==='darwin'?'Contents/Resources':'resources','own-updater','desktop-version.json'),meta=fs.existsSync(file)?read(file):null;if(meta?.updaterContract!==2||meta.runtimeContract!==1)fail('INCOMPATIBLE_BINDING','Upgrade the desktop to component contract 2 before independently changing its tools');}
|
|
11
13
|
async function skill(){const source=path.join(packageRoot,'skills','codex-own');if(sub==='source')return {ok:true,source,version:pkg.version};const agent=value('agent')||'codex';if(!['codex','sealseek'].includes(agent))fail('UNSUPPORTED_AGENT',agent);const base=value('target-root')||(agent==='codex'?path.join(os.homedir(),'.codex','skills'):null);if(!base)fail('TARGET_REQUIRED','Provide the configured Agent skill root with --target-root');const dest=path.join(base,'codex-own'),marker=path.join(dest,'.codex-own-managed.json');rejectLinks(base);rejectLinks(dest);const exists=fs.existsSync(dest);const owned=exists&&fs.existsSync(marker)&&read(marker).package===pkg.name;const srcHash=hash(fs.readFileSync(path.join(source,'SKILL.md')));if(sub==='status')return {ok:true,source,destination:dest,installed:exists,managed:owned,current:owned&&hash(fs.readFileSync(path.join(dest,'SKILL.md')))===srcHash};if(!['install','update'].includes(sub))fail('INVALID_COMMAND','skill source/status/install/update');yes();if(exists&&!owned)fail('UNMANAGED_TARGET','Existing skill is not managed');let backup=null;if(exists){backup=dest+'.backup-'+Date.now();fs.cpSync(dest,backup,{recursive:true,force:false,errorOnExist:true});}fs.mkdirSync(dest,{recursive:true});const ui=path.join(dest,'agents','openai.yaml'),customUI=fs.existsSync(ui)?fs.readFileSync(ui):null;fs.cpSync(source,dest,{recursive:true});if(customUI)fs.writeFileSync(ui,customUI);fs.writeFileSync(marker,JSON.stringify({package:pkg.name,version:pkg.version,hash:srcHash}));if(hash(fs.readFileSync(path.join(dest,'SKILL.md')))!==srcHash)fail('VERIFY_FAILED','Skill copy mismatch');return {ok:true,source,destination:dest,backup,version:pkg.version};}
|
|
12
14
|
async function main(){
|
|
13
15
|
if(!command||command==='help'||command==='--help')return {commands:['version','capabilities','doctor','install --yes [--adopt]','configure --api-key-file <file> --yes','update check','update install --yes','runtime install|update|doctor|env|run|rollback','launch','rollback --restore-data --yes','skill source|status|install|update'],note:'Application data and private tools remain outside npm manager packages. Quit the app before desktop updates. Manager, desktop and tool runtime versions are reported separately.'};
|
|
14
|
-
if(command==='version'||command==='--version')return {manager:pkg.version,desktop:state()?.version||null};
|
|
16
|
+
if(command==='version'||command==='--version')return {manager:pkg.version,desktop:state()?.version||null,desktopIdentity:state()?.desktop?.identity||null,runtime:toolEnv.active()?.version||null};
|
|
15
17
|
if(command==='capabilities')return read(path.join(packageRoot,'capabilities.json'));
|
|
16
18
|
if(command==='doctor')return {...doctor(),runtime:runtimeDoctor()};
|
|
17
19
|
if(command==='runtime'){
|
|
18
20
|
if(sub==='doctor')return runtimeDoctor();
|
|
19
21
|
if(sub==='env'){const env=toolEnv.environment();if(!env)fail('RUNTIME_NOT_INSTALLED','Install private tools first');return {ok:true,contract:1,root:toolEnv.runtimeRoot(),environment:Object.fromEntries(Object.entries(env).filter(([k])=>['PATH','ZDOTDIR','BASH_ENV','CODEX_OWN_NODE','CODEX_OWN_PYTHON','CODEX_OWN_RUNTIME_VERSION','AGENT_WORKBENCH_NPM_CLI'].includes(k)||/^(NPM_CONFIG_|PYTHON|PIP_)/.test(k)))};}
|
|
20
22
|
if(sub==='run'){const i=args.indexOf('--');process.exitCode=runRuntime(i<0?args.slice(2):args.slice(i+1));return;}
|
|
21
|
-
if(sub==='install'||sub==='update'){yes();const m=await manifestFor(value('version')||await latest());return installRuntime(m.runtimes);}
|
|
22
|
-
if(sub==='rollback'){yes();return rollbackRuntime();}
|
|
23
|
+
if(sub==='install'||sub==='update'){yes();const m=await manifestFor(value('version')||await latest());if(toolEnv.active()?.version!==m.runtimes.version)toolSelectionGate();return installRuntime(m.runtimes,{selection:'user-update'});}
|
|
24
|
+
if(sub==='rollback'){yes();if(toolEnv.active()?.previous)toolSelectionGate();return rollbackRuntime();}
|
|
23
25
|
return {commands:['runtime install --yes','runtime update --yes','runtime doctor','runtime env','runtime run -- <node|npm|npx|python|python3|pip> [args]','runtime rollback --yes']};
|
|
24
26
|
}
|
|
25
27
|
if(command==='launch')return launch();
|
|
26
28
|
if(command==='skill')return skill();
|
|
27
29
|
if(command==='configure'){yes();return configure(value('api-key-file'));}
|
|
28
30
|
if(command==='install'){yes();return install({adopt:flag('adopt'),version:value('version'),archive:value('archive'),manifest:value('manifest')?read(value('manifest')):undefined});}
|
|
29
|
-
if(command==='update'){const v=await latest();if(sub==='check')return {ok:true,manager:pkg.version,desktop:state()?.version||null,available:v,
|
|
31
|
+
if(command==='update'){const v=await latest();if(sub==='check'){const m=await manifestFor(v),c=components(state(),m,paths().platform,toolEnv.active()?.version);return {ok:true,manager:pkg.version,desktop:state()?.version||null,available:v,...c,managerUpdate:pkg.version!==v,updateAvailable:c.desktopUpdate||c.runtimeUpdate};}if(sub==='install'){yes();return install({version:v});}}
|
|
30
32
|
if(command==='rollback'){yes();return rollback({restoreData:flag('restore-data')});}
|
|
31
33
|
fail('INVALID_COMMAND','See codex-own help');
|
|
32
34
|
}
|
package/capabilities.json
CHANGED
|
@@ -80,5 +80,19 @@
|
|
|
80
80
|
"npmGlobalPackages": "private runtimes/npm-global prefix",
|
|
81
81
|
"rollback": "verify predecessor and switch receipt; no conversation data restore",
|
|
82
82
|
"firstBootstrap": "existing Node >=22 and npm required"
|
|
83
|
+
},
|
|
84
|
+
"releaseBinding": {
|
|
85
|
+
"contract": 2,
|
|
86
|
+
"managerVersion": "npm package version",
|
|
87
|
+
"desktopVersion": "release.json.version",
|
|
88
|
+
"runtimeVersion": "release.json.runtimes.version",
|
|
89
|
+
"identity": "SHA256 of canonical component metadata and raw program/tool assets",
|
|
90
|
+
"compatibility": {
|
|
91
|
+
"manager": 2,
|
|
92
|
+
"embeddedUpdater": 2,
|
|
93
|
+
"toolEnvironment": 1
|
|
94
|
+
},
|
|
95
|
+
"sameDesktop": "zero desktop download and activation; app may remain open",
|
|
96
|
+
"legacy": "one bridge desktop upgrade required before ordinary manager-only reuse"
|
|
83
97
|
}
|
|
84
98
|
}
|
package/lib/app-update.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {latest,manifestFor,validateManifest,paths,state,locked,rejectLinks,exclusive,digestFile,tarExecutable,install,fail,running,launch} from './manager.mjs';
|
|
3
|
+
import {latest,manifestFor,validateManifest,paths,state,installedBinding,locked,rejectLinks,exclusive,digestFile,tarExecutable,install,fail,running,launch} from './manager.mjs';
|
|
4
4
|
import {assembleNpmAsset} from './npm-assets.mjs';
|
|
5
5
|
import {reuseInstalledBlocks} from './content-blocks.mjs';
|
|
6
|
-
import {ensureRuntime} from './tool-runtime.mjs';
|
|
6
|
+
import {ensureRuntime,toolEnv} from './tool-runtime.mjs';
|
|
7
|
+
import {components} from './binding.mjs';
|
|
7
8
|
|
|
8
9
|
// Desktop release versions use the stable npm channel, independently of upstream's app version.
|
|
9
10
|
export function isNewer(candidate,current) {
|
|
@@ -14,8 +15,15 @@ export function isNewer(candidate,current) {
|
|
|
14
15
|
}
|
|
15
16
|
export async function checkUpdate(current,{resolve=latest,manifest=manifestFor,p=paths()}={}) {
|
|
16
17
|
const available=await resolve();
|
|
18
|
+
const m=await manifest(available);
|
|
19
|
+
if(m.binding){
|
|
20
|
+
validateManifest(m,p.platform);
|
|
21
|
+
if(m.binding.managerVersion!==available)fail('INVALID_MANIFEST','Manager binding differs from npm');
|
|
22
|
+
const s=state(p)||{version:current},c=components(s,m,p.platform,toolEnv.active(path.join(p.root,'runtimes'))?.version);
|
|
23
|
+
return {ok:true,current,available:m.version,managerAvailable:available,...c,updateAvailable:c.desktopUpdate||c.runtimeUpdate};
|
|
24
|
+
}
|
|
17
25
|
const updateAvailable=isNewer(available,current);
|
|
18
|
-
if(updateAvailable){
|
|
26
|
+
if(updateAvailable){if(m.schema!==2)fail('NPM_ASSETS_REQUIRED','This update is not distributed through npm');if(m.version!==available)fail('INVALID_MANIFEST','Manifest version does not match npm');validateManifest(m,p.platform);}
|
|
19
27
|
return {ok:true,current,available,updateAvailable};
|
|
20
28
|
}
|
|
21
29
|
export async function prepareUpdate(current,{p=paths(),resolve=latest,manifest=manifestFor,assemble=assembleNpmAsset,report=()=>{},signal}={}) {
|
|
@@ -23,20 +31,21 @@ export async function prepareUpdate(current,{p=paths(),resolve=latest,manifest=m
|
|
|
23
31
|
if(state(p)?.version!==current)fail('UNMANAGED_TARGET','The running desktop must match its managed installation');
|
|
24
32
|
const checked=await checkUpdate(current,{resolve,manifest,p});
|
|
25
33
|
if(!checked.updateAvailable)return {...checked,unchanged:true};
|
|
26
|
-
const m=await manifest(checked.available),asset=validateManifest(m,p.platform);
|
|
34
|
+
const m=await manifest(checked.managerAvailable||checked.available),asset=validateManifest(m,p.platform);
|
|
27
35
|
if(m.schema!==2||m.version!==checked.available)fail('INVALID_MANIFEST','Unexpected desktop manifest');
|
|
28
36
|
const base=path.join(p.manager,'prepared');rejectLinks(base);fs.mkdirSync(base,{recursive:true,mode:0o700});
|
|
29
37
|
const dir=fs.mkdtempSync(path.join(base,'update-')),archive=path.join(dir,'desktop.tar.gz');
|
|
30
38
|
const cache=path.join(p.manager,'npm-assets');rejectLinks(cache);
|
|
31
39
|
try{
|
|
32
40
|
// The app may stay open: only read program resources, never user data.
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
const native=checked.desktopUpdate!==false;
|
|
42
|
+
if(native&&p.target){rejectLinks(p.target);await reuseInstalledBlocks(asset,p.target,cache,{report,signal});}
|
|
43
|
+
const transfer=native?await assemble(asset,archive,cache,tarExecutable(),{report,signal}):{downloadBytes:0,downloadBlocks:0};
|
|
35
44
|
if(m.runtimes)await ensureRuntime(m.runtimes,{p,report,signal});
|
|
36
45
|
// Verify even when a download adapter is supplied.
|
|
37
|
-
if(fs.statSync(archive).size!==asset.size||await digestFile(archive)!==asset.sha256)fail('CHECKSUM_MISMATCH','Prepared update checksum mismatch');
|
|
46
|
+
if(native&&(fs.statSync(archive).size!==asset.size||await digestFile(archive)!==asset.sha256))fail('CHECKSUM_MISMATCH','Prepared update checksum mismatch');
|
|
38
47
|
const receipt=path.join(dir,'receipt.json');
|
|
39
|
-
exclusive(receipt,{schema:1,current,version:m.version,platform:p.platform,manifest:m,...(transfer?{transfer}:{})});
|
|
48
|
+
exclusive(receipt,{schema:m.binding?2:1,current,version:m.version,platform:p.platform,manifest:m,...(m.binding?{desktopUpdate:native,runtimeVersion:m.runtimes.version}:{}),...(transfer?{transfer}:{})});
|
|
40
49
|
return {...checked,receipt,...(transfer?{transfer}:{})};
|
|
41
50
|
}catch(e){fs.rmSync(dir,{recursive:true,force:true});throw e;}
|
|
42
51
|
});
|
|
@@ -45,13 +54,16 @@ export function readPrepared(receipt,p=paths()) {
|
|
|
45
54
|
const base=path.join(p.manager,'prepared');rejectLinks(receipt);
|
|
46
55
|
if(path.basename(receipt)!=='receipt.json'||path.dirname(path.dirname(path.resolve(receipt)))!==path.resolve(base))fail('UNSAFE_PATH','Unexpected update receipt');
|
|
47
56
|
const r=JSON.parse(fs.readFileSync(receipt,'utf8'));
|
|
48
|
-
if(
|
|
57
|
+
if(fs.existsSync(receipt+'.applied.json'))fail('UPDATE_ALREADY_APPLIED','This prepared release was already applied');
|
|
58
|
+
if(![1,2].includes(r.schema)||r.platform!==p.platform||r.version!==r.manifest?.version||r.manifest?.schema!==2||(r.schema===1&&!isNewer(r.version,r.current))||(r.schema===2&&(!r.manifest.binding||r.runtimeVersion!==r.manifest.runtimes?.version)))fail('INVALID_MANIFEST','Invalid prepared update');
|
|
49
59
|
validateManifest(r.manifest,p.platform);
|
|
50
|
-
return {...r,archive:path.join(path.dirname(receipt),'desktop.tar.gz')};
|
|
60
|
+
return {...r,archive:r.schema===2&&r.desktopUpdate===false?undefined:path.join(path.dirname(receipt),'desktop.tar.gz')};
|
|
51
61
|
}
|
|
52
62
|
export async function applyPrepared(receipt,{p=paths(),apply=install}={}) {
|
|
53
63
|
const r=readPrepared(receipt,p);
|
|
54
|
-
|
|
64
|
+
const result=await apply({version:r.manifest.binding?.managerVersion||r.version,manifest:r.manifest,archive:r.archive,expectedCurrent:r.current});
|
|
65
|
+
if(result.ok===true&&!fs.existsSync(receipt+'.applied.json'))exclusive(receipt+'.applied.json',{ok:true,version:r.version,managerVersion:r.manifest.binding?.managerVersion||r.version});
|
|
66
|
+
return result;
|
|
55
67
|
}
|
|
56
68
|
export async function restorePrepared(current,p=paths()) {
|
|
57
69
|
const base=path.join(p.manager,'prepared');rejectLinks(base);
|
|
@@ -59,9 +71,9 @@ export async function restorePrepared(current,p=paths()) {
|
|
|
59
71
|
const candidates=[];
|
|
60
72
|
for(const name of fs.readdirSync(base).filter(n=>/^update-[a-zA-Z0-9]+$/.test(n))){
|
|
61
73
|
const receipt=path.join(base,name,'receipt.json');
|
|
62
|
-
try{const r=readPrepared(receipt,p);if(r.current===current&&fs.statSync(r.archive).size===validateManifest(r.manifest,p.platform).size&&await digestFile(r.archive)===validateManifest(r.manifest,p.platform).sha256)candidates.push({ok:true,current,available:r.version,updateAvailable:true,receipt});}catch{}
|
|
74
|
+
try{const r=readPrepared(receipt,p),selected=installedBinding(p)?.binding?.managerVersion,target=r.manifest.binding?.managerVersion;if(target&&selected&&(selected===target||isNewer(selected,target)))continue;if(r.current===current&&(!r.archive||(fs.statSync(r.archive).size===validateManifest(r.manifest,p.platform).size&&await digestFile(r.archive)===validateManifest(r.manifest,p.platform).sha256)))candidates.push({ok:true,current,available:r.version,managerAvailable:target,runtimeVersion:r.runtimeVersion,updateAvailable:true,receipt});}catch{}
|
|
63
75
|
}
|
|
64
|
-
return candidates.sort((a,b)=>isNewer(a.available,b.available)?-1:isNewer(b.available,a.available)?1:0)[0]||null;
|
|
76
|
+
return candidates.sort((a,b)=>isNewer(a.managerAvailable||a.available,b.managerAvailable||b.available)?-1:isNewer(b.managerAvailable||b.available,a.managerAvailable||a.available)?1:0)[0]||null;
|
|
65
77
|
}
|
|
66
78
|
export async function waitForExit({isRunning=running,timeoutMs=120000,intervalMs=500,sleep=ms=>new Promise(r=>setTimeout(r,ms))}={}) {
|
|
67
79
|
const end=Date.now()+timeoutMs;
|
package/lib/binding.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Release bindings select immutable components; npm versions identify managers.
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
export const MANAGER_CONTRACT=2, UPDATER_CONTRACT=2, RUNTIME_CONTRACT=1;
|
|
4
|
+
export const canonical=value=>JSON.stringify(value,(_key,v)=>v&&typeof v==='object'&&!Array.isArray(v)?Object.fromEntries(Object.keys(v).sort().map(k=>[k,v[k]])):v);
|
|
5
|
+
export const identity=value=>crypto.createHash('sha256').update(canonical(value===undefined?null:value)).digest('hex');
|
|
6
|
+
const hash=v=>/^[a-f0-9]{64}$/.test(v||'');
|
|
7
|
+
const semver=v=>/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(v||'');
|
|
8
|
+
const bad=message=>{throw Object.assign(Error(message),{code:'INCOMPATIBLE_BINDING'});};
|
|
9
|
+
export function validateBinding(m,{managerVersion,requireProof=false}={}){
|
|
10
|
+
const b=m.binding;if(!b){if(requireProof)bad('Missing component binding');return null;}
|
|
11
|
+
if(b.schema!==1||b.managerContract!==MANAGER_CONTRACT||!semver(b.managerVersion)||(managerVersion&&b.managerVersion!==managerVersion))bad('Unsupported manager binding');
|
|
12
|
+
if(b.desktop?.artifactVersion!==m.version||b.desktop.identity!==identity(m.platforms)||!hash(b.desktop.inputFingerprint)||![1,UPDATER_CONTRACT].includes(b.desktop.updaterContract))bad('Desktop identity or contract mismatch');
|
|
13
|
+
if(!m.runtimes||b.runtimes?.artifactVersion!==m.runtimes.version||b.runtimes.identity!==identity(m.runtimes)||!hash(b.runtimes.inputFingerprint)||b.runtimes.contract!==RUNTIME_CONTRACT)bad('Runtime identity or contract mismatch');
|
|
14
|
+
for(const component of [b.desktop,b.runtimes,...(b.managerSource?[{source:b.managerSource}]:[])]){
|
|
15
|
+
const s=component.source;for(const record of [s,...(s?.publication?[s.publication]:[])])if(!record||record.repository!=='petercjl/codex-own'||!/^[a-f0-9]{40}$/.test(record.ref||'')||!/^\d+$/.test(String(record.run||''))||!record.workflow?.startsWith('.github/workflows/')||(record.triggerRef&&!/^[a-f0-9]{40}$/.test(record.triggerRef)))bad('Missing original build provenance');
|
|
16
|
+
}
|
|
17
|
+
if(requireProof&&(b.desktop.updaterContract!==UPDATER_CONTRACT||b.desktop.verifiedPlatforms?.join(',')!=='darwin-arm64,win32-x64'||b.runtimes.verifiedPlatforms?.join(',')!=='darwin-arm64,win32-x64'))bad('A verified bridge desktop and both runtime platforms are required for ordinary reuse');
|
|
18
|
+
if(requireProof&&(!semver(b.desktop.publishedIn)||!semver(b.runtimes.publishedIn)||!b.managerSource))bad('Missing original publication binding');
|
|
19
|
+
if(requireProof)for(const platform of ['darwin-arm64','win32-x64']){const a=m.platforms[platform],probe=a?.probe;if(!probe||!hash(probe.sha256)||!Number.isSafeInteger(probe.offset)||!Number.isSafeInteger(probe.size)||probe.offset<0||probe.size<1||probe.offset+probe.size>a.size)bad('Missing pinned native invocation proof');}
|
|
20
|
+
if(requireProof)for(const platform of ['darwin-arm64','win32-x64']){const c=b.runtimes.components?.[platform],a=m.runtimes.platforms[platform];if(!c||c.node?.identity!==a.node.sha256||c.python?.identity!==a.python.sha256||!hash(c.npm?.identity)||!semver(c.npm.version))bad('Missing independent Node/npm/Python identities');}
|
|
21
|
+
return b;
|
|
22
|
+
}
|
|
23
|
+
export function packageManifest(p,version){
|
|
24
|
+
const m=p.ownDesktopManifest;
|
|
25
|
+
if(p.name!=='@petercjl/codex-own'||p.version!==version||m?.schema!==2)bad('Unexpected npm package identity');
|
|
26
|
+
if(m.binding)validateBinding(m,{managerVersion:version});else if(m.version!==version)bad('Legacy manager/desktop version mismatch');
|
|
27
|
+
return m;
|
|
28
|
+
}
|
|
29
|
+
export function desktopIdentity(m,platform){return m.binding?m.platforms[platform].sha256:null;}
|
|
30
|
+
export function sameDesktop(s,m,platform){
|
|
31
|
+
if(!s)return false;const id=desktopIdentity(m,platform);
|
|
32
|
+
return id?(s.desktop?s.desktop.identity===id:s.version===m.version):s.version===m.version;
|
|
33
|
+
}
|
|
34
|
+
export function components(s,m,platform,runtimeVersion){
|
|
35
|
+
if(s?.desktop&&s.version===m.version&&s.desktop.identity!==m.platforms[platform].sha256)bad('Published desktop version cannot change identity');
|
|
36
|
+
const parse=v=>/^\d+\.\d+\.\d+$/.test(v||'')?v.split('.').map(Number):null,a=parse(s?.version),b=parse(m.version);
|
|
37
|
+
const newerInstalled=!!a&&!!b&&a.some((n,i)=>a.slice(0,i).every((x,j)=>x===b[j])&&n>b[i]);
|
|
38
|
+
return {managerVersion:m.binding?.managerVersion||m.version,desktopVersion:m.version,runtimeVersion:m.runtimes?.version||null,newerInstalled,desktopUpdate:!newerInstalled&&!sameDesktop(s,m,platform),runtimeUpdate:!newerInstalled&&!!m.runtimes&&runtimeVersion!==m.runtimes.version};
|
|
39
|
+
}
|
package/lib/manager.mjs
CHANGED
|
@@ -7,7 +7,8 @@ import {execFileSync, spawn} from 'node:child_process';
|
|
|
7
7
|
import {networkFetch, verifiedDownload, progress} from './network.mjs';
|
|
8
8
|
import {validateParts, assembleNpmAsset} from './npm-assets.mjs';
|
|
9
9
|
import {reuseInstalledBlocks} from './content-blocks.mjs';
|
|
10
|
-
import {ensureRuntime,validateRuntime} from './tool-runtime.mjs';
|
|
10
|
+
import {ensureRuntime,validateRuntime,restoreRuntimeSelection} from './tool-runtime.mjs';
|
|
11
|
+
import {validateBinding,packageManifest,sameDesktop,identity as bindingHash} from './binding.mjs';
|
|
11
12
|
|
|
12
13
|
export const packageRoot = path.resolve(import.meta.dirname, '..');
|
|
13
14
|
export const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'),'utf8'));
|
|
@@ -26,6 +27,7 @@ export function rejectLinks(p) {
|
|
|
26
27
|
export function validateManifest(m, platform) {
|
|
27
28
|
if(![1,2].includes(m.schema)||m.repository!=='petercjl/codex-own'||!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(m.version))fail('INVALID_MANIFEST','Invalid release identity');
|
|
28
29
|
if(m.runtimes)validateRuntime(m.runtimes,platform);
|
|
30
|
+
validateBinding(m);
|
|
29
31
|
const a=m.platforms?.[platform];if(!a)fail('PLATFORM_UNAVAILABLE',`No verified build for ${platform} in ${m.version}`);
|
|
30
32
|
if(!/^[a-f0-9]{64}$/.test(a.sha256)||!Number.isSafeInteger(a.size)||a.size<=0)fail('INVALID_MANIFEST','Invalid archive checksum/size');
|
|
31
33
|
if(m.schema===2){validateParts(a);if(a.entry!==(platform==='darwin-arm64'?'codex-own.app':'app'))fail('INVALID_MANIFEST','Unexpected archive entry');return a;}
|
|
@@ -77,7 +79,7 @@ export function snapshot(p,dir) {
|
|
|
77
79
|
fs.mkdirSync(dir,{recursive:true,mode:0o700});
|
|
78
80
|
for(const name of ['core','desktop'])if(fs.existsSync(p[name]))fs.cpSync(p[name],path.join(dir,name),{recursive:true,errorOnExist:true,force:false});
|
|
79
81
|
}
|
|
80
|
-
export function switchApp({p,source,version,adopt=false,identity=appIdentity,isRunning=running,backupData=snapshot}) {
|
|
82
|
+
export function switchApp({p,source,version,manifest,runtimeSelection,adopt=false,identity=appIdentity,isRunning=running,backupData=snapshot}) {
|
|
81
83
|
rejectLinks(p.target);rejectLinks(p.root);
|
|
82
84
|
if(isRunning(p).length)fail('APP_RUNNING','Quit codex-own before updating');
|
|
83
85
|
const previous=state(p), exists=fs.existsSync(p.target);
|
|
@@ -91,25 +93,39 @@ export function switchApp({p,source,version,adopt=false,identity=appIdentity,isR
|
|
|
91
93
|
const journal=path.join(p.manager,'transaction-'+id+'.json');exclusive(journal,{target:p.target,old,next,version,previous,backup});
|
|
92
94
|
let moved=false,activated=false;
|
|
93
95
|
try {if(exists){fs.renameSync(p.target,old);moved=true;}fs.renameSync(next,p.target);activated=true;
|
|
94
|
-
|
|
96
|
+
const active=path.join(p.root,'runtimes','active.json');
|
|
97
|
+
writeState(p,{schema:1,identity:'local.codex-own',version,...(manifest?{desktop:{version,identity:manifest.platforms[p.platform].sha256},releaseManifest:manifest}:{}),target:p.target,installedAt:new Date().toISOString(),previous:exists?{app:old,data:path.join(backup,'data'),state:previous,runtimeSelection:runtimeSelection===undefined?(fs.existsSync(active)?read(active):null):runtimeSelection}:null});
|
|
95
98
|
}catch(e){if(activated&&fs.existsSync(p.target))fs.renameSync(p.target,path.join(path.dirname(p.target),'.codex-own-failed-'+id));if(moved)fs.renameSync(old,p.target);throw e;}
|
|
96
99
|
exclusive(path.join(p.manager,'transaction-'+id+'.complete.json'),{ok:true});
|
|
97
100
|
return {ok:true,version,target:p.target,backup:exists?old:null,dataBackup:exists?path.join(backup,'data'):null};
|
|
98
101
|
}
|
|
99
102
|
export async function locked(p,fn){rejectLinks(p.root);fs.mkdirSync(p.manager,{recursive:true,mode:0o700});const lock=path.join(p.manager,'operation.lock');let fd;try{fd=fs.openSync(lock,'wx',0o600);}catch{fail('BUSY','Another operation or interrupted lock exists; inspect doctor before recovery');}try{fs.writeSync(fd,JSON.stringify({pid:process.pid,time:new Date().toISOString()}));return await fn();}finally{fs.closeSync(fd);fs.unlinkSync(lock);}}
|
|
100
103
|
export async function latest() {const r=await networkFetch('https://registry.npmjs.org/@petercjl%2fcodex-own/latest',{signal:AbortSignal.timeout(30000)});if(!r.ok)fail('REGISTRY_UNAVAILABLE',`Registry HTTP ${r.status}`);const p=await r.json();if(p.name!==pkg.name)fail('INVALID_MANIFEST','Registry package mismatch');return p.version;}
|
|
101
|
-
export async function manifestFor(v){if(v===pkg.version){const local=read(path.join(packageRoot,'release.json'));if(local.version===v)return local;}if(!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(v))fail('INVALID_VERSION','Invalid version');const r=await networkFetch(`https://registry.npmjs.org/@petercjl%2fcodex-own/${v}`,{signal:AbortSignal.timeout(30000)});if(!r.ok)fail('MANIFEST_UNAVAILABLE',`npm HTTP ${r.status}`);
|
|
104
|
+
export async function manifestFor(v){if(v===pkg.version){const local=read(path.join(packageRoot,'release.json'));if((local.binding?.managerVersion===v||local.version===v)&&Object.keys(local.platforms||{}).length){validateBinding(local,{managerVersion:v});return local;}}if(!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(v))fail('INVALID_VERSION','Invalid version');const r=await networkFetch(`https://registry.npmjs.org/@petercjl%2fcodex-own/${v}`,{signal:AbortSignal.timeout(30000)});if(!r.ok)fail('MANIFEST_UNAVAILABLE',`npm HTTP ${r.status}`);return packageManifest(await r.json(),v);}
|
|
105
|
+
export function installedBinding(p=paths()){const f=path.join(p.manager,'binding.json');return fs.existsSync(f)?read(f).manifest:null;}
|
|
106
|
+
export function recordBinding(p,m){
|
|
107
|
+
const f=path.join(p.manager,'binding.json');rejectLinks(f);const old=installedBinding(p);if(old&&bindingHash(old)===bindingHash(m))return;
|
|
108
|
+
if(fs.existsSync(f))fs.copyFileSync(f,f+'.backup-'+crypto.randomUUID(),fs.constants.COPYFILE_EXCL);
|
|
109
|
+
const next=f+'.new-'+crypto.randomUUID();exclusive(next,{schema:1,manifest:m});fs.renameSync(next,f);
|
|
110
|
+
}
|
|
102
111
|
export async function configure(keyFile){if(!keyFile)fail('KEY_FILE_REQUIRED','Provide --api-key-file');const key=fs.readFileSync(keyFile,'utf8').trim();if(!/^sk-[A-Za-z0-9_-]{10,}$/.test(key))fail('INVALID_KEY_FILE','Expected a single API key');const p=paths();return locked(p,async()=>{if(running(p).length)fail('APP_RUNNING','Quit codex-own first');rejectLinks(p.core);fs.mkdirSync(p.core,{recursive:true,mode:0o700});fs.chmodSync(p.core,0o700);
|
|
103
112
|
if(process.platform==='win32'){const sid=execFileSync('powershell.exe',['-NoProfile','-Command','[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value'],{encoding:'utf8'}).trim();execFileSync('icacls.exe',[p.core,'/inheritance:r','/grant:r',`*${sid}:(OI)(CI)F`],{stdio:'pipe'});}
|
|
104
113
|
const auth=path.join(p.core,'auth.json');rejectLinks(auth);let backup=null;if(fs.existsSync(auth)){backup=auth+'.backup-'+crypto.randomUUID();fs.copyFileSync(auth,backup,fs.constants.COPYFILE_EXCL);fs.chmodSync(backup,0o600);}const tmp=auth+'.new-'+crypto.randomUUID();exclusive(tmp,{auth_mode:'apikey',OPENAI_API_KEY:key});fs.renameSync(tmp,auth);
|
|
105
114
|
const models=path.join(p.core,'models.json');if(!fs.existsSync(models))fs.copyFileSync(path.join(packageRoot,'defaults','models.json'),models,fs.constants.COPYFILE_EXCL);
|
|
106
115
|
const config=path.join(p.core,'config.toml');if(!fs.existsSync(config))fs.writeFileSync(config,`model = "deepseek-v4.1-flash"\nmodel_provider = "dashscope_deepseek"\nforced_login_method = "api"\ncli_auth_credentials_store = "file"\nmodel_catalog_json = ${JSON.stringify(models.replaceAll('\\','/'))}\nmodel_reasoning_effort = "high"\nweb_search = "disabled"\n[model_providers.dashscope_deepseek]\nname = "DeepSeek-DashScope"\nbase_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"\nwire_api = "responses"\nrequires_openai_auth = true\n`,{flag:'wx',mode:0o600});return {ok:true,configured:true,backup,configPreserved:true};});}
|
|
107
|
-
export async function install({version,adopt=false,archive,manifest,expectedCurrent}={}) {
|
|
108
|
-
|
|
116
|
+
export async function install({version,adopt=false,archive,manifest,expectedCurrent,p=paths(),runtimeInstaller=ensureRuntime,identify=appIdentity}={}) {
|
|
117
|
+
if(!['darwin-arm64','win32-x64'].includes(p.platform))fail('UNSUPPORTED_PLATFORM',p.platform);
|
|
109
118
|
progress('准备安装:检查版本、平台与独立目录');
|
|
110
119
|
return locked(p,async()=>{const m=manifest||await manifestFor(version||pkg.version);const a=validateManifest(m,p.platform);
|
|
120
|
+
const activeRuntimeFile=path.join(p.root,'runtimes','active.json'),previousRuntime=fs.existsSync(activeRuntimeFile)?read(activeRuntimeFile):null;
|
|
111
121
|
if(expectedCurrent!==undefined&&state(p)?.version!==expectedCurrent)fail('INSTALLATION_CHANGED','Installation changed after the update was prepared; check again');
|
|
112
|
-
|
|
122
|
+
const prior=state(p),same=sameDesktop(prior,m,p.platform)||(!prior?.desktop&&prior?.version===m.version);
|
|
123
|
+
if(same&&!archive){
|
|
124
|
+
if(!fs.existsSync(p.target)||identify(p.target)!=='local.codex-own')fail('UNMANAGED_TARGET','Installed component identity is missing');
|
|
125
|
+
const tools=m.runtimes?await runtimeInstaller(m.runtimes,{p,report:progress}):null;
|
|
126
|
+
if(m.binding){recordBinding(p,m);if(!prior.desktop||bindingHash(prior.releaseManifest)!==bindingHash(m))writeState(p,{...prior,desktop:{version:m.version,identity:a.sha256},releaseManifest:m});}
|
|
127
|
+
return {ok:true,unchanged:true,version:m.version,managerVersion:m.binding?.managerVersion||pkg.version,runtimeVersion:m.runtimes?.version||null,runtimeChanged:!!tools&&!tools.unchanged,transfer:{downloadBytes:0,downloadBlocks:0},desktopActivated:false};
|
|
128
|
+
}
|
|
113
129
|
if(running(p).length)fail('APP_RUNNING','Quit codex-own before install/update');
|
|
114
130
|
const stage=fs.mkdtempSync(path.join(p.manager,'stage-'));let f=archive,transfer;
|
|
115
131
|
if(f){if(fs.statSync(f).size!==a.size||await digestFile(f)!==a.sha256)fail('CHECKSUM_MISMATCH','Local archive checksum mismatch');}
|
|
@@ -123,7 +139,7 @@ export async function install({version,adopt=false,archive,manifest,expectedCurr
|
|
|
123
139
|
if(process.platform==='darwin')execFileSync('codesign',['--verify','--deep','--strict',source],{stdio:'pipe'});
|
|
124
140
|
if(m.runtimes)await ensureRuntime(m.runtimes,{p,report:progress});
|
|
125
141
|
progress('正在安装到独立目录');
|
|
126
|
-
const result=switchApp({p,source,version:m.version,adopt});
|
|
142
|
+
const result=switchApp({p,source,version:m.version,manifest:m,runtimeSelection:previousRuntime,adopt});if(m.binding)recordBinding(p,m);
|
|
127
143
|
if(process.platform==='darwin')execFileSync('/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister',['-f',p.target],{stdio:'pipe'});
|
|
128
144
|
else shortcut(p);
|
|
129
145
|
fs.rmSync(stage,{recursive:true,force:true});progress('安装完成');return {...result,downloadSource:archive?'local-archive':'registry.npmjs.org',...(transfer?{transfer}:{})};
|
|
@@ -139,5 +155,8 @@ export async function rollback({restoreData=false}={}){const p=paths();return lo
|
|
|
139
155
|
const backup=path.join(p.manager,'backups','rollback-'+crypto.randomUUID());snapshot(p,backup);
|
|
140
156
|
const preserved=path.join(path.dirname(p.target),'.codex-own-before-rollback-'+crypto.randomUUID());fs.renameSync(p.target,preserved);fs.renameSync(prev.app,p.target);
|
|
141
157
|
for(const n of ['core','desktop']){const snap=path.join(prev.data,n);if(fs.existsSync(p[n]))fs.renameSync(p[n],path.join(p.root,n+'.before-rollback-'+crypto.randomUUID()));if(fs.existsSync(snap))fs.cpSync(snap,p[n],{recursive:true,force:false,errorOnExist:true});}
|
|
158
|
+
if(prev.runtimeSelection)restoreRuntimeSelection(prev.runtimeSelection,{p});
|
|
159
|
+
if(prev.state?.releaseManifest){const m=prev.state.releaseManifest;if(!prev.runtimeSelection&&m.runtimes)await ensureRuntime(m.runtimes,{p,report:progress});if(m.binding)recordBinding(p,m);}
|
|
160
|
+
else{const f=path.join(p.manager,'binding.json');if(fs.existsSync(f))fs.renameSync(f,f+'.before-legacy-rollback-'+crypto.randomUUID());}
|
|
142
161
|
writeState(p,{...(prev.state||{schema:1,identity:'local.codex-own',version:'adopted-legacy',target:p.target}),previous:null});return {ok:true,preserved,dataBackup:backup};});}
|
|
143
162
|
export function doctor(){const p=paths(),s=state(p);let tar=true;try{execFileSync(tarExecutable(),['--version'],{stdio:'pipe'});}catch{tar=false;}const pending=fs.existsSync(p.manager)?fs.readdirSync(p.manager).filter(n=>/^transaction-.*\.json$/.test(n)&&!n.endsWith('.complete.json')&&!fs.existsSync(path.join(p.manager,n.replace('.json','.complete.json')))):[];return {ok:tar&&!pending.length,managerVersion:pkg.version,platform:p.platform,supported:['darwin-arm64','win32-x64'].includes(p.platform),installed:s?.version||null,paths:p,tar,pendingTransactions:pending,lock:fs.existsSync(path.join(p.manager,'operation.lock')),configured:fs.existsSync(path.join(p.core,'auth.json')),windowsGuiTested:false};}
|
package/lib/tool-runtime.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import {execFileSync,spawnSync} from 'node:child_process';
|
|
5
5
|
import {createRequire} from 'node:module';
|
|
6
6
|
import {assembleNpmAsset,validateParts} from './npm-assets.mjs';
|
|
7
|
-
import {paths,locked,rejectLinks,exclusive,unpack,fail,tarExecutable,digestFile} from './manager.mjs';
|
|
7
|
+
import {paths,state,locked,rejectLinks,exclusive,unpack,fail,tarExecutable,digestFile} from './manager.mjs';
|
|
8
8
|
const require=createRequire(import.meta.url);
|
|
9
9
|
export const toolEnv=require('./tool-env.cjs');
|
|
10
10
|
export function validateRuntime(m,platform){
|
|
@@ -29,8 +29,16 @@ export function runtimeDoctor(base=toolEnv.runtimeRoot(),{version}={}){
|
|
|
29
29
|
export async function ensureRuntime(m,{p=paths(),assemble=assembleNpmAsset,report=()=>{},localAssets,signal}={}){
|
|
30
30
|
const assets=validateRuntime(m,p.platform),base=path.join(p.root,'runtimes');rejectLinks(base);fs.mkdirSync(base,{recursive:true,mode:0o700});
|
|
31
31
|
const previous=toolEnv.active(base),target=path.join(base,'versions',m.version);rejectLinks(target);
|
|
32
|
-
if(previous?.version===m.version){const d=runtimeDoctor(base);if(!d.ok)fail(d.code,d.message||'Invalid private runtime');return {...d,unchanged:true};}
|
|
33
|
-
if(fs.existsSync(target))
|
|
32
|
+
if(previous?.version===m.version){const receipt=path.join(target,'codex-own-runtime.json');const stored=fs.existsSync(receipt)?JSON.parse(fs.readFileSync(receipt)):null;if(!stored||stored.version!==m.version||stored.platform!==p.platform||['node','python'].some(n=>stored.assets?.[n]!==assets[n].sha256))fail('RUNTIME_VERSION_EXISTS','Immutable active runtime differs from requested binding');const d=runtimeDoctor(base);if(!d.ok)fail(d.code,d.message||'Invalid private runtime');return {...d,unchanged:true};}
|
|
33
|
+
if(fs.existsSync(target)){
|
|
34
|
+
const receipt=path.join(target,'codex-own-runtime.json');
|
|
35
|
+
if(!fs.existsSync(receipt))fail('RUNTIME_VERSION_EXISTS','Existing runtime has no verified receipt');
|
|
36
|
+
const old=JSON.parse(fs.readFileSync(receipt));
|
|
37
|
+
if(old.version!==m.version||old.platform!==p.platform||['node','python'].some(n=>old.assets?.[n]!==assets[n].sha256))fail('RUNTIME_VERSION_EXISTS','Immutable runtime identity differs from binding');
|
|
38
|
+
const d=runtimeDoctor(base,{version:m.version});if(!d.ok)fail(d.code,d.message);
|
|
39
|
+
writeActive(base,{schema:1,version:m.version,platform:p.platform,previous:previous?.version||null,installedAt:new Date().toISOString()});
|
|
40
|
+
return {...d,reactivated:true};
|
|
41
|
+
}
|
|
34
42
|
fs.mkdirSync(path.dirname(target),{recursive:true,mode:0o700});
|
|
35
43
|
const stage=fs.mkdtempSync(path.join(base,'stage-')),candidate=path.join(stage,'candidate');fs.mkdirSync(candidate);let created=false,committed=false;
|
|
36
44
|
try{
|
|
@@ -67,13 +75,18 @@ export async function ensureRuntime(m,{p=paths(),assemble=assembleNpmAsset,repor
|
|
|
67
75
|
return runtimeDoctor(base);
|
|
68
76
|
}finally{for(const f of [stage,...(created&&!committed?[target]:[])])try{fs.rmSync(f,{recursive:true,force:true,maxRetries:10,retryDelay:200});}catch{report(`临时目录被系统占用,已保留:${f}`);}}
|
|
69
77
|
}
|
|
70
|
-
export async function installRuntime(m,options={}){const p=options.p||paths();return locked(p,()=>ensureRuntime(m,{...options,p}));}
|
|
78
|
+
export async function installRuntime(m,options={}){const p=options.p||paths();return locked(p,async()=>{const result=await ensureRuntime(m,{...options,p});if(options.selection){const base=path.join(p.root,'runtimes'),active=JSON.parse(fs.readFileSync(path.join(base,'active.json')));writeActive(base,{...active,selection:options.selection,selectedForDesktop:state(p)?.version||null});}return result;});}
|
|
79
|
+
export function restoreRuntimeSelection(selected,{p=paths()}={}){
|
|
80
|
+
if(!/^node[0-9.]+-python[0-9.]+-r\d+$/.test(selected?.version||''))fail('INVALID_RUNTIME_RECEIPT','Invalid previous tool selection');
|
|
81
|
+
const base=path.join(p.root,'runtimes'),f=path.join(base,'versions',selected.version,'codex-own-runtime.json');rejectLinks(f);const old=JSON.parse(fs.readFileSync(f));if(old.version!==selected.version||old.platform!==p.platform)fail('INVALID_RUNTIME_RECEIPT','Previous tool platform differs');
|
|
82
|
+
const result=runtimeDoctor(base,{version:selected.version});if(!result.ok)fail('RUNTIME_INVALID','Previous tools failed verification');writeActive(base,{schema:1,version:selected.version,platform:p.platform,previous:toolEnv.active(base)?.version===selected.version?(selected.previous||null):(toolEnv.active(base)?.version||null),...(['user-update','user-rollback'].includes(selected.selection)?{selection:selected.selection,selectedForDesktop:selected.selectedForDesktop}:{}),installedAt:new Date().toISOString()});return result;
|
|
83
|
+
}
|
|
71
84
|
export async function rollbackRuntime({p=paths()}={}){return locked(p,async()=>{
|
|
72
85
|
const base=path.join(p.root,'runtimes'),r=toolEnv.active(base);if(!r?.previous)fail('NO_RUNTIME_ROLLBACK','No previous runtime');
|
|
73
86
|
const f=path.join(base,'versions',r.previous,'codex-own-runtime.json');rejectLinks(f);const old=JSON.parse(fs.readFileSync(f));
|
|
74
87
|
if(old.version!==r.previous||old.platform!==p.platform)fail('INVALID_RUNTIME_RECEIPT','Previous runtime does not match');
|
|
75
88
|
const checked=runtimeDoctor(base,{version:r.previous});if(!checked.ok)fail('RUNTIME_INVALID','Previous runtime verification failed; active tools preserved');
|
|
76
|
-
writeActive(base,{schema:1,version:r.previous,platform:p.platform,previous:r.version,installedAt:new Date().toISOString()});return runtimeDoctor(base);
|
|
89
|
+
writeActive(base,{schema:1,version:r.previous,platform:p.platform,previous:r.version,selection:'user-rollback',selectedForDesktop:state(p)?.version||null,installedAt:new Date().toISOString()});return runtimeDoctor(base);
|
|
77
90
|
});}
|
|
78
91
|
export function runRuntime(args){if(!args.length)fail('COMMAND_REQUIRED','runtime run -- <node|npm|npx|python|python3|pip> [args]');const base=toolEnv.runtimeRoot(),r=toolEnv.active(base);if(!r)fail('RUNTIME_NOT_INSTALLED','Install codex-own runtime first');
|
|
79
92
|
const [name,...rest]=args;const command=name==='node'?{file:r.node,args:rest}:['npm','npx'].includes(name)?{file:r.node,args:[name==='npm'?r.npm:path.join(path.dirname(r.npm),'npx-cli.js'),...rest]}:['python','python3','pip'].includes(name)?{file:r.python,args:[...(name==='pip'?['-m','pip']:[]),...rest]}:null;
|