afucontext 0.0.0-stage → 0.1.18

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.
Files changed (32) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +182 -2
  3. package/README.zh-CN.md +183 -0
  4. package/bin/afu.mjs +127 -0
  5. package/bin/cache-lease.mjs +16 -0
  6. package/bin/host-autoload.py +546 -0
  7. package/bin/install-guide.mjs +56 -0
  8. package/bin/install-process.mjs +19 -0
  9. package/bin/platform.mjs +74 -0
  10. package/bin/prebuilt.mjs +64 -0
  11. package/bin/runtime.mjs +115 -0
  12. package/package.json +29 -4
  13. package/platform-dependencies.json +15 -0
  14. package/platform-locks/dsh/package-lock.json +425 -0
  15. package/platform-locks/dsh/package.json +16 -0
  16. package/platform-locks/pi/package-lock.json +1789 -0
  17. package/platform-locks/pi/package.json +9 -0
  18. package/python/afu_context_local-0.1.18-py3-none-any.whl +0 -0
  19. package/python/constraints.txt +25 -0
  20. package/release.json +7 -0
  21. package/scripts/dsh-overlay.mjs +105 -0
  22. package/scripts/dsh-presets.mjs +49 -0
  23. package/scripts/dsh-web-probe.mjs +14 -0
  24. package/src/afu_host_integrations/dsh/dist/commands.js +28 -0
  25. package/src/afu_host_integrations/dsh/dist/index.js +548 -0
  26. package/src/afu_host_integrations/pi/dist/index.js +565 -0
  27. package/src/afu_host_integrations/shared/runtime-client/dist/commands.js +40 -0
  28. package/src/afu_host_integrations/shared/runtime-client/dist/host-worker.js +170 -0
  29. package/src/afu_host_integrations/shared/runtime-client/dist/index.js +243 -0
  30. package/src/afu_host_integrations/shared/runtime-client/dist/prefix.js +12 -0
  31. package/src/afu_host_integrations/shared/runtime-client/dist/protocol.js +79 -0
  32. package/src/afu_host_integrations/shared/runtime-client/dist/telemetry.js +78 -0
