@petercjl/codex-own 0.6.4 → 0.6.5

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/lib/manager.mjs CHANGED
@@ -11,6 +11,9 @@ import {ensureRuntime,validateRuntime,restoreRuntimeSelection} from './tool-runt
11
11
  import {validateBinding,packageManifest,sameDesktop,identity as bindingHash} from './binding.mjs';
12
12
  import {deepseekConfiguration} from './model-configuration.mjs';
13
13
  import windowsAgentShell from './windows-agent-shell.cjs';
14
+ import {extractChecked} from './extraction.mjs';
15
+ import {copyNewDirectory,removeTemporary,removeNewTreeSync} from './file-operations.mjs';
16
+ import {lockStatus,temporaryStages} from './maintenance.mjs';
14
17
 
15
18
  export const packageRoot = path.resolve(import.meta.dirname, '..');
16
19
  export const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'),'utf8'));
@@ -52,14 +55,7 @@ export function tarExecutable(platform=process.platform,env=process.env,exists=f
52
55
  return executable;
53
56
  }
54
57
  export function unpack(archive,dest) {
55
- // Older Windows tar builds convert absolute argv paths through the ANSI code
56
- // page. CreateProcessW's cwd supports Unicode; sibling relative args avoid it.
57
- const cwd=path.dirname(path.resolve(archive)),source=path.basename(archive),target=path.relative(cwd,path.resolve(dest));
58
- const names=execFileSync(tarExecutable(),['-tf',source],{cwd,encoding:'utf8',maxBuffer:30*1024*1024}).split('\n').filter(Boolean);checkEntries(names);
59
- fs.mkdirSync(dest);execFileSync(tarExecutable(),['-xf',source,'-C',target],{cwd,stdio:'pipe'});
60
- // Bundled framework links may be relative but must stay within the extracted tree.
61
- function walk(p){for(const x of fs.readdirSync(p,{withFileTypes:true})){const f=path.join(p,x.name);if(x.isSymbolicLink()){const resolved=path.resolve(path.dirname(f),fs.readlinkSync(f));if(!resolved.startsWith(dest+path.sep))fail('UNSAFE_ARCHIVE','Escaping symlink');}else if(x.isDirectory())walk(f);}}
62
- walk(dest);
58
+ return extractChecked(archive,dest,{tar:tarExecutable(),report:progress});
63
59
  }
64
60
  export function macOwnProcessIds(lines) {
65
61
  return lines.filter(l=>l.includes('codex-own.app/Contents/')&&!l.includes(' ps -')&&!l.includes('/Contents/Resources/cua_node/')&&!/\/Contents\/MacOS\/WinAppHelper(?:\s|$)/.test(l)).map(l=>l.trim().split(/\s+/)[0]);
@@ -91,17 +87,22 @@ export function switchApp({p,source,version,manifest,runtimeSelection,adopt=fals
91
87
  const id=crypto.randomUUID(),backup=path.join(p.manager,'backups',id);fs.mkdirSync(backup,{recursive:true,mode:0o700});
92
88
  if(exists)backupData(p,path.join(backup,'data'));
93
89
  const old=path.join(path.dirname(p.target),'.codex-own-backup-'+id), next=path.join(path.dirname(p.target),'.codex-own-next-'+id);
94
- fs.mkdirSync(path.dirname(p.target),{recursive:true});fs.cpSync(source,next,{recursive:true,force:false,errorOnExist:true,dereference:false,verbatimSymlinks:true});
95
- const journal=path.join(p.manager,'transaction-'+id+'.json');exclusive(journal,{target:p.target,old,next,version,previous,backup});
90
+ const ownership=path.join(p.manager,'temporary-'+id+'.json');
91
+ const journal=path.join(p.manager,'transaction-'+id+'.json');let preparedNext=false;
92
+ try{
93
+ fs.mkdirSync(path.dirname(p.target),{recursive:true});copyNewDirectory(source,next,{onCreate:()=>{preparedNext=true;exclusive(ownership,{package:pkg.name,kind:'next',path:next,target:p.target});}});
94
+ exclusive(journal,{target:p.target,old,next,version,previous,backup});
95
+ }catch(e){if(preparedNext&&fs.existsSync(next))try{removeNewTreeSync(next);}catch{e.partialTarget=next;}throw e;}
96
96
  let moved=false,activated=false;
97
97
  try {if(exists){fs.renameSync(p.target,old);moved=true;}fs.renameSync(next,p.target);activated=true;
98
98
  const active=path.join(p.root,'runtimes','active.json');
99
99
  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});
100
- }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;}
100
+ }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);if(fs.existsSync(next))try{removeNewTreeSync(next);}catch{e.partialTarget=next;}throw e;}
101
101
  exclusive(path.join(p.manager,'transaction-'+id+'.complete.json'),{ok:true});
