draply-dev 1.5.5 → 1.5.6
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/cli.js +236 -109
- package/package.json +27 -27
- package/src/overlay.js +1257 -1150
package/bin/cli.js
CHANGED
|
@@ -13,13 +13,16 @@ const pkg = require('../package.json');
|
|
|
13
13
|
if (args.includes('--help') || args.includes('-h')) {
|
|
14
14
|
console.log(`\n \x1b[36mDraply v${pkg.version}\x1b[0m — Visual overlay for any frontend project\n`);
|
|
15
15
|
console.log(` Usage: npx draply <target-port> [proxy-port] [target-host]\n`);
|
|
16
|
+
console.log(` Commands:`);
|
|
17
|
+
console.log(` login Authenticate with Draply to unlock features`);
|
|
18
|
+
console.log(` logout Log out of Draply`);
|
|
16
19
|
console.log(` Arguments:`);
|
|
17
20
|
console.log(` target-port Port your dev server runs on (default: 3000)`);
|
|
18
21
|
console.log(` proxy-port Port for Draply proxy (default: 4000)`);
|
|
19
22
|
console.log(` target-host Hostname of dev server (default: localhost)\n`);
|
|
20
23
|
console.log(` Examples:`);
|
|
21
24
|
console.log(` npx draply 3000 Proxy localhost:3000 → localhost:4000`);
|
|
22
|
-
console.log(` npx draply
|
|
25
|
+
console.log(` npx draply login Log in via browser\n`);
|
|
23
26
|
console.log(` Options:`);
|
|
24
27
|
console.log(` --help, -h Show this help`);
|
|
25
28
|
console.log(` --version, -v Show version\n`);
|
|
@@ -30,6 +33,98 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
30
33
|
process.exit(0);
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
// Config — stored in ~/.draply/ to avoid accidental git commits (#19)
|
|
37
|
+
const configDir = path.join(require('os').homedir(), '.draply');
|
|
38
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
39
|
+
const configPath = path.join(configDir, 'config.json');
|
|
40
|
+
function loadConfig() { try { return JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { return {}; } }
|
|
41
|
+
function saveConfig(cfg) { fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf8'); }
|
|
42
|
+
|
|
43
|
+
// ── Auth Commands ───────────────────────────────────────────────────────────
|
|
44
|
+
if (args[0] === 'login') {
|
|
45
|
+
const url = require('url');
|
|
46
|
+
const { exec } = require('child_process');
|
|
47
|
+
const BACKEND_URL = 'https://draply-backend.onrender.com';
|
|
48
|
+
const PORT = 8080;
|
|
49
|
+
|
|
50
|
+
console.log('\n \x1b[36m●\x1b[0m Инициализация входа в Draply...');
|
|
51
|
+
|
|
52
|
+
const loginServer = http.createServer((req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const reqUrl = url.parse(req.url, true);
|
|
55
|
+
if (reqUrl.pathname === '/callback') {
|
|
56
|
+
const token = reqUrl.query.token;
|
|
57
|
+
if (token) {
|
|
58
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
59
|
+
res.end(`
|
|
60
|
+
<html>
|
|
61
|
+
<body style="background: #050505; color: white; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
|
|
62
|
+
<div style="text-align: center;">
|
|
63
|
+
<h1 style="color: #22c55e;">Login Successful!</h1>
|
|
64
|
+
<p style="opacity: 0.7;">You can now close this window and return to your terminal.</p>
|
|
65
|
+
</div>
|
|
66
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
const cfg = loadConfig();
|
|
72
|
+
cfg.draplyToken = token;
|
|
73
|
+
saveConfig(cfg);
|
|
74
|
+
|
|
75
|
+
console.log('\n \x1b[32m✓\x1b[0m Успешная авторизация! Токен сохранен.');
|
|
76
|
+
console.log(' \x1b[90mТеперь твой аккаунт привязан к CLI.\x1b[0m\n');
|
|
77
|
+
process.exit(0);
|
|
78
|
+
} else {
|
|
79
|
+
res.writeHead(400); res.end('Auth failed');
|
|
80
|
+
console.log('\n \x1b[31m✖\x1b[0m Ошибка: Токен не получен.\n');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
res.writeHead(404); res.end();
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(err);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
loginServer.listen(PORT, () => {
|
|
93
|
+
const redirectUri = encodeURIComponent(`http://localhost:${PORT}/callback`);
|
|
94
|
+
const authUrl = `${BACKEND_URL}/auth/github?redirect_uri=${redirectUri}`;
|
|
95
|
+
|
|
96
|
+
console.log(` \x1b[90mОткрываем браузер для авторизации...\x1b[0m`);
|
|
97
|
+
let command;
|
|
98
|
+
switch (process.platform) {
|
|
99
|
+
case 'darwin': command = `open "${authUrl}"`; break;
|
|
100
|
+
case 'win32': command = `start "" "${authUrl}"`; break;
|
|
101
|
+
default: command = `xdg-open "${authUrl}"`; break;
|
|
102
|
+
}
|
|
103
|
+
exec(command, (err) => {
|
|
104
|
+
if (err) console.log(`\n \x1b[33m⚠\x1b[0m Не удалось автоматически открыть браузер. Перейди по ссылке вручную:\n ${authUrl}\n`);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
loginServer.on('error', (e) => {
|
|
109
|
+
if (e.code === 'EADDRINUSE') {
|
|
110
|
+
console.log(`\n \x1b[31m✖\x1b[0m Ошибка: Порт ${PORT} занят. Освободи его или закрой другие программы.\n`);
|
|
111
|
+
} else {
|
|
112
|
+
console.error(e);
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return; // Stop execution here, don't start the proxy
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (args[0] === 'logout') {
|
|
121
|
+
const cfg = loadConfig();
|
|
122
|
+
delete cfg.draplyToken;
|
|
123
|
+
saveConfig(cfg);
|
|
124
|
+
console.log('\n \x1b[32m✓\x1b[0m Ты успешно вышел из аккаунта Draply.\n');
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
33
128
|
const targetPort = parseInt(args.filter(a => !a.startsWith('-'))[0]) || 3000;
|
|
34
129
|
const proxyPort = parseInt(args.filter(a => !a.startsWith('-'))[1]) || 4000;
|
|
35
130
|
const targetHost = args.filter(a => !a.startsWith('-'))[2] || 'localhost';
|
|
@@ -38,10 +133,24 @@ const MARKER = 'data-ps-done="1"';
|
|
|
38
133
|
|
|
39
134
|
function injectOverlay(html) {
|
|
40
135
|
if (html.includes(MARKER)) return html;
|
|
41
|
-
|
|
136
|
+
|
|
137
|
+
let isPro = false;
|
|
138
|
+
const cfg = loadConfig();
|
|
139
|
+
if (cfg.draplyToken) {
|
|
140
|
+
try {
|
|
141
|
+
const payloadBase64Url = cfg.draplyToken.split('.')[1];
|
|
142
|
+
if (payloadBase64Url) {
|
|
143
|
+
const payloadBase64 = payloadBase64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
144
|
+
const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf8');
|
|
145
|
+
const payload = JSON.parse(payloadJson);
|
|
146
|
+
isPro = payload.isPro === true;
|
|
147
|
+
}
|
|
148
|
+
} catch(e) {}
|
|
149
|
+
}
|
|
150
|
+
|
|
42
151
|
// Read overlay.js per request to allow hot-reloading during development
|
|
43
152
|
const OVERLAY_JS = fs.readFileSync(path.join(__dirname, '../src/overlay.js'), 'utf8');
|
|
44
|
-
const INJECT_TAG = `\n<link rel="stylesheet" href="/draply.css">\n<script ${MARKER}>\n${OVERLAY_JS}\n</script>`;
|
|
153
|
+
const INJECT_TAG = `\n<link rel="stylesheet" href="/draply.css">\n<script>window.__DRAPLY_PRO__ = ${isPro};</script>\n<script ${MARKER}>\n${OVERLAY_JS}\n</script>`;
|
|
45
154
|
|
|
46
155
|
if (html.includes('</body>')) return html.replace(/<\/body>/i, () => INJECT_TAG + '\n</body>');
|
|
47
156
|
if (html.includes('</html>')) return html.replace(/<\/html>/i, () => INJECT_TAG + '\n</html>');
|
|
@@ -66,12 +175,7 @@ if (!fs.existsSync(overridesPath)) {
|
|
|
66
175
|
fs.writeFileSync(overridesPath, '/* draply */\n', 'utf8');
|
|
67
176
|
}
|
|
68
177
|
|
|
69
|
-
// Config
|
|
70
|
-
const configDir = path.join(require('os').homedir(), '.draply');
|
|
71
|
-
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
72
|
-
const configPath = path.join(configDir, 'config.json');
|
|
73
|
-
function loadConfig() { try { return JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { return {}; } }
|
|
74
|
-
function saveConfig(cfg) { fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf8'); }
|
|
178
|
+
// Config functions moved to the top of the file
|
|
75
179
|
|
|
76
180
|
// Multi-provider AI call (#10)
|
|
77
181
|
async function callAI(cfg, prompt) {
|
|
@@ -81,7 +185,7 @@ async function callAI(cfg, prompt) {
|
|
|
81
185
|
// Anthropic — different API format
|
|
82
186
|
if (provider === 'anthropic') {
|
|
83
187
|
const body = JSON.stringify({
|
|
84
|
-
model: cfg.model || 'claude-sonnet-
|
|
188
|
+
model: cfg.model || 'claude-3-7-sonnet-20250219', max_tokens: 8192,
|
|
85
189
|
messages: [{ role: 'user', content: prompt }]
|
|
86
190
|
});
|
|
87
191
|
return new Promise((resolve, reject) => {
|
|
@@ -90,7 +194,7 @@ async function callAI(cfg, prompt) {
|
|
|
90
194
|
headers: { 'Content-Type': 'application/json', 'x-api-key': cfg.apiKey, 'anthropic-version': '2023-06-01', 'Content-Length': Buffer.byteLength(body) }
|
|
91
195
|
}, r => {
|
|
92
196
|
const ch = []; r.on('data', c => ch.push(c));
|
|
93
|
-
r.on('end', () => { try { const d = JSON.parse(Buffer.concat(ch).toString()); d.error ? reject(new Error(d.error.message)) : resolve(d.content?.[0]?.text || ''); } catch(e) { reject(new Error('Invalid Anthropic response')); } });
|
|
197
|
+
r.on('end', () => { try { const d = JSON.parse(Buffer.concat(ch).toString()); d.error ? reject(new Error(d.error.message)) : resolve(d.content?.[0]?.text || ''); } catch (e) { reject(new Error('Invalid Anthropic response')); } });
|
|
94
198
|
});
|
|
95
199
|
req.on('error', reject); req.write(body); req.end();
|
|
96
200
|
});
|
|
@@ -106,7 +210,7 @@ async function callAI(cfg, prompt) {
|
|
|
106
210
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
|
|
107
211
|
}, r => {
|
|
108
212
|
const ch = []; r.on('data', c => ch.push(c));
|
|
109
|
-
r.on('end', () => { try { const d = JSON.parse(Buffer.concat(ch).toString()); resolve(d.message?.content || ''); } catch(e) { reject(new Error('Invalid Ollama response')); } });
|
|
213
|
+
r.on('end', () => { try { const d = JSON.parse(Buffer.concat(ch).toString()); resolve(d.message?.content || ''); } catch (e) { reject(new Error('Invalid Ollama response')); } });
|
|
110
214
|
});
|
|
111
215
|
req.on('error', reject); req.write(body); req.end();
|
|
112
216
|
});
|
|
@@ -115,7 +219,7 @@ async function callAI(cfg, prompt) {
|
|
|
115
219
|
// OpenAI / Groq / Gemini — compatible API
|
|
116
220
|
const hosts = {
|
|
117
221
|
openai: ['api.openai.com', '/v1/chat/completions', 'gpt-4o-mini'],
|
|
118
|
-
groq:
|
|
222
|
+
groq: ['api.groq.com', '/openai/v1/chat/completions', 'llama-3.3-70b-versatile'],
|
|
119
223
|
gemini: ['generativelanguage.googleapis.com', '/v1beta/openai/chat/completions', 'gemini-2.5-flash']
|
|
120
224
|
};
|
|
121
225
|
const [hostname, apiPath, defaultModel] = hosts[provider] || hosts.groq;
|
|
@@ -126,7 +230,7 @@ async function callAI(cfg, prompt) {
|
|
|
126
230
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Length': Buffer.byteLength(body) }
|
|
127
231
|
}, r => {
|
|
128
232
|
const ch = []; r.on('data', c => ch.push(c));
|
|
129
|
-
r.on('end', () => { try { const d = JSON.parse(Buffer.concat(ch).toString()); d.error ? reject(new Error(d.error?.message || JSON.stringify(d.error))) : resolve(d.choices?.[0]?.message?.content || ''); } catch(e) { reject(new Error('Invalid API response')); } });
|
|
233
|
+
r.on('end', () => { try { const d = JSON.parse(Buffer.concat(ch).toString()); d.error ? reject(new Error(d.error?.message || JSON.stringify(d.error))) : resolve(d.choices?.[0]?.message?.content || ''); } catch (e) { reject(new Error('Invalid API response')); } });
|
|
130
234
|
});
|
|
131
235
|
req.on('error', reject); req.write(body); req.end();
|
|
132
236
|
});
|
|
@@ -155,7 +259,7 @@ const server = http.createServer((req, res) => {
|
|
|
155
259
|
const { name, base64 } = JSON.parse(body);
|
|
156
260
|
const assetsDir = path.join(process.cwd(), 'assets_draply');
|
|
157
261
|
if (!fs.existsSync(assetsDir)) fs.mkdirSync(assetsDir);
|
|
158
|
-
|
|
262
|
+
|
|
159
263
|
const ext = path.extname(name) || '.png';
|
|
160
264
|
const base = path.basename(name, ext);
|
|
161
265
|
let finalName = name;
|
|
@@ -164,7 +268,7 @@ const server = http.createServer((req, res) => {
|
|
|
164
268
|
finalName = `${base}-${counter}${ext}`;
|
|
165
269
|
counter++;
|
|
166
270
|
}
|
|
167
|
-
|
|
271
|
+
|
|
168
272
|
const data = base64.replace(/^data:image\/\w+;base64,/, '');
|
|
169
273
|
fs.writeFileSync(path.join(assetsDir, finalName), data, 'base64');
|
|
170
274
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -241,7 +345,7 @@ const server = http.createServer((req, res) => {
|
|
|
241
345
|
try {
|
|
242
346
|
const { changes } = JSON.parse(body);
|
|
243
347
|
const cfg = loadConfig();
|
|
244
|
-
|
|
348
|
+
|
|
245
349
|
// If no config, fallback to saving standard CSS
|
|
246
350
|
// Always write CSS backup (#13)
|
|
247
351
|
const cssLines = [];
|
|
@@ -259,9 +363,9 @@ const server = http.createServer((req, res) => {
|
|
|
259
363
|
}
|
|
260
364
|
|
|
261
365
|
if (!cfg.apiKey) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
366
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
367
|
+
res.end(JSON.stringify({ ok: true, fallback: true }));
|
|
368
|
+
return;
|
|
265
369
|
}
|
|
266
370
|
|
|
267
371
|
const results = [];
|
|
@@ -293,13 +397,13 @@ const server = http.createServer((req, res) => {
|
|
|
293
397
|
for (const ch of (changes || [])) {
|
|
294
398
|
let targetFile = ch.exactFile;
|
|
295
399
|
if (!targetFile || !fs.existsSync(targetFile)) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
400
|
+
const fallbackHTML = path.join(process.cwd(), 'index.html');
|
|
401
|
+
if (fs.existsSync(fallbackHTML)) {
|
|
402
|
+
targetFile = fallbackHTML;
|
|
403
|
+
} else {
|
|
404
|
+
results.push({ selector: ch.selector, ok: false, reason: 'Source file not linked' });
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
303
407
|
}
|
|
304
408
|
if (!fileMap.has(targetFile)) {
|
|
305
409
|
fileMap.set(targetFile, { content: fs.readFileSync(targetFile, 'utf8'), items: [] });
|
|
@@ -309,47 +413,47 @@ const server = http.createServer((req, res) => {
|
|
|
309
413
|
|
|
310
414
|
// 2. Call AI per file using XML Patches
|
|
311
415
|
for (const [filePath, { content: rawContent, items }] of fileMap) {
|
|
312
|
-
|
|
416
|
+
|
|
313
417
|
let changesBlock = '';
|
|
314
418
|
const content = rawContent.replace(/\r\n/g, '\n');
|
|
315
419
|
const lines = content.split('\n');
|
|
316
420
|
let classNamesToFind = items.map(c => {
|
|
317
|
-
|
|
318
|
-
|
|
421
|
+
const parts = c.selector.split('.');
|
|
422
|
+
return parts.length > 1 ? parts.pop() : '';
|
|
319
423
|
}).filter(Boolean);
|
|
320
424
|
let idsToFind = items.map(c => {
|
|
321
|
-
|
|
322
|
-
|
|
425
|
+
const parts = c.selector.split('#');
|
|
426
|
+
return parts.length > 1 ? parts.pop() : '';
|
|
323
427
|
}).filter(Boolean);
|
|
324
428
|
|
|
325
429
|
let ctxStart = -1, ctxEnd = -1;
|
|
326
430
|
for (let i = 0; i < lines.length; i++) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
431
|
+
const hasClass = classNamesToFind.some(cls => lines[i].includes(cls));
|
|
432
|
+
const hasId = idsToFind.some(id => lines[i].includes(id));
|
|
433
|
+
if (hasClass || hasId) {
|
|
434
|
+
const s = Math.max(0, i - 60);
|
|
435
|
+
const e = Math.min(lines.length - 1, i + 60);
|
|
436
|
+
if (ctxStart === -1 || s < ctxStart) ctxStart = s;
|
|
437
|
+
if (ctxEnd === -1 || e > ctxEnd) ctxEnd = e;
|
|
438
|
+
}
|
|
335
439
|
}
|
|
336
440
|
if (ctxStart === -1 && items.some(c => c.type === 'create')) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
441
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
442
|
+
if (lines[i].includes('</body>')) {
|
|
443
|
+
ctxStart = Math.max(0, i - 20);
|
|
444
|
+
ctxEnd = Math.min(lines.length - 1, i + 20);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
344
448
|
}
|
|
345
449
|
if (ctxStart === -1) { ctxStart = 0; ctxEnd = Math.min(lines.length - 1, 300); }
|
|
346
|
-
|
|
450
|
+
|
|
347
451
|
const snippet = lines.slice(ctxStart, ctxEnd + 1).join('\n');
|
|
348
452
|
|
|
349
453
|
items.forEach(item => {
|
|
350
454
|
const propsStr = Object.entries(item.props).map(([k, v]) => ` ${k}: ${v}`).join('\n');
|
|
351
455
|
changesBlock += `\nTarget Selector: ${item.selector}\nType: ${item.type}\nChanges:\n${propsStr}\n`;
|
|
352
|
-
if (item.type === 'create') changesBlock += `HTML to Insert: ${item.outerHTML}\n`;
|
|
456
|
+
if (item.type === 'create') changesBlock += `HTML to Insert: ${item.outerHTML}\nTarget Parent: ${item.parentSelector || 'body'}\n`;
|
|
353
457
|
});
|
|
354
458
|
|
|
355
459
|
const prompt = `You are a strict code editor applying style changes to a file.
|
|
@@ -370,7 +474,7 @@ Rules:
|
|
|
370
474
|
- REPLACE old style values if they exist, DO NOT duplicate keys!
|
|
371
475
|
- If 'innerText' or 'innerHTML' is provided in Changes, update the text content inside the target HTML element! Use ONLY the exact text provided in the request (preserving internal HTML tags like spans for colors/styles if present). DO NOT add signatures, names, attributions, or "complete" the text based on context.
|
|
372
476
|
- If 'src' is provided in Changes, update the src attribute of the target HTML element!
|
|
373
|
-
- If a change is marked for a new element (type: create)
|
|
477
|
+
- If a change is marked for a new element (type: create), INSERT the provided 'outerHTML' inside the specified 'Target Parent' element. If parent is 'body' or not found, insert before the closing </body> tag.
|
|
374
478
|
|
|
375
479
|
|
|
376
480
|
Example response:
|
|
@@ -387,68 +491,91 @@ Return ONLY the patch blocks.`;
|
|
|
387
491
|
|
|
388
492
|
let apiResult = '';
|
|
389
493
|
try {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
494
|
+
// AI call — supports Groq/OpenAI/Anthropic/Ollama (#10)
|
|
495
|
+
apiResult = await callAI(cfg, prompt);
|
|
496
|
+
|
|
497
|
+
const patches = [];
|
|
498
|
+
const patchRegex = /<search>([\s\S]*?)<\/search>[\s\S]*?<replace>([\s\S]*?)<\/replace>/g;
|
|
499
|
+
let match;
|
|
500
|
+
while ((match = patchRegex.exec(apiResult)) !== null) {
|
|
501
|
+
let s = match[1], r = match[2];
|
|
502
|
+
s = s.replace(/^\n+/, '').replace(/\n+$/, '').replace(/\r/g, '');
|
|
503
|
+
r = r.replace(/^\n+/, '').replace(/\n+$/, '').replace(/\r/g, '');
|
|
504
|
+
if (s.trim()) patches.push({ search: s, replace: r });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let newContent = content;
|
|
508
|
+
let applied = 0;
|
|
509
|
+
for (const patch of patches) {
|
|
510
|
+
if (newContent.includes(patch.search)) {
|
|
511
|
+
newContent = newContent.replace(patch.search, patch.replace);
|
|
512
|
+
applied++;
|
|
513
|
+
} else {
|
|
514
|
+
// Fallback to whitespace-agnostic matching
|
|
515
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
516
|
+
const looseRegexStr = escapeRegExp(patch.search.trim()).replace(/\s+/g, '\\s+');
|
|
517
|
+
const looseRegex = new RegExp(looseRegexStr);
|
|
518
|
+
if (looseRegex.test(newContent)) {
|
|
519
|
+
newContent = newContent.replace(looseRegex, patch.replace);
|
|
520
|
+
applied++;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (applied > 0) {
|
|
526
|
+
// Restore original line endings if they were \r\n
|
|
527
|
+
const finalContent = rawContent.includes('\r\n') ? newContent.replace(/\n/g, '\r\n') : newContent;
|
|
528
|
+
fs.writeFileSync(filePath, finalContent, 'utf8');
|
|
529
|
+
console.log(` \x1b[32m✓\x1b[0m Modified ${path.basename(filePath)} (${applied} patch(es))`);
|
|
530
|
+
items.forEach(item => results.push({ selector: item.selector, ok: true }));
|
|
531
|
+
} else {
|
|
532
|
+
console.warn(` \x1b[33m⚠\x1b[0m AI matched 0 patches in ${path.basename(filePath)}`);
|
|
533
|
+
console.log("Full AI response context for debugging:");
|
|
534
|
+
console.log(apiResult);
|
|
535
|
+
|
|
536
|
+
// Forced fallback for creation if AI failed match but we have a creation request
|
|
537
|
+
const creationItems = items.filter(it => it.type === 'create');
|
|
538
|
+
if (creationItems.length > 0) {
|
|
539
|
+
console.log(`Attempting forced creation fallback for ${creationItems.length} element(s)...`);
|
|
540
|
+
let workingContent = content;
|
|
541
|
+
let insertedCount = 0;
|
|
542
|
+
for (const ci of creationItems) {
|
|
543
|
+
let insertIdx = -1;
|
|
544
|
+
// Try to find parent container first
|
|
545
|
+
if (ci.parentSelector && ci.parentSelector !== 'body') {
|
|
546
|
+
const parts = ci.parentSelector.split(/(?=[.#])/).filter(Boolean);
|
|
547
|
+
const last = parts[parts.length - 1];
|
|
548
|
+
if (last) {
|
|
549
|
+
const needle = last.startsWith('#') || last.startsWith('.') ? last.substring(1) : last;
|
|
550
|
+
const tagMatch = workingContent.indexOf(needle);
|
|
551
|
+
if (tagMatch >= 0) {
|
|
552
|
+
// Find the closing > of the opening tag
|
|
553
|
+
const closeAngle = workingContent.indexOf('>', tagMatch);
|
|
554
|
+
if (closeAngle >= 0) insertIdx = closeAngle + 1;
|
|
555
|
+
}
|
|
445
556
|
}
|
|
446
|
-
|
|
447
|
-
|
|
557
|
+
}
|
|
558
|
+
if (insertIdx === -1) {
|
|
559
|
+
insertIdx = workingContent.toLowerCase().lastIndexOf('</body>');
|
|
560
|
+
}
|
|
561
|
+
if (insertIdx >= 0) {
|
|
562
|
+
workingContent = workingContent.slice(0, insertIdx) + '\n' + ci.outerHTML + workingContent.slice(insertIdx);
|
|
563
|
+
insertedCount++;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (insertedCount > 0) {
|
|
567
|
+
const final = rawContent.includes('\r\n') ? workingContent.replace(/\n/g, '\r\n') : workingContent;
|
|
568
|
+
fs.writeFileSync(filePath, final, 'utf8');
|
|
569
|
+
console.log(` \x1b[32m✓\x1b[0m Forced creation applied (${insertedCount} elements) to ${path.basename(filePath)}`);
|
|
570
|
+
items.forEach(item => results.push({ selector: item.selector, ok: true }));
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
448
573
|
}
|
|
574
|
+
items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'AI patch failed to match' }));
|
|
575
|
+
}
|
|
449
576
|
} catch (e) {
|
|
450
|
-
|
|
451
|
-
|
|
577
|
+
console.error(` \x1b[31m✖\x1b[0m AI Error for ${path.basename(filePath)}:`, e.message);
|
|
578
|
+
items.forEach(item => results.push({ selector: item.selector, ok: false, reason: e.message }));
|
|
452
579
|
}
|
|
453
580
|
}
|
|
454
581
|
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "draply-dev",
|
|
3
|
+
"version": "1.5.6",
|
|
4
|
+
"description": "Visual overlay for any frontend project — move, resize, restyle live in the browser, save to CSS",
|
|
5
|
+
"author": "Arman",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"draply": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"css",
|
|
16
|
+
"visual-editor",
|
|
17
|
+
"frontend",
|
|
18
|
+
"overlay",
|
|
19
|
+
"design-tool",
|
|
20
|
+
"no-code",
|
|
21
|
+
"drag-and-drop"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": "\u003e=16.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|