@deneb-ui/cli 2.0.41 → 2.0.43
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/bin/index.js +11 -2
- package/package.json +4 -2
- package/src/arc/ai-agent.cjs +328 -0
- package/src/arc/ai-prompts.cjs +214 -0
- package/src/arc/component-registry.cjs +158 -0
- package/src/arc/index.cjs +133 -1
- package/src/arc/pr-agent.cjs +295 -0
- package/src/arc/printer.cjs +46 -0
- package/src/arc/version.cjs +2 -2
package/bin/index.js
CHANGED
|
@@ -1021,6 +1021,8 @@ if (command === 'init') {
|
|
|
1021
1021
|
let explain = false;
|
|
1022
1022
|
let legacy = false;
|
|
1023
1023
|
let telemetry = 'off';
|
|
1024
|
+
let aiEnabled = false;
|
|
1025
|
+
let aiDryRun = false;
|
|
1024
1026
|
for (let i = 0; i < commandArgs.length; i++) {
|
|
1025
1027
|
const arg = commandArgs[i];
|
|
1026
1028
|
if (arg === '--recipe' || arg === '-r') {
|
|
@@ -1033,6 +1035,11 @@ if (command === 'init') {
|
|
|
1033
1035
|
explain = true;
|
|
1034
1036
|
} else if (arg === '--legacy') {
|
|
1035
1037
|
legacy = true;
|
|
1038
|
+
} else if (arg === '--ai') {
|
|
1039
|
+
aiEnabled = true;
|
|
1040
|
+
} else if (arg === '--ai-dry-run') {
|
|
1041
|
+
aiEnabled = true;
|
|
1042
|
+
aiDryRun = true;
|
|
1036
1043
|
} else if (arg === '--telemetry' && commandArgs[i + 1]) {
|
|
1037
1044
|
telemetry = commandArgs[++i];
|
|
1038
1045
|
} else if (arg.startsWith('--telemetry=')) {
|
|
@@ -1041,7 +1048,7 @@ if (command === 'init') {
|
|
|
1041
1048
|
targetInput = arg;
|
|
1042
1049
|
}
|
|
1043
1050
|
}
|
|
1044
|
-
initProject(targetInput, { recipeName, dryRun, explain, legacy, telemetry });
|
|
1051
|
+
initProject(targetInput, { recipeName, dryRun, explain, legacy, telemetry, aiEnabled, aiDryRun });
|
|
1045
1052
|
} else if (command === 'create') {
|
|
1046
1053
|
createTemplate(commandArgs[0]);
|
|
1047
1054
|
} else if (command === 'add') {
|
|
@@ -1108,7 +1115,7 @@ if (command === 'init') {
|
|
|
1108
1115
|
|
|
1109
1116
|
Core Commands:
|
|
1110
1117
|
init Deneb ARC: convert an existing React/Next.js app into a Fivora-editable storefront
|
|
1111
|
-
flags: --dry-run --explain --recipe <name> --legacy --telemetry off|anonymous|enhanced
|
|
1118
|
+
flags: --dry-run --explain --recipe <name> --legacy --ai --ai-dry-run --telemetry off|anonymous|enhanced
|
|
1112
1119
|
doctor Run comprehensive environment, manifest, visual editing AST & asset diagnostic checks (flags: --fix, --json)
|
|
1113
1120
|
save-recipe Learn and save calibrated fixes & schemas into reusable recipe bank (e.g. deneb save-recipe . shoes-store)
|
|
1114
1121
|
learn Alias for save-recipe
|
|
@@ -1136,6 +1143,8 @@ Examples:
|
|
|
1136
1143
|
deneb init --recipe electronics
|
|
1137
1144
|
deneb init --recipe cosmetics
|
|
1138
1145
|
deneb init --legacy
|
|
1146
|
+
deneb init --ai
|
|
1147
|
+
deneb init --ai-dry-run
|
|
1139
1148
|
deneb fonts list
|
|
1140
1149
|
deneb fonts install .
|
|
1141
1150
|
deneb fonts install . --font inter --font playfair-display
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.43",
|
|
4
4
|
"description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deneb": "bin/index.js",
|
|
@@ -49,8 +49,10 @@
|
|
|
49
49
|
"license": "MIT",
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@babel/parser": "^7.28.0",
|
|
52
|
-
"@deneb-ui/core": "^2.0.
|
|
52
|
+
"@deneb-ui/core": "^2.0.43",
|
|
53
|
+
"@octokit/rest": "^22.0.1",
|
|
53
54
|
"adm-zip": "^0.6.0",
|
|
55
|
+
"dotenv": "^17.4.2",
|
|
54
56
|
"recast": "^0.23.11"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deneb ARC — AI Agent
|
|
5
|
+
*
|
|
6
|
+
* OpenAI API integration with self-healing validation loop.
|
|
7
|
+
* Generates editable wrapper components for unknown UI components
|
|
8
|
+
* and validates them through ARC's strict pipeline before accepting.
|
|
9
|
+
*
|
|
10
|
+
* Default engine: GPT-4o-mini (configurable via OPENAI_CONTENT_MODEL).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { parseSource } = require('./ast.cjs');
|
|
16
|
+
const { buildComponentPrompt, buildFixPrompt, buildDocsPrompt } = require('./ai-prompts.cjs');
|
|
17
|
+
|
|
18
|
+
const MAX_RETRIES = 3;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load AI configuration from environment variables.
|
|
22
|
+
* Call loadEnv() before using this if .env is not loaded yet.
|
|
23
|
+
*/
|
|
24
|
+
function getAiConfig() {
|
|
25
|
+
return {
|
|
26
|
+
apiKey: process.env.OPENAI_API_KEY || '',
|
|
27
|
+
model: process.env.OPENAI_CONTENT_MODEL || 'gpt-4o-mini',
|
|
28
|
+
maxTokens: parseInt(process.env.OPENAI_MAX_OUTPUT_TOKENS || '4000', 10),
|
|
29
|
+
timeoutMs: parseInt(process.env.OPENAI_CONTENT_TIMEOUT_MS || '120000', 10),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load .env file purely locally.
|
|
35
|
+
* Searches upward from targetDir or process.cwd(), supporting local .env and .env.local files.
|
|
36
|
+
* Eliminates the need for any global system configuration.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [startDir] - Starting directory (defaults to process.cwd())
|
|
39
|
+
*/
|
|
40
|
+
function loadEnv(startDir) {
|
|
41
|
+
const checked = new Set();
|
|
42
|
+
const candidates = [];
|
|
43
|
+
|
|
44
|
+
let curr = path.resolve(startDir || process.cwd());
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
candidates.push(path.join(curr, '.env'));
|
|
47
|
+
candidates.push(path.join(curr, '.env.local'));
|
|
48
|
+
candidates.push(path.join(curr, 'core', '.env'));
|
|
49
|
+
const parent = path.dirname(curr);
|
|
50
|
+
if (parent === curr) break;
|
|
51
|
+
curr = parent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
candidates.push(path.resolve(__dirname, '..', '..', '..', '..', '.env'));
|
|
55
|
+
candidates.push(path.resolve(__dirname, '..', '..', '.env'));
|
|
56
|
+
|
|
57
|
+
for (const envPath of candidates) {
|
|
58
|
+
if (!checked.has(envPath) && fs.existsSync(envPath)) {
|
|
59
|
+
try {
|
|
60
|
+
require('dotenv').config({ path: envPath });
|
|
61
|
+
} catch {
|
|
62
|
+
// dotenv not available — env vars must be set manually
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
checked.add(envPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if the AI agent is properly configured and ready to use.
|
|
72
|
+
* @returns {{ ready: boolean, reason?: string }}
|
|
73
|
+
*/
|
|
74
|
+
function checkAiReady() {
|
|
75
|
+
loadEnv();
|
|
76
|
+
const config = getAiConfig();
|
|
77
|
+
if (!config.apiKey) {
|
|
78
|
+
return { ready: false, reason: 'OPENAI_API_KEY not set. Create a .env file (see .env.example) or set the environment variable.' };
|
|
79
|
+
}
|
|
80
|
+
if (!config.apiKey.startsWith('sk-')) {
|
|
81
|
+
return { ready: false, reason: 'OPENAI_API_KEY does not look valid (should start with sk-).' };
|
|
82
|
+
}
|
|
83
|
+
return { ready: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Call the OpenAI Chat Completions API.
|
|
88
|
+
*
|
|
89
|
+
* Uses native fetch (Node 18+) — no external HTTP library needed.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} prompt - The user prompt
|
|
92
|
+
* @param {object} [opts] - Optional overrides
|
|
93
|
+
* @returns {Promise<{ content: string, inputTokens: number, outputTokens: number }>}
|
|
94
|
+
*/
|
|
95
|
+
async function callOpenAI(prompt, opts = {}) {
|
|
96
|
+
const config = getAiConfig();
|
|
97
|
+
const model = opts.model || config.model;
|
|
98
|
+
const maxTokens = opts.maxTokens || config.maxTokens;
|
|
99
|
+
const timeoutMs = opts.timeoutMs || config.timeoutMs;
|
|
100
|
+
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
model,
|
|
113
|
+
messages: [
|
|
114
|
+
{ role: 'system', content: 'You are a senior React/TypeScript developer specializing in the DENEB UI framework. Output only raw code — no markdown, no explanations.' },
|
|
115
|
+
{ role: 'user', content: prompt },
|
|
116
|
+
],
|
|
117
|
+
max_tokens: maxTokens,
|
|
118
|
+
temperature: 0.2,
|
|
119
|
+
}),
|
|
120
|
+
signal: controller.signal,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errorBody = await response.text().catch(() => '');
|
|
125
|
+
throw new Error(`OpenAI API error ${response.status}: ${errorBody.slice(0, 200)}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const data = await response.json();
|
|
129
|
+
const choice = data.choices?.[0];
|
|
130
|
+
if (!choice || !choice.message?.content) {
|
|
131
|
+
throw new Error('OpenAI returned empty response — no content in choices[0].message');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let content = choice.message.content.trim();
|
|
135
|
+
// Strip markdown fences if the model wraps output despite instructions
|
|
136
|
+
content = content.replace(/^```(?:tsx?|jsx?|typescript|javascript)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
content,
|
|
140
|
+
inputTokens: data.usage?.prompt_tokens || 0,
|
|
141
|
+
outputTokens: data.usage?.completion_tokens || 0,
|
|
142
|
+
};
|
|
143
|
+
} finally {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate generated component code using ARC's validators.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} code - The generated TypeScript/JSX code
|
|
152
|
+
* @param {string} filename - Virtual filename for error messages
|
|
153
|
+
* @returns {{ passed: boolean, errors: string[] }}
|
|
154
|
+
*/
|
|
155
|
+
function validateGeneratedCode(code, filename) {
|
|
156
|
+
const errors = [];
|
|
157
|
+
|
|
158
|
+
// 1. AST syntax check
|
|
159
|
+
try {
|
|
160
|
+
parseSource(code, filename);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
errors.push(`AST syntax error: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 2. Must have data-preview-field-path markers
|
|
166
|
+
if (!code.includes('data-preview-field-path')) {
|
|
167
|
+
errors.push('Missing data-preview-field-path markers — the component has no editable fields.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Must have data-preview-item-path on the root wrapper
|
|
171
|
+
if (!code.includes('data-preview-item-path')) {
|
|
172
|
+
errors.push('Missing data-preview-item-path on root wrapper element.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 4. Must have proper TypeScript exports
|
|
176
|
+
if (!code.includes('export function') && !code.includes('export const')) {
|
|
177
|
+
errors.push('Missing named export — component must use `export function Editable...()`.');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 5. Must have TypeScript interface
|
|
181
|
+
if (!code.includes('export interface')) {
|
|
182
|
+
errors.push('Missing exported TypeScript interface for component props.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 6. Must import EditableText or EditableImage (at least one)
|
|
186
|
+
if (!code.includes("from './EditableText'") && !code.includes("from './EditableImage'")) {
|
|
187
|
+
errors.push('Must import at least EditableText or EditableImage from relative path.');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 7. Must NOT have default export (Deneb convention)
|
|
191
|
+
if (/export\s+default\s+/m.test(code)) {
|
|
192
|
+
errors.push('Must NOT use default export — use named export only (Deneb convention).');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 8. Check for forbidden patterns
|
|
196
|
+
if (code.includes('useState') || code.includes('useEffect') || code.includes('useContext')) {
|
|
197
|
+
errors.push('Must NOT import React hooks (useState, useEffect, useContext). Component must be stateless.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (code.includes('useSiteData')) {
|
|
201
|
+
errors.push('Must NOT import useSiteData directly. The wrapper uses EditableText/EditableImage primitives instead.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
passed: errors.length === 0,
|
|
206
|
+
errors,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract the list of editable field paths from generated component code.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} code - The generated component code
|
|
214
|
+
* @returns {string[]} Array of field path patterns found
|
|
215
|
+
*/
|
|
216
|
+
function extractFieldPaths(code) {
|
|
217
|
+
const matches = code.matchAll(/data-preview-field-path=\{[`"']([^`"']+)[`"']\}/g);
|
|
218
|
+
const paths = new Set();
|
|
219
|
+
for (const m of matches) {
|
|
220
|
+
// Normalize template literal patterns like `${itemPath}.name`
|
|
221
|
+
const path = m[1].replace(/\$\{[^}]+\}/g, '*');
|
|
222
|
+
paths.add(path);
|
|
223
|
+
}
|
|
224
|
+
return [...paths];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Main AI adaptation function with self-healing validation loop.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} sourceCode - The unknown component's source code
|
|
231
|
+
* @param {string} componentName - The component name (e.g. "Carousel")
|
|
232
|
+
* @param {object} profile - ARC project profile
|
|
233
|
+
* @param {object} [callbacks] - Optional progress callbacks
|
|
234
|
+
* @param {function} [callbacks.onAttempt] - Called at start of each attempt: (attemptNum, maxRetries)
|
|
235
|
+
* @param {function} [callbacks.onValidationFail] - Called when validation fails: (errors, attemptNum)
|
|
236
|
+
* @param {function} [callbacks.onSuccess] - Called on success: (attemptNum)
|
|
237
|
+
* @returns {Promise<{ success: boolean, code?: string, fields?: string[], attempts: number, totalInputTokens: number, totalOutputTokens: number, error?: string }>}
|
|
238
|
+
*/
|
|
239
|
+
async function adaptComponent(sourceCode, componentName, profile, callbacks = {}) {
|
|
240
|
+
let totalInputTokens = 0;
|
|
241
|
+
let totalOutputTokens = 0;
|
|
242
|
+
|
|
243
|
+
// Generate initial code
|
|
244
|
+
const prompt = buildComponentPrompt(sourceCode, componentName, profile);
|
|
245
|
+
let lastCode = '';
|
|
246
|
+
let lastErrors = [];
|
|
247
|
+
|
|
248
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
249
|
+
if (callbacks.onAttempt) callbacks.onAttempt(attempt, MAX_RETRIES);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
let result;
|
|
253
|
+
if (attempt === 1) {
|
|
254
|
+
result = await callOpenAI(prompt);
|
|
255
|
+
} else {
|
|
256
|
+
const fixPrompt = buildFixPrompt(lastCode, lastErrors.join('\n'), attempt);
|
|
257
|
+
result = await callOpenAI(fixPrompt);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
totalInputTokens += result.inputTokens;
|
|
261
|
+
totalOutputTokens += result.outputTokens;
|
|
262
|
+
lastCode = result.content;
|
|
263
|
+
|
|
264
|
+
// Validate the generated code
|
|
265
|
+
const filename = `Editable${componentName}.tsx`;
|
|
266
|
+
const validation = validateGeneratedCode(lastCode, filename);
|
|
267
|
+
|
|
268
|
+
if (validation.passed) {
|
|
269
|
+
if (callbacks.onSuccess) callbacks.onSuccess(attempt);
|
|
270
|
+
return {
|
|
271
|
+
success: true,
|
|
272
|
+
code: lastCode,
|
|
273
|
+
fields: extractFieldPaths(lastCode),
|
|
274
|
+
attempts: attempt,
|
|
275
|
+
totalInputTokens,
|
|
276
|
+
totalOutputTokens,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validation failed — prepare for retry
|
|
281
|
+
lastErrors = validation.errors;
|
|
282
|
+
if (callbacks.onValidationFail) callbacks.onValidationFail(validation.errors, attempt);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
lastErrors = [err.message];
|
|
285
|
+
if (callbacks.onValidationFail) callbacks.onValidationFail([err.message], attempt);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// All retries exhausted
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
attempts: MAX_RETRIES,
|
|
293
|
+
totalInputTokens,
|
|
294
|
+
totalOutputTokens,
|
|
295
|
+
error: `Failed after ${MAX_RETRIES} attempts. Last errors:\n${lastErrors.join('\n')}`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Generate documentation page for a successfully adapted component.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} componentName - e.g. "Carousel"
|
|
303
|
+
* @param {string} componentCode - The validated Editable*.tsx source
|
|
304
|
+
* @param {string[]} editableFields - List of field paths
|
|
305
|
+
* @returns {Promise<{ success: boolean, code?: string, error?: string }>}
|
|
306
|
+
*/
|
|
307
|
+
async function generateDocsPage(componentName, componentCode, editableFields) {
|
|
308
|
+
const prompt = buildDocsPrompt(componentName, componentCode, editableFields);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const result = await callOpenAI(prompt);
|
|
312
|
+
return { success: true, code: result.content };
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return { success: false, error: err.message };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
loadEnv,
|
|
320
|
+
checkAiReady,
|
|
321
|
+
getAiConfig,
|
|
322
|
+
callOpenAI,
|
|
323
|
+
validateGeneratedCode,
|
|
324
|
+
extractFieldPaths,
|
|
325
|
+
adaptComponent,
|
|
326
|
+
generateDocsPage,
|
|
327
|
+
MAX_RETRIES,
|
|
328
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deneb ARC — AI Prompt Templates
|
|
5
|
+
*
|
|
6
|
+
* Carefully engineered prompt templates that teach GPT-4o-mini the exact
|
|
7
|
+
* Deneb editable component conventions. Uses a real EditableServiceCard
|
|
8
|
+
* as a reference pattern so the AI learns the correct structure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─── Reference component (shortened for token efficiency) ────────
|
|
12
|
+
const REFERENCE_COMPONENT = `
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { EditableText } from './EditableText';
|
|
15
|
+
import { EditableImage } from './EditableImage';
|
|
16
|
+
|
|
17
|
+
export interface ServiceItem {
|
|
18
|
+
id?: string | number;
|
|
19
|
+
name?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
price?: string | number;
|
|
22
|
+
description?: string;
|
|
23
|
+
imageUrl?: string;
|
|
24
|
+
image?: string;
|
|
25
|
+
features?: unknown[];
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EditableServiceCardProps extends React.HTMLAttributes<HTMLElement> {
|
|
30
|
+
itemPath: string;
|
|
31
|
+
service: ServiceItem;
|
|
32
|
+
imageFallback?: string;
|
|
33
|
+
as?: React.ElementType;
|
|
34
|
+
align?: 'left' | 'center' | 'right';
|
|
35
|
+
showPrice?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function EditableServiceCard({
|
|
39
|
+
itemPath,
|
|
40
|
+
service,
|
|
41
|
+
imageFallback = '/placeholder.svg',
|
|
42
|
+
as: Component = 'article',
|
|
43
|
+
align = 'left',
|
|
44
|
+
showPrice = true,
|
|
45
|
+
className = '',
|
|
46
|
+
style,
|
|
47
|
+
children,
|
|
48
|
+
...props
|
|
49
|
+
}: EditableServiceCardProps) {
|
|
50
|
+
const name = String(service?.name || service?.title || '');
|
|
51
|
+
const description = String(service?.description || '');
|
|
52
|
+
const imageUrl = String(service?.imageUrl || service?.image || '');
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Component
|
|
56
|
+
data-preview-item-path={itemPath}
|
|
57
|
+
className={\`editable-service-card \${className}\`.trim()}
|
|
58
|
+
style={{ textAlign: align, ...style }}
|
|
59
|
+
{...(props as any)}
|
|
60
|
+
>
|
|
61
|
+
<div className="service-card-image-wrap">
|
|
62
|
+
<EditableImage
|
|
63
|
+
id={\`\${itemPath}.imageUrl\`}
|
|
64
|
+
data-preview-field-path={\`\${itemPath}.imageUrl\`}
|
|
65
|
+
src={imageUrl}
|
|
66
|
+
fallbackSrc={imageFallback}
|
|
67
|
+
alt={name}
|
|
68
|
+
className="service-card-image"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="service-card-body">
|
|
72
|
+
<EditableText
|
|
73
|
+
as="h2"
|
|
74
|
+
id={\`\${itemPath}.name\`}
|
|
75
|
+
data-preview-field-path={\`\${itemPath}.name\`}
|
|
76
|
+
defaultValue={name}
|
|
77
|
+
className="service-card-title"
|
|
78
|
+
/>
|
|
79
|
+
<EditableText
|
|
80
|
+
as="p"
|
|
81
|
+
id={\`\${itemPath}.description\`}
|
|
82
|
+
data-preview-field-path={\`\${itemPath}.description\`}
|
|
83
|
+
defaultValue={description}
|
|
84
|
+
className="service-card-description"
|
|
85
|
+
/>
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
</Component>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
`.trim();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build the main component generation prompt.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} sourceCode - The unknown component's source code
|
|
97
|
+
* @param {string} componentName - The component's name (e.g. "Carousel")
|
|
98
|
+
* @param {object} profile - ARC project profile (framework, libraries, etc.)
|
|
99
|
+
* @returns {string} The complete prompt for OpenAI
|
|
100
|
+
*/
|
|
101
|
+
function buildComponentPrompt(sourceCode, componentName, profile) {
|
|
102
|
+
return `You are an expert React/TypeScript developer working on the DENEB UI framework.
|
|
103
|
+
Your task is to create an editable wrapper component for an existing component.
|
|
104
|
+
|
|
105
|
+
## RULES (MANDATORY — violating any rule means the output is rejected):
|
|
106
|
+
|
|
107
|
+
1. **Import only from relative paths**: Use \`./EditableText\` and \`./EditableImage\` (these are the ONLY two Deneb primitives you may use).
|
|
108
|
+
2. **Every user-visible text** must be wrapped in \`<EditableText>\` with:
|
|
109
|
+
- \`as\` prop matching the original HTML tag (h1, h2, p, span, etc.)
|
|
110
|
+
- \`id={\`\${itemPath}.fieldName\`}\`
|
|
111
|
+
- \`data-preview-field-path={\`\${itemPath}.fieldName\`}\`
|
|
112
|
+
- \`defaultValue={value}\`
|
|
113
|
+
3. **Every user-visible image** must use \`<EditableImage>\` with:
|
|
114
|
+
- \`id={\`\${itemPath}.fieldName\`}\`
|
|
115
|
+
- \`data-preview-field-path={\`\${itemPath}.fieldName\`}\`
|
|
116
|
+
- \`src={value}\`
|
|
117
|
+
- \`fallbackSrc={imageFallback}\`
|
|
118
|
+
4. **Lists/arrays** must have \`data-preview-list-path\` on the container and \`data-preview-item-path\` on each item.
|
|
119
|
+
5. **Root wrapper** must have \`data-preview-item-path={itemPath}\`.
|
|
120
|
+
6. **Export a TypeScript interface** named \`Editable${componentName}Props\` extending \`React.HTMLAttributes<HTMLElement>\`.
|
|
121
|
+
7. **Export a named function** (not default export) named \`Editable${componentName}\`.
|
|
122
|
+
8. **Preserve all original CSS classes and styling** — do NOT remove or change className values.
|
|
123
|
+
9. **Props must include**: \`itemPath: string\` (required), data object, \`as?: React.ElementType\`, \`className?\`, \`style?\`, \`children?\`.
|
|
124
|
+
10. **Use \`'use strict'\` is NOT needed** — this is a .tsx file.
|
|
125
|
+
11. **Do NOT import React hooks** like useState, useEffect, useContext. The component must be stateless.
|
|
126
|
+
12. **Do NOT import useSiteData** — the wrapper component does not need it directly.
|
|
127
|
+
|
|
128
|
+
## REFERENCE PATTERN (follow this structure exactly):
|
|
129
|
+
|
|
130
|
+
\`\`\`tsx
|
|
131
|
+
${REFERENCE_COMPONENT}
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
## SOURCE COMPONENT TO ADAPT:
|
|
135
|
+
|
|
136
|
+
Component name: ${componentName}
|
|
137
|
+
Framework: ${profile.framework || 'nextjs'}
|
|
138
|
+
Libraries: ${(profile.componentLibraries || []).join(', ') || 'none detected'}
|
|
139
|
+
|
|
140
|
+
\`\`\`tsx
|
|
141
|
+
${sourceCode}
|
|
142
|
+
\`\`\`
|
|
143
|
+
|
|
144
|
+
## YOUR OUTPUT:
|
|
145
|
+
|
|
146
|
+
Generate ONLY the complete TypeScript (.tsx) file content for \`Editable${componentName}.tsx\`.
|
|
147
|
+
Do NOT include markdown fences, explanations, or comments outside the code.
|
|
148
|
+
Output the raw TypeScript code directly.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build the error-fix prompt when the validator rejects AI-generated code.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} previousCode - The code that failed validation
|
|
155
|
+
* @param {string} errorMessage - The exact validator error
|
|
156
|
+
* @param {number} attempt - Current attempt number
|
|
157
|
+
* @returns {string} The fix prompt
|
|
158
|
+
*/
|
|
159
|
+
function buildFixPrompt(previousCode, errorMessage, attempt) {
|
|
160
|
+
return `The DENEB ARC validator REJECTED your generated component (attempt ${attempt}/3).
|
|
161
|
+
|
|
162
|
+
## EXACT ERROR:
|
|
163
|
+
${errorMessage}
|
|
164
|
+
|
|
165
|
+
## YOUR PREVIOUS CODE:
|
|
166
|
+
\`\`\`tsx
|
|
167
|
+
${previousCode}
|
|
168
|
+
\`\`\`
|
|
169
|
+
|
|
170
|
+
## INSTRUCTIONS:
|
|
171
|
+
1. Fix ONLY the specific error described above.
|
|
172
|
+
2. Do NOT change anything else — preserve all existing field paths, markers, and class names.
|
|
173
|
+
3. Output the COMPLETE corrected .tsx file (not a diff, not a snippet — the full file).
|
|
174
|
+
4. Do NOT include markdown fences or explanations. Output raw TypeScript code only.`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build the docs page generation prompt.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} componentName - e.g. "Carousel"
|
|
181
|
+
* @param {string} componentCode - The validated EditableCarousel.tsx source
|
|
182
|
+
* @param {string[]} editableFields - List of field paths discovered
|
|
183
|
+
* @returns {string} The docs prompt
|
|
184
|
+
*/
|
|
185
|
+
function buildDocsPrompt(componentName, componentCode, editableFields) {
|
|
186
|
+
return `Generate a Next.js documentation page for the DENEB UI component "Editable${componentName}".
|
|
187
|
+
|
|
188
|
+
## COMPONENT CODE:
|
|
189
|
+
\`\`\`tsx
|
|
190
|
+
${componentCode}
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
## EDITABLE FIELDS:
|
|
194
|
+
${editableFields.map((f) => `- ${f}`).join('\n')}
|
|
195
|
+
|
|
196
|
+
## OUTPUT FORMAT:
|
|
197
|
+
Generate a React component that exports a default function named \`Editable${componentName}DocsPage\`.
|
|
198
|
+
It should render:
|
|
199
|
+
1. A title section with the component name
|
|
200
|
+
2. A description paragraph explaining what the component does
|
|
201
|
+
3. A props table showing all available props with their types and defaults
|
|
202
|
+
4. A usage example code block
|
|
203
|
+
5. A list of editable field paths
|
|
204
|
+
|
|
205
|
+
Use plain HTML/JSX with className for styling. Do NOT import any external UI libraries.
|
|
206
|
+
Output raw TSX code only — no markdown fences or explanations.`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
REFERENCE_COMPONENT,
|
|
211
|
+
buildComponentPrompt,
|
|
212
|
+
buildFixPrompt,
|
|
213
|
+
buildDocsPrompt,
|
|
214
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deneb ARC — Component Registry
|
|
5
|
+
*
|
|
6
|
+
* Master lookup of all known editable components in @deneb-ui/ui.
|
|
7
|
+
* Used by the AI agent to classify "known vs unknown" components
|
|
8
|
+
* during `deneb init --ai`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const KNOWN_EDITABLE_COMPONENTS = new Set([
|
|
12
|
+
// ─── Core Text & Primitives ────────────────────────────────────
|
|
13
|
+
'EditableText',
|
|
14
|
+
'EditableHeading',
|
|
15
|
+
'EditableParagraph',
|
|
16
|
+
'EditableBadge',
|
|
17
|
+
'EditableQuote',
|
|
18
|
+
'EditableButton',
|
|
19
|
+
'EditableImage',
|
|
20
|
+
'EditableMap',
|
|
21
|
+
'EditableList',
|
|
22
|
+
'EditableBox',
|
|
23
|
+
'EditableGrid',
|
|
24
|
+
'EditableSection',
|
|
25
|
+
'EditableDialog',
|
|
26
|
+
|
|
27
|
+
// ─── Product & Commerce ────────────────────────────────────────
|
|
28
|
+
'EditableProductCard',
|
|
29
|
+
'EditableProductGrid',
|
|
30
|
+
'EditableProductDetail',
|
|
31
|
+
'EditableCartDrawer',
|
|
32
|
+
'EditableFilterSidebar',
|
|
33
|
+
'EditablePricingCard',
|
|
34
|
+
'ProductQuickView',
|
|
35
|
+
|
|
36
|
+
// ─── Content & Social Proof ────────────────────────────────────
|
|
37
|
+
'EditableServiceCard',
|
|
38
|
+
'EditableCard',
|
|
39
|
+
'EditableTestimonialCard',
|
|
40
|
+
'EditableTestimonialSection',
|
|
41
|
+
'EditableCustomerReviews',
|
|
42
|
+
'EditableGoogleFeedback',
|
|
43
|
+
'EditableFAQAccordion',
|
|
44
|
+
'EditableContactForm',
|
|
45
|
+
|
|
46
|
+
// ─── Layout & Navigation ───────────────────────────────────────
|
|
47
|
+
'EditableNavbar',
|
|
48
|
+
'EditableFooter',
|
|
49
|
+
'EditableHero',
|
|
50
|
+
'EditableHeroCentered',
|
|
51
|
+
'EditableHeroSplit',
|
|
52
|
+
'EditableAnnouncementBar',
|
|
53
|
+
'EditableCategoryPills',
|
|
54
|
+
'StickyMobileBar',
|
|
55
|
+
'TrustBadges',
|
|
56
|
+
'CookieConsentBanner',
|
|
57
|
+
|
|
58
|
+
// ─── Infrastructure ────────────────────────────────────────────
|
|
59
|
+
'SiteDataProvider',
|
|
60
|
+
'ThemeStyles',
|
|
61
|
+
'ResponsiveBaseStyles',
|
|
62
|
+
'DenebComponentStyles',
|
|
63
|
+
'FontLoader',
|
|
64
|
+
'PreviewField',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Canonical short-name aliases that Shadcn/HeroUI developers might use.
|
|
69
|
+
* Maps common short names to their Deneb equivalents.
|
|
70
|
+
*/
|
|
71
|
+
const ALIAS_MAP = {
|
|
72
|
+
'Button': 'EditableButton',
|
|
73
|
+
'Card': 'EditableCard',
|
|
74
|
+
'Dialog': 'EditableDialog',
|
|
75
|
+
'Text': 'EditableText',
|
|
76
|
+
'Heading': 'EditableHeading',
|
|
77
|
+
'Paragraph': 'EditableParagraph',
|
|
78
|
+
'Badge': 'EditableBadge',
|
|
79
|
+
'Quote': 'EditableQuote',
|
|
80
|
+
'Image': 'EditableImage',
|
|
81
|
+
'Map': 'EditableMap',
|
|
82
|
+
'Grid': 'EditableGrid',
|
|
83
|
+
'Section': 'EditableSection',
|
|
84
|
+
'Box': 'EditableBox',
|
|
85
|
+
'List': 'EditableList',
|
|
86
|
+
'ProductCard': 'EditableProductCard',
|
|
87
|
+
'ProductGrid': 'EditableProductGrid',
|
|
88
|
+
'ProductDetail': 'EditableProductDetail',
|
|
89
|
+
'CustomerReviews': 'EditableCustomerReviews',
|
|
90
|
+
'GoogleFeedback': 'EditableGoogleFeedback',
|
|
91
|
+
'ServiceCard': 'EditableServiceCard',
|
|
92
|
+
'PricingCard': 'EditablePricingCard',
|
|
93
|
+
'TestimonialCard': 'EditableTestimonialCard',
|
|
94
|
+
'TestimonialSection': 'EditableTestimonialSection',
|
|
95
|
+
'Testimonials': 'EditableTestimonialSection',
|
|
96
|
+
'FAQ': 'EditableFAQAccordion',
|
|
97
|
+
'Accordion': 'EditableFAQAccordion',
|
|
98
|
+
'ContactForm': 'EditableContactForm',
|
|
99
|
+
'Navbar': 'EditableNavbar',
|
|
100
|
+
'Header': 'EditableNavbar',
|
|
101
|
+
'Footer': 'EditableFooter',
|
|
102
|
+
'Hero': 'EditableHeroCentered',
|
|
103
|
+
'HeroSplit': 'EditableHeroSplit',
|
|
104
|
+
'AnnouncementBar': 'EditableAnnouncementBar',
|
|
105
|
+
'CategoryPills': 'EditableCategoryPills',
|
|
106
|
+
'CartDrawer': 'EditableCartDrawer',
|
|
107
|
+
'FilterSidebar': 'EditableFilterSidebar',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a component name is already covered by @deneb-ui/ui.
|
|
112
|
+
* Checks both the full Editable* name and common aliases.
|
|
113
|
+
*/
|
|
114
|
+
function isKnownComponent(name) {
|
|
115
|
+
if (!name || typeof name !== 'string') return false;
|
|
116
|
+
if (KNOWN_EDITABLE_COMPONENTS.has(name)) return true;
|
|
117
|
+
if (ALIAS_MAP[name]) return true;
|
|
118
|
+
// Also check if user passed the Editable-prefixed version
|
|
119
|
+
if (KNOWN_EDITABLE_COMPONENTS.has(`Editable${name}`)) return true;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns the list of all known editable component names.
|
|
125
|
+
*/
|
|
126
|
+
function getKnownComponentNames() {
|
|
127
|
+
return [...KNOWN_EDITABLE_COMPONENTS];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Given a list of discovered component names from a project scan,
|
|
132
|
+
* returns { known: string[], unknown: string[] }.
|
|
133
|
+
*/
|
|
134
|
+
function classifyComponents(componentNames) {
|
|
135
|
+
const known = [];
|
|
136
|
+
const unknown = [];
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
|
|
139
|
+
for (const name of componentNames) {
|
|
140
|
+
if (seen.has(name)) continue;
|
|
141
|
+
seen.add(name);
|
|
142
|
+
if (isKnownComponent(name)) {
|
|
143
|
+
known.push(name);
|
|
144
|
+
} else {
|
|
145
|
+
unknown.push(name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { known, unknown };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
KNOWN_EDITABLE_COMPONENTS,
|
|
154
|
+
ALIAS_MAP,
|
|
155
|
+
isKnownComponent,
|
|
156
|
+
getKnownComponentNames,
|
|
157
|
+
classifyComponents,
|
|
158
|
+
};
|
package/src/arc/index.cjs
CHANGED
|
@@ -36,6 +36,9 @@ const {
|
|
|
36
36
|
findUncoveredVisibleText,
|
|
37
37
|
} = require('./fivora-contract.cjs');
|
|
38
38
|
const printer = require('./printer.cjs');
|
|
39
|
+
const { classifyComponents } = require('./component-registry.cjs');
|
|
40
|
+
const { checkAiReady, adaptComponent, generateDocsPage, loadEnv } = require('./ai-agent.cjs');
|
|
41
|
+
const { checkGithubReady, createComponentPR } = require('./pr-agent.cjs');
|
|
39
42
|
|
|
40
43
|
function parseArcOptions(raw = {}) {
|
|
41
44
|
return {
|
|
@@ -46,6 +49,8 @@ function parseArcOptions(raw = {}) {
|
|
|
46
49
|
skipInstall: Boolean(raw.skipInstall),
|
|
47
50
|
detectedPages: raw.detectedPages || null,
|
|
48
51
|
json: Boolean(raw.json),
|
|
52
|
+
aiEnabled: Boolean(raw.aiEnabled),
|
|
53
|
+
aiDryRun: Boolean(raw.aiDryRun),
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -224,7 +229,7 @@ function analyzeProjectFiles(profile, graph) {
|
|
|
224
229
|
return analyses;
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
function
|
|
232
|
+
async function runDenebArcAsync(projectDir, projectName, options = {}) {
|
|
228
233
|
const opts = parseArcOptions(options);
|
|
229
234
|
const runId = createRunId();
|
|
230
235
|
const startedAt = new Date().toISOString();
|
|
@@ -255,6 +260,133 @@ function runDenebArc(projectDir, projectName, options = {}) {
|
|
|
255
260
|
);
|
|
256
261
|
printer.printScan(profile, graph, candidateCount, actionCount);
|
|
257
262
|
|
|
263
|
+
// ─── AI Agent: Classify & Adapt Unknown Components ──────────
|
|
264
|
+
let aiResults = [];
|
|
265
|
+
loadEnv(projectDir);
|
|
266
|
+
const aiCheck = checkAiReady();
|
|
267
|
+
if (!aiCheck.ready) {
|
|
268
|
+
printer.warn(`AI Agent disabled: ${aiCheck.reason}`);
|
|
269
|
+
} else {
|
|
270
|
+
const componentNames = (profile.components || []).map((c) => c.name || c.tag).filter(Boolean);
|
|
271
|
+
const { unknown } = classifyComponents(componentNames);
|
|
272
|
+
|
|
273
|
+
if (unknown.length > 0) {
|
|
274
|
+
printer.printAiDetected(unknown.length);
|
|
275
|
+
|
|
276
|
+
for (const componentName of unknown) {
|
|
277
|
+
const meta = (profile.components || []).find((c) => (c.name || c.tag) === componentName);
|
|
278
|
+
const absFile = meta?.file ? path.join(projectDir, meta.file) : null;
|
|
279
|
+
let sourceCode = '';
|
|
280
|
+
if (absFile && fs.existsSync(absFile)) {
|
|
281
|
+
try { sourceCode = fs.readFileSync(absFile, 'utf8'); } catch { /* skip */ }
|
|
282
|
+
}
|
|
283
|
+
if (!sourceCode) {
|
|
284
|
+
printer.printAiSkipped(componentName, 'could not read source file');
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const aiResult = await adaptComponent(sourceCode, componentName, profile, {
|
|
290
|
+
onAttempt: (attempt, max) => printer.printAiAttempt(componentName, attempt, max),
|
|
291
|
+
onValidationFail: (errors, attempt) => printer.printAiValidationFail(errors, attempt),
|
|
292
|
+
onSuccess: (attempt) => printer.printAiSuccess(componentName, attempt, 0),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (aiResult.success) {
|
|
296
|
+
aiResults.push({ componentName, ...aiResult });
|
|
297
|
+
printer.printAiSuccess(componentName, aiResult.attempts, aiResult.fields.length);
|
|
298
|
+
} else {
|
|
299
|
+
printer.printAiSkipped(componentName, aiResult.error || 'failed after max retries');
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
printer.printAiSkipped(componentName, err.message);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Background: Open PRs for successfully adapted components
|
|
307
|
+
const ghCheck = checkGithubReady();
|
|
308
|
+
if (ghCheck.ready && aiResults.length > 0) {
|
|
309
|
+
for (const aiResult of aiResults) {
|
|
310
|
+
try {
|
|
311
|
+
const docsResult = await generateDocsPage(aiResult.componentName, aiResult.code, aiResult.fields);
|
|
312
|
+
const prResult = await createComponentPR({
|
|
313
|
+
componentName: aiResult.componentName,
|
|
314
|
+
componentCode: aiResult.code,
|
|
315
|
+
docsCode: docsResult.success ? docsResult.code : null,
|
|
316
|
+
editableFields: aiResult.fields,
|
|
317
|
+
attempts: aiResult.attempts,
|
|
318
|
+
totalInputTokens: aiResult.totalInputTokens,
|
|
319
|
+
totalOutputTokens: aiResult.totalOutputTokens,
|
|
320
|
+
dryRun: opts.aiDryRun,
|
|
321
|
+
});
|
|
322
|
+
if (prResult.corePr) {
|
|
323
|
+
printer.printAiPr('chamikathereal/core → deneb-ui/core', `feat/ai-editable-${aiResult.componentName.toLowerCase()}`, prResult.corePr.number);
|
|
324
|
+
}
|
|
325
|
+
if (prResult.uiPr) {
|
|
326
|
+
printer.printAiPr('chamikathereal/ui → deneb-ui/ui', `feat/ai-docs-${aiResult.componentName.toLowerCase()}`, prResult.uiPr.number);
|
|
327
|
+
}
|
|
328
|
+
for (const err of prResult.errors) {
|
|
329
|
+
printer.warn(`PR: ${err}`);
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
332
|
+
printer.warn(`PR creation failed for ${aiResult.componentName}: ${err.message}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} else if (!ghCheck.ready && aiResults.length > 0) {
|
|
336
|
+
printer.warn(`GitHub PRs skipped: ${ghCheck.reason}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const totalTokens = aiResults.reduce((n, r) => n + r.totalInputTokens + r.totalOutputTokens, 0);
|
|
340
|
+
printer.printAiSummary(aiResults.length, unknown.length - aiResults.length, totalTokens);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return runArcTransformations(projectDir, projectName, opts, profile, graph, analyses, runId, startedAt);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function runDenebArcSync(projectDir, projectName, options = {}) {
|
|
348
|
+
const opts = parseArcOptions(options);
|
|
349
|
+
const runId = createRunId();
|
|
350
|
+
const startedAt = new Date().toISOString();
|
|
351
|
+
|
|
352
|
+
printer.printBanner(opts.dryRun ? 'dry-run' : opts.explain ? 'explain' : 'run');
|
|
353
|
+
|
|
354
|
+
const profile = scanProject(projectDir);
|
|
355
|
+
if (opts.detectedPages && Array.isArray(opts.detectedPages) && opts.detectedPages.length) {
|
|
356
|
+
const scannedIds = new Set(profile.routes.map((r) => r.id));
|
|
357
|
+
for (const page of opts.detectedPages) {
|
|
358
|
+
if (page && page.id && !scannedIds.has(page.id)) {
|
|
359
|
+
profile.routes.push(page);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
printer.printProfile(profile);
|
|
365
|
+
const graph = buildDependencyGraph(profile);
|
|
366
|
+
const analyses = analyzeProjectFiles(profile, graph);
|
|
367
|
+
|
|
368
|
+
const candidateCount = analyses.reduce(
|
|
369
|
+
(n, a) => n + (a.candidates || []).filter((c) => c.kind !== 'decoration' && c.kind !== 'already-editable' && !c.skip).length,
|
|
370
|
+
0
|
|
371
|
+
);
|
|
372
|
+
const actionCount = analyses.reduce(
|
|
373
|
+
(n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.kind === 'url').length,
|
|
374
|
+
0
|
|
375
|
+
);
|
|
376
|
+
printer.printScan(profile, graph, candidateCount, actionCount);
|
|
377
|
+
|
|
378
|
+
return runArcTransformations(projectDir, projectName, opts, profile, graph, analyses, runId, startedAt);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function runDenebArc(projectDir, projectName, options = {}) {
|
|
382
|
+
const opts = parseArcOptions(options);
|
|
383
|
+
if (opts.aiEnabled) {
|
|
384
|
+
return runDenebArcAsync(projectDir, projectName, options);
|
|
385
|
+
}
|
|
386
|
+
return runDenebArcSync(projectDir, projectName, options);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function runArcTransformations(projectDir, projectName, opts, profile, graph, analyses, runId, startedAt) {
|
|
258
390
|
const sourceAbs = profile.jsxFiles.map((f) => path.join(projectDir, f));
|
|
259
391
|
const recipeMatch = matchRecipeV2(projectDir, profile, sourceAbs, opts.recipeName);
|
|
260
392
|
if (recipeMatch.recipe) {
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deneb ARC — PR Agent
|
|
5
|
+
*
|
|
6
|
+
* GitHub API integration for automated Pull Request creation.
|
|
7
|
+
* Creates branches, commits files, and opens PRs from the user's
|
|
8
|
+
* personal fork into the upstream deneb-ui organization repos.
|
|
9
|
+
*
|
|
10
|
+
* All commits are authored under the configured GITHUB_USERNAME
|
|
11
|
+
* and GITHUB_EMAIL for full contribution credit.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { loadEnv } = require('./ai-agent.cjs');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get GitHub configuration from environment variables.
|
|
18
|
+
*/
|
|
19
|
+
function getGithubConfig() {
|
|
20
|
+
return {
|
|
21
|
+
pat: process.env.GITHUB_PAT || '',
|
|
22
|
+
username: process.env.GITHUB_USERNAME || 'chamikathereal',
|
|
23
|
+
email: process.env.GITHUB_EMAIL || 'dmforceeg@gmail.com',
|
|
24
|
+
org: process.env.GITHUB_ORG || 'deneb-ui',
|
|
25
|
+
coreRepo: process.env.GITHUB_CORE_REPO || 'core',
|
|
26
|
+
uiRepo: process.env.GITHUB_UI_REPO || 'ui',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if GitHub integration is ready.
|
|
32
|
+
* @returns {{ ready: boolean, reason?: string }}
|
|
33
|
+
*/
|
|
34
|
+
function checkGithubReady() {
|
|
35
|
+
loadEnv();
|
|
36
|
+
const config = getGithubConfig();
|
|
37
|
+
if (!config.pat) {
|
|
38
|
+
return { ready: false, reason: 'GITHUB_PAT not set. Generate a Personal Access Token at https://github.com/settings/tokens with "repo" scope.' };
|
|
39
|
+
}
|
|
40
|
+
if (!config.pat.startsWith('ghp_') && !config.pat.startsWith('github_pat_')) {
|
|
41
|
+
return { ready: false, reason: 'GITHUB_PAT does not look valid (should start with ghp_ or github_pat_).' };
|
|
42
|
+
}
|
|
43
|
+
return { ready: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a GitHub API client using @octokit/rest.
|
|
48
|
+
* Returns null if the PAT is not configured.
|
|
49
|
+
*/
|
|
50
|
+
function createOctokitClient() {
|
|
51
|
+
const config = getGithubConfig();
|
|
52
|
+
if (!config.pat) return null;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const { Octokit } = require('@octokit/rest');
|
|
56
|
+
return new Octokit({ auth: config.pat });
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the SHA of the latest commit on a branch.
|
|
64
|
+
*/
|
|
65
|
+
async function getLatestCommitSha(octokit, owner, repo, branch = 'main') {
|
|
66
|
+
const { data } = await octokit.repos.getBranch({
|
|
67
|
+
owner,
|
|
68
|
+
repo,
|
|
69
|
+
branch,
|
|
70
|
+
});
|
|
71
|
+
return data.commit.sha;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a new branch from the latest main.
|
|
76
|
+
*/
|
|
77
|
+
async function createBranch(octokit, owner, repo, branchName, baseSha) {
|
|
78
|
+
await octokit.git.createRef({
|
|
79
|
+
owner,
|
|
80
|
+
repo,
|
|
81
|
+
ref: `refs/heads/${branchName}`,
|
|
82
|
+
sha: baseSha,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create or update a file in a repository on a specific branch.
|
|
88
|
+
*/
|
|
89
|
+
async function createOrUpdateFile(octokit, owner, repo, branch, filePath, content, commitMessage) {
|
|
90
|
+
const config = getGithubConfig();
|
|
91
|
+
|
|
92
|
+
// Check if file already exists to get its SHA
|
|
93
|
+
let existingSha;
|
|
94
|
+
try {
|
|
95
|
+
const { data } = await octokit.repos.getContent({
|
|
96
|
+
owner,
|
|
97
|
+
repo,
|
|
98
|
+
path: filePath,
|
|
99
|
+
ref: branch,
|
|
100
|
+
});
|
|
101
|
+
existingSha = data.sha;
|
|
102
|
+
} catch {
|
|
103
|
+
// File does not exist — will create
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const params = {
|
|
107
|
+
owner,
|
|
108
|
+
repo,
|
|
109
|
+
path: filePath,
|
|
110
|
+
message: commitMessage,
|
|
111
|
+
content: Buffer.from(content, 'utf8').toString('base64'),
|
|
112
|
+
branch,
|
|
113
|
+
committer: {
|
|
114
|
+
name: config.username,
|
|
115
|
+
email: config.email,
|
|
116
|
+
},
|
|
117
|
+
author: {
|
|
118
|
+
name: config.username,
|
|
119
|
+
email: config.email,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
if (existingSha) params.sha = existingSha;
|
|
123
|
+
|
|
124
|
+
await octokit.repos.createOrUpdateFileContents(params);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Open a Pull Request from the user's fork to the upstream repo.
|
|
129
|
+
*/
|
|
130
|
+
async function openPullRequest(octokit, { upstreamOwner, upstreamRepo, forkOwner, branch, title, body }) {
|
|
131
|
+
const { data } = await octokit.pulls.create({
|
|
132
|
+
owner: upstreamOwner,
|
|
133
|
+
repo: upstreamRepo,
|
|
134
|
+
title,
|
|
135
|
+
body,
|
|
136
|
+
head: `${forkOwner}:${branch}`,
|
|
137
|
+
base: 'main',
|
|
138
|
+
});
|
|
139
|
+
return { number: data.number, url: data.html_url };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build a PR body with validation results and component details.
|
|
144
|
+
*/
|
|
145
|
+
function buildPrBody({ componentName, editableFields, attempts, totalInputTokens, totalOutputTokens, fingerprint }) {
|
|
146
|
+
const fieldsTable = editableFields.map((f) => `| \`${f}\` |`).join('\n');
|
|
147
|
+
|
|
148
|
+
return `## 🤖 AI-Generated Editable Component: \`Editable${componentName}\`
|
|
149
|
+
|
|
150
|
+
This component was automatically generated by **Deneb ARC AI Agent** and passed all validation checks.
|
|
151
|
+
|
|
152
|
+
### Editable Fields Discovered
|
|
153
|
+
| Field Path |
|
|
154
|
+
|---|
|
|
155
|
+
${fieldsTable}
|
|
156
|
+
|
|
157
|
+
### Validation Results
|
|
158
|
+
- ✅ AST Syntax: Passed
|
|
159
|
+
- ✅ Fivora Contract Markers: Passed
|
|
160
|
+
- ✅ TypeScript Interface: Passed
|
|
161
|
+
- ✅ Design Preservation: Intact
|
|
162
|
+
|
|
163
|
+
### Generation Stats
|
|
164
|
+
- **Model:** ${process.env.OPENAI_CONTENT_MODEL || 'gpt-4o-mini'}
|
|
165
|
+
- **Attempts:** ${attempts}/3
|
|
166
|
+
- **Tokens:** ${totalInputTokens.toLocaleString()} input / ${totalOutputTokens.toLocaleString()} output
|
|
167
|
+
- **Fingerprint:** \`${fingerprint || 'n/a'}\`
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
*Generated by Deneb ARC AI Agent — [deneb.fivora.site](https://deneb.fivora.site)*`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Full PR workflow: create branch → commit files → open PR.
|
|
175
|
+
*
|
|
176
|
+
* Opens PRs for both the core repo (component) and the ui repo (docs).
|
|
177
|
+
*
|
|
178
|
+
* @param {object} params
|
|
179
|
+
* @param {string} params.componentName - e.g. "Carousel"
|
|
180
|
+
* @param {string} params.componentCode - The validated EditableCarousel.tsx code
|
|
181
|
+
* @param {string} params.docsCode - The generated docs page code (optional)
|
|
182
|
+
* @param {string[]} params.editableFields - List of editable field paths
|
|
183
|
+
* @param {number} params.attempts - Number of AI attempts taken
|
|
184
|
+
* @param {number} params.totalInputTokens - Total tokens used
|
|
185
|
+
* @param {number} params.totalOutputTokens - Total tokens used
|
|
186
|
+
* @param {string} [params.fingerprint] - Structural fingerprint
|
|
187
|
+
* @param {boolean} [params.dryRun=false] - If true, skip actual API calls
|
|
188
|
+
* @returns {Promise<{ corePr?: { number: number, url: string }, uiPr?: { number: number, url: string }, errors: string[] }>}
|
|
189
|
+
*/
|
|
190
|
+
async function createComponentPR(params) {
|
|
191
|
+
const {
|
|
192
|
+
componentName,
|
|
193
|
+
componentCode,
|
|
194
|
+
docsCode,
|
|
195
|
+
editableFields,
|
|
196
|
+
attempts,
|
|
197
|
+
totalInputTokens,
|
|
198
|
+
totalOutputTokens,
|
|
199
|
+
fingerprint,
|
|
200
|
+
dryRun = false,
|
|
201
|
+
} = params;
|
|
202
|
+
|
|
203
|
+
const errors = [];
|
|
204
|
+
let corePr = null;
|
|
205
|
+
let uiPr = null;
|
|
206
|
+
|
|
207
|
+
if (dryRun) {
|
|
208
|
+
return {
|
|
209
|
+
corePr: { number: 0, url: '(dry-run — PR not created)' },
|
|
210
|
+
uiPr: docsCode ? { number: 0, url: '(dry-run — PR not created)' } : null,
|
|
211
|
+
errors: [],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const octokit = createOctokitClient();
|
|
216
|
+
if (!octokit) {
|
|
217
|
+
return { errors: ['GitHub client not available — check GITHUB_PAT and @octokit/rest installation.'] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const config = getGithubConfig();
|
|
221
|
+
const slugName = componentName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
222
|
+
const timestamp = Date.now().toString(36);
|
|
223
|
+
const prBody = buildPrBody({ componentName, editableFields, attempts, totalInputTokens, totalOutputTokens, fingerprint });
|
|
224
|
+
|
|
225
|
+
// ── Core Repo PR ───────────────────────────────────────────────
|
|
226
|
+
try {
|
|
227
|
+
const coreBranch = `feat/ai-editable-${slugName}-${timestamp}`;
|
|
228
|
+
const baseSha = await getLatestCommitSha(octokit, config.username, config.coreRepo);
|
|
229
|
+
await createBranch(octokit, config.username, config.coreRepo, coreBranch, baseSha);
|
|
230
|
+
|
|
231
|
+
// Commit the component file
|
|
232
|
+
await createOrUpdateFile(
|
|
233
|
+
octokit,
|
|
234
|
+
config.username,
|
|
235
|
+
config.coreRepo,
|
|
236
|
+
coreBranch,
|
|
237
|
+
`packages/deneb-ui/src/Editable${componentName}.tsx`,
|
|
238
|
+
componentCode,
|
|
239
|
+
`feat(ui): add Editable${componentName} component [AI-generated]`
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Open PR to upstream
|
|
243
|
+
corePr = await openPullRequest(octokit, {
|
|
244
|
+
upstreamOwner: config.org,
|
|
245
|
+
upstreamRepo: config.coreRepo,
|
|
246
|
+
forkOwner: config.username,
|
|
247
|
+
branch: coreBranch,
|
|
248
|
+
title: `feat(ui): add Editable${componentName} component [AI-generated]`,
|
|
249
|
+
body: prBody,
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
errors.push(`Core PR failed: ${err.message}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── UI Repo PR (docs page) ─────────────────────────────────────
|
|
256
|
+
if (docsCode) {
|
|
257
|
+
try {
|
|
258
|
+
const uiBranch = `feat/ai-docs-${slugName}-${timestamp}`;
|
|
259
|
+
const uiBaseSha = await getLatestCommitSha(octokit, config.username, config.uiRepo);
|
|
260
|
+
await createBranch(octokit, config.username, config.uiRepo, uiBranch, uiBaseSha);
|
|
261
|
+
|
|
262
|
+
// Commit the docs page
|
|
263
|
+
await createOrUpdateFile(
|
|
264
|
+
octokit,
|
|
265
|
+
config.username,
|
|
266
|
+
config.uiRepo,
|
|
267
|
+
uiBranch,
|
|
268
|
+
`src/app/docs/components/editable-${slugName}/page.tsx`,
|
|
269
|
+
docsCode,
|
|
270
|
+
`docs: add Editable${componentName} documentation [AI-generated]`
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Open PR to upstream
|
|
274
|
+
uiPr = await openPullRequest(octokit, {
|
|
275
|
+
upstreamOwner: config.org,
|
|
276
|
+
upstreamRepo: config.uiRepo,
|
|
277
|
+
forkOwner: config.username,
|
|
278
|
+
branch: uiBranch,
|
|
279
|
+
title: `docs: add Editable${componentName} documentation [AI-generated]`,
|
|
280
|
+
body: prBody,
|
|
281
|
+
});
|
|
282
|
+
} catch (err) {
|
|
283
|
+
errors.push(`UI docs PR failed: ${err.message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { corePr, uiPr, errors };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
getGithubConfig,
|
|
292
|
+
checkGithubReady,
|
|
293
|
+
createComponentPR,
|
|
294
|
+
buildPrBody,
|
|
295
|
+
};
|
package/src/arc/printer.cjs
CHANGED
|
@@ -163,6 +163,45 @@ function printRollback(reason) {
|
|
|
163
163
|
console.log(`${C.dim}${reason}${C.reset}\n`);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// ─── AI Agent Progress Messages ──────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function printAiDetected(unknownCount) {
|
|
169
|
+
console.log(`\n ${C.cyan}⚡${C.reset} ${C.bold}AI Agent:${C.reset} Detected ${unknownCount} unknown component${unknownCount > 1 ? 's' : ''}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function printAiAttempt(componentName, attempt, maxRetries) {
|
|
173
|
+
console.log(` ${C.cyan}⚡${C.reset} Adapting ${C.bold}<${componentName} />${C.reset}... attempt ${attempt}/${maxRetries}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function printAiValidationFail(errors, attempt) {
|
|
177
|
+
for (const err of errors.slice(0, 3)) {
|
|
178
|
+
console.log(` ${C.red}❌${C.reset} ${err}`);
|
|
179
|
+
}
|
|
180
|
+
if (errors.length > 3) {
|
|
181
|
+
console.log(` ${C.dim}... ${errors.length - 3} more errors${C.reset}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function printAiSuccess(componentName, attempt, fieldsCount) {
|
|
186
|
+
console.log(` ${C.green}✅${C.reset} All checks passed! (${fieldsCount} editable field${fieldsCount !== 1 ? 's' : ''})`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function printAiSkipped(componentName, reason) {
|
|
190
|
+
console.log(` ${C.yellow}⚠${C.reset} Skipped ${C.bold}<${componentName} />${C.reset}: ${reason}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function printAiPr(repoName, branchName, prNumber) {
|
|
194
|
+
if (prNumber === 0) {
|
|
195
|
+
console.log(` ${C.cyan}📦${C.reset} ${C.dim}(dry-run)${C.reset} ${repoName}: ${branchName}`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` ${C.cyan}📦${C.reset} PR opened: ${C.bold}${repoName}${C.reset} (#${prNumber})`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function printAiSummary(adapted, skipped, totalTokens) {
|
|
202
|
+
console.log(`\n ${C.cyan}⚡${C.reset} AI Agent Summary: ${C.green}${adapted} adapted${C.reset}, ${C.yellow}${skipped} skipped${C.reset}, ~${totalTokens.toLocaleString()} tokens used`);
|
|
203
|
+
}
|
|
204
|
+
|
|
166
205
|
module.exports = {
|
|
167
206
|
printBanner,
|
|
168
207
|
printProfile,
|
|
@@ -177,6 +216,13 @@ module.exports = {
|
|
|
177
216
|
printUncoveredText,
|
|
178
217
|
printDeveloperNextSteps,
|
|
179
218
|
printRollback,
|
|
219
|
+
printAiDetected,
|
|
220
|
+
printAiAttempt,
|
|
221
|
+
printAiValidationFail,
|
|
222
|
+
printAiSuccess,
|
|
223
|
+
printAiSkipped,
|
|
224
|
+
printAiPr,
|
|
225
|
+
printAiSummary,
|
|
180
226
|
ok,
|
|
181
227
|
warn,
|
|
182
228
|
info,
|
package/src/arc/version.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const ARC_NAME = 'Deneb ARC';
|
|
4
|
-
const ARC_FULL_NAME = 'Deneb Adaptive Refactoring Compiler';
|
|
5
|
-
const ARC_VERSION = '
|
|
4
|
+
const ARC_FULL_NAME = 'Deneb Adaptive Refactoring Compiler — AI-Augmented';
|
|
5
|
+
const ARC_VERSION = '2.0.0';
|
|
6
6
|
const SCHEMA_VERSION = 2;
|
|
7
7
|
const ENGINE_ID = 'deneb-arc';
|
|
8
8
|
|