@givolt/i3-composer-agent 2.3.1 → 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 +31 -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-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 +138 -19
- 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/enterprise-admin/capability-definition.json +294 -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.json +9 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +4 -4
- package/skills/i3-ui-prototype/assets/templates/index.html +3 -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 +6 -0
- package/skills/i3-ui-prototype/references/entry-contract.md +71 -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/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 +1 -1
- 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/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 +26 -6
- 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; });
|
|
@@ -48,17 +48,27 @@ function tokensCss(tokens) {
|
|
|
48
48
|
return ':root {\n' + entries.map(([key, value]) => ` --${key}: ${value};`).join('\n') + '\n}\n';
|
|
49
49
|
}
|
|
50
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
|
+
}
|
|
51
55
|
for (let folder = root, i = 0; i < 5; i++, folder = path.dirname(folder)) {
|
|
52
56
|
for (const name of ['skill.json', '.claude-plugin/plugin.json', '.codebuddy-plugin/plugin.json', 'package.json']) {
|
|
53
57
|
const file = path.join(folder, name);
|
|
54
|
-
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
|
+
}
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
return 'unversioned';
|
|
58
68
|
}
|
|
59
69
|
function queryProvider(root, query, options = {}) {
|
|
60
70
|
const mode = options.stack ? ['--stack', options.stack] : options.designSystem ? ['--design-system'] : ['--domain', options.domain || 'ux'];
|
|
61
|
-
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
|
+
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 });
|
|
62
72
|
ensure(run.status === 0, `UI/UX Pro Max query unavailable: ${run.error?.code || (run.stderr || '').trim().slice(0, 400)}`);
|
|
63
73
|
return JSON.parse(run.stdout);
|
|
64
74
|
}
|
|
@@ -85,6 +95,7 @@ function checkDesignProvider(options = {}) {
|
|
|
85
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 技能路径') };
|
|
86
96
|
}
|
|
87
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);
|
|
88
99
|
const project = path.resolve(options.project || '.');
|
|
89
100
|
const id = identifier(options.id);
|
|
90
101
|
const selected = registry.resolve(options.template || registry.defaultId());
|
|
@@ -139,6 +150,7 @@ function initPrototype(options, registry = templateRegistry) {
|
|
|
139
150
|
}
|
|
140
151
|
function contentManifest(directory, registry = templateRegistry) {
|
|
141
152
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
153
|
+
if (metadata.schemaVersion === 2) return require('./native-pilot-lib.cjs').manifest(directory);
|
|
142
154
|
const identity = { id: metadata.id, revision: metadata.revision, template: metadata.template, expertMode: metadata.expertMode, artifactKind: metadata.artifactKind };
|
|
143
155
|
for (const field of registry.forInstance(metadata).runtime.identityFields) if (Object.hasOwn(metadata, field)) identity[field] = metadata[field];
|
|
144
156
|
const paths = ['requirements-map.json', 'qa/design-advice.json', ...['app', 'mocks', 'design-system'].flatMap(folder => files(directory, folder))];
|
|
@@ -146,6 +158,7 @@ function contentManifest(directory, registry = templateRegistry) {
|
|
|
146
158
|
return { schemaVersion: 1, revision: metadata.revision, identity, files: entries, digest: hash(JSON.stringify({ identity, files: entries })) };
|
|
147
159
|
}
|
|
148
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);
|
|
149
162
|
const errors = [];
|
|
150
163
|
try {
|
|
151
164
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
@@ -164,7 +177,8 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
|
|
|
164
177
|
ensure(requirement.id && !ids.has(requirement.id), 'Requirement identifiers must be unique'); ids.add(requirement.id);
|
|
165
178
|
ensure(requirement.pages?.length && requirement.scenarios?.length && requirement.roles?.length && requirement.actions?.length, `Incomplete business mapping: ${requirement.id}`);
|
|
166
179
|
}
|
|
167
|
-
const runtimeFiles = selected.runtime.requiredFiles;
|
|
180
|
+
const runtimeFiles = [...selected.runtime.requiredFiles];
|
|
181
|
+
if (metadata.runtime?.offlineVersion === 1) runtimeFiles.push('app/offline.js');
|
|
168
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}`);
|
|
169
183
|
const browserFiles = files(directory, 'app').filter(file => /\.(?:html|mjs|js|css)$/.test(file) && !selected.runtime.excludedBrowserPrefixes.some(prefix => file.startsWith(prefix)));
|
|
170
184
|
for (const file of browserFiles) {
|
|
@@ -179,7 +193,7 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
|
|
|
179
193
|
const manifest = contentManifest(directory, registry);
|
|
180
194
|
if (metadata.expertMode === 'professional' && options.acceptance) {
|
|
181
195
|
const advice = json(contained(directory, 'qa/design-advice.json'));
|
|
182
|
-
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');
|
|
183
197
|
}
|
|
184
198
|
if (options.acceptance) {
|
|
185
199
|
const evidence = json(contained(directory, 'qa/browser.json'));
|
|
@@ -193,6 +207,7 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
|
|
|
193
207
|
} catch (error) { errors.push(error.message); return { status: 'failed', errors }; }
|
|
194
208
|
}
|
|
195
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);
|
|
196
211
|
const current = contentManifest(directory, registry);
|
|
197
212
|
ensure(confirmation.revision === current.revision && confirmation.digest === current.digest, 'Human confirmation must match current changed content / 确认版本及内容必须一致');
|
|
198
213
|
ensure(confirmation.reviewer?.trim() && confirmation.statement?.trim() && confirmation.source?.trim(), 'Explicit human confirmation / 人类确认 required');
|
|
@@ -213,12 +228,16 @@ function freezeBaseline(directory, confirmation, registry = templateRegistry) {
|
|
|
213
228
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
214
229
|
metadata.review = { status: 'approved', revision: current.revision, digest: current.digest, source: confirmation.source };
|
|
215
230
|
metadata.verification = { status: 'passed', digest: current.digest };
|
|
216
|
-
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
|
+
}
|
|
217
235
|
atomicJson(directory, 'prototype.json', metadata);
|
|
218
236
|
} finally { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); }
|
|
219
237
|
return current;
|
|
220
238
|
}
|
|
221
239
|
function compareBaseline(directory, registry = templateRegistry) {
|
|
240
|
+
if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').compareBaseline(directory);
|
|
222
241
|
if (!fs.existsSync(contained(directory, 'baseline.json'))) return { status: 'no-baseline', changes: [], protectedChanges: [] };
|
|
223
242
|
const baseline = json(contained(directory, 'baseline.json'));
|
|
224
243
|
const snapshot = contained(directory, baseline.directory);
|
|
@@ -238,7 +257,8 @@ function compareBaseline(directory, registry = templateRegistry) {
|
|
|
238
257
|
if (selectionChanged) protectedChanges.push('template-or-expert-mode');
|
|
239
258
|
return { status: changes.length || protectedChanges.length ? 'changed' : 'unchanged', baselineRevision: baseline.revision, currentRevision: current.revision, changes, protectedChanges };
|
|
240
259
|
}
|
|
241
|
-
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 });
|
|
242
262
|
identifier(revision);
|
|
243
263
|
const metadata = json(contained(directory, 'prototype.json'));
|
|
244
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; }
|