aub-workspace 0.3.0
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/LICENSE +201 -0
- package/README.md +23 -0
- package/bin/aub-workspace.mjs +246 -0
- package/package.json +32 -0
- package/vendor/aub/apps/editor/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
- package/vendor/aub/apps/editor/dist/assets/angular-importer.lib-dB_jK4mR.js +32 -0
- package/vendor/aub/apps/editor/dist/assets/canvas-tools-CuYC7cA2.js +364 -0
- package/vendor/aub/apps/editor/dist/assets/design-bridge.lib-DJvaK6AX.js +1 -0
- package/vendor/aub/apps/editor/dist/assets/export-agent-prompt.lib-BsP0KNqo.js +2 -0
- package/vendor/aub/apps/editor/dist/assets/export-md.lib-DdmdeWgO.js +3 -0
- package/vendor/aub/apps/editor/dist/assets/handoff-package.lib-DDYpcEma.js +20 -0
- package/vendor/aub/apps/editor/dist/assets/implementation-report.lib-CmsSB_8s.js +1 -0
- package/vendor/aub/apps/editor/dist/assets/index-BCH-ek3h.js +2 -0
- package/vendor/aub/apps/editor/dist/assets/index-lAnc928Q.css +1 -0
- package/vendor/aub/apps/editor/dist/assets/index-vt1nM1M4.js +507 -0
- package/vendor/aub/apps/editor/dist/assets/jszip.min-CRfXyL92.js +12 -0
- package/vendor/aub/apps/editor/dist/assets/react-vendor-ByX9Pqse.js +40 -0
- package/vendor/aub/apps/editor/dist/brand/android-chrome-192x192.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/android-chrome-512x512.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/app-icon-1024.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/app-icon-192.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/app-icon-512.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/apple-touch-icon.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/aub-logo-mark.svg +28 -0
- package/vendor/aub/apps/editor/dist/brand/favicon-16x16.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/favicon-32x32.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/favicon-48x48.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/favicon.ico +0 -0
- package/vendor/aub/apps/editor/dist/brand/favicon.svg +9 -0
- package/vendor/aub/apps/editor/dist/brand/maskable-icon-512.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/mstile-150x150.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/safari-pinned-tab.svg +8 -0
- package/vendor/aub/apps/editor/dist/browserconfig.xml +9 -0
- package/vendor/aub/apps/editor/dist/index.html +22 -0
- package/vendor/aub/apps/editor/dist/manifest.webmanifest +28 -0
- package/vendor/aub/apps/editor/dist/template-previews/admin-table.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/booking.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/calendar.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/catalog.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/chat.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/checkout.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/crm.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/dashboard.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/feed.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/files.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/kanban.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/landing.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/mail.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/onboarding.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/pricing.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/product-detail.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/settings.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/wiki.png +0 -0
- package/vendor/aub/apps/mcp-server/dist/aub.js +15 -0
- package/vendor/aub/apps/mcp-server/dist/context.js +1 -0
- package/vendor/aub/apps/mcp-server/dist/http.js +123 -0
- package/vendor/aub/apps/mcp-server/dist/index.js +23 -0
- package/vendor/aub/apps/mcp-server/dist/repo.js +17 -0
- package/vendor/aub/apps/mcp-server/dist/schema.js +42 -0
- package/vendor/aub/apps/mcp-server/dist/server.js +80 -0
- package/vendor/aub/apps/mcp-server/dist/tools/approve-component-candidate.js +27 -0
- package/vendor/aub/apps/mcp-server/dist/tools/diff-blueprints.js +27 -0
- package/vendor/aub/apps/mcp-server/dist/tools/export-handoff.js +87 -0
- package/vendor/aub/apps/mcp-server/dist/tools/export-prompt.js +35 -0
- package/vendor/aub/apps/mcp-server/dist/tools/export-template-authoring-prompt.js +13 -0
- package/vendor/aub/apps/mcp-server/dist/tools/generate-template-from-source.js +25 -0
- package/vendor/aub/apps/mcp-server/dist/tools/get-aub-session.js +13 -0
- package/vendor/aub/apps/mcp-server/dist/tools/get-blueprint.js +28 -0
- package/vendor/aub/apps/mcp-server/dist/tools/get-project.js +45 -0
- package/vendor/aub/apps/mcp-server/dist/tools/get-workspace-status.js +10 -0
- package/vendor/aub/apps/mcp-server/dist/tools/import-design-bridge.js +62 -0
- package/vendor/aub/apps/mcp-server/dist/tools/list-blueprints.js +11 -0
- package/vendor/aub/apps/mcp-server/dist/tools/list-projects.js +11 -0
- package/vendor/aub/apps/mcp-server/dist/tools/lock-blueprint.js +33 -0
- package/vendor/aub/apps/mcp-server/dist/tools/migrate-blueprint.js +38 -0
- package/vendor/aub/apps/mcp-server/dist/tools/resolve-component.js +51 -0
- package/vendor/aub/apps/mcp-server/dist/tools/scaffold-blueprint.js +53 -0
- package/vendor/aub/apps/mcp-server/dist/tools/scan-project-ui.js +18 -0
- package/vendor/aub/apps/mcp-server/dist/tools/submit-report.js +48 -0
- package/vendor/aub/apps/mcp-server/dist/tools/update-aub-session.js +14 -0
- package/vendor/aub/apps/mcp-server/dist/tools/validate-blueprint.js +67 -0
- package/vendor/aub/apps/mcp-server/dist/tools/validate-project.js +74 -0
- package/vendor/aub/apps/mcp-server/dist/tools/write-blueprint.js +72 -0
- package/vendor/aub/apps/mcp-server/dist/workspace.js +138 -0
- package/vendor/aub/docs/agent-handoff.md +85 -0
- package/vendor/aub/docs/agent-handoff.zh-Hant.md +85 -0
- package/vendor/aub/docs/template-authoring-agent.md +86 -0
- package/vendor/aub/schema/aub-ci.schema.json +34 -0
- package/vendor/aub/schema/aub.registry.schema.json +118 -0
- package/vendor/aub/schema/design-bridge.schema.json +44 -0
- package/vendor/aub/schema/implementation-report.schema.json +93 -0
- package/vendor/aub/schema/project-types.ts +72 -0
- package/vendor/aub/schema/registry/components.json +118 -0
- package/vendor/aub/schema/types.js +13 -0
- package/vendor/aub/schema/types.ts +348 -0
- package/vendor/aub/schema/ui-blueprint-lock.schema.json +61 -0
- package/vendor/aub/schema/ui-blueprint.schema.json +1339 -0
- package/vendor/aub/schema/ui-project.schema.json +139 -0
- package/vendor/aub/scripts/agent-implementation-benchmark.lib.mjs +125 -0
- package/vendor/aub/scripts/angular-importer.lib.mjs +982 -0
- package/vendor/aub/scripts/check-editor-bundle-budget.mjs +36 -0
- package/vendor/aub/scripts/ci-verify.lib.mjs +256 -0
- package/vendor/aub/scripts/ci-verify.mjs +45 -0
- package/vendor/aub/scripts/create-authoring-kit.mjs +84 -0
- package/vendor/aub/scripts/create-implementation-report.mjs +24 -0
- package/vendor/aub/scripts/design-bridge.lib.d.mts +32 -0
- package/vendor/aub/scripts/design-bridge.lib.mjs +69 -0
- package/vendor/aub/scripts/diff-blueprint.lib.d.mts +18 -0
- package/vendor/aub/scripts/diff-blueprint.lib.mjs +148 -0
- package/vendor/aub/scripts/diff-blueprint.mjs +25 -0
- package/vendor/aub/scripts/export-agent-prompt.lib.d.mts +10 -0
- package/vendor/aub/scripts/export-agent-prompt.lib.mjs +160 -0
- package/vendor/aub/scripts/export-agent-prompt.mjs +79 -0
- package/vendor/aub/scripts/export-md.lib.d.mts +3 -0
- package/vendor/aub/scripts/export-md.lib.mjs +302 -0
- package/vendor/aub/scripts/export-md.mjs +43 -0
- package/vendor/aub/scripts/generate-registry-artifacts.lib.mjs +118 -0
- package/vendor/aub/scripts/generate-registry-artifacts.mjs +65 -0
- package/vendor/aub/scripts/generate-site-locales.mjs +545 -0
- package/vendor/aub/scripts/handoff-package.lib.d.mts +20 -0
- package/vendor/aub/scripts/handoff-package.lib.mjs +111 -0
- package/vendor/aub/scripts/implementation-report.lib.d.mts +21 -0
- package/vendor/aub/scripts/implementation-report.lib.mjs +97 -0
- package/vendor/aub/scripts/import-angular-component.mjs +72 -0
- package/vendor/aub/scripts/import-design-bridge.mjs +59 -0
- package/vendor/aub/scripts/lock-blueprint.lib.d.mts +23 -0
- package/vendor/aub/scripts/lock-blueprint.lib.mjs +58 -0
- package/vendor/aub/scripts/lock-blueprint.mjs +36 -0
- package/vendor/aub/scripts/migrate-blueprint-cli.mjs +28 -0
- package/vendor/aub/scripts/migrate-blueprint.d.mts +5 -0
- package/vendor/aub/scripts/migrate-blueprint.mjs +95 -0
- package/vendor/aub/scripts/package-workspace-cli.mjs +34 -0
- package/vendor/aub/scripts/project.lib.d.mts +44 -0
- package/vendor/aub/scripts/project.lib.mjs +175 -0
- package/vendor/aub/scripts/project.mjs +332 -0
- package/vendor/aub/scripts/registry.lib.d.mts +52 -0
- package/vendor/aub/scripts/registry.lib.mjs +222 -0
- package/vendor/aub/scripts/run-agent-implementation.mjs +423 -0
- package/vendor/aub/scripts/run-agent-readability.mjs +145 -0
- package/vendor/aub/scripts/run-ollama-prompt.mjs +30 -0
- package/vendor/aub/scripts/scaffold-blueprint.lib.d.mts +38 -0
- package/vendor/aub/scripts/scaffold-blueprint.lib.mjs +316 -0
- package/vendor/aub/scripts/scaffold-blueprint.mjs +86 -0
- package/vendor/aub/scripts/score-agent-implementation.mjs +27 -0
- package/vendor/aub/scripts/score-agent-readability.mjs +54 -0
- package/vendor/aub/scripts/sync-brand-assets.mjs +33 -0
- package/vendor/aub/scripts/validate-blueprint.lib.d.mts +14 -0
- package/vendor/aub/scripts/validate-blueprint.lib.mjs +136 -0
- package/vendor/aub/scripts/validate.mjs +128 -0
- package/vendor/aub/scripts/verify-implementation-report.mjs +36 -0
- package/vendor/aub/scripts/workspace-loop.lib.d.mts +17 -0
- package/vendor/aub/scripts/workspace-loop.lib.mjs +674 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, rename, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { defaultDesignSystem } from './migrate-blueprint.mjs';
|
|
5
|
+
|
|
6
|
+
export const WORKSPACE_LOOP_VERSION = '0.1.0';
|
|
7
|
+
export const AUB_DIR = '.aub';
|
|
8
|
+
export const SESSION_PATH = '.aub/session.json';
|
|
9
|
+
export const COMPONENT_CANDIDATES_PATH = '.aub/component-candidates.json';
|
|
10
|
+
export const TEMPLATE_DIR = '.aub/templates';
|
|
11
|
+
export const TEMPLATE_FORMAT = 'aub-workspace-template';
|
|
12
|
+
export const TEMPLATE_FORMAT_VERSION = '0.1.0';
|
|
13
|
+
|
|
14
|
+
const IGNORE_DIRS = new Set([
|
|
15
|
+
'.git',
|
|
16
|
+
'.aub',
|
|
17
|
+
'.next',
|
|
18
|
+
'.nuxt',
|
|
19
|
+
'.output',
|
|
20
|
+
'coverage',
|
|
21
|
+
'dist',
|
|
22
|
+
'build',
|
|
23
|
+
'node_modules',
|
|
24
|
+
'.pnpm-store',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const SOURCE_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.vue', '.html']);
|
|
28
|
+
const CORE_KIND_BY_NAME = [
|
|
29
|
+
[/button|cta|action/i, 'button'],
|
|
30
|
+
[/sidebar|nav/i, 'sidebar'],
|
|
31
|
+
[/header|topbar|toolbar/i, 'top_bar'],
|
|
32
|
+
[/table|grid/i, 'data_table'],
|
|
33
|
+
[/form|fields?/i, 'form'],
|
|
34
|
+
[/input|search/i, 'text_input'],
|
|
35
|
+
[/card|tile|panel/i, 'card'],
|
|
36
|
+
[/chart|sparkline|graph/i, 'chart_placeholder'],
|
|
37
|
+
[/modal|dialog/i, 'modal'],
|
|
38
|
+
[/drawer/i, 'drawer'],
|
|
39
|
+
[/tabs?/i, 'tabs'],
|
|
40
|
+
[/list|feed/i, 'list'],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export function resolveWorkspacePath(root, filePath) {
|
|
44
|
+
const absRoot = resolve(root);
|
|
45
|
+
const absPath = resolve(absRoot, filePath);
|
|
46
|
+
const rel = relative(absRoot, absPath);
|
|
47
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || rel === '' || rel.startsWith('/')) {
|
|
48
|
+
throw new Error(`Path must stay inside the workspace root: ${filePath}`);
|
|
49
|
+
}
|
|
50
|
+
return absPath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function exists(path) {
|
|
54
|
+
try {
|
|
55
|
+
await access(path);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readJsonIfExists(path, fallback) {
|
|
63
|
+
if (!(await exists(path))) return fallback;
|
|
64
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function writeJsonAtomic(path, value) {
|
|
68
|
+
await mkdir(dirname(path), { recursive: true });
|
|
69
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
70
|
+
const tempPath = `${path}.${process.pid}.tmp`;
|
|
71
|
+
await writeFile(tempPath, content, 'utf8');
|
|
72
|
+
await rename(tempPath, path);
|
|
73
|
+
return { bytes: Buffer.byteLength(content) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toWorkspacePath(root, absPath) {
|
|
77
|
+
return relative(root, absPath).split(sep).join('/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function slugify(value, fallback = 'item') {
|
|
81
|
+
return String(value || fallback)
|
|
82
|
+
.trim()
|
|
83
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
86
|
+
.replace(/^-+|-+$/g, '') || fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function snake(value, fallback = 'component') {
|
|
90
|
+
return slugify(value, fallback).replace(/-/g, '_');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function title(value, fallback = 'Screen') {
|
|
94
|
+
const cleaned = String(value || fallback)
|
|
95
|
+
.replace(/\.[^.]+$/, '')
|
|
96
|
+
.replace(/[-_]+/g, ' ')
|
|
97
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
98
|
+
.trim();
|
|
99
|
+
return cleaned
|
|
100
|
+
? cleaned.replace(/\b\w/g, (letter) => letter.toUpperCase())
|
|
101
|
+
: fallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function inferNamespace(root, packageJson) {
|
|
105
|
+
const packageName = packageJson?.name;
|
|
106
|
+
const base = packageName
|
|
107
|
+
? packageName.split('/').pop()
|
|
108
|
+
: basename(root);
|
|
109
|
+
return slugify(base, 'app').replace(/[^a-z0-9]/g, '') || 'app';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function walk(root, dir, out, limit = 2000) {
|
|
113
|
+
if (out.length >= limit) return;
|
|
114
|
+
let dirents;
|
|
115
|
+
try {
|
|
116
|
+
dirents = await readdir(dir, { withFileTypes: true });
|
|
117
|
+
} catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
for (const dirent of dirents) {
|
|
121
|
+
if (out.length >= limit) return;
|
|
122
|
+
const full = join(dir, dirent.name);
|
|
123
|
+
if (dirent.isDirectory()) {
|
|
124
|
+
if (IGNORE_DIRS.has(dirent.name) || dirent.name.startsWith('.')) continue;
|
|
125
|
+
await walk(root, full, out, limit);
|
|
126
|
+
} else if (dirent.isFile()) {
|
|
127
|
+
out.push({ path: toWorkspacePath(root, full), absPath: full, name: dirent.name });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readPackage(root) {
|
|
133
|
+
const path = join(root, 'package.json');
|
|
134
|
+
if (!(await exists(path))) return null;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function detectFrameworks(packageJson, files) {
|
|
143
|
+
const deps = {
|
|
144
|
+
...(packageJson?.dependencies ?? {}),
|
|
145
|
+
...(packageJson?.devDependencies ?? {}),
|
|
146
|
+
};
|
|
147
|
+
const names = new Set(Object.keys(deps));
|
|
148
|
+
const frameworks = [];
|
|
149
|
+
if (names.has('next') || files.some((file) => /^app\/.+\/page\.[tj]sx?$|^pages\/.+\.[tj]sx?$/.test(file.path))) {
|
|
150
|
+
frameworks.push('next');
|
|
151
|
+
}
|
|
152
|
+
if (names.has('react') && !frameworks.includes('next')) frameworks.push('react');
|
|
153
|
+
if (names.has('nuxt') || files.some((file) => /^pages\/.+\.vue$|^app\.vue$/.test(file.path))) frameworks.push('nuxt');
|
|
154
|
+
if (names.has('vue') && !frameworks.includes('nuxt')) frameworks.push('vue');
|
|
155
|
+
if (names.has('@angular/core') || files.some((file) => /\.component\.(ts|html)$/.test(file.name))) frameworks.push('angular');
|
|
156
|
+
return frameworks.length ? frameworks : ['unknown'];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function routeFromPath(path) {
|
|
160
|
+
let route = path
|
|
161
|
+
.replace(/^src\//, '')
|
|
162
|
+
.replace(/^app\//, '/')
|
|
163
|
+
.replace(/^pages\//, '/')
|
|
164
|
+
.replace(/\/page\.[tj]sx?$/, '')
|
|
165
|
+
.replace(/\/index\.[tj]sx?$/, '')
|
|
166
|
+
.replace(/\.vue$/, '')
|
|
167
|
+
.replace(/\.[tj]sx?$/, '')
|
|
168
|
+
.replace(/\.component\.html$/, '');
|
|
169
|
+
if (!route.startsWith('/')) route = `/${route}`;
|
|
170
|
+
route = route.replace(/\[(.+?)\]/g, ':$1').replace(/\/+/g, '/');
|
|
171
|
+
return route === '/index' || route === '/app' ? '/' : route;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function detectRoutes(files) {
|
|
175
|
+
return files
|
|
176
|
+
.filter((file) => {
|
|
177
|
+
const path = file.path;
|
|
178
|
+
return /^app\/(?:.+\/)?page\.[tj]sx?$/.test(path)
|
|
179
|
+
|| /^pages\/.+\.(tsx|jsx|ts|js|vue)$/.test(path)
|
|
180
|
+
|| /^src\/pages\/.+\.(tsx|jsx|ts|js|vue)$/.test(path)
|
|
181
|
+
|| /\.component\.html$/.test(path);
|
|
182
|
+
})
|
|
183
|
+
.map((file) => ({
|
|
184
|
+
id: slugify(routeFromPath(file.path), 'route'),
|
|
185
|
+
path: file.path,
|
|
186
|
+
route: routeFromPath(file.path),
|
|
187
|
+
kind: /\.vue$/.test(file.path) ? 'vue-route' : /\.component\.html$/.test(file.path) ? 'angular-template' : 'route',
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractReactExports(content) {
|
|
192
|
+
const names = new Set();
|
|
193
|
+
for (const match of content.matchAll(/export\s+(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)/g)) names.add(match[1]);
|
|
194
|
+
for (const match of content.matchAll(/export\s+const\s+([A-Z][A-Za-z0-9_]*)\s*=/g)) names.add(match[1]);
|
|
195
|
+
for (const match of content.matchAll(/function\s+([A-Z][A-Za-z0-9_]*)\s*\(/g)) names.add(match[1]);
|
|
196
|
+
return [...names];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extractVueName(content, path) {
|
|
200
|
+
const define = content.match(/name:\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
201
|
+
return define ?? title(basename(path), 'VueComponent').replace(/\s+/g, '');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function extractAngularSelector(content) {
|
|
205
|
+
return content.match(/selector:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function extractProps(content) {
|
|
209
|
+
const props = new Set();
|
|
210
|
+
const iface = content.match(/interface\s+\w*Props\s*{([\s\S]*?)}/)?.[1];
|
|
211
|
+
if (iface) {
|
|
212
|
+
for (const match of iface.matchAll(/([A-Za-z_$][A-Za-z0-9_$]*)\??\s*:/g)) props.add(match[1]);
|
|
213
|
+
}
|
|
214
|
+
for (const match of content.matchAll(/@Input\(\)?\s+([A-Za-z_$][A-Za-z0-9_$]*)/g)) props.add(match[1]);
|
|
215
|
+
const defineProps = content.match(/defineProps\s*<\s*{([\s\S]*?)}\s*>/)?.[1];
|
|
216
|
+
if (defineProps) {
|
|
217
|
+
for (const match of defineProps.matchAll(/([A-Za-z_$][A-Za-z0-9_$]*)\??\s*:/g)) props.add(match[1]);
|
|
218
|
+
}
|
|
219
|
+
return [...props].slice(0, 20);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function inferCoreType(name) {
|
|
223
|
+
return CORE_KIND_BY_NAME.find(([regex]) => regex.test(name))?.[1] ?? 'card';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function detectComponents(root, files, namespace, frameworks) {
|
|
227
|
+
const candidates = [];
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
const ext = extname(file.path).toLowerCase();
|
|
230
|
+
if (!SOURCE_EXTENSIONS.has(ext)) continue;
|
|
231
|
+
if (!/(component|components|ui|widgets|shared|src)/i.test(file.path)) continue;
|
|
232
|
+
let content = '';
|
|
233
|
+
try {
|
|
234
|
+
content = await readFile(file.absPath, 'utf8');
|
|
235
|
+
} catch {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const names = ext === '.vue'
|
|
239
|
+
? [extractVueName(content, file.path)]
|
|
240
|
+
: ext === '.ts' && /\.component\.ts$/.test(file.path)
|
|
241
|
+
? [title(basename(file.path).replace(/\.component\.ts$/, ''), 'AngularComponent').replace(/\s+/g, '')]
|
|
242
|
+
: extractReactExports(content);
|
|
243
|
+
const selector = extractAngularSelector(content);
|
|
244
|
+
for (const componentName of names.filter(Boolean)) {
|
|
245
|
+
const baseName = selector ?? componentName;
|
|
246
|
+
const suggestedComponent = snake(baseName.replace(/^[a-z]+-/, ''), 'component');
|
|
247
|
+
if (!suggestedComponent || ['page', 'index', 'app'].includes(suggestedComponent)) continue;
|
|
248
|
+
candidates.push({
|
|
249
|
+
id: `${slugify(file.path)}-${suggestedComponent}`,
|
|
250
|
+
status: 'candidate',
|
|
251
|
+
sourcePath: file.path,
|
|
252
|
+
framework: frameworks.includes('angular') && selector ? 'angular' : frameworks[0],
|
|
253
|
+
componentName,
|
|
254
|
+
selector,
|
|
255
|
+
suggestedType: `${namespace}:${suggestedComponent}`,
|
|
256
|
+
suggestedCoreType: inferCoreType(componentName),
|
|
257
|
+
isContainer: /card|panel|layout|section|list|table|form|shell|modal|drawer/i.test(componentName),
|
|
258
|
+
props: extractProps(content),
|
|
259
|
+
usageCount: countUsage(baseName, files, root),
|
|
260
|
+
confidence: selector ? 0.82 : 0.72,
|
|
261
|
+
reason: 'Static scan found a reusable project component. Approve before adding it to aub.registry.json.',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const byId = new Map();
|
|
266
|
+
for (const candidate of candidates) byId.set(candidate.id, candidate);
|
|
267
|
+
return [...byId.values()].slice(0, 100);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function countUsage(name, files) {
|
|
271
|
+
const tag = String(name).replace(/^[a-z]+-/, '');
|
|
272
|
+
let count = 0;
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
if (!SOURCE_EXTENSIONS.has(extname(file.path).toLowerCase())) continue;
|
|
275
|
+
try {
|
|
276
|
+
const text = existsSync(file.absPath) ? String(readFileSyncSafe(file.absPath)) : '';
|
|
277
|
+
if (text.includes(`<${name}`) || text.includes(`<${tag}`) || text.includes(name)) count += 1;
|
|
278
|
+
} catch {
|
|
279
|
+
// Ignore unreadable files.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return Math.max(1, count);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function readFileSyncSafe(path) {
|
|
286
|
+
// Small helper isolated so async scanning can keep the component usage pass simple.
|
|
287
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function readAubSession(root) {
|
|
291
|
+
return readJsonIfExists(join(root, SESSION_PATH), {
|
|
292
|
+
version: WORKSPACE_LOOP_VERSION,
|
|
293
|
+
activeBlueprint: null,
|
|
294
|
+
activeProject: null,
|
|
295
|
+
targetRoute: null,
|
|
296
|
+
preview: {
|
|
297
|
+
devServerUrl: null,
|
|
298
|
+
route: null,
|
|
299
|
+
lastImplementationReport: null,
|
|
300
|
+
},
|
|
301
|
+
updatedAt: null,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function updateAubSession(root, patch = {}) {
|
|
306
|
+
const current = await readAubSession(root);
|
|
307
|
+
const next = {
|
|
308
|
+
...current,
|
|
309
|
+
...patch,
|
|
310
|
+
preview: {
|
|
311
|
+
...(current.preview ?? {}),
|
|
312
|
+
...(patch.preview ?? {}),
|
|
313
|
+
},
|
|
314
|
+
updatedAt: new Date().toISOString(),
|
|
315
|
+
};
|
|
316
|
+
await writeJsonAtomic(join(root, SESSION_PATH), next);
|
|
317
|
+
return { path: SESSION_PATH, session: next };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function readComponentCandidates(root) {
|
|
321
|
+
const doc = await readJsonIfExists(join(root, COMPONENT_CANDIDATES_PATH), null);
|
|
322
|
+
if (!doc) {
|
|
323
|
+
return { format: 'aub-component-candidates', format_version: WORKSPACE_LOOP_VERSION, candidates: [] };
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
format: doc.format ?? 'aub-component-candidates',
|
|
327
|
+
format_version: doc.format_version ?? WORKSPACE_LOOP_VERSION,
|
|
328
|
+
candidates: Array.isArray(doc.candidates) ? doc.candidates : [],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function writeComponentCandidates(root, candidates) {
|
|
333
|
+
const doc = {
|
|
334
|
+
format: 'aub-component-candidates',
|
|
335
|
+
format_version: WORKSPACE_LOOP_VERSION,
|
|
336
|
+
updatedAt: new Date().toISOString(),
|
|
337
|
+
candidates,
|
|
338
|
+
};
|
|
339
|
+
await writeJsonAtomic(join(root, COMPONENT_CANDIDATES_PATH), doc);
|
|
340
|
+
return doc;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function listWorkspaceTemplates(root) {
|
|
344
|
+
const dir = join(root, TEMPLATE_DIR);
|
|
345
|
+
if (!(await exists(dir))) return [];
|
|
346
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
347
|
+
const templates = [];
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
if (!entry.isFile() || !entry.name.endsWith('.aub.template.json')) continue;
|
|
350
|
+
const absPath = join(dir, entry.name);
|
|
351
|
+
try {
|
|
352
|
+
const template = JSON.parse(await readFile(absPath, 'utf8'));
|
|
353
|
+
templates.push({
|
|
354
|
+
path: toWorkspacePath(root, absPath),
|
|
355
|
+
...template,
|
|
356
|
+
});
|
|
357
|
+
} catch {
|
|
358
|
+
// Invalid templates are ignored by status and surfaced when opened directly.
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return templates.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function getWorkspaceStatus(root) {
|
|
365
|
+
const files = [];
|
|
366
|
+
await walk(root, root, files, 1500);
|
|
367
|
+
const packageJson = await readPackage(root);
|
|
368
|
+
const frameworks = detectFrameworks(packageJson, files);
|
|
369
|
+
const routes = detectRoutes(files);
|
|
370
|
+
const session = await readAubSession(root);
|
|
371
|
+
const candidates = await readComponentCandidates(root);
|
|
372
|
+
const templates = await listWorkspaceTemplates(root);
|
|
373
|
+
return {
|
|
374
|
+
root,
|
|
375
|
+
aubDir: AUB_DIR,
|
|
376
|
+
packageName: packageJson?.name ?? null,
|
|
377
|
+
frameworks,
|
|
378
|
+
routeCount: routes.length,
|
|
379
|
+
componentCandidateCount: candidates.candidates.length,
|
|
380
|
+
templateCount: templates.length,
|
|
381
|
+
session,
|
|
382
|
+
routes,
|
|
383
|
+
componentCandidates: candidates.candidates,
|
|
384
|
+
templates,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function scanProjectUi(root, options = {}) {
|
|
389
|
+
const files = [];
|
|
390
|
+
await walk(root, root, files, options.limit ?? 2000);
|
|
391
|
+
const packageJson = await readPackage(root);
|
|
392
|
+
const namespace = snake(options.namespace ?? inferNamespace(root, packageJson), 'app');
|
|
393
|
+
const frameworks = detectFrameworks(packageJson, files);
|
|
394
|
+
const routes = detectRoutes(files);
|
|
395
|
+
const candidates = await detectComponents(root, files, namespace, frameworks);
|
|
396
|
+
const doc = await writeComponentCandidates(root, candidates);
|
|
397
|
+
return {
|
|
398
|
+
root,
|
|
399
|
+
packageName: packageJson?.name ?? null,
|
|
400
|
+
namespace,
|
|
401
|
+
frameworks,
|
|
402
|
+
routes,
|
|
403
|
+
components: candidates,
|
|
404
|
+
componentCandidatesPath: COMPONENT_CANDIDATES_PATH,
|
|
405
|
+
componentCandidates: doc,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function makeBlueprint({ id, name, framework, source, route, componentRefs = [] }) {
|
|
410
|
+
const screenId = `workspace.${slugify(id, 'screen').replace(/-/g, '.')}`;
|
|
411
|
+
const rootChildren = ['hero_section', ...componentRefs.map((ref, index) => `component_${index + 1}`)];
|
|
412
|
+
const nodes = [
|
|
413
|
+
{
|
|
414
|
+
id: 'root',
|
|
415
|
+
type: 'page',
|
|
416
|
+
name,
|
|
417
|
+
role: `Workspace-derived screen from ${source.path}.`,
|
|
418
|
+
parent_id: null,
|
|
419
|
+
children: rootChildren,
|
|
420
|
+
layout: { mode: 'freeform' },
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
id: 'hero_section',
|
|
424
|
+
type: 'section',
|
|
425
|
+
name: 'Imported structure',
|
|
426
|
+
role: 'Represents the source route or component at a reviewable level before implementation.',
|
|
427
|
+
parent_id: 'root',
|
|
428
|
+
children: ['source_heading', 'source_summary'],
|
|
429
|
+
layout: { mode: 'auto', display: 'flex', direction: 'column', gap: { x: 12, y: 12 }, padding: { top: 32, right: 32, bottom: 32, left: 32 } },
|
|
430
|
+
placements: {
|
|
431
|
+
desktop: { x: 64, y: 64, width: 760, height: 220, z_index: 1 },
|
|
432
|
+
tablet: { x: 48, y: 56, width: 640, height: 220, z_index: 1 },
|
|
433
|
+
mobile: { x: 16, y: 48, width: 358, height: 240, z_index: 1 },
|
|
434
|
+
},
|
|
435
|
+
source: { file: source.path },
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: 'source_heading',
|
|
439
|
+
type: 'heading',
|
|
440
|
+
name: `${name} heading`,
|
|
441
|
+
role: 'Visible heading inferred from the source entry.',
|
|
442
|
+
parent_id: 'hero_section',
|
|
443
|
+
children: [],
|
|
444
|
+
content: { text: name },
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
id: 'source_summary',
|
|
448
|
+
type: 'text',
|
|
449
|
+
name: 'Source summary',
|
|
450
|
+
role: 'Records where the candidate template came from.',
|
|
451
|
+
parent_id: 'hero_section',
|
|
452
|
+
children: [],
|
|
453
|
+
content: { text: `Generated from ${framework} source ${source.path}${route ? ` (${route})` : ''}. Review before approving.` },
|
|
454
|
+
},
|
|
455
|
+
...componentRefs.map((ref, index) => ({
|
|
456
|
+
id: `component_${index + 1}`,
|
|
457
|
+
type: ref.coreType ?? 'card',
|
|
458
|
+
name: ref.name,
|
|
459
|
+
role: `Placeholder for project component ${ref.suggestedType}. Approve the component candidate before implementation reuse.`,
|
|
460
|
+
parent_id: 'root',
|
|
461
|
+
children: [],
|
|
462
|
+
layout: { mode: 'auto' },
|
|
463
|
+
content: { label: ref.suggestedType },
|
|
464
|
+
placements: {
|
|
465
|
+
desktop: { x: 64 + index * 280, y: 330, width: 240, height: 140, z_index: 1 },
|
|
466
|
+
tablet: { x: 48 + index * 220, y: 320, width: 200, height: 140, z_index: 1 },
|
|
467
|
+
mobile: { x: 16, y: 320 + index * 156, width: 358, height: 140, z_index: 1 },
|
|
468
|
+
},
|
|
469
|
+
source: { file: ref.sourcePath },
|
|
470
|
+
})),
|
|
471
|
+
];
|
|
472
|
+
return {
|
|
473
|
+
version: '0.3.0',
|
|
474
|
+
screen: {
|
|
475
|
+
id: screenId,
|
|
476
|
+
name,
|
|
477
|
+
type: route === '/' ? 'landing' : 'workspace',
|
|
478
|
+
platform: 'web',
|
|
479
|
+
primary_user_goal: `Review and implement the ${name} screen using the existing project source as context.`,
|
|
480
|
+
notes: 'Generated by AUB workspace scanner. Candidate custom components require approval before registry writes.',
|
|
481
|
+
},
|
|
482
|
+
viewports: [
|
|
483
|
+
{ id: 'desktop', width: 1440, height: 900 },
|
|
484
|
+
{ id: 'tablet', width: 1024, height: 768 },
|
|
485
|
+
{ id: 'mobile', width: 390, height: 844 },
|
|
486
|
+
],
|
|
487
|
+
design_system: defaultDesignSystem(),
|
|
488
|
+
provenance: {
|
|
489
|
+
source_kind: 'other',
|
|
490
|
+
framework,
|
|
491
|
+
importer_version: `workspace-loop-${WORKSPACE_LOOP_VERSION}`,
|
|
492
|
+
entry_file: source.path,
|
|
493
|
+
source_files: [source.path, ...componentRefs.map((ref) => ref.sourcePath)],
|
|
494
|
+
},
|
|
495
|
+
nodes,
|
|
496
|
+
interactions: [],
|
|
497
|
+
responsive: [],
|
|
498
|
+
acceptance: [
|
|
499
|
+
{ id: 'acc_workspace_source_structure', type: 'layout', statement: 'The implementation preserves the reviewed Blueprint hierarchy and source route intent.', target: 'root', priority: 'must', verification_method: 'manual_ia_review' },
|
|
500
|
+
{ id: 'acc_workspace_component_reuse', type: 'content', statement: 'Approved custom components are reused through aub.registry.json mappings instead of recreated as lookalikes.', target: '*', priority: 'must', verification_method: 'code_diff' },
|
|
501
|
+
{ id: 'acc_workspace_responsive', type: 'responsive', statement: 'Desktop, tablet, and mobile layouts remain readable with no horizontal overflow.', target: 'desktop,tablet,mobile', priority: 'must', verification_method: 'screenshot_diff' },
|
|
502
|
+
{ id: 'acc_workspace_interactions', type: 'interaction', statement: 'Visible controls keep their original route/component behavior.', target: '*', priority: 'must', verification_method: 'manual_ia_review' },
|
|
503
|
+
{ id: 'acc_workspace_a11y', type: 'a11y', statement: 'Interactive elements expose accessible labels and focus states.', target: '*', priority: 'should', verification_method: 'axe_audit' },
|
|
504
|
+
],
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export async function generateTemplateFromSource(root, args = {}) {
|
|
509
|
+
if (!args.sourcePath) throw new Error('Provide sourcePath.');
|
|
510
|
+
const sourcePath = resolveWorkspacePath(root, args.sourcePath);
|
|
511
|
+
const relPath = toWorkspacePath(root, sourcePath);
|
|
512
|
+
const candidates = (await readComponentCandidates(root)).candidates;
|
|
513
|
+
const nearbyCandidates = candidates
|
|
514
|
+
.filter((candidate) => candidate.sourcePath === relPath || dirname(candidate.sourcePath) === dirname(relPath))
|
|
515
|
+
.slice(0, 4);
|
|
516
|
+
const selectedCandidates = (nearbyCandidates.length ? nearbyCandidates : candidates.slice(0, 4))
|
|
517
|
+
.map((candidate) => ({
|
|
518
|
+
name: candidate.componentName,
|
|
519
|
+
suggestedType: candidate.suggestedType,
|
|
520
|
+
sourcePath: candidate.sourcePath,
|
|
521
|
+
coreType: candidate.suggestedCoreType,
|
|
522
|
+
}));
|
|
523
|
+
const framework = args.framework ?? inferFrameworkFromPath(relPath);
|
|
524
|
+
const name = args.name ?? title(basename(relPath), 'Workspace screen');
|
|
525
|
+
const route = args.route ?? routeFromPath(relPath);
|
|
526
|
+
const blueprint = makeBlueprint({
|
|
527
|
+
id: args.id ?? slugify(`${framework}-${name}`),
|
|
528
|
+
name,
|
|
529
|
+
framework,
|
|
530
|
+
route,
|
|
531
|
+
source: { path: relPath },
|
|
532
|
+
componentRefs: selectedCandidates,
|
|
533
|
+
});
|
|
534
|
+
const template = {
|
|
535
|
+
format: TEMPLATE_FORMAT,
|
|
536
|
+
format_version: TEMPLATE_FORMAT_VERSION,
|
|
537
|
+
id: args.id ?? slugify(`${framework}-${name}`),
|
|
538
|
+
name,
|
|
539
|
+
category: args.category ?? 'workspace',
|
|
540
|
+
framework,
|
|
541
|
+
source: {
|
|
542
|
+
kind: args.sourceKind ?? 'source-file',
|
|
543
|
+
path: relPath,
|
|
544
|
+
route,
|
|
545
|
+
},
|
|
546
|
+
blueprint,
|
|
547
|
+
registryRefs: selectedCandidates.map((candidate) => candidate.suggestedType),
|
|
548
|
+
confidence: selectedCandidates.length ? 0.72 : 0.62,
|
|
549
|
+
status: args.status === 'approved' ? 'approved' : 'candidate',
|
|
550
|
+
createdAt: new Date().toISOString(),
|
|
551
|
+
};
|
|
552
|
+
const output = args.output ?? `${TEMPLATE_DIR}/${slugify(template.id)}.aub.template.json`;
|
|
553
|
+
const outputPath = resolveWorkspacePath(root, output);
|
|
554
|
+
await writeJsonAtomic(outputPath, template);
|
|
555
|
+
return {
|
|
556
|
+
savedPath: toWorkspacePath(root, outputPath),
|
|
557
|
+
template,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function inferFrameworkFromPath(path) {
|
|
562
|
+
if (path.endsWith('.vue')) return 'vue';
|
|
563
|
+
if (/\.component\.(ts|html)$/.test(path)) return 'angular';
|
|
564
|
+
if (/^app\//.test(path) || /^pages\//.test(path)) return 'next';
|
|
565
|
+
return 'react';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export async function approveComponentCandidate(root, args = {}) {
|
|
569
|
+
if (!args.id) throw new Error('Provide candidate id.');
|
|
570
|
+
const doc = await readComponentCandidates(root);
|
|
571
|
+
const candidate = doc.candidates.find((item) => item.id === args.id);
|
|
572
|
+
if (!candidate) throw new Error(`Component candidate not found: ${args.id}`);
|
|
573
|
+
|
|
574
|
+
if (args.action === 'ignore') {
|
|
575
|
+
candidate.status = 'ignored';
|
|
576
|
+
candidate.reviewedAt = new Date().toISOString();
|
|
577
|
+
await writeComponentCandidates(root, doc.candidates);
|
|
578
|
+
return { candidate, registryPath: null };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (args.action === 'map_core') {
|
|
582
|
+
candidate.status = 'approved';
|
|
583
|
+
candidate.approvedAs = args.coreType ?? candidate.suggestedCoreType ?? 'card';
|
|
584
|
+
candidate.reviewedAt = new Date().toISOString();
|
|
585
|
+
await writeComponentCandidates(root, doc.candidates);
|
|
586
|
+
return { candidate, registryPath: null };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (args.action !== 'create_extension') {
|
|
590
|
+
throw new Error('action must be one of create_extension, map_core, ignore.');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const namespacedType = args.namespacedType ?? candidate.suggestedType;
|
|
594
|
+
if (!/^[a-z][a-z0-9]*:[a-z][a-z0-9_]*$/.test(namespacedType)) {
|
|
595
|
+
throw new Error(`Invalid namespaced type: ${namespacedType}`);
|
|
596
|
+
}
|
|
597
|
+
const registryPath = join(root, 'aub.registry.json');
|
|
598
|
+
const registry = await readJsonIfExists(registryPath, {
|
|
599
|
+
$schema: 'https://henrylau1103.github.io/AUB/schema/aub.registry.schema.json',
|
|
600
|
+
version: '0.1.0',
|
|
601
|
+
description: 'AUB workspace custom components.',
|
|
602
|
+
components: [],
|
|
603
|
+
});
|
|
604
|
+
if (!Array.isArray(registry.components)) registry.components = [];
|
|
605
|
+
const existing = registry.components.find((item) => item.name === namespacedType);
|
|
606
|
+
const componentEntry = {
|
|
607
|
+
name: namespacedType,
|
|
608
|
+
isContainer: Boolean(args.isContainer ?? candidate.isContainer),
|
|
609
|
+
description: args.description ?? `${candidate.componentName} scanned from ${candidate.sourcePath}.`,
|
|
610
|
+
implementations: [{
|
|
611
|
+
id: candidate.framework || 'app',
|
|
612
|
+
framework: normalizeFramework(candidate.framework),
|
|
613
|
+
module: args.module ?? candidate.sourcePath,
|
|
614
|
+
export: args.export ?? candidate.componentName,
|
|
615
|
+
importStyle: args.importStyle ?? 'named',
|
|
616
|
+
sourcePath: candidate.sourcePath,
|
|
617
|
+
props: Object.fromEntries((candidate.props ?? []).map((prop) => [prop, { from: `content.${prop}`, required: false }])),
|
|
618
|
+
notes: 'Approved from AUB component candidate review. Preserve production behavior.',
|
|
619
|
+
}],
|
|
620
|
+
};
|
|
621
|
+
if (existing) Object.assign(existing, componentEntry);
|
|
622
|
+
else registry.components.push(componentEntry);
|
|
623
|
+
await writeJsonAtomic(registryPath, registry);
|
|
624
|
+
|
|
625
|
+
candidate.status = 'approved';
|
|
626
|
+
candidate.approvedAs = namespacedType;
|
|
627
|
+
candidate.reviewedAt = new Date().toISOString();
|
|
628
|
+
await writeComponentCandidates(root, doc.candidates);
|
|
629
|
+
return {
|
|
630
|
+
candidate,
|
|
631
|
+
registryPath: 'aub.registry.json',
|
|
632
|
+
registryComponent: componentEntry,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function normalizeFramework(framework) {
|
|
637
|
+
if (['react', 'vue', 'angular', 'svelte', 'web-component', 'html'].includes(framework)) return framework;
|
|
638
|
+
if (framework === 'next') return 'react';
|
|
639
|
+
if (framework === 'nuxt') return 'vue';
|
|
640
|
+
return 'other';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function templateAuthoringPrompt() {
|
|
644
|
+
return [
|
|
645
|
+
'# AUB Workspace Template Authoring Contract',
|
|
646
|
+
'',
|
|
647
|
+
'You are scanning an existing application to create AUB workspace templates.',
|
|
648
|
+
'',
|
|
649
|
+
'Rules:',
|
|
650
|
+
'- Return only valid AUB workspace template documents or component candidates.',
|
|
651
|
+
'- Do not invent core component types. Use AUB core types or create a candidate namespaced type.',
|
|
652
|
+
'- Custom project components must be written to `.aub/component-candidates.json` first, never directly to `aub.registry.json`.',
|
|
653
|
+
'- A user must approve each candidate before it can become a registry extension.',
|
|
654
|
+
'- Every template must include source references and a confidence score.',
|
|
655
|
+
'- Low confidence mappings must remain `status: "candidate"`.',
|
|
656
|
+
'',
|
|
657
|
+
'Template shape:',
|
|
658
|
+
'```json',
|
|
659
|
+
JSON.stringify({
|
|
660
|
+
format: TEMPLATE_FORMAT,
|
|
661
|
+
format_version: TEMPLATE_FORMAT_VERSION,
|
|
662
|
+
id: 'workspace-settings',
|
|
663
|
+
name: 'Settings',
|
|
664
|
+
category: 'workspace',
|
|
665
|
+
framework: 'react',
|
|
666
|
+
source: { kind: 'route', path: 'src/pages/settings.tsx', route: '/settings' },
|
|
667
|
+
blueprint: { version: '0.3.0' },
|
|
668
|
+
registryRefs: ['app:settings_panel'],
|
|
669
|
+
confidence: 0.72,
|
|
670
|
+
status: 'candidate',
|
|
671
|
+
}, null, 2),
|
|
672
|
+
'```',
|
|
673
|
+
].join('\n');
|
|
674
|
+
}
|