@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.
Files changed (116) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +5 -7
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-prototype.toml +5 -7
  7. package/README.md +12 -7
  8. package/bin/i3-composer-agent.cjs +33 -2
  9. package/bin/install.cjs +14 -0
  10. package/bin/native-migration.cjs +27 -3
  11. package/commands/i3-prototype.toml +5 -7
  12. package/docs/codebuddy-setup.md +65 -2
  13. package/docs/codex-setup.md +4 -0
  14. package/docs/other-hosts.md +1 -1
  15. package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
  16. package/docs/ui-prototype-native/USAGE.md +91 -0
  17. package/docs/workbuddy-setup.md +4 -0
  18. package/evals/cases/i3-frontend-ui-engineering.json +16 -0
  19. package/evals/cases/i3-ui-prototype.json +109 -0
  20. package/evals/cases/i3-using-agent-skills.json +28 -0
  21. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
  22. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
  23. package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
  24. package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
  25. package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
  26. package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
  27. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
  28. package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
  29. package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
  30. package/package.json +31 -1
  31. package/plugin.json +1 -1
  32. package/scripts/design-provider-test.js +7 -0
  33. package/scripts/enterprise-template-test.js +27 -1
  34. package/scripts/lib/prototype-fixture.cjs +17 -0
  35. package/scripts/prototype-locations-test.js +58 -0
  36. package/scripts/ui-prototype-content-test.js +123 -0
  37. package/scripts/ui-prototype-dependencies-test.js +213 -0
  38. package/scripts/ui-prototype-devtools-test.js +53 -0
  39. package/scripts/ui-prototype-native-test.js +504 -0
  40. package/scripts/ui-prototype-template-test.js +261 -0
  41. package/scripts/ui-prototype-test.js +10 -0
  42. package/skills/i3-ui-prototype/SKILL.md +138 -19
  43. package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
  44. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
  45. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
  46. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
  47. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
  48. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
  49. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
  50. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
  51. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
  52. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
  53. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
  54. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
  55. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
  56. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
  57. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
  58. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
  59. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
  60. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
  61. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
  62. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
  63. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
  64. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
  65. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
  66. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
  67. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
  68. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
  69. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +51 -0
  70. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +18 -0
  71. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
  72. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
  73. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
  74. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
  75. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
  76. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
  77. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +737 -0
  78. package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
  79. package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
  80. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
  81. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
  82. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
  83. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
  84. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
  85. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +4 -4
  86. package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
  87. package/skills/i3-ui-prototype/references/artifact-contract.md +25 -0
  88. package/skills/i3-ui-prototype/references/content-template.md +76 -0
  89. package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
  90. package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
  91. package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
  92. package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
  93. package/skills/i3-ui-prototype/references/interaction-acceptance.md +165 -0
  94. package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
  95. package/skills/i3-ui-prototype/references/optional-skills.md +59 -0
  96. package/skills/i3-ui-prototype/references/template-adapters.md +1 -1
  97. package/skills/i3-ui-prototype/references/workflow-contract.md +58 -4
  98. package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
  99. package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
  100. package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
  101. package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
  102. package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
  103. package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
  104. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +91 -0
  105. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +91 -0
  106. package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
  107. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +244 -0
  108. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +34 -0
  109. package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
  110. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
  111. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +26 -6
  112. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +65 -0
  113. package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
  114. package/skills/i3-ui-prototype/scripts/template-contract.cjs +398 -0
  115. package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
  116. 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)) { try { const version = json(file).version; if (version) return version; } catch {} }
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.some(record => ['adopted', 'adjusted'].includes(record.decision) && record.query && record.resultIdentity && record.rationale && record.verification), 'Professional advice has not been verified and adopted');
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') metadata.expert.status = 'used';
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; }