@givolt/i3-composer-agent 2.3.0 → 2.3.1

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.
@@ -6,7 +6,7 @@
6
6
  "plugins": [
7
7
  {
8
8
  "name": "i3-composer-agent",
9
- "version": "2.3.0",
9
+ "version": "2.3.1",
10
10
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
11
11
  "source": {
12
12
  "source": "local",
@@ -8,7 +8,7 @@
8
8
  "plugins": [
9
9
  {
10
10
  "name": "i3-composer-agent",
11
- "version": "2.3.0",
11
+ "version": "2.3.1",
12
12
  "source": "./",
13
13
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
14
14
  "homepage": "https://github.com/49780522/i3-composer-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i3 Composer Agent v2.3.0
1
+ # i3 Composer Agent v2.3.1
2
2
 
3
3
  **Intelligence. Integration. Innovation.**
4
4
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 使用本地分发目录
8
8
 
9
- 以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.0` 的对应入口。
9
+ 以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.1` 的对应入口。
10
10
 
11
11
  ### fx
12
12
 
package/package.json CHANGED
@@ -168,6 +168,8 @@
168
168
  "plugin.json",
169
169
  "README.md",
170
170
  "LICENSE",
171
+ "scripts/design-provider-discovery-test.js",
172
+ "scripts/design-provider-test.js",
171
173
  "scripts/enterprise-template-test.js",
172
174
  "scripts/floor-guard-reference-test.js",
173
175
  "scripts/lib/skill-lint-test.js",
@@ -354,6 +356,8 @@
354
356
  "package.json",
355
357
  "README.md",
356
358
  "LICENSE",
359
+ "scripts/design-provider-discovery-test.js",
360
+ "scripts/design-provider-test.js",
357
361
  "scripts/enterprise-template-test.js",
358
362
  "scripts/floor-guard-reference-test.js",
359
363
  "scripts/lib/skill-lint-test.js",
@@ -374,7 +378,7 @@
374
378
  "scripts/validate-versions.js"
375
379
  ]
376
380
  },
377
- "version": "2.3.0",
381
+ "version": "2.3.1",
378
382
  "repository": {
379
383
  "type": "git",
380
384
  "url": "https://github.com/49780522/i3-composer-agent.git"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "homepage": "https://github.com/49780522/i3-composer-agent",
6
6
  "repository": "https://github.com/49780522/i3-composer-agent"
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const childProcess = require('node:child_process');
8
+ const installationFile = path.join(__dirname, 'lib/installation.js');
9
+ const skillsDirectory = fs.existsSync(installationFile) ? require(installationFile).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
10
+ const moduleFile = path.join(skillsDirectory, 'i3-ui-prototype/scripts/design-provider-discovery.cjs');
11
+
12
+ function fixture(t) {
13
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-provider-discovery-'));
14
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
15
+ const home = path.join(root, 'home'), project = path.join(root, 'project');
16
+ fs.mkdirSync(home); fs.mkdirSync(project);
17
+ return { root, home, project, env: {} };
18
+ }
19
+ function write(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, typeof value === 'string' ? value : JSON.stringify(value)); }
20
+ function skill(root) { write(path.join(root, 'SKILL.md'), '# Fixture UI/UX'); return root; }
21
+ function discover(options, run = () => { throw new Error('Unexpected host CLI call'); }) {
22
+ const original = childProcess.spawnSync;
23
+ childProcess.spawnSync = run;
24
+ delete require.cache[require.resolve(moduleFile)];
25
+ let api;
26
+ try { api = require(moduleFile); } finally { childProcess.spawnSync = original; }
27
+ return api.discoverProviderCandidates(options);
28
+ }
29
+ function codexPlugin(f, overrides = {}) {
30
+ const record = { pluginId: 'ui-ux-pro-max@ui-ux-pro-max-skill', name: 'ui-ux-pro-max', marketplaceName: 'ui-ux-pro-max-skill', version: '2.15.0', installed: true, enabled: true, ...overrides };
31
+ const cache = path.join(f.env.CODEX_HOME || path.join(f.home, '.codex'), 'plugins/cache/ui-ux-pro-max-skill/ui-ux-pro-max', record.version);
32
+ if (!record.version.includes('..')) {
33
+ write(path.join(cache, '.claude-plugin/plugin.json'), { name: 'ui-ux-pro-max', version: record.version, skills: './.claude/skills/' });
34
+ skill(path.join(cache, '.claude/skills/ui-ux-pro-max'));
35
+ }
36
+ return { record, cache, run: () => ({ status: 0, stdout: JSON.stringify({ installed: [record], available: [] }) }) };
37
+ }
38
+ function registryPlugin(f, host, records, enabled = true) {
39
+ const config = path.join(f.home, `.${host}`), id = 'ui-ux-pro-max@fixture-market';
40
+ write(path.join(config, 'settings.json'), { enabledPlugins: { [id]: enabled } });
41
+ write(path.join(config, 'plugins/installed_plugins.json'), { version: 2, plugins: { [id]: records } });
42
+ for (const record of records) {
43
+ write(path.join(record.installPath, host === 'claude' ? '.claude-plugin/plugin.json' : '.codebuddy-plugin/plugin.json'), { name: 'ui-ux-pro-max', version: record.version, ...(host === 'claude' ? { skills: './.claude/skills/' } : {}) });
44
+ skill(path.join(record.installPath, host === 'claude' ? '.claude/skills/ui-ux-pro-max' : 'skills/ui-ux-pro-max'));
45
+ }
46
+ return config;
47
+ }
48
+
49
+ test('explicit provider has precedence over host roots and does not invoke discovery', t => {
50
+ const f = fixture(t), root = path.join(f.root, 'explicit');
51
+ assert.deepEqual(discover({ ...f, provider: root, providerRoots: ['/ignored'], host: 'codex' }), { candidates: [{ root, source: 'explicit' }], diagnostics: [] });
52
+ });
53
+ test('host-exposed provider roots work without host context and preserve order', t => {
54
+ const f = fixture(t), roots = [path.join(f.root, 'first'), path.join(f.root, 'second')];
55
+ assert.deepEqual(discover({ ...f, providerRoots: roots }).candidates.map(c => c.root), roots);
56
+ assert.equal(discover({ ...f, providerRoots: [] }).contextRequired, undefined);
57
+ });
58
+ test('unknown host requires context and cannot read another host cache', t => {
59
+ const f = fixture(t); skill(path.join(f.home, '.claude/skills/ui-ux-pro-max'));
60
+ assert.equal(discover(f).contextRequired, true);
61
+ assert.equal(discover({ ...f, host: 'unknown' }).contextRequired, true);
62
+ assert.deepEqual(discover({ ...f, host: 'unknown' }).candidates, []);
63
+ });
64
+ test('discover false prevents automatic host access', t => {
65
+ const f = fixture(t); skill(path.join(f.home, '.agents/skills/ui-ux-pro-max'));
66
+ assert.deepEqual(discover({ ...f, host: 'codex', discover: false }), { candidates: [], diagnostics: [], contextRequired: true });
67
+ });
68
+ test('Codex returns project then global skill locations and the exact enabled plugin version', t => {
69
+ const f = fixture(t), plugin = codexPlugin(f);
70
+ const project = skill(path.join(f.project, '.agents/skills/ui-ux-pro-max'));
71
+ const global = skill(path.join(f.home, '.codex/skills/ui-ux-pro-max'));
72
+ const stale = path.join(f.home, '.codex/plugins/cache/ui-ux-pro-max-skill/ui-ux-pro-max/99.0.0/.claude/skills/ui-ux-pro-max'); skill(stale);
73
+ let command;
74
+ const result = discover({ ...f, host: 'codex' }, (...args) => { command = args; return plugin.run(); });
75
+ assert.deepEqual(result.candidates.map(c => c.root), [project, global, path.join(plugin.cache, '.claude/skills/ui-ux-pro-max')]);
76
+ assert.equal(command[0], 'codex');
77
+ assert.deepEqual(command[1], ['plugin', 'list', '--marketplace', 'ui-ux-pro-max-skill', '--json']);
78
+ assert.ok(command[2].timeout > 0 && command[2].timeout <= 15000);
79
+ assert.equal(command[2].shell, false);
80
+ });
81
+ test('Codex honors isolated CODEX_HOME and never scans the default cache', t => {
82
+ const f = fixture(t); f.env.CODEX_HOME = path.join(f.root, 'custom-codex');
83
+ const plugin = codexPlugin(f);
84
+ const result = discover({ ...f, host: 'codex' }, plugin.run);
85
+ assert.equal(result.candidates.at(-1).root, path.join(plugin.cache, '.claude/skills/ui-ux-pro-max'));
86
+ });
87
+ test('Codex disabled cached plugin is not a candidate', t => {
88
+ const f = fixture(t), plugin = codexPlugin(f, { enabled: false });
89
+ const result = discover({ ...f, host: 'codex' }, plugin.run);
90
+ assert.deepEqual(result.candidates, []);
91
+ assert.equal(result.diagnostics[0].availability, 'disabled');
92
+ });
93
+ test('Codex missing installation does not inspect cached versions', t => {
94
+ const f = fixture(t); codexPlugin(f);
95
+ const result = discover({ ...f, host: 'codex' }, () => ({ status: 0, stdout: '{"installed":[],"available":[]}' }));
96
+ assert.deepEqual(result.candidates, []);
97
+ assert.ok(result.diagnostics.every(d => d.availability === 'missing'));
98
+ });
99
+ for (const [name, run] of [
100
+ ['missing CLI', () => ({ status: null, error: { code: 'ENOENT' }, stdout: '' })],
101
+ ['failed CLI', () => ({ status: 1, stderr: 'failure', stdout: '' })],
102
+ ['invalid JSON', () => ({ status: 0, stdout: '{bad' })],
103
+ ['invalid schema', () => ({ status: 0, stdout: '{}' })],
104
+ ]) test(`Codex ${name} is a discovery error, not a missing installation`, t => {
105
+ const f = fixture(t), result = discover({ ...f, host: 'codex' }, run);
106
+ assert.equal(result.diagnostics[0].availability, 'discovery-error');
107
+ });
108
+ test('Codex rejects multiple active installed records instead of selecting a version', t => {
109
+ const f = fixture(t), { record } = codexPlugin(f);
110
+ const result = discover({ ...f, host: 'codex' }, () => ({ status: 0, stdout: JSON.stringify({ installed: [record, { ...record, version: '2.14.0' }] }) }));
111
+ assert.deepEqual(result.candidates, []);
112
+ assert.equal(result.diagnostics[0].availability, 'ambiguous');
113
+ });
114
+ for (const [name, change] of [
115
+ ['wrong manifest version', m => { m.version = '2.14.0'; }],
116
+ ['wrong manifest name', m => { m.name = 'different'; }],
117
+ ['escaping skills declaration', m => { m.skills = '../../../outside'; }],
118
+ ['missing skills declaration', m => { delete m.skills; }],
119
+ ]) test(`Codex rejects ${name}`, t => {
120
+ const f = fixture(t), plugin = codexPlugin(f), file = path.join(plugin.cache, '.claude-plugin/plugin.json');
121
+ const manifest = JSON.parse(fs.readFileSync(file)); change(manifest); write(file, manifest);
122
+ const result = discover({ ...f, host: 'codex' }, plugin.run);
123
+ assert.deepEqual(result.candidates, []);
124
+ assert.equal(result.diagnostics[0].availability, 'discovery-error');
125
+ });
126
+ test('Codex validates the active version as a single safe path segment', t => {
127
+ const f = fixture(t), plugin = codexPlugin(f, { version: '../../escape' });
128
+ assert.equal(discover({ ...f, host: 'codex' }, plugin.run).diagnostics[0].availability, 'discovery-error');
129
+ });
130
+ test('Codex rejects a child skill symlink escaping the registered plugin root', t => {
131
+ const f = fixture(t), plugin = codexPlugin(f), outside = skill(path.join(f.root, 'outside'));
132
+ const child = path.join(plugin.cache, '.claude/skills/ui-ux-pro-max');
133
+ fs.rmSync(child, { recursive: true }); fs.symlinkSync(outside, child, 'dir');
134
+ const result = discover({ ...f, host: 'codex' }, plugin.run);
135
+ assert.deepEqual(result.candidates, []);
136
+ assert.equal(result.diagnostics[0].availability, 'discovery-error');
137
+ });
138
+ for (const host of ['claude', 'workbuddy', 'codebuddy']) test(`${host} selects only enabled registered plugins`, t => {
139
+ const f = fixture(t), installPath = path.join(f.root, host, 'plugin');
140
+ registryPlugin(f, host, [{ scope: 'user', installPath, version: '2.0.0' }]);
141
+ const result = discover({ ...f, host });
142
+ assert.deepEqual(result.candidates.map(c => c.root), [path.join(installPath, host === 'claude' ? '.claude/skills/ui-ux-pro-max' : 'skills/ui-ux-pro-max')]);
143
+ registryPlugin(f, host, [{ scope: 'user', installPath, version: '2.0.0' }], false);
144
+ const disabled = discover({ ...f, host });
145
+ assert.deepEqual(disabled.candidates, []); assert.equal(disabled.diagnostics[0].availability, 'disabled');
146
+ });
147
+ test('missing optional registry is normal, while corrupt metadata is an error', t => {
148
+ const f = fixture(t);
149
+ assert.ok(discover({ ...f, host: 'workbuddy' }).diagnostics.every(d => d.availability === 'missing'));
150
+ write(path.join(f.home, '.workbuddy/plugins/installed_plugins.json'), '{invalid');
151
+ assert.equal(discover({ ...f, host: 'workbuddy' }).diagnostics[0].availability, 'discovery-error');
152
+ });
153
+ test('a registered plugin with missing enablement metadata needs diagnosis instead of installation', t => {
154
+ const f = fixture(t);
155
+ const config = registryPlugin(f, 'workbuddy', [{ scope: 'user', installPath: path.join(f.root, 'plugin'), version: '2.0.0' }]);
156
+ fs.unlinkSync(path.join(config, 'settings.json'));
157
+ const result = discover({ ...f, host: 'workbuddy' });
158
+ assert.deepEqual(result.candidates, []);
159
+ assert.equal(result.diagnostics[0].availability, 'discovery-error');
160
+ });
161
+ test('project plugin scope must match the current project path', t => {
162
+ const f = fixture(t), installPath = path.join(f.root, 'plugin');
163
+ registryPlugin(f, 'claude', [{ scope: 'project', projectPath: path.join(f.root, 'other-project'), installPath, version: '2.0.0' }]);
164
+ assert.deepEqual(discover({ ...f, host: 'claude-code' }).candidates, []);
165
+ registryPlugin(f, 'claude', [{ scope: 'project', projectPath: f.project, installPath, version: '2.0.0' }]);
166
+ assert.equal(discover({ ...f, host: 'claude-code' }).candidates.length, 1);
167
+ registryPlugin(f, 'claude', [{ scope: 'project', installPath, version: '2.0.0' }]);
168
+ assert.deepEqual(discover({ ...f, host: 'claude-code' }).candidates, []);
169
+ });
170
+ test('ambiguous matching registry entries do not pick one arbitrarily', t => {
171
+ const f = fixture(t);
172
+ registryPlugin(f, 'workbuddy', [
173
+ { scope: 'user', installPath: path.join(f.root, 'first'), version: '2.0.0' },
174
+ { scope: 'user', installPath: path.join(f.root, 'second'), version: '2.1.0' },
175
+ ]);
176
+ const result = discover({ ...f, host: 'workbuddy' });
177
+ assert.deepEqual(result.candidates, []); assert.equal(result.diagnostics[0].availability, 'ambiguous');
178
+ });
179
+ test('host selection never borrows another host global plugin', t => {
180
+ const f = fixture(t);
181
+ registryPlugin(f, 'claude', [{ scope: 'user', installPath: path.join(f.root, 'claude'), version: '2.0.0' }]);
182
+ registryPlugin(f, 'workbuddy', [{ scope: 'user', installPath: path.join(f.root, 'workbuddy'), version: '2.0.0' }]);
183
+ assert.deepEqual(discover({ ...f, host: 'codebuddy' }).candidates, []);
184
+ });
185
+ test('WorkBuddy uses its global config override and the project CodeBuddy skills directory', t => {
186
+ const f = fixture(t); f.env.WORKBUDDY_CONFIG_DIR = path.join(f.root, 'custom-workbuddy');
187
+ const projectRoot = skill(path.join(f.project, '.codebuddy/skills/ui-ux-pro-max'));
188
+ const globalRoot = skill(path.join(f.env.WORKBUDDY_CONFIG_DIR, 'skills/ui-ux-pro-max'));
189
+ assert.deepEqual(discover({ ...f, host: 'workbuddy' }).candidates.map(c => c.root), [projectRoot, globalRoot]);
190
+ });
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+ const { test, mock } = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const cp = require('node:child_process');
8
+ mock.method(cp, 'spawnSync', (command, args) => {
9
+ if (!args.includes('--json') || args[0] !== '-B') return { status: 1, stdout: '', stderr: 'No plugin CLI in isolated test' };
10
+ if (args[1].includes('query-fails')) return { status: 1, stdout: '', stderr: 'Python module unavailable' };
11
+ return { status: 0, stdout: JSON.stringify({ domain: 'ux', results: [{ name: 'Error summary' }] }), stderr: '' };
12
+ });
13
+ const installationFile = path.join(__dirname, 'lib/installation.js');
14
+ const skillsDirectory = fs.existsSync(installationFile) ? require(installationFile).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
15
+ const lib = require(path.join(skillsDirectory, 'i3-ui-prototype/scripts/prototype-lib.cjs'));
16
+ function fixture(t) {
17
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-provider-'));
18
+ t.after(() => fs.rmSync(home, { recursive: true, force: true }));
19
+ return home;
20
+ }
21
+ function provider(home, name) {
22
+ const root = path.join(home, name);
23
+ for (const file of ['SKILL.md', 'scripts/search.py', 'scripts/core.py', 'scripts/design_system.py', ...['ux-guidelines', 'styles', 'colors', 'typography', 'products', 'landing', 'ui-reasoning'].map(name => `data/${name}.csv`)]) {
24
+ fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
25
+ fs.writeFileSync(path.join(root, file), 'fixture');
26
+ }
27
+ return root;
28
+ }
29
+ test('an incomplete project provider does not shadow a usable global provider', t => {
30
+ const home = fixture(t), local = path.join(home, 'project-provider'), global = provider(home, 'global-provider');
31
+ fs.mkdirSync(local);
32
+ const result = lib.checkDesignProvider({ providerRoots: [local, global] });
33
+ assert.equal(result.availability, 'available');
34
+ assert.equal(result.root, global);
35
+ assert.equal(result.attempts[0].availability, 'unusable');
36
+ assert.ok(result.attempts[0].missing.includes('scripts/search.py'));
37
+ });
38
+ test('a failed project query does not shadow a usable global provider', t => {
39
+ const home = fixture(t), local = provider(home, 'query-fails'), global = provider(home, 'global-provider');
40
+ const result = lib.checkDesignProvider({ providerRoots: [local, global] });
41
+ assert.equal(result.availability, 'available');
42
+ assert.equal(result.root, global);
43
+ assert.match(result.attempts[0].reason, /Python module unavailable/);
44
+ });
45
+ test('an explicit provider failure never silently selects a different provider', t => {
46
+ const home = fixture(t), selected = path.join(home, 'missing'), other = provider(home, 'other');
47
+ const result = lib.checkDesignProvider({ provider: selected, providerRoots: [other] });
48
+ assert.equal(result.availability, 'missing');
49
+ assert.deepEqual(result.searched, [selected]);
50
+ });
51
+ test('without host or provider the result requests context, not installation', t => {
52
+ const home = fixture(t);
53
+ const result = lib.initPrototype({ project: home, id: 'demo', home, env: {} });
54
+ assert.equal(result.provider.availability, 'needs-context');
55
+ assert.equal(result.choices[0].id, 'locate');
56
+ assert.ok(!result.choices.some(choice => choice.id === 'install'));
57
+ assert.equal(fs.existsSync(path.join(home, 'prototypes')), false);
58
+ });
59
+ test('all unusable providers preserve their individual failure reasons without asking to reinstall', t => {
60
+ const home = fixture(t), incomplete = path.join(home, 'incomplete'), failing = provider(home, 'query-fails');
61
+ fs.mkdirSync(incomplete);
62
+ const result = lib.initPrototype({ project: home, id: 'demo', providerRoots: [incomplete, failing] });
63
+ assert.equal(result.provider.availability, 'unusable');
64
+ assert.equal(result.provider.attempts.length, 2);
65
+ assert.equal(result.choices[0].id, 'repair');
66
+ assert.ok(!result.choices.some(choice => choice.id === 'install'));
67
+ assert.equal(fs.existsSync(path.join(home, 'prototypes')), false);
68
+ });
@@ -10,6 +10,7 @@ const install = path.join(__dirname, 'lib/installation.js');
10
10
  const skills = fs.existsSync(install) ? require(install).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
11
11
  const skill = path.join(skills, 'i3-ui-prototype');
12
12
  const lib = require(path.join(skill, 'scripts/prototype-lib.cjs'));
13
+ const enterprise = require(path.join(skill, 'scripts/enterprise-template.cjs'));
13
14
  const read = (root, file) => JSON.parse(fs.readFileSync(path.join(root, file), 'utf8'));
14
15
  function project(t) {
15
16
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'enterprise-template-'));
@@ -19,6 +20,45 @@ function project(t) {
19
20
  function initialize(t, options = {}) {
20
21
  return lib.initPrototype({ project: project(t), id: 'enterprise-demo', expertMode: 'template', purpose: 'template-review', ...options }).directory;
21
22
  }
23
+ test('runtime branding changes uppercase product text only in runtime HTML and JavaScript', () => {
24
+ const source = 'EASEL easel-html /easel EASEL 研发';
25
+ for (const file of ['index.html', 'src/shell/ConsoleShell.html', 'src/models/data.js']) {
26
+ assert.equal(enterprise.brandRuntimeResource(file, source), 'i3-Composer easel-html /easel i3-Composer 研发');
27
+ }
28
+ for (const file of ['origin.json', 'licenses/EASEL.txt', 'qa/REVIEW.md', 'src/styles.css', 'tests/behavior.js', 'app.js']) {
29
+ const bytes = Buffer.from(source);
30
+ assert.equal(enterprise.brandRuntimeResource(file, bytes), bytes, file);
31
+ }
32
+ assert.ok(Buffer.isBuffer(enterprise.brandRuntimeResource('src/models/data.js', Buffer.from(source))));
33
+ });
34
+ test('generated enterprise runtime uses i3-Composer branding with one final adaptation record per changed source', t => {
35
+ const dir = initialize(t, { title: 'i3-Composer 品牌验收' });
36
+ const shell = fs.readFileSync(path.join(dir, 'app/src/shell/ConsoleShell.html'), 'utf8');
37
+ assert.match(shell, /<strong>i3-Composer<\/strong>/);
38
+ assert.doesNotMatch(shell, /EASEL/);
39
+ assert.match(fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8'), /<title>i3-Composer/);
40
+ const models = fs.readFileSync(path.join(dir, 'app/src/models/data.js'), 'utf8');
41
+ assert.match(models, /i3-Composer 研发/);
42
+ assert.doesNotMatch(models, /EASEL/);
43
+ assert.match(fs.readFileSync(path.join(dir, 'app/src/permissions/MemberEditor.html'), 'utf8'), /i3-Composer 业务角色/);
44
+ assert.doesNotMatch(read(dir, 'requirements-map.json').scope, /EASEL/);
45
+ assert.doesNotMatch(fs.readFileSync(path.join(dir, 'mocks/README.md'), 'utf8'), /EASEL/);
46
+ const origin = enterprise.verifyReference(), source = read(dir, 'design-system/source.json');
47
+ assert.equal(origin.fileCount, 326);
48
+ assert.equal(new Set(source.adaptations.map(record => record.file)).size, source.adaptations.length);
49
+ const adapted = new Map(source.adaptations.map(record => [record.file, record]));
50
+ for (const [file, expected] of Object.entries(origin.files)) {
51
+ const generated = crypto.createHash('sha256').update(fs.readFileSync(path.join(dir, 'app', file))).digest('hex');
52
+ if (generated === expected) continue;
53
+ const record = adapted.get(file);
54
+ assert.ok(record, `Unrecorded runtime adaptation: ${file}`);
55
+ assert.equal(record.source, expected, file);
56
+ assert.equal(record.generated, generated, file);
57
+ assert.ok(record.reason, file);
58
+ }
59
+ assert.match(adapted.get('index.html').reason, /i3-Composer/);
60
+ assert.match(adapted.get('src/models/data.js').reason, /i3-Composer/);
61
+ });
22
62
  test('four prototype families expose the EASEL enterprise template and explicitly defer the other three', () => {
23
63
  const entries = lib.listTemplates();
24
64
  assert.deepEqual(entries.map(x => x.id), ['enterprise-admin', 'ai-workbench', 'digital-twin', 'mobile-app']);
@@ -39,19 +39,19 @@ Skill/Command/Agent 标识、文件路径、命令参数、代码/API 名称、
39
39
 
40
40
  ### 2. 检测专业能力并继续模板预览
41
41
 
42
- 新增原型默认 `professional`(专业增强模式)。从当前宿主实际暴露的技能位置发现 UI/UX Pro Max,再运行本技能 `scripts/check-design-provider.cjs --provider <实际技能根>`。不能硬编码开发者的缓存路径、把另一宿主的安装误当当前可用,或仅凭目录存在判断可执行。需要按宿主目录检测时使用明确的 `--host` 和 `--project`。
42
+ 新增原型默认 `professional`(专业增强模式)。优先使用当前宿主暴露的 UI/UX Pro Max 技能位置,运行 `scripts/check-design-provider.cjs --provider <实际技能根>`;也可传入明确的 `--host` 和 `--project` 自动检测项目、全局 Skill 和已启用插件。项目安装的本技能可直接复用同一宿主的全局 UI/UX Pro Max,无需再复制到项目。不能硬编码缓存版本、借用另一宿主安装,或仅凭目录存在判断可执行。明确指定的 provider 失败时报告该路径,不自动换成别的副本。
43
43
 
44
- 检测不阻断模板预览,先继续展示模板并记录选择。生成前已可用时自动使用;缺失或不可执行时明确给出安装/修复与“模板模式继续”“先整理需求和模板选择”。用户选择安装后按该宿主的官方方法安装并验证;已有授权或已选择模板模式时不重复确认。安装系统运行环境需根据实际缺项处理,不能静默改动操作系统。
44
+ 检测不阻断模板预览,先继续展示模板并记录选择。自动检测遇到不可用项目副本时继续检测全局副本;运行探测通过后使用。缺少宿主信息时补充宿主或实际技能路径;已安装但停用、资源不全、查询失败、发现信息不完整时分别提示启用、修复或定位,不能一律要求重新安装。确认当前宿主未安装时提供安装入口;始终允许明确选择“模板模式继续”或“先整理需求和模板选择”,默认不降级。用户选择安装后按该宿主的官方方法安装并验证;已有授权或已选择模板模式时不重复确认。安装系统运行环境需根据实际缺项处理,不能静默改动操作系统。
45
45
 
46
46
  模板模式使用 `template`,仍使用同一套业务和浏览器验收标准。不得静默切换,交付记录必须说明实际模式。专业接入的查询与建议适配见 [expert-adapter.md](references/expert-adapter.md)。
47
47
 
48
48
  ### 3. 选择模板并固定版本
49
49
 
50
- 运行 `node <skill-root>/scripts/init-prototype.cjs templates` 查看四类:企业管理后台、AI 工作台、数字孪生大屏、移动端应用。按 availability 区分可操作模板与 planned;不能把待建设类型生成成其他类型。通过 `node <skill-root>/assets/templates/serve-gallery.cjs` 打开选择页。企业后台直接复用 EASEL 完整 HTML 审查版,当前可供人工测试,其余三类待后续实施。已有模板选择时沿用。
50
+ 运行 `node <skill-root>/scripts/init-prototype.cjs templates` 查看四类:企业管理后台、AI 工作台、数字孪生大屏、移动端应用。按 availability 区分可操作模板与 planned;不能把待建设类型生成成其他类型。通过 `node <skill-root>/assets/templates/serve-gallery.cjs` 打开选择页。企业后台使用 i3-Composer 品牌的完整 HTML 审查版,当前可供人工测试,其余三类待后续实施。已有模板选择时沿用。
51
51
 
52
52
  选择页只切换预览,不创建或确认业务实例。可生成模板使用 catalog/manifest 显式指定运行层;未知或缺失 adapter 不回退到 common。资源组织、独立模板扩展与旧实例兼容见 [template-adapters.md](references/template-adapters.md)。
53
53
 
54
- 企业后台保留原 EASEL 的全部页面、样式、控件和交互。审查整套模板时显式使用 `--purpose template-review`;为新业务生成时沿用默认 business-prototype,替换项目菜单、字段、状态、角色、数据和需求映射,完成后才将 business.status 标为 adapted。旧 classic-admin、modern-workspace、data-operations 仅保留兼容和历史样例迭代,不再作为新项目首选。框架工程保持既有设计规则,交接包含真实组件映射,HTML 审查版不等于真实 Vue/React 组件工程。
54
+ 企业后台保留原 EASEL 的全部页面、样式、控件和交互;预览及新实例的页面标题、导航、示例项目和相关说明统一使用 i3-Composer,来源快照与内部兼容标识保留。审查整套模板时显式使用 `--purpose template-review`;为新业务生成时沿用默认 business-prototype,替换项目菜单、字段、状态、角色、数据和需求映射,完成后才将 business.status 标为 adapted。旧 classic-admin、modern-workspace、data-operations 仅保留兼容和历史样例迭代,不再作为新项目首选。框架工程保持既有设计规则,交接包含真实组件映射,HTML 审查版不等于真实 Vue/React 组件工程。
55
55
 
56
56
  创建示例:
57
57
 
@@ -61,7 +61,7 @@ node <skill-root>/scripts/init-prototype.cjs --project <project-root> --id enter
61
61
  node <skill-root>/scripts/init-prototype.cjs --project <project-root> --id enterprise-review --template enterprise-admin --purpose template-review --expert-mode template
62
62
  ```
63
63
 
64
- 企业后台初始化保留完整 EASEL 示例。template-review 用于本次模板人工审查;business-prototype 初始为 needs-adaptation,未完成实际业务适配时验收守卫拒绝通过。复制的 app/qa 是历史来源材料,本次验收使用原型根 qa/。目标已存在时使用迭代流程,不覆盖。完整复用、来源核对与响应式规则见 [enterprise-admin.md](references/enterprise-admin.md)。
64
+ 企业后台初始化保留完整 i3-Composer 示例。template-review 用于本次模板人工审查;business-prototype 初始为 needs-adaptation,未完成实际业务适配时验收守卫拒绝通过。复制的 app/qa 是历史来源材料,本次验收使用原型根 qa/。目标已存在时使用迭代流程,不覆盖。完整复用、来源核对与响应式规则见 [enterprise-admin.md](references/enterprise-admin.md)。
65
65
 
66
66
  ### 4. 专业设计与业务实现
67
67
 
@@ -4,7 +4,8 @@
4
4
  {
5
5
  "id": "enterprise-admin",
6
6
  "name": "企业管理后台",
7
- "version": "0.1.0",
7
+ "brand": "i3-Composer",
8
+ "version": "0.1.1",
8
9
  "availability": "available",
9
10
  "reviewStatus": "pending-review",
10
11
  "artifactKind": "html-review",
@@ -17,7 +18,7 @@
17
18
  ],
18
19
  "preview": "enterprise-admin/index.html#/workbench",
19
20
  "fixed": [
20
- "原 EASEL 设计 token",
21
+ "既有设计 token",
21
22
  "导航和控件规范",
22
23
  "权限及状态语义"
23
24
  ],
@@ -8,7 +8,7 @@
8
8
  默认使用简体中文解释、追问及生成报告;用户明确要求其他语言时可切换。原方法正文、引用、阈值和审批条件完整保留。代码标识、路径、命令、工具参数、JSON 键/枚举与原文引用保持原样。
9
9
  <!-- i3:zh-guide:end -->
10
10
 
11
- 本模板直接复用 EASEL v0.14 的 Antdv Next HTML 审查成果,保留其全部页面、交互、控件、规则测试及来源材料。当前用于人工审查,用户尚未认可本次模板候选。
11
+ 本模板统一使用 i3-Composer 品牌,基于 EASEL v0.14 的 Antdv Next HTML 审查成果,保留其全部页面、交互、控件、规则测试及来源材料。当前用于人工审查,用户尚未认可本次模板候选。
12
12
 
13
13
  ## 设计基础
14
14
 
@@ -23,10 +23,10 @@
23
23
 
24
24
  ## 使用范围
25
25
 
26
- `template-review` 用于审查完整 EASEL 示例;保留示例菜单、业务及演示数据便于对照。`business-prototype` 需要替换目标项目的菜单、页面、字段、角色、状态、模拟数据和需求映射,未完成时记录 needs-adaptation,不能通过业务验收。
26
+ `template-review` 用于审查完整 i3-Composer 示例;保留示例菜单、业务及演示数据便于对照。`business-prototype` 需要替换目标项目的菜单、页面、字段、角色、状态、模拟数据和需求映射,未完成时记录 needs-adaptation,不能通过业务验收。
27
27
 
28
28
  UI/UX Pro Max 在既有设计规则内提供校核建议,不能擅自替换模板风格。当前验收读取原型根 `qa/`;`app/qa/` 为复制的历史来源记录,不作为本次验收。
29
29
 
30
30
  ## 内容与滚动
31
31
 
32
- 完整保留原 EASEL 内容。窄屏下允许表格和标签栏局部横向滚动,滚动条须清晰可见;不得通过隐藏、删列、裁剪内容满足页面宽度检查。原始 reference 文件保持来源字节,滚动修复放在实例 usability.css 中。
32
+ 完整保留原有内容。窄屏下允许表格和标签栏局部横向滚动,滚动条须清晰可见;不得通过隐藏、删列、裁剪内容满足页面宽度检查。原始 reference 文件保持来源字节,品牌文案只在预览响应与生成副本中适配,滚动修复放在实例 usability.css 中。
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>EASEL · Antdv Next 交互原型 · HTML 模板</title>
6
+ <title>i3-Composer · 企业管理后台原型</title>
7
7
  <link rel="icon" href="data:,">
8
8
  <link rel="stylesheet" href="reference/styles/reset.css">
9
9
  <link rel="stylesheet" href="reference/styles/antdv.css">
@@ -5,5 +5,5 @@
5
5
  "resources": {
6
6
  "root": "enterprise-admin"
7
7
  },
8
- "defaultTitle": "企业管理后台 · EASEL 完整示例"
8
+ "defaultTitle": "i3-Composer · 企业管理后台"
9
9
  }
@@ -2,7 +2,7 @@
2
2
  <html lang="zh-CN">
3
3
  <head>
4
4
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
- <title>选择原型类型 · i3 Composer</title><link rel="icon" href="data:,">
5
+ <title>选择原型类型 · i3-Composer</title><link rel="icon" href="data:,">
6
6
  <style>
7
7
  *{box-sizing:border-box}html{height:100%}body{height:100vh;height:100dvh;display:flex;flex-direction:column;margin:0;background:#f5f5f5;color:#262626;font:14px/1.5 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}header{flex:none;padding:20px 24px 16px;background:#fff;border-bottom:1px solid #e8e8e8}h1{font-size:22px;margin:0 0 4px}p{margin:0;color:#595959}.options{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:16px 0}.option{text-align:left;border:1px solid #d9d9d9;border-radius:8px;padding:12px 14px;background:#fff;color:inherit;font:inherit;cursor:pointer}.option strong{display:block;font-size:16px;margin-bottom:4px}.option span{display:block;font-size:13px;color:#595959}.option[aria-pressed=true]{border-color:#1677ff;background:#f0f7ff}.option:disabled{cursor:default;background:#fafafa;color:#8c8c8c}.option:disabled span{color:#8c8c8c}.option:focus-visible,a:focus-visible{outline:3px solid #1677ff;outline-offset:3px}.summary{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}.summary a{color:#0958d9;text-decoration:none;white-space:nowrap}.summary a:hover{text-decoration:underline}iframe{display:block;flex:1 0 240px;width:100%;min-height:240px;border:0;background:#fff}.error{color:#cf1322}
8
8
  @media(max-width:900px){header{padding:16px}.options{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.option{padding:10px}.option strong{font-size:14px}.summary{gap:8px}}
@@ -29,7 +29,7 @@ try{
29
29
  const url=new URL(entry.preview,assetRoot);if(url.origin!==location.origin)throw new Error('模板入口必须来自本地资源');
30
30
  document.querySelectorAll('.option').forEach(item=>item.setAttribute('aria-pressed',String(item===button)));
31
31
  frame.src=url.href;frame.title=entry.name+'完整交互演示';document.querySelector('#open').href=url.href;
32
- summary.textContent=entry.name+' · '+entry.version+' · 来源:'+entry.source+'。保留完整示例,当前等待人工评审。';
32
+ summary.textContent=(entry.brand?entry.brand+' · ':'')+entry.name+' · '+entry.version+'。保留完整示例,当前等待人工评审。';
33
33
  });
34
34
  document.querySelector('.options').append(button);
35
35
  if(!selected&&!button.disabled){button.click();selected=true}
@@ -2,6 +2,7 @@
2
2
  const http = require('node:http');
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { brandRuntimeResource } = require('../../scripts/enterprise-template.cjs');
5
6
  const root = path.resolve(__dirname);
6
7
  const types = { '.html': 'text/html', '.mjs': 'text/javascript', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.webp': 'image/webp' };
7
8
  const server = http.createServer((req, res) => {
@@ -11,6 +12,12 @@ const server = http.createServer((req, res) => {
11
12
  const file = path.resolve(root, '.' + relative);
12
13
  if (!file.startsWith(root + path.sep) || !fs.existsSync(file) || !fs.statSync(file).isFile() || !fs.realpathSync(file).startsWith(root + path.sep)) { res.writeHead(404).end(); return; }
13
14
  res.writeHead(200, { 'Content-Type': (types[path.extname(file)] || 'application/octet-stream') + '; charset=utf-8', 'Cache-Control': 'no-store' });
15
+ const enterpriseReference = path.join(root, 'enterprise-admin/reference');
16
+ if (file.startsWith(enterpriseReference + path.sep) && /\.(?:html|js|mjs)$/.test(file)) {
17
+ const relative = path.relative(enterpriseReference, file).split(path.sep).join('/');
18
+ res.end(brandRuntimeResource(relative, fs.readFileSync(file, 'utf8')));
19
+ return;
20
+ }
14
21
  fs.createReadStream(file).pipe(res);
15
22
  });
16
23
  server.listen(Number(process.argv[2] || 0), '127.0.0.1', () => process.stdout.write(`模板预览:http://127.0.0.1:${server.address().port}/\n`));
@@ -8,14 +8,14 @@
8
8
  默认使用简体中文解释、追问及生成报告;用户明确要求其他语言时可切换。原方法正文、引用、阈值和审批条件完整保留。代码标识、路径、命令、工具参数、JSON 键/枚举与原文引用保持原样。
9
9
  <!-- i3:zh-guide:end -->
10
10
 
11
- 企业后台的来源是工作区的 EASEL v0.14 HTML 审查成果。模板完整保留 326 个来源文件:运行页面、控件、样式、图标、规则测试、来源、许可及历史验收材料。原 history/easel-history 目录不修改。
11
+ 企业后台模板统一使用 i3-Composer 品牌,来源是工作区的 EASEL v0.14 HTML 审查成果。模板完整保留 326 个来源文件:运行页面、控件、样式、图标、规则测试、来源、许可及历史验收材料。原 history/easel-history 目录不修改。
12
12
 
13
13
  ## 完整示例与新业务
14
14
 
15
15
  - 用户要审查原来的整套后台时,使用 enterprise-admin 和 purpose=template-review,保留示例菜单与业务,便于和原版对照。
16
16
  - 用户要根据新业务做原型时,使用默认 business-prototype,先明确模块、字段、角色、状态和需求映射,修改原型副本中的相关文件;未适配状态 needs-adaptation 不能通过业务验收。
17
- - 审查完整示例不代表批准其全部 EASEL 业务,也不代表其他项目已完成业务适配。
18
- - 模板根 origin.json 保存全部来源文件的 SHA-256。reference/ 是完整来源快照,品牌转换不改写其中内容。实例仅进行明确的启动、设计 token 和本地数据命名空间接入。
17
+ - 审查完整示例不代表批准其全部示例业务,也不代表其他项目已完成业务适配。
18
+ - 模板根 origin.json 保存全部来源文件的 SHA-256。reference/ 是完整来源快照,品牌转换不改写其中内容。预览响应与新生成实例共用品牌适配,页面标题、导航、示例项目及授权说明统一显示 i3-Composer;生成副本同时接入启动、设计 token 和本地数据命名空间。
19
19
  - 资源库中的 5 个 Markdown 文件按技能命名规范存放为小写名称,原名到存放路径记录在 origin.storagePaths;内容不变,生成实例时还原原文件名,原文链接继续有效。既有验证器保持原有规则。
20
20
  - design-system/source.json 记录本实例的局部接入及用途。app/qa 保存的是来源历史记录,当前验收使用原型根 qa/。
21
21
 
@@ -8,17 +8,23 @@
8
8
  默认使用简体中文解释、追问及生成报告;用户明确要求其他语言时可切换。原方法正文、引用、阈值和审批条件完整保留。代码标识、路径、命令、工具参数、JSON 键/枚举与原文引用保持原样。
9
9
  <!-- i3:zh-guide:end -->
10
10
 
11
- 专业增强模式对应 professional,模板模式对应 template。依赖在用户实际执行的宿主中检测;推荐先使用宿主暴露的技能根,再使用明确 --host 指定的项目和全局目录。Codex 插件安装应由宿主提供的实际路径传给 --provider,不假设固定缓存版本。
11
+ 专业增强模式对应 professional,模板模式对应 template。依赖在用户实际执行的宿主中检测,安装范围互相独立:项目级 i3-ui-prototype 可以直接使用同一宿主全局安装的 UI/UX Pro Max,不要求项目再安装一份。
12
+
13
+ 优先将宿主暴露的实际技能根传给 --provider;明确指定路径时只检查该副本。自动检测传 --host 与 --project,按项目 Skill、全局 Skill、当前宿主已安装且启用的插件顺序检查,不可用副本不会遮蔽后续可用副本。Codex 通过原生 `codex plugin list --marketplace ui-ux-pro-max-skill --json` 核对启用状态与确切安装版本;Claude Code、WorkBuddy 和 CodeBuddy 通过各自 installed_plugins.json 与 enabledPlugins 核对。插件入口按 manifest 定位,不遍历缓存选最高版本,也不读取另一宿主的安装。Cursor、Gemini 保留普通 Skill 目录检测;未覆盖的插件安装方式使用宿主提供的 --provider 路径。
14
+
15
+ 检测输出包含 source、searched、attempts 与 diagnostics。未传宿主/路径返回 needs-context;插件发现失败或安装记录冲突时补充实际路径;停用返回 disabled;文件缺失或真实查询失败返回 unusable。上述情况分别定位、启用或修复,不要求重复安装。自动发现所需 CLI 不可用时,不将其当作“未安装”的证据。
12
16
 
13
17
  检测 SKILL.md、Python 查询模块与数据库,执行只读 UX 探测。安装版本/资源摘要和当前查询记录保存为来源;探测成功只说明可执行,不意味着业务建议已经采用。
14
18
 
15
- 缺失依赖时显示:当前环境不能使用 UI/UX Pro Max,可安装并验证,或选择模板模式继续;也可先整理需求和比较模板。默认不降级。用户已经选择安装或模板模式时沿用选择,不重复请求相同许可。
19
+ 确认当前宿主未安装时提示安装并验证;也可选择模板模式继续,或先整理需求和比较模板。默认不降级。用户已经选择安装或模板模式时沿用选择,不重复请求相同许可。
16
20
 
17
21
  按维护方 [官方安装说明](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill#installation) 选择当前宿主适用入口。对于未实测的宿主只说明已验证方法和限制,不猜测安装路径。选择安装后应安装完整技能,包括 Python 脚本、数据库与必要引用,重新检测可用性。不能只复制 SKILL.md。不自动安装 Python 或其他系统运行环境。
18
22
 
19
23
  使用已安装专业技能的指令和查询机制,向数据库传递抽象设计问题,不传私有业务数据。整体设计建议用 --design-system;专项交互用 --domain;实际技术栈支持时另查 --stack。HTML 模板不等于 html-tailwind,不为了查询参数而更换原型技术。
20
24
 
21
25
  ```sh
26
+ node <skill-root>/scripts/check-design-provider.cjs --host codex --project <project-root>
27
+ # 宿主提供实际路径时:
22
28
  node <skill-root>/scripts/check-design-provider.cjs --provider <uiux-root> --query "error summary validation" --domain ux
23
29
  ```
24
30
 
@@ -19,8 +19,8 @@ module.exports = {
19
19
  const purpose = options.purpose || 'business-prototype';
20
20
  const runtime = enterprise.initialize(directory, { ...options, purpose, templateRoot: context.resource('root') });
21
21
  return {
22
- metadata: { purpose, runtime, business: { status: purpose === 'template-review' ? 'reference-demo' : 'needs-adaptation', source: 'EASEL v0.14 示例业务' } },
23
- handoff: `用途:${purpose}。业务适配:${purpose === 'template-review' ? 'reference-demo' : 'needs-adaptation'}。完整保留 EASEL 示例,不能当作其他业务已完成;app/qa 为历史来源证据,当前验收读取根 qa/。\n\n`,
22
+ metadata: { purpose, runtime, business: { status: purpose === 'template-review' ? 'reference-demo' : 'needs-adaptation', source: 'i3-Composer 示例业务' } },
23
+ handoff: `用途:${purpose}。业务适配:${purpose === 'template-review' ? 'reference-demo' : 'needs-adaptation'}。完整保留 i3-Composer 示例,不能当作其他业务已完成;app/qa 为历史来源证据,当前验收读取根 qa/。\n\n`,
24
24
  };
25
25
  },
26
26
  validateRuntime(metadata, options = {}) {
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const os = require('node:os');
5
+ const { spawnSync } = require('node:child_process');
6
+
7
+ const PROVIDER = 'ui-ux-pro-max';
8
+ const MARKETPLACE = 'ui-ux-pro-max-skill';
9
+ const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
10
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+ const safeSegment = value => typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._+-]*$/.test(value) && !value.includes('..');
12
+ function ensure(condition, message) { if (!condition) throw new Error(message); }
13
+ function canonical(file) { return fs.existsSync(file) ? fs.realpathSync(file) : path.resolve(file); }
14
+
15
+ function pluginRoot(installation, manifests, fallback) {
16
+ ensure(typeof installation.installPath === 'string' && path.isAbsolute(installation.installPath), 'Plugin installPath must be absolute');
17
+ ensure(safeSegment(installation.version), 'Installed plugin version is invalid');
18
+ const root = installation.installPath;
19
+ const manifestFile = manifests.map(name => path.join(root, name)).find(file => fs.existsSync(file));
20
+ ensure(manifestFile, 'Installed plugin manifest is missing');
21
+ const manifest = json(manifestFile);
22
+ ensure(manifest.name === PROVIDER && manifest.version === installation.version, 'Installed plugin manifest name or version does not match its registration');
23
+ const declared = manifest.skills === undefined ? fallback : manifest.skills;
24
+ ensure(typeof declared === 'string' && declared && !path.isAbsolute(declared) && !declared.includes('\\'), 'Plugin skills declaration is missing or invalid');
25
+ const folder = path.resolve(root, declared), relative = path.relative(path.resolve(root), folder);
26
+ ensure(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative), 'Plugin skills declaration escapes its root');
27
+ if (fs.existsSync(folder)) {
28
+ const realRelative = path.relative(canonical(root), canonical(folder));
29
+ ensure(realRelative !== '..' && !realRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(realRelative), 'Plugin skills path escapes its root');
30
+ }
31
+ const skill = path.join(folder, PROVIDER);
32
+ ensure(fs.existsSync(path.join(skill, 'SKILL.md')), 'Installed plugin skill entrypoint is missing');
33
+ const skillRelative = path.relative(canonical(root), canonical(skill));
34
+ ensure(skillRelative !== '..' && !skillRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(skillRelative), 'Plugin skill path escapes its root');
35
+ return skill;
36
+ }
37
+
38
+ function codexCandidates(config, project, env, add, report) {
39
+ const source = 'codex-plugin';
40
+ try {
41
+ const run = spawnSync('codex', ['plugin', 'list', '--marketplace', MARKETPLACE, '--json'], { cwd: project, env, encoding: 'utf8', timeout: 10000, maxBuffer: 1024 * 1024, shell: false });
42
+ ensure(!run.error && run.status === 0, `Codex plugin discovery failed${run.error?.code ? ` (${run.error.code})` : ''}`);
43
+ const data = JSON.parse(run.stdout);
44
+ ensure(object(data) && Array.isArray(data.installed), 'Codex plugin discovery returned invalid metadata');
45
+ const records = data.installed.filter(record => record?.pluginId === `${PROVIDER}@${MARKETPLACE}`);
46
+ if (!records.length) { report(source, 'missing', 'Current Codex host has no registered UI/UX Pro Max plugin'); return; }
47
+ ensure(records.every(record => record.name === PROVIDER && record.marketplaceName === MARKETPLACE && typeof record.enabled === 'boolean' && typeof record.installed === 'boolean'), 'Codex plugin registration is invalid');
48
+ const active = records.filter(record => record.installed && record.enabled);
49
+ if (!active.length) { report(source, records.some(record => record.installed) ? 'disabled' : 'missing', 'UI/UX Pro Max is not enabled in the current Codex host'); return; }
50
+ if (active.length !== 1) { report(source, 'ambiguous', 'Multiple active Codex UI/UX Pro Max registrations require an explicit provider path'); return; }
51
+ const record = active[0];
52
+ ensure(safeSegment(record.version), 'Codex plugin version is invalid');
53
+ const installPath = path.join(config, 'plugins/cache', MARKETPLACE, PROVIDER, record.version);
54
+ add(pluginRoot({ installPath, version: record.version }, ['.codex-plugin/plugin.json', '.claude-plugin/plugin.json']), source);
55
+ } catch (error) { report(source, 'discovery-error', error.message); }
56
+ }
57
+
58
+ function registeredCandidates(host, config, project, add, report) {
59
+ const source = `${host}-plugin`, registryFile = path.join(config, 'plugins/installed_plugins.json'), settingsFile = path.join(config, 'settings.json');
60
+ try {
61
+ if (!fs.existsSync(registryFile)) { report(source, 'missing', 'Optional host plugin installation registry is absent'); return; }
62
+ const registry = json(registryFile);
63
+ ensure(registry.version === 2 && object(registry.plugins), 'Host plugin installation registry has an unsupported or invalid schema');
64
+ const entries = Object.entries(registry.plugins).filter(([id]) => id.startsWith(`${PROVIDER}@`));
65
+ if (!entries.length) { report(source, 'missing', 'Current host has no registered UI/UX Pro Max plugin'); return; }
66
+ ensure(fs.existsSync(settingsFile), 'Registered plugin enablement metadata is missing');
67
+ const settings = json(settingsFile);
68
+ ensure(object(settings) && object(settings.enabledPlugins), 'Host plugin enablement metadata is invalid');
69
+ const active = [];
70
+ let disabled = false;
71
+ for (const [id, records] of entries) {
72
+ ensure(Array.isArray(records), 'Host plugin registration must contain an installation array');
73
+ if (settings.enabledPlugins[id] !== true) { disabled = true; continue; }
74
+ for (const record of records) {
75
+ ensure(object(record), 'Host plugin installation entry is invalid');
76
+ if (record.scope === 'user') active.push(record);
77
+ else if (record.scope === 'project') {
78
+ ensure(typeof record.projectPath === 'string' && path.isAbsolute(record.projectPath), 'Project-scoped plugin requires an absolute projectPath');
79
+ if (canonical(record.projectPath) === canonical(project)) active.push(record);
80
+ } else throw new Error('Host plugin installation scope is unsupported');
81
+ }
82
+ }
83
+ if (!active.length) { report(source, disabled ? 'disabled' : 'missing', 'No enabled UI/UX Pro Max installation applies to the current project'); return; }
84
+ if (active.length !== 1) { report(source, 'ambiguous', 'Multiple applicable plugin installations require an explicit provider path'); return; }
85
+ const fallback = host === 'workbuddy' || host === 'codebuddy' ? './skills/' : undefined;
86
+ const manifests = host === 'claude' ? ['.claude-plugin/plugin.json'] : ['.codebuddy-plugin/plugin.json', '.claude-plugin/plugin.json'];
87
+ add(pluginRoot(active[0], manifests, fallback), source);
88
+ } catch (error) { report(source, 'discovery-error', error.message); }
89
+ }
90
+
91
+ function discoverProviderCandidates(options = {}) {
92
+ const result = { candidates: [], diagnostics: [] }, seen = new Set();
93
+ const report = (source, availability, reason) => result.diagnostics.push({ source, availability, reason });
94
+ const add = (root, source) => {
95
+ ensure(typeof root === 'string' && root, 'Provider root must be a nonempty path');
96
+ root = path.resolve(root);
97
+ if (!seen.has(root)) { seen.add(root); result.candidates.push({ root, source }); }
98
+ };
99
+ if (options.provider !== undefined) {
100
+ try { add(options.provider, 'explicit'); } catch (error) { report('explicit', 'discovery-error', error.message); }
101
+ return result;
102
+ }
103
+ if (options.providerRoots !== undefined) {
104
+ try {
105
+ ensure(Array.isArray(options.providerRoots), 'Host-exposed providerRoots must be an array');
106
+ for (const root of options.providerRoots) add(root, 'host-exposed');
107
+ } catch (error) { report('host-exposed', 'discovery-error', error.message); }
108
+ return result;
109
+ }
110
+ if (options.discover === false) return { ...result, contextRequired: true };
111
+ const host = options.host === 'claude-code' ? 'claude' : options.host;
112
+ if (!['codex', 'claude', 'workbuddy', 'codebuddy', 'gemini', 'cursor'].includes(host)) return { ...result, contextRequired: true };
113
+ const home = options.home || os.homedir(), env = options.env === undefined ? process.env : options.env, project = path.resolve(options.project || '.');
114
+ const configName = host === 'claude' ? 'CLAUDE_CONFIG_DIR' : `${host.toUpperCase()}_${host === 'codex' ? 'HOME' : 'CONFIG_DIR'}`;
115
+ const config = path.resolve(env[configName] || path.join(home, `.${host}`));
116
+ const projectFolder = host === 'codex' ? '.agents' : host === 'workbuddy' ? '.codebuddy' : `.${host}`;
117
+ const locations = [
118
+ [path.join(project, projectFolder, 'skills', PROVIDER), 'project-skill'],
119
+ ...(host === 'codex' ? [[path.join(home, '.agents/skills', PROVIDER), 'global-skill']] : []),
120
+ [path.join(config, 'skills', PROVIDER), 'global-skill'],
121
+ ];
122
+ for (const [root, source] of locations) if (fs.existsSync(path.join(root, 'SKILL.md'))) add(root, source);
123
+ if (host === 'codex') codexCandidates(config, project, env, add, report);
124
+ else if (['claude', 'workbuddy', 'codebuddy'].includes(host)) registeredCandidates(host, config, project, add, report);
125
+ return result;
126
+ }
127
+
128
+ module.exports = { discoverProviderCandidates };
@@ -29,9 +29,15 @@ function verifyReference(root = assetRoot) {
29
29
  assert.equal(hash(fs.readFileSync(path.join(reference, 'assets-manifest.json'))), origin.sourceManifestDigest, 'Invalid source manifest digest');
30
30
  return origin;
31
31
  }
32
+ function brandRuntimeResource(file, content) {
33
+ if (file !== 'index.html' && !/^src\/(?:[^/.][^/]*\/)*[^/]+\.(?:js|mjs|html)$/.test(file)) return content;
34
+ const text = Buffer.isBuffer(content) ? content.toString('utf8') : content;
35
+ const branded = text.replaceAll('EASEL', 'i3-Composer');
36
+ return branded === text ? content : Buffer.isBuffer(content) ? Buffer.from(branded, 'utf8') : branded;
37
+ }
32
38
  const mapping = {
33
39
  schemaVersion: 1,
34
- scope: 'EASEL 企业后台完整示例;模板审查范围,不代表其他业务已适配',
40
+ scope: 'i3-Composer 企业后台完整示例;模板审查范围,不代表其他业务已适配',
35
41
  requirements: [
36
42
  { id: 'ENT-01', pages: ['#/workbench'], roles: ['演示身份'], actions: ['查看工作台和导航'], scenarios: ['enterprise-navigation'] },
37
43
  { id: 'ENT-02', pages: ['#/models'], roles: ['有模型编辑权限的演示身份'], actions: ['新增模型', '校验和保存草稿'], scenarios: ['enterprise-form'] },
@@ -42,7 +48,7 @@ const mapping = {
42
48
  { id: 'ENT-07', pages: ['#/models'], roles: ['演示身份'], actions: ['查看空、失败等演示反馈'], scenarios: ['enterprise-feedback'] },
43
49
  { id: 'ENT-08', pages: ['app/routes.json'], roles: ['演示身份'], actions: ['打开原版已提供的全部页面入口'], scenarios: ['enterprise-routes'] },
44
50
  ],
45
- openQuestions: ['新项目的模块、字段、状态、权限及接口需按实际需求替换。原有 EASEL 示例不自动成为新业务规格。'],
51
+ openQuestions: ['新项目的模块、字段、状态、权限及接口需按实际需求替换。原有 i3-Composer 示例不自动成为新业务规格。'],
46
52
  };
47
53
  function initialize(directory, { id, title, purpose, templateRoot = assetRoot }) {
48
54
  const origin = verifyReference(templateRoot);
@@ -57,20 +63,23 @@ function initialize(directory, { id, title, purpose, templateRoot = assetRoot })
57
63
  fs.renameSync(path.join(app, stored), path.join(app, original));
58
64
  }
59
65
  const adaptations = [];
60
- for (const file of inventory(app).filter(name => /\.(?:js|mjs)$/.test(name))) {
66
+ for (const file of inventory(app).filter(name => name !== 'index.html' && /\.(?:js|mjs|html)$/.test(name))) {
61
67
  const location = path.join(app, file), old = fs.readFileSync(location, 'utf8');
62
- const next = old.replaceAll('i3-prototype-antdv-', storagePrefix);
68
+ const reasons = [], branded = brandRuntimeResource(file, old);
69
+ if (branded !== old) reasons.push('运行时可见品牌与示例数据统一为 i3-Composer');
70
+ const next = /\.(?:js|mjs)$/.test(file) ? branded.replaceAll('i3-prototype-antdv-', storagePrefix) : branded;
71
+ if (next !== branded) reasons.push('替换本地演示存储前缀,隔离不同原型实例');
63
72
  if (next !== old) {
64
73
  fs.writeFileSync(location, next);
65
- adaptations.push({ file, reason: '仅替换本地演示存储前缀,隔离不同原型实例', source: origin.files[file], generated: hash(next) });
74
+ adaptations.push({ file, reason: reasons.join(';'), source: origin.files[file], generated: hash(next) });
66
75
  }
67
76
  }
68
77
  const index = path.join(app, 'index.html'), original = fs.readFileSync(index, 'utf8');
69
78
  assert.ok(original.includes('src="app.js"'), 'Review changed EASEL boot entry before adapting');
70
- const adapted = original.replace('</head>', ' <link rel="stylesheet" href="tokens.css">\n <link rel="stylesheet" href="usability.css">\n</head>')
79
+ const adapted = brandRuntimeResource('index.html', original).replace('</head>', ' <link rel="stylesheet" href="tokens.css">\n <link rel="stylesheet" href="usability.css">\n</head>')
71
80
  .replace('src="app.js"', 'src="enterprise-bootstrap.mjs"');
72
81
  fs.writeFileSync(index, adapted);
73
- adaptations.push({ file: 'index.html', reason: '接入原有设计 token、实例配置和独立滚动适配,完整保留页面内容', source: origin.files['index.html'], generated: hash(adapted) });
82
+ adaptations.push({ file: 'index.html', reason: '默认品牌统一为 i3-Composer;接入原有设计 token、实例配置和独立滚动适配,完整保留页面内容', source: origin.files['index.html'], generated: hash(adapted) });
74
83
  fs.copyFileSync(path.join(templateRoot, 'usability.css'), path.join(app, 'usability.css'));
75
84
  fs.writeFileSync(path.join(app, 'enterprise-bootstrap.mjs'), "const response = await fetch(new URL('./config.json', import.meta.url));\nif (!response.ok) throw new Error('无法读取企业后台实例配置');\nconst config = await response.json();\ndocument.title = config.title;\nawait import('./app.js');\n");
76
85
  fs.copyFileSync(path.join(templateRoot, 'serve.cjs'), path.join(app, 'serve.cjs'));
@@ -78,11 +87,11 @@ function initialize(directory, { id, title, purpose, templateRoot = assetRoot })
78
87
  fs.writeFileSync(path.join(directory, 'design-system/MASTER.md'), fs.readFileSync(path.join(templateRoot, 'design-system.md')));
79
88
  fs.writeFileSync(path.join(directory, 'design-system/source.json'), JSON.stringify({
80
89
  schemaVersion: 1, source: origin.source, sourceVersion: origin.sourceVersion, sourceManifestDigest: origin.sourceManifestDigest,
81
- sourceFileCount: origin.fileCount, adaptations, purpose, description: '所有来源文件已复制;局部适配仅用于初始化接入。app/qa 是历史来源证据,当前验收使用原型根 qa/。',
90
+ sourceFileCount: origin.fileCount, adaptations, purpose, description: '所有来源文件已复制;运行时统一使用 i3-Composer 品牌,并保留初始化接入和存储隔离记录。app/qa 是历史来源证据,当前验收使用原型根 qa/。',
82
91
  }, null, 2) + '\n');
83
92
  fs.mkdirSync(path.join(directory, 'mocks'), { recursive: true });
84
- fs.writeFileSync(path.join(directory, 'mocks/README.md'), '# 演示数据\n\n完整 EASEL 示例的数据与规则保留在 app/src/ 各模块中,并使用实例独立的本地存储。它们用于模板审查;新业务需要按需求替换,不能当作真实服务或已完成业务适配。\n');
93
+ fs.writeFileSync(path.join(directory, 'mocks/README.md'), '# 演示数据\n\n完整 i3-Composer 示例的数据与规则保留在 app/src/ 各模块中,并使用实例独立的本地存储。它们用于模板审查;新业务需要按需求替换,不能当作真实服务或已完成业务适配。\n');
85
94
  fs.writeFileSync(path.join(directory, 'requirements-map.json'), JSON.stringify(mapping, null, 2) + '\n');
86
95
  return { kind: 'easel-html', entry: 'app/index.html', sourceVersion: origin.sourceVersion, instanceId, storagePrefix };
87
96
  }
88
- module.exports = { verifyReference, initialize };
97
+ module.exports = { verifyReference, initialize, brandRuntimeResource };
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
  const fs = require('node:fs');
3
3
  const path = require('node:path');
4
- const os = require('node:os');
5
4
  const crypto = require('node:crypto');
6
5
  const { spawnSync } = require('node:child_process');
7
6
  const templateRegistry = require('./template-adapters.cjs').createTemplateRegistry();
7
+ const { discoverProviderCandidates } = require('./design-provider-discovery.cjs');
8
8
  const hash = value => crypto.createHash('sha256').update(value).digest('hex');
9
9
  const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
10
10
  function ensure(condition, message) { if (!condition) throw new Error(message); }
@@ -47,18 +47,9 @@ function tokensCss(tokens) {
47
47
  ensure(entries.length && entries.every(([key, value]) => /^[a-z][a-z0-9-]*$/.test(key) && typeof value === 'string' && !/[;{}<>\n\r]/.test(value)), 'Invalid design token values');
48
48
  return ':root {\n' + entries.map(([key, value]) => ` --${key}: ${value};`).join('\n') + '\n}\n';
49
49
  }
50
- function providerRoots(options = {}) {
51
- if (options.providerRoots) return options.providerRoots;
52
- if (options.provider) return [options.provider];
53
- if (options.discover === false) return [];
54
- const homes = { codex: '.agents', workbuddy: '.workbuddy', codebuddy: '.codebuddy', 'claude-code': '.claude', gemini: '.gemini', cursor: '.cursor' };
55
- const folder = homes[options.host];
56
- if (!folder) return [];
57
- return [options.project && path.join(options.project, folder, 'skills/ui-ux-pro-max'), path.join(os.homedir(), folder, 'skills/ui-ux-pro-max')].filter(Boolean);
58
- }
59
50
  function providerVersion(root) {
60
51
  for (let folder = root, i = 0; i < 5; i++, folder = path.dirname(folder)) {
61
- for (const name of ['skill.json', '.claude-plugin/plugin.json', 'package.json']) {
52
+ for (const name of ['skill.json', '.claude-plugin/plugin.json', '.codebuddy-plugin/plugin.json', 'package.json']) {
62
53
  const file = path.join(folder, name);
63
54
  if (fs.existsSync(file)) { try { const version = json(file).version; if (version) return version; } catch {} }
64
55
  }
@@ -68,23 +59,30 @@ function providerVersion(root) {
68
59
  function queryProvider(root, query, options = {}) {
69
60
  const mode = options.stack ? ['--stack', options.stack] : options.designSystem ? ['--design-system'] : ['--domain', options.domain || 'ux'];
70
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
- ensure(run.status === 0, `UI/UX Pro Max query unavailable: ${run.error?.code || run.stderr.trim().slice(0, 400)}`);
62
+ ensure(run.status === 0, `UI/UX Pro Max query unavailable: ${run.error?.code || (run.stderr || '').trim().slice(0, 400)}`);
72
63
  return JSON.parse(run.stdout);
73
64
  }
74
65
  function checkDesignProvider(options = {}) {
75
- const roots = providerRoots(options).map(root => path.resolve(root));
76
- for (const root of roots) {
77
- if (!fs.existsSync(root)) continue;
66
+ const discovery = discoverProviderCandidates(options);
67
+ const attempts = [];
68
+ const roots = discovery.candidates.map(candidate => path.resolve(candidate.root));
69
+ const diagnostics = discovery.diagnostics;
70
+ if (discovery.contextRequired) return { availability: 'needs-context', searched: roots, attempts, diagnostics, reason: '请指定当前宿主 --host,或传入宿主实际提供的 --provider 技能路径;尚不能判断是否已安装' };
71
+ for (const candidate of discovery.candidates) {
72
+ const root = path.resolve(candidate.root);
73
+ if (!fs.existsSync(root)) { attempts.push({ root, source: candidate.source, availability: 'missing', reason: '技能路径不存在' }); continue; }
78
74
  const required = ['SKILL.md', 'scripts/search.py', 'scripts/core.py', 'scripts/design_system.py', ...['ux-guidelines', 'styles', 'colors', 'typography', 'products', 'landing', 'ui-reasoning'].map(name => `data/${name}.csv`)];
79
75
  const missing = required.filter(file => !fs.existsSync(path.join(root, file)));
80
- if (missing.length) return { availability: 'unusable', root, missing, reason: '技能资源不完整' };
76
+ if (missing.length) { attempts.push({ root, source: candidate.source, availability: 'unusable', missing, reason: '技能资源不完整' }); continue; }
81
77
  try {
82
78
  const probe = queryProvider(root, 'error summary validation', { python: options.python, domain: 'ux' });
83
79
  ensure(probe.domain === 'ux' && probe.results?.length, 'No verified UX probe result');
84
- return { availability: 'available', root, version: providerVersion(root), digest: hash(required.map(file => hash(fs.readFileSync(path.join(root, file)))).join('\n')) };
85
- } catch (error) { return { availability: 'unusable', root, reason: error.message }; }
80
+ attempts.push({ root, source: candidate.source, availability: 'available' });
81
+ return { availability: 'available', root, source: candidate.source, version: providerVersion(root), digest: hash(required.map(file => hash(fs.readFileSync(path.join(root, file)))).join('\n')), searched: roots, attempts, diagnostics };
82
+ } catch (error) { attempts.push({ root, source: candidate.source, availability: 'unusable', reason: error.message }); }
86
83
  }
87
- return { availability: 'missing', searched: roots, reason: '当前宿主未发现可用的 UI/UX Pro Max;可传入宿主实际提供的 --provider 路径' };
84
+ const failure = attempts.find(attempt => attempt.availability === 'unusable') || ['ambiguous', 'disabled', 'discovery-error'].map(status => diagnostics.find(item => item.availability === status)).find(Boolean);
85
+ return { ...(failure || {}), availability: failure?.availability || 'missing', searched: roots, attempts, diagnostics, reason: failure?.reason || (options.provider ? '指定的 UI/UX Pro Max 技能路径不存在,请核对 --provider' : '当前宿主未发现 UI/UX Pro Max;可安装,或传入宿主实际提供的 --provider 技能路径') };
88
86
  }
89
87
  function initPrototype(options, registry = templateRegistry) {
90
88
  const project = path.resolve(options.project || '.');
@@ -94,7 +92,13 @@ function initPrototype(options, registry = templateRegistry) {
94
92
  const expertMode = options.expertMode || 'professional';
95
93
  ensure(['professional', 'template'].includes(expertMode), 'Mode must be professional or template');
96
94
  const provider = expertMode === 'professional' ? checkDesignProvider({ ...options, project }) : null;
97
- if (provider && provider.availability !== 'available') return { status: 'needs-design-provider', provider, message: '专业增强模式需要 UI/UX Pro Max。请安装并验证,或明确选择模板模式继续。', choices: [{ id: 'install', label: '安装并继续' }, { id: 'template', label: '模板模式继续' }, { id: 'prepare', label: '先整理需求和模板选择' }] };
95
+ if (provider && provider.availability !== 'available') {
96
+ let action = { id: 'locate', label: '提供当前宿主或实际技能路径' };
97
+ if (provider.availability === 'missing' && !options.provider) action = { id: 'install', label: '安装并继续' };
98
+ if (provider.availability === 'unusable') action = { id: 'repair', label: '修复已安装技能并重试' };
99
+ if (provider.availability === 'disabled') action = { id: 'enable', label: '启用已安装插件并重试' };
100
+ return { status: 'needs-design-provider', provider, message: `专业增强模式需要可用的 UI/UX Pro Max。${provider.reason}。也可明确选择模板模式继续。`, choices: [action, { id: 'template', label: '模板模式继续' }, { id: 'prepare', label: '先整理需求和模板选择' }] };
101
+ }
98
102
  ensure(fs.existsSync(project) && fs.statSync(project).isDirectory(), 'Project directory does not exist');
99
103
  const directory = contained(project, `prototypes/${id}`);
100
104
  ensure(!fs.existsSync(directory), 'Prototype already exists / 原型已存在,使用 revise 或另选标识');