@petercjl/codex-own 0.5.0 → 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 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,updateAvailable:state()?.version!==v};if(sub==='install'){yes();return install({version: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
  }
@@ -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){const m=await manifest(available);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);}
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
- if(p.target){rejectLinks(p.target);await reuseInstalledBlocks(asset,p.target,cache,{report,signal});}
34
- const transfer=await assemble(asset,archive,cache,tarExecutable(),{report,signal});
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(r.schema!==1||r.platform!==p.platform||r.version!==r.manifest?.version||r.manifest?.schema!==2||!isNewer(r.version,r.current))fail('INVALID_MANIFEST','Invalid prepared update');
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
- return apply({version:r.version,manifest:r.manifest,archive:r.archive,expectedCurrent:r.current});
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;
@@ -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
- writeState(p,{schema:1,identity:'local.codex-own',version,target:p.target,installedAt:new Date().toISOString(),previous:exists?{app:old,data:path.join(backup,'data'),state:previous}:null});
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}`);const p=await r.json(),m=p.ownDesktopManifest;if(p.name!==pkg.name||p.version!==v||m?.version!==v||m?.schema!==2)fail('MANIFEST_UNAVAILABLE','This version has no npm desktop manifest; update the npm manager to latest.');return m;}
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
- const p=paths();if(!['darwin-arm64','win32-x64'].includes(p.platform))fail('UNSUPPORTED_PLATFORM',p.platform);
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
- if(state(p)?.version===m.version&&!archive){if(m.runtimes)await ensureRuntime(m.runtimes,{p,report:progress});return {ok:true,unchanged:true,version:m.version};}
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};}
@@ -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))fail('RUNTIME_VERSION_EXISTS','Existing runtime version preserved; inspect runtime doctor and receipt before recovery');
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;