@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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude/commands/i3-prototype.md +5 -7
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini/commands/i3-prototype.toml +5 -7
- package/README.md +16 -7
- package/bin/agent-targets.json +4 -2
- package/bin/i3-composer-agent.cjs +74 -7
- package/bin/install.cjs +16 -1
- package/bin/native-layout.cjs +1 -1
- package/bin/native-migration.cjs +27 -3
- package/bin/tool-selector.cjs +24 -7
- package/bin/wizard.cjs +89 -15
- package/commands/i3-prototype.toml +5 -7
- package/docs/codebuddy-setup.md +91 -3
- package/docs/codex-setup.md +6 -0
- package/docs/other-hosts.md +1 -1
- package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
- package/docs/ui-prototype-native/USAGE.md +93 -0
- package/docs/workbuddy-setup.md +6 -0
- package/evals/cases/i3-frontend-ui-engineering.json +16 -0
- package/evals/cases/i3-ui-prototype.json +109 -0
- package/evals/cases/i3-using-agent-skills.json +28 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
- package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
- package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
- package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
- package/hooks/codebuddy-upstream.cjs +93 -0
- package/hooks/i3-prototype.cjs +12 -0
- package/hooks/manage.cjs +162 -0
- package/package.json +61 -1
- package/plugin.json +1 -1
- package/scripts/design-provider-test.js +7 -0
- package/scripts/enterprise-template-test.js +27 -1
- package/scripts/lib/prototype-fixture.cjs +17 -0
- package/scripts/prototype-cache-test.js +55 -0
- package/scripts/prototype-generation-entry-test.js +6 -0
- package/scripts/prototype-generation-evidence-test.js +10 -0
- package/scripts/prototype-generation-native-test.js +78 -0
- package/scripts/prototype-generation-test.js +99 -0
- package/scripts/prototype-inventory-test.js +43 -0
- package/scripts/prototype-locations-test.js +79 -0
- package/scripts/prototype-project-test.js +183 -0
- package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
- package/scripts/ui-prototype-content-test.js +123 -0
- package/scripts/ui-prototype-dependencies-test.js +213 -0
- package/scripts/ui-prototype-devtools-test.js +53 -0
- package/scripts/ui-prototype-execution-evidence-test.js +62 -0
- package/scripts/ui-prototype-execution-gates-test.js +66 -0
- package/scripts/ui-prototype-execution-policy-test.js +42 -0
- package/scripts/ui-prototype-execution-runtime-test.js +27 -0
- package/scripts/ui-prototype-ide-source-test.js +73 -0
- package/scripts/ui-prototype-native-test.js +504 -0
- package/scripts/ui-prototype-registration-test.js +53 -0
- package/scripts/ui-prototype-template-test.js +261 -0
- package/scripts/ui-prototype-test.js +10 -0
- package/scripts/ui-prototype-transcript-auto-test.js +67 -0
- package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
- package/skills/i3-incremental-implementation/SKILL.md +1 -1
- package/skills/i3-ui-prototype/SKILL.md +158 -19
- package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
- package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
- package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
- package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
- package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
- package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
- package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
- package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
- package/skills/i3-ui-prototype/references/content-template.md +81 -0
- package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
- package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
- package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
- package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
- package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
- package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
- package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
- package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
- package/skills/i3-ui-prototype/references/project-layout.md +92 -0
- package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
- package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
- package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
- package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
- package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
- package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
- package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
- package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
- package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
- package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
- package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
- package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
- package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
- package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
- package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
- package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
- package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
- package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
- package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
- package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
- package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
- package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
- package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
- package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
- package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
- package/skills/i3-using-agent-skills/SKILL.md +2 -5
|
@@ -0,0 +1,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)) {
|
|
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
|
-
|
|
106
|
-
const
|
|
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
|
-
|
|
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
|
-
}
|
|
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.
|
|
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')
|
|
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; }
|