@givolt/i3-composer-agent 2.3.1 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) 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 +16 -7
  8. package/bin/agent-targets.json +4 -2
  9. package/bin/i3-composer-agent.cjs +74 -7
  10. package/bin/install.cjs +16 -1
  11. package/bin/native-layout.cjs +1 -1
  12. package/bin/native-migration.cjs +27 -3
  13. package/bin/tool-selector.cjs +24 -7
  14. package/bin/wizard.cjs +89 -15
  15. package/commands/i3-prototype.toml +5 -7
  16. package/docs/codebuddy-setup.md +91 -3
  17. package/docs/codex-setup.md +6 -0
  18. package/docs/other-hosts.md +1 -1
  19. package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
  20. package/docs/ui-prototype-native/USAGE.md +93 -0
  21. package/docs/workbuddy-setup.md +6 -0
  22. package/evals/cases/i3-frontend-ui-engineering.json +16 -0
  23. package/evals/cases/i3-ui-prototype.json +109 -0
  24. package/evals/cases/i3-using-agent-skills.json +28 -0
  25. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
  26. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
  27. package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
  28. package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
  29. package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
  30. package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
  31. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
  32. package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
  33. package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
  34. package/hooks/codebuddy-upstream.cjs +93 -0
  35. package/hooks/i3-prototype.cjs +12 -0
  36. package/hooks/manage.cjs +162 -0
  37. package/package.json +61 -1
  38. package/plugin.json +1 -1
  39. package/scripts/design-provider-test.js +7 -0
  40. package/scripts/enterprise-template-test.js +27 -1
  41. package/scripts/lib/prototype-fixture.cjs +17 -0
  42. package/scripts/prototype-cache-test.js +55 -0
  43. package/scripts/prototype-generation-entry-test.js +6 -0
  44. package/scripts/prototype-generation-evidence-test.js +10 -0
  45. package/scripts/prototype-generation-native-test.js +78 -0
  46. package/scripts/prototype-generation-test.js +99 -0
  47. package/scripts/prototype-inventory-test.js +43 -0
  48. package/scripts/prototype-locations-test.js +79 -0
  49. package/scripts/prototype-project-test.js +183 -0
  50. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  51. package/scripts/ui-prototype-content-test.js +123 -0
  52. package/scripts/ui-prototype-dependencies-test.js +213 -0
  53. package/scripts/ui-prototype-devtools-test.js +53 -0
  54. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  55. package/scripts/ui-prototype-execution-gates-test.js +66 -0
  56. package/scripts/ui-prototype-execution-policy-test.js +42 -0
  57. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  58. package/scripts/ui-prototype-ide-source-test.js +73 -0
  59. package/scripts/ui-prototype-native-test.js +504 -0
  60. package/scripts/ui-prototype-registration-test.js +53 -0
  61. package/scripts/ui-prototype-template-test.js +261 -0
  62. package/scripts/ui-prototype-test.js +10 -0
  63. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  64. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  65. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  66. package/skills/i3-ui-prototype/SKILL.md +158 -19
  67. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  68. package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
  69. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
  70. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
  71. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  72. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
  73. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
  74. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
  75. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
  76. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
  77. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
  78. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
  79. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
  80. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
  81. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
  82. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
  83. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
  84. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
  85. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
  86. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
  87. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
  88. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
  89. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
  90. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
  91. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
  92. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
  93. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
  94. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
  95. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
  96. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  97. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
  98. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  99. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
  100. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
  101. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
  102. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  103. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
  104. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
  105. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
  106. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
  107. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
  108. package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
  109. package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
  110. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  111. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
  112. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  113. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  114. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
  115. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
  116. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
  117. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  118. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
  119. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
  120. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  121. package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
  122. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  123. package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
  124. package/skills/i3-ui-prototype/references/content-template.md +81 -0
  125. package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
  126. package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
  127. package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
  128. package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
  129. package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
  130. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  131. package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
  132. package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
  133. package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
  134. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  135. package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
  136. package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
  137. package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
  138. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  139. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  140. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
  141. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  142. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
  143. package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
  144. package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
  145. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  146. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  147. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
  148. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
  149. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  150. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  151. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  152. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  153. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  154. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
  155. package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
  156. package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
  157. package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
  158. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
  159. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
  160. package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
  161. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
  162. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
  163. package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
  164. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  165. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
  166. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  167. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
  168. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
  169. package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
  170. package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
  171. package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
  172. package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
  173. package/skills/i3-using-agent-skills/SKILL.md +2 -5
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+ const { fs, path, crypto, hash, assert, json, id, inside, write } = require('./native-files.cjs');
3
+ function paths(project, prototypeId) {
4
+ const root = fs.realpathSync(path.resolve(project || '.'));
5
+ assert.ok(fs.statSync(root).isDirectory(), 'Project must be a directory');
6
+ if (prototypeId !== undefined) id(prototypeId);
7
+ return { project: root, output: inside(root, 'prototypes'), index: inside(root, 'prototypes/index.json'),
8
+ legacyIndex: inside(root, '.i3-ui-prototypes.json'), lock: inside(root, 'prototypes/.index.lock'), legacyLock: inside(root, '.i3-ui-prototypes.lock'),
9
+ ...(prototypeId === undefined ? {} : { directory: inside(root, `prototypes/${prototypeId}`), preparation: inside(root, `prototypes/.preparation/${prototypeId}`), scopeFile: inside(root, `prototypes/.preparation/${prototypeId}/scope.json`) }) };
10
+ }
11
+ function moduleIds(scope = {}) {
12
+ if (scope.schemaVersion !== undefined) assert.ok([1, 2, 3].includes(scope.schemaVersion), 'Unsupported scope schemaVersion');
13
+ if (scope.moduleIds === undefined && ![2, 3].includes(scope.schemaVersion)) return [];
14
+ assert.ok(Array.isArray(scope.moduleIds), 'scope.moduleIds must be an array; use [] for unbound scope');
15
+ for (const value of scope.moduleIds) assert.ok(typeof value === 'string' && value.length <= 128 && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value), 'Invalid module identifier in scope.moduleIds; use stable PRD module IDs, not paths or requirement text');
16
+ return [...new Set(scope.moduleIds)];
17
+ }
18
+ function validate(registry, project) {
19
+ assert.equal(registry.schemaVersion, 1, 'Invalid project prototype registry version');
20
+ assert.ok(Array.isArray(registry.prototypes), 'Invalid project prototype registry');
21
+ const ids = new Set();
22
+ for (const entry of registry.prototypes) {
23
+ id(entry.id); assert.ok(!ids.has(entry.id), 'Duplicate prototype ID in registry'); ids.add(entry.id);
24
+ inside(project, entry.directory);
25
+ assert.ok(entry.appliesTo === undefined || (Array.isArray(entry.appliesTo) && entry.appliesTo.every(value => typeof value === 'string')), 'Invalid appliesTo array');
26
+ }
27
+ return registry;
28
+ }
29
+ function read(project) {
30
+ const locations = paths(project), modern = fs.existsSync(locations.index), legacy = fs.existsSync(locations.legacyIndex);
31
+ if (modern && legacy) assert.deepEqual(json(locations.index), json(locations.legacyIndex), 'Conflicting prototype indices; resolve or explicitly migrate before writing');
32
+ const file = modern || !legacy ? locations.index : locations.legacyIndex;
33
+ const bytes = fs.existsSync(file) ? fs.readFileSync(file) : null;
34
+ return { ...locations, file, legacy: !modern && legacy, duplicateLegacy: modern && legacy,
35
+ bytes, registry: validate(bytes ? JSON.parse(bytes.toString('utf8')) : { schemaVersion: 1, prototypes: [] }, locations.project) };
36
+ }
37
+ function transaction(project, operation, { allowDuplicate = false } = {}) {
38
+ const initial = read(project);
39
+ assert.ok(allowDuplicate || !initial.duplicateLegacy, 'Duplicate active indices; explicitly resolve before writing');
40
+ assert.ok(!fs.existsSync(initial.legacyLock), 'Legacy prototype writer lock is active');
41
+ const createdOutput = !fs.existsSync(initial.output), locks = [];
42
+ fs.mkdirSync(initial.output, { recursive: true });
43
+ const acquire = file => { const fd = fs.openSync(file, 'wx'); locks.push({ file, fd }); };
44
+ try {
45
+ acquire(initial.lock);
46
+ if (initial.legacy || initial.duplicateLegacy) acquire(initial.legacyLock);
47
+ else assert.ok(!fs.existsSync(initial.legacyLock), 'Legacy prototype writer lock is active');
48
+ const current = read(project);
49
+ assert.equal(current.file, initial.file, 'Prototype index location changed while acquiring lock');
50
+ assert.ok(allowDuplicate || !current.duplicateLegacy, 'Duplicate active indices');
51
+ return operation({ ...current, save() {
52
+ const latest = read(project);
53
+ assert.equal(latest.file, current.file, 'Prototype index location changed');
54
+ assert.deepEqual(latest.bytes, current.bytes, 'Prototype index changed outside the active writer');
55
+ validate(current.registry, current.project);
56
+ write(current.project, path.relative(current.project, current.file).split(path.sep).join('/'), current.registry);
57
+ } });
58
+ } finally {
59
+ for (const { file, fd } of locks.reverse()) { fs.closeSync(fd); fs.unlinkSync(file); }
60
+ if (createdOutput && fs.existsSync(initial.output) && !fs.readdirSync(initial.output).length) fs.rmdirSync(initial.output);
61
+ }
62
+ }
63
+ function query(options = {}) {
64
+ const current = read(options.project), prototypes = current.registry.prototypes;
65
+ if (options.module !== undefined) assert.ok(typeof options.module === 'string' && options.module.trim(), 'Provide a nonempty module identifier or existing legacy label');
66
+ const errors = [], invalid = new Set();
67
+ for (const entry of prototypes) {
68
+ try {
69
+ const scopeFile = inside(current.project, `${entry.directory}/inputs/scope.json`);
70
+ if (fs.existsSync(scopeFile)) {
71
+ const scope = json(scopeFile);
72
+ if (scope.schemaVersion === 2 || scope.moduleIds !== undefined) assert.deepEqual(entry.appliesTo || [], moduleIds(scope), 'appliesTo differs from fixed scope.moduleIds; explicitly revise or synchronize the index');
73
+ }
74
+ } catch (error) { invalid.add(entry.id); errors.push(`${entry.id}: ${error.message}`); }
75
+ }
76
+ const matches = options.module === undefined ? [] : prototypes.filter(entry => !invalid.has(entry.id) && entry.appliesTo?.includes(options.module));
77
+ return { status: errors.length ? 'failed' : 'checked', index: current.file, legacy: current.legacy, prototypes, errors,
78
+ matches, unbound: prototypes.filter(entry => !invalid.has(entry.id) && !entry.appliesTo?.length), selection: matches.length > 1 ? 'ambiguous' : matches.length ? 'matched' : 'none', readOnly: true,
79
+ note: 'Module association does not prove implementation, current verification or human approval.' };
80
+ }
81
+ function withInstanceAssociation(directory, scope, operation) {
82
+ const parent = path.dirname(path.resolve(directory));
83
+ if (path.basename(parent) !== 'prototypes' || (scope.schemaVersion !== 2 && scope.moduleIds === undefined)) return operation();
84
+ const project = path.dirname(parent), current = read(project);
85
+ if (!current.registry.prototypes.some(entry => path.resolve(project, entry.directory) === path.resolve(directory))) return operation();
86
+ const appliesTo = moduleIds(scope), metadataFile = inside(directory, 'prototype.json'), before = fs.readFileSync(metadataFile);
87
+ return transaction(project, store => {
88
+ const entry = store.registry.prototypes.find(item => path.resolve(project, item.directory) === path.resolve(directory));
89
+ assert.ok(entry, 'Prototype entry disappeared during revision');
90
+ try { const result = operation(); entry.appliesTo = appliesTo; store.save(); return result; }
91
+ catch (error) { write(directory, 'prototype.json', before.toString('utf8')); throw error; }
92
+ });
93
+ }
94
+ function migrate(options = {}) {
95
+ assert.ok(options.check === undefined || typeof options.check === 'boolean', '--check must be a boolean flag');
96
+ const initial = read(options.project);
97
+ if (!fs.existsSync(initial.legacyIndex)) return { status: 'current', index: initial.index, sideEffects: [] };
98
+ const original = fs.readFileSync(initial.legacyIndex), digest = hash(original);
99
+ if (options.expectedDigest !== undefined) assert.equal(options.expectedDigest, digest, 'Legacy index changed since preview');
100
+ const plan = { source: initial.legacyIndex, destination: initial.index, sourceDigest: digest, prototypes: initial.registry.prototypes.length };
101
+ if (options.check) return { status: 'migration-preview', ...plan, sideEffects: [] };
102
+ return transaction(options.project, store => {
103
+ assert.deepEqual(fs.readFileSync(store.legacyIndex), original, 'Legacy index changed during migration');
104
+ const folder = inside(store.project, `prototypes/.migration/index-${crypto.randomUUID()}`);
105
+ fs.mkdirSync(folder, { recursive: true });
106
+ const backup = inside(folder, 'legacy-index.json'), temp = inside(store.project, `prototypes/.index-${crypto.randomUUID()}.tmp`);
107
+ fs.writeFileSync(backup, original, { flag: 'wx' });
108
+ write(folder, 'source.json', { ...plan, backup, purpose: 'Source snapshot before explicit index migration' });
109
+ let created = false;
110
+ try {
111
+ if (!fs.existsSync(store.index)) {
112
+ fs.writeFileSync(temp, original, { flag: 'wx' });
113
+ try { fs.linkSync(temp, store.index); created = true; }
114
+ finally { if (fs.existsSync(temp)) fs.unlinkSync(temp); }
115
+ } else assert.deepEqual(fs.readFileSync(store.index), original, 'Destination index already exists with different content');
116
+ assert.deepEqual(fs.readFileSync(backup), original, 'Migration backup mismatch');
117
+ assert.deepEqual(fs.readFileSync(store.legacyIndex), original, 'Legacy index changed before commit');
118
+ fs.unlinkSync(store.legacyIndex);
119
+ return { status: 'migrated', ...plan, backup };
120
+ } catch (error) {
121
+ if (created && fs.existsSync(store.index) && fs.readFileSync(store.index).equals(original)) fs.unlinkSync(store.index);
122
+ throw error;
123
+ }
124
+ }, { allowDuplicate: true });
125
+ }
126
+ module.exports = { paths, moduleIds, read, transaction, query, withInstanceAssociation, migrate };
@@ -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; });
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Explicit configuration lifecycle. check is read-only and never invokes the installer.
4
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
5
+ const R=require('./codebuddy-registration.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs');
6
+ function context(options) {
7
+ assert.equal(options.host||'codebuddy','codebuddy','A+B supports CodeBuddy only');
8
+ const project=fs.realpathSync(path.resolve(options.project||'.')),scope=options.scope;
9
+ assert.ok(['project','global'].includes(scope),'Specify project or global installation scope');
10
+ const base=scope==='project'?path.join(project,'.codebuddy'):C.globalBase(options);
11
+ const recordText=R.fileIO.read(path.join(base,'state/i3-prototype/installation.json'));
12
+ const record=recordText?JSON.parse(recordText):null;
13
+ const unified=path.join(base,'i3-composer-agent/hooks/i3-prototype.cjs');
14
+ const scriptPath=options.entryPath||(fs.existsSync(unified)?unified:path.join(base,'skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs'));
15
+ return {project,scope,base,record,scriptPath};
16
+ }
17
+ function prepare(options,runtime={}) {
18
+ const current=runtime.current||context(options),readText=runtime.readText||R.fileIO.read,action=options.action||'keep';
19
+ assert.ok(['keep','enable','disable'].includes(action),'Invalid Hook configuration action');
20
+ if(['keep','disable'].includes(action)&&!current.record){
21
+ if(action==='disable'){const settings=R.config(readText(path.join(current.base,'settings.json')));assert.ok(!Object.values(settings.hooks||{}).flat().some(g=>g.hooks.some(h=>h.command?.includes(R.OWNER))),'Unknown owned-looking Hook has no ownership record; preserve it for review');}
22
+ return {summary:{status:'not-enabled',scope:current.scope,changed:false},unchanged:true};
23
+ }
24
+ const product=options.product||current.record?.product;assert.ok(['ide','cli'].includes(product),'Use --codebuddy-product ide or cli when enabling Hooks');
25
+ if(action!=='disable') {
26
+ const manifest=readText(path.join(current.base,'.i3-composer-agent.json'));
27
+ if(manifest)assert.ok(!JSON.parse(manifest).agents?.includes('workbuddy'),'Shared CodeBuddy/WorkBuddy registration cannot safely identify the product; preserve settings and use an exclusive CodeBuddy scope');
28
+ assert.ok(!options.agents?.includes('workbuddy')||current.scope!=='project','Do not enable CodeBuddy audit in a shared WorkBuddy project target');
29
+ }
30
+ const explicit=options.transcriptRoot,roots=explicit==='auto'?[]:explicit?[path.resolve(explicit)]:current.record?.transcriptRoots||[];
31
+ const mode=explicit?(explicit==='auto'?'auto':'manual'):current.record?.transcriptMode||(roots.length?'manual':'auto');
32
+ if(action!=='disable') {
33
+ for(const root of roots){const relative=path.relative(current.project,root);assert.ok(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative),'Native log root must be outside the business project; QA exports are not native provenance');}
34
+ }
35
+ const settingsText=readText(path.join(current.base,'settings.json')),settings=R.config(settingsText);
36
+ if(action!=='disable')assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents automatic auditing; preserve the existing policy');
37
+ return R.plan({action,...current,product,nodePath:process.execPath,scriptDigest:runtime.scriptDigest||C.bundle(options.entrySource||(path.basename(current.scriptPath)==='i3-prototype.cjs'?current.scriptPath:path.join(__dirname,'hosts/codebuddy.cjs'))),currentText:settingsText,
38
+ transcriptMode:mode,transcriptRoots:roots,productVersion:options.productVersion||current.record?.productVersion||'unreported'});
39
+ }
40
+ function configure(options) {
41
+ const change=prepare(options);if(change.unchanged||options.preview)return change.summary;
42
+ return R.apply(change);
43
+ }
44
+ function check(options,runtime={}) {
45
+ const current=runtime.current||context(options),profile=require('../assets/hosts/codebuddy/profile.json');
46
+ const result={scope:current.scope,base:current.base,scriptPath:current.scriptPath,filesPresent:fs.existsSync(current.scriptPath),profile:profile.id,nativeValidation:profile.nativeValidation,readOnly:true,sideEffects:[]};
47
+ if(process.platform==='win32')return {...result,status:'unsupported',reason:'CodeBuddy Hook quoting on Windows has not been validated'};
48
+ if(!current.record){
49
+ const settings=R.config(R.fileIO.read(path.join(current.base,'settings.json')));
50
+ if(Object.values(settings.hooks||{}).flat().some(group=>group.hooks.some(h=>h.command?.includes(R.OWNER))))return {...result,status:'failed',errors:['Unknown prototype Hook registration has no ownership record; preserve it for review']};
51
+ return {...result,status:result.filesPresent?'files-present':'not-enabled'};
52
+ }
53
+ try {
54
+ const record=(runtime.registration||C.registration)(current.base),effective=(runtime.select||C.select)({project:current.project,product:record.product,home:options.home,env:options.env});
55
+ Object.assign(result,{product:record.product,productVersion:record.productVersion,registrationDigest:record.registrationDigest,effective:effective?.base===current.base,transcriptRoots:record.transcriptRoots,
56
+ transcript:{mode:record.transcriptMode||(record.transcriptRoots?.length?'manual':'auto'),status:'awaiting-native-event',invocationVerified:false}});
57
+ let observation=null;
58
+ const parent=path.join(current.base,'state/i3-prototype',C.projectKey(current.project));
59
+ const entries=runtime.entries||[];
60
+ if(!runtime.entries&&fs.existsSync(parent))for(const name of fs.readdirSync(parent)) {
61
+ if(!/^[a-f0-9]{24}$/.test(name))continue;
62
+ const raw=R.fileIO.read(path.join(parent,name,'session.json'));if(!raw)continue;
63
+ entries.push(...Object.values(JSON.parse(raw).bindings||{}));
64
+ }
65
+ for(const entry of entries)if(entry.project===current.project&&entry.registrationDigest===record.registrationDigest&&entry.settingsDigest===record.settingsDigest&&(!observation||entry.lastHook.at>observation.lastHook.at))observation=entry;
66
+ if(observation) {
67
+ result.transcript.path=observation.source.path;
68
+ C.prefix(observation.source.path,observation.source.bytes,observation.source.digest);
69
+ C.source({session_id:observation.sessionId,transcript_path:observation.source.path},record,current.project);
70
+ Object.assign(result.transcript,{status:'verified',invocationVerified:true});
71
+ result.observation={sessionId:observation.sessionId,projectKey:observation.projectKey,runId:observation.runId,event:observation.lastHook.event,at:observation.lastHook.at};
72
+ }
73
+ const recent=observation&&Date.now()-Date.parse(observation.lastHook.at)>=0&&Date.now()-Date.parse(observation.lastHook.at)<30*60*1000;
74
+ return {...result,status:result.effective&&recent?'active':record.product==='cli'?'pending-host-review':'registered',
75
+ activationScope:'Only the displayed project/session/configuration and adapter bytes; not all projects or business acceptance',
76
+ next:record.product==='cli'?'Review /hooks in CodeBuddy Code, then use a new session.':'Reload CodeBuddy and verify a real prototype write task.'};
77
+ }catch(error){if(result.transcript)Object.assign(result.transcript,{status:'unverified',invocationVerified:false});return {...result,status:'failed',errors:[error.message]};}
78
+ }
79
+ function main(argv=process.argv.slice(2)) {
80
+ const central=[path.resolve(__dirname,'../../../i3-composer-agent/hooks/manage.cjs'),path.resolve(__dirname,'../../../hooks/manage.cjs')].find(file=>fs.existsSync(file));
81
+ if(central)return require(central).main([...argv,'--groups','prototype']);
82
+ const options={action:argv.shift()||'check',host:'codebuddy',project:process.cwd()};
83
+ while(argv.length){const arg=argv.shift();assert.ok(arg.startsWith('--')&&argv[0]&&!argv[0].startsWith('--'),'Hook option needs a value');const key=arg.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());assert.ok(['host','scope','project','product','productVersion','transcriptRoot'].includes(key),'Unknown Hook option');options[key]=argv.shift();}
84
+ return options.action==='check'?check(options):configure(options);
85
+ }
86
+ module.exports={context,globalBase:C.globalBase,prepare,configure,check,main};
87
+ if(require.main===module)try{const result=main();process.stdout.write(JSON.stringify(result,null,2)+'\n');if(result.status==='failed')process.exitCode=1;}catch(error){process.stderr.write(error.message+'\n');process.exitCode=1;}
@@ -5,6 +5,7 @@ const crypto = require('node:crypto');
5
5
  const { spawnSync } = require('node:child_process');