102
+ if(fs.existsSync(ownership))try{fs.unlinkSync(ownership);}catch{}
102
103
  return {ok:true,version,target:p.target,backup:exists?old:null,dataBackup:exists?path.join(backup,'data'):null};
103
104
  }
104
- 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);}}
105
+ export async function locked(p,fn,{recovery=false}={}){rejectLinks(p.root);fs.mkdirSync(p.manager,{recursive:true,mode:0o700});const lock=path.join(p.manager,'operation.lock'),guard=path.join(p.manager,'recovery.lock');if(!recovery&&fs.existsSync(guard))fail('BUSY','Recovery is active');let fd;try{fd=fs.openSync(lock,'wx',0o600);}catch{fail('BUSY','Another operation or interrupted lock exists; inspect doctor and repair plan');}try{if(!recovery&&fs.existsSync(guard))fail('BUSY','Recovery is active');fs.writeSync(fd,JSON.stringify({pid:process.pid,time:new Date().toISOString()}));return await fn();}finally{fs.closeSync(fd);fs.unlinkSync(lock);}}
105
106
  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;}
106
107
  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);}
107
108
  export function installedBinding(p=paths()){const f=path.join(p.manager,'binding.json');return fs.existsSync(f)?read(f).manifest:null;}
@@ -119,7 +120,7 @@ export async function configure(keyFile,{p=paths(),isRunning=running}={}){if(!ke
119
120
  if(!fs.existsSync(models))fs.copyFileSync(path.join(packageRoot,'defaults','models.json'),models,fs.constants.COPYFILE_EXCL);
120
121
  if(previous!==updated){const next=config+'.new-'+crypto.randomUUID();fs.writeFileSync(next,updated,{flag:'wx',mode:0o600});fs.renameSync(next,config);if(fs.readFileSync(config,'utf8')!==updated)fail('VERIFY_FAILED','Model configuration did not match the requested provider');}
121
122
  return {ok:true,configured:true,backup,configBackup,configPreserved:previous===updated,settingsPreserved:true,modelProvider:'dashscope_deepseek'};});}
