@josephyulei/summon-foundation 0.1.11 → 0.1.13

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  保留真实下载状态与独立确认。基础环境不能安全启动页面时,由 Codex 对话先披露并取得独立批准,再准备专用环境;页面启动后全程同页。具体方法见仓库 `docs/install-with-codex.md`,不能先运行 npx 来准备缺失的 Node。不修改共享 PATH;获取器专用环境及其回执按缓存保留,程序另用随包运行时。发行发现仅需 curl,获取还需 tar;已验证载荷的确认、恢复和卸载不依赖下载工具。
6
6
 
7
- 0.1.11 单页试用入口与 Foundation 0.2.16 的运行引擎共同支持当前用户共用安装及清晰的完成、取消和部分完成反馈。同一页面内保留目录选择、程序和 Skill 独立确认。真实安装、新任务发现和主动告知仍需独立验收。
7
+ 0.1.12 单页试用入口与 Foundation 0.2.17 的运行引擎共同支持当前用户共用安装及清晰的完成、取消和部分完成反馈。同一页面内保留目录选择、程序和 Skill 独立确认。真实安装、新任务发现和主动告知仍需独立验收。
8
8
 
9
9
  首次 URL 持续显示本次流程;确认仅转交本次受控子进程的当前计划,不新开manager页。--acquire仍只读获取;--resume仅在用户明确要求后恢复未完成Skill,不重放程序安装。
10
10
 
@@ -16,7 +16,7 @@ Foundation 的薄获取入口,不包含完整运行包,没有 npm 安装生
16
16
  确认 registry 中存在本版本及对应不可变 Release 后,在具备安装权限的新 Codex 对话粘贴:
17
17
 
18
18
  ```text
19
- npx --yes --package @josephyulei/summon-foundation@0.1.11 summon foundation
19
+ npx --yes --package @josephyulei/summon-foundation@0.1.12 summon foundation
20
20
  ```
21
21
 
