@givolt/i3-composer-agent 2.3.1 → 2.3.3
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/.agents/plugins/marketplace.json +1 -1
- package/.claude/commands/i3-prototype.md +5 -7
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini/commands/i3-prototype.toml +5 -7
- package/README.md +16 -7
- package/bin/agent-targets.json +4 -2
- package/bin/i3-composer-agent.cjs +74 -7
- package/bin/install.cjs +16 -1
- package/bin/native-layout.cjs +1 -1
- package/bin/native-migration.cjs +27 -3
- package/bin/tool-selector.cjs +24 -7
- package/bin/wizard.cjs +89 -15
- package/commands/i3-prototype.toml +5 -7
- package/docs/codebuddy-setup.md +91 -3
- package/docs/codex-setup.md +6 -0
- package/docs/other-hosts.md +1 -1
- package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
- package/docs/ui-prototype-native/USAGE.md +93 -0
- package/docs/workbuddy-setup.md +6 -0
- package/evals/cases/i3-frontend-ui-engineering.json +16 -0
- package/evals/cases/i3-ui-prototype.json +109 -0
- package/evals/cases/i3-using-agent-skills.json +28 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
- package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
- package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
- package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
- package/hooks/codebuddy-upstream.cjs +93 -0
- package/hooks/i3-prototype.cjs +12 -0
- package/hooks/manage.cjs +162 -0
- package/package.json +61 -1
- package/plugin.json +1 -1
- package/scripts/design-provider-test.js +7 -0
- package/scripts/enterprise-template-test.js +27 -1
- package/scripts/lib/prototype-fixture.cjs +17 -0
- package/scripts/prototype-cache-test.js +55 -0
- package/scripts/prototype-generation-entry-test.js +6 -0
- package/scripts/prototype-generation-evidence-test.js +10 -0
- package/scripts/prototype-generation-native-test.js +78 -0
- package/scripts/prototype-generation-test.js +99 -0
- package/scripts/prototype-inventory-test.js +43 -0
- package/scripts/prototype-locations-test.js +79 -0
- package/scripts/prototype-project-test.js +183 -0
- package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
- package/scripts/ui-prototype-content-test.js +123 -0
- package/scripts/ui-prototype-dependencies-test.js +213 -0
- package/scripts/ui-prototype-devtools-test.js +53 -0
- package/scripts/ui-prototype-execution-evidence-test.js +62 -0
- package/scripts/ui-prototype-execution-gates-test.js +66 -0
- package/scripts/ui-prototype-execution-policy-test.js +42 -0
- package/scripts/ui-prototype-execution-runtime-test.js +27 -0
- package/scripts/ui-prototype-ide-source-test.js +73 -0
- package/scripts/ui-prototype-native-test.js +504 -0
- package/scripts/ui-prototype-registration-test.js +53 -0
- package/scripts/ui-prototype-template-test.js +261 -0
- package/scripts/ui-prototype-test.js +10 -0
- package/scripts/ui-prototype-transcript-auto-test.js +67 -0
- package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
- package/skills/i3-incremental-implementation/SKILL.md +1 -1
- package/skills/i3-ui-prototype/SKILL.md +158 -19
- package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
- package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
- package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
- package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
- package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
- package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
- package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
- package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
- package/skills/i3-ui-prototype/references/content-template.md +81 -0
- package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
- package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
- package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
- package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
- package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
- package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
- package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
- package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
- package/skills/i3-ui-prototype/references/project-layout.md +92 -0
- package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
- package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
- package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
- package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
- package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
- package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
- package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
- package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
- package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
- package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
- package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
- package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
- package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
- package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
- package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
- package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
- package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
- package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
- package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
- package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
- package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
- package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
- package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
- package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
- package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
- package/skills/i3-using-agent-skills/SKILL.md +2 -5
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const F=require('./native-files.cjs');
|
|
3
|
+
const {fs,path,crypto,assert,hash,json,id,inside,walk,write,entries,excluded}=F;
|
|
4
|
+
const skillRoot=path.resolve(__dirname,'..');
|
|
5
|
+
const frameworkRoot=path.join(skillRoot,'assets/frameworks/vue-antdv-next');
|
|
6
|
+
const templates=()=>require('./template-contract.cjs');
|
|
7
|
+
const projectRegistry=require('./project-registry.cjs');
|
|
8
|
+
function metadata(directory) { const data=json(inside(directory,'prototype.json')); assert.equal(data.schemaVersion,2,'Not a native v2 prototype'); assert.equal(data.runtime.kind,'vue-antdv-next'); return data; }
|
|
9
|
+
function effectiveReview(data,current) {
|
|
10
|
+
const review=data.review;
|
|
11
|
+
if(review.status!=='approved'||(review.revision===current.revision&&review.digest===current.digest))return review;
|
|
12
|
+
return {status:'pending-review',previousApproval:review,reason:'The stored approval applies to a different revision or candidate digest'};
|
|
13
|
+
}
|
|
14
|
+
function analyze(options={}) {
|
|
15
|
+
if(options.execution!==undefined)assert.ok(['inline','delegated'].includes(options.execution),'Invalid execution declaration: use inline or delegated');
|
|
16
|
+
const project=path.resolve(options.project||'.');
|
|
17
|
+
return {operation:'analyze',readOnly:true,project,projectExists:fs.existsSync(project),supportedOperations:['analyze','create','revise','inspect','confirm-baseline'],execution:options.execution??null,executionEvidence:options.execution===undefined?'unverified':'caller-declared',sideEffects:[]};
|
|
18
|
+
}
|
|
19
|
+
function prepareTemplate(options) { return templates().prepareTemplate(options); }
|
|
20
|
+
function manifest(directory) {
|
|
21
|
+
const data=metadata(directory);
|
|
22
|
+
const inputFiles=walk(directory,'inputs');
|
|
23
|
+
const inputs=entries(directory,inputFiles), inputDigest=hash(JSON.stringify(inputs));
|
|
24
|
+
const files=entries(directory,[...inputFiles,'requirements-map.json',...['app','mocks','design-system'].flatMap(folder=>walk(directory,folder,excluded))]);
|
|
25
|
+
const identity={...(data.runtime.contentReference?{business:data.business}:{}),schemaVersion:2,id:data.id,revision:data.revision,artifactKind:data.artifactKind,runtime:data.runtime,template:data.template,framework:data.framework,expertMode:data.expertMode};
|
|
26
|
+
return {schemaVersion:2,revision:data.revision,identity,inputDigest,files,digest:hash(JSON.stringify({identity,files}))};
|
|
27
|
+
}
|
|
28
|
+
function defaultScope() {
|
|
29
|
+
return {schemaVersion:1,title:'设备清单首期演示需求',requirements:[
|
|
30
|
+
{id:'PILOT-01',text:'设备列表支持查询、当前页多选、分页和批量变更状态',source:{type:'prd',reference:'pilot-fixture/PILOT-01',statement:'21条记录,每页10条;换页或筛选清空选择;批量操作仅影响选中记录'}},
|
|
31
|
+
{id:'PILOT-02',text:'编辑抽屉使用真实表单校验,失败保留输入,成功更新对应记录',source:{type:'prd',reference:'pilot-fixture/PILOT-02',statement:'设备名称必填;可模拟提交失败并重试'}},
|
|
32
|
+
{id:'PILOT-03',text:'加载、空、失败和恢复可触发;抽屉开合、遮罩模糊及焦点可验证',source:{type:'prd',reference:'pilot-fixture/PILOT-03',statement:'首期演示规则;不是任何生产项目的已确认权限或流程'}}
|
|
33
|
+
],exemptions:[],openQuestions:[]};
|
|
34
|
+
}
|
|
35
|
+
function createPrototype(options) {
|
|
36
|
+
const installedFile=path.resolve(__dirname,'../../../.i3-composer-agent.json');
|
|
37
|
+
if(fs.existsSync(installedFile))options=require('./execution-policy.cjs').creationOptions(options,json(installedFile));
|
|
38
|
+
if(options.executionPolicy!==undefined){assert.equal(options.executionPolicy,require('./execution-policy.cjs').ID,'Unsupported execution policy');assert.equal(options.host,'codebuddy','Explicit CodeBuddy host required for A+B');}
|
|
39
|
+
assert.ok(options.framework===undefined||options.framework==='vue-antdv-next','Unsupported framework / 不支持的框架');
|
|
40
|
+
assert.ok(options.title===undefined||(typeof options.title==='string'&&options.title.trim()),'Title must be a nonempty string');
|
|
41
|
+
const project=path.resolve(options.project||'.'); assert.ok(fs.statSync(project).isDirectory(),'Project must exist'); id(options.id);
|
|
42
|
+
const mode=options.expertMode||'professional'; assert.ok(['professional','template'].includes(mode),'Invalid expert mode');
|
|
43
|
+
let provider=null;
|
|
44
|
+
if(mode==='professional') {
|
|
45
|
+
provider=require('./prototype-lib.cjs').checkDesignProvider({...options,project});
|
|
46
|
+
if(provider.availability!=='available') return {status:'needs-design-provider',provider,sideEffects:[],choices:['locate-or-repair','explicit-template-mode','analyze']};
|
|
47
|
+
}
|
|
48
|
+
assert.ok(!(options.templateRoot && options.template), 'Choose a template ID or an explicit template root');
|
|
49
|
+
const template=options.templateRoot?templates().loadTemplate(path.resolve(options.templateRoot)):templates().loadBundledTemplate(options.template);
|
|
50
|
+
const reference=template.kind==='content-reference';
|
|
51
|
+
assert.ok(!reference||options.scope,'A sourced scope file is required for business generation; no default device demo');
|
|
52
|
+
const scope=options.scope?json(path.resolve(options.scope)):defaultScope();
|
|
53
|
+
const appliesTo=projectRegistry.moduleIds(scope);
|
|
54
|
+
const selection=reference?require('./content-template.cjs').scopeInputs(template,scope,templates()):null;
|
|
55
|
+
const generation=scope.schemaVersion===3?require('./generation-contract.cjs').compile(template,scope):null;
|
|
56
|
+
const content=reference?{pageTitle:scope.title||'业务原型'}:templates().projectAdminListContent(template);
|
|
57
|
+
if(options.title!==undefined)content.pageTitle=options.title;
|
|
58
|
+
const profile=json(path.join(frameworkRoot,'profile.json'));
|
|
59
|
+
const target=inside(project,`prototypes/${options.id}`); assert.ok(!fs.existsSync(target),'Prototype exists; revise without reinitializing');
|
|
60
|
+
return projectRegistry.transaction(project,store=>{
|
|
61
|
+
let stage=null,published=false;
|
|
62
|
+
try {
|
|
63
|
+
fs.mkdirSync(path.dirname(target),{recursive:true}); stage=fs.mkdtempSync(path.join(path.dirname(target),'.native-'));
|
|
64
|
+
const registry=store.registry;
|
|
65
|
+
assert.equal(registry.schemaVersion,1); assert.ok(Array.isArray(registry.prototypes)); assert.ok(!registry.prototypes.some(p=>p.id===options.id),'Duplicate prototype ID');
|
|
66
|
+
const frameworkFiles=entries(frameworkRoot,walk(frameworkRoot,'',excluded));
|
|
67
|
+
const instanceId=crypto.randomUUID();
|
|
68
|
+
const data={schemaVersion:2,id:options.id,title:options.title||content.pageTitle||'设备清单原型',revision:'r001',artifactKind:'framework-prototype',runtime:{kind:'vue-antdv-next',version:'1',...(reference?{contentReference:true}:{})},...(reference?{business:{status:'needs-adaptation',source:'inputs/scope.json'}}:{}),template:{id:template.id,version:template.version,digest:template.digest},framework:{id:'vue-antdv-next',version:profile.version||'1',digest:hash(JSON.stringify(frameworkFiles))},expertMode:mode,expert:{status:provider?'pending':'skipped',provider},verification:{status:'not-run'},review:{status:'pending-review'},instanceId};
|
|
69
|
+
if(generation)data.runtime.generationContract=1;
|
|
70
|
+
if(options.executionPolicy){data.runtime.executionPolicy=options.executionPolicy;write(stage,'inputs/execution-policy.json',require('./execution-policy.cjs').create({host:options.host,mode,officialCallable:!options.officialSourceFallback}));}
|
|
71
|
+
write(stage,'prototype.json',data);
|
|
72
|
+
for(const file of template.files) {
|
|
73
|
+
assert.equal(file.encoding,'base64');
|
|
74
|
+
const destination=inside(stage,`inputs/template/${file.path}`); fs.mkdirSync(path.dirname(destination),{recursive:true}); fs.writeFileSync(destination,Buffer.from(file.content,'base64'));
|
|
75
|
+
}
|
|
76
|
+
write(stage,'inputs/template-snapshot.json',template);
|
|
77
|
+
write(stage,'inputs/scope.json',scope);
|
|
78
|
+
if(generation)write(stage,'inputs/generation.json',generation);
|
|
79
|
+
write(stage,'inputs/capabilities.json',selection?selection.fixed:{schemaVersion:1,definition:template.definition,bindings:template.index.bindings,regions:template.index.regions,decisions:[]});
|
|
80
|
+
write(stage,'inputs/framework.json',{profile,componentMap:generation?require('./generation-evidence.cjs').framework():json(path.join(frameworkRoot,'component-map.json')),acceptance:json(path.join(frameworkRoot,'acceptance.json')),files:frameworkFiles});
|
|
81
|
+
const checked=templates().checkInstanceCapabilities(path.join(stage,'inputs')); assert.equal(checked.status,'passed',checked.errors?.join('; '));
|
|
82
|
+
const scenarioDetails=[...checked.requiredScenarios,...(generation?.scenarios||[])],requiredScenarios=scenarioDetails.map(s=>typeof s==='string'?s:s.id);
|
|
83
|
+
const hasDrawer=reference&&(selection.fixed.regions.some(region=>region.mode==='edit-form')||generation?.bindings.some(binding=>binding.capabilityId==='dialog'&&binding.parameters.presentation==='drawer'));
|
|
84
|
+
write(stage,'inputs/acceptance.json',{schemaVersion:1,requiredScenarios,scenarioDetails,frameworkScenarios:reference?['framework-responsive','framework-instance-isolation','template-content-projection',...(hasDrawer?['framework-drawer-motion','framework-mask-blur','framework-focus-scroll']:[])]:['framework-drawer-motion','framework-mask-blur','framework-focus-scroll','framework-states','framework-responsive','framework-instance-isolation','template-content-projection'],browserReview:json(path.join(frameworkRoot,'acceptance.json')).browserReview});
|
|
85
|
+
if(options.executionPolicy){const all=json(inside(stage,'inputs/acceptance.json'));assert.ok(options.executionAssertions,`Strict policy requires --execution-assertions with fixed expected JSON facts for: ${[...all.requiredScenarios,...all.frameworkScenarios].join(', ')}`);const fixed=require('./execution-assertions.cjs').validate(json(path.resolve(options.executionAssertions)),[...all.requiredScenarios,...all.frameworkScenarios]);write(stage,'inputs/execution-assertions.json',fixed);}
|
|
86
|
+
const requirements=json(path.join(stage,'inputs/scope.json')).requirements;
|
|
87
|
+
write(stage,'requirements-map.json',{schemaVersion:2,requirements:requirements.map(r=>({...r,pages:reference?r.pages:template.index.pages.map(page=>page.id),scenarios:reference?scenarioDetails.filter(s=>s.bindingId.startsWith('app-shell/')||r.pages.some(page=>s.bindingId.startsWith(page+'/'))).map(s=>s.id):requiredScenarios})),bindings:[...(selection?selection.fixed.bindings:template.index.bindings),...(generation?.bindings||[])]});
|
|
88
|
+
const starter=path.join(frameworkRoot,reference?profile.starter:(profile.legacyStarter||profile.starter));
|
|
89
|
+
fs.cpSync(starter,path.join(stage,'app'),{recursive:true,filter:file=>!excluded(`starter/${path.relative(starter,file).split(path.sep).join('/')}`,path.basename(file))});
|
|
90
|
+
const themeBinding=(selection?selection.fixed.bindings:template.index.bindings).find(binding=>binding.capabilityId==='theme');
|
|
91
|
+
write(stage,'app/public/prototype-config.json',{instanceId,storageNamespace:`i3-native-${options.id}-${instanceId}`,mockVersion:reference?'business-v1':'admin-list-v1',title:data.title,...(reference?{pages:scope.pages.map(page=>({id:page.id,title:page.title}))}:{content}),...(generation?{presentation:generation.presentation,navigation:generation.navigation,generation:{pages:generation.pages}}:{}),...(themeBinding?{theme:templates().resolveThemeParameters(themeBinding.parameters)}:{})});
|
|
92
|
+
write(stage,'mocks/contract.json',reference?{schemaVersion:1,instanceId,status:'needs-business-data',requirements:scope.requirements.map(r=>r.id)}:{schemaVersion:1,seed:'admin-list-v1',count:21,pageSize:10,selection:'current-page-clear-on-page-or-filter',instanceId});
|
|
93
|
+
write(stage,'design-system/MASTER.md',`# ${data.title}\n\n使用 antDv-next 原生组件与主题。管理列表采用紧凑信息层级,正文与操作可读;错误在字段附近呈现。抽屉保留进入与退出过程,显式开启遮罩模糊。\n\n本地模拟,不代表生产权限。\n`);
|
|
94
|
+
if(generation)write(stage,'design-system/MASTER.md',require('./generation-evidence.cjs').designSummary(generation,`# ${data.title}\n\nPrototypeShell 提供已选原生外壳;按逐区域/字段/操作合同实现业务,补齐真实源码映射和浏览器证据。骨架不是业务完成。本地模拟不代表生产权限。`));
|
|
95
|
+
if(generation)write(stage,'design-system/implementation.json',require('./generation-evidence.cjs').initialize(generation));
|
|
96
|
+
write(stage,'design-system/decisions.json',{schemaVersion:1,records:[],reviewStatus:mode==='template'?'skipped':'pending'});
|
|
97
|
+
write(stage,'qa/design-advice.json',{schemaVersion:2,provider,queries:[],records:[],status:mode==='template'?'skipped':'pending'});
|
|
98
|
+
write(stage,'HANDOFF.md',`# ${data.title}\n\n本次为 ${mode} 模式;来源 ${template.id} ${template.version}。运行形态为真实 Vue+antDv-next。读取 inputs 中固定合同和实际源码,勿从安装目录更新旧合同。\n\n使用 native-pilot.cjs inspect/build/preview。当前未验证,待用户评审;不要把检查通过当用户认可。\n`);
|
|
99
|
+
const candidate=manifest(stage); data.inputs={digest:candidate.inputDigest}; write(stage,'prototype.json',data);
|
|
100
|
+
fs.renameSync(stage,target); published=true;
|
|
101
|
+
const digest=manifest(target).digest;
|
|
102
|
+
registry.prototypes.push({id:options.id,directory:`prototypes/${options.id}`,appliesTo,schemaVersion:2}); store.save();
|
|
103
|
+
return {status:'created',directory:target,revision:'r001',digest};
|
|
104
|
+
} catch(error) { if(published) fs.rmSync(target,{recursive:true,force:true}); throw error; }
|
|
105
|
+
finally { if(stage&&fs.existsSync(stage)) fs.rmSync(stage,{recursive:true,force:true}); }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function inspectDesignReview(directory,current=manifest(directory)) {
|
|
109
|
+
const data=metadata(directory);
|
|
110
|
+
if(data.expertMode!=='professional') return {status:'skipped',queries:0,records:0,adoptedAdvice:[]};
|
|
111
|
+
const advice=json(inside(directory,'qa/design-advice.json')); assert.equal(advice.provider?.availability,'available','Professional provider not available');
|
|
112
|
+
assert.ok(advice.provider.digest,'Professional provider identity required');
|
|
113
|
+
assert.ok(advice.queries?.length && advice.queries.every(q=>q.query && q.source && q.resultDigest),'Real design query record required');
|
|
114
|
+
assert.ok(['reviewed','no-match'].includes(advice.status),'Design advice review pending');
|
|
115
|
+
assert.ok(advice.records?.length && advice.records.every(r=>['adopted','adjusted','rejected','no-match'].includes(r.decision) && r.query && advice.queries.some(q=>q.query===r.query) && r.resultIdentity && r.rationale && r.verification),'Advice decisions need query, identity, reasons and verification');
|
|
116
|
+
const decisions=json(inside(directory,'design-system/decisions.json'));
|
|
117
|
+
assert.ok(['reviewed','no-match'].includes(decisions.reviewStatus),'Design contract review pending');
|
|
118
|
+
const stable=r=>({query:r.query,resultIdentity:r.resultIdentity,decision:r.decision,rationale:r.rationale,implementation:r.implementation||null,verificationScenario:r.verificationScenario||null});
|
|
119
|
+
assert.deepEqual(decisions.records.map(stable),advice.records.map(stable),'Design contract differs from advice review');
|
|
120
|
+
const adoptedAdvice=advice.records.filter(r=>['adopted','adjusted'].includes(r.decision));
|
|
121
|
+
for(const record of adoptedAdvice)assert.ok(Object.hasOwn(current.files,record.implementation) && record.verificationScenario,'Adopted advice needs implementation and verified scenario');
|
|
122
|
+
|
|
123
|
+
return {status:advice.status,queries:advice.queries.length,records:advice.records.length,adoptedAdvice};
|
|
124
|
+
}
|
|
125
|
+
function inspectWithInputs(directory,options={},acceptedInputDigest) {
|
|
126
|
+
try {
|
|
127
|
+
const data=metadata(directory); const current=manifest(directory);
|
|
128
|
+
assert.equal(acceptedInputDigest??data.inputs.digest,current.inputDigest,'Input contract changed; explicitly revise fixed inputs');
|
|
129
|
+
const capability=templates().checkInstanceCapabilities(path.join(directory,'inputs')); assert.equal(capability.status,'passed',capability.errors?.join('; '));
|
|
130
|
+
const acceptance=json(inside(directory,'inputs/acceptance.json'));
|
|
131
|
+
require('./execution-runtime.cjs').policy(directory);
|
|
132
|
+
require('./native-devtools.cjs').readPolicy(directory);
|
|
133
|
+
for(const scenario of capability.requiredScenarios.map(s=>typeof s==='string'?s:s.id)) assert.ok(acceptance.requiredScenarios.includes(scenario),`Required scenario omitted: ${scenario}`);
|
|
134
|
+
const fixedFramework=json(inside(directory,'inputs/framework.json'));
|
|
135
|
+
if(fixedFramework.acceptance?.scenarios?.some(scenario=>scenario.id==='template-content-projection'&&scenario.required))assert.ok(acceptance.frameworkScenarios.includes('template-content-projection'),'Required template-content-projection scenario omitted from fixed framework acceptance');
|
|
136
|
+
const template=json(inside(directory,'inputs/template-snapshot.json')),mapping=json(inside(directory,'requirements-map.json')),scope=json(inside(directory,'inputs/scope.json'));
|
|
137
|
+
const generation=scope.schemaVersion===3?json(inside(directory,'inputs/generation.json')):null;
|
|
138
|
+
if(generation){assert.equal(data.runtime.generationContract,1,'Generation runtime identity is required');require('./generation-contract.cjs').verify(template,scope,generation);for(const scenario of generation.scenarios)assert.ok(acceptance.requiredScenarios.includes(scenario.id),`Generation scenario omitted: ${scenario.id}`);}
|
|
139
|
+
projectRegistry.moduleIds(scope);
|
|
140
|
+
const pageIds=new Set(data.runtime.contentReference?['app-shell',...scope.pages.map(page=>page.id)]:template.index.pages.map(page=>page.id)),bindingIds=new Set([...json(inside(directory,'inputs/capabilities.json')).bindings,...(generation?.bindings||[])].map(binding=>binding.id));
|
|
141
|
+
for(const requirement of scope.requirements)assert.ok(mapping.requirements.some(item=>item.id===requirement.id),`Requirement omitted from mapping: ${requirement.id}`);
|
|
142
|
+
for(const requirement of mapping.requirements) {
|
|
143
|
+
assert.ok(scope.requirements.some(item=>item.id===requirement.id),`Unknown mapped requirement: ${requirement.id}`);
|
|
144
|
+
if(data.runtime.contentReference)assert.deepEqual(requirement.pages,scope.requirements.find(item=>item.id===requirement.id).pages,`Business requirement page mapping changed: ${requirement.id}`);
|
|
145
|
+
assert.ok(requirement.pages?.length&&requirement.pages.every(page=>pageIds.has(page)),`Unknown page identity in requirement: ${requirement.id}`);
|
|
146
|
+
assert.ok(requirement.scenarios?.length&&requirement.scenarios.every(scenario=>acceptance.requiredScenarios.includes(scenario)||acceptance.frameworkScenarios.includes(scenario)),`Unknown scenario in requirement: ${requirement.id}`);
|
|
147
|
+
}
|
|
148
|
+
for(const binding of mapping.bindings||[])assert.ok(bindingIds.has(binding.id),`Unknown binding in mapping: ${binding.id}`);
|
|
149
|
+
for(const file of ['app/package.json','app/package-lock.json','app/src/App.vue','app/public/prototype-config.json','design-system/MASTER.md']) assert.ok(fs.existsSync(inside(directory,file)),`Missing ${file}`);
|
|
150
|
+
const config=json(inside(directory,'app/public/prototype-config.json')); assert.equal(config.instanceId,data.instanceId); assert.ok(config.storageNamespace.includes(data.instanceId),'Instance storage not isolated');
|
|
151
|
+
if(data.runtime.contentReference) {
|
|
152
|
+
assert.deepEqual(config.pages,scope.pages.map(page=>({id:page.id,title:page.title})),'Business configuration differs from fixed page scope');
|
|
153
|
+
assert.ok(['needs-adaptation','adapted'].includes(data.business?.status),'Unknown business implementation status');
|
|
154
|
+
const fixedBindings=[...json(inside(directory,'inputs/capabilities.json')).bindings,...(generation?.bindings||[])];
|
|
155
|
+
assert.deepEqual((mapping.bindings||[]).map(binding=>binding.id).sort(),fixedBindings.map(binding=>binding.id).sort(),'Business mapping omits fixed capabilities');
|
|
156
|
+
}
|
|
157
|
+
const themeBinding=json(inside(directory,'inputs/capabilities.json')).bindings.find(binding=>data.runtime.contentReference?binding.capabilityId==='theme':binding.id==='devices/device-list/theme');
|
|
158
|
+
if(themeBinding)assert.deepEqual(config.theme,templates().resolveThemeParameters(themeBinding.parameters),'Theme configuration differs from fixed input binding');
|
|
159
|
+
else assert.equal(config.theme,undefined,'Theme requires an explicit optional capability binding');
|
|
160
|
+
if(generation){assert.deepEqual(config.presentation,generation.presentation,'Customer presentation differs from fixed choices');assert.deepEqual(config.navigation,generation.navigation,'Navigation differs from fixed generation');assert.deepEqual(config.generation,{pages:generation.pages},'Business page patterns differ from fixed generation');}
|
|
161
|
+
const deps=json(inside(directory,'app/package.json')).dependencies; assert.ok(deps['antdv-next'] && deps.vue,'Native framework dependencies missing');
|
|
162
|
+
if(generation)require('./generation-evidence.cjs').validateImplementation(directory,generation,json(inside(directory,'design-system/implementation.json')),{complete:!!options.acceptance,frameworkMap:fixedFramework.componentMap});
|
|
163
|
+
if(options.acceptance) {
|
|
164
|
+
if(generation){assert.equal(generation.pendingDecisions.length,0,'Reference applicability decisions remain unresolved');assert.equal(generation.pendingReferenceElements.length,0,'Reference functional elements remain unaccounted for');}
|
|
165
|
+
if(data.runtime.contentReference)assert.equal(data.business?.status,'adapted','Business implementation pending; a scaffold cannot pass acceptance');
|
|
166
|
+
const adoptedAdvice=inspectDesignReview(directory,current).adoptedAdvice;
|
|
167
|
+
const report=options.report||json(inside(directory,json(inside(directory,'qa/latest.json')).report));
|
|
168
|
+
assert.equal(report.status,'passed'); assert.equal(report.candidateDigest,current.digest,'Browser evidence is stale'); assert.equal(report.revision,current.revision);
|
|
169
|
+
const build=verifyBuild(directory,report.buildId); assert.equal(report.buildDigest,build.buildDigest);
|
|
170
|
+
assert.ok(report.executor && report.tool && report.startedAt && report.completedAt && report.source,'Browser run provenance required');
|
|
171
|
+
require('./native-build.cjs').verifyBrowserReport(directory,report,build);
|
|
172
|
+
assert.deepEqual(report.consoleErrors,[],'Browser errors remain');
|
|
173
|
+
const required=[...acceptance.requiredScenarios,...acceptance.frameworkScenarios];
|
|
174
|
+
for(const scenario of required) assert.ok(report.journeys.some(j=>j.id===scenario && j.status==='passed'),`Missing browser scenario: ${scenario}`);
|
|
175
|
+
if(generation)require('./generation-evidence.cjs').validateObservations(directory,generation,report);
|
|
176
|
+
for(const record of adoptedAdvice)assert.ok(report.journeys.some(j=>j.id===record.verificationScenario && j.status==='passed'),`Advice verification missing: ${record.resultIdentity}`);
|
|
177
|
+
require('./execution-runtime.cjs').requireStage(directory,'delivery',module.exports,{businessReport:report});
|
|
178
|
+
}
|
|
179
|
+
return {status:'passed',level:options.acceptance?'acceptance':'structure',revision:current.revision,digest:current.digest,verification:data.verification.status,review:effectiveReview(data,current).status};
|
|
180
|
+
} catch(error) { return {status:'failed',errors:[error.message]}; }
|
|
181
|
+
}
|
|
182
|
+
function inspect(directory,options={}) { return inspectWithInputs(directory,options); }
|
|
183
|
+
function expected(directory,options) {
|
|
184
|
+
const current=manifest(directory);
|
|
185
|
+
assert.equal(options.expectedRevision,current.revision,'Stale revision / 版本冲突'); assert.equal(options.expectedDigest,current.digest,'Candidate changed / 内容变化'); return current;
|
|
186
|
+
}
|
|
187
|
+
function claim(directory,options) {
|
|
188
|
+
id(options.owner); expected(directory,options);
|
|
189
|
+
const lock=inside(directory,'.prototype-write.lock'); const fd=fs.openSync(lock,'wx');
|
|
190
|
+
try { fs.writeFileSync(fd,JSON.stringify({owner:options.owner,revision:options.expectedRevision,digest:options.expectedDigest})); }
|
|
191
|
+
finally { fs.closeSync(fd); }
|
|
192
|
+
write(directory,'execution.json',{owner:options.owner,status:'editing',revision:options.expectedRevision,digest:options.expectedDigest}); return {owner:options.owner};
|
|
193
|
+
}
|
|
194
|
+
function release(directory,owner) {
|
|
195
|
+
const lock=inside(directory,'.prototype-write.lock'); assert.equal(json(lock).owner,owner,'Different writing owner');
|
|
196
|
+
fs.unlinkSync(lock); write(directory,'execution.json',{owner,status:'released'}); return {status:'released'};
|
|
197
|
+
}
|
|
198
|
+
function mutate(directory,options,fn) {
|
|
199
|
+
const lock=inside(directory,'.prototype-write.lock'); const temporary=!fs.existsSync(lock); const owner=options.owner||`run-${crypto.randomUUID()}`;
|
|
200
|
+
if(temporary) claim(directory,{...options,owner}); else { assert.equal(json(lock).owner,owner,'Candidate has another writing owner'); expected(directory,options); }
|
|
201
|
+
try { return fn(); } finally { if(temporary) release(directory,owner); }
|
|
202
|
+
}
|
|
203
|
+
function revise(directory,options) {
|
|
204
|
+
id(options.revision);
|
|
205
|
+
assert.ok(options.acceptInputs===undefined||typeof options.acceptInputs==='boolean','--accept-inputs must be a boolean flag');
|
|
206
|
+
return mutate(directory,options,()=>{
|
|
207
|
+
const current=manifest(directory);
|
|
208
|
+
const data=metadata(directory); assert.notEqual(data.revision,options.revision); assert.ok(!fs.existsSync(inside(directory,`baselines/${options.revision}`)),'Revision already frozen');
|
|
209
|
+
if(options.acceptInputs) {
|
|
210
|
+
const checked=inspectWithInputs(directory,{},current.inputDigest);
|
|
211
|
+
assert.equal(checked.status,'passed',checked.errors?.join('; '));
|
|
212
|
+
} else assert.equal(data.inputs.digest,current.inputDigest,'Input contract changed; revise --accept-inputs must explicitly validate the new contract');
|
|
213
|
+
assert.equal(manifest(directory).digest,current.digest,'Candidate changed while validating fixed inputs');
|
|
214
|
+
const scope=json(inside(directory,'inputs/scope.json')); projectRegistry.moduleIds(scope);
|
|
215
|
+
return projectRegistry.withInstanceAssociation(directory,scope,()=>{
|
|
216
|
+
if(options.acceptInputs)data.inputs={digest:current.inputDigest};
|
|
217
|
+
data.revision=options.revision; data.verification={status:'not-run'}; data.review={status:'pending-review'};
|
|
218
|
+
write(directory,'prototype.json',data); return manifest(directory);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function replan(directory,options) { const owner=options.owner||`replan-${crypto.randomUUID()}`;return mutate(directory,{...options,owner},()=>require('./generation-replan.cjs').run(directory,{...options,owner},module.exports)); }
|
|
223
|
+
function build(directory,options={}) { const gates=require('./execution-runtime.cjs');gates.requireStage(directory,'implementation',module.exports);gates.requireStage(directory,'design',module.exports);return require('./native-build.cjs').build(directory,options,{manifest,inspect}); }
|
|
224
|
+
function synchronizeIndex(options={}) {
|
|
225
|
+
id(options.id);
|
|
226
|
+
const index=projectRegistry.read(options.project),entry=index.registry.prototypes.find(item=>item.id===options.id);
|
|
227
|
+
assert.ok(entry,'Prototype is not registered');
|
|
228
|
+
const directory=inside(index.project,entry.directory),scope=json(inside(directory,'inputs/scope.json'));
|
|
229
|
+
assert.ok(scope.moduleIds!==undefined||scope.schemaVersion===2,'Legacy scope has no explicit moduleIds; do not invent or rewrite old input contracts');
|
|
230
|
+
const current=manifest(directory);
|
|
231
|
+
return mutate(directory,{...options,expectedRevision:current.revision,expectedDigest:current.digest},()=>{
|
|
232
|
+
const checked=inspect(directory);assert.equal(checked.status,'passed',checked.errors?.join('; '));
|
|
233
|
+
return projectRegistry.transaction(index.project,store=>{
|
|
234
|
+
const selected=store.registry.prototypes.find(item=>item.id===options.id);
|
|
235
|
+
assert.ok(selected&&selected.directory===entry.directory,'Prototype index entry changed during synchronization');
|
|
236
|
+
const fixed=json(inside(directory,'inputs/scope.json')),appliesTo=projectRegistry.moduleIds(fixed);
|
|
237
|
+
assert.equal(manifest(directory).digest,current.digest,'Candidate changed during synchronization');
|
|
238
|
+
selected.appliesTo=appliesTo;store.save();
|
|
239
|
+
return {status:'synchronized',index:store.file,id:selected.id,appliesTo};
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function verifyBuild(directory,buildId) { return require('./native-build.cjs').verifyBuild(directory,buildId,{manifest}); }
|
|
244
|
+
function preview(directory,options={}) { return require('./native-build.cjs').preview(directory,options,{manifest}); }
|
|
245
|
+
function recordRun(directory,report) {
|
|
246
|
+
report=JSON.parse(JSON.stringify(report));
|
|
247
|
+
id(report.id); const current=manifest(directory); assert.equal(report.candidateDigest,current.digest,'Candidate changed during browser run'); assert.equal(report.revision,current.revision);
|
|
248
|
+
const result=inspect(directory,{acceptance:true,report}); assert.equal(result.status,'passed',result.errors?.join('; '));
|
|
249
|
+
assert.equal(manifest(directory).digest,current.digest,'Candidate changed while checking browser report');
|
|
250
|
+
const relative=`qa/runs/${report.id}/report.json`,existing=inside(directory,relative);
|
|
251
|
+
if(fs.existsSync(existing))assert.deepEqual(json(existing),report,'Published browser run is immutable; use a new run ID');
|
|
252
|
+
else write(directory,relative,report);
|
|
253
|
+
write(directory,'qa/latest.json',{report:relative});
|
|
254
|
+
const data=metadata(directory); data.verification={status:'passed',digest:current.digest,run:report.id};
|
|
255
|
+
data.review=effectiveReview(data,current);
|
|
256
|
+
if(data.expertMode==='professional') {
|
|
257
|
+
const advice=json(inside(directory,'qa/design-advice.json'));
|
|
258
|
+
data.expert.status=advice.records.some(record=>['adopted','adjusted'].includes(record.decision))?'used':advice.records.every(record=>record.decision==='no-match')?'no-match':'reviewed';
|
|
259
|
+
}
|
|
260
|
+
write(directory,'prototype.json',data); return {...result,verification:'passed'};
|
|
261
|
+
}
|
|
262
|
+
function confirmBaseline(directory,confirmation) {
|
|
263
|
+
assert.ok(confirmation.reviewer && confirmation.statement && confirmation.source,'Explicit human confirmation required / 人类确认');
|
|
264
|
+
const current=manifest(directory); assert.equal(confirmation.revision,current.revision); assert.equal(confirmation.digest,current.digest);
|
|
265
|
+
return mutate(directory,{expectedRevision:current.revision,expectedDigest:current.digest},()=>{
|
|
266
|
+
const checked=inspect(directory,{acceptance:true}); assert.equal(checked.status,'passed',checked.errors?.join('; '));
|
|
267
|
+
const devtoolsReview=require('./native-devtools.cjs').check(directory,module.exports);assert.ok(!devtoolsReview.required||devtoolsReview.status==='current',`Current DevTools review required: ${devtoolsReview.errors?.join('; ')||devtoolsReview.status}`);
|
|
268
|
+
const target=inside(directory,`baselines/${id(current.revision)}`); assert.ok(!fs.existsSync(target),'Baseline exists');
|
|
269
|
+
fs.mkdirSync(path.dirname(target),{recursive:true});const stage=fs.mkdtempSync(path.join(path.dirname(target),'.freeze-'));
|
|
270
|
+
try {
|
|
271
|
+
for(const [file,digest]of Object.entries(current.files)) {
|
|
272
|
+
const destination=inside(stage,file);fs.mkdirSync(path.dirname(destination),{recursive:true});fs.copyFileSync(inside(directory,file),destination);
|
|
273
|
+
assert.equal(hash(fs.readFileSync(destination)),digest,`Snapshot copy differs: ${file}`);
|
|
274
|
+
}
|
|
275
|
+
write(stage,'manifest.json',current);write(stage,'review.json',confirmation);const reviewDigest=hash(fs.readFileSync(path.join(stage,'review.json')));
|
|
276
|
+
assert.equal(manifest(directory).digest,current.digest,'Candidate changed during freezing');
|
|
277
|
+
fs.renameSync(stage,target);
|
|
278
|
+
write(directory,'baseline.json',{schemaVersion:2,revision:current.revision,digest:current.digest,reviewDigest,directory:`baselines/${current.revision}`});
|
|
279
|
+
const data=metadata(directory);data.review={status:'approved',revision:current.revision,digest:current.digest};write(directory,'prototype.json',data);return current;
|
|
280
|
+
} finally {if(fs.existsSync(stage))fs.rmSync(stage,{recursive:true,force:true});}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function compareBaseline(directory) {
|
|
284
|
+
if(!fs.existsSync(inside(directory,'baseline.json'))) return {status:'no-baseline',changes:[],protectedChanges:[]};
|
|
285
|
+
const baseline=json(inside(directory,'baseline.json')); const folder=inside(directory,baseline.directory); const saved=json(path.join(folder,'manifest.json'));
|
|
286
|
+
if(hash(fs.readFileSync(path.join(folder,'review.json')))!==baseline.reviewDigest || saved.digest!==baseline.digest || hash(JSON.stringify({identity:saved.identity,files:saved.files}))!==saved.digest || Object.entries(saved.files).some(([f,d])=>!fs.existsSync(inside(folder,f))||hash(fs.readFileSync(inside(folder,f)))!==d)) return {status:'corrupt-baseline'};
|
|
287
|
+
const current=manifest(directory); const changes=[...new Set([...Object.keys(saved.files),...Object.keys(current.files)])].filter(f=>saved.files[f]!==current.files[f]);
|
|
288
|
+
return {status:current.digest===saved.digest?'unchanged':'changed',changes,protectedChanges:changes,revision:current.revision,baselineRevision:saved.revision};
|
|
289
|
+
}
|
|
290
|
+
function state(directory,options={}) { return require('./native-delivery.cjs').state(directory,options,module.exports); }
|
|
291
|
+
function finalize(directory,options={}) { return require('./native-delivery.cjs').finalize(directory,options,module.exports); }
|
|
292
|
+
module.exports={analyze,prepareTemplate,createPrototype,manifest,inspect,inspectDesignReview,effectiveReview,claim,release,revise,replan,synchronizeIndex,build,verifyBuild,preview,recordRun,confirmBaseline,compareBaseline,state,finalize};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const path=require('node:path');
|
|
4
|
+
const fs=require('node:fs');
|
|
5
|
+
const lib=require('./native-pilot-lib.cjs');
|
|
6
|
+
async function main(argv=process.argv.slice(2)) {
|
|
7
|
+
const operation=argv.shift()||'analyze'; const positional=[]; const options={};
|
|
8
|
+
while(argv.length) { const value=argv.shift(); if(value==='--') { options.command=argv.splice(0); break; } if(value.startsWith('--')) { const key=value.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); options[key]=!argv.length||argv[0].startsWith('--')?true:argv.shift(); } else positional.push(value); }
|
|
9
|
+
const root=positional[0]?path.resolve(positional[0]):null;
|
|
10
|
+
switch(operation) {
|
|
11
|
+
case 'analyze': return lib.analyze(options);
|
|
12
|
+
case 'paths': { const context=require('./prototype-locations.cjs').resolveLocations(options); return {...require('./project-registry.cjs').paths(options.project,options.id),host:context.host,scope:context.scope,projectKey:context.projectKey,toolsDirectory:context.toolsDirectory,cacheDirectory:context.cacheDirectory}; }
|
|
13
|
+
case 'index': return require('./project-registry.cjs').query(options);
|
|
14
|
+
case 'sync-index': return lib.synchronizeIndex(options);
|
|
15
|
+
case 'migrate-index': return require('./project-registry.cjs').migrate(options);
|
|
16
|
+
case 'browser-cache': return require('./browser-workspace.cjs').prepare(options);
|
|
17
|
+
case 'browser-cli': return require('./browser-workspace.cjs').runCli(options);
|
|
18
|
+
case 'archive-browser-output': return require('./browser-workspace.cjs').archive(options);
|
|
19
|
+
case 'prepare-template': return lib.prepareTemplate(options);
|
|
20
|
+
case 'sync-template': { const templates=require('./template-contract.cjs'); return options.check?templates.loadTemplate(root).index:templates.syncTemplate(root); }
|
|
21
|
+
case 'template-page': { const templates=require('./template-contract.cjs'); return require('./content-template.cjs').pageContent(root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),options.page); }
|
|
22
|
+
case 'generation-options': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),meta=require('./generation-contract.cjs').resource(snapshot,'generation.json');return {status:'passed',readOnly:true,template:snapshot.id,...meta,patterns:require('./generation-contract.cjs').definition().patterns}; }
|
|
23
|
+
case 'generation-plan': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template);const contract=require('./generation-contract.cjs');return contract.view(contract.compile(snapshot,JSON.parse(fs.readFileSync(path.resolve(options.scope),'utf8'))),options); }
|
|
24
|
+
case 'inventory': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),value=require('./template-inventory.cjs').verifySnapshot(snapshot);return options.details?value:{status:'passed',readOnly:true,template:snapshot.id,files:Object.keys(value.files).length,routes:value.routes.length,pages:value.pages.length,fragments:value.fragments.length,components:Object.keys(value.components).length,events:value.eventCount}; }
|
|
25
|
+
case 'sync-generation-assets': return require('./generation-assets.cjs').refresh(path.resolve(__dirname,'..'),{check:!!options.check});
|
|
26
|
+
case 'create': return lib.createPrototype(options);
|
|
27
|
+
case 'execution-begin': return require('./execution-runtime.cjs').begin(root,lib);
|
|
28
|
+
case 'execution-assertions-template': return require('./execution-assertions.cjs').template(options);
|
|
29
|
+
case 'execution-template': return require('./execution-runtime.cjs').template(root,lib);
|
|
30
|
+
case 'execution-check': return require('./execution-runtime.cjs').check(root,options.stage||'delivery',lib);
|
|
31
|
+
case 'manifest': return lib.manifest(root);
|
|
32
|
+
case 'inspect': return lib.inspect(root,{acceptance:!!options.acceptance});
|
|
33
|
+
case 'state': return lib.state(root,options);
|
|
34
|
+
case 'browser': return require('./native-browser.cjs').main(root,options);
|
|
35
|
+
case 'record-devtools-review': return require('./native-devtools.cjs').record(root,JSON.parse(fs.readFileSync(path.resolve(options.report),'utf8')),lib);
|
|
36
|
+
case 'finalize': return lib.finalize(root,options);
|
|
37
|
+
case 'build': return lib.build(root,options);
|
|
38
|
+
case 'preview': { const running=await lib.preview(root,options); process.stdout.write(JSON.stringify({url:running.url,buildId:running.build.buildId})+'\n'); return null; }
|
|
39
|
+
case 'revise': return lib.revise(root,options);
|
|
40
|
+
case 'replan': return lib.replan(root,options);
|
|
41
|
+
case 'claim': return lib.claim(root,options);
|
|
42
|
+
case 'release': return lib.release(root,options.owner);
|
|
43
|
+
case 'record-run': return lib.recordRun(root,JSON.parse(fs.readFileSync(path.resolve(options.report),'utf8')));
|
|
44
|
+
case 'compare-baseline': return lib.compareBaseline(root);
|
|
45
|
+
case 'confirm-baseline': return lib.confirmBaseline(root,JSON.parse(fs.readFileSync(path.resolve(options.confirmation),'utf8')));
|
|
46
|
+
default: throw new Error(`Unknown operation: ${operation}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if(require.main===module) main().then(result=>{if(result!==null){process.stdout.write(JSON.stringify(result,null,2)+'\n');if(result.status==='failed')process.exitCode=1;}}).catch(error=>{process.stderr.write(JSON.stringify({status:'failed',operation:process.argv[2]||'analyze',errors:[error.message]})+'\n');process.exitCode=1;});
|
|
50
|
+
module.exports={main};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
const sources = require('../assets/optional-skills/sources.json');
|
|
7
|
+
|
|
8
|
+
function selection(value = 'none') {
|
|
9
|
+
if (typeof value !== 'string') throw new Error('Optional Skill selection must be a string');
|
|
10
|
+
const names = [...new Set(value.split(',').map(name => name.trim()))];
|
|
11
|
+
if (names.length === 1 && names[0] === 'none') return [];
|
|
12
|
+
if (!names.length || names.some(name => !['uiux', 'antdv'].includes(name))) throw new Error('Choose none, uiux, antdv or uiux,antdv');
|
|
13
|
+
return names;
|
|
14
|
+
}
|
|
15
|
+
function safePath(project, relative) {
|
|
16
|
+
let current = project;
|
|
17
|
+
for (const part of relative.split('/')) {
|
|
18
|
+
if (!part || part === '..') throw new Error('Invalid install path');
|
|
19
|
+
current = path.join(current, part);
|
|
20
|
+
try { if (fs.lstatSync(current).isSymbolicLink()) throw new Error(`Install path contains a symlink: ${current}`); }
|
|
21
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
22
|
+
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
25
|
+
function verify(data, expected, algorithm = 'sha256', encoding = 'hex') {
|
|
26
|
+
if (crypto.createHash(algorithm).update(data).digest(encoding) !== expected) throw new Error('Official download integrity mismatch');
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
async function download(url, expected, algorithm, encoding) {
|
|
30
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(60000) });
|
|
31
|
+
if (!response.ok) throw new Error(`Official download failed: HTTP ${response.status}`);
|
|
32
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
33
|
+
if (data.length > 50 * 1024 * 1024) throw new Error('Official download exceeds size limit');
|
|
34
|
+
return verify(data, expected, algorithm, encoding);
|
|
35
|
+
}
|
|
36
|
+
function npmEntry() {
|
|
37
|
+
const directory = path.dirname(process.execPath);
|
|
38
|
+
const candidates = [process.env.npm_execpath, path.join(directory, '../lib/node_modules/npm/bin/npm-cli.js'), path.join(directory, 'node_modules/npm/bin/npm-cli.js')];
|
|
39
|
+
for (const folder of (process.env.PATH || '').split(path.delimiter)) {
|
|
40
|
+
const entry = path.join(folder, 'npm');
|
|
41
|
+
if (fs.existsSync(entry)) candidates.push(fs.realpathSync(entry));
|
|
42
|
+
}
|
|
43
|
+
const found = candidates.find(file => file && path.basename(file) === 'npm-cli.js' && fs.existsSync(file));
|
|
44
|
+
if (!found) throw new Error('Cannot locate npm-cli.js; use a standard Node.js/npm installation');
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
function run(entry, args, cwd, env) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn(process.execPath, [entry, ...args], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
50
|
+
let output = '', failed = false;
|
|
51
|
+
const timer = setTimeout(() => { failed = true; child.kill(); reject(new Error('Official installer timed out')); }, 180000);
|
|
52
|
+
for (const stream of [child.stdout, child.stderr]) stream.on('data', chunk => {
|
|
53
|
+
output += chunk;
|
|
54
|
+
if (output.length > 5 * 1024 * 1024) { failed = true; child.kill(); reject(new Error('Official installer output exceeds limit')); }
|
|
55
|
+
});
|
|
56
|
+
child.once('error', error => { clearTimeout(timer); failed = true; reject(error); });
|
|
57
|
+
child.once('close', code => { clearTimeout(timer); if (!failed) code === 0 ? resolve() : reject(new Error(`Official installer failed (${code}): ${output.slice(-1500)}`)); });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function validateSkill(folder, name) {
|
|
61
|
+
const entry = fs.readFileSync(path.join(folder, 'SKILL.md'), 'utf8');
|
|
62
|
+
if (!new RegExp(`^name:\\s*["']?${name}["']?\\s*$`, 'm').test(entry)) throw new Error(`Official Skill identity mismatch: ${name}`);
|
|
63
|
+
function visit(directory) {
|
|
64
|
+
for (const item of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
65
|
+
if (item.isSymbolicLink()) throw new Error('Official Skill contains a symbolic link');
|
|
66
|
+
if (item.isDirectory()) visit(path.join(directory, item.name));
|
|
67
|
+
else if (!item.isFile()) throw new Error('Official Skill contains an unsupported file');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
visit(folder);
|
|
71
|
+
if (name === 'ui-ux-pro-max') for (const file of ['scripts/search.py', 'scripts/core.py', 'scripts/design_system.py', 'data/ux-guidelines.csv']) {
|
|
72
|
+
if (!fs.existsSync(path.join(folder, file))) throw new Error(`Incomplete UIUX resources: ${file}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function uiuxPaths(packageRoot, skill, context, platform) {
|
|
76
|
+
const templates = path.join(packageRoot, 'assets/templates');
|
|
77
|
+
const config = JSON.parse(fs.readFileSync(path.join(templates, 'platforms', `${platform}.json`), 'utf8'));
|
|
78
|
+
const template = fs.readFileSync(path.join(templates, 'base/skill-content.md'), 'utf8');
|
|
79
|
+
const scriptFile = path.join(context.destination, 'scripts/search.py');
|
|
80
|
+
const script = (context.scope === 'global' ? scriptFile : path.relative(context.project, scriptFile)).split(path.sep).join('/');
|
|
81
|
+
const command = /^[a-zA-Z0-9_./:-]+$/.test(script) ? script : process.platform === 'win32' ? "'" + script.replaceAll("'", "''") + "'" : "'" + script.replaceAll("'", "'\\''") + "'";
|
|
82
|
+
const parameters = { TITLE: config.title, DESCRIPTION: config.description, SCRIPT_PATH: command, SKILL_OR_WORKFLOW: config.skillOrWorkflow,
|
|
83
|
+
QUICK_REFERENCE: config.sections.quickReference ? '\n' + fs.readFileSync(path.join(templates, 'base/quick-reference.md'), 'utf8') : '' };
|
|
84
|
+
const file = path.join(skill, 'SKILL.md'), original = fs.readFileSync(file, 'utf8');
|
|
85
|
+
const header = original.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/)?.[0];
|
|
86
|
+
if (!header) throw new Error('Official UIUX frontmatter is missing');
|
|
87
|
+
// Only official template placeholders are parameterized. The downloaded
|
|
88
|
+
// template and CLI code remain unchanged; paths work in every supported host.
|
|
89
|
+
const body = template.replace(/\{\{([A-Z_]+)\}\}/g, (match, key) => {
|
|
90
|
+
if (!Object.hasOwn(parameters, key)) throw new Error(`Unknown official UIUX placeholder: ${key}`);
|
|
91
|
+
return parameters[key];
|
|
92
|
+
});
|
|
93
|
+
fs.writeFileSync(file, header + body);
|
|
94
|
+
return { platform, script, command, templateSha256: crypto.createHash('sha256').update(template).digest('hex') };
|
|
95
|
+
}
|
|
96
|
+
async function prepareOfficial(name, stage, context) {
|
|
97
|
+
const source = sources[name], tool = path.join(stage, 'tool'), workspace = path.join(stage, 'workspace');
|
|
98
|
+
fs.mkdirSync(tool, { recursive: true }); fs.mkdirSync(workspace);
|
|
99
|
+
const archive = await download(source.tarball, source.integrity.slice(7), 'sha512', 'base64');
|
|
100
|
+
fs.writeFileSync(path.join(tool, 'source.tgz'), archive);
|
|
101
|
+
const env = { ...process.env, npm_config_cache: path.join(stage, 'npm-cache'), npm_config_offline: 'false', NPM_CONFIG_OFFLINE: 'false', npm_config_update_notifier: 'false', ANTDV_NO_AUTO_REPORT: '1' };
|
|
102
|
+
await run(npmEntry(), ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--package-lock', './source.tgz'], tool, env);
|
|
103
|
+
const packageRoot = path.join(tool, 'node_modules', source.packageName), pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
104
|
+
if (pkg.name !== source.packageName || pkg.version !== source.version) throw new Error('Official CLI version mismatch');
|
|
105
|
+
let skill, pathParameters;
|
|
106
|
+
if (name === 'uiux') {
|
|
107
|
+
const platform = ['codebuddy', 'workbuddy'].includes(context.host) ? 'codebuddy' : context.host === 'codex' ? 'codex' : context.host === 'claude-code' ? 'claude' : 'universal';
|
|
108
|
+
await run(path.join(packageRoot, pkg.bin.uipro), ['init', '--ai', platform, '--offline'], workspace, env);
|
|
109
|
+
const config = JSON.parse(fs.readFileSync(path.join(packageRoot, 'assets/templates/platforms', `${platform}.json`), 'utf8'));
|
|
110
|
+
skill = path.join(workspace, config.folderStructure.root, config.folderStructure.skillPath);
|
|
111
|
+
pathParameters = uiuxPaths(packageRoot, skill, context, platform);
|
|
112
|
+
} else {
|
|
113
|
+
skill = path.join(workspace, 'antdv'); fs.mkdirSync(skill);
|
|
114
|
+
fs.writeFileSync(path.join(skill, 'SKILL.md'), await download(source.skillUrl, source.skillSha256));
|
|
115
|
+
}
|
|
116
|
+
fs.writeFileSync(path.join(skill, 'LICENSE'), await download(source.licenseUrl, source.licenseSha256));
|
|
117
|
+
const receipt = JSON.stringify({ ...source, installedAt: new Date().toISOString(), installer: 'i3 optional dependency preparation; official templates retained',
|
|
118
|
+
...(pathParameters ? { pathParameters } : {}) }, null, 2) + '\n';
|
|
119
|
+
fs.writeFileSync(path.join(skill, 'UPSTREAM-ORIGIN.json'), receipt);
|
|
120
|
+
if (name === 'antdv') fs.writeFileSync(path.join(tool, 'UPSTREAM-ORIGIN.json'), receipt);
|
|
121
|
+
return { skill, tool };
|
|
122
|
+
}
|
|
123
|
+
async function install(options = {}, services = {}) {
|
|
124
|
+
if (options.provider !== undefined || options.tool !== undefined) throw new Error('--provider and --tool are for read-only check');
|
|
125
|
+
const deps = require('./prototype-dependencies.cjs');
|
|
126
|
+
const names = selection(options.skills), before = deps.check(options), context = before.locations;
|
|
127
|
+
if (options.migrateTools && (context.scope !== 'project' || !names.includes('antdv'))) throw new Error('--migrate-tools requires project scope and selected antdv');
|
|
128
|
+
if (!names.length) return { status: 'skipped', selected: [], detection: before };
|
|
129
|
+
const pending = [], preserved = [];
|
|
130
|
+
const safe = file => safePath(path.parse(file).root, path.relative(path.parse(file).root, file).split(path.sep).join('/'));
|
|
131
|
+
for (const name of names) {
|
|
132
|
+
const source = sources[name], destination = safe(path.join(context.skillsDirectory, source.skillName));
|
|
133
|
+
const available = name === 'uiux' ? before.uiux.availability === 'available' : before.antdv.skill.availability === 'available';
|
|
134
|
+
const needsSkill = !available;
|
|
135
|
+
if (needsSkill && fs.existsSync(destination)) throw new Error(`Existing Skill preserved; repair or choose its actual provider path: ${destination}`);
|
|
136
|
+
const toolDestination = name === 'antdv' ? safe(path.join(context.toolsDirectory, 'antdv-cli')) : null;
|
|
137
|
+
if (name === 'antdv' && before.antdv.tool.availability === 'unusable') throw new Error(`Existing tool unusable; preserved: ${before.antdv.tool.root}`);
|
|
138
|
+
const needsTool = name === 'antdv' && before.antdv.tool.availability === 'missing' && !fs.existsSync(toolDestination);
|
|
139
|
+
if (name === 'antdv' && before.antdv.tool.availability === 'missing' && fs.existsSync(toolDestination)) throw new Error(`Existing tool directory preserved: ${toolDestination}`);
|
|
140
|
+
let migration;
|
|
141
|
+
if (name === 'antdv' && options.migrateTools && fs.existsSync(path.join(context.project, '.prototype-tools'))) {
|
|
142
|
+
migration = safe(path.join(context.project, '.prototype-tools/antdv-cli'));
|
|
143
|
+
if (!fs.existsSync(migration)) {
|
|
144
|
+
if (fs.existsSync(path.join(context.project, '.prototype-tools/node_modules/@antdv-next/cli'))) throw new Error('Legacy mixed tools have no independent i3 ownership; preserved');
|
|
145
|
+
migration = undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (migration) {
|
|
149
|
+
let receipt;
|
|
150
|
+
try { receipt = JSON.parse(fs.readFileSync(path.join(migration, 'UPSTREAM-ORIGIN.json'), 'utf8')); } catch { /* handled below */ }
|
|
151
|
+
if (!receipt?.installer?.startsWith('i3 optional dependency preparation') || receipt.packageName !== sources.antdv.packageName || receipt.version !== sources.antdv.version || receipt.integrity !== sources.antdv.integrity) throw new Error('Legacy tool has no verified i3 ownership; preserved');
|
|
152
|
+
if (deps.antdvTool(context.project, { ...options, host: context.host, scope: context.scope, tool: path.join(migration, 'node_modules/@antdv-next/cli') }).availability !== 'available') throw new Error('Legacy tool query failed; preserved');
|
|
153
|
+
if (fs.existsSync(toolDestination)) throw new Error(`Existing tool directory preserved: ${toolDestination}`);
|
|
154
|
+
}
|
|
155
|
+
if (needsSkill || needsTool || migration) pending.push({ name, destination, needsSkill, toolDestination, needsTool, migration });
|
|
156
|
+
if (!needsSkill) preserved.push(name);
|
|
157
|
+
}
|
|
158
|
+
if (!pending.length) return { status: 'reused', selected: names, preserved, detection: before };
|
|
159
|
+
const tools = safe(context.toolsDirectory), committed = [], created = [];
|
|
160
|
+
const lock = path.join(tools, '.i3-optional-install.lock');
|
|
161
|
+
let descriptor, stage, result;
|
|
162
|
+
const mkdir = directory => { if (!fs.existsSync(directory)) { mkdir(path.dirname(directory)); fs.mkdirSync(directory); created.push(directory); } };
|
|
163
|
+
try {
|
|
164
|
+
mkdir(tools); descriptor = fs.openSync(lock, 'wx');
|
|
165
|
+
stage = fs.mkdtempSync(path.join(tools, '.i3-optional-stage-'));
|
|
166
|
+
const moves = [];
|
|
167
|
+
for (const item of pending) {
|
|
168
|
+
if (item.needsSkill || item.needsTool) {
|
|
169
|
+
(services.progress || (message => process.stderr.write(message + '\n')))(`准备官方依赖:${item.name}(${context.host} / ${context.scope})`);
|
|
170
|
+
const folder = path.join(stage, item.name); fs.mkdirSync(folder);
|
|
171
|
+
const prepared = await (services.prepare || prepareOfficial)(item.name, folder, { ...context, destination: item.destination });
|
|
172
|
+
if (item.needsSkill) { validateSkill(prepared.skill, sources[item.name].skillName); moves.push({ from: prepared.skill, to: item.destination }); }
|
|
173
|
+
if (item.needsTool) moves.push({ from: prepared.tool, to: item.toolDestination });
|
|
174
|
+
}
|
|
175
|
+
if (item.migration) moves.push({ from: item.migration, to: item.toolDestination, migration: true });
|
|
176
|
+
}
|
|
177
|
+
for (const move of moves) {
|
|
178
|
+
const { from, to } = move; safe(from); safe(to);
|
|
179
|
+
if (fs.existsSync(to)) throw new Error(`Destination appeared during install; preserved: ${to}`);
|
|
180
|
+
mkdir(path.dirname(to)); fs.renameSync(from, to); committed.push(move);
|
|
181
|
+
}
|
|
182
|
+
const detection = deps.check(options);
|
|
183
|
+
if (names.includes('antdv') && detection.antdv.tool.availability !== 'available') throw new Error('Installed or migrated antDv tool query failed');
|
|
184
|
+
const migrated = committed.filter(item => item.migration).map(item => ({ from: item.from, to: item.to }));
|
|
185
|
+
result = { status: migrated.length && pending.every(item => !item.needsSkill && !item.needsTool) ? 'migrated' : 'installed', selected: names, preserved,
|
|
186
|
+
installed: pending.filter(item => item.needsSkill || item.needsTool).map(item => item.name), migrated, detection,
|
|
187
|
+
note: 'Reload the selected host or open a new session if needed. Installation/probing does not prove Skill invocation. Existing prototypes are unchanged.' };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
for (const move of committed.reverse()) {
|
|
190
|
+
if (move.migration) fs.renameSync(move.to, move.from);
|
|
191
|
+
else fs.rmSync(move.to, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
} finally {
|
|
195
|
+
if (stage) fs.rmSync(stage, { recursive: true, force: true });
|
|
196
|
+
if (descriptor !== undefined) { fs.closeSync(descriptor); fs.unlinkSync(lock); }
|
|
197
|
+
for (const directory of created.reverse()) if (fs.existsSync(directory) && !fs.readdirSync(directory).length) fs.rmdirSync(directory);
|
|
198
|
+
}
|
|
199
|
+
const legacy = path.join(context.project, '.prototype-tools');
|
|
200
|
+
if (result.migrated.length && !fs.readdirSync(legacy).length) fs.rmdirSync(legacy);
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
module.exports = { install, selection, safePath, verify, validateSkill };
|