draply-dev 1.5.4 → 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 +238 -110
- package/package.json +27 -27
- package/src/overlay.js +1258 -1097
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,16 +210,17 @@ 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
|
});
|
|
113
217
|
}
|
|
114
218
|
|
|
115
|
-
// OpenAI / Groq — compatible API
|
|
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'],
|
|
223
|
+
gemini: ['generativelanguage.googleapis.com', '/v1beta/openai/chat/completions', 'gemini-2.5-flash']
|
|
119
224
|
};
|
|
120
225
|
const [hostname, apiPath, defaultModel] = hosts[provider] || hosts.groq;
|
|
121
226
|
const body = JSON.stringify({ model: cfg.model || defaultModel, messages: [{ role: 'user', content: prompt }], temperature: 0.1, max_tokens: 8192 });
|
|
@@ -125,7 +230,7 @@ async function callAI(cfg, prompt) {
|
|
|
125
230
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Length': Buffer.byteLength(body) }
|
|
126
231
|
}, r => {
|
|
127
232
|
const ch = []; r.on('data', c => ch.push(c));
|
|
128
|
-
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')); } });
|
|
129
234
|
});
|
|
130
235
|
req.on('error', reject); req.write(body); req.end();
|
|
131
236
|
});
|
|
@@ -154,7 +259,7 @@ const server = http.createServer((req, res) => {
|
|
|
154
259
|
const { name, base64 } = JSON.parse(body);
|
|
155
260
|
const assetsDir = path.join(process.cwd(), 'assets_draply');
|
|
156
261
|
if (!fs.existsSync(assetsDir)) fs.mkdirSync(assetsDir);
|
|
157
|
-
|
|
262
|
+
|
|
158
263
|
const ext = path.extname(name) || '.png';
|
|
159
264
|
const base = path.basename(name, ext);
|
|
160
265
|
let finalName = name;
|
|
@@ -163,7 +268,7 @@ const server = http.createServer((req, res) => {
|
|
|
163
268
|
finalName = `${base}-${counter}${ext}`;
|
|
164
269
|
counter++;
|
|
165
270
|
}
|
|
166
|
-
|
|
271
|
+
|
|
167
272
|
const data = base64.replace(/^data:image\/\w+;base64,/, '');
|
|
168
273
|
fs.writeFileSync(path.join(assetsDir, finalName), data, 'base64');
|
|
169
274
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -240,7 +345,7 @@ const server = http.createServer((req, res) => {
|
|
|
240
345
|
try {
|
|
241
346
|
const { changes } = JSON.parse(body);
|
|
242
347
|
const cfg = loadConfig();
|
|
243
|
-
|
|
348
|
+
|
|
244
349
|
// If no config, fallback to saving standard CSS
|
|
245
350
|
// Always write CSS backup (#13)
|
|
246
351
|
const cssLines = [];
|
|
@@ -258,9 +363,9 @@ const server = http.createServer((req, res) => {
|
|
|
258
363
|
}
|
|
259
364
|
|
|
260
365
|
if (!cfg.apiKey) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
366
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
367
|
+
res.end(JSON.stringify({ ok: true, fallback: true }));
|
|
368
|
+
return;
|
|
264
369
|
}
|
|
265
370
|
|
|
266
371
|
const results = [];
|
|
@@ -292,13 +397,13 @@ const server = http.createServer((req, res) => {
|
|
|
292
397
|
for (const ch of (changes || [])) {
|
|
293
398
|
let targetFile = ch.exactFile;
|
|
294
399
|
if (!targetFile || !fs.existsSync(targetFile)) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
}
|
|
302
407
|
}
|
|
303
408
|
if (!fileMap.has(targetFile)) {
|
|
304
409
|
fileMap.set(targetFile, { content: fs.readFileSync(targetFile, 'utf8'), items: [] });
|
|
@@ -308,47 +413,47 @@ const server = http.createServer((req, res) => {
|
|
|
308
413
|
|
|
309
414
|
// 2. Call AI per file using XML Patches
|
|
310
415
|
for (const [filePath, { content: rawContent, items }] of fileMap) {
|
|
311
|
-
|
|
416
|
+
|
|
312
417
|
let changesBlock = '';
|
|
313
418
|
const content = rawContent.replace(/\r\n/g, '\n');
|
|
314
419
|
const lines = content.split('\n');
|
|
315
420
|
let classNamesToFind = items.map(c => {
|
|
316
|
-
|
|
317
|
-
|
|
421
|
+
const parts = c.selector.split('.');
|
|
422
|
+
return parts.length > 1 ? parts.pop() : '';
|
|
318
423
|
}).filter(Boolean);
|
|
319
424
|
let idsToFind = items.map(c => {
|
|
320
|
-
|
|
321
|
-
|
|
425
|
+
const parts = c.selector.split('#');
|
|
426
|
+
return parts.length > 1 ? parts.pop() : '';
|
|
322
427
|
}).filter(Boolean);
|
|
323
428
|
|
|
324
429
|
let ctxStart = -1, ctxEnd = -1;
|
|
325
430
|
for (let i = 0; i < lines.length; i++) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
}
|
|
334
439
|
}
|
|
335
440
|
if (ctxStart === -1 && items.some(c => c.type === 'create')) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
448
|
}
|
|
344
449
|
if (ctxStart === -1) { ctxStart = 0; ctxEnd = Math.min(lines.length - 1, 300); }
|
|
345
|
-
|
|
450
|
+
|
|
346
451
|
const snippet = lines.slice(ctxStart, ctxEnd + 1).join('\n');
|
|
347
452
|
|
|
348
453
|
items.forEach(item => {
|
|
349
454
|
const propsStr = Object.entries(item.props).map(([k, v]) => ` ${k}: ${v}`).join('\n');
|
|
350
455
|
changesBlock += `\nTarget Selector: ${item.selector}\nType: ${item.type}\nChanges:\n${propsStr}\n`;
|
|
351
|
-
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`;
|
|
352
457
|
});
|
|
353
458
|
|
|
354
459
|
const prompt = `You are a strict code editor applying style changes to a file.
|
|
@@ -369,7 +474,7 @@ Rules:
|
|
|
369
474
|
- REPLACE old style values if they exist, DO NOT duplicate keys!
|
|
370
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.
|
|
371
476
|
- If 'src' is provided in Changes, update the src attribute of the target HTML element!
|
|
372
|
-
- 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.
|
|
373
478
|
|
|
374
479
|
|
|
375
480
|
Example response:
|
|
@@ -386,68 +491,91 @@ Return ONLY the patch blocks.`;
|
|
|
386
491
|
|
|
387
492
|
let apiResult = '';
|
|
388
493
|
try {
|
|
389
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
444
556
|
}
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
}
|
|
447
573
|
}
|
|
574
|
+
items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'AI patch failed to match' }));
|
|
575
|
+
}
|
|
448
576
|
} catch (e) {
|
|
449
|
-
|
|
450
|
-
|
|
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 }));
|
|
451
579
|
}
|
|
452
580
|
}
|
|
453
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
|
+
}
|