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.
Files changed (152) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/bin/aub-workspace.mjs +246 -0
  4. package/package.json +32 -0
  5. package/vendor/aub/apps/editor/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  6. package/vendor/aub/apps/editor/dist/assets/angular-importer.lib-dB_jK4mR.js +32 -0
  7. package/vendor/aub/apps/editor/dist/assets/canvas-tools-CuYC7cA2.js +364 -0
  8. package/vendor/aub/apps/editor/dist/assets/design-bridge.lib-DJvaK6AX.js +1 -0
  9. package/vendor/aub/apps/editor/dist/assets/export-agent-prompt.lib-BsP0KNqo.js +2 -0
  10. package/vendor/aub/apps/editor/dist/assets/export-md.lib-DdmdeWgO.js +3 -0
  11. package/vendor/aub/apps/editor/dist/assets/handoff-package.lib-DDYpcEma.js +20 -0
  12. package/vendor/aub/apps/editor/dist/assets/implementation-report.lib-CmsSB_8s.js +1 -0
  13. package/vendor/aub/apps/editor/dist/assets/index-BCH-ek3h.js +2 -0
  14. package/vendor/aub/apps/editor/dist/assets/index-lAnc928Q.css +1 -0
  15. package/vendor/aub/apps/editor/dist/assets/index-vt1nM1M4.js +507 -0
  16. package/vendor/aub/apps/editor/dist/assets/jszip.min-CRfXyL92.js +12 -0
  17. package/vendor/aub/apps/editor/dist/assets/react-vendor-ByX9Pqse.js +40 -0
  18. package/vendor/aub/apps/editor/dist/brand/android-chrome-192x192.png +0 -0
  19. package/vendor/aub/apps/editor/dist/brand/android-chrome-512x512.png +0 -0
  20. package/vendor/aub/apps/editor/dist/brand/app-icon-1024.png +0 -0
  21. package/vendor/aub/apps/editor/dist/brand/app-icon-192.png +0 -0
  22. package/vendor/aub/apps/editor/dist/brand/app-icon-512.png +0 -0
  23. package/vendor/aub/apps/editor/dist/brand/apple-touch-icon.png +0 -0
  24. package/vendor/aub/apps/editor/dist/brand/aub-logo-mark.svg +28 -0
  25. package/vendor/aub/apps/editor/dist/brand/favicon-16x16.png +0 -0
  26. package/vendor/aub/apps/editor/dist/brand/favicon-32x32.png +0 -0
  27. package/vendor/aub/apps/editor/dist/brand/favicon-48x48.png +0 -0
  28. package/vendor/aub/apps/editor/dist/brand/favicon.ico +0 -0
  29. package/vendor/aub/apps/editor/dist/brand/favicon.svg +9 -0
  30. package/vendor/aub/apps/editor/dist/brand/maskable-icon-512.png +0 -0
  31. package/vendor/aub/apps/editor/dist/brand/mstile-150x150.png +0 -0
  32. package/vendor/aub/apps/editor/dist/brand/safari-pinned-tab.svg +8 -0
  33. package/vendor/aub/apps/editor/dist/browserconfig.xml +9 -0
  34. package/vendor/aub/apps/editor/dist/index.html +22 -0
  35. package/vendor/aub/apps/editor/dist/manifest.webmanifest +28 -0
  36. package/vendor/aub/apps/editor/dist/template-previews/admin-table.png +0 -0
  37. package/vendor/aub/apps/editor/dist/template-previews/booking.png +0 -0
  38. package/vendor/aub/apps/editor/dist/template-previews/calendar.png +0 -0
  39. package/vendor/aub/apps/editor/dist/template-previews/catalog.png +0 -0
  40. package/vendor/aub/apps/editor/dist/template-previews/chat.png +0 -0
  41. package/vendor/aub/apps/editor/dist/template-previews/checkout.png +0 -0
  42. package/vendor/aub/apps/editor/dist/template-previews/crm.png +0 -0
  43. package/vendor/aub/apps/editor/dist/template-previews/dashboard.png +0 -0
  44. package/vendor/aub/apps/editor/dist/template-previews/feed.png +0 -0
  45. package/vendor/aub/apps/editor/dist/template-previews/files.png +0 -0
  46. package/vendor/aub/apps/editor/dist/template-previews/kanban.png +0 -0
  47. package/vendor/aub/apps/editor/dist/template-previews/landing.png +0 -0
  48. package/vendor/aub/apps/editor/dist/template-previews/mail.png +0 -0
  49. package/vendor/aub/apps/editor/dist/template-previews/onboarding.png +0 -0
  50. package/vendor/aub/apps/editor/dist/template-previews/pricing.png +0 -0
  51. package/vendor/aub/apps/editor/dist/template-previews/product-detail.png +0 -0
  52. package/vendor/aub/apps/editor/dist/template-previews/settings.png +0 -0
  53. package/vendor/aub/apps/editor/dist/template-previews/wiki.png +0 -0
  54. package/vendor/aub/apps/mcp-server/dist/aub.js +15 -0
  55. package/vendor/aub/apps/mcp-server/dist/context.js +1 -0
  56. package/vendor/aub/apps/mcp-server/dist/http.js +123 -0
  57. package/vendor/aub/apps/mcp-server/dist/index.js +23 -0
  58. package/vendor/aub/apps/mcp-server/dist/repo.js +17 -0
  59. package/vendor/aub/apps/mcp-server/dist/schema.js +42 -0
  60. package/vendor/aub/apps/mcp-server/dist/server.js +80 -0
  61. package/vendor/aub/apps/mcp-server/dist/tools/approve-component-candidate.js +27 -0
  62. package/vendor/aub/apps/mcp-server/dist/tools/diff-blueprints.js +27 -0
  63. package/vendor/aub/apps/mcp-server/dist/tools/export-handoff.js +87 -0
  64. package/vendor/aub/apps/mcp-server/dist/tools/export-prompt.js +35 -0
  65. package/vendor/aub/apps/mcp-server/dist/tools/export-template-authoring-prompt.js +13 -0
  66. package/vendor/aub/apps/mcp-server/dist/tools/generate-template-from-source.js +25 -0
  67. package/vendor/aub/apps/mcp-server/dist/tools/get-aub-session.js +13 -0
  68. package/vendor/aub/apps/mcp-server/dist/tools/get-blueprint.js +28 -0
  69. package/vendor/aub/apps/mcp-server/dist/tools/get-project.js +45 -0
  70. package/vendor/aub/apps/mcp-server/dist/tools/get-workspace-status.js +10 -0
  71. package/vendor/aub/apps/mcp-server/dist/tools/import-design-bridge.js +62 -0
  72. package/vendor/aub/apps/mcp-server/dist/tools/list-blueprints.js +11 -0
  73. package/vendor/aub/apps/mcp-server/dist/tools/list-projects.js +11 -0
  74. package/vendor/aub/apps/mcp-server/dist/tools/lock-blueprint.js +33 -0
  75. package/vendor/aub/apps/mcp-server/dist/tools/migrate-blueprint.js +38 -0
  76. package/vendor/aub/apps/mcp-server/dist/tools/resolve-component.js +51 -0
  77. package/vendor/aub/apps/mcp-server/dist/tools/scaffold-blueprint.js +53 -0
  78. package/vendor/aub/apps/mcp-server/dist/tools/scan-project-ui.js +18 -0
  79. package/vendor/aub/apps/mcp-server/dist/tools/submit-report.js +48 -0
  80. package/vendor/aub/apps/mcp-server/dist/tools/update-aub-session.js +14 -0
  81. package/vendor/aub/apps/mcp-server/dist/tools/validate-blueprint.js +67 -0
  82. package/vendor/aub/apps/mcp-server/dist/tools/validate-project.js +74 -0
  83. package/vendor/aub/apps/mcp-server/dist/tools/write-blueprint.js +72 -0
  84. package/vendor/aub/apps/mcp-server/dist/workspace.js +138 -0
  85. package/vendor/aub/docs/agent-handoff.md +85 -0
  86. package/vendor/aub/docs/agent-handoff.zh-Hant.md +85 -0
  87. package/vendor/aub/docs/template-authoring-agent.md +86 -0
  88. package/vendor/aub/schema/aub-ci.schema.json +34 -0
  89. package/vendor/aub/schema/aub.registry.schema.json +118 -0
  90. package/vendor/aub/schema/design-bridge.schema.json +44 -0
  91. package/vendor/aub/schema/implementation-report.schema.json +93 -0
  92. package/vendor/aub/schema/project-types.ts +72 -0
  93. package/vendor/aub/schema/registry/components.json +118 -0
  94. package/vendor/aub/schema/types.js +13 -0
  95. package/vendor/aub/schema/types.ts +348 -0
  96. package/vendor/aub/schema/ui-blueprint-lock.schema.json +61 -0
  97. package/vendor/aub/schema/ui-blueprint.schema.json +1339 -0
  98. package/vendor/aub/schema/ui-project.schema.json +139 -0
  99. package/vendor/aub/scripts/agent-implementation-benchmark.lib.mjs +125 -0
  100. package/vendor/aub/scripts/angular-importer.lib.mjs +982 -0
  101. package/vendor/aub/scripts/check-editor-bundle-budget.mjs +36 -0
  102. package/vendor/aub/scripts/ci-verify.lib.mjs +256 -0
  103. package/vendor/aub/scripts/ci-verify.mjs +45 -0
  104. package/vendor/aub/scripts/create-authoring-kit.mjs +84 -0
  105. package/vendor/aub/scripts/create-implementation-report.mjs +24 -0
  106. package/vendor/aub/scripts/design-bridge.lib.d.mts +32 -0
  107. package/vendor/aub/scripts/design-bridge.lib.mjs +69 -0
  108. package/vendor/aub/scripts/diff-blueprint.lib.d.mts +18 -0
  109. package/vendor/aub/scripts/diff-blueprint.lib.mjs +148 -0
  110. package/vendor/aub/scripts/diff-blueprint.mjs +25 -0
  111. package/vendor/aub/scripts/export-agent-prompt.lib.d.mts +10 -0
  112. package/vendor/aub/scripts/export-agent-prompt.lib.mjs +160 -0
  113. package/vendor/aub/scripts/export-agent-prompt.mjs +79 -0
  114. package/vendor/aub/scripts/export-md.lib.d.mts +3 -0
  115. package/vendor/aub/scripts/export-md.lib.mjs +302 -0
  116. package/vendor/aub/scripts/export-md.mjs +43 -0
  117. package/vendor/aub/scripts/generate-registry-artifacts.lib.mjs +118 -0
  118. package/vendor/aub/scripts/generate-registry-artifacts.mjs +65 -0
  119. package/vendor/aub/scripts/generate-site-locales.mjs +545 -0
  120. package/vendor/aub/scripts/handoff-package.lib.d.mts +20 -0
  121. package/vendor/aub/scripts/handoff-package.lib.mjs +111 -0
  122. package/vendor/aub/scripts/implementation-report.lib.d.mts +21 -0
  123. package/vendor/aub/scripts/implementation-report.lib.mjs +97 -0
  124. package/vendor/aub/scripts/import-angular-component.mjs +72 -0
  125. package/vendor/aub/scripts/import-design-bridge.mjs +59 -0
  126. package/vendor/aub/scripts/lock-blueprint.lib.d.mts +23 -0
  127. package/vendor/aub/scripts/lock-blueprint.lib.mjs +58 -0
  128. package/vendor/aub/scripts/lock-blueprint.mjs +36 -0
  129. package/vendor/aub/scripts/migrate-blueprint-cli.mjs +28 -0
  130. package/vendor/aub/scripts/migrate-blueprint.d.mts +5 -0
  131. package/vendor/aub/scripts/migrate-blueprint.mjs +95 -0
  132. package/vendor/aub/scripts/package-workspace-cli.mjs +34 -0
  133. package/vendor/aub/scripts/project.lib.d.mts +44 -0
  134. package/vendor/aub/scripts/project.lib.mjs +175 -0
  135. package/vendor/aub/scripts/project.mjs +332 -0
  136. package/vendor/aub/scripts/registry.lib.d.mts +52 -0
  137. package/vendor/aub/scripts/registry.lib.mjs +222 -0
  138. package/vendor/aub/scripts/run-agent-implementation.mjs +423 -0
  139. package/vendor/aub/scripts/run-agent-readability.mjs +145 -0
  140. package/vendor/aub/scripts/run-ollama-prompt.mjs +30 -0
  141. package/vendor/aub/scripts/scaffold-blueprint.lib.d.mts +38 -0
  142. package/vendor/aub/scripts/scaffold-blueprint.lib.mjs +316 -0
  143. package/vendor/aub/scripts/scaffold-blueprint.mjs +86 -0
  144. package/vendor/aub/scripts/score-agent-implementation.mjs +27 -0
  145. package/vendor/aub/scripts/score-agent-readability.mjs +54 -0
  146. package/vendor/aub/scripts/sync-brand-assets.mjs +33 -0
  147. package/vendor/aub/scripts/validate-blueprint.lib.d.mts +14 -0
  148. package/vendor/aub/scripts/validate-blueprint.lib.mjs +136 -0
  149. package/vendor/aub/scripts/validate.mjs +128 -0
  150. package/vendor/aub/scripts/verify-implementation-report.mjs +36 -0
  151. package/vendor/aub/scripts/workspace-loop.lib.d.mts +17 -0
  152. package/vendor/aub/scripts/workspace-loop.lib.mjs +674 -0
