@petercjl/codex-own 0.6.3 → 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/README.md +83 -39
- package/bin/codex-own.mjs +4 -34
- package/capabilities.json +56 -1
- package/lib/archive-inventory.mjs +40 -0
- package/lib/cleanup.mjs +177 -0
- package/lib/cli.mjs +47 -0
- package/lib/extraction.mjs +42 -0
- package/lib/file-operations.mjs +22 -0
- package/lib/maintenance.mjs +54 -0
- package/lib/manager.mjs +21 -18
- package/lib/network.mjs +3 -1
- package/lib/npm-assets.mjs +1 -0
- package/lib/tool-runtime.mjs +6 -3
- package/package.json +449 -468
- package/release.json +446 -467
- package/skills/codex-own/SKILL.md +14 -0
- package/skills/codex-own/capabilities.json +2 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {execFileSync} from 'node:child_process';
|
|
4
|
+
import {archiveInventory,validateArchiveName,extractedHash} from './archive-inventory.mjs';
|
|
5
|
+
|
|
6
|
+
const error=(code,message,extra={})=>Object.assign(new Error(message),{code,...extra});
|
|
7
|
+
export function archiveNames(text){
|
|
8
|
+
const names=text.split('\n').map(n=>n.replace(/\r$/,'')).filter(Boolean);
|
|
9
|
+
if(!names.length||names.length>200000)throw error('UNSAFE_ARCHIVE','Empty or excessive archive inventory');
|
|
10
|
+
for(const n of names){const normalized=n.replaceAll('\\','/');if(/^[a-zA-Z]:|^\/|^\\|[\0\r\n]/.test(n)||normalized.split('/').includes('..'))throw error('UNSAFE_ARCHIVE','Unsafe archive entry');}
|
|
11
|
+
return [...new Set(names)];
|
|
12
|
+
}
|
|
13
|
+
function inspectTree(root){
|
|
14
|
+
function walk(dir){for(const x of fs.readdirSync(dir,{withFileTypes:true})){
|
|
15
|
+
const f=path.join(dir,x.name),s=fs.lstatSync(f);
|
|
16
|
+
if(s.isSymbolicLink()){const link=fs.readlinkSync(f),resolved=path.resolve(path.dirname(f),link);if(path.isAbsolute(link)||!resolved.startsWith(root+path.sep))throw error('UNSAFE_ARCHIVE','Escaping archive symlink');}
|
|
17
|
+
else if(s.isDirectory())walk(f);else if(!s.isFile())throw error('UNSAFE_ARCHIVE','Unsupported archive file type');
|
|
18
|
+
}}walk(root);
|
|
19
|
+
}
|
|
20
|
+
export function extractChecked(archive,dest,{tar,attempts=3,report=()=>{},inventory=archiveInventory,execute=execFileSync,delay=ms=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms)}={}){
|
|
21
|
+
if(!Number.isInteger(attempts)||attempts<1||attempts>3)throw error('INVALID_ARGUMENT','Extraction attempts must be 1..3');
|
|
22
|
+
const cwd=path.dirname(path.resolve(archive)),source=path.basename(archive),root=path.resolve(dest),target=path.relative(cwd,root);
|
|
23
|
+
const entries=inventory(archive);for(const e of entries)validateArchiveName(e.name);const names=entries.map(e=>e.name);
|
|
24
|
+
fs.mkdirSync(root);let missing=names,lastError;
|
|
25
|
+
const absent=()=>entries.filter(e=>{try{const file=path.join(root,e.name),s=fs.lstatSync(file);if(e.type==='Directory')return !s.isDirectory();if(e.type==='SymbolicLink')return !s.isSymbolicLink()||e.link!==undefined&&fs.readlinkSync(file)!==e.link;if(!s.isFile())return true;return e.size!==undefined&&e.type!=='Link'&&s.size!==e.size||!!e.sha256&&extractedHash(file)!==e.sha256;}catch(e){if(['ENOENT','ENOTDIR','EPERM','EBUSY','EACCES'].includes(e.code))return true;throw e;}}).map(e=>e.name);
|
|
26
|
+
for(let attempt=1;attempt<=attempts;attempt++){
|
|
27
|
+
// A previous partial extraction must not provide an escaping link for retry.
|
|
28
|
+
inspectTree(root);
|
|
29
|
+
try{
|
|
30
|
+
if(attempt===1||!missing.length||missing.length>32||missing.some(n=>/[^\x20-\x7e]/.test(n)))execute(tar,['-xf',source,'-C',target],{cwd,stdio:'pipe'});
|
|
31
|
+
else{
|
|
32
|
+
// One entry at a time also keeps Windows argv bounded for long inventories.
|
|
33
|
+
for(const n of missing)execute(tar,['-xf',source,'-C',target,'--',n],{cwd,stdio:'pipe'});
|
|
34
|
+
}
|
|
35
|
+
lastError=null;
|
|
36
|
+
}catch(e){lastError=e;}
|
|
37
|
+
inspectTree(root);report('正在核对解压文件大小与内容校验和');missing=absent();
|
|
38
|
+
if(!missing.length&&!lastError)return {entries:names.length,attempts:attempt};
|
|
39
|
+
if(attempt<attempts){report(`解压验证未完成:${missing.length} 项缺失或内容不符,重试 ${attempt}/${attempts-1}`);delay(attempt*500);}
|
|
40
|
+
}
|
|
41
|
+
throw error(missing.length?'UNPACK_INCOMPLETE':'UNPACK_FAILED',`解压验证失败,${missing.length} 项缺失或内容不符;可能被占用或被安全软件拦截。示例:${missing.slice(0,5).join(', ')||'tar 未正常退出'}`,{missing:missing.slice(0,30),missingCount:missing.length,stage:root,tarExit:lastError?.status??null});
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Reserve a new directory atomically, then copy its children. This avoids the
|
|
5
|
+
// Windows Node 24.22 strict cp(existing empty directory) incompatibility while
|
|
6
|
+
// still refusing an existing destination and every existing child.
|
|
7
|
+
export function copyNewDirectory(source,target,{onCreate=()=>{},copy=fs.cpSync}={}){
|
|
8
|
+
fs.mkdirSync(target);
|
|
9
|
+
try{onCreate();for(const name of fs.readdirSync(source))copy(path.join(source,name),path.join(target,name),{recursive:true,force:false,errorOnExist:true,dereference:false,verbatimSymlinks:true});}
|
|
10
|
+
catch(e){try{removeNewTreeSync(target);}catch{e.partialTarget=target;}throw e;}
|
|
11
|
+
}
|
|
12
|
+
// Do not invoke native recursive rmSync: older Windows runtimes can terminate
|
|
13
|
+
// the process on sharing violations instead of returning a JavaScript error.
|
|
14
|
+
export function removeNewTreeSync(file){
|
|
15
|
+
const st=fs.lstatSync(file);
|
|
16
|
+
if(st.isDirectory()&&!st.isSymbolicLink()){for(const n of fs.readdirSync(file))removeNewTreeSync(path.join(file,n));fs.rmdirSync(file);}
|
|
17
|
+
else fs.unlinkSync(file);
|
|
18
|
+
}
|
|
19
|
+
export async function removeTemporary(file,{report=()=>{}}={}){
|
|
20
|
+
try{await fs.promises.rm(file,{recursive:true,force:true,maxRetries:3,retryDelay:200});return true;}
|
|
21
|
+
catch(e){report(`临时目录被占用,保留供 repair 检查:${file}`);return false;}
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import {paths,locked,rejectLinks,running,fail} from './manager.mjs';
|
|
5
|
+
|
|
6
|
+
const uuid='[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
|
|
7
|
+
function readJSON(f){try{return JSON.parse(fs.readFileSync(f,'utf8'));}catch{return null;}}
|
|
8
|
+
export function lockStatus(p,{alive=pid=>{try{process.kill(pid,0);return true;}catch(e){return e.code==='ESRCH'?false:null;}}}={}){
|
|
9
|
+
const file=path.join(p.manager,'operation.lock');
|
|
10
|
+
if(!fs.existsSync(file))return {exists:false};
|
|
11
|
+
rejectLinks(file);const s=fs.statSync(file),v=readJSON(file),valid=Number.isSafeInteger(v?.pid)&&v.pid>0;
|
|
12
|
+
return {exists:true,pid:valid?v.pid:null,time:typeof v?.time==='string'?v.time:null,alive:valid?alive(v.pid):null,fingerprint:{ino:s.ino,size:s.size,mtimeMs:s.mtimeMs}};
|
|
13
|
+
}
|
|
14
|
+
export function incompleteTransactions(p){return 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')))):[];}
|
|
15
|
+
function size(file){const s=fs.lstatSync(file);if(s.isSymbolicLink())return s.size;if(s.isFile())return s.size;if(!s.isDirectory())return 0;return fs.readdirSync(file).reduce((n,x)=>n+size(path.join(file,x)),0);}
|
|
16
|
+
export function temporaryStages(p){
|
|
17
|
+
const items=[];
|
|
18
|
+
for(const dir of [p.manager,path.join(p.root,'runtimes')]){
|
|
19
|
+
rejectLinks(dir);if(!fs.existsSync(dir))continue;
|
|
20
|
+
for(const n of fs.readdirSync(dir).filter(n=>/^stage-[A-Za-z0-9_-]+$/.test(n))){const f=path.join(dir,n);const st=fs.lstatSync(f);if(st.isDirectory()&&!st.isSymbolicLink())items.push({path:f,bytes:size(f),kind:'stage'});}
|
|
21
|
+
}
|
|
22
|
+
if(fs.existsSync(p.manager))for(const n of fs.readdirSync(p.manager).filter(n=>new RegExp('^temporary-'+uuid+'\\.json$').test(n))){
|
|
23
|
+
const receipt=path.join(p.manager,n);rejectLinks(receipt);const r=readJSON(receipt),id=n.slice(10,-5),expected=path.join(path.dirname(p.target),'.codex-own-next-'+id);
|
|
24
|
+
if(r?.package!=='@petercjl/codex-own'||r.kind!=='next'||r.path!==expected||r.target!==p.target||!fs.existsSync(expected))continue;
|
|
25
|
+
rejectLinks(expected);items.push({path:expected,bytes:size(expected),kind:'next',receipt});
|
|
26
|
+
}
|
|
27
|
+
return items;
|
|
28
|
+
}
|
|
29
|
+
export function repairPlan({p=paths(),isRunning=running}={}){
|
|
30
|
+
const lock=lockStatus(p),pending=incompleteTransactions(p),active=isRunning(p);
|
|
31
|
+
const reason=pending.length?'INCOMPLETE_TRANSACTION':active.length?'APP_RUNNING':lock.exists&&lock.alive!==false?'LOCK_OWNER_UNCERTAIN_OR_ALIVE':null;
|
|
32
|
+
return {ok:true,allowed:!reason,reason,lock,pendingTransactions:pending,orphanStages:temporaryStages(p),policy:'only inactive staging and receipted next copies; preserve applications, credentials, conversations, caches and rollback'};
|
|
33
|
+
}
|
|
34
|
+
export async function repairRun({p=paths(),isRunning=running}={}){
|
|
35
|
+
rejectLinks(p.root);fs.mkdirSync(p.manager,{recursive:true,mode:0o700});
|
|
36
|
+
const guard=path.join(p.manager,'recovery.lock');rejectLinks(guard);let fd;
|
|
37
|
+
try{fd=fs.openSync(guard,'wx',0o600);}catch{fail('BUSY','Another recovery is active; inspect recovery.lock before recovery');}
|
|
38
|
+
try{
|
|
39
|
+
fs.writeSync(fd,JSON.stringify({pid:process.pid,time:new Date().toISOString()}));const plan=repairPlan({p,isRunning});
|
|
40
|
+
if(!plan.allowed)fail(plan.reason,'Recovery is deferred; preserve the lock and transaction evidence');
|
|
41
|
+
let recoveredLock=null;
|
|
42
|
+
if(plan.lock.exists){const f=path.join(p.manager,'operation.lock'),now=lockStatus(p);if(JSON.stringify(now.fingerprint)!==JSON.stringify(plan.lock.fingerprint)||now.alive!==false)fail('BUSY','Lock changed during recovery');
|
|
43
|
+
recoveredLock=path.join(p.manager,'recovered-lock-'+crypto.randomUUID()+'.json');fs.renameSync(f,recoveredLock);
|
|
44
|
+
}
|
|
45
|
+
return await locked(p,async()=>{
|
|
46
|
+
const removed=[],warnings=[];let reclaimedBytes=0;
|
|
47
|
+
for(const item of temporaryStages(p)){
|
|
48
|
+
rejectLinks(item.path);try{await fs.promises.rm(item.path,{recursive:true,maxRetries:3,retryDelay:200});if(item.receipt)fs.unlinkSync(item.receipt);removed.push(item.path);reclaimedBytes+=item.bytes;}
|
|
49
|
+
catch(e){warnings.push({path:item.path,code:e.code||'CLEANUP_FAILED'});}
|
|
50
|
+
}
|
|
51
|
+
return {ok:true,recoveredLock,removed,reclaimedBytes,warnings};
|
|
52
|
+
},{recovery:true});
|
|
53
|
+
}finally{fs.closeSync(fd);fs.unlinkSync(guard);}
|
|
54
|
+
}
|
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
|
-
|
|
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
|
-
|
|
95
|
-
const journal=path.join(p.manager,'transaction-'+id+'.json');
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/npm-assets.mjs
CHANGED
|
@@ -50,6 +50,7 @@ export async function assembleNpmAsset(asset, dest, cache, tar, {download = veri
|
|
|
50
50
|
const file = path.join(cache, `${part.sha256}.bin`);
|
|
51
51
|
safeCache(file);
|
|
52
52
|
if (cached.has(part.sha256)) {
|
|
53
|
+
const used=new Date();fs.utimesSync(file,used,used);
|
|
53
54
|
report(`[${i+1}/${asset.parts.length}] 复用已校验分片`); return file;
|
|
54
55
|
}
|
|
55
56
|
if (fs.existsSync(file)) fs.unlinkSync(file);
|
package/lib/tool-runtime.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
76
|
-
}finally{for(const f of [stage,...(created&&!committed?[target]:[])])
|
|
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()}={}){
|