6
6
  const templateRegistry = require('./template-adapters.cjs').createTemplateRegistry();
7
7
  const { discoverProviderCandidates } = require('./design-provider-discovery.cjs');
8
+ const projectRegistry = require('./project-registry.cjs');
8
9
  const hash = value => crypto.createHash('sha256').update(value).digest('hex');
9
10
  const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
10
11
  function ensure(condition, message) { if (!condition) throw new Error(message); }
@@ -48,17 +49,27 @@ function tokensCss(tokens) {
48
49
  return ':root {\n' + entries.map(([key, value]) => ` --${key}: ${value};`).join('\n') + '\n}\n';
49
50
  }
50
51
  function providerVersion(root) {
52
+ const receipt = path.join(root, 'UPSTREAM-ORIGIN.json');
53
+ if (fs.existsSync(receipt)) {
54
+ try { const source = json(receipt); if (source.packageName === 'ui-ux-pro-max-cli' && typeof source.version === 'string') return source.version; } catch {}
55
+ }
51
56
  for (let folder = root, i = 0; i < 5; i++, folder = path.dirname(folder)) {
52
57
  for (const name of ['skill.json', '.claude-plugin/plugin.json', '.codebuddy-plugin/plugin.json', 'package.json']) {
53
58
  const file = path.join(folder, name);
54
- if (fs.existsSync(file)) { try { const version = json(file).version; if (version) return version; } catch {} }
59
+ if (fs.existsSync(file)) {
60
+ try {
61
+ const metadata = json(file);
62
+ if (name === 'package.json' && !['ui-ux-pro-max', 'ui-ux-pro-max-cli'].includes(metadata.name)) continue;
63
+ if (metadata.version) return metadata.version;
64
+ } catch {}
65
+ }
55
66
  }
56
67
  }
57
68
  return 'unversioned';
58
69
  }