@@ -0,0 +1,222 @@
1
+ // Resolves the set of known component types for validation: core types from the
2
+ // curated registry plus optional project-defined extension types declared in an
3
+ // aub.registry.json file. Extension types are namespaced (team:component) so they
4
+ // never collide with core snake_case types and are always explicit — agents still
5
+ // resolve them, they are never free-guessed.
6
+
7
+ import { readFile } from 'node:fs/promises';
8
+ import { existsSync } from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { dirname, resolve, join } from 'node:path';
11
+ import { loadCoreRegistry, computeTypeLists } from './generate-registry-artifacts.lib.mjs';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const ROOT = resolve(__dirname, '..');
15
+
16
+ export const EXTENSION_REGISTRY_FILENAME = 'aub.registry.json';
17
+ export const EXTENSION_NAME_PATTERN = /^[a-z][a-z0-9]*:[a-z][a-z0-9_]*$/;
18
+
19
+ /** Build a Map<typeName, metadata> for the core registry. */
20
+ export async function buildCoreKnownTypes() {
21
+ const registry = await loadCoreRegistry();
22
+ const known = new Map();
23
+ for (const category of registry.categories ?? []) {
24
+ for (const type of category.types ?? []) {
25
+ known.set(type.name, {
26
+ isContainer: Boolean(type.isContainer),
27
+ source: 'core',
28
+ description: type.description ?? '',
29
+ implementations: [],
30
+ });
31
+ }
32
+ }
33
+ return known;
34
+ }
35
+
36
+ /**
37
+ * Walk up from startDir looking for an aub.registry.json. Stops at the filesystem
38
+ * root or after a generous depth. Returns the absolute path or null.
39
+ */
40
+ export function discoverExtensionRegistry(startDir = process.cwd()) {
41
+ let dir = resolve(startDir);
42
+ for (let i = 0; i < 64; i += 1) {
43
+ const candidate = join(dir, EXTENSION_REGISTRY_FILENAME);
44
+ if (existsSync(candidate)) return candidate;
45
+ const parent = dirname(dir);
46
+ if (parent === dir) break;
47
+ dir = parent;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Validate and normalize an extension registry document. Returns
54
+ * { components: [{ name, isContainer, description, implementations }] }.
55
+ * Throws on malformed input.
56
+ */
57
+ export function parseExtensionRegistry(doc, coreTypes, sourceLabel = EXTENSION_REGISTRY_FILENAME) {
58
+ if (!doc || typeof doc !== 'object') {
59
+ throw new Error(`${sourceLabel}: must be a JSON object`);
60
+ }
61
+ const components = doc.components;
62
+ if (!Array.isArray(components)) {
63
+ throw new Error(`${sourceLabel}: missing required "components" array`);
64
+ }
65
+ const seen = new Set();
66
+ const normalized = [];
67
+ for (const entry of components) {
68
+ if (!entry || typeof entry !== 'object') {
69
+ throw new Error(`${sourceLabel}: each component must be an object`);
70
+ }
71
+ const { name } = entry;
72
+ if (typeof name !== 'string' || !EXTENSION_NAME_PATTERN.test(name)) {
73
+ throw new Error(
74
+ `${sourceLabel}: component name "${String(name)}" must match team:component (e.g. acme:data_card)`
75
+ );
76
+ }
77
+ if (coreTypes.has(name)) {
78
+ throw new Error(`${sourceLabel}: extension "${name}" collides with a core component type`);
79
+ }
80
+ if (seen.has(name)) {
81
+ throw new Error(`${sourceLabel}: duplicate extension component "${name}"`);
82
+ }
83
+ if (typeof entry.isContainer !== 'boolean') {
84
+ throw new Error(`${sourceLabel}: extension "${name}" must declare isContainer (boolean)`);
85
+ }
86
+ const implementations = normalizeImplementations(entry.implementations, name, sourceLabel);
87
+ seen.add(name);
88
+ normalized.push({
89
+ name,
90
+ isContainer: entry.isContainer,
91
+ description: typeof entry.description === 'string' ? entry.description : '',
92
+ implementations,
93
+ });
94
+ }
95
+ return { components: normalized };
96
+ }
97
+
98
+ function normalizeImplementations(input, componentName, sourceLabel) {
99
+ if (input == null) return [];
100
+ if (!Array.isArray(input) || input.length === 0) {
101
+ throw new Error(`${sourceLabel}: extension "${componentName}" implementations must be a non-empty array`);
102
+ }
103
+ const seen = new Set();
104
+ return input.map((implementation) => {
105
+ if (!implementation || typeof implementation !== 'object' || Array.isArray(implementation)) {
106
+ throw new Error(`${sourceLabel}: extension "${componentName}" implementation must be an object`);
107
+ }
108
+ const { id, framework, module, export: exportName, importStyle, sourcePath, storybookUrl, docsUrl, props, notes } =
109
+ implementation;
110
+ if (typeof id !== 'string' || !/^[a-z][a-z0-9_-]*$/.test(id)) {
111
+ throw new Error(`${sourceLabel}: extension "${componentName}" implementation id is invalid`);
112
+ }
113
+ if (seen.has(id)) {
114
+ throw new Error(`${sourceLabel}: extension "${componentName}" has duplicate implementation id "${id}"`);
115
+ }
116
+ if (!['react', 'vue', 'angular', 'svelte', 'web-component', 'html', 'other'].includes(framework)) {
117
+ throw new Error(`${sourceLabel}: extension "${componentName}" implementation "${id}" has invalid framework`);
118
+ }
119
+ if (typeof module !== 'string' || !module.trim()) {
120
+ throw new Error(`${sourceLabel}: extension "${componentName}" implementation "${id}" must declare module`);
121
+ }
122
+ if (
123
+ importStyle != null &&
124
+ !['named', 'default', 'namespace', 'side-effect', 'custom-element'].includes(importStyle)
125
+ ) {
126
+ throw new Error(`${sourceLabel}: extension "${componentName}" implementation "${id}" has invalid importStyle`);
127
+ }
128
+ if (props != null && (!props || typeof props !== 'object' || Array.isArray(props))) {
129
+ throw new Error(`${sourceLabel}: extension "${componentName}" implementation "${id}" props must be an object`);
130
+ }
131
+ const normalizedProps = {};
132
+ for (const [propName, mapping] of Object.entries(props ?? {})) {
133
+ if (!/^[A-Za-z_$][A-Za-z0-9_$.-]*$/.test(propName)) {
134
+ throw new Error(
135
+ `${sourceLabel}: extension "${componentName}" implementation "${id}" prop name "${propName}" is invalid`
136
+ );
137
+ }
138
+ if (
139
+ !mapping ||
140
+ typeof mapping !== 'object' ||
141
+ Array.isArray(mapping) ||
142
+ typeof mapping.from !== 'string' ||
143
+ !mapping.from.trim()
144
+ ) {
145
+ throw new Error(
146
+ `${sourceLabel}: extension "${componentName}" implementation "${id}" prop "${propName}" must declare from`
147
+ );
148
+ }
149
+ normalizedProps[propName] = {
150
+ from: mapping.from,
151
+ ...(typeof mapping.required === 'boolean' ? { required: mapping.required } : {}),
152
+ ...(typeof mapping.description === 'string' ? { description: mapping.description } : {}),
153
+ };
154
+ }
155
+ for (const [field, value] of [
156
+ ['storybookUrl', storybookUrl],
157
+ ['docsUrl', docsUrl],
158
+ ]) {
159
+ if (value != null) {
160
+ try {
161
+ new URL(value);
162
+ } catch {
163
+ throw new Error(
164
+ `${sourceLabel}: extension "${componentName}" implementation "${id}" ${field} must be a URI`
165
+ );
166
+ }
167
+ }
168
+ }
169
+ seen.add(id);
170
+ return {
171
+ id,
172
+ framework,
173
+ module,
174
+ ...(typeof exportName === 'string' ? { export: exportName } : {}),
175
+ importStyle: importStyle ?? 'named',
176
+ ...(typeof sourcePath === 'string' ? { sourcePath } : {}),
177
+ ...(typeof storybookUrl === 'string' ? { storybookUrl } : {}),
178
+ ...(typeof docsUrl === 'string' ? { docsUrl } : {}),
179
+ ...(Object.keys(normalizedProps).length > 0 ? { props: normalizedProps } : {}),
180
+ ...(typeof notes === 'string' ? { notes } : {}),
181
+ };
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Resolve the full known-type map: core types plus any extension types. Discovery
187
+ * order: explicit extensionPath > auto-discovered file from startDir. Pass
188
+ * { extensionPath: null, discover: false } to use core types only.
189
+ */
190
+ export async function buildKnownTypes({ extensionPath, startDir = process.cwd(), discover = true } = {}) {
191
+ const known = await buildCoreKnownTypes();
192
+ const coreTypes = new Set(known.keys());
193
+
194
+ let path = extensionPath ?? null;
195
+ if (!path && discover) path = discoverExtensionRegistry(startDir);
196
+ if (!path) return { knownTypes: known, extensionPath: null, extensions: [] };
197
+
198
+ const raw = await readFile(path, 'utf8');
199
+ let doc;
200
+ try {
201
+ doc = JSON.parse(raw);
202
+ } catch (err) {
203
+ throw new Error(`${path}: invalid JSON (${err.message})`);
204
+ }
205
+ const { components } = parseExtensionRegistry(doc, coreTypes, path);
206
+ for (const component of components) {
207
+ known.set(component.name, {
208
+ isContainer: component.isContainer,
209
+ source: 'extension',
210
+ description: component.description,
211
+ implementations: component.implementations,
212
+ });
213
+ }
214
+ return { knownTypes: known, extensionPath: path, extensions: components };
215
+ }
216
+
217
+ /** Convenience: the ordered core type lists (re-exported for callers/tests). */
218
+ export async function coreTypeLists() {
219
+ return computeTypeLists(await loadCoreRegistry());
220
+ }
221
+
222
+ export { ROOT as REPO_ROOT };
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import { createServer } from 'node:http';
5
+ import { existsSync } from 'node:fs';
6
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { createImplementationReportTemplate } from './implementation-report.lib.mjs';
11
+ import { scoreImplementationBenchmark } from './agent-implementation-benchmark.lib.mjs';
12
+
13
+ const args = process.argv.slice(2);
14
+ const separator = args.indexOf('--');
15
+ const options = separator >= 0 ? args.slice(0, separator) : args;
16
+ const command = separator >= 0 ? args.slice(separator + 1) : [];
17
+ const name = options.find((value) => !value.startsWith('--'));
18
+ const allowsExternal = options.includes('--allow-external');
19
+
20
+ if (!name || command.length === 0) {
21
+ console.error('Usage: node scripts/run-agent-implementation.mjs <agent-name> --allow-external -- <command> [args...]');
22
+ process.exit(2);
23
+ }
24
+ if (!allowsExternal) {
25
+ console.error('Refusing to run an external agent without the explicit --allow-external flag.');
26
+ process.exit(2);
27
+ }
28
+
29
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
30
+ const benchmarkDir = resolve(root, 'benchmarks/agent-implementation');
31
+ const resultRoot = resolve(benchmarkDir, 'results');
32
+ const slug = name.replace(/[^a-zA-Z0-9._-]+/g, '-');
33
+ const resultDir = resolve(resultRoot, slug);
34
+ const [prompt, fixtureText, referenceHtml] = await Promise.all([
35
+ readFile(resolve(benchmarkDir, 'prompt.md'), 'utf8'),
36
+ readFile(resolve(root, 'examples/freeform-actions.ui.json'), 'utf8'),
37
+ readFile(resolve(benchmarkDir, 'reference.html'), 'utf8'),
38
+ ]);
39
+ const blueprint = JSON.parse(fixtureText);
40
+ const benchmarkInput = [
41
+ prompt.trim(),
42
+ '',
43
+ '<implementation_report_template>',
44
+ JSON.stringify(createImplementationReportTemplate(blueprint), null, 2),
45
+ '</implementation_report_template>',
46
+ '',
47
+ '<blueprint_json>',
48
+ fixtureText.trim(),
49
+ '</blueprint_json>',
50
+ '',
51
+ ].join('\n');
52
+
53
+ await mkdir(resultDir, { recursive: true });
54
+ const execution = await run(command[0], command.slice(1), benchmarkInput, root);
55
+ await Promise.all([
56
+ writeFile(resolve(resultDir, 'agent.stdout.txt'), execution.stdout, 'utf8'),
57
+ writeFile(resolve(resultDir, 'agent.stderr.txt'), execution.stderr, 'utf8'),
58
+ ]);
59
+ if (execution.code !== 0) {
60
+ console.error(`${name} exited with code ${execution.code}. See ${resultDir}/agent.stderr.txt`);
61
+ process.exit(execution.code || 1);
62
+ }
63
+
64
+ const output = extractOutput(execution.stdout);
65
+ await Promise.all([
66
+ writeFile(resolve(resultDir, 'index.html'), output.html, 'utf8'),
67
+ writeFile(resolve(resultDir, 'implementation-report.json'), `${JSON.stringify(output.implementation_report, null, 2)}\n`, 'utf8'),
68
+ writeFile(resolve(resultDir, 'agent-output.json'), `${JSON.stringify(output, null, 2)}\n`, 'utf8'),
69
+ ]);
70
+
71
+ const measurements = await measureImplementations({
72
+ blueprint,
73
+ candidateHtml: output.html,
74
+ referenceHtml,
75
+ resultDir,
76
+ });
77
+ const scoring = scoreImplementationBenchmark(
78
+ blueprint,
79
+ measurements.candidate,
80
+ measurements.reference,
81
+ output.implementation_report
82
+ );
83
+ const report = {
84
+ agent: name,
85
+ command: command.map(redactArgument),
86
+ executed_at: new Date().toISOString(),
87
+ ...scoring,
88
+ };
89
+ await Promise.all([
90
+ writeFile(resolve(resultDir, 'measurements.json'), `${JSON.stringify(measurements, null, 2)}\n`, 'utf8'),
91
+ writeFile(resolve(resultDir, 'report.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8'),
92
+ ]);
93
+ console.log(JSON.stringify(report, null, 2));
94
+ process.exit(report.ready ? 0 : 1);
95
+
96
+ async function measureImplementations({ blueprint, candidateHtml, referenceHtml, resultDir }) {
97
+ const server = createServer((request, response) => {
98
+ response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
99
+ response.end(request.url?.startsWith('/reference') ? referenceHtml : candidateHtml);
100
+ });
101
+ await new Promise((resolveListen) => server.listen(0, '127.0.0.1', resolveListen));
102
+ const address = server.address();
103
+ const port = typeof address === 'object' && address ? address.port : 0;
104
+ const chromePort = await reservePort();
105
+ const chromeDir = await mkdtemp(resolve(tmpdir(), 'aub-benchmark-chrome-'));
106
+ const chrome = spawn(findChrome(), [
107
+ '--headless=new',
108
+ '--disable-gpu',
109
+ '--hide-scrollbars',
110
+ '--no-first-run',
111
+ '--no-default-browser-check',
112
+ `--remote-debugging-port=${chromePort}`,
113
+ `--user-data-dir=${chromeDir}`,
114
+ 'about:blank',
115
+ ], { stdio: ['ignore', 'ignore', 'pipe'] });
116
+ let chromeError = '';
117
+ chrome.stderr.setEncoding('utf8');
118
+ chrome.stderr.on('data', (chunk) => { chromeError += chunk; });
119
+
120
+ try {
121
+ const target = await openChromeTarget(chromePort);
122
+ const cdp = await connectCdp(target.webSocketDebuggerUrl);
123
+ try {
124
+ await cdp.command('Page.enable');
125
+ await cdp.command('Runtime.enable');
126
+ const candidate = await measureDocument(cdp, blueprint, `http://127.0.0.1:${port}/candidate`, 'candidate', resultDir);
127
+ const reference = await measureDocument(cdp, blueprint, `http://127.0.0.1:${port}/reference`, 'reference', resultDir);
128
+ return { candidate, reference };
129
+ } finally {
130
+ cdp.close();
131
+ }
132
+ } catch (error) {
133
+ throw new Error(`Chrome measurement failed: ${error.message}\n${chromeError.slice(-2000)}`);
134
+ } finally {
135
+ chrome.kill('SIGTERM');
136
+ server.close();
137
+ await rm(chromeDir, { recursive: true, force: true });
138
+ }
139
+ }
140
+
141
+ async function measureDocument(cdp, blueprint, url, prefix, resultDir) {
142
+ const result = {
143
+ viewports: {},
144
+ interactions: {},
145
+ has_focus_visible: false,
146
+ };
147
+ for (const viewport of blueprint.viewports) {
148
+ await cdp.command('Emulation.setDeviceMetricsOverride', {
149
+ width: viewport.width,
150
+ height: viewport.height,
151
+ deviceScaleFactor: 1,
152
+ mobile: viewport.id === 'mobile',
153
+ });
154
+ await cdp.command('Page.navigate', { url: `${url}?viewport=${viewport.id}` });
155
+ await waitForDocument(cdp);
156
+ const measurement = await evaluate(cdp, measurementExpression(blueprint.nodes.map((node) => node.id)));
157
+ const screenshot = await cdp.command('Page.captureScreenshot', {
158
+ format: 'png',
159
+ fromSurface: true,
160
+ captureBeyondViewport: false,
161
+ });
162
+ const screenshotBytes = Buffer.from(screenshot.data, 'base64');
163
+ const screenshotName = `${prefix}-${viewport.id}.png`;
164
+ await writeFile(resolve(resultDir, screenshotName), screenshotBytes);
165
+ result.viewports[viewport.id] = {
166
+ ...measurement,
167
+ screenshot: screenshotName,
168
+ screenshot_bytes: screenshotBytes.length,
169
+ };
170
+ }
171
+
172
+ if (prefix === 'candidate') {
173
+ await cdp.command('Emulation.setDeviceMetricsOverride', {
174
+ width: blueprint.viewports[0].width,
175
+ height: blueprint.viewports[0].height,
176
+ deviceScaleFactor: 1,
177
+ mobile: false,
178
+ });
179
+ await cdp.command('Page.navigate', { url });
180
+ await waitForDocument(cdp);
181
+ for (const interaction of blueprint.interactions) {
182
+ result.interactions[interaction.source_node_id] = await evaluate(cdp, `(() => {
183
+ const target = document.querySelector('[data-aub-node="${escapeJs(interaction.source_node_id)}"]');
184
+ if (!target) return null;
185
+ delete document.body.dataset.lastAction;
186
+ target.click();
187
+ return document.body.dataset.lastAction || null;
188
+ })()`);
189
+ }
190
+ result.has_focus_visible = await evaluate(cdp, `Array.from(document.styleSheets).some((sheet) => {
191
+ try { return Array.from(sheet.cssRules).some((rule) => rule.cssText.includes(':focus-visible')); }
192
+ catch { return false; }
193
+ })`);
194
+ }
195
+ return result;
196
+ }
197
+
198
+ function measurementExpression(nodeIds) {
199
+ return `(() => {
200
+ const ids = ${JSON.stringify(nodeIds)};
201
+ const nodes = {};
202
+ for (const id of ids) {
203
+ const element = document.querySelector('[data-aub-node="' + id + '"]');
204
+ if (!element) continue;
205
+ const rect = element.getBoundingClientRect();
206
+ const style = getComputedStyle(element);
207
+ nodes[id] = {
208
+ text: element.textContent || '',
209
+ rect: {
210
+ x: Math.round(rect.x * 100) / 100,
211
+ y: Math.round(rect.y * 100) / 100,
212
+ width: Math.round(rect.width * 100) / 100,
213
+ height: Math.round(rect.height * 100) / 100,
214
+ z_index: Number(style.zIndex) || 0
215
+ },
216
+ styles: {
217
+ position: style.position,
218
+ display: style.display,
219
+ flexDirection: style.flexDirection,
220
+ rowGap: style.rowGap,
221
+ columnGap: style.columnGap,
222
+ paddingTop: style.paddingTop,
223
+ paddingRight: style.paddingRight,
224
+ paddingBottom: style.paddingBottom,
225
+ paddingLeft: style.paddingLeft,
226
+ marginTop: style.marginTop,
227
+ marginRight: style.marginRight,
228
+ marginBottom: style.marginBottom,
229
+ marginLeft: style.marginLeft,
230
+ backgroundColor: style.backgroundColor,
231
+ color: style.color,
232
+ borderRadius: style.borderRadius,
233
+ boxShadow: style.boxShadow,
234
+ fontFamily: style.fontFamily,
235
+ fontSize: style.fontSize,
236
+ fontWeight: style.fontWeight,
237
+ lineHeight: style.lineHeight
238
+ },
239
+ parent_node_id: element.parentElement?.closest('[data-aub-node]')?.getAttribute('data-aub-node') || null
240
+ };
241
+ }
242
+ const root = document.querySelector('[data-aub-node="root"]');
243
+ return {
244
+ nodes,
245
+ root_children: root ? Array.from(root.children).map((child) => child.getAttribute('data-aub-node')).filter(Boolean) : [],
246
+ horizontal_overflow: document.documentElement.scrollWidth > window.innerWidth + 1
247
+ };
248
+ })()`;
249
+ }
250
+
251
+ async function waitForDocument(cdp) {
252
+ for (let attempt = 0; attempt < 80; attempt += 1) {
253
+ const ready = await evaluate(cdp, `document.readyState === 'complete'`);
254
+ if (ready) {
255
+ await new Promise((resolveWait) => setTimeout(resolveWait, 50));
256
+ return;
257
+ }
258
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
259
+ }
260
+ throw new Error('Timed out waiting for benchmark document.');
261
+ }
262
+
263
+ async function evaluate(cdp, expression) {
264
+ const response = await cdp.command('Runtime.evaluate', {
265
+ expression,
266
+ returnByValue: true,
267
+ awaitPromise: true,
268
+ });
269
+ if (response.exceptionDetails) throw new Error(response.exceptionDetails.text ?? 'Runtime evaluation failed.');
270
+ return response.result?.value;
271
+ }
272
+
273
+ async function openChromeTarget(port) {
274
+ for (let attempt = 0; attempt < 100; attempt += 1) {
275
+ try {
276
+ const response = await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' });
277
+ if (response.ok) return response.json();
278
+ } catch {
279
+ // Chrome is still starting.
280
+ }
281
+ await new Promise((resolveWait) => setTimeout(resolveWait, 50));
282
+ }
283
+ throw new Error('Timed out connecting to local Chrome.');
284
+ }
285
+
286
+ async function connectCdp(url) {
287
+ if (typeof WebSocket === 'undefined') {
288
+ throw new Error('This benchmark requires a Node.js runtime with global WebSocket support.');
289
+ }
290
+ const socket = new WebSocket(url);
291
+ await new Promise((resolveOpen, rejectOpen) => {
292
+ socket.addEventListener('open', resolveOpen, { once: true });
293
+ socket.addEventListener('error', rejectOpen, { once: true });
294
+ });
295
+ let nextId = 1;
296
+ const pending = new Map();
297
+ socket.addEventListener('message', (event) => {
298
+ const message = JSON.parse(String(event.data));
299
+ if (!message.id) return;
300
+ const request = pending.get(message.id);
301
+ if (!request) return;
302
+ pending.delete(message.id);
303
+ if (message.error) request.reject(new Error(message.error.message));
304
+ else request.resolve(message.result ?? {});
305
+ });
306
+ return {
307
+ command(method, params = {}) {
308
+ const id = nextId;
309
+ nextId += 1;
310
+ return new Promise((resolveCommand, rejectCommand) => {
311
+ pending.set(id, { resolve: resolveCommand, reject: rejectCommand });
312
+ socket.send(JSON.stringify({ id, method, params }));
313
+ });
314
+ },
315
+ close() {
316
+ socket.close();
317
+ },
318
+ };
319
+ }
320
+
321
+ function reservePort() {
322
+ const server = createServer();
323
+ return new Promise((resolvePort, rejectPort) => {
324
+ server.once('error', rejectPort);
325
+ server.listen(0, '127.0.0.1', () => {
326
+ const address = server.address();
327
+ const port = typeof address === 'object' && address ? address.port : 0;
328
+ server.close(() => resolvePort(port));
329
+ });
330
+ });
331
+ }
332
+
333
+ function findChrome() {
334
+ const candidates = [
335
+ process.env.AUB_CHROME_BIN,
336
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
337
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
338
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
339
+ 'google-chrome',
340
+ 'chromium',
341
+ ].filter(Boolean);
342
+ const absolute = candidates.find((candidate) => candidate.startsWith('/') && existsSync(candidate));
343
+ return absolute ?? candidates.find((candidate) => !candidate.startsWith('/')) ?? candidates[0];
344
+ }
345
+
346
+ function run(executable, commandArgs, stdin, cwd) {
347
+ return new Promise((resolveRun, rejectRun) => {
348
+ const child = spawn(executable, commandArgs, {
349
+ cwd,
350
+ env: process.env,
351
+ stdio: ['pipe', 'pipe', 'pipe'],
352
+ });
353
+ let stdout = '';
354
+ let stderr = '';
355
+ child.stdout.setEncoding('utf8');
356
+ child.stderr.setEncoding('utf8');
357
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
358
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
359
+ child.on('error', rejectRun);
360
+ child.on('close', (code) => resolveRun({ code: code ?? 1, stdout, stderr }));
361
+ child.stdin.end(stdin);
362
+ });
363
+ }
364
+
365
+ function extractOutput(text) {
366
+ const trimmed = text.trim();
367
+ const candidates = [trimmed, trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim()].filter(Boolean);
368
+ for (const candidate of candidates) {
369
+ try {
370
+ const output = JSON.parse(candidate);
371
+ if (isOutput(output)) return output;
372
+ } catch {
373
+ // Continue to balanced object extraction.
374
+ }
375
+ }
376
+ for (let start = trimmed.indexOf('{'); start >= 0; start = trimmed.indexOf('{', start + 1)) {
377
+ const candidate = balancedObjectAt(trimmed, start);
378
+ if (!candidate) continue;
379
+ try {
380
+ const output = JSON.parse(candidate);
381
+ if (isOutput(output)) return output;
382
+ } catch {
383
+ // Try the next opening brace.
384
+ }
385
+ }
386
+ throw new Error('Agent output did not contain a JSON object with html and implementation_report.');
387
+ }
388
+
389
+ function isOutput(value) {
390
+ return Boolean(value && typeof value.html === 'string' && value.implementation_report && typeof value.implementation_report === 'object');
391
+ }
392
+
393
+ function balancedObjectAt(text, start) {
394
+ let depth = 0;
395
+ let inString = false;
396
+ let escaped = false;
397
+ for (let index = start; index < text.length; index += 1) {
398
+ const character = text[index];
399
+ if (inString) {
400
+ if (escaped) escaped = false;
401
+ else if (character === '\\') escaped = true;
402
+ else if (character === '"') inString = false;
403
+ continue;
404
+ }
405
+ if (character === '"') inString = true;
406
+ else if (character === '{') depth += 1;
407
+ else if (character === '}') {
408
+ depth -= 1;
409
+ if (depth === 0) return text.slice(start, index + 1);
410
+ }
411
+ }
412
+ return null;
413
+ }
414
+
415
+ function escapeJs(value) {
416
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
417
+ }
418
+
419
+ function redactArgument(argument) {
420
+ return /(?:key|token|secret|password)=/i.test(argument)
421
+ ? argument.replace(/=.*/, '=REDACTED')
422
+ : argument;
423
+ }