@josephyulei/summon-foundation 0.1.6 → 0.1.7

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
@@ -1,6 +1,8 @@
1
1
  # summon foundation
2
2
 
3
- 0.1.6 单页试用入口与 Foundation 0.2.12 起的运行引擎共同支持同一页面内的目录选择、程序和 Skill 独立确认;旧发行版本不具备本次单页修订。真实安装、新任务发现和主动告知仍需独立验收。
3
+ 本轮修订包含真实下载状态、专用入口环境和项目范围;以 registry 中本版本实际存在为执行前提。基础环境不能安全启动页面时,由 Codex 对话先披露并取得独立批准,再准备专用环境;页面启动后全程同页。具体方法见仓库 `docs/install-with-codex.md`,不能先运行 npx 来准备缺失的 Node。不修改共享 PATH;获取器专用环境及其回执按缓存保留,程序另用随包运行时。发行发现仅需 curl,获取还需 tar;已验证载荷的确认、恢复和卸载不依赖下载工具。
4
+
5
+ 0.1.7 单页试用入口与 Foundation 0.2.12 起的运行引擎共同支持同一页面内的目录选择、程序和 Skill 独立确认;旧发行版本不具备本次单页修订。真实安装、新任务发现和主动告知仍需独立验收。
4
6
 
5
7
  首次 URL 持续显示本次流程;确认仅转交本次受控子进程的当前计划,不新开manager页。--acquire仍只读获取;--resume仅在用户明确要求后恢复未完成Skill,不重放程序安装。
6
8
 
@@ -12,7 +14,7 @@ Foundation 的薄获取入口,不包含完整运行包,没有 npm 安装生
12
14
  确认 registry 中存在本版本及对应不可变 Release 后,在具备安装权限的新 Codex 对话粘贴:
13
15
 
14
16
  ```text
15
- npx --yes --package @josephyulei/summon-foundation@0.1.6 summon foundation
17
+ npx --yes --package @josephyulei/summon-foundation@0.1.7 summon foundation
16
18
  ```
17
19
 