package/bin/afu.mjs ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ /** No postinstall side effects; recover only our versioned runtime cache. */
3
+ import {spawn,spawnSync} from 'node:child_process';
4
+ import {readFileSync,readdirSync} from 'node:fs';
5
+ import {dirname,join,resolve} from 'node:path';
6
+ import {homedir,constants} from 'node:os';
7
+ import {fileURLToPath} from 'node:url';
8
+ import {guide,hostCommand} from './install-guide.mjs';
9
+ import {prepareRuntime} from './runtime.mjs';
10
+ import {holdCache} from './cache-lease.mjs';
11
+ import {preparePlatform} from './platform.mjs';
12
+ const root=resolve(dirname(fileURLToPath(import.meta.url)),'..');
13
+ const manifest=JSON.parse(readFileSync(join(root,'release.json'),'utf8'));
14
+ const env={...process.env,AFU_INSTALL_ROOT:root};
15
+ let args=process.argv.slice(2);
16
+ if(['install','upgrade','status','remove','gc','native','repair'].includes(args[0])){
17
+ const at=args.indexOf('--state');if(at>=0&&args[at+1])env.AFU_STATE_DIR=resolve(args[at+1]);
18
+ const inline=args.find(a=>a.startsWith('--state='));if(inline&&inline.length>8)env.AFU_STATE_DIR=resolve(inline.slice(8));
19
+ }
20
+ let release;
21
+ // Ctrl-C already reaches the child through the shared foreground process group;
22
+ // relaying SIGINT again would deliver it twice. Only forward signals sent to
23
+ // this launcher alone, and only to our own child.
24
+ function relaySignals(child){
25
+ process.on('SIGINT',()=>{});
26
+ for(const signal of ['SIGTERM','SIGHUP'])process.on(signal,()=>child.kill(signal));
27
+ }
28
+ function exitCode(code,signal){
29
+ if(code!==null&&code!==undefined)return code;
30
+ return signal&&constants.signals[signal]?128+constants.signals[signal]:1;
31
+ }
32
+ /** Installed integrations, with what each upgrade needs (config dir, host home, profile). */
33
+ function installedHosts(state){
34
+ const read=f=>{try{return JSON.parse(readFileSync(f,'utf8'));}catch{return null;}};
35
+ const found=[];let names=[];
36
+ try{names=readdirSync(join(state,'integrations')).filter(n=>n.endsWith('.json'));}catch{return found;}
37
+ for(const name of names){
38
+ const record=read(join(state,'integrations',name));
39
+ if(!record)continue;
40
+ if(typeof record.location==='string'&&!/[\\/]/.test(record.location)){
41
+ const receipt=read(join(state,'native',record.location,'install.json'));
42
+ if(receipt&&!receipt.removed&&['claude','codex'].includes(receipt.host))
43
+ found.push({host:receipt.host,version:receipt.version,args:receipt.directory?['--config-dir',receipt.directory]:[],env:{}});
44
+ }else if(['pi','dsh'].includes(record.host)&&!record.removed){
45
+ if(record.plugin_runtime)found.push({host:'dsh',version:record.version,args:['--runtime-only'],env:{}});
46
+ for(const [key,entry] of Object.entries(record.autoload||{})){
47
+ if(record.host==='pi'){
48
+ const agent=entry.entry?dirname(dirname(entry.entry)):null;
49
+ found.push({host:'pi',version:record.version,args:[],env:agent?{PI_CODING_AGENT_DIR:agent}:{}});
50
+ }else{
51
+ const home=entry.manifest?dirname(dirname(dirname(entry.manifest))):null;
52
+ found.push({host:'dsh',version:record.version,args:['--profile',key],env:home?{DSH_HOME:home}:{}});
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return found;
58
+ }
59
+ /** Upgrade every installed host to this launcher's version, one after another. */
60
+ function updateAll(extra){
61
+ const at=extra.indexOf('--state');
62
+ const state=at>=0&&extra[at+1]?resolve(extra[at+1]):(env.AFU_STATE_DIR||join(homedir(),'.local/share/afu-context'));
63
+ const hosts=installedHosts(state);
64
+ if(!hosts.length){console.error('AFU: nothing installed to update; run install HOST first');return 1;}
65
+ let failed=0;
66
+ for(const item of hosts){
67
+ const label=item.host+(item.args[0]==='--profile'?` (profile ${item.args[1]})`:item.args[0]==='--runtime-only'?' (dsh plugin runtime)':'');
68
+ console.error(`AFU: updating ${label}: ${item.version||'unknown'} -> ${manifest.version}`);
69
+ // COSEC: this launcher's own script, argv array, no shell.
70
+ const run=spawnSync(process.execPath,[fileURLToPath(import.meta.url),'upgrade',item.host,...item.args,...extra],{stdio:'inherit',env:{...env,...item.env}});
71
+ if(run.status!==0){failed++;console.error(`AFU: update of ${label} failed; it keeps its current version`);}
72
+ }
73
+ console.error(failed?`AFU: ${hosts.length-failed} of ${hosts.length} updated to ${manifest.version}`:`AFU: all ${hosts.length} integrations are on ${manifest.version}. Open new host sessions to use it.`);
74
+ return failed?1:0;
75
+ }
76
+ try{
77
+ // Only the bare launcher prints its own banner; "afu ui --help" and
78
+ // "afu claude --help" belong to the subcommand or the host.
79
+ if(!args.length || (['--help','-h'].includes(args[0]))){
80
+ const pkg=manifest.name||'afucontext',npx=`npx --yes --registry=https://registry.npmjs.org ${pkg}@latest`;
81
+ console.log(`afuctx ${manifest.version}: AFU Context, local context compression for coding agents\n\n ${npx} install [claude|codex|pi|dsh]\n ${npx} update upgrade every installed host\n ${npx} remove HOST\n ${npx} status [HOST]\n ${npx} dashboard open the local dashboard\n ${npx} gc --dry-run\n ${npx} --version\n\nAfter installing, the same commands are on PATH as afuctx (status, dashboard, --help, --version) and afuctx-admin (dashboard). Host login remains managed by the host.`);
82
+ process.exit(0);
83
+ }
84
+ if(['--version','-v','version'].includes(args[0])){console.log(manifest.version);process.exit(0);}
85
+ if(args[0]==='update'){process.exitCode=updateAll(args.slice(1));process.exit();}
86
+ if(['dashboard','admin'].includes(args[0]))args=['ui',...args.slice(1)];
87
+ args=await guide(args,env);
88
+ if(['install','upgrade'].includes(args[0])&&args.includes('--dry-run')){
89
+ console.log(JSON.stringify({preflight:'passed',host:args[1],configuration_changed:false,request_observed:false}));process.exit(0);
90
+ }
91
+ let removed=false;
92
+ if(['pi','dsh'].includes(args[0])){
93
+ const receipt=join(env.AFU_STATE_DIR??join(homedir(),'.local/share/afu-context'),'integrations',args[0]+'.json');
94
+ try{removed=JSON.parse(readFileSync(receipt,'utf8')).removed===true;}catch(e){if(e.code!=='ENOENT')throw Error('INVALID_INSTALLATION_RECORD');}
95
+ }
96
+ if(removed){
97
+ const split=args.indexOf('--');
98
+ if(split<0 && args.length>1)throw Error('PLUGIN_REMOVED: pass host arguments after --');
99
+ // COSEC: fixed host executable, argv array, no shell; no runtime downloads.
100
+ const [binary,...prefix]=hostCommand(args[0],env);
101
+ const child=spawn(binary,[...prefix,...(split<0?[]:args.slice(split+1))],{stdio:'inherit',env,shell:false});
102
+ relaySignals(child);
103
+ child.on('error',()=>{console.error('AFU: HOST_EXECUTABLE_UNAVAILABLE');process.exitCode=1;});
104
+ child.on('exit',(code,signal)=>{process.exitCode=exitCode(code,signal);});
105
+ }else{
106
+ const hostAction = ['pi','dsh'].includes(args[1]) && ['install','upgrade','remove','status'].includes(args[0]);
107
+ // Status always goes through one reporter (it understands autoload receipts,
108
+ // --json and --state); only Pi/DSH install/upgrade/remove need host-autoload.
109
+ const directHost = hostAction && args[0]!=='status';
110
+ if (directHost && ['install','upgrade'].includes(args[0])) {
111
+ // Autoload paths must outlive npx eviction and the ordinary launcher cache GC.
112
+ const state = env.AFU_STATE_DIR || join(homedir(), '.local/share/afu-context');
113
+ env.AFU_RUNTIME_DIR = join(state, 'autoload', 'runtimes');
114
+ }
115
+ env.AFU_NODE = process.execPath;
116
+ if (args[0] === 'pi') env.AFU_EXPLICIT_EXTENSION = 'pi';
117
+ release=await holdCache(manifest.build,env);
118
+ const location=await prepareRuntime(root,manifest,env);
119
+ const python=join(location,process.platform==='win32'?'Scripts/python.exe':'bin/python');
120
+ env.AFU_INSTALL_ROOT=await preparePlatform(root,manifest,args,env);
121
+ const entry = directHost ? [join(root,'bin/host-autoload.py')] : ['-m','afu_context.local_cli'];
122
+ const child=spawn(python,[...entry,...args],{stdio:'inherit',env});
123
+ relaySignals(child);
124
+ child.on('error',()=>{release?.();console.error('AFU failed to start');process.exitCode=1;});
125
+ child.on('exit',(code,signal)=>{release?.();if(code===0&&['install','upgrade'].includes(args[0]))console.error(`AFU: installed ${args[1]} ${manifest.version}; ${['claude','codex'].includes(args[1])?'service checked; open a new '+args[1]+' session':directHost?'open a new host session':'launch with afu '+args[1]}. Waiting for the first observed request; compression is not yet verified.`);process.exitCode=exitCode(code,signal);});
126
+ }
127
+ }catch(error){release?.();console.error('AFU: '+error.message);process.exitCode=1;}
@@ -0,0 +1,16 @@
1
+ import {mkdirSync,writeFileSync,rmSync} from 'node:fs';
2
+ import {join} from 'node:path';
3
+ import {homedir} from 'node:os';
4
+ import {randomUUID,createHash} from 'node:crypto';
5
+ import {recoverLock} from './runtime.mjs';
6
+ export async function holdCache(build,env=process.env){
7
+ if(!/^[a-f0-9]{64}$/.test(build))throw Error('Invalid build identity');
8
+ const base=env.AFU_RUNTIME_DIR??join(homedir(),'.local/share/afu-context/runtimes');
9
+ mkdirSync(base,{recursive:true,mode:0o700});
10
+ const guard=join(base,'.gc-lock');const end=Date.now()+10000;
11
+ while(true){try{mkdirSync(guard);writeFileSync(join(guard,'owner.json'),JSON.stringify({pid:process.pid}));break;}catch(e){if(e.code!=='EEXIST')throw e;recoverLock(guard);if(Date.now()>end)throw Error('CACHE_GC_BUSY');await new Promise(r=>setTimeout(r,50));}}
12
+ const file=join(base,'.clients',randomUUID()+'.json');
13
+ const state=env.AFU_STATE_DIR??join(homedir(),'.local/share/afu-context');
14
+ try{mkdirSync(join(base,'.owners'),{recursive:true,mode:0o700});writeFileSync(join(base,'.owners',createHash('sha256').update(state).digest('hex')+'.json'),JSON.stringify({state}),{mode:0o600});mkdirSync(join(base,'.clients'),{recursive:true,mode:0o700});writeFileSync(file,JSON.stringify({pid:process.pid,build}),{mode:0o600});}finally{rmSync(guard,{recursive:true,force:true});}
15
+ const release=()=>rmSync(file,{force:true});process.once('exit',release);return release;
16
+ }