59
70
  function queryProvider(root, query, options = {}) {
60
71
  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 });
72
+ 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
73
  ensure(run.status === 0, `UI/UX Pro Max query unavailable: ${run.error?.code || (run.stderr || '').trim().slice(0, 400)}`);
63
74
  return JSON.parse(run.stdout);
64
75
  }
@@ -85,6 +96,7 @@ function checkDesignProvider(options = {}) {
85
96
  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
97
  }
87
98
  function initPrototype(options, registry = templateRegistry) {
99
+ if (options.framework === 'vue-antdv-next' || options.template === 'admin-list') return require('./native-pilot-lib.cjs').createPrototype(options);
88
100
  const project = path.resolve(options.project || '.');
89
101
  const id = identifier(options.id);
90
102
  const selected = registry.resolve(options.template || registry.defaultId());
@@ -102,11 +114,8 @@ function initPrototype(options, registry = templateRegistry) {
102
114
  ensure(fs.existsSync(project) && fs.statSync(project).isDirectory(), 'Project directory does not exist');
103
115
  const directory = contained(project, `prototypes/${id}`);
104
116
  ensure(!fs.existsSync(directory), 'Prototype already exists / 原型已存在,使用 revise 或另选标识');
105
- const lockPath = contained(project, '.i3-ui-prototypes.lock');
106
- const lock = fs.openSync(lockPath, 'wx');
107
- try {
108
- const registryPath = contained(project, '.i3-ui-prototypes.json');
109
- const registry = fs.existsSync(registryPath) ? json(registryPath) : { schemaVersion: 1, prototypes: [] };
117
+ return projectRegistry.transaction(project, store => {
118
+ const registry = store.registry;
110
119
  ensure(registry.schemaVersion === 1 && Array.isArray(registry.prototypes), 'Invalid project prototype registry');
111
120
  ensure(!registry.prototypes.some(entry => entry.id === id), 'Prototype registry already contains identifier');
112
121
  const preset = selected.preset;
@@ -131,14 +140,15 @@ function initPrototype(options, registry = templateRegistry) {
131
140
  fs.renameSync(staging, directory);
132
141
  created = true;
133
142
  registry.prototypes.push({ id, directory: `prototypes/${id}`, appliesTo: [] });
134
- atomicJson(project, '.i3-ui-prototypes.json', registry);
143
+ store.save();
135
144
  } catch (error) { if (created) fs.rmSync(directory, { recursive: true, force: true }); throw error; }
136
145
  finally { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); }
137
146
  return { status: 'created', directory, template: template.id, expertMode };
138
- } finally { fs.closeSync(lock); fs.unlinkSync(lockPath); }
147
+ });
139
148
  }