18
20
  旧版退出推荐入口;支持边界见[现行支持策略](https://github.com/y15801172513-lang/foundation/blob/main/docs/install-platform-024.md#现行支持策略)。旧不可变版本不修改,不自动迁移或清除旧安装。
@@ -0,0 +1,51 @@
1
+ #!/bin/sh
2
+ # C6: bootstrap the npm entry, not Foundation itself. No Node/npm is needed
3
+ # to inspect this plan. Keep pins aligned with official-node-runtime.mjs.
4
+ set -eu
5
+ umask 077
6
+ fail(){ printf '错误:%s\n' "$1" >&2; exit 1; }
7
+ version=24.14.1
8
+ archive_hash=25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe
9
+ node_hash=35d4bc736bbe4161f0c074aa140ddb74586e46969bc2ad63d32c838bec4463ff
10
+ license_hash=4573185d56580da2b890ba34a85a409257640f1c5632eade4300137266194d18
11
+ url=https://nodejs.org/dist/v24.14.1/node-v24.14.1-darwin-arm64.tar.gz
12
+ prefix=node-v24.14.1-darwin-arm64
13
+ mode=${1:-}; destination=${2:-}; approval=${3:-}
14
+ [ "$#" -ge 2 ] && [ "$#" -le 3 ] || fail '用法:sh prepare-environment.sh --inspect /绝对独立目录;本人批准后以 --prepare 同一路径 精确计划摘要执行'
15
+ case "$mode" in --inspect|--prepare) ;; *) fail '仅支持查看计划或按批准准备环境' ;; esac
16
+ case "$destination" in /*) ;; *) fail '环境位置必须是绝对路径' ;; esac
17
+ case "$destination" in *'
18
+ '*|*'/../'*|*'/./'*|*/|'/') fail '环境位置必须是规范化独立目录' ;; esac
19
+ [ "$(/usr/bin/uname -s)" = Darwin ] && [ "$(/usr/bin/uname -m)" = arm64 ] || fail '此设备暂不支持;不会通过准备环境绕过平台限制'
20
+ for tool in /usr/bin/curl /usr/bin/tar /usr/bin/shasum /usr/bin/stat /bin/mkdir /bin/chmod; do [ -x "$tool" ] || fail "缺少可信系统工具:$tool"; done
21
+ parent=${destination%/*}; [ -n "$parent" ] || fail '不能写系统根目录'
22
+ [ -d "$parent" ] && [ -w "$parent" ] && [ -x "$parent" ] || fail '父目录不存在或无权限;不会代建系统目录'
23
+ [ "$(CDPATH= cd -- "$parent" && pwd -P)" = "$parent" ] || fail '父目录含链接或非规范路径'
24
+ cursor=$parent
25
+ while [ "$cursor" != / ]; do [ ! -L "$cursor" ] || fail '拒绝符号链接祖先'; cursor=${cursor%/*}; [ -n "$cursor" ] || cursor=/; done
26
+ [ ! -e "$destination" ] && [ ! -L "$destination" ] || fail '目标已经存在;保留已有或失败材料,请核验后选择新的独立目录'
27
+ parent_identity=$(/usr/bin/stat -f '%d:%i:%u:%Lp' "$parent")
28
+ plan=$(printf 'foundation-entry-environment-v1\n%s\n%s\n%s\n%s\n%s\nno-shared-settings\n' "$version" "$destination" "$parent_identity" "$url" "$archive_hash" | /usr/bin/shasum -a 256)
29
+ plan=${plan%% *}
30
+ printf '基础环境计划:Node.js %s(含 npm),仅用于启动获取器\n来源:%s\n位置:%s\n影响:不修改现有 Node、Shell、PATH 或系统设置;不安装 Foundation、不注册 Skill\n保留:失败材料和环境回执;不自动清理\n计划摘要:%s\n' "$version" "$url" "$destination" "$plan"
31
+ [ "$mode" = --prepare ] || exit 0
32
+ [ -n "$approval" ] && [ "$approval" = "$plan" ] || fail '缺少匹配的本人批准计划摘要;没有准备环境'
33
+ # The invoking Codex task must obtain human approval for the displayed plan.
34
+ # This argument binds that request; it is not evidence of a UI confirmation.
35
+ [ "$(CDPATH= cd -- "$parent" && pwd -P)" = "$parent" ] && [ "$(/usr/bin/stat -f '%d:%i:%u:%Lp' "$parent")" = "$parent_identity" ] || fail '父目录在批准后变化;没有准备环境'
36
+ /bin/mkdir -m 700 -- "$destination" || fail '创建独占环境目录失败'
37
+ printf 'state=preparing\nplan=%s\nsource=%s\narchive_sha256=%s\nshared_settings_changed=false\n' "$plan" "$url" "$archive_hash" > "$destination/environment-result.txt"
38
+ trap 'printf "state=failed-or-interrupted\n" >> "$destination/environment-result.txt"' EXIT
39
+ /usr/bin/curl -q --fail --silent --show-error --location --max-redirs 3 --proto '=https' --proto-redir '=https' --connect-timeout 15 --max-time 600 --max-filesize 134217728 -o "$destination/node.tar.gz" "$url" || fail '官方环境下载失败;保留材料,不执行'
40
+ observed=$(/usr/bin/shasum -a 256 "$destination/node.tar.gz"); [ "${observed%% *}" = "$archive_hash" ] || fail '官方环境归档摘要不匹配'
41
+ /usr/bin/tar -xzpf "$destination/node.tar.gz" --no-same-owner --no-acls --no-fflags --no-mac-metadata --no-xattrs -C "$destination" || fail '已验证归档解包失败'
42
+ node="$destination/$prefix/bin/node"
43
+ [ -f "$node" ] && [ ! -L "$node" ] && [ -x "$node" ] || fail '缺少绑定运行时'
44
+ observed=$(/usr/bin/shasum -a 256 "$node"); [ "${observed%% *}" = "$node_hash" ] || fail '运行时字节不符'
45
+ observed=$(/usr/bin/shasum -a 256 "$destination/$prefix/LICENSE"); [ "${observed%% *}" = "$license_hash" ] || fail '许可字节不符'
46
+ unset NODE_OPTIONS NODE_PATH NODE_V8_COVERAGE NODE_REDIRECT_WARNINGS NODE_COMPILE_CACHE NODE_COMPILE_CACHE_PORTABLE NODE_PRESERVE_SYMLINKS
47
+ [ "$("$node" --version)" = "v$version" ] || fail '环境准备后版本复检失败'
48
+ [ -f "$destination/$prefix/lib/node_modules/npm/bin/npm-cli.js" ] || fail '官方环境缺少 npm 入口'
49
+ printf 'state=prepared-and-rechecked\nnode_version=%s\nnode_sha256=%s\nnode=%s\nnpm_cli=%s\n' "$version" "$node_hash" "$node" "$destination/$prefix/lib/node_modules/npm/bin/npm-cli.js" >> "$destination/environment-result.txt"
50
+ trap - EXIT
51
+ printf '基础环境已准备并复检。由当前任务以此 Node 和 npm 入口启动已固定版本的 Foundation 单页;不要更改全局 PATH。\n回执:%s/environment-result.txt\n' "$destination"
package/bin/summon.mjs CHANGED
@@ -13,6 +13,7 @@ import {resumeSkillHandoff} from '../lib/resume-handoff.mjs';
13
13
  import {parseSummonArgs} from '../lib/cli-options.mjs';
14
14
  import {createOperation,saveOperation,readOperation,runtimeObservation} from '../lib/operation-result.mjs';
15
15
  import {createJourneyControl} from '../lib/journey-control.mjs';
16
+ import {inspectEntryEnvironment} from '../lib/environment-check.mjs';
16
17
 
17
18
  const help=`summon foundation [--version x.y.z] [--destination /absolute/folder]
18
19
  summon foundation --inspect [--version x.y.z]
@@ -50,21 +51,25 @@ function stageDirectory(){
50
51
  return fs.mkdtempSync(path.join(cache,'acquisition.'));
51
52
  }
52
53
  let operation=null,stage=null,progressPage=null;
53
- const journeyControl=createJourneyControl(()=>operation,()=>progressPage?.publish());
54
+ const journeyControl=createJourneyControl(()=>operation,patch=>patch?updateOperation(patch):progressPage?.publish());
54
55
  function updateOperation(patch){if(!operation)return;Object.assign(operation,patch,{updatedAt:new Date().toISOString()});const file=saveOperation(operation,stage);if(file)operation.resultFile=file;progressPage?.publish();console.log(JSON.stringify({status:'FOUNDATION_INSTALL_OPERATION',...operation,resultFile:file,recordSaved:!!file}));}
55
56
  try{
56
57
  const args=parseSummonArgs(process.argv.slice(2));
57
58
  if(args.help){console.log(help);process.exit(0);}
58
59
  if(args.status){console.log(JSON.stringify(readOperation(args.status),null,2));process.exit(0);}
59
- const [major,minor]=process.versions.node.split('.').map(Number);
60
- if(major<22||(major===22&&minor<9))fail('获取入口需要 Node.js 22.9 或以上;尚未安装,不自动安装依赖');
61
- if(process.platform!=='darwin'||process.arch!=='arm64')fail('当前仅支持 macOS arm64;尚未安装');
60
+ const initialEnvironment=inspectEntryEnvironment({purpose:args.uninstall?'uninstall':args.resume?'resume':args.inspect?'discover':'acquire'});
61
+ if(!initialEnvironment.supported||!initialEnvironment.compatible){
62
+ operation=createOperation();updateOperation({phase:'checking-environment',environment:initialEnvironment});
63
+ fail(!initialEnvironment.supported?'当前仅支持 macOS arm64;尚未安装':'获取入口需要 Node.js 22.9 或以上;当前入口不能安全承诺同页准备,请按安装说明在对话中先确认独立基础环境,不自动安装依赖');
64
+ }
62
65
  const maintenance=args.update||args.uninstall;
63
66
  if(args.prepare||args.acquire||maintenance||args.resume)sourceGuard();
64
- if(args.prepare||args.acquire||maintenance){operation=createOperation();stage=stageDirectory();updateOperation({});if(!saveOperation(operation,stage))fail('无法在已披露的获取目录保存操作记录;尚未联网或安装');}
67
+ if(args.prepare||args.acquire||maintenance){operation=createOperation();Object.assign(operation,{phase:'checking-environment',environment:initialEnvironment});stage=stageDirectory();updateOperation({});if(!saveOperation(operation,stage))fail('无法在已披露的获取目录保存操作记录;尚未联网或安装');}
65
68
  if(args.prepare)console.log('正在准备 Foundation 安装:将查询并固定本次正式版本,说明缓存位置后下载核验。未选目录会在安装页选择;之后仍需本人确认精确计划。请保持当前 Codex 任务等待并打开返回的内置浏览器网址;不会自动改用系统浏览器。');
66
69
  if(args.resume){
67
70
  operation=readOperation(args.resume);stage=path.dirname(plainPath(args.resume));
71
+ updateOperation({environment:initialEnvironment});
72
+ if(initialEnvironment.state!=='ready')fail('恢复所需环境无法核实;保留已有安装和操作记录');
68
73
  progressPage=await startProgressPage(()=>operation,{control:journeyControl});
69
74
  console.log(JSON.stringify({status:'FOUNDATION_SINGLE_PAGE',url:progressPage.url,operationId:operation.operationId}));
70
75
  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];
@@ -77,12 +82,14 @@ try{
77
82
  if(!current?.identity?.installId)fail('当前安装身份无法核实;尚未获取或执行维护');
78
83
  const capability=spawnSync(installed.launcher,['--help'],{encoding:'utf8',env,timeout:15000});
79
84
  if(capability.status!==0||!capability.stdout.includes('--journey-channel'))fail('当前安装不受新版单页流程支持。请另行确认卸载旧版并重新安装新版;本次不删除、不迁移、不回退旧页面');
80
- updateOperation({kind:args.update?'update':'uninstall',installationRoot:args.root,currentVersion:current.version,installId:current.identity.installId});
85
+ const environment=inspectEntryEnvironment({purpose:args.update?'acquire':'uninstall'});
86
+ updateOperation({kind:args.update?'update':'uninstall',installationRoot:args.root,currentVersion:current.version,installId:current.identity.installId,environment,usageScope:current.usageScope||{kind:'user',project:null}});
81
87
  progressPage=await startProgressPage(()=>operation,{control:journeyControl});updateOperation({progressUrl:progressPage.url});
82
88
  console.log(JSON.stringify({status:'FOUNDATION_SINGLE_PAGE',url:progressPage.url,operationId:operation.operationId}));
89
+ if(environment.state!=='ready')fail('环境复检未通过:'+environment.state+';缺少 '+environment.missing.join('、')+';保留当前安装,不修改系统工具');
83
90
  let candidate;
84
91
  if(args.update){
85
- 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})});
92
+ 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}:{})})});
86
93
  const directory=plainPath(path.dirname(receipt.launcher)),manifest=JSON.parse(fs.readFileSync(plainPath(path.join(directory,'manifest.json'))));
87
94
  if(manifest.productVersion!==context.version)fail('获取版本与候选不一致');
88
95
  candidate={path:directory,manifestHash:manifest.candidateHash,version:manifest.productVersion,bytes:manifest.totalBytes,runtimeHash:manifest.files.find(f=>f.path===manifest.runtime.path)?.sha256};
@@ -92,6 +99,7 @@ try{
92
99
  process.exitCode=operation.state==='completed'?0:1;
93
100
  await progressPage.close();progressPage=null;
94
101
  }else if(!args.prepare&&!args.acquire){
102
+ if(initialEnvironment.state!=='ready'){operation=createOperation();updateOperation({phase:'checking-environment',environment:initialEnvironment});fail('发行查询所需环境无法核实;尚未联网');}
95
103
  const context=inspectRelease(args.version);
96
104
  const guide=`https://github.com/${context.repository.full_name}/blob/${context.documentationCommit}/docs/install-with-codex.md`;
97
105
  console.log(JSON.stringify({schemaVersion:'1.0.0',status:'RELEASE_DISCOVERED_NOT_ACQUIRED',product:'Foundation',version:context.version,sourceCommit:context.sourceCommit,documentationCommit:context.documentationCommit,repository:context.repository.full_name,guide,installationPerformed:false,skillRegistered:false,
@@ -103,13 +111,15 @@ try{
103
111
  progressPage=await startProgressPage(()=>operation,args.acquire?{}:{control:journeyControl});
104
112
  updateOperation({progressUrl:progressPage.url});
105
113
  console.log(JSON.stringify({status:'FOUNDATION_ACQUISITION_PROGRESS_PAGE',url:progressPage.url,operationId:operation.operationId,next:'在 Codex 内置浏览器打开本次页面,所有所需确认在原页展开;保持任务等待,不自动打开系统浏览器。'}));
114
+ const environment=inspectEntryEnvironment({purpose:'acquire'});updateOperation({phase:'checking-environment',environment});
115
+ if(environment.state!=='ready')fail('获取环境检查未通过:'+environment.state+';缺少 '+environment.missing.join('、')+';不会以安装 Node 替代缺失的系统工具');
106
116
  if(!args.acquire){
107
117
  const metadata=inspectRelease(args.version);args.version=metadata.version;
108
- updateOperation({phase:'choosing-intent',version:metadata.version,sourceCommit:metadata.sourceCommit,acquisitionRoot:stage,destinationIntent:args.destination||'',journeyContext:{id:operation.operationId,skillChoice:'undecided'}});
118
+ updateOperation({phase:'choosing-intent',environment,version:metadata.version,sourceCommit:metadata.sourceCommit,acquisitionRoot:stage,destinationIntent:args.destination||'',journeyContext:{id:operation.operationId,skillChoice:'undecided'}});
109
119
  const choice=await journeyControl.waitChoice();
110
- updateOperation({phase:'discovering',intentSelected:true,destinationIntent:choice.destination,journeyContext:{id:operation.operationId,skillChoice:choice.skillChoice}});
120
+ updateOperation({phase:'discovering',intentSelected:true,destinationIntent:choice.destination,scopeIntent:{kind:choice.scopeKind,projectRoot:choice.projectRoot},destinationCheck:choice.destinationCheck,journeyContext:{id:operation.operationId,skillChoice:choice.skillChoice}});
111
121
  }
112
- 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})});console.log(JSON.stringify({status:'GITHUB_ACQUISITION_VERIFIED',...receipt,destinationIntent:args.destination}));
122
+ 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}:{})})});console.log(JSON.stringify({status:'GITHUB_ACQUISITION_VERIFIED',...receipt,destinationIntent:args.destination}));
113
123
  updateOperation({phase:'acquired',version:context.version,sourceCommit:context.sourceCommit});
114
124
  if(args.acquire){updateOperation({state:'update-input-ready',terminal:true});console.log(JSON.stringify({status:'UPDATE_INPUT_READY_NOT_APPLIED',candidate:path.dirname(receipt.launcher),receipt:path.join(stage,'acquisition.json'),next:'使用当前健康安装的稳定入口生成独立更新计划;本人确认后才更新。获取回执不是删除授权。'}));await progressPage.close();progressPage=null;process.exit(0);}
115
125
  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];
package/lib/acquire.mjs CHANGED
@@ -91,9 +91,10 @@ async function trustRoot(stage) {
91
91
  await updater.refresh();const info=await updater.getTargetInfo('trusted_root.json');if(!info)fail('GitHub 信任目标缺失');
92
92
  return json(fs.readFileSync(await updater.downloadTarget(info)));
93
93
  }
94
- async function verifiedAsset(context,asset,stage,trust,onProgress) {
94
+ async function verifiedAsset(context,asset,stage,trust,onProgress=()=>{},onDownloaded=()=>{}) {
95
95
  if(!asset||!Number.isSafeInteger(asset.size)||asset.size<=0||asset.size>250_000_000||!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(asset.name)||!/^sha256:[a-f0-9]{64}$/.test(asset.digest)||asset.browser_download_url!==`https://github.com/${policy.repository}/releases/download/${context.release.tag_name}/${asset.name}`)fail('资产元数据无效');
96
- const bytes=asset.size>10_000_000?await downloadLarge(asset.browser_download_url,asset.size,onProgress):download(asset.browser_download_url,asset.size+1);
96
+ const bytes=asset.size>10_000_000?await downloadLarge(asset.browser_download_url,asset.size,p=>onProgress({...p,asset:asset.name})):download(asset.browser_download_url,asset.size+1);
97
+ onDownloaded();
97
98
  if(bytes.length!==asset.size||`sha256:${hash(bytes)}`!==asset.digest)fail('下载长度或摘要不匹配');
98
99
  const proofs=api(`attestations/${asset.digest}?predicate_type=release&per_page=100`).attestations;
99
100
  if(!Array.isArray(proofs)||proofs.length>=100)fail('证明列表不完整或分页待核实');
@@ -152,7 +153,7 @@ export async function acquireRelease(context,stage,{onPhase=()=>{},onProgress=()
152
153
  if(release.repositoryId!==policy.repositoryId||release.sourceCommit!==context.sourceCommit||!/^[a-f0-9]{64}$/.test(release.candidateHash)||release.url!==`https://github.com/${policy.repository}/releases/download/${context.release.tag_name}/${assetName}`||!/^\d+\.\d+\.\d+$/.test(release.runtime?.version)||release.runtime.url!==`https://nodejs.org/dist/v${release.runtime.version}/node-v${release.runtime.version}-darwin-arm64.tar.gz`)fail('清单不能更换仓库、提交或官方运行时来源');
153
154
  const assets=context.release.assets.filter(x=>x.name===assetName&&x.size===release.bytes&&x.digest===`sha256:${release.sha256}`);if(assets.length!==1)fail('归档身份不匹配');
154
155
  onPhase('fetching-and-verifying-runtime');
155
- const archive=await verifiedAsset(context,assets[0],stage,trust,onProgress);
156
+ const archive=await verifiedAsset(context,assets[0],stage,trust,onProgress,()=>onPhase('verifying-runtime'));
156
157
  onPhase('extracting-and-validating');
157
158
  const listing=spawnSync('/usr/bin/tar',['-tzf',archive.file],{encoding:'utf8',maxBuffer:10_000_000});
158
159
  const types=spawnSync('/usr/bin/tar',['-tvzf',archive.file],{encoding:'utf8',maxBuffer:10_000_000});
@@ -7,7 +7,7 @@ export function acquireInWorker({version,stage,operationId,onContext=()=>{},onPh
7
7
  const interrupt=()=>{failure=Object.assign(Error('获取任务被中断;保留本次材料'),{code:'ACQUISITION_INTERRUPTED'});void worker.terminate();};
8
8
  process.once('SIGINT',interrupt);process.once('SIGTERM',interrupt);
9
9
  worker.on('message',event=>{
10
- if(event.type==='context'){context=event.context;onContext(context);}
10
+ if(event.type==='context'){context=event.context;onContext(context);worker.postMessage({type:'context-recorded'});}
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;
@@ -5,7 +5,14 @@ import {inspectRelease, acquireRelease} from './acquire.mjs';
5
5
 
6
6
  try {
7
7
  const context = inspectRelease(workerData.version);
8
+ // The parent persists context atomically in the stage. Finish that write
9
+ // before checking that the stage contains only this operation's record.
10
+ const recorded = new Promise(resolve=>parentPort.once('message',message=>{
11
+ if(message.type!=='context-recorded')throw Error('获取上下文确认无效');
12
+ resolve();
13
+ }));
8
14
  parentPort.postMessage({type:'context',context});
15
+ await recorded;
9
16
  const receipt = await acquireRelease(context, workerData.stage, {
10
17
  operationId:workerData.operationId,
11
18
  onPhase:phase=>parentPort.postMessage({type:'phase',phase}),
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {spawnSync} from 'node:child_process';
4
+ import crypto from 'node:crypto';
5
+ import {plainPath} from './acquire.mjs';
6
+
7
+ export function classifyEntryEnvironment({platform,arch,nodeVersion,tools}){
8
+ const [major,minor]=String(nodeVersion||'').split('.').map(Number);
9
+ const supported=platform==='darwin'&&arch==='arm64';
10
+ const compatible=Number.isInteger(major)&&Number.isInteger(minor)&&(major>22||major===22&&minor>=9);
11
+ const missing=tools.filter(x=>!x.available).map(x=>x.path);
12
+ return{platform,arch,nodeVersion,supported,compatible,tools,state:!supported?'unsupported':!compatible?'environment-required':missing.length?'blocked':'ready',missing,sharedSettingsChanged:false};
13
+ }
14
+ export function inspectEntryEnvironment({purpose='acquire'}={}){
15
+ // Hash/proof verification uses Node crypto. System shasum belongs only to the
16
+ // no-Node preparation script, not an npm action or installed confirmation.
17
+ const requirements={discover:['/usr/bin/curl'],acquire:['/usr/bin/curl','/usr/bin/tar'],confirm:[],uninstall:[],resume:[]};
18
+ if(!Object.hasOwn(requirements,purpose))throw Error('未知环境检查动作');
19
+ const tools=requirements[purpose].map(file=>{let available=false;try{fs.accessSync(file,fs.constants.X_OK);available=fs.statSync(file).isFile();}catch{}return{path:file,available};});
20
+ const result={...classifyEntryEnvironment({platform:process.platform,arch:process.arch,nodeVersion:process.versions.node,tools}),purpose,entryRuntime:process.execPath,checkedAt:new Date().toISOString(),programRuntime:'随程序携带,不替换现有 Node'};
21
+ const receipt=path.resolve(path.dirname(process.execPath),'../..','environment-result.txt');
22
+ try{if(fs.existsSync(receipt)){
23
+ plainPath(receipt);const stat=fs.lstatSync(receipt);if(!stat.isFile()||stat.size>65536)throw Error('基础环境回执异常,保留并核实');
24
+ const fields=Object.fromEntries(fs.readFileSync(receipt,'utf8').trim().split('\n').map(line=>{const i=line.indexOf('=');return[line.slice(0,i),line.slice(i+1)];}));
25
+ const hash=crypto.createHash('sha256').update(fs.readFileSync(process.execPath)).digest('hex');
26
+ if(fields.state!=='prepared-and-rechecked'||fields.node!==process.execPath||fields.node_sha256!==hash||fields.node_version!==process.versions.node)throw Error('基础环境回执与当前运行文件不一致,不继续');
27
+ result.preparation={receipt,plan:fields.plan,state:fields.state,humanApproval:'由启动任务核验,本记录不是人类批准证明',retention:'获取器专用环境保留为缓存,不自动清理;不属于程序安装根'};
28
+ }}catch{result.state='blocked';result.preparation={receipt,state:'verification-required',reason:'基础环境回执与当前运行文件无法核实;保留材料,不继续'};}
29
+ return result;
30
+ }
31
+ export function inspectEntryDestination(input){
32
+ let destination=String(input||'').trim();
33
+ if((destination.startsWith('"')&&destination.endsWith('"'))||(destination.startsWith("'")&&destination.endsWith("'")))destination=destination.slice(1,-1);
34
+ plainPath(destination);
35
+ if(fs.existsSync(destination)&&(!fs.lstatSync(destination).isDirectory()||fs.readdirSync(destination).length))throw Error('安装目录已有内容或不是目录;请选择独立空目录,不覆盖已有文件');
36
+ const parent=path.dirname(destination);plainPath(parent);if(!fs.existsSync(parent)||!fs.lstatSync(parent).isDirectory())throw Error('安装目录的父目录必须存在');
37
+ fs.accessSync(parent,fs.constants.W_OK|fs.constants.X_OK);
38
+ const disk=fs.statfsSync(parent),availableBytes=Number(disk.bavail)*Number(disk.bsize);
39
+ if(!Number.isFinite(availableBytes)||availableBytes<=0)throw Error('无法确认可用空间;尚未下载或安装');
40
+ return{destination,parent,availableBytes,checkedAt:new Date().toISOString(),authority:'intent-only; runtime rechecks exact bytes and plan'};
41
+ }
42
+
43
+ // Intent preflight only. The installed engine repeats identity checks and seals
44
+ // the actual scope in the independently confirmed lifecycle plan.
45
+ export function inspectScopeIntent(kind, input, destination){
46
+ if(kind==='user')return {kind,projectRoot:null};
47
+ if(kind!=='project'||typeof input!=='string')throw Error('请选择有效的使用范围');
48
+ let projectRoot=input.trim();
49
+ if((projectRoot.startsWith('"')&&projectRoot.endsWith('"'))||(projectRoot.startsWith("'")&&projectRoot.endsWith("'")))projectRoot=projectRoot.slice(1,-1);
50
+ plainPath(projectRoot);
51
+ if(!fs.lstatSync(projectRoot).isDirectory())throw Error('项目根不是已有目录');
52
+ const git=spawnSync('/usr/bin/git',['-C',projectRoot,'rev-parse','--show-toplevel'],{encoding:'utf8',env:{PATH:'/usr/bin:/bin',GIT_OPTIONAL_LOCKS:'0'},timeout:10000});
53
+ if(git.status!==0||git.stdout.trim()!==projectRoot)throw Error('仅当前项目模式需要明确的 Git 仓库根;不会自动创建仓库');
54
+ if(destination===projectRoot||destination.startsWith(projectRoot+path.sep)||projectRoot.startsWith(destination+path.sep))throw Error('程序目录不能与业务项目互相包含;请选择独立目录');
55
+ return {kind,projectRoot};
56
+ }
@@ -1,4 +1,9 @@
1
1
  import crypto from 'node:crypto';
2
+ import {inspectEntryDestination,inspectScopeIntent,inspectEntryEnvironment} from './environment-check.mjs';
3
+
4
+ // Legacy engines already mean user scope. Do not send new keys to their strict
5
+ // selection parser; project scope must remain explicit and may never downgrade.
6
+ const scopeFields=value=>value.scopeKind==='project'?{scopeKind:'project',projectRoot:value.projectRoot}:{};
2
7
 
3
8
  // Only the exact child created by this invocation may supply an active view.
4
9
  // Browser input cannot name a URL, route, transport token or future plan.
@@ -31,7 +36,7 @@ export function createJourneyControl(readRecord, changed) {
31
36
  if(!selection&&result.body.session.sessionId!==event.sessionId)throw Error('确认会话不匹配');
32
37
  active={owner,type:selection?'selection':'manager',view:result.body};changed();
33
38
  if(selection&&choice){
34
- const selected=await request(owner,'__foundation/install/selection',{nonce:result.body.nonce,action:'select',destination:choice.destination,skillChoice:choice.skillChoice});
39
+ const selected=await request(owner,'__foundation/install/selection',{nonce:result.body.nonce,action:'select',destination:choice.destination,skillChoice:choice.skillChoice,...scopeFields(choice)});
35
40
  if(selected.status!==200){active.error=selected.body.message;choice=null;changed();}
36
41
  }
37
42
  },
@@ -50,17 +55,27 @@ export function createJourneyControl(readRecord, changed) {
50
55
  }
51
56
  if(body.action==='choose'&&resolveChoice){
52
57
  if(typeof body.destination!=='string'||!body.destination.trim()||!['selected','skipped'].includes(body.skillChoice))throw Error('请选择完整目录和 Skill 意向');
53
- choice={destination:body.destination,skillChoice:body.skillChoice};const resolve=resolveChoice;clearTimeout(choiceTimer);resolveChoice=null;rejectChoice=null;resolve(choice);changed();return {status:200,body:{state:'intent-selected'}};
58
+ const destinationCheck=inspectEntryDestination(body.destination);
59
+ const scopeKind=body.scopeKind||'user';if(!['user','project'].includes(scopeKind)||scopeKind==='project'&&(!body.projectRoot||typeof body.projectRoot!=='string'))throw Error('请选择使用范围和完整项目根路径');
60
+ const scope=inspectScopeIntent(scopeKind,body.projectRoot,destinationCheck.destination);
61
+ choice={destination:destinationCheck.destination,skillChoice:body.skillChoice,scopeKind,projectRoot:scope.projectRoot,destinationCheck};const resolve=resolveChoice;clearTimeout(choiceTimer);resolveChoice=null;rejectChoice=null;resolve(choice);changed();return {status:200,body:{state:'intent-selected'}};
54
62
  }
55
63
  if(!active)throw Error('当前没有可确认的计划;请等待或查询结果');
56
64
  const a=active;
57
65
  if(a.type==='selection'){
58
66
  if(!['select','cancel'].includes(body.action))throw Error('目录操作无效');
59
- const result=await request(a.owner,'__foundation/install/selection',{nonce:a.view.nonce,action:body.action,destination:body.destination,skillChoice:body.skillChoice});
67
+ const result=await request(a.owner,'__foundation/install/selection',body.action==='cancel'?{nonce:a.view.nonce,action:'cancel'}:{nonce:a.view.nonce,action:body.action,destination:body.destination,skillChoice:body.skillChoice,...scopeFields(body)});
60
68
  if(result.status!==200)a.error=result.body.message;changed();return result;
61
69
  }
62
70
  const s=a.view.session;
63
71
  if(body.sessionId!==s.sessionId||body.planHash!==s.planHash||body.managerNonce!==a.view.managerNonce||!['cancel-no-change',a.view.action].includes(body.action))throw Error('当前计划已变化或请求无效');
72
+ if(body.action!=='cancel-no-change'){
73
+ const environment=inspectEntryEnvironment({purpose:'confirm'});
74
+ // Keep the failed snapshot with this operation even if no mutation is
75
+ // forwarded; acquisition-only tools must not gate a verified plan.
76
+ Object.assign(readRecord(),{environment});changed({environment});
77
+ if(environment.state!=='ready')throw Error('执行前环境复检未通过;没有提交确认,请核实当前工具环境');
78
+ }
64
79
  const result=await request(a.owner,'__foundation/manager/confirm',{operationId:body.operationId,sessionId:s.sessionId,planHash:s.planHash,managerNonce:body.managerNonce,action:body.action});
65
80
  if(active===a&&result.body.state)a.view.session={...s,...result.body};changed();return result;
66
81
  }
@@ -3,15 +3,30 @@ import crypto from 'node:crypto';
3
3
  import {journeyView} from './journey-view.mjs';
4
4
  import {singlePageDocument} from './single-page-view.mjs';
5
5
 
6
- export function downloadText(download) {
6
+ export function downloadText(download, compact = false) {
7
7
  if(!download||!Number.isFinite(download.downloadedBytes)||download.downloadedBytes<0)return '';
8
8
  const amount=n=>(n/1048576).toFixed(2)+' MiB';
9
9
  const known=Number.isFinite(download.totalBytes)&&download.totalBytes>0;
10
- return '已接收 '+amount(download.downloadedBytes)+(known?' / '+amount(download.totalBytes)+(download.downloadedBytes<=download.totalBytes?' · '+Math.floor(download.downloadedBytes/download.totalBytes*100)+'%':'(长度待核实)'):'(总量未知)');
10
+ const valid=known&&download.downloadedBytes<=download.totalBytes;
11
+ const percent=valid&&download.downloadedBytes>0?Math.floor(download.downloadedBytes/download.totalBytes*100):null;
12
+ if(compact)return (percent===null?'已下载 ':'下载 '+percent+'% · ')+amount(download.downloadedBytes)+(known?' / '+amount(download.totalBytes)+(valid?'':'(长度待核实)'):'');
13
+ return '已接收 '+amount(download.downloadedBytes)+(known?' / '+amount(download.totalBytes)+(valid?(percent===null?'':' · '+percent+'%'):'(长度待核实)'):'(总量未知)');
14
+ }
15
+
16
+ export function downloadStatus(record) {
17
+ if(record.kind==='uninstall')return '';
18
+ if(record.terminal)return record.state==='failed'?'下载检查失败':'本次获取已结束';
19
+ if(record.phase==='fetching-and-verifying-runtime'){
20
+ const d=record.download,prefix=Number.isSafeInteger(d?.attempt)&&d.attempt>1?'重试 '+d.attempt+' · ':'';
21
+ if(d?.downloadedBytes===0)return prefix+'等待下载';
22
+ if(Number.isFinite(d?.totalBytes)&&d.totalBytes>0&&d.downloadedBytes===d.totalBytes)return '正在检查文件';
23
+ return prefix+(downloadText(d,true)||'等待下载');
24
+ }
25
+ return ({discovering:'正在查询版本','verifying-trust':'正在检查来源','fetching-and-verifying-catalog':'正在检查清单','verifying-runtime':'正在检查文件','extracting-and-validating':'正在展开检查','acquired':'检查完成'})[record.phase]||'';
11
26
  }
12
27
 
13
28
  export function acquisitionProgress(record) {
14
- const phases={discovering:'查询正式发行','verifying-trust':'核验发行来源','fetching-and-verifying-catalog':'获取并核验发行清单','fetching-and-verifying-runtime':'下载并核验运行文件','extracting-and-validating':'展开并检查文件','acquired':'获取验证完成','starting-confirmation-service':'准备目录选择与确认页','selecting-directory':'等待你选择目录','awaiting-confirmation':'等待你确认安装',executing:'正在安装','runtime-ended':'读取安装结果',finished:'安装结果已返回'};
29
+ const phases={discovering:'查询正式发行','verifying-trust':'核验发行来源','fetching-and-verifying-catalog':'获取并核验发行清单','fetching-and-verifying-runtime':'正在下载运行文件','verifying-runtime':'正在检查文件摘要与发行证明','extracting-and-validating':'展开并检查文件','acquired':'获取验证完成','starting-confirmation-service':'准备目录选择与确认页','selecting-directory':'等待你选择目录','awaiting-confirmation':'等待你确认安装',executing:'正在安装','runtime-ended':'读取安装结果',finished:'安装结果已返回'};
15
30
  const terminal=record.terminal;
16
31
  const acquired=['acquired','starting-confirmation-service','selecting-directory','directory-selection-ended','awaiting-confirmation','executing','runtime-ended','finished'].includes(record.phase);
17
32
  const observations={acquire:{state:acquired?'completed':terminal?'failed':'running',evidence:record.resultFile}};
@@ -35,7 +50,7 @@ export function acquisitionProgress(record) {
35
50
  const verb=kind==='uninstall'?'卸载':kind==='update'?'更新':'安装';
36
51
  Object.assign(phases,{'awaiting-confirmation':'等待你确认'+verb,executing:'正在'+verb,'runtime-ended':'读取'+verb+'结果',finished:verb+'结果已返回'});
37
52
  if(record.terminal&&record.state==='partial'&&record.programState==='completed'&&kind==='update')journey.title='程序已更新,Skill 尚未更新';
38
- return {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,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};
53
+ return {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};
39
54
  }
40
55
 
41
56
  // A bounded, read-only loopback view over the existing in-memory operation.
@@ -1,4 +1,5 @@
1
1
  // Product-area CSS transplanted from the approved 040 reference.
2
2
  // Source SHA-256: e9949cc0d67e979b0297ca654eda1f163f2178b3338d4c4cf3ea9363e3cd8644
3
3
  // Demo-only selectors remain inert; there are no demo controls in production.
4
- 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}#foundation-flow-demo .step-state{font-size:12px;color:var(--fp-muted)}#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}}";
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}}";
@@ -17,6 +17,7 @@ function singlePageClient(csrf) {
17
17
  const order=kind==='uninstall'?['remove','program']:['select','acquire','program','material','register','optional-skill','health'];
18
18
  const steps=p.progress.journey.steps.map(s=>({...s})).sort((a,b)=>order.indexOf(a.key)-order.indexOf(b.key));
19
19
  if(choice)for(const s of steps)s.state=s.key==='select'?'pending':'planned';
20
+ if(r.environment)steps.unshift({key:'environment',title:'检查使用环境',state:r.environment.state==='ready'?'completed':'failed',evidence:(r.environment.preparation?'对话准备的基础环境已复检;':'复用现有入口环境;')+r.environment.platform+' '+r.environment.arch+';入口 Node '+r.environment.nodeVersion+';程序使用随包运行时,不修改共享设置'+(r.environment.preparation?'。独立环境回执:'+r.environment.preparation.receipt+';缓存不自动清理':'')});
20
21
  if(r.intentSelected){const s=steps.find(s=>s.key==='select');if(s)s.state='completed';}
21
22
  let active=choice?'select':v?.type==='selection'?'select':v?.session?.capabilityId?(v.session.operation==='install'?'material':v.session.operation==='uninstall'?'remove':'register'):v?.session?'program':steps.find(s=>!['completed','skipped','retained'].includes(s.state))?.key;
22
23
  // The final result check must not look finished while a chosen Skill step
@@ -40,17 +41,27 @@ function singlePageClient(csrf) {
40
41
  let body='';
41
42
  if(current&&(choice||v?.type==='selection'))body='<form id="current-form"><label class="field" for="install-path">安装到哪里?</label><input id="install-path" name="destination" type="text" value="'+esc(r.destinationIntent||v?.suggestion||'')+'" spellcheck="false" required><p class="hint">请输入完整路径,可以包含成对引号。不会覆盖已有内容,最终目录仍由程序检查。</p><label class="check"><input id="include-skill" name="includeSkill" type="checkbox" '+(r.journeyContext?.skillChoice==='skipped'?'':'checked')+'><span>同时安装 Foundation Skill<br><span class="hint">让 Codex 能识别 Foundation 指令,并按 Foundation 规范工作。</span></span></label><p class="hint">获取缓存:'+esc(r.acquisitionRoot||'以当前任务已披露位置为准')+'。选择不是安装批准。</p><div id="step-error" class="error" role="alert">'+esc(v?.error||'')+'</div><div class="actions"><button class="primary" name="action" value="'+(choice?'choose':'select')+'">使用这个位置</button><button name="action" value="'+(choice?'cancel-intent':'cancel')+'" formnovalidate>取消本步</button></div></form>';
42
43
  else if(current&&v?.session){const f=v.feedback;body='<p>'+esc(f.detail)+'</p><div class="facts">'+f.namedTargets.map(t=>'<p>'+esc(t.label)+':'+esc(t.path||'待核实')+'</p>').join('')+'<p>保留:'+esc(f.preserves.join(';'))+'</p><p>'+esc(f.notDone)+'</p>'+cleanupText(v.session)+((v.session.deletes||[]).length?'<details open><summary>删除范围</summary>'+v.session.deletes.map(x=>'<p>'+esc(x==='foundation-installation:full'?'本安装中经所有权核验的程序、运行时和状态文件;未知或用户修改的文件保留。':typeof x==='string'?x:JSON.stringify(x))+'</p>').join('')+'</details>':'')+'</div><p class="hint">每次确认只针对当前计划,不批准后续步骤。Skill 文件就位不等于新对话已识别。</p><form id="current-form"><div id="step-error" class="error" role="alert"></div><div class="actions">'+(v.session.state==='pending'?'<button class="'+(kind==='uninstall'?'danger':'primary')+'" name="action" value="'+esc(v.action)+'">确认'+esc(titles[s.key])+'</button><button name="action" value="cancel-no-change">取消本步</button>':'<p>本步已开始或结束,不能重复确认。</p>')+'</div></form>';}
43
- else if(current)body='<p role="status">'+esc(p.progress.phase)+'</p><p class="hint">下一步会在本页展开,不需要查找其他网址。</p>';
44
- return '<li class="step '+(current?'current':done||skip?'done':'later')+'"><div class="step-top"><span class="step-number">'+(skip?'−':done?'✓':i+1)+'</span><span class="step-title">'+esc(titles[s.key]||s.title)+'</span><span class="step-state">'+esc(skip?'已跳过':done?'已完成':stateLabels[s.state]||(current?(choice?'等待选择':p.progress.phase):r.terminal?'本次未执行':'等待前序步骤'))+'</span></div>'+(current?'<div class="step-body">'+body+'</div>':done||skip?'<details class="completed-detail"><summary>查看结果</summary><p>'+esc(skip?'本次未执行此步骤。':s.evidence?JSON.stringify(s.evidence):'结果来自本次操作记录')+'</p></details>':'')+'</li>';
44
+ else if(current)body='<p role="status" '+(s.key==='acquire'?'data-acquisition-phase':'')+'>'+esc(p.progress.phase)+'</p><p class="hint">下一步会在本页展开,不需要查找其他网址。</p>'+(s.key==='acquire'?'<p class="error" data-download-connection role="status"></p>':'');
45
+ else if(s.key==='acquire'&&s.state==='failed')body='<p class="error">'+esc(p.progress.next)+'</p>';
46
+ if(current&&(choice||v?.type==='selection'))body=body.replace('<form id="current-form">','<form id="current-form"><label class="field" for="scope-kind">在哪里使用?</label><select id="scope-kind" name="scopeKind"><option value="user" '+(r.scopeIntent?.kind!=='project'?'selected':'')+'>当前用户使用(推荐)</option><option value="project" '+(r.scopeIntent?.kind==='project'?'selected':'')+'>仅当前项目使用</option></select><p class="hint">当前用户:多个项目共用,Skill 为用户级,不是所有系统账户。项目模式:绑定具体仓库,Skill 仅放入该项目;已有用户级 Skill 不会消失,同名冲突会停止注册。</p><label class="field" for="project-root">项目根路径(仅项目模式需要)</label><input id="project-root" type="text" name="projectRoot" value="'+esc(r.scopeIntent?.projectRoot||'')+'" spellcheck="false"><p class="hint">程序仍使用与业务项目不重叠的独立目录,通过真实项目身份限定使用,不修改业务依赖或 AGENTS。</p>');
47
+ return '<li data-step="'+esc(s.key)+'" class="step '+(current?'current':done||skip?'done':'later')+'"><div class="step-top"><span class="step-number">'+(skip?'−':done?'✓':i+1)+'</span><span class="step-title">'+esc(titles[s.key]||s.title)+'</span><span class="step-state">'+esc(skip?'已跳过':done?'已完成':stateLabels[s.state]||(current?(choice?'等待选择':p.progress.phase):r.terminal?'本次未执行':'等待前序步骤'))+'</span></div>'+(body?'<div class="step-body">'+body+'</div>':done||skip?'<details class="completed-detail"><summary>查看结果</summary><p>'+esc(skip?'本次未执行此步骤。':s.evidence?JSON.stringify(s.evidence):'结果来自本次操作记录')+'</p></details>':'')+'</li>';
45
48
  }).join('');
46
49
  }
47
50
  if(busy&&executing){const live=q('.current .step-state');if(live)live.textContent='正在执行';}
51
+ // Byte/phase updates must not rebuild the form or depend on its revision key.
52
+ const downloadRow=q('[data-step="acquire"]');
53
+ if(downloadRow&&(active==='acquire'||r.terminal&&steps.find(s=>s.key==='acquire')?.state==='failed')){
54
+ downloadRow.querySelector('.step-state').textContent=p.progress.downloadStatus||p.progress.phase;
55
+ const phase=downloadRow.querySelector('[data-acquisition-phase]');if(phase)phase.textContent=p.progress.downloadStatus==='正在检查文件'?'正在检查文件':p.progress.phase;
56
+ const connection=downloadRow.querySelector('[data-download-connection]');if(connection)connection.textContent='';
57
+ }
48
58
  q('#flow-result').hidden=!r.terminal;
49
59
  if(r.terminal)q('#flow-result').innerHTML='<h2>'+esc(r.state==='completed'?verb+'完成':'部分完成 / 待核实')+'</h2><p>'+esc(p.progress.program)+'</p><p>Skill:'+esc(p.progress.skill)+'</p><p>项目:'+esc(p.progress.project)+'</p><p class="hint">下一步:'+esc(p.progress.next)+'</p>';
50
60
  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);
51
61
  }
52
62
  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})};
63
+ if(['choose','select'].includes(action)){body.scopeKind=form.elements.scopeKind.value;body.projectRoot=body.scopeKind==='project'?form.elements.projectRoot.value:null;}
53
64
  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='请求结果待核实,不自动重试。已收到的结果保留;刷新查询同次流程。';}});
54
- 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?'结果服务已结束,上方保留最后核实结果。可在对话中按本次操作记录再次查询。':'连接已中断,保留最后核实结果;不自动重试操作。';}}
65
+ 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)+' · 待核实';}}}
55
66
  async function poll(){if(stopped)return;await refresh();if(!stopped)setTimeout(poll,750);}poll();window.addEventListener('pagehide',()=>{stopped=true},{once:true});
56
67
  }
@@ -13,7 +13,8 @@ export async function followSelectedSkill({operation,env,onChange,journeyControl
13
13
  const identity=initial.installation?.current;
14
14
  if(!identity?.identity?.installId||initial.bridge?.currentVersion!==identity.version)throw Error('当前安装身份无法核实,停止 Skill 接续');
15
15
  if(operation.installId&&operation.installId!==identity.identity.installId)throw Error('恢复记录属于另一安装,停止接续');
16
- onChange({installId:identity.identity.installId,installedCandidateHash:identity.candidateHash});
16
+ onChange({installId:identity.identity.installId,installedCandidateHash:identity.candidateHash,usageScope:identity.usageScope||{kind:'user',project:null}});
17
+ const discoveryNext=identity.usageScope?.kind==='project'?'请在绑定项目 '+identity.usageScope.project.path+' 内新开 Codex 对话验证识别;不会供其他项目使用。':'新开 Codex 对话验证识别。';
17
18
  const manifestFile=plainPath(path.join(root,identity.appPath,'artifacts/skills/ai-product-foundation-kit/capability.json'));
18
19
  if(!manifestFile.startsWith(root+path.sep))throw Error('Skill 材料必须属于当前安装');
19
20
  if(initial.codexSkill?.state==='files-installed-host-discovery-unverified'){
@@ -49,5 +50,5 @@ const event=await observePlan(client,requested,observation=>onChange({phase:obse
49
50
  }
50
51
  const verified=checkIdentity().codexSkill;
51
52
  if(verified?.state!=='files-installed-host-discovery-unverified')throw Error('Skill 注册文件与归属未通过核验;程序成功保留');
52
- return {state:'completed',phase:'finished',terminal:true,skillRegistered:true,skillDestination:verified.destination,skillSteps:steps,hostDiscoveryVerified:false,next:'程序与所选 Skill 文件已就位;新开 Codex 对话验证识别,项目接入另行确认。'};
53
+ return {state:'completed',phase:'finished',terminal:true,skillRegistered:true,skillDestination:verified.destination,skillSteps:steps,hostDiscoveryVerified:false,next:'程序与所选 Skill 文件已就位;'+discoveryNext+' 项目接入另行确认。'};
53
54
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@josephyulei/summon-foundation",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@josephyulei/summon-foundation",
9
- "version": "0.1.6",
9
+ "version": "0.1.7",
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.6",
3
+ "version": "0.1.7",
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"},