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.
Files changed (3) hide show
  1. package/bin/cli.js +236 -109
  2. package/package.json +27 -27
  3. 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 5500 4001 Proxy localhost:5500 → localhost:4001\n`);
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 stored in ~/.draply/ to avoid accidental git commits (#19)
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-4-20250514', max_tokens: 8192,
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: ['api.groq.com', '/openai/v1/chat/completions', 'llama-3.3-70b-versatile'],
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
- res.writeHead(200, { 'Content-Type': 'application/json' });
263
- res.end(JSON.stringify({ ok: true, fallback: true }));
264
- return;
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
- const fallbackHTML = path.join(process.cwd(), 'index.html');
297
- if (fs.existsSync(fallbackHTML)) {
298
- targetFile = fallbackHTML;
299
- } else {
300
- results.push({ selector: ch.selector, ok: false, reason: 'Source file not linked' });
301
- continue;
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
- const parts = c.selector.split('.');
318
- return parts.length > 1 ? parts.pop() : '';
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
- const parts = c.selector.split('#');
322
- return parts.length > 1 ? parts.pop() : '';
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
- const hasClass = classNamesToFind.some(cls => lines[i].includes(cls));
328
- const hasId = idsToFind.some(id => lines[i].includes(id));
329
- if (hasClass || hasId) {
330
- const s = Math.max(0, i - 60);
331
- const e = Math.min(lines.length - 1, i + 60);
332
- if (ctxStart === -1 || s < ctxStart) ctxStart = s;
333
- if (ctxEnd === -1 || e > ctxEnd) ctxEnd = e;
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
- for (let i = lines.length - 1; i >= 0; i--) {
338
- if (lines[i].includes('</body>')) {
339
- ctxStart = Math.max(0, i - 20);
340
- ctxEnd = Math.min(lines.length - 1, i + 20);
341
- break;
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) and you cannot find the target selector, INSERT the provided 'outerHTML' before the closing </body> tag or in an appropriate container.
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
- // AI call — supports Groq/OpenAI/Anthropic/Ollama (#10)
391
- apiResult = await callAI(cfg, prompt);
392
-
393
- const patches = [];
394
- const patchRegex = /<search>([\s\S]*?)<\/search>[\s\S]*?<replace>([\s\S]*?)<\/replace>/g;
395
- let match;
396
- while ((match = patchRegex.exec(apiResult)) !== null) {
397
- let s = match[1], r = match[2];
398
- s = s.replace(/^\n+/, '').replace(/\n+$/, '').replace(/\r/g, '');
399
- r = r.replace(/^\n+/, '').replace(/\n+$/, '').replace(/\r/g, '');
400
- if (s.trim()) patches.push({ search: s, replace: r });
401
- }
402
-
403
- let newContent = content;
404
- let applied = 0;
405
- for (const patch of patches) {
406
- if (newContent.includes(patch.search)) {
407
- newContent = newContent.replace(patch.search, patch.replace);
408
- applied++;
409
- } else {
410
- // Fallback to whitespace-agnostic matching
411
- const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
412
- const looseRegexStr = escapeRegExp(patch.search.trim()).replace(/\s+/g, '\\s+');
413
- const looseRegex = new RegExp(looseRegexStr);
414
- if (looseRegex.test(newContent)) {
415
- newContent = newContent.replace(looseRegex, patch.replace);
416
- applied++;
417
- }
418
- }
419
- }
420
-
421
- if (applied > 0) {
422
- // Restore original line endings if they were \r\n
423
- const finalContent = rawContent.includes('\r\n') ? newContent.replace(/\n/g, '\r\n') : newContent;
424
- fs.writeFileSync(filePath, finalContent, 'utf8');
425
- console.log(` \x1b[32m✓\x1b[0m Modified ${path.basename(filePath)} (${applied} patch(es))`);
426
- items.forEach(item => results.push({ selector: item.selector, ok: true }));
427
- } else {
428
- console.warn(` \x1b[33m⚠\x1b[0m AI matched 0 patches in ${path.basename(filePath)}`);
429
- console.log("Full AI response context for debugging:");
430
- console.log(apiResult);
431
-
432
- // Forced fallback for creation if AI failed match but we have a creation request
433
- const creationItems = items.filter(it => it.type === 'create');
434
- if (creationItems.length > 0) {
435
- console.log(`Attempting forced creation fallback for ${creationItems.length} element(s)...`);
436
- const index = content.toLowerCase().lastIndexOf('</body>');
437
- if (index >= 0) {
438
- const insertHTML = creationItems.map(ci => ci.outerHTML).join('\n');
439
- const patched = content.slice(0, index) + insertHTML + '\n' + content.slice(index);
440
- const final = rawContent.includes('\r\n') ? patched.replace(/\n/g, '\r\n') : patched;
441
- fs.writeFileSync(filePath, final, 'utf8');
442
- console.log(` \x1b[32m✓\x1b[0m Forced creation applied (${creationItems.length} elements) to ${path.basename(filePath)}`);
443
- items.forEach(item => results.push({ selector: item.selector, ok: true }));
444
- continue;
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
- items.forEach(item => results.push({ selector: item.selector, ok: false, reason: 'AI patch failed to match' }));
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
- console.error(` \x1b[31m✖\x1b[0m AI Error for ${path.basename(filePath)}:`, e.message);
451
- items.forEach(item => results.push({ selector: item.selector, ok: false, reason: e.message }));
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
- "name": "draply-dev",
3
- "version": "1.5.5",
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": ">=16.0.0"
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
+ }