140
149
  function contentManifest(directory, registry = templateRegistry) {
141
150
  const metadata = json(contained(directory, 'prototype.json'));
151
+ if (metadata.schemaVersion === 2) return require('./native-pilot-lib.cjs').manifest(directory);
142
152
  const identity = { id: metadata.id, revision: metadata.revision, template: metadata.template, expertMode: metadata.expertMode, artifactKind: metadata.artifactKind };
143
153
  for (const field of registry.forInstance(metadata).runtime.identityFields) if (Object.hasOwn(metadata, field)) identity[field] = metadata[field];
144
154
  const paths = ['requirements-map.json', 'qa/design-advice.json', ...['app', 'mocks', 'design-system'].flatMap(folder => files(directory, folder))];
@@ -146,6 +156,7 @@ function contentManifest(directory, registry = templateRegistry) {
146
156
  return { schemaVersion: 1, revision: metadata.revision, identity, files: entries, digest: hash(JSON.stringify({ identity, files: entries })) };
147
157
  }
148
158
  function checkPrototype(directory, options = {}, registry = templateRegistry) {
159
+ if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').inspect(directory, options);
149
160
  const errors = [];
150
161
  try {
151
162
  const metadata = json(contained(directory, 'prototype.json'));
@@ -164,7 +175,8 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
164
175
  ensure(requirement.id && !ids.has(requirement.id), 'Requirement identifiers must be unique'); ids.add(requirement.id);
165
176
  ensure(requirement.pages?.length && requirement.scenarios?.length && requirement.roles?.length && requirement.actions?.length, `Incomplete business mapping: ${requirement.id}`);
166
177
  }
167
- const runtimeFiles = selected.runtime.requiredFiles;
178
+ const runtimeFiles = [...selected.runtime.requiredFiles];
179
+ if (metadata.runtime?.offlineVersion === 1) runtimeFiles.push('app/offline.js');
168
180
  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
181
  const browserFiles = files(directory, 'app').filter(file => /\.(?:html|mjs|js|css)$/.test(file) && !selected.runtime.excludedBrowserPrefixes.some(prefix => file.startsWith(prefix)));
170
182
  for (const file of browserFiles) {
@@ -179,7 +191,7 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
179
191
  const manifest = contentManifest(directory, registry);
180
192
  if (metadata.expertMode === 'professional' && options.acceptance) {
181
193
  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');
194
+ 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
195
  }
184
196
  if (options.acceptance) {
185
197
  const evidence = json(contained(directory, 'qa/browser.json'));
@@ -193,6 +205,7 @@ function checkPrototype(directory, options = {}, registry = templateRegistry) {
193
205
  } catch (error) { errors.push(error.message); return { status: 'failed', errors }; }
194
206
  }
195
207
  function freezeBaseline(directory, confirmation, registry = templateRegistry) {
208
+ if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').confirmBaseline(directory, confirmation);
196
209
  const current = contentManifest(directory, registry);
197
210
  ensure(confirmation.revision === current.revision && confirmation.digest === current.digest, 'Human confirmation must match current changed content / 确认版本及内容必须一致');
198
211
  ensure(confirmation.reviewer?.trim() && confirmation.statement?.trim() && confirmation.source?.trim(), 'Explicit human confirmation / 人类确认 required');
@@ -213,12 +226,16 @@ function freezeBaseline(directory, confirmation, registry = templateRegistry) {
213
226
  const metadata = json(contained(directory, 'prototype.json'));
214
227
  metadata.review = { status: 'approved', revision: current.revision, digest: current.digest, source: confirmation.source };
215
228
  metadata.verification = { status: 'passed', digest: current.digest };
216
- if (metadata.expertMode === 'professional') metadata.expert.status = 'used';
229
+ if (metadata.expertMode === 'professional') {
230
+ const advice = json(contained(directory, 'qa/design-advice.json'));
231
+ metadata.expert.status = advice.records.some(record => ['adopted', 'adjusted'].includes(record.decision)) ? 'used' : advice.records.every(record => record.decision === 'no-match') ? 'no-match' : 'reviewed';
232
+ }
217
233
  atomicJson(directory, 'prototype.json', metadata);
218
234
  } finally { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); }
219
235
  return current;
220
236
  }
221
237
  function compareBaseline(directory, registry = templateRegistry) {
238
+ if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').compareBaseline(directory);
222
239
  if (!fs.existsSync(contained(directory, 'baseline.json'))) return { status: 'no-baseline', changes: [], protectedChanges: [] };
223
240
  const baseline = json(contained(directory, 'baseline.json'));
224
241
  const snapshot = contained(directory, baseline.directory);
@@ -238,7 +255,8 @@ function compareBaseline(directory, registry = templateRegistry) {
238
255
  if (selectionChanged) protectedChanges.push('template-or-expert-mode');
239
256
  return { status: changes.length || protectedChanges.length ? 'changed' : 'unchanged', baselineRevision: baseline.revision, currentRevision: current.revision, changes, protectedChanges };
240
257
  }
241
- function revisePrototype(directory, revision) {
258
+ function revisePrototype(directory, revision, options = {}) {
259
+ if (json(contained(directory, 'prototype.json')).schemaVersion === 2) return require('./native-pilot-lib.cjs').revise(directory, { ...options, revision });
242
260
  identifier(revision);
243
261
  const metadata = json(contained(directory, 'prototype.json'));
244
262
  ensure(revision !== metadata.revision && !fs.existsSync(contained(directory, `baselines/${revision}`)), 'Use a new unused revision identifier');
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const os = require('node:os');
5
+ const crypto = require('node:crypto');
6
+ // Reviewed mirror of i3's installer target table; maintenance tests require byte equality.
7
+ const registry = require('../assets/optional-skills/host-targets.json');
8
+ const aliases = { claude: 'claude-code', gemini: 'gemini-cli', copilot: 'github-copilot' };
9
+ function globalSkillsDir(agent, home, env) {
10
+ const config = agent.globalSkillsDir;
11
+ if (!config) return null;
12
+ let base = home;
13
+ if (config.base === 'config') base = env.XDG_CONFIG_HOME && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(home, '.config');
14
+ if (config.base === 'env') base = env[config.env]?.trim() || path.join(home, config.fallback);
15
+ if (config.env === 'CODEBUDDY_CONFIG_DIR' && (!path.isAbsolute(base) || /[\x00\r\n]/.test(base))) throw new Error('CODEBUDDY_CONFIG_DIR must be an absolute safe directory');
16
+ if (config.alternatives && !fs.existsSync(path.join(home, path.dirname(config.path)))) {
17
+ const alternative = config.alternatives.find(relative => fs.existsSync(path.join(home, path.dirname(relative))));
18
+ if (alternative) return path.join(home, alternative);
19
+ }
20
+ return path.resolve(base, config.path);
21
+ }
22
+ function installedContext() {
23
+ const file = path.resolve(__dirname, '../../../.i3-composer-agent.json');
24
+ if (!fs.existsSync(file)) return {};
25
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
26
+ if (data.name !== 'i3-composer-agent' || !Array.isArray(data.agents)) throw new Error('Invalid i3 installation context');
27
+ return { hosts: data.agents, scope: data.scope };
28
+ }
29
+ function resolveLocations(options = {}) {
30
+ const installed = installedContext();
31
+ if (!options.host && installed.hosts?.length > 1) throw new Error('Multiple installed hosts; specify --host');
32
+ const selected = options.host || installed.hosts?.[0] || 'codebuddy';
33
+ const host = aliases[selected] || selected, scope = options.scope || 'project';
34
+ const agent = registry.agents.find(item => item.id === host);
35
+ if (!agent) throw new Error(`Unknown host: ${host}`);
36
+ if (!['project', 'global'].includes(scope)) throw new Error('Use scope project or global');
37
+ const project = fs.realpathSync(path.resolve(options.project || '.'));
38
+ if (!fs.statSync(project).isDirectory()) throw new Error('Project must be a directory');
39
+ const home = path.resolve(options.home || os.homedir()), env = options.env || process.env;
40
+ const projectSkills = path.resolve(project, agent.projectSkillsDir), globalSkills = globalSkillsDir(agent, home, env);
41
+ if (scope === 'global' && !globalSkills) throw new Error(`${agent.label}: project installation only`);
42
+ const skillsDirectory = scope === 'project' ? projectSkills : globalSkills, base = path.dirname(skillsDirectory);
43
+ const projectBase = path.dirname(projectSkills), globalBase = globalSkills ? path.dirname(globalSkills) : null;
44
+ // Some hosts put skills directly in the workspace; tools still get a host directory.
45
+ const projectToolsBase = projectBase === project ? path.join(project, `.${host}`) : projectBase;
46
+ const projectKey = `${path.basename(project).replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 32) || 'project'}-${crypto.createHash('sha256').update(project).digest('hex').slice(0, 16)}`;
47
+ const cacheBase = path.join(scope === 'global' ? globalBase : projectToolsBase, 'cache/i3-prototype');
48
+ return { host, scope, project, home, skillsDirectory, base, projectSkills, globalSkills,
49
+ projectBase, globalBase, projectToolsBase, projectKey, cacheDirectory: scope === 'global' ? path.join(cacheBase, projectKey) : cacheBase,
50
+ toolsDirectory: path.join(scope === 'global' ? globalBase : projectToolsBase, 'tools/i3-prototype') };
51
+ }
52
+ function scopeBases(context) {
53
+ return context.scope === 'global' ? [[context.globalBase, 'global']] : [[context.projectBase, 'project'], ...(context.globalBase ? [[context.globalBase, 'global']] : [])];
54
+ }
55
+ function toolCandidates(context, options = {}) {
56
+ if (options.tool !== undefined) {
57
+ if (!path.isAbsolute(options.tool)) throw new Error('--tool requires an absolute antDv package directory');
58
+ return [{ root: path.resolve(options.tool), source: 'explicit-tool' }];
59
+ }
60
+ return [
61
+ ...toolBases(context).map(([base, scope]) => ({ root: path.join(base, 'tools/i3-prototype/antdv-cli/node_modules/@antdv-next/cli'), source: `${scope}-tool` })),
62
+ ...(context.scope === 'project' ? ['.prototype-tools/antdv-cli/node_modules/@antdv-next/cli', '.prototype-tools/node_modules/@antdv-next/cli']
63
+ .map(relative => ({ root: path.join(context.project, relative), source: 'legacy-project-tool' })) : []),
64
+ ].filter((item, index, all) => all.findIndex(other => other.root === item.root) === index);
65
+ }
66
+ function toolBases(context) {
67
+ return context.scope === 'global' ? [[context.globalBase, 'global']] : [[context.projectToolsBase, 'project'], ...(context.globalBase ? [[context.globalBase, 'global']] : [])];
68
+ }
69
+ 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; }