@givolt/i3-composer-agent 2.3.0 → 2.3.2
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 +12 -7
- package/bin/i3-composer-agent.cjs +33 -2
- package/bin/install.cjs +14 -0
- package/bin/native-migration.cjs +27 -3
- package/commands/i3-prototype.toml +5 -7
- package/docs/codebuddy-setup.md +65 -2
- package/docs/codex-setup.md +4 -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 +91 -0
- package/docs/workbuddy-setup.md +4 -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/package.json +35 -1
- package/plugin.json +1 -1
- package/scripts/design-provider-discovery-test.js +190 -0
- package/scripts/design-provider-test.js +75 -0
- package/scripts/enterprise-template-test.js +67 -1
- package/scripts/lib/prototype-fixture.cjs +17 -0
- package/scripts/prototype-locations-test.js +58 -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-native-test.js +504 -0
- package/scripts/ui-prototype-template-test.js +261 -0
- package/scripts/ui-prototype-test.js +10 -0
- package/skills/i3-ui-prototype/SKILL.md +142 -23
- 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/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 +51 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +18 -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 +13 -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/optional-skills/host-targets.json +737 -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 +3 -2
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/design-system.md +3 -3
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/manifest.json +1 -1
- 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.json +9 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +6 -6
- package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
- package/skills/i3-ui-prototype/assets/templates/serve-gallery.cjs +7 -0
- package/skills/i3-ui-prototype/references/artifact-contract.md +25 -0
- package/skills/i3-ui-prototype/references/content-template.md +76 -0
- package/skills/i3-ui-prototype/references/enterprise-admin.md +9 -3
- package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
- package/skills/i3-ui-prototype/references/expert-adapter.md +39 -5
- package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +165 -0
- package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
- package/skills/i3-ui-prototype/references/optional-skills.md +59 -0
- package/skills/i3-ui-prototype/references/template-adapters.md +1 -1
- package/skills/i3-ui-prototype/references/workflow-contract.md +58 -4
- package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +3 -3
- package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
- package/skills/i3-ui-prototype/scripts/design-provider-discovery.cjs +128 -0
- package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +30 -14
- 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 +91 -0
- package/skills/i3-ui-prototype/scripts/native-devtools.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +244 -0
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +34 -0
- package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
- package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +50 -26
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +65 -0
- package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
- package/skills/i3-ui-prototype/scripts/template-contract.cjs +398 -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,34 @@
|
|
|
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.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 'prepare-template': return lib.prepareTemplate(options);
|
|
13
|
+
case 'sync-template': { const templates=require('./template-contract.cjs'); return options.check?templates.loadTemplate(root).index:templates.syncTemplate(root); }
|
|
14
|
+
case 'template-page': { const templates=require('./template-contract.cjs'); return require('./content-template.cjs').pageContent(root?templates.loadTemplate(root):templates.loadBundledTemplate(),options.page); }
|
|
15
|
+
case 'create': return lib.createPrototype(options);
|
|
16
|
+
case 'manifest': return lib.manifest(root);
|
|
17
|
+
case 'inspect': return lib.inspect(root,{acceptance:!!options.acceptance});
|
|
18
|
+
case 'state': return lib.state(root,options);
|
|
19
|
+
case 'browser': return require('./native-browser.cjs').main(root,options);
|
|
20
|
+
case 'record-devtools-review': return require('./native-devtools.cjs').record(root,JSON.parse(fs.readFileSync(path.resolve(options.report),'utf8')),lib);
|
|
21
|
+
case 'finalize': return lib.finalize(root,options);
|
|
22
|
+
case 'build': return lib.build(root,options);
|
|
23
|
+
case 'preview': { const running=await lib.preview(root,options); process.stdout.write(JSON.stringify({url:running.url,buildId:running.build.buildId})+'\n'); return null; }
|
|
24
|
+
case 'revise': return lib.revise(root,options);
|
|
25
|
+
case 'claim': return lib.claim(root,options);
|
|
26
|
+
case 'release': return lib.release(root,options.owner);
|
|
27
|
+
case 'record-run': return lib.recordRun(root,JSON.parse(fs.readFileSync(path.resolve(options.report),'utf8')));
|
|
28
|
+
case 'compare-baseline': return lib.compareBaseline(root);
|
|
29
|
+
case 'confirm-baseline': return lib.confirmBaseline(root,JSON.parse(fs.readFileSync(path.resolve(options.confirmation),'utf8')));
|
|
30
|
+
default: throw new Error(`Unknown operation: ${operation}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
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;});
|
|
34
|
+
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 };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const { checkDesignProvider } = require('./prototype-lib.cjs');
|
|
7
|
+
const { discoverProviderCandidates } = require('./design-provider-discovery.cjs');
|
|
8
|
+
const { resolveLocations, scopeBases, toolCandidates } = require('./prototype-locations.cjs');
|
|
9
|
+
|
|
10
|
+
function skillAt(project, name, options = {}) {
|
|
11
|
+
const context = resolveLocations({ ...options, project });
|
|
12
|
+
const candidates = scopeBases(context).map(([root, scope]) => [path.join(root, 'skills', name), `${scope}-skill`]);
|
|
13
|
+
const legacyConfig = (options.env || process.env).CODEBUDDY_CONFIG_DIR;
|
|
14
|
+
if (context.host === 'codebuddy' && legacyConfig) candidates.push([path.resolve(legacyConfig, 'skills', name), 'global-skill']);
|
|
15
|
+
const found = candidates.find(([root]) => fs.existsSync(path.join(root, 'SKILL.md')));
|
|
16
|
+
return found ? { availability: 'available', root: found[0], source: found[1], invocationVerified: false }
|
|
17
|
+
: { availability: 'missing', searched: candidates.map(([root]) => root), invocationVerified: false };
|
|
18
|
+
}
|
|
19
|
+
function antdvTool(project, options = {}) {
|
|
20
|
+
const candidates = toolCandidates(resolveLocations({ ...options, project }), options);
|
|
21
|
+
const found = candidates.find(item => fs.existsSync(item.root));
|
|
22
|
+
if (!found) return { availability: 'missing', searched: candidates.map(item => item.root), componentVersion: '1.5.3' };
|
|
23
|
+
const { root, source } = found;
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
26
|
+
if (pkg.name !== '@antdv-next/cli' || typeof pkg.bin?.antdv !== 'string') throw new Error('Unexpected antDv CLI package');
|
|
27
|
+
const entry = path.resolve(root, pkg.bin.antdv), relative = path.relative(root, entry);
|
|
28
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('CLI entry escapes its package');
|
|
29
|
+
const args = [entry, 'info', 'Table', '--ver', '1.5.2', '--format', 'json'];
|
|
30
|
+
const result = spawnSync(process.execPath, args, { cwd: project, env: { ...(options.env || process.env), ANTDV_NO_AUTO_REPORT: '1' }, encoding: 'utf8', timeout: 15000, maxBuffer: 5 * 1024 * 1024 });
|
|
31
|
+
if (result.status !== 0) throw new Error(result.error?.message || result.stderr || 'CLI query failed');
|
|
32
|
+
const data = JSON.parse(result.stdout);
|
|
33
|
+
if (!data || data.name !== 'Table' || !Array.isArray(data.props) || data.error) throw new Error('CLI returned no Table knowledge');
|
|
34
|
+
return { availability: 'available', root, source, entry, version: pkg.version, command: [process.execPath, ...args], knowledgeVersion: '1.5.2', componentVersion: '1.5.3', exactKnowledgeMatch: false,
|
|
35
|
+
limitation: 'CLI knowledge 1.5.2 is reference only; verify target 1.5.3 package types/source.' };
|
|
36
|
+
} catch (error) { return { availability: 'unusable', root, source, reason: error.message }; }
|
|
37
|
+
}
|
|
38
|
+
function check(options = {}) {
|
|
39
|
+
const locations = resolveLocations(options), { project, host, scope } = locations;
|
|
40
|
+
const roots = scopeBases(locations).map(([base]) => path.join(base, 'skills/ui-ux-pro-max'));
|
|
41
|
+
const sources = new Map(scopeBases(locations).map(([base, scope]) => [path.join(base, 'skills/ui-ux-pro-max'), `${scope}-skill`]));
|
|
42
|
+
let discovery;
|
|
43
|
+
if (options.provider === undefined) {
|
|
44
|
+
const aliases = { 'gemini-cli': 'gemini', 'claude-code': 'claude' };
|
|
45
|
+
// Resolve global registrations from home so project-scoped plugins from the
|
|
46
|
+
// business workspace are never reused by a global installation.
|
|
47
|
+
discovery = discoverProviderCandidates({ ...options, project: scope === 'global' ? locations.home : project, host: aliases[host] || host });
|
|
48
|
+
for (const item of discovery.candidates) { roots.push(item.root); if (!sources.has(item.root)) sources.set(item.root, item.source); }
|
|
49
|
+
}
|
|
50
|
+
const uiux = checkDesignProvider({ ...options, project, host, ...(options.provider === undefined ? { providerRoots: [...new Set(roots)] } : {}) });
|
|
51
|
+
if (discovery) {
|
|
52
|
+
uiux.diagnostics.push(...discovery.diagnostics);
|
|
53
|
+
if (uiux.root) uiux.source = sources.get(uiux.root) || uiux.source;
|
|
54
|
+
for (const attempt of uiux.attempts) attempt.source = sources.get(attempt.root) || attempt.source;
|
|
55
|
+
const failure = ['ambiguous', 'disabled', 'discovery-error'].map(status => discovery.diagnostics.find(item => item.availability === status)).find(Boolean);
|
|
56
|
+
if (uiux.availability === 'missing' && failure) Object.assign(uiux, { availability: failure.availability, reason: failure.reason });
|
|
57
|
+
}
|
|
58
|
+
const scoped = { ...options, host, scope };
|
|
59
|
+
const antdv = { skill: skillAt(project, 'antdv', scoped), tool: antdvTool(project, scoped) };
|
|
60
|
+
return { status: 'checked', host, scope, project, locations, uiux, antdv, optional: true, professionalReady: uiux.availability === 'available',
|
|
61
|
+
invocationVerified: false, note: 'Read-only availability probe; the selected host must discover and actually invoke Skills. No dependency installation, migration or prototype changes.' };
|
|
62
|
+
}
|
|
63
|
+
function parse(argv) {
|
|
64
|
+
const operation = argv.shift() || 'check', options = {};
|
|
65
|
+
if (!['check', 'install'].includes(operation)) throw new Error('Use check or install');
|
|
66
|
+
const flags = new Set(['project', 'host', 'scope', 'skills', 'provider', 'python', 'tool']);
|
|
67
|
+
while (argv.length) {
|
|
68
|
+
const key = argv.shift();
|
|
69
|
+
if (key === '--migrate-tools' && operation === 'install') { options.migrateTools = true; continue; }
|
|
70
|
+
if (!key.startsWith('--') || !flags.has(key.slice(2)) || !argv[0] || argv[0].startsWith('--')) throw new Error(`Invalid option: ${key}`);
|
|
71
|
+
options[key.slice(2)] = argv.shift();
|
|
72
|
+
}
|
|
73
|
+
return { operation, options };
|
|
74
|
+
}
|
|
75
|
+
async function main(argv = process.argv.slice(2)) {
|
|
76
|
+
const { operation, options } = parse([...argv]);
|
|
77
|
+
return operation === 'check' ? check(options) : require('./optional-skill-install.cjs').install(options);
|
|
78
|
+
}
|
|
79
|
+
module.exports = { check, skillAt, antdvTool, parse, main };
|
|
80
|
+
if (require.main === module) main().then(result => process.stdout.write(JSON.stringify(result, null, 2) + '\n'))
|
|
81
|
+
.catch(error => { process.stderr.write(JSON.stringify({ status: 'failed', errors: [error.message] }) + '\n'); process.exitCode = 1; });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const os = require('node:os');
|
|
5
4
|
const crypto = require('node:crypto');
|
|
6
5
|
const { spawnSync } = require('node:child_process');
|
|
7
6
|
const templateRegistry = require('./template-adapters.cjs').createTemplateRegistry();
|
|
7
|
+
const { discoverProviderCandidates } = require('./design-provider-discovery.cjs');
|
|
8
8
|
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
9
9
|
const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
10
10
|
function ensure(condition, message) { if (!condition) throw new Error(message); }
|
|
@@ -47,46 +47,55 @@ function tokensCss(tokens) {
|
|
|
47
47
|
ensure(entries.length && entries.every(([key, value]) => /^[a-z][a-z0-9-]*$/.test(key) && typeof value === 'string' && !/[;{}<>\n\r]/.test(value)), 'Invalid design token values');
|
|
48
48
|
return ':root {\n' + entries.map(([key, value]) => ` --${key}: ${value};`).join('\n') + '\n}\n';
|
|
49
49
|
}
|
|
50
|
-
function providerRoots(options = {}) {
|
|
51
|
-
if (options.providerRoots) return options.providerRoots;
|
|
52
|
-
if (options.provider) return [options.provider];
|
|
53
|
-
if (options.discover === false) return [];
|
|
54
|
-
const homes = { codex: '.agents', workbuddy: '.workbuddy', codebuddy: '.codebuddy', 'claude-code': '.claude', gemini: '.gemini', cursor: '.cursor' };
|
|
55
|
-
const folder = homes[options.host];
|
|
56
|
-
if (!folder) return [];
|
|
57
|
-
return [options.project && path.join(options.project, folder, 'skills/ui-ux-pro-max'), path.join(os.homedir(), folder, 'skills/ui-ux-pro-max')].filter(Boolean);
|
|
58
|
-
}
|
|
59
50
|
function providerVersion(root) {
|
|
51
|
+
const receipt = path.join(root, 'UPSTREAM-ORIGIN.json');
|
|
52
|
+
if (fs.existsSync(receipt)) {
|
|
53
|
+
try { const source = json(receipt); if (source.packageName === 'ui-ux-pro-max-cli' && typeof source.version === 'string') return source.version; } catch {}
|
|
54
|
+
}
|
|
60
55
|
for (let folder = root, i = 0; i < 5; i++, folder = path.dirname(folder)) {
|
|
61
|
-
for (const name of ['skill.json', '.claude-plugin/plugin.json', 'package.json']) {
|
|
56
|
+
for (const name of ['skill.json', '.claude-plugin/plugin.json', '.codebuddy-plugin/plugin.json', 'package.json']) {
|
|
62
57
|
const file = path.join(folder, name);
|
|
63
|
-
if (fs.existsSync(file)) {
|
|
58
|
+
if (fs.existsSync(file)) {
|
|
59
|
+
try {
|
|
60
|
+
const metadata = json(file);
|
|
61
|
+
if (name === 'package.json' && !['ui-ux-pro-max', 'ui-ux-pro-max-cli'].includes(metadata.name)) continue;
|
|
62
|
+
if (metadata.version) return metadata.version;
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
return 'unversioned';
|
|
67
68
|
}
|
|
68
69
|
function queryProvider(root, query, options = {}) {
|
|
69
70
|
const mode = options.stack ? ['--stack', options.stack] : options.designSystem ? ['--design-system'] : ['--domain', options.domain || 'ux'];
|
|
70
|
-
const run = spawnSync(options.python || 'python3', ['-B', path.join(root, 'scripts/search.py'), query, ...mode, '--json'], { encoding: 'utf8', timeout: 15000, maxBuffer: 5 * 1024 * 1024 });
|
|
71
|
-
ensure(run.status === 0, `UI/UX Pro Max query unavailable: ${run.error?.code || run.stderr.trim().slice(0, 400)}`);
|
|
71
|
+
const run = spawnSync(options.python || (process.platform === 'win32' ? 'python' : 'python3'), ['-B', path.join(root, 'scripts/search.py'), query, ...mode, '--json'], { encoding: 'utf8', timeout: 15000, maxBuffer: 5 * 1024 * 1024 });
|
|
72
|
+
ensure(run.status === 0, `UI/UX Pro Max query unavailable: ${run.error?.code || (run.stderr || '').trim().slice(0, 400)}`);
|
|
72
73
|
return JSON.parse(run.stdout);
|
|
73
74
|
}
|
|
74
75
|
function checkDesignProvider(options = {}) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const discovery = discoverProviderCandidates(options);
|
|
77
|
+
const attempts = [];
|
|
78
|
+
const roots = discovery.candidates.map(candidate => path.resolve(candidate.root));
|
|
79
|
+
const diagnostics = discovery.diagnostics;
|
|
80
|
+
if (discovery.contextRequired) return { availability: 'needs-context', searched: roots, attempts, diagnostics, reason: '请指定当前宿主 --host,或传入宿主实际提供的 --provider 技能路径;尚不能判断是否已安装' };
|
|
81
|
+
for (const candidate of discovery.candidates) {
|
|
82
|
+
const root = path.resolve(candidate.root);
|
|
83
|
+
if (!fs.existsSync(root)) { attempts.push({ root, source: candidate.source, availability: 'missing', reason: '技能路径不存在' }); continue; }
|
|
78
84
|
const required = ['SKILL.md', 'scripts/search.py', 'scripts/core.py', 'scripts/design_system.py', ...['ux-guidelines', 'styles', 'colors', 'typography', 'products', 'landing', 'ui-reasoning'].map(name => `data/${name}.csv`)];
|
|
79
85
|
const missing = required.filter(file => !fs.existsSync(path.join(root, file)));
|
|
80
|
-
if (missing.length)
|
|
86
|
+
if (missing.length) { attempts.push({ root, source: candidate.source, availability: 'unusable', missing, reason: '技能资源不完整' }); continue; }
|
|
81
87
|
try {
|
|
82
88
|
const probe = queryProvider(root, 'error summary validation', { python: options.python, domain: 'ux' });
|
|
83
89
|
ensure(probe.domain === 'ux' && probe.results?.length, 'No verified UX probe result');
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
attempts.push({ root, source: candidate.source, availability: 'available' });
|
|
91
|
+
return { availability: 'available', root, source: candidate.source, version: providerVersion(root), digest: hash(required.map(file => hash(fs.readFileSync(path.join(root, file)))).join('\n')), searched: roots, attempts, diagnostics };
|
|
92
|
+
} catch (error) { attempts.push({ root, source: candidate.source, availability: 'unusable', reason: error.message }); }
|
|
86
93
|
}
|
|
87
|
-
|
|
94
|
+
const failure = attempts.find(attempt => attempt.availability === 'unusable') || ['ambiguous', 'disabled', 'discovery-error'].map(status => diagnostics.find(item => item.availability === status)).find(Boolean);
|
|
95
|
+
return { ...(failure || {}), availability: failure?.availability || 'missing', searched: roots, attempts, diagnostics, reason: failure?.reason || (options.provider ? '指定的 UI/UX Pro Max 技能路径不存在,请核对 --provider' : '当前宿主未发现 UI/UX Pro Max;可安装,或传入宿主实际提供的 --provider 技能路径') };
|
|
88
96
|
}
|
|
89
97
|
function initPrototype(options, registry = templateRegistry) {
|
|
98
|
+
if (options.framework === 'vue-antdv-next' || options.template === 'admin-list') return require('./native-pilot-lib.cjs').createPrototype(options);
|
|
90
99
|
const project = path.resolve(options.project || '.');
|
|
91
100
|
const id = identifier(options.id);
|
|
92
101
|
const selected = registry.resolve(options.template || registry.defaultId());
|
|
@@ -94,7 +103,13 @@ function initPrototype(options, registry = templateRegistry) {
|
|
|
94
103
|
const expertMode = options.expertMode || 'professional';
|
|
95
104
|
ensure(['professional', 'template'].includes(expertMode), 'Mode must be professional or template');
|
|
96
105
|
const provider = expertMode === 'professional' ? checkDesignProvider({ ...options, project }) : null;
|
|
97
|
-
if (provider && provider.availability !== 'available')
|
|
106
|
+
if (provider && provider.availability !== 'available') {
|
|
107
|
+
let action = { id: 'locate', label: '提供当前宿主或实际技能路径' };
|
|
108
|
+
if (provider.availability === 'missing' && !options.provider) action = { id: 'install', label: '安装并继续' };
|
|
109
|
+
if (provider.availability === 'unusable') action = { id: 'repair', label: '修复已安装技能并重试' };
|
|
110
|
+
if (provider.availability === 'disabled') action = { id: 'enable', label: '启用已安装插件并重试' };
|
|
111
|
+
return { status: 'needs-design-provider', provider, message: `专业增强模式需要可用的 UI/UX Pro Max。${provider.reason}。也可明确选择模板模式继续。`, choices: [action, { id: 'template', label: '模板模式继续' }, { id: 'prepare', label: '先整理需求和模板选择' }] };
|
|
112
|
+
}
|
|
98
113
|
ensure(fs.existsSync(project) && fs.statSync(project).isDirectory(), 'Project directory does not exist');
|
|
99
114
|
const directory = contained(project, `prototypes/${id}`);
|
|
100
115
|
ensure(!fs.existsSync(directory), 'Prototype already exists / 原型已存在,使用 revise 或另选标识');
|
|
@@ -135,6 +150,7 @@ function initPrototype(options, registry = templateRegistry) {
|
|
|
135
150
|
}
|
|
136
151
|
function contentManifest(directory, registry = templateRegistry) {
|
|
137
152
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
153
|
+
if (metadata.schemaVersion === 2) return require('./native-pilot-lib.cjs').manifest(directory);
|
|
138
154
|
const identity = { id: metadata.id, revision: metadata.revision, template: metadata.template, expertMode: metadata.expertMode, artifactKind: metadata.artifactKind };
|
|
139
155
|
for (const field of registry.forInstance(metadata).runtime.identityFields) if (Object.hasOwn(metadata, field)) identity[field] = metadata[field];
|
|
140
156
|
const paths = ['requirements-map.json', 'qa/design-advice.json', ...['app', 'mocks', 'design-system'].flatMap(folder => files(directory, folder))];
|
|
@@ -142,6 +158,7 @@ function contentManifest(directory, registry = templateRegistry) {
|
|
|
142
158
|
return { schemaVersion: 1, revision: metadata.revision, identity, files: entries, digest: hash(JSON.stringify({ identity, files: entries })) };
|
|
143
159
|
}
|
|
144
160
|
function checkPrototype(directory, options = {}, registry = templateRegistry) {
|
|
161
|
+
if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').inspect(directory, options);
|
|
145
162
|
const errors = [];
|
|
146
163
|
try {
|
|
147
164
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
@@ -160,7 +177,8 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
|
|
|
160
177
|
ensure(requirement.id && !ids.has(requirement.id), 'Requirement identifiers must be unique'); ids.add(requirement.id);
|
|
161
178
|
ensure(requirement.pages?.length && requirement.scenarios?.length && requirement.roles?.length && requirement.actions?.length, `Incomplete business mapping: ${requirement.id}`);
|
|
162
179
|
}
|
|
163
|
-
const runtimeFiles = selected.runtime.requiredFiles;
|
|
180
|
+
const runtimeFiles = [...selected.runtime.requiredFiles];
|
|
181
|
+
if (metadata.runtime?.offlineVersion === 1) runtimeFiles.push('app/offline.js');
|
|
164
182
|
for (const file of ['app/index.html', 'app/tokens.css', 'app/config.json', 'design-system/MASTER.md', 'design-system/tokens.json', ...runtimeFiles]) contained(directory, file) && ensure(fs.existsSync(path.join(directory, file)), `Missing resource: ${file}`);
|
|
165
183
|
const browserFiles = files(directory, 'app').filter(file => /\.(?:html|mjs|js|css)$/.test(file) && !selected.runtime.excludedBrowserPrefixes.some(prefix => file.startsWith(prefix)));
|
|
166
184
|
for (const file of browserFiles) {
|
|
@@ -175,7 +193,7 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
|
|
|
175
193
|
const manifest = contentManifest(directory, registry);
|
|
176
194
|
if (metadata.expertMode === 'professional' && options.acceptance) {
|
|
177
195
|
const advice = json(contained(directory, 'qa/design-advice.json'));
|
|
178
|
-
ensure(advice.provider?.digest && advice.records.
|
|
196
|
+
ensure(advice.provider?.digest && advice.records.length && advice.records.every(record => ['adopted', 'adjusted', 'rejected', 'no-match'].includes(record.decision) && record.query && record.resultIdentity && record.rationale && record.verification), 'Professional advice has not been queried and reviewed');
|
|
179
197
|
}
|
|
180
198
|
if (options.acceptance) {
|
|
181
199
|
const evidence = json(contained(directory, 'qa/browser.json'));
|
|
@@ -189,6 +207,7 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
|
|
|
189
207
|
} catch (error) { errors.push(error.message); return { status: 'failed', errors }; }
|
|
190
208
|
}
|
|
191
209
|
function freezeBaseline(directory, confirmation, registry = templateRegistry) {
|
|
210
|
+
if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').confirmBaseline(directory, confirmation);
|
|
192
211
|
const current = contentManifest(directory, registry);
|
|
193
212
|
ensure(confirmation.revision === current.revision && confirmation.digest === current.digest, 'Human confirmation must match current changed content / 确认版本及内容必须一致');
|
|
194
213
|
ensure(confirmation.reviewer?.trim() && confirmation.statement?.trim() && confirmation.source?.trim(), 'Explicit human confirmation / 人类确认 required');
|
|
@@ -209,12 +228,16 @@ function freezeBaseline(directory, confirmation, registry = templateRegistry) {
|
|
|
209
228
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
210
229
|
metadata.review = { status: 'approved', revision: current.revision, digest: current.digest, source: confirmation.source };
|
|
211
230
|
metadata.verification = { status: 'passed', digest: current.digest };
|
|
212
|
-
if (metadata.expertMode === 'professional')
|
|
231
|
+
if (metadata.expertMode === 'professional') {
|
|
232
|
+
const advice = json(contained(directory, 'qa/design-advice.json'));
|
|
233
|
+
metadata.expert.status = advice.records.some(record => ['adopted', 'adjusted'].includes(record.decision)) ? 'used' : advice.records.every(record => record.decision === 'no-match') ? 'no-match' : 'reviewed';
|
|
234
|
+
}
|
|
213
235
|
atomicJson(directory, 'prototype.json', metadata);
|
|
214
236
|
} finally { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); }
|
|
215
237
|
return current;
|
|
216
238
|
}
|
|
217
239
|
function compareBaseline(directory, registry = templateRegistry) {
|
|
240
|
+
if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').compareBaseline(directory);
|
|
218
241
|
if (!fs.existsSync(contained(directory, 'baseline.json'))) return { status: 'no-baseline', changes: [], protectedChanges: [] };
|
|
219
242
|
const baseline = json(contained(directory, 'baseline.json'));
|
|
220
243
|
const snapshot = contained(directory, baseline.directory);
|
|
@@ -234,7 +257,8 @@ function compareBaseline(directory, registry = templateRegistry) {
|
|
|
234
257
|
if (selectionChanged) protectedChanges.push('template-or-expert-mode');
|
|
235
258
|
return { status: changes.length || protectedChanges.length ? 'changed' : 'unchanged', baselineRevision: baseline.revision, currentRevision: current.revision, changes, protectedChanges };
|
|
236
259
|
}
|
|
237
|
-
function revisePrototype(directory, revision) {
|
|
260
|
+
function revisePrototype(directory, revision, options = {}) {
|
|
261
|
+
if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').revise(directory, { ...options, revision });
|
|
238
262
|
identifier(revision);
|
|
239
263
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
240
264
|
ensure(revision !== metadata.revision && !fs.existsSync(contained(directory, `baselines/${revision}`)), 'Use a new unused revision identifier');
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
// Reviewed mirror of i3's installer target table; maintenance tests require byte equality.
|
|
6
|
+
const registry = require('../assets/optional-skills/host-targets.json');
|
|
7
|
+
const aliases = { claude: 'claude-code', gemini: 'gemini-cli', copilot: 'github-copilot' };
|
|
8
|
+
function globalSkillsDir(agent, home, env) {
|
|
9
|
+
const config = agent.globalSkillsDir;
|
|
10
|
+
if (!config) return null;
|
|
11
|
+
let base = home;
|
|
12
|
+
if (config.base === 'config') base = env.XDG_CONFIG_HOME && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(home, '.config');
|
|
13
|
+
if (config.base === 'env') base = env[config.env]?.trim() || path.join(home, config.fallback);
|
|
14
|
+
if (config.alternatives && !fs.existsSync(path.join(home, path.dirname(config.path)))) {
|
|
15
|
+
const alternative = config.alternatives.find(relative => fs.existsSync(path.join(home, path.dirname(relative))));
|
|
16
|
+
if (alternative) return path.join(home, alternative);
|
|
17
|
+
}
|
|
18
|
+
return path.resolve(base, config.path);
|
|
19
|
+
}
|
|
20
|
+
function installedContext() {
|
|
21
|
+
const file = path.resolve(__dirname, '../../../.i3-composer-agent.json');
|
|
22
|
+
if (!fs.existsSync(file)) return {};
|
|
23
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
24
|
+
if (data.name !== 'i3-composer-agent' || !Array.isArray(data.agents)) throw new Error('Invalid i3 installation context');
|
|
25
|
+
return { hosts: data.agents, scope: data.scope };
|
|
26
|
+
}
|
|
27
|
+
function resolveLocations(options = {}) {
|
|
28
|
+
const installed = installedContext();
|
|
29
|
+
if (!options.host && installed.hosts?.length > 1) throw new Error('Multiple installed hosts; specify --host');
|
|
30
|
+
const selected = options.host || installed.hosts?.[0] || 'codebuddy';
|
|
31
|
+
const host = aliases[selected] || selected, scope = options.scope || 'project';
|
|
32
|
+
const agent = registry.agents.find(item => item.id === host);
|
|
33
|
+
if (!agent) throw new Error(`Unknown host: ${host}`);
|
|
34
|
+
if (!['project', 'global'].includes(scope)) throw new Error('Use scope project or global');
|
|
35
|
+
const project = fs.realpathSync(path.resolve(options.project || '.'));
|
|
36
|
+
if (!fs.statSync(project).isDirectory()) throw new Error('Project must be a directory');
|
|
37
|
+
const home = path.resolve(options.home || os.homedir()), env = options.env || process.env;
|
|
38
|
+
const projectSkills = path.resolve(project, agent.projectSkillsDir), globalSkills = globalSkillsDir(agent, home, env);
|
|
39
|
+
if (scope === 'global' && !globalSkills) throw new Error(`${agent.label}: project installation only`);
|
|
40
|
+
const skillsDirectory = scope === 'project' ? projectSkills : globalSkills, base = path.dirname(skillsDirectory);
|
|
41
|
+
const projectBase = path.dirname(projectSkills), globalBase = globalSkills ? path.dirname(globalSkills) : null;
|
|
42
|
+
// Some hosts put skills directly in the workspace; tools still get a host directory.
|
|
43
|
+
const projectToolsBase = projectBase === project ? path.join(project, `.${host}`) : projectBase;
|
|
44
|
+
return { host, scope, project, home, skillsDirectory, base, projectSkills, globalSkills,
|
|
45
|
+
projectBase, globalBase, projectToolsBase,
|
|
46
|
+
toolsDirectory: path.join(scope === 'global' ? globalBase : projectToolsBase, 'tools/i3-prototype') };
|
|
47
|
+
}
|
|
48
|
+
function scopeBases(context) {
|
|
49
|
+
return context.scope === 'global' ? [[context.globalBase, 'global']] : [[context.projectBase, 'project'], ...(context.globalBase ? [[context.globalBase, 'global']] : [])];
|
|
50
|
+
}
|
|
51
|
+
function toolCandidates(context, options = {}) {
|
|
52
|
+
if (options.tool !== undefined) {
|
|
53
|
+
if (!path.isAbsolute(options.tool)) throw new Error('--tool requires an absolute antDv package directory');
|
|
54
|
+
return [{ root: path.resolve(options.tool), source: 'explicit-tool' }];
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
...toolBases(context).map(([base, scope]) => ({ root: path.join(base, 'tools/i3-prototype/antdv-cli/node_modules/@antdv-next/cli'), source: `${scope}-tool` })),
|
|
58
|
+
...(context.scope === 'project' ? ['.prototype-tools/antdv-cli/node_modules/@antdv-next/cli', '.prototype-tools/node_modules/@antdv-next/cli']
|
|
59
|
+
.map(relative => ({ root: path.join(context.project, relative), source: 'legacy-project-tool' })) : []),
|
|
60
|
+
].filter((item, index, all) => all.findIndex(other => other.root === item.root) === index);
|
|
61
|
+
}
|
|
62
|
+
function toolBases(context) {
|
|
63
|
+
return context.scope === 'global' ? [[context.globalBase, 'global']] : [[context.projectToolsBase, 'project'], ...(context.globalBase ? [[context.globalBase, 'global']] : [])];
|
|
64
|
+
}
|
|
65
|
+
module.exports = { registry, globalSkillsDir, resolveLocations, scopeBases, toolBases, toolCandidates };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { prepareTemplate, syncTemplate, loadTemplate } = require('./template-contract.cjs');
|
|
4
|
+
try {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const option = name => { const index = args.indexOf(name); return index < 0 ? undefined : args[index + 1]; };
|
|
7
|
+
let result;
|
|
8
|
+
if (args.includes('--prepare')) {
|
|
9
|
+
if (!option('--project')) throw new Error('--prepare requires --project <directory>');
|
|
10
|
+
const prepared = prepareTemplate({ project: option('--project'), id: option('--id') });
|
|
11
|
+
result = { status: 'passed', directory: prepared.directory, created: prepared.created, digest: prepared.index.digest };
|
|
12
|
+
} else {
|
|
13
|
+
const directory = args.find(arg => !arg.startsWith('--'));
|
|
14
|
+
if (!directory) throw new Error('Usage: sync-template.cjs <directory> [--check] | --prepare --project <directory> [--id <id>]');
|
|
15
|
+
const index = args.includes('--check') ? loadTemplate(directory).index : syncTemplate(directory);
|
|
16
|
+
result = { status: 'passed', templateId: index.templateId, digest: index.digest, bindings: index.bindings.length };
|
|
17
|
+
}
|
|
18
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
19
|
+
} catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
|