22
22
  旧版退出推荐入口;支持边界见[现行支持策略](https://github.com/y15801172513-lang/foundation/blob/main/docs/install-platform-024.md#现行支持策略)。旧不可变版本不修改,不自动迁移或清除旧安装。
package/bin/summon.mjs CHANGED
@@ -8,7 +8,7 @@ import {acquireInWorker} from '../lib/acquisition-task.mjs';
8
8
  import {startProgressPage} from '../lib/progress-page.mjs';
9
9
  import {followSelectedSkill} from '../lib/skill-handoff.mjs';
10
10
  import {followMaintenance} from '../lib/maintenance-handoff.mjs';
11
- import {installedClient} from '../lib/manager-step.mjs';
11
+ import {installedClient,assertUpdateEngineSupport,operationFailure} from '../lib/manager-step.mjs';
12
12
  import {resumeSkillHandoff} from '../lib/resume-handoff.mjs';
13
13
  import {parseSummonArgs} from '../lib/cli-options.mjs';
14
14
  import {createOperation,saveOperation,readOperation,runtimeObservation,operationExitCode} from '../lib/operation-result.mjs';
@@ -20,7 +20,7 @@ summon foundation --inspect [--version x.y.z]
20
20
  summon foundation --prepare --version x.y.z --destination /absolute/folder
21
21
  summon foundation --acquire --version x.y.z
22
22
  summon foundation --status /absolute/acquisition/operation-result.json
23
- summon foundation --update --root /absolute/installed-folder --version x.y.z
23
+ summon foundation --update --root /absolute/installed-folder [--version x.y.z]
24
24
  summon foundation --uninstall --root /absolute/installed-folder
25
25
  summon foundation --resume /absolute/acquisition/operation-result.json
26
26
  默认命令下载并验证正式发行,进入安装准备;未给目录时在安装页选择。不会静默安装或注册 Skill。
@@ -78,7 +78,7 @@ try{
78
78
  }else if(maintenance){
79
79
  plainPath(args.root);
80
80
  const env={...process.env};for(const key of ['NODE_OPTIONS','NODE_PATH','NODE_V8_COVERAGE','NODE_REDIRECT_WARNINGS','NODE_COMPILE_CACHE','NODE_COMPILE_CACHE_PORTABLE','NODE_PRESERVE_SYMLINKS'])delete env[key];
81
- const installed=installedClient(args.root,env),current=installed.call(['manager','inspect','--root',args.root]).installation?.current;
81
+ const installed=installedClient(args.root,env),inspection=installed.call(['manager','inspect','--root',args.root]),current=inspection.installation?.current;
82
82
  if(!current?.identity?.installId)fail('当前安装身份无法核实;尚未获取或执行维护');
83
83
  const capability=spawnSync(installed.launcher,['--help'],{encoding:'utf8',env,timeout:15000});
84
84
  if(capability.status!==0||!capability.stdout.includes('--journey-channel'))fail('当前安装不受新版单页流程支持。请另行确认卸载旧版并重新安装新版;本次不删除、不迁移、不回退旧页面');
@@ -89,7 +89,15 @@ try{
89
89
  if(environment.state!=='ready')fail('环境复检未通过:'+environment.state+';缺少 '+environment.missing.join('、')+';保留当前安装,不修改系统工具');
90
90
  let candidate;
91
91
  if(args.update){
92
+ if(args.version&&args.version!==current.version)assertUpdateEngineSupport(inspection,{root:args.root,stage});
92
93
  updateOperation({phase:'discovering'});
94
+ const target=inspectRelease(args.version);args.version=target.version;
95
+ updateOperation({version:target.version,sourceCommit:target.sourceCommit});
96
+ if(target.version===current.version){
97
+ updateOperation({state:'completed',terminal:true,phase:'finished',programState:'unchanged',installationWrites:'none',next:'当前已是所选正式版本;未下载、更新或重新注册 Skill。'});
98
+ await progressPage.close();progressPage=null;process.exit(0);
99
+ }
100
+ assertUpdateEngineSupport(inspection,{root:args.root,stage});
93
101
  const {context,receipt}=await acquireInWorker({version:args.version,stage,operationId:operation.operationId,onContext:context=>updateOperation({version:context.version,sourceCommit:context.sourceCommit}),onProgress:download=>updateOperation({download}),onPhase:phase=>updateOperation({phase,...(phase==='fetching-and-verifying-runtime'?{download:null}:{})})});
94
102
  const directory=plainPath(path.dirname(receipt.launcher)),manifest=JSON.parse(fs.readFileSync(plainPath(path.join(directory,'manifest.json'))));
95
103
  if(manifest.productVersion!==context.version)fail('获取版本与候选不一致');
@@ -149,4 +157,4 @@ try{
149
157
  if(!operation.terminal)updateOperation({state:'verification-required',terminal:true,childExitCode:ended.code,next:'服务退出不等于成功或取消;只读核验原 session 与 installed 状态,不自动重试'});
150
158
  process.exitCode=operationExitCode(operation);
151
159
  }
152
- }catch(e){const message=String(e.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]');updateOperation({state:e.code==='ENTRY_CHOICE_CANCELLED'?'cancelled-no-install':e.code==='ENTRY_CHOICE_EXPIRED'?'expired-no-install':operation?.programState==='completed'?'partial':operation&&operation.installationWrites!=='none'?'verification-required':'failed',terminal:true,...(operation?.programState==='completed'?{failedPhase:operation.phase,phase:'finished'}:{}),errorCode:['ACQUISITION_TRANSPORT_FAILED','ACQUISITION_VALIDATION_FAILED','ENTRY_CHOICE_CANCELLED','ENTRY_CHOICE_EXPIRED'].includes(e.code)?e.code:'INSTALL_ENTRY_FAILED',diagnostic:e.diagnostic||null,next:message+'。先核验旧工具进程已结束及同次结果;保留材料,不自动重试或重放安装确认'});console.error(`错误:${message}`);process.exitCode=operationExitCode(operation);}finally{if(progressPage)await progressPage.close();}
160
+ }catch(e){const failure=operationFailure(e);const message=String(e.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]');updateOperation({state:e.code==='ENTRY_CHOICE_CANCELLED'?'cancelled-no-install':e.code==='ENTRY_CHOICE_EXPIRED'?'expired-no-install':operation?.programState==='completed'?'partial':operation&&operation.installationWrites!=='none'?'verification-required':'failed',terminal:true,...(operation?.programState==='completed'?{failedPhase:operation.phase,phase:'finished'}:{}),errorCode:failure.code,errorStage:failure.stage,retryable:failure.retryable,diagnostic:{...failure,...(e.code==='ACQUISITION_TRANSPORT_FAILED'?{transport:e.diagnostic}:{} )},next:(e.diagnostic?.category?failure.next:message)+'。先核验旧工具进程已结束及同次结果;保留材料,不自动重试或重放安装确认'});console.error(`错误:[${failure.code} / ${failure.stage} / retryable=${failure.retryable}] ${message}`);process.exitCode=operationExitCode(operation);}finally{if(progressPage)await progressPage.close();}
package/lib/acquire.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import {plainPath} from './local-path.mjs';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import crypto from 'node:crypto';
@@ -55,16 +56,7 @@ async function downloadLarge(url,total,onProgress) {
55
56
  }
56
57
  }
57
58
  function regular(file) { const s=fs.lstatSync(file); if(!s.isFile()||s.isSymbolicLink())fail('不是普通文件');return s; }
58
- export function plainPath(file) {
59
- if(!path.isAbsolute(file)||path.normalize(file)!==file)fail('路径必须是规范化绝对路径');
60
- for(let cursor=file;cursor!==path.dirname(cursor);cursor=path.dirname(cursor)){
61
- if(fs.existsSync(cursor)||fs.lstatSync(cursor,{throwIfNoEntry:false})){
62
- const s=fs.lstatSync(cursor);if(s.isSymbolicLink())fail('路径包含符号链接');
63
- if(fs.realpathSync(cursor)!==cursor)fail('路径真实位置不一致');
64
- }
65
- }
66
- return file;
67
- }
59
+ export {plainPath} from './local-path.mjs';
68
60
  export function inspectRelease(version) {
69
61
  if(!Number.isSafeInteger(policy.repositoryId)||policy.repositoryId<=0)fail('公共仓库身份尚未绑定;此源码不是可用发行入口');
70
62
  if(version!==undefined&&!semver.test(version))fail('版本必须是明确的 x.y.z');
@@ -11,7 +11,7 @@ export function acquireInWorker({version,stage,operationId,onContext=()=>{},onPh
11
11
  else if(event.type==='phase')onPhase(event.phase);
12
12
  else if(event.type==='progress')onProgress(event.download);
13
13
  else if(event.type==='complete')receipt=event.receipt;
14
- else if(event.type==='failure')failure=Object.assign(Error(event.message),{code:event.code,diagnostic:event.diagnostic});
14
+ else if(event.type==='failure')failure=Object.assign(Error(event.message),{code:event.code,stage:event.stage,retryable:event.retryable,diagnostic:event.diagnostic});
15
15
  });
16
16
  worker.on('error',error=>{failure=error;});
17
17
  worker.once('exit',code=>{
@@ -3,6 +3,7 @@
3
3
  import {parentPort, workerData} from 'node:worker_threads';
4
4
  import {inspectRelease, acquireRelease} from './acquire.mjs';
5
5
 
6
+ let phase='discovering';
6
7
  try {
7
8
  const context = inspectRelease(workerData.version);
8
9
  // The parent persists context atomically in the stage. Finish that write
@@ -15,10 +16,10 @@ try {
15
16
  await recorded;
16
17
  const receipt = await acquireRelease(context, workerData.stage, {
17
18
  operationId:workerData.operationId,
18
- onPhase:phase=>parentPort.postMessage({type:'phase',phase}),
19
+ onPhase:value=>{phase=value;parentPort.postMessage({type:'phase',phase});},
19
20
  onProgress:download=>parentPort.postMessage({type:'progress',download}),
20
21
  });
21
22
  parentPort.postMessage({type:'complete',receipt});
22
23
  } catch(error) {
23
- parentPort.postMessage({type:'failure',code:error.code,diagnostic:error.diagnostic||null,message:String(error.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]')});
24
+ parentPort.postMessage({type:'failure',code:error.code||'ACQUISITION_VALIDATION_FAILED',stage:phase,retryable:error.code==='ACQUISITION_TRANSPORT_FAILED',diagnostic:error.diagnostic||null,message:String(error.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]')});
24
25
  }
@@ -16,7 +16,7 @@ export function parseSummonArgs(args){
16
16
  if(seen.has('--status')){if(!result.status||seen.size!==1)fail('结果查询需要非空路径且不能与其他操作组合');return result;}
17
17
  if(seen.has('--resume')){if(!result.resume||seen.size!==1)fail('恢复需要单一结果路径,不接受新目标或额外操作');return result;}
18
18
  if(result.update||result.uninstall){
19
- if(result.update&&result.uninstall||!result.root||result.inspect||result.acquire||result.prepare||result.destination||result.uninstall&&result.version||result.update&&!result.version)fail('维护需要单一动作、明确 --root;更新还需要固定 --version');
19
+ if(result.update&&result.uninstall||!result.root||result.inspect||result.acquire||result.prepare||result.destination||result.uninstall&&result.version)fail('维护需要单一动作、明确 --root;更新可用 --version 指定目标,否则查询并固定最新正式版本');
20
20
  return result;
21
21
  }
22
22
  if(result.root)fail('--root 仅用于明确的更新或卸载');
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
- import {plainPath} from './acquire.mjs';
4
+ import {plainPath} from './local-path.mjs';
5
5
 
6
6
  export function classifyEntryEnvironment({platform,arch,nodeVersion,tools}){
7
7
  const [major,minor]=String(nodeVersion||'').split('.').map(Number);
@@ -11,12 +11,17 @@ function scopeFields(value){
11
11
  // Only the exact child created by this invocation may supply an active view.
12
12
  // Browser input cannot name a URL, route, transport token or future plan.
13
13
  export function createJourneyControl(readRecord, changed) {
14
- const children=new Map();let active=null,origin=null,choice=null,resolveChoice,rejectChoice,choiceTimer,viewRevision=0;
14
+ const children=new Map();let active=null,origin=null,choice=null,resolveChoice,rejectChoice,choiceTimer,viewRevision=0,submitting=0;
15
15
  const headers=owner=>({origin,'content-type':'application/json','x-foundation-journey':owner.token,'x-foundation-operation':readRecord().operationId});
16
16
  const request=async(owner,route,body)=>{
17
17
  if(owner.child.exitCode!==null||owner.child.signalCode)throw Error('确认服务已退出,结果待核实');
18
- const response=await fetch(owner.url+route,{method:body?'POST':'GET',headers:headers(owner),...(body?{body:JSON.stringify(body)}:{}),redirect:'error',signal:AbortSignal.timeout(120000)});
19
- return {status:response.status,body:await response.json()};
18
+ const ended=new AbortController();
19
+ const onClose=()=>ended.abort(Error('确认服务已退出,结果待核实'));
20
+ owner.child.once('close',onClose);
21
+ try {
22
+ const response=await fetch(owner.url+route,{method:body?'POST':'GET',headers:headers(owner),...(body?{body:JSON.stringify(body)}:{}),redirect:'error',signal:AbortSignal.any([ended.signal,AbortSignal.timeout(body?120000:5000)])});
23
+ return {status:response.status,body:await response.json()};
24
+ } finally { owner.child.removeListener('close',onClose); }
20
25
  };
21
26
  return {
22
27
  setOrigin(value){if(origin)throw Error('单页来源不能重绑定');origin=value;},
@@ -43,6 +48,14 @@ export function createJourneyControl(readRecord, changed) {
43
48
  if(selected.status!==200){active.error=selected.body.message;choice=null;changed();}
44
49
  }
45
50
  },
51
+ pending(){return submitting>0;},
52
+ async reconcile(){
53
+ const a=active;if(!a)return Boolean(readRecord().terminal||readRecord().phase==='choosing-intent');
54
+ const result=await request(a.owner,a.type==='selection'?'__foundation/install/view':'__foundation/manager/view');
55
+ if(active!==a)return false;
56
+ if(result.status!==200||result.body.operationId!==readRecord().operationId||a.type==='manager'&&result.body.session?.sessionId!==a.view.session?.sessionId)throw Error('状态核对身份不符');
57
+ a.view=result.body;return true;
58
+ },
46
59
  view(){return active?{type:active.type,...active.view,error:active.error||null}:null;},
47
60
  waitChoice({timeoutMs=600000}={}){return new Promise((resolve,reject)=>{
48
61
  if(resolveChoice)throw Error('目录选择已在等待');
@@ -51,6 +64,7 @@ export function createJourneyControl(readRecord, changed) {
51
64
  changed();
52
65
  });},
53
66
  async submit(body){
67
+ submitting++;try{
54
68
  if(body.operationId!==readRecord().operationId)throw Error('流程身份不匹配');
55
69
  if(body.action==='cancel-intent'&&rejectChoice){
56
70
  clearTimeout(choiceTimer);const reject=rejectChoice;resolveChoice=null;rejectChoice=null;
@@ -80,6 +94,7 @@ export function createJourneyControl(readRecord, changed) {
80
94
  }
81
95
  const result=await request(a.owner,'__foundation/manager/confirm',{operationId:body.operationId,sessionId:s.sessionId,planHash:s.planHash,managerNonce:body.managerNonce,action:body.action});
82
96
  if(active===a&&result.body.state)a.view.session={...s,...result.body};changed();return result;
97
+ }finally{submitting--;}
83
98
  }
84
99
  };
85
100
  }
@@ -0,0 +1,13 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const fail=message=>{throw Object.assign(Error('错误:'+message),{code:'ACQUISITION_VALIDATION_FAILED'})};
4
+ export function plainPath(file) {
5
+ if(!path.isAbsolute(file)||path.normalize(file)!==file)fail('路径必须是规范化绝对路径');
6
+ for(let cursor=file;cursor!==path.dirname(cursor);cursor=path.dirname(cursor)){
7
+ if(fs.existsSync(cursor)||fs.lstatSync(cursor,{throwIfNoEntry:false})){
8
+ const s=fs.lstatSync(cursor);if(s.isSymbolicLink())fail('路径包含符号链接');
9
+ if(fs.realpathSync(cursor)!==cursor)fail('路径真实位置不一致');
10
+ }
11
+ }
12
+ return file;
13
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import {isDeepStrictEqual} from 'node:util';
4
- import {plainPath} from './acquire.mjs';
4
+ import {plainPath} from './local-path.mjs';
5
5
  import {installedClient,observePlan} from './manager-step.mjs';
6
6
  import {followSelectedSkill} from './skill-handoff.mjs';
7
7
 
@@ -1,6 +1,21 @@
1
1
  import {spawn,spawnSync} from 'node:child_process';
2
2
  import path from 'node:path';
3
- import {plainPath} from './acquire.mjs';
3
+ import fs from 'node:fs';
4
+ import {plainPath} from './local-path.mjs';
5
+
6
+ import {operationFailure,installedCommandFailure} from './operation-failure.mjs';
7
+ export {operationFailure,installedCommandFailure} from './operation-failure.mjs';
8
+
9
+ export function assertUpdateEngineSupport(inspection,{root,stage}={}) {
10
+ if(['0.2.15','0.2.16'].includes(inspection?.installation?.current?.version)&&root&&stage){
11
+ const relative=path.relative(path.dirname(plainPath(root)),plainPath(stage));
12
+ if(relative&&!relative.startsWith('..'+path.sep)&&relative!=='..'&&!path.isAbsolute(relative))return;
13
+ }
14
+ if(inspection?.supportedLifecycleOptions?.updateCandidateAuthority!=='os-account-acquisition-v1'){
15
+ const detail=operationFailure({code:'UPDATE_ENGINE_ACQUISITION_UNSUPPORTED',stage:'authority',retryable:false});
16
+ throw Object.assign(Error(detail.next),detail,{diagnostic:detail});
17
+ }
18
+ }
4
19
 
5
20
  // A client of the existing installed manager, never a confirmation authority.
6
21
  export function installedClient(root,env,journeyControl=null) {
@@ -8,7 +23,7 @@ export function installedClient(root,env,journeyControl=null) {
8
23
  const launcher=plainPath(path.join(root,'bin/foundation-kit'));
9
24
  const call=args=>{
10
25
  const r=spawnSync(launcher,args,{env,encoding:'utf8',timeout:30000,maxBuffer:4*1024*1024});
11
- if(r.status!==0){const e=Error('已安装入口检查失败;保留已核实结果,不自动重放');e.code='INSTALLED_COMMAND_FAILED';e.command=args.slice(0,2);e.exitCode=r.status;throw e;}
26
+ if(r.status!==0)throw installedCommandFailure(r);
12
27
  return JSON.parse(r.stdout);
13
28
  };
14
29
  return {root,launcher,call,env,journeyControl};
@@ -58,3 +73,17 @@ export async function observePlan(client,requested,onChange) {
58
73
  });
59
74
  });
60
75
  }
76
+
77
+ // Resolve only the explicitly registered hint; never search for an executable.
78
+ export function discoverRegisteredInstallation(bindingFile,env){
79
+ const file=plainPath(bindingFile),stat=fs.lstatSync(file);
80
+ if(!stat.isFile()||stat.uid!==process.getuid()||stat.nlink!==1||stat.size>65536)throw Error('安装定位记录类型或归属无效');
81
+ const binding=JSON.parse(fs.readFileSync(file,'utf8'));
82
+ if(binding.schemaVersion!=='1.0.0'||binding.resolver!=='installed-current'||binding.authority!=='discovery-hint-only'||typeof binding.installId!=='string')throw Error('安装定位记录无效,不授予维护权限');
83
+ const client=installedClient(binding.installationRoot,env);
84
+ const inspection=client.call(['manager','inspect','--root',client.root]);
85
+ const current=inspection.installation?.current;
86
+ if(current?.identity?.installId!==binding.installId||inspection.bridge?.currentVersion!==current.version)throw Error('注册位置与当前安装身份或健康不一致');
87
+ if(inspection.supportedLifecycleOptions?.maintenance?.protocol!=='installed-maintenance/1')throw Error('当前安装缺少随包维护能力;请按该版本正式恢复说明处理,不替换旧引擎');
88
+ return {installationRoot:client.root,installId:binding.installId,launcher:client.launcher,currentVersion:current.version,candidateHash:current.candidateHash,inspection,executionAuthority:false};
89
+ }
@@ -0,0 +1,18 @@
1
+ export function operationFailure(error) {
2
+ const code=/^[A-Z][A-Z0-9_]{1,79}$/.test(error?.code||'')?error.code:'INSTALL_ENTRY_FAILED';
3
+ const stage=/^[a-z][a-z-]{0,39}$/.test(error?.stage||'')?error.stage:'entry';
4
+ const category=code==='ACQUISITION_TRANSPORT_FAILED'?'transport':/PATH|TRUSTED_ROOT|SYMLINK|ANCESTOR|ACQUISITION_CACHE|UPDATE_ENGINE/.test(code)?'local-path':/CANDIDATE|INTEGRITY|SIGNATURE|ACQUISITION_VALIDATION/.test(code)?'integrity':/CONFIRM|PLAN|SESSION/.test(code)?'confirmation':'engine';
5
+ const next=category==='local-path'?'当前引擎不能安全接收此更新候选。保留原安装与已下载材料,不重复下载或搬移候选;核实版本兼容性。旧引擎不支持时,须另行确认保留项目资料后的卸载与新版首装。':category==='integrity'?'候选完整性或来源核验失败;保留证据,不执行或手工修补材料。':category==='transport'?'网络传输未完成;保留本次记录,核实网络后再明确发起获取,尚未执行安装。':category==='confirmation'?'确认或计划未完成;核实同次状态,不重放旧批准。':'当前操作未核实完成;保留结果,检查同次记录,不自动重试。';
6
+ return {code,stage,retryable:error?.retryable===true,category,next};
7
+ }
8
+
9
+ export function installedCommandFailure(result) {
10
+ let value;
11
+ for(const text of [result.stderr,result.stdout]){
12
+ if(typeof text!=='string'||text.length>65536)continue;
13
+ try{const parsed=JSON.parse(text);value=parsed.error||parsed;if(value?.code)break;}catch{}
14
+ }
15
+ const detail=operationFailure(value||{code:'INSTALLED_COMMAND_FAILED',stage:'installed-command'});
16
+ // Never forward arbitrary stderr/message/details, credentials or plan nonces.
17
+ return Object.assign(Error(detail.next),detail,{diagnostic:detail,exitCode:result.status});
18
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
+ import {plainPath} from './local-path.mjs';
4
5
 
5
6
  export function createOperation() {
6
7
  return {schemaVersion:'1.0.0',operationId:crypto.randomUUID(),pid:process.pid,startedAt:new Date().toISOString(),phase:'discovering',state:'running',terminal:false,installationWrites:'none',skillRegistered:false};
@@ -47,3 +48,46 @@ export function readOperation(file) {
47
48
  const r=JSON.parse(fs.readFileSync(file));if(r.schemaVersion!=='1.0.0'||typeof r.operationId!=='string'||typeof r.terminal!=='boolean')throw Error('结果记录无效');
48
49
  return {...r,...(!r.terminal?{state:'verification-required',note:'这是最后一次记录,不证明原进程仍活着。先查原工具句柄和同次记录;不自动重试。'}:{}),readOnly:true,executionAuthority:false};
49
50
  }
51
+
52
+ // Bounded discovery only: these are the two product-owned account locations.
53
+ // Records are observations, never installation or execution authority.
54
+ export function discoverOperations({home,installationRoot,installId}) {
55
+ plainPath(home);plainPath(installationRoot);
56
+ if(!/^[a-zA-Z0-9-]+$/.test(installId||''))throw Error('安装身份格式无效');
57
+ const locations=[
58
+ [path.join(home,'Library','Application Support','Foundation Maintenance',installId),/^operation-[a-f0-9-]+$/],
59
+ [path.join(home,'Library','Caches','ai-product-foundation-kit-acquisition'),/^acquisition\.[a-zA-Z0-9]+$/]
60
+ ];
61
+ const operations=[],unavailable=[];
62
+ for(const [directory,pattern] of locations){
63
+ plainPath(directory);if(!fs.existsSync(directory))continue;
64
+ const stat=fs.lstatSync(directory);
65
+ if(!stat.isDirectory()||stat.uid!==process.getuid()||(stat.mode&511)!==448)throw Error('操作记录目录归属或权限无效');
66
+ for(const name of fs.readdirSync(directory).filter(n=>pattern.test(n)).sort()){
67
+ const file=path.join(directory,name,'operation-result.json');
68
+ try{
69
+ plainPath(file);
70
+ const folder=fs.lstatSync(path.dirname(file));
71
+ if(!folder.isDirectory()||folder.uid!==process.getuid()||(folder.mode&511)!==448)throw Error('操作目录权限无效');
72
+ if(!fs.existsSync(file))continue;
73
+ const record=readOperation(file);
74
+ if(record.installationRoot!==installationRoot||record.installId!==installId)continue;
75
+ const kind=record.kind||(record.programState==='completed'?'install':null);
76
+ const completed=record.state==='completed'||kind==='uninstall'&&record.programState==='completed';
77
+ const remaining=completed?[]:kind==='uninstall'?[
78
+ ...(record.maintenanceSteps?.remove?.state==='completed'?[]:['核对当前 Skill 归属;仅存在时另行确认移除']),
79
+ '重新确认程序卸载'
80
+ ]:record.programState==='completed'&&record.journeyContext?.skillChoice==='selected'&&!record.skillRegistered&&record.skillSteps?.register?.state!=='completed'?['核对当前 Skill 材料和注册,仅为缺项请求新确认']:[];
81
+ operations.push({...record,kind,resultFile:file,completedActions:[...(record.programState==='completed'?['程序'+(kind==='uninstall'?'卸载':'安装或更新')]:[]),...(record.maintenanceSteps?.remove?.state==='completed'?['移除 Skill']:[])],remainingActions:remaining,resumable:remaining.length>0});
82
+ }catch(error){unavailable.push({file,reason:error.message});}
83
+ }
84
+ }
85
+ const superseded=new Set(operations.map(o=>o.resumes).filter(Boolean));
86
+ return {operations:operations.map(o=>({...o,resumable:o.resumable&&!superseded.has(o.operationId),superseded:superseded.has(o.operationId)})),unavailable,mutationPerformed:false,executionAuthority:false};
87
+ }
88
+
89
+ export function selectResumableOperation(discovery,operationId=null){
90
+ const choices=discovery.operations.filter(o=>o.resumable&&(!operationId||o.operationId===operationId));
91
+ if(choices.length!==1)throw Error(choices.length?'有多个未完成操作,请从只读查询结果选择 --operation-id;不要猜测或重放批准':'未找到可续接操作;先只读核对当前状态');
92
+ return choices[0];
93
+ }
@@ -53,7 +53,7 @@ export function acquisitionProgress(record) {
53
53
  const content=lifecycleContent(record,observations);
54
54
  Object.assign(phases,{'awaiting-confirmation':'等待你确认'+verb,executing:'正在'+verb,'runtime-ended':'读取'+verb+'结果',finished:verb+'结果已返回'});
55
55
  if(record.terminal&&record.state==='partial'&&record.programState==='completed'&&kind==='update')journey.title='程序已更新,Skill 尚未更新';
56
- return {content,journey,operationId:record.operationId,title:journey.title,phase:({'skill-confirmation':'等待你确认 Codex 操作','skill-executing':'正在处理 Codex 对话能力'})[record.phase]||phases[record.phase]||'正在核实本次操作',state:record.state,version:record.currentVersion&&kind==='update'?record.currentVersion+' → '+record.version:record.version||record.currentVersion||null,download:record.download||null,downloadStatus:downloadStatus(record),program:record.programState==='completed'?'程序'+verb+'已完成;'+(kind==='uninstall'?'结果由独立回执保留':record.runtimeHealth==='passed'?'稳定入口健康已核验':'健康待核实'):record.installationWrites==='none'?'尚未执行程序操作':'程序结果以同次记录为准',skill:kind==='uninstall'?(observations.remove?.state==='completed'?'已解除已归属 Codex 注册;修改和未知文件按计划保留':'本次没有已核实的注册移除结果'):record.skillRegistered===true?'Skill 文件与归属已核验;新对话识别另验':record.journeyContext?.skillChoice==='skipped'?'本次不接入 Codex':record.journeyContext?.skillChoice==='selected'?'已选择接入 Codex;仅在分别确认后执行':'Codex 接入可选,尚未选择',project:'未自动接入项目',installationRoot:record.installationRoot||null,skillDestination:record.skillDestination||null,next:record.next||(record.terminal&&record.state==='completed'&&record.journeyContext?.skillChoice==='skipped'?'本次已跳过 Codex 接入;程序操作已完成,项目另行确认。':journey.summary),nextUrl:record.terminal?(kind!=='uninstall'&&record.workbench?.state==='ready'?record.workbench.url:null):record.confirmationUrl||null,nextLabel:record.terminal?'打开已安装工作台':record.phase?.startsWith('skill-')?'查看 Codex 精确确认':kind==='install'?'继续到目录选择 / 本人确认页':'查看'+verb+'确认',resultFile:record.resultFile||null,terminal,diagnostic:record.diagnostic||null};
56
+ return {content,journey,operationId:record.operationId,title:journey.title,phase:({'skill-confirmation':'等待你确认 Codex 操作','skill-executing':'正在处理 Codex 对话能力'})[record.phase]||phases[record.phase]||'正在核实本次操作',state:record.state,version:record.currentVersion&&kind==='update'?record.currentVersion+' → '+record.version:record.version||record.currentVersion||null,download:record.download||null,downloadStatus:downloadStatus(record),program:record.programState==='completed'?'程序'+verb+'已完成;'+(kind==='uninstall'?'结果由独立回执保留':record.runtimeHealth==='passed'?'稳定入口健康已核验':'健康待核实'):record.installationWrites==='none'?'尚未执行程序操作':'程序结果以同次记录为准',skill:kind==='uninstall'?(observations.remove?.state==='completed'?'已解除已归属 Codex 注册;修改和未知文件按计划保留':'本次没有已核实的注册移除结果'):record.skillRegistered===true?'Skill 文件与归属已核验;新对话识别另验':record.journeyContext?.skillChoice==='skipped'?'本次不接入 Codex':record.journeyContext?.skillChoice==='selected'?'已选择接入 Codex;仅在分别确认后执行':'Codex 接入可选,尚未选择',project:'未自动接入项目',installationRoot:record.installationRoot||null,skillDestination:record.skillDestination||null,next:record.next||(record.terminal&&record.state==='completed'&&record.journeyContext?.skillChoice==='skipped'?'本次已跳过 Codex 接入;程序操作已完成,项目另行确认。':journey.summary),nextUrl:record.terminal?(kind!=='uninstall'&&record.workbench?.state==='ready'?record.workbench.url:null):record.confirmationUrl||null,nextLabel:record.terminal?'打开已安装工作台':record.phase?.startsWith('skill-')?'查看 Codex 精确确认':kind==='install'?'继续到目录选择 / 本人确认页':'查看'+verb+'确认',resultFile:record.resultFile||null,terminal,errorCode:record.errorCode||null,errorStage:record.errorStage||null,retryable:record.retryable===true,diagnostic:record.diagnostic||null};
57
57
  }
58
58
 
59
59
  // A bounded, read-only loopback view over the existing in-memory operation.
@@ -66,7 +66,7 @@ export async function startProgressPage(readRecord, {control = null} = {}) {
66
66
  const current=()=>JSON.stringify(acquisitionProgress(readRecord()));
67
67
  const server=http.createServer(async(req,res)=>{
68
68
  if(req.headers.host!==origin||req.headers.origin&&req.headers.origin!==`http://${origin}`){res.writeHead(403).end();return;}
69
- if(control&&req.method==='GET'&&req.url===base+'/state'){const record=readRecord();if(record.terminal)res.once('finish',()=>{terminalObserved=true;terminalRead?.();});res.writeHead(200,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify({record,progress:acquisitionProgress(record),view:control.view()}));return;}
69
+ if(control&&req.method==='GET'&&req.url===base+'/state'){let reconciled=false;try{reconciled=await control.reconcile?.()===true;}catch{/* The child may exit between steps; retain the operation snapshot without claiming reconciliation. */}const record=readRecord();if(record.terminal)res.once('finish',()=>{terminalObserved=true;terminalRead?.();});res.writeHead(200,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify({record,progress:acquisitionProgress(record),view:control.view(),reconciled,submissionPending:control.pending?.()===true}));return;}
70
70
  if(control&&req.method==='POST'&&req.url===base+'/action'){
71
71
  if(req.headers.origin!==`http://${origin}`||req.headers['content-type']!=='application/json'){res.writeHead(403).end();return;}
72
72
  try{let body='',size=0;for await(const chunk of req){size+=chunk.length;if(size>16384)throw Error('请求过大');body+=chunk;}const input=JSON.parse(body);if(input.csrf!==token)throw Error('页面确认身份不符');const result=await control.submit(input);res.writeHead(result.status,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify(result.body));}catch(error){res.writeHead(409,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify({message:error.message}));}return;
@@ -84,6 +84,13 @@ export async function startProgressPage(readRecord, {control = null} = {}) {
84
84
  // Allow the same page to receive the terminal snapshot before this bounded
85
85
  // service exits. No polling or page acknowledgement authorizes a mutation.
86
86
  if(control&&readRecord().terminal&&!terminalObserved)await new Promise(resolve=>{const timer=setTimeout(resolve,15000);terminalRead=()=>{clearTimeout(timer);resolve();};});
87
- for(const res of clients)res.end();clients.clear();return new Promise(resolve=>server.close(resolve));
87
+ for(const res of clients)res.end();clients.clear();return new Promise(resolve=>{
88
+ // A browser may retain an unread/aborted response after the terminal
89
+ // snapshot. Stop accepting work, then bound connection draining so the
90
+ // completed CLI does not remain a live owner that blocks later recovery.
91
+ const drain=setTimeout(()=>server.closeAllConnections(),2000);
92
+ server.close(()=>{clearTimeout(drain);resolve();});
93
+ server.closeIdleConnections();
94
+ });
88
95
  }};
89
96
  }
@@ -3,22 +3,23 @@ import {installedClient} from './manager-step.mjs';
3
3
  import {followSelectedSkill} from './skill-handoff.mjs';
4
4
 
5
5
  // Explicit user-requested recovery, never an automatic replay of approval.
6
- export async function resumeSkillHandoff({file,env,onChange,journeyControl=null}) {
6
+ export async function resumeSkillHandoff({file,env,onChange,journeyControl=null,newOperation=null}) {
7
7
  const previous=readOperation(file);
8
8
  if(previous.pid&&previous.pid!==process.pid){
9
9
  try{process.kill(previous.pid,0);throw Error('原进程仍存在,先在原任务查询;不并发恢复');}
10
10
  catch(e){if(e.code!=='ESRCH')throw e;}
11
11
  }
12
+ if(previous.skillRegistered||previous.skillSteps?.register?.state==='completed')throw Error('旧记录已完成 Skill 注册;不通过恢复重新安装后来删除的 Skill');
12
13
  if(previous.programState!=='completed'||previous.journeyContext?.skillChoice!=='selected'||!previous.installationRoot)throw Error('该记录不是程序完成后的 Skill 待续;先只读核实原操作,不重跑程序');
13
14
  const client=installedClient(previous.installationRoot,env),current=client.call(['manager','inspect','--root',previous.installationRoot]).installation?.current;
14
15
  if(!previous.installId||current?.identity?.installId!==previous.installId||previous.installedCandidateHash&&current.candidateHash!==previous.installedCandidateHash)throw Error('恢复记录与当前安装不一致;需重新核实');
15
16
  if(previous.currentPlanRef){
16
17
  const status=client.call(['manager','status','--plan-ref',previous.currentPlanRef]);
17
- if(['executing','consumed'].includes(status.state))throw Error('旧计划仅有执行中记录;先核实或按原恢复机制处理,不能自动重试');
18
+ if(['pending','executing','consumed'].includes(status.state))throw Error('旧计划仅有执行中记录;先核实或按原恢复机制处理,不能自动重试');
18
19
  // Completed material is reused by capability status. Cancelled/expired
19
20
  // confirmation is not reused: followSelectedSkill requests a fresh plan.
20
21
  }
21
- const operation={...previous,pid:process.pid,state:'awaiting-skill',terminal:false};
22
+ const operation={...previous,...(newOperation||{}),pid:process.pid,state:'awaiting-skill',terminal:false};
22
23
  onChange(operation);
23
24
  return followSelectedSkill({operation,env,onChange,journeyControl});
24
25
  }
@@ -2,4 +2,4 @@
2
2
  // Source SHA-256: e9949cc0d67e979b0297ca654eda1f163f2178b3338d4c4cf3ea9363e3cd8644
3
3
  // Demo-only selectors remain inert; there are no demo controls in production.
4
4
  // C5: download metadata wraps within the existing right-hand status slot.
5
- export const singlePageStyle = "#foundation-flow-demo{--fp-bg:light-dark(#fff,#191b20);--fp-soft:light-dark(#f7f8fa,#23262c);--fp-text:light-dark(#20252d,#eff1f5);--fp-muted:light-dark(#606a78,#adb5c3);--fp-line:light-dark(#e5e8ee,#383d46);--fp-accent:light-dark(#315ee8,#98b3ff);--fp-active:light-dark(#f3f6ff,#232f48);--fp-good:light-dark(#23784c,#8ddcb1);--fp-error:light-dark(#b33434,#ffadad);font-family:Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;color:var(--fp-text);font-size:14px;line-height:1.6;color-scheme:light dark}\n#foundation-flow-demo *{box-sizing:border-box}#foundation-flow-demo [hidden]{display:none!important}#foundation-flow-demo button,#foundation-flow-demo input,#foundation-flow-demo select{font:inherit;color:inherit}#foundation-flow-demo button,#foundation-flow-demo select{cursor:pointer}#foundation-flow-demo button{border:1px solid var(--fp-line);background:var(--fp-bg);border-radius:7px;padding:9px 15px;min-height:42px}#foundation-flow-demo button:disabled{opacity:.55;cursor:default}#foundation-flow-demo .demo-tools{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:14px;color:var(--fp-muted);font-size:12px}#foundation-flow-demo .demo-tools label{margin-left:auto;display:flex;align-items:center;gap:8px}#foundation-flow-demo select{padding:7px;border:1px solid var(--fp-line);border-radius:6px;background:var(--fp-bg)}#foundation-flow-demo .product{background:var(--fp-bg);border:1px solid var(--fp-line);border-radius:14px;overflow:hidden}#foundation-flow-demo .brand{display:flex;align-items:center;gap:9px;padding:18px 26px;border-bottom:1px solid var(--fp-line);font-weight:500}#foundation-flow-demo .brand-mark{display:grid;place-items:center;width:26px;height:26px;border-radius:6px;background:var(--fp-text);color:var(--fp-bg)}#foundation-flow-demo .private-label{margin-left:auto;font-size:12px;color:var(--fp-muted);font-weight:400}#foundation-flow-demo .overview{padding:24px 26px 20px}#foundation-flow-demo h1{font-size:25px;font-weight:500;line-height:1.3;margin:5px 0 8px}#foundation-flow-demo p{margin:6px 0}#foundation-flow-demo .eyebrow{color:var(--fp-accent);font-size:12px;font-weight:500}#foundation-flow-demo #flow-description{color:var(--fp-muted)}#foundation-flow-demo .metadata{display:flex;flex-direction:column;gap:4px;margin:15px 0;color:var(--fp-muted);font-size:12px;overflow-wrap:anywhere}#foundation-flow-demo .progress-label{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:12px;margin-bottom:8px}#foundation-flow-demo #flow-status{color:var(--fp-accent)}#foundation-flow-demo .track{height:5px;background:var(--fp-soft);border-radius:4px;overflow:hidden}#foundation-flow-demo #flow-bar{height:100%;background:var(--fp-accent);width:0;transition:width .2s}#foundation-flow-demo .steps{list-style:none;padding:0 26px;margin:0}#foundation-flow-demo .step{border-top:1px solid var(--fp-line);padding:15px 0}#foundation-flow-demo .step.current{background:var(--fp-active);border:1px solid var(--fp-line);border-radius:10px;padding:16px;margin:4px -16px 8px}#foundation-flow-demo .step-top{display:flex;align-items:center;gap:12px}#foundation-flow-demo .step-number{flex:none;border:1px solid var(--fp-line);border-radius:50%;width:25px;height:25px;display:grid;place-items:center;font-size:12px;color:var(--fp-muted)}#foundation-flow-demo .done .step-number{color:var(--fp-good);border-color:transparent;background:var(--fp-soft)}#foundation-flow-demo .current .step-number{background:var(--fp-accent);color:var(--fp-bg);border-color:transparent}#foundation-flow-demo .step-title{font-weight:500;flex:1;min-width:0;overflow-wrap:anywhere}#foundation-flow-demo .step-state{font-size:12px;color:var(--fp-muted);max-width:48%;text-align:right;overflow-wrap:anywhere}#foundation-flow-demo .done .step-state{color:var(--fp-good)}#foundation-flow-demo .step-body{margin:12px 0 0 37px;color:var(--fp-muted)}#foundation-flow-demo .field{display:block;margin:13px 0 5px;color:var(--fp-text)}#foundation-flow-demo input[type=text]{width:100%;min-width:0;font-size:16px;background:var(--fp-bg);border:1px solid var(--fp-line);border-radius:7px;padding:10px}#foundation-flow-demo .check{display:flex;align-items:flex-start;gap:9px;margin:14px 0;color:var(--fp-text)}#foundation-flow-demo .check input{margin-top:6px;accent-color:var(--fp-accent)}#foundation-flow-demo .hint{font-size:12px;color:var(--fp-muted);overflow-wrap:anywhere}#foundation-flow-demo .actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:17px}#foundation-flow-demo .primary{background:var(--fp-accent);border-color:var(--fp-accent);color:var(--fp-bg);font-weight:500}#foundation-flow-demo .danger{background:var(--fp-error);border-color:var(--fp-error);color:var(--fp-bg)}#foundation-flow-demo .error{color:var(--fp-error);font-size:13px;margin-top:6px}#foundation-flow-demo .facts{border-left:2px solid var(--fp-line);padding-left:12px;margin:12px 0;overflow-wrap:anywhere}#foundation-flow-demo .result{margin:14px 26px 24px;padding:18px;background:var(--fp-soft);border-radius:10px}#foundation-flow-demo .result h2{font-size:18px;font-weight:500;margin:0 0 8px}#foundation-flow-demo footer{padding:17px 26px;border-top:1px solid var(--fp-line);color:var(--fp-muted);font-size:12px}#foundation-flow-demo summary{cursor:pointer}#foundation-flow-demo .completed-detail{margin-left:37px;font-size:12px;color:var(--fp-muted)}\n@media(max-width:480px){#foundation-flow-demo .overview,#foundation-flow-demo .brand,#foundation-flow-demo footer{padding-left:17px;padding-right:17px}#foundation-flow-demo .steps{padding:0 17px}#foundation-flow-demo .step.current{margin-left:-8px;margin-right:-8px;padding:12px 8px}#foundation-flow-demo .step-body{margin-left:0}#foundation-flow-demo .step-top{gap:8px}#foundation-flow-demo .step-state{max-width:82px;text-align:right}#foundation-flow-demo .demo-tools label{margin-left:0}#foundation-flow-demo .result{margin:14px 17px}#foundation-flow-demo h1{font-size:23px}}\n@media(prefers-reduced-motion:reduce){#foundation-flow-demo #flow-bar{transition:none}}";
5
+ export const singlePageStyle = "#foundation-flow-demo{max-width:960px;margin:24px auto;padding:0 16px;overflow-wrap:anywhere}#foundation-flow-demo button:focus-visible,#foundation-flow-demo input:focus-visible,#foundation-flow-demo summary:focus-visible{outline:3px solid var(--fp-accent);outline-offset:3px}#foundation-flow-demo h2{font-size:18px;font-weight:500}#foundation-flow-demo pre{white-space:pre-wrap;overflow-wrap:anywhere}#foundation-flow-demo{--fp-bg:light-dark(#fff,#191b20);--fp-soft:light-dark(#f7f8fa,#23262c);--fp-text:light-dark(#20252d,#eff1f5);--fp-muted:light-dark(#606a78,#adb5c3);--fp-line:light-dark(#e5e8ee,#383d46);--fp-accent:light-dark(#315ee8,#98b3ff);--fp-active:light-dark(#f3f6ff,#232f48);--fp-good:light-dark(#23784c,#8ddcb1);--fp-error:light-dark(#b33434,#ffadad);font-family:Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;color:var(--fp-text);font-size:14px;line-height:1.6;color-scheme:light dark}\n#foundation-flow-demo *{box-sizing:border-box}#foundation-flow-demo [hidden]{display:none!important}#foundation-flow-demo button,#foundation-flow-demo input,#foundation-flow-demo select{font:inherit;color:inherit}#foundation-flow-demo button,#foundation-flow-demo select{cursor:pointer}#foundation-flow-demo button{border:1px solid var(--fp-line);background:var(--fp-bg);border-radius:7px;padding:9px 15px;min-height:42px}#foundation-flow-demo button:disabled{opacity:.55;cursor:default}#foundation-flow-demo .demo-tools{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:14px;color:var(--fp-muted);font-size:12px}#foundation-flow-demo .demo-tools label{margin-left:auto;display:flex;align-items:center;gap:8px}#foundation-flow-demo select{padding:7px;border:1px solid var(--fp-line);border-radius:6px;background:var(--fp-bg)}#foundation-flow-demo .product{background:var(--fp-bg);border:1px solid var(--fp-line);border-radius:14px;overflow:hidden}#foundation-flow-demo .brand{display:flex;align-items:center;gap:9px;padding:18px 26px;border-bottom:1px solid var(--fp-line);font-weight:500}#foundation-flow-demo .brand-mark{display:grid;place-items:center;width:26px;height:26px;border-radius:6px;background:var(--fp-text);color:var(--fp-bg)}#foundation-flow-demo .private-label{margin-left:auto;font-size:12px;color:var(--fp-muted);font-weight:400}#foundation-flow-demo .overview{padding:24px 26px 20px}#foundation-flow-demo h1{font-size:25px;font-weight:500;line-height:1.3;margin:5px 0 8px}#foundation-flow-demo p{margin:6px 0}#foundation-flow-demo .eyebrow{color:var(--fp-accent);font-size:12px;font-weight:500}#foundation-flow-demo #flow-description{color:var(--fp-muted)}#foundation-flow-demo .metadata{display:flex;flex-direction:column;gap:4px;margin:15px 0;color:var(--fp-muted);font-size:12px;overflow-wrap:anywhere}#foundation-flow-demo .progress-label{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:12px;margin-bottom:8px}#foundation-flow-demo #flow-status{color:var(--fp-accent)}#foundation-flow-demo .track{height:5px;background:var(--fp-soft);border-radius:4px;overflow:hidden}#foundation-flow-demo #flow-bar{height:100%;background:var(--fp-accent);width:0;transition:width .2s}#foundation-flow-demo .steps{list-style:none;padding:0 26px;margin:0}#foundation-flow-demo .step{border-top:1px solid var(--fp-line);padding:15px 0}#foundation-flow-demo .step.current{background:var(--fp-active);border:1px solid var(--fp-line);border-radius:10px;padding:16px;margin:4px -16px 8px}#foundation-flow-demo .step-top{display:flex;align-items:center;gap:12px}#foundation-flow-demo .step-number{flex:none;border:1px solid var(--fp-line);border-radius:50%;width:25px;height:25px;display:grid;place-items:center;font-size:12px;color:var(--fp-muted)}#foundation-flow-demo .done .step-number{color:var(--fp-good);border-color:transparent;background:var(--fp-soft)}#foundation-flow-demo .current .step-number{background:var(--fp-accent);color:var(--fp-bg);border-color:transparent}#foundation-flow-demo .step-title{font-weight:500;flex:1;min-width:0;overflow-wrap:anywhere}#foundation-flow-demo .step-state{font-size:12px;color:var(--fp-muted);max-width:48%;text-align:right;overflow-wrap:anywhere}#foundation-flow-demo .done .step-state{color:var(--fp-good)}#foundation-flow-demo .step-body{margin:12px 0 0 37px;color:var(--fp-muted)}#foundation-flow-demo .field{display:block;margin:13px 0 5px;color:var(--fp-text)}#foundation-flow-demo input[type=text]{width:100%;min-width:0;font-size:16px;background:var(--fp-bg);border:1px solid var(--fp-line);border-radius:7px;padding:10px}#foundation-flow-demo .check{display:flex;align-items:flex-start;gap:9px;margin:14px 0;color:var(--fp-text)}#foundation-flow-demo .check input{margin-top:6px;accent-color:var(--fp-accent)}#foundation-flow-demo .hint{font-size:12px;color:var(--fp-muted);overflow-wrap:anywhere}#foundation-flow-demo .actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:17px}#foundation-flow-demo .primary{background:var(--fp-accent);border-color:var(--fp-accent);color:var(--fp-bg);font-weight:500}#foundation-flow-demo .danger{background:var(--fp-error);border-color:var(--fp-error);color:var(--fp-bg)}#foundation-flow-demo .error{color:var(--fp-error);font-size:13px;margin-top:6px}#foundation-flow-demo .facts{border-left:2px solid var(--fp-line);padding-left:12px;margin:12px 0;overflow-wrap:anywhere}#foundation-flow-demo .result{margin:14px 26px 24px;padding:18px;background:var(--fp-soft);border-radius:10px}#foundation-flow-demo .result h2{font-size:18px;font-weight:500;margin:0 0 8px}#foundation-flow-demo footer{padding:17px 26px;border-top:1px solid var(--fp-line);color:var(--fp-muted);font-size:12px}#foundation-flow-demo summary{cursor:pointer}#foundation-flow-demo .completed-detail{margin-left:37px;font-size:12px;color:var(--fp-muted)}\n@media(max-width:480px){#foundation-flow-demo .overview,#foundation-flow-demo .brand,#foundation-flow-demo footer{padding-left:17px;padding-right:17px}#foundation-flow-demo .steps{padding:0 17px}#foundation-flow-demo .step.current{margin-left:-8px;margin-right:-8px;padding:12px 8px}#foundation-flow-demo .step-body{margin-left:0}#foundation-flow-demo .step-top{gap:8px}#foundation-flow-demo .step-state{max-width:82px;text-align:right}#foundation-flow-demo .demo-tools label{margin-left:0}#foundation-flow-demo .result{margin:14px 17px}#foundation-flow-demo h1{font-size:23px}}\n@media(prefers-reduced-motion:reduce){#foundation-flow-demo #flow-bar{transition:none}}";
@@ -1,16 +1,18 @@
1
1
  import {singlePageStyle} from './single-page-style.mjs';
2
2
 
3
3
  // The approved product DOM/CSS; the client consumes facts, not demo timers.
4
- export function singlePageDocument(csrf, {client = singlePageClient, projectPreparation = false} = {}) {
5
- return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Foundation ${projectPreparation ? "项目准备" : "安装与维护"}</title><style>body{margin:0}${singlePageStyle}</style><body><div id="foundation-flow-demo" aria-label="Foundation ${projectPreparation ? "项目准备" : "安装与维护"}"><main class="product"><header class="brand"><span class="brand-mark">F</span><span>Foundation</span><span class="private-label">${projectPreparation ? "项目准备" : "安装与维护"}</span></header><section class="overview"><div class="eyebrow" id="flow-eyebrow">正在核实</div><h1 id="flow-title">Foundation</h1><p id="flow-description"></p><div class="metadata"><span id="flow-version"></span><span id="flow-destination"></span></div><div class="progress-label"><span id="flow-count"></span><span id="flow-status" role="status" aria-live="polite"></span></div><div class="track" role="progressbar" aria-label="已完成步骤" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div id="flow-bar"></div></div></section><ol id="flow-steps" class="steps"></ol><section id="flow-result" class="result" hidden aria-live="polite"></section><footer><p id="connection" role="status"></p><details><summary>技术详情</summary><pre id="raw" style="white-space:pre-wrap;overflow-wrap:anywhere"></pre></details></footer></main></div><script>(${client.toString()})(${JSON.stringify(csrf).replaceAll("<", "\\u003c")});</script></body></html>`;
4
+ export function singlePageDocument(csrf, {client = singlePageClient, projectPreparation = false, content = null, script = null} = {}) {
5
+ return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Foundation ${projectPreparation ? "项目准备" : "安装与维护"}</title><style>body{margin:0}${singlePageStyle}</style><body><div id="foundation-flow-demo" aria-label="Foundation ${projectPreparation ? "项目准备" : "安装与维护"}"><main class="product"><header class="brand"><span class="brand-mark">F</span><span>Foundation</span><span class="private-label">${projectPreparation ? "项目准备" : "安装与维护"}</span></header>${content ?? `<section class="overview"><div class="eyebrow" id="flow-eyebrow">正在核实</div><h1 id="flow-title">Foundation</h1><p id="flow-description"></p><div class="metadata"><span id="flow-version"></span><span id="flow-destination"></span></div><div class="progress-label"><span id="flow-count"></span><span id="flow-status" role="status" aria-live="polite"></span></div><div class="track" role="progressbar" aria-label="已完成步骤" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div id="flow-bar"></div></div></section><ol id="flow-steps" class="steps"></ol><section id="flow-result" class="result" hidden aria-live="polite"></section><footer><p id="connection" role="status"></p><details><summary>技术详情</summary><pre id="raw" style="white-space:pre-wrap;overflow-wrap:anywhere"></pre></details></footer>`}</main></div><script>${script ?? `(${client.toString()})(${JSON.stringify(csrf).replaceAll("<", "\\u003c")});`}</script></body></html>`;
6
6
  }
7
7
 
8
8
  function singlePageClient(csrf) {
9
9
  const q=s=>document.querySelector(s),esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
10
- let last=null,busy=false,revision=null,stopped=false,formIdentity=null;
10
+ let last=null,busy=false,revision=null,stopped=false,formIdentity=null,submitted=null,posting=false;
11
11
  const verbs={install:'安装',update:'更新',uninstall:'卸载'};
12
12
  function cleanupText(session){const c=session?.hostCleanup;if(!c)return '';return '<div class="facts"><p>更新成功并通过健康检查后,清理本次下载的归档和展开候选。</p><p>清理位置:'+esc(c.root)+'</p><p>范围:'+esc(c.files?.length??'待核实')+' 个文件;'+(Number.isFinite(c.bytes)?(c.bytes/1048576).toFixed(2)+' MiB':'大小待核实')+'</p><p>保留获取回执、验证材料、回退版本、项目和未知或已修改文件;仍在使用的材料不会强删。</p></div>';}
13
13
  function show(p){
14
+ if(last&&p.record.operationId!==last.record.operationId)throw Error('流程身份变化');
15
+ if(submitted&&!posting&&p.record.operationId===submitted.operationId&&(p.record.terminal||p.reconciled===true&&!p.submissionPending&&p.view?.session?.sessionId===submitted.sessionId||p.view?.session?.sessionId&&p.view.session.sessionId!==submitted.sessionId||p.view?.session?.sessionId===submitted.sessionId&&p.view.session.state!=='pending'||submitted.choice&&(p.record.phase!=='choosing-intent'||p.reconciled===true&&!p.submissionPending))){busy=false;submitted=null;revision=null;}
14
16
  last=p;const r=p.record,v=p.view,c=p.progress.content,kind=r.kind||'install',verb=verbs[kind],choice=r.phase==='choosing-intent'&&!r.terminal;
15
17
  const titles={select:'选择安装位置',acquire:kind==='update'?'下载并检查新版':'下载并检查文件',program:verb+' Foundation',material:'准备安装 Skill',register:kind==='update'?'更新 Codex 中的 Skill':'将 Skill 安装到 Codex',remove:'移除 Codex 中的 Skill',health:'检查'+verb+'结果','optional-skill':'Foundation Skill'};
16
18
  const stateLabels={failed:'本步失败',cancelled:'已取消',expired:'已过期','verification-required':'结果待核实',running:'正在执行',executing:'正在执行',consumed:'正在执行',undecided:'可选,待选择'};
@@ -31,7 +33,7 @@ function singlePageClient(csrf) {
31
33
  q('#flow-eyebrow').textContent=r.terminal?'本次结果':executing?'正在执行':busy?'正在提交请求':v||choice?'等待你的操作':'正在进行';
32
34
  q('#flow-description').textContent=c.description;
33
35
  q('#flow-version').textContent='版本:'+(p.progress.version||'待核实');q('#flow-destination').textContent='程序位置:'+(r.installationRoot||r.destinationIntent||'等待选择');
34
- q('#flow-count').textContent='已处理 '+count+' / '+steps.length+' 步';q('#flow-status').textContent=executing?'正在执行当前步骤':busy?'正在提交请求':r.terminal?(r.state==='completed'?'本次流程已完成':'查看未完成项'):('当前:'+(titles[active]||p.progress.phase));
36
+ q('#flow-count').textContent='已处理 '+count+' / '+steps.length+' 步';q('#flow-status').textContent=r.terminal?(r.state==='completed'?'本次流程已完成':'查看未完成项'):executing?'正在执行当前步骤':busy?'正在提交请求':('当前:'+(titles[active]||p.progress.phase));
35
37
  const percent=steps.length?count/steps.length*100:0;q('#flow-bar').style.width=percent+'%';q('.track').setAttribute('aria-valuenow',String(Math.round(percent)));
36
38
  const key=JSON.stringify([active,v?.session?.sessionId,v?.session?.state,v?.selectionId,r.terminal,steps.map(s=>s.state),v?.error]);
37
39
  if(key!==revision&&!busy){
@@ -62,9 +64,10 @@ function singlePageClient(csrf) {
62
64
  if(r.terminal)q('#flow-result').innerHTML='<h2>'+esc(c.title)+'</h2><p>'+esc(c.program)+'</p><p>Codex 对话功能:'+esc(c.skill)+'</p><p>项目:'+esc(c.project)+'</p><p class="hint">下一步:'+esc(c.next||p.progress.next)+'</p>';
63
65
  const safe=JSON.parse(JSON.stringify(p));if(safe.view){delete safe.view.managerNonce;delete safe.view.nonce;}q('#raw').textContent=JSON.stringify(safe,null,2);
64
66
  }
65
- document.addEventListener('submit',async e=>{if(e.target.id!=='current-form')return;e.preventDefault();if(busy||!last)return;busy=true;const form=e.target;form.querySelectorAll('button').forEach(b=>b.disabled=true);q('#flow-status').textContent='正在提交请求';const action=e.submitter.value,v=last.view;const body={csrf,operationId:last.record.operationId,action,...(['choose','select'].includes(action)?{destination:form.elements.destination.value,skillChoice:form.elements.includeSkill.checked?'selected':'skipped'}:{managerNonce:v?.managerNonce,sessionId:v?.session?.sessionId,planHash:v?.session?.planHash})};
67
+ document.addEventListener('submit',async e=>{if(e.target.id!=='current-form')return;e.preventDefault();if(busy||!last)return;busy=true;posting=true;submitted={operationId:last.record.operationId,sessionId:last.view?.session?.sessionId,choice:last.record.phase==='choosing-intent'};const form=e.target;form.querySelectorAll('button').forEach(b=>b.disabled=true);q('#flow-status').textContent='正在提交请求';const action=e.submitter.value,v=last.view;const body={csrf,operationId:last.record.operationId,action,...(['choose','select'].includes(action)?{destination:form.elements.destination.value,skillChoice:form.elements.includeSkill.checked?'selected':'skipped'}:{managerNonce:v?.managerNonce,sessionId:v?.session?.sessionId,planHash:v?.session?.planHash})};
66
68
 
67
- try{const response=await fetch(location.pathname+'/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});const result=await response.json();if(!response.ok){q('#step-error').textContent=result.message||result.code||'本步未完成,请核实';busy=false;form.querySelectorAll('button').forEach(b=>b.disabled=false);return;}busy=false;revision=null;await refresh();}catch{q('#connection').textContent='请求结果待核实,不自动重试。已收到的结果保留;刷新查询同次流程。';}});
68
- async function refresh(){try{const response=await fetch(location.pathname+'/state');if(!response.ok)throw Error();show(await response.json());q('#connection').textContent='';}catch{q('#connection').textContent=last?.record?.terminal?'结果服务已结束,上方保留最后核实结果。可在对话中按本次操作记录再次查询。':'连接已中断,保留最后核实结果;不自动重试操作。';const note=q('.current [data-download-connection]');if(note){note.textContent='连接中断,进度待核实;保留上次收到的数值。';q('.current .step-state').textContent=(last.progress.downloadStatus||last.progress.phase)+' · 待核实';}}}
69
+ try{const response=await fetch(location.pathname+'/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});const result=await response.json();if(!response.ok){q('#step-error').textContent=result.message||result.code||'本步未完成,请核实';busy=false;submitted=null;form.querySelectorAll('button').forEach(b=>b.disabled=false);return;}busy=false;submitted=null;revision=null;await refresh();}catch{q('#connection').textContent='请求结果待核实,不自动重试。已收到的结果保留;使用“重新核对状态”查询同次流程。';}finally{posting=false;}});
70
+ async function refresh(){try{const response=await fetch(location.pathname+'/state');if(!response.ok)throw Error();show(await response.json());q('#connection').textContent=busy?'提交结果仍待核实;不会重复提交原确认。':'';}catch{q('#connection').textContent=last?.record?.terminal?'结果服务已结束,上方保留最后核实结果。可在对话中按本次操作记录再次查询。':'连接已中断,保留最后核实结果;不自动重试操作。';const note=q('.current [data-download-connection]');if(note){note.textContent='连接中断,进度待核实;保留上次收到的数值。';q('.current .step-state').textContent=(last.progress.downloadStatus||last.progress.phase)+' · 待核实';}}}
71
+ const read=document.createElement('button');read.id='read-status';read.type='button';read.textContent='重新核对状态';read.addEventListener('click',refresh);q('footer').prepend(read);
69
72
  async function poll(){if(stopped)return;await refresh();if(!stopped)setTimeout(poll,750);}poll();window.addEventListener('pagehide',()=>{stopped=true},{once:true});
70
73
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import {installedClient,observePlan} from './manager-step.mjs';
4
- import {plainPath} from './acquire.mjs';
4
+ import {plainPath} from './local-path.mjs';
5
5
 
6
6
  // Coordinates existing exact-plan APIs. It never sends a confirmation request.
7
7
  // The installed launcher resolves current on every call; downloaded code is not
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@josephyulei/summon-foundation",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@josephyulei/summon-foundation",
9
- "version": "0.1.11",
9
+ "version": "0.1.13",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@sigstore/bundle": "4.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyulei/summon-foundation",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Prepare a verified Foundation release for an explicitly confirmed Codex installation",
5
5
  "type": "module",
6
6
  "bin": {"summon": "bin/summon.mjs"},