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.
Files changed (3) hide show
  1. package/bin/cli.js +238 -110
  2. package/package.json +27 -27
  3. 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 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,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: ['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'],
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
- res.writeHead(200, { 'Content-Type': 'application/json' });
262
- res.end(JSON.stringify({ ok: true, fallback: true }));
263
- return;
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
- const fallbackHTML = path.join(process.cwd(), 'index.html');
296
- if (fs.existsSync(fallbackHTML)) {
297
- targetFile = fallbackHTML;
298
- } else {
299
- results.push({ selector: ch.selector, ok: false, reason: 'Source file not linked' });
300
- continue;
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
- const parts = c.selector.split('.');
317
- return parts.length > 1 ? parts.pop() : '';
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
- const parts = c.selector.split('#');
321
- return parts.length > 1 ? parts.pop() : '';
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
- const hasClass = classNamesToFind.some(cls => lines[i].includes(cls));
327
- const hasId = idsToFind.some(id => lines[i].includes(id));
328
- if (hasClass || hasId) {
329
- const s = Math.max(0, i - 60);
330
- const e = Math.min(lines.length - 1, i + 60);
331
- if (ctxStart === -1 || s < ctxStart) ctxStart = s;
332
- if (ctxEnd === -1 || e > ctxEnd) ctxEnd = e;
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
- for (let i = lines.length - 1; i >= 0; i--) {
337
- if (lines[i].includes('</body>')) {
338
- ctxStart = Math.max(0, i - 20);
339
- ctxEnd = Math.min(lines.length - 1, i + 20);
340
- break;
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) 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.
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
- // AI call — supports Groq/OpenAI/Anthropic/Ollama (#10)
390
- apiResult = await callAI(cfg, prompt);
391
-
392
- const patches = [];
393
- const patchRegex = /<search>([\s\S]*?)<\/search>[\s\S]*?<replace>([\s\S]*?)<\/replace>/g;
394
- let match;
395
- while ((match = patchRegex.exec(apiResult)) !== null) {
396
- let s = match[1], r = match[2];
397
- s = s.replace(/^\n+/, '').replace(/\n+$/, '').replace(/\r/g, '');
398
- r = r.replace(/^\n+/, '').replace(/\n+$/, '').replace(/\r/g, '');
399
- if (s.trim()) patches.push({ search: s, replace: r });
400
- }
401
-
402
- let newContent = content;
403
- let applied = 0;
404
- for (const patch of patches) {
405
- if (newContent.includes(patch.search)) {
406
- newContent = newContent.replace(patch.search, patch.replace);
407
- applied++;
408
- } else {
409
- // Fallback to whitespace-agnostic matching
410
- const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
411
- const looseRegexStr = escapeRegExp(patch.search.trim()).replace(/\s+/g, '\\s+');
412
- const looseRegex = new RegExp(looseRegexStr);
413
- if (looseRegex.test(newContent)) {
414
- newContent = newContent.replace(looseRegex, patch.replace);
415
- applied++;
416
- }
417
- }
418
- }
419
-
420
- if (applied > 0) {
421
- // Restore original line endings if they were \r\n
422
- const finalContent = rawContent.includes('\r\n') ? newContent.replace(/\n/g, '\r\n') : newContent;
423
- fs.writeFileSync(filePath, finalContent, 'utf8');
424
- console.log(` \x1b[32m✓\x1b[0m Modified ${path.basename(filePath)} (${applied} patch(es))`);
425
- items.forEach(item => results.push({ selector: item.selector, ok: true }));
426
- } else {
427
- console.warn(` \x1b[33m⚠\x1b[0m AI matched 0 patches in ${path.basename(filePath)}`);
428
- console.log("Full AI response context for debugging:");
429
- console.log(apiResult);
430
-
431
- // Forced fallback for creation if AI failed match but we have a creation request
432
- const creationItems = items.filter(it => it.type === 'create');
433
- if (creationItems.length > 0) {
434
- console.log(`Attempting forced creation fallback for ${creationItems.length} element(s)...`);
435
- const index = content.toLowerCase().lastIndexOf('</body>');
436
- if (index >= 0) {
437
- const insertHTML = creationItems.map(ci => ci.outerHTML).join('\n');
438
- const patched = content.slice(0, index) + insertHTML + '\n' + content.slice(index);
439
- const final = rawContent.includes('\r\n') ? patched.replace(/\n/g, '\r\n') : patched;
440
- fs.writeFileSync(filePath, final, 'utf8');
441
- console.log(` \x1b[32m✓\x1b[0m Forced creation applied (${creationItems.length} elements) to ${path.basename(filePath)}`);
442
- items.forEach(item => results.push({ selector: item.selector, ok: true }));
443
- 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
+ }
444
556
  }
445
- }
446
- 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
+ }
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
- console.error(` \x1b[31m✖\x1b[0m AI Error for ${path.basename(filePath)}:`, e.message);
450
- 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 }));
451
579
  }
452
580
  }
453
581
 
package/package.json CHANGED
@@ -1,27 +1,27 @@
1
- {
2
- "name": "draply-dev",
3
- "version": "1.5.4",
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
+ }