122
- export async function install({version,adopt=false,archive,manifest,expectedCurrent,p=paths(),runtimeInstaller=ensureRuntime,identify=appIdentity}={}) {
123
+ export async function install({version,adopt=false,archive,manifest,expectedCurrent,p=paths(),runtimeInstaller=ensureRuntime,identify=appIdentity,isRunning=running}={}) {
123
124
  if(!['darwin-arm64','win32-x64'].includes(p.platform))fail('UNSUPPORTED_PLATFORM',p.platform);
124
125
  progress('准备安装:检查版本、平台与独立目录');
125
126
  return locked(p,async()=>{const m=manifest||await manifestFor(version||pkg.version);const a=validateManifest(m,p.platform);
@@ -132,8 +133,9 @@ export async function install({version,adopt=false,archive,manifest,expectedCurr
132
133
  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});}
133
134
  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};
134
135
  }
135
- if(running(p).length)fail('APP_RUNNING','Quit codex-own before install/update');
136
+ if(isRunning(p).length)fail('APP_RUNNING','Quit codex-own before install/update');
136
137
  const stage=fs.mkdtempSync(path.join(p.manager,'stage-'));let f=archive,transfer;
138
+ try{
137
139
  if(f){if(fs.statSync(f).size!==a.size||await digestFile(f)!==a.sha256)fail('CHECKSUM_MISMATCH','Local archive checksum mismatch');}
138
140
  else{if(m.schema!==2)fail('NPM_ASSETS_REQUIRED','Update @petercjl/codex-own to the latest npm-only release.');f=path.join(stage,'app.tar');const cache=path.join(p.manager,'npm-assets');rejectLinks(cache);
139
141
  if(state(p)?.identity==='local.codex-own'&&fs.existsSync(p.target)&&appIdentity(p.target)==='local.codex-own'){
@@ -143,12 +145,13 @@ export async function install({version,adopt=false,archive,manifest,expectedCurr
143
145
  progress('校验通过,正在解压桌面程序');
144
146
  const content=path.join(stage,'unpacked');unpack(f,content);const source=path.join(content,a.entry);
145
147
  if(process.platform==='darwin')execFileSync('codesign',['--verify','--deep','--strict',source],{stdio:'pipe'});
146
- if(m.runtimes)await ensureRuntime(m.runtimes,{p,report:progress});
148
+ if(m.runtimes)await runtimeInstaller(m.runtimes,{p,report:progress});
147
149
  progress('正在安装到独立目录');
148
- const result=switchApp({p,source,version:m.version,manifest:m,runtimeSelection:previousRuntime,adopt});if(m.binding)recordBinding(p,m);
150
+ const result=switchApp({p,source,version:m.version,manifest:m,runtimeSelection:previousRuntime,adopt,isRunning});if(m.binding)recordBinding(p,m);
149
151
  if(process.platform==='darwin')execFileSync('/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister',['-f',p.target],{stdio:'pipe'});
150
152
  else shortcut(p);
151
- fs.rmSync(stage,{recursive:true,force:true});progress('安装完成');return {...result,downloadSource:archive?'local-archive':'registry.npmjs.org',...(transfer?{transfer}:{})};
153
+ progress('安装完成');return {...result,downloadSource:archive?'local-archive':'registry.npmjs.org',...(transfer?{transfer}:{})};
154
+ }finally{await removeTemporary(stage,{report:progress});}
152
155
  });
153
156
  }
154
157
  export function shortcut(p) {
@@ -165,4 +168,4 @@ export async function rollback({restoreData=false}={}){const p=paths();return lo
165
168
  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);}
166
169
  else{const f=path.join(p.manager,'binding.json');if(fs.existsSync(f))fs.renameSync(f,f+'.before-legacy-rollback-'+crypto.randomUUID());}
167
170
  writeState(p,{...(prev.state||{schema:1,identity:'local.codex-own',version:'adopted-legacy',target:p.target}),previous:null});return {ok:true,preserved,dataBackup:backup};});}
168
- 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')),agentShell:{...windowsAgentShell.discover(),desktopAdapterBundled:process.platform==='win32'&&fs.existsSync(path.join(p.target,'resources','own-updater','lib','windows-agent-shell.cjs')),policyContract:1},windowsGuiTested:false};}
171
+ 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&&!fs.existsSync(path.join(p.manager,'operation.lock'))&&!fs.existsSync(path.join(p.manager,'recovery.lock')),bootstrap:{nodeVersion:process.version,executable:process.execPath,architecture:process.arch,supported:Number(process.versions.node.split('.')[0])>=22},lockDetails:lockStatus(p),orphanStages:temporaryStages(p),recoveryLock:fs.existsSync(path.join(p.manager,'recovery.lock')),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')),agentShell:{...windowsAgentShell.discover(),desktopAdapterBundled:process.platform==='win32'&&fs.existsSync(path.join(p.target,'resources','own-updater','lib','windows-agent-shell.cjs')),policyContract:1},windowsGuiTested:false};}
package/lib/network.mjs CHANGED
@@ -3,7 +3,9 @@ import crypto from 'node:crypto';
3
3
  import {EnvHttpProxyAgent, fetch} from 'undici';
4
4
 
5
5
  const dispatcher = new EnvHttpProxyAgent();
6
- export const progress = message => process.stderr.write(`[codex-own] ${message}\n`);
6
+ let progressMode='stderr';
7
+ export function setProgressMode(mode){if(!['stderr','stdout','quiet'].includes(mode))throw Object.assign(new Error('Progress must be stderr, stdout or quiet'),{code:'INVALID_ARGUMENT'});progressMode=mode;}
8
+ export const progress = message => {if(progressMode!=='quiet')(progressMode==='stdout'?process.stdout:process.stderr).write(`[codex-own] ${message}\n`);};
7
9
  export function networkFetch(url, options = {}) {
8
10
  return fetch(url, {dispatcher, redirect: 'error', ...options});
9
11
  }
@@ -5,6 +5,7 @@ import {execFileSync,spawnSync} from 'node:child_process';
5
5
  import {createRequire} from 'node:module';
6
6
  import {assembleNpmAsset,validateParts} from './npm-assets.mjs';
7
7
  import {paths,state,locked,rejectLinks,exclusive,unpack,fail,tarExecutable,digestFile} from './manager.mjs';
8
+ import {copyNewDirectory,removeTemporary} from './file-operations.mjs';
8
9
  const require=createRequire(import.meta.url);
9
10
  export const toolEnv=require('./tool-env.cjs');
10
11
  export function validateRuntime(m,platform){
@@ -17,6 +18,8 @@ function writeActive(base,r){
17
18
  const f=path.join(base,'active.json');rejectLinks(f);
18
19
  if(fs.existsSync(f))fs.copyFileSync(f,f+'.backup-'+crypto.randomUUID(),fs.constants.COPYFILE_EXCL);
19
20
  const tmp=f+'.new-'+crypto.randomUUID();exclusive(tmp,r);fs.renameSync(tmp,f);
21
+ // Bound verified selection receipts, preserving every immutable tool version.
22
+ try{const history=fs.readdirSync(base).filter(n=>/^active\.json\.backup-[a-f0-9-]{36}$/.test(n)).map(n=>path.join(base,n)).filter(p=>{const s=fs.lstatSync(p);if(!s.isFile()||s.isSymbolicLink())return false;try{const v=JSON.parse(fs.readFileSync(p));return v.schema===1&&/^node[0-9.]+-python[0-9.]+-r\d+$/.test(v.version);}catch{return false;}}).sort((a,b)=>fs.statSync(b).mtimeMs-fs.statSync(a).mtimeMs);for(const p of history.slice(20))fs.unlinkSync(p);}catch{}
20
23
  }
21
24
  export function runtimeDoctor(base=toolEnv.runtimeRoot(),{version}={}){
22
25
  try{const r=toolEnv.active(base,process.platform,version);if(!r)return {ok:false,code:'RUNTIME_NOT_INSTALLED',root:base};
@@ -63,7 +66,7 @@ export async function ensureRuntime(m,{p=paths(),assemble=assembleNpmAsset,repor
63
66
  // Windows scanners may hold executables after execution. Never rename an
64
67
  // executed tool directory: copy to a fresh immutable final path, probe there,
65
68
  // and only then atomically switch the small active receipt.
66
- fs.mkdirSync(target);created=true;fs.cpSync(candidate,target,{recursive:true,force:false,errorOnExist:true,verbatimSymlinks:true});
69
+ copyNewDirectory(candidate,target,{onCreate:()=>{created=true;}});
67
70
  if(process.platform==='darwin'){const shell=path.join(target,'shell');fs.mkdirSync(shell);for(const [name,body]of Object.entries(toolEnv.shellFiles(base,m.version)))fs.writeFileSync(path.join(shell,name),body,{flag:'wx',mode:0o600});}
68
71
  const tested=runtimeDoctor(base,{version:m.version});if(!tested.ok)fail('RUNTIME_PROBE_FAILED',tested.message||'Tool verification failed');
69
72
  const expected=m.version.match(/^node([0-9.]+)-python([0-9.]+)-/);
@@ -72,8 +75,8 @@ export async function ensureRuntime(m,{p=paths(),assemble=assembleNpmAsset,repor
72
75
  exclusive(path.join(target,'codex-own-runtime.json'),{schema:1,version:m.version,platform:p.platform,assets:{node:assets.node.sha256,python:assets.python.sha256},verified:tested});
73
76
  writeActive(base,{schema:1,version:m.version,platform:p.platform,previous:previous?.version||null,installedAt:new Date().toISOString()});
74
77
  committed=true;
75
- return runtimeDoctor(base);
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}`);}}
78
+ return tested;
79
+ }finally{for(const f of [stage,...(created&&!committed?[target]:[])])await removeTemporary(f,{report});}
77
80
  }
78
81
  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
82
  export function restoreRuntimeSelection(selected,{p=paths()}={}){