@rahul_ur/devlink 1.0.10 → 1.0.12

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 (2) hide show
  1. package/bin/cli.mjs +307 -39
  2. package/package.json +7 -7
package/bin/cli.mjs CHANGED
@@ -18,6 +18,71 @@ function exists(p) { return fs.existsSync(p); }
18
18
  function readJson(p) { return JSON.parse(fs.readFileSync(p, 'utf8')); }
19
19
  function writeJson(p, v) { fs.writeFileSync(p, `${JSON.stringify(v, null, 2)}\n`); }
20
20
 
21
+ // Package name -> human-readable framework name, used to give an accurate
22
+ // "devlink doesn't support this yet" message instead of the generic
23
+ // "couldn't find @vitejs/plugin-react import" warning, which wrongly
24
+ // implies there's a manual fix available (there isn't — devlink's inspector
25
+ // hooks in via a Babel plugin specifically on @vitejs/plugin-react).
26
+ const NON_REACT_VITE_PLUGINS = [
27
+ ['@vitejs/plugin-vue', 'Vue'],
28
+ ['@vitejs/plugin-vue2', 'Vue 2'],
29
+ ['@sveltejs/vite-plugin-svelte', 'Svelte'],
30
+ ['vite-plugin-solid', 'Solid'],
31
+ ['@preact/preset-vite', 'Preact'],
32
+ ['vite-plugin-vue2', 'Vue 2'],
33
+ ['@builder.io/qwik', 'Qwik'],
34
+ ];
35
+
36
+ // Is TypeScript in play for this project? Used to decide the extension for
37
+ // any file we generate from scratch (vite.config.*, devlink.config.*) so we
38
+ // don't hand a plain-JS project a .ts file (which breaks its build unless a
39
+ // TS toolchain is already configured) or vice versa.
40
+ function projectUsesTypeScript() {
41
+ if (exists(path.join(projectRoot, 'tsconfig.json'))) return true;
42
+ const pkgPath = path.join(projectRoot, 'package.json');
43
+ if (!exists(pkgPath)) return false;
44
+ const pkg = readJson(pkgPath);
45
+ const all = { ...pkg.dependencies, ...pkg.devDependencies };
46
+ return Boolean(all['typescript']);
47
+ }
48
+
49
+ // ─── Bracket matching ───────────────────────────────────────────────────────
50
+ // Small helper for safely editing arbitrary existing config files. Plain
51
+ // string.replace()/regex on a whole file is fragile the moment the thing
52
+ // being matched (e.g. a `react()` call) has its own nested braces — this
53
+ // walks the source char-by-char (skipping over strings/template
54
+ // literals/comments so brackets inside them don't throw off the count) and
55
+ // returns the index of the matching closing bracket for an opening one.
56
+ function findMatchingBracket(src, openIndex, openChar, closeChar) {
57
+ let depth = 0;
58
+ let i = openIndex;
59
+ while (i < src.length) {
60
+ const c = src[i];
61
+ if (c === '"' || c === "'" || c === '`') {
62
+ const quote = c;
63
+ i++;
64
+ while (i < src.length && src[i] !== quote) {
65
+ if (src[i] === '\\') i++;
66
+ i++;
67
+ }
68
+ } else if (c === '/' && src[i + 1] === '/') {
69
+ while (i < src.length && src[i] !== '\n') i++;
70
+ } else if (c === '/' && src[i + 1] === '*') {
71
+ i += 2;
72
+ while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++;
73
+ i++;
74
+ } else {
75
+ if (c === openChar) depth++;
76
+ else if (c === closeChar) {
77
+ depth--;
78
+ if (depth === 0) return i;
79
+ }
80
+ }
81
+ i++;
82
+ }
83
+ return -1;
84
+ }
85
+
21
86
  // ─── .env merging ───────────────────────────────────────────────────────────
22
87
  // Sets one or more keys in .env at the project root without touching any
23
88
  // other lines already in there (e.g. keys someone set by hand, or ones
@@ -82,19 +147,21 @@ async function promptForApiKeys() {
82
147
  console.log('\x1b[36m Optional: cloud model API keys\x1b[0m');
83
148
  console.log('\x1b[36m─────────────────────────────────────────────────\x1b[0m');
84
149
  console.log('The AI companion works fully offline with Ollama. Add these only');
85
- console.log('if you also want Claude or GPT/Codex available in the model picker.');
86
- console.log('Leave blank and press Enter to skip either one.\n');
150
+ console.log('if you also want them available in the model picker.');
151
+ console.log('Leave any blank and press Enter to skip it.\n');
87
152
 
88
153
  const rl = createInterface({ input: process.stdin, output: process.stdout });
89
- let anthropicKey, openaiKey;
154
+ let anthropicKey, openaiKey, geminiKey, zenmuxKey;
90
155
  try {
91
156
  anthropicKey = (await rl.question(' Anthropic API key (Claude) — from https://console.anthropic.com/settings/keys: ')).trim();
92
157
  openaiKey = (await rl.question(' OpenAI API key (GPT/Codex) — from https://platform.openai.com/api-keys: ')).trim();
158
+ geminiKey = (await rl.question(' Google API key (Gemini) — from https://aistudio.google.com/app/apikey: ')).trim();
159
+ zenmuxKey = (await rl.question(' ZenMux API key (GLM 5.2 + others) — from https://zenmux.ai/settings/keys: ')).trim();
93
160
  } finally {
94
161
  rl.close();
95
162
  }
96
163
 
97
- if (!anthropicKey && !openaiKey) {
164
+ if (!anthropicKey && !openaiKey && !geminiKey && !zenmuxKey) {
98
165
  log('no keys entered — skipping. You can rerun `devlink setup` anytime to add them.');
99
166
  return;
100
167
  }
@@ -103,16 +170,18 @@ async function promptForApiKeys() {
103
170
  setEnvVars(envPath, {
104
171
  ANTHROPIC_API_KEY: anthropicKey,
105
172
  OPENAI_API_KEY: openaiKey,
173
+ GEMINI_API_KEY: geminiKey,
174
+ ZENMUX_API_KEY: zenmuxKey,
106
175
  });
107
176
  ensureEnvGitignored();
108
177
 
109
178
  const examplePath = path.join(projectRoot, '.env.example');
110
179
  if (!exists(examplePath)) {
111
- fs.writeFileSync(examplePath, 'ANTHROPIC_API_KEY=\nOPENAI_API_KEY=\n');
180
+ fs.writeFileSync(examplePath, 'ANTHROPIC_API_KEY=\nOPENAI_API_KEY=\nGEMINI_API_KEY=\nZENMUX_API_KEY=\n');
112
181
  ok('created .env.example template');
113
182
  }
114
183
 
115
- const added = [anthropicKey && 'Claude', openaiKey && 'GPT/Codex'].filter(Boolean).join(' and ');
184
+ const added = [anthropicKey && 'Claude', openaiKey && 'GPT/Codex', geminiKey && 'Gemini', zenmuxKey && 'ZenMux'].filter(Boolean).join(', ');
116
185
  ok(`saved ${added} API key(s) to .env — restart devlink:bridge to pick them up`);
117
186
  }
118
187
 
@@ -129,6 +198,38 @@ function detectFramework() {
129
198
  return 'unknown';
130
199
  }
131
200
 
201
+ // When the current directory isn't a recognizable frontend project (e.g.
202
+ // you're standing at the root of a monorepo whose real Vite/Next/CRA app
203
+ // lives in ./frontend or ./apps/web), blindly writing devlink.config.js and
204
+ // a devlink:bridge script *here* would just be clutter in the wrong place.
205
+ // Check one level of subdirectories for a package.json that looks like a
206
+ // real frontend project, so we can point the person at it instead.
207
+ function findFrontendSubdirectories() {
208
+ const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.turbo', '.devlink']);
209
+ let entries;
210
+ try {
211
+ entries = fs.readdirSync(projectRoot, { withFileTypes: true });
212
+ } catch {
213
+ return [];
214
+ }
215
+ const candidates = [];
216
+ for (const entry of entries) {
217
+ if (!entry.isDirectory() || SKIP.has(entry.name) || entry.name.startsWith('.')) continue;
218
+ const pkgPath = path.join(projectRoot, entry.name, 'package.json');
219
+ if (!exists(pkgPath)) continue;
220
+ try {
221
+ const pkg = readJson(pkgPath);
222
+ const all = { ...pkg.dependencies, ...pkg.devDependencies };
223
+ if (all['vite'] || all['@vitejs/plugin-react'] || all['react-scripts'] || all['next']) {
224
+ candidates.push(entry.name);
225
+ }
226
+ } catch {
227
+ // Unparsable package.json in that subdir — not a candidate, skip it.
228
+ }
229
+ }
230
+ return candidates;
231
+ }
232
+
132
233
  // ─── Add npm scripts ──────────────────────────────────────────────────────────
133
234
 
134
235
  function addScripts() {
@@ -147,11 +248,13 @@ function addScripts() {
147
248
  // ─── Setup Vite ───────────────────────────────────────────────────────────────
148
249
 
149
250
  function setupVite() {
150
- const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs'];
251
+ const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs', 'vite.config.cts', 'vite.config.cjs'];
151
252
  const configFile = candidates.find(f => exists(path.join(projectRoot, f)));
152
253
 
153
254
  if (!configFile) {
154
- // Create vite.config.ts from scratch
255
+ // Create a config from scratch — match extension to whether the project
256
+ // is actually TypeScript, rather than always assuming .ts.
257
+ const ext = projectUsesTypeScript() ? 'ts' : 'js';
155
258
  const content = `import { defineConfig } from 'vite';
156
259
  import react from '@vitejs/plugin-react';
157
260
  import { devlinkBabelConfig } from '@rahul_ur/devlink-vite-plugin';
@@ -168,38 +271,176 @@ export default defineConfig({
168
271
  ],
169
272
  });
170
273
  `;
171
- fs.writeFileSync(path.join(projectRoot, 'vite.config.ts'), content);
172
- ok('created vite.config.ts with devlink config');
274
+ fs.writeFileSync(path.join(projectRoot, `vite.config.${ext}`), content);
275
+ ok(`created vite.config.${ext} with devlink config`);
173
276
  return;
174
277
  }
175
278
 
176
279
  const configPath = path.join(projectRoot, configFile);
177
- let src = fs.readFileSync(configPath, 'utf8');
280
+ const original = fs.readFileSync(configPath, 'utf8');
178
281
 
179
- if (src.includes('@rahul_ur/devlink-vite-plugin') || src.includes('@rahul_ur/devlink-babel-plugin')) {
282
+ if (original.includes('@rahul_ur/devlink-vite-plugin') || original.includes('@rahul_ur/devlink-babel-plugin')) {
180
283
  ok(`${configFile} already has devlink config`);
181
284
  return;
182
285
  }
183
286
 
184
- // Add import line after last import
185
- const importLine = `import { devlinkBabelConfig } from '@rahul_ur/devlink-vite-plugin';\n`;
186
- const lastImport = src.lastIndexOf('\nimport ');
187
- const insertAt = lastImport === -1 ? 0 : src.indexOf('\n', lastImport) + 1;
188
- src = src.slice(0, insertAt) + importLine + src.slice(insertAt);
287
+ // Bail out cleanly (no partial edits) for the one framework variant this
288
+ // technique can't support: @vitejs/plugin-react-swc compiles with SWC,
289
+ // not Babel, so a Babel plugin passed through `babel: { plugins }` is
290
+ // silently ignored. Rather than write a config that looks patched but
291
+ // does nothing, tell the person up front.
292
+ if (/@vitejs\/plugin-react-swc/.test(original)) {
293
+ warn(`${configFile} uses @vitejs/plugin-react-swc, which compiles with SWC and doesn't run Babel plugins.`);
294
+ warn(`devlink's inspector needs the Babel-based @vitejs/plugin-react instead. Skipping auto-patch — see the README for manual setup options.`);
295
+ return;
296
+ }
189
297
 
190
- // Patch react() call
191
- if (src.includes('react()')) {
192
- src = src.replace(
193
- 'react()',
194
- `react({\n babel: {\n plugins: [devlinkBabelConfig({ root: process.cwd() })].filter(Boolean),\n },\n })`
195
- );
196
- ok(`patched ${configFile} — react() updated`);
197
- } else if (src.match(/react\s*\(\s*\{/)) {
198
- warn(`Could not auto-patch ${configFile}. Add manually:\n\n react({ babel: { plugins: [devlinkBabelConfig({ root: process.cwd() })] } })\n`);
298
+ // Find the local name the React plugin was imported as (usually `react`,
299
+ // but people alias it — `import reactPlugin from '@vitejs/plugin-react'`).
300
+ // Also accept CommonJS-style configs (`const react = require('@vitejs/plugin-react')`),
301
+ // since a plain vite.config.js has no obligation to be ESM.
302
+ const importMatch =
303
+ original.match(/import\s+(\w+)\s+from\s+['"]@vitejs\/plugin-react['"]/) ||
304
+ original.match(/const\s+(\w+)\s*=\s*require\(\s*['"]@vitejs\/plugin-react['"]\s*\)/);
305
+ if (!importMatch) {
306
+ const nonReactPlugin = NON_REACT_VITE_PLUGINS.find(([pkg]) => original.includes(pkg));
307
+ if (nonReactPlugin) {
308
+ const [, framework] = nonReactPlugin;
309
+ warn(`${configFile} uses a ${framework} plugin, not @vitejs/plugin-react.`);
310
+ warn(`devlink's inspector currently only supports React projects (it hooks in via a Babel plugin on @vitejs/plugin-react) — there's no equivalent for ${framework} yet. Skipping auto-patch.`);
311
+ return;
312
+ }
313
+ warn(`Could not find an "import ... from '@vitejs/plugin-react'" (or the CommonJS equivalent) in ${configFile}. Add devlink manually:\n\n import { devlinkBabelConfig } from '@rahul_ur/devlink-vite-plugin';\n // inside react({ babel: { plugins: [devlinkBabelConfig({ root: process.cwd() })] } })\n`);
314
+ return;
315
+ }
316
+ const reactLocalName = importMatch[1];
317
+ const isCjsConfig = importMatch[0].trimStart().startsWith('const');
318
+
319
+ // Locate the actual call, e.g. `react(...)` or `react (...)`, skipping the
320
+ // import line itself.
321
+ const callRegex = new RegExp(`\\b${reactLocalName}\\s*\\(`, 'g');
322
+ callRegex.lastIndex = importMatch.index + importMatch[0].length;
323
+ const callMatch = callRegex.exec(original);
324
+ if (!callMatch) {
325
+ warn(`Could not find a ${reactLocalName}(...) call in ${configFile} to patch. Add devlink manually — see README.`);
326
+ return;
199
327
  }
200
328
 
201
- fs.writeFileSync(configPath, src);
202
- ok(`updated ${configFile}`);
329
+ const openParenIndex = callMatch.index + callMatch[0].length - 1;
330
+ const closeParenIndex = findMatchingBracket(original, openParenIndex, '(', ')');
331
+ if (closeParenIndex === -1) {
332
+ warn(`Could not parse the ${reactLocalName}(...) call in ${configFile} (unbalanced parens?). Add devlink manually — see README.`);
333
+ return;
334
+ }
335
+
336
+ const argsText = original.slice(openParenIndex + 1, closeParenIndex);
337
+ const trimmedArgs = argsText.trim();
338
+ const devlinkPluginCall = `devlinkBabelConfig({ root: process.cwd() })`;
339
+
340
+ // We compute a single (insertAt, textToInsert) point-insertion into
341
+ // `original` — deliberately avoiding building up nested replacement
342
+ // strings with their own relative offsets, since re-deriving absolute
343
+ // positions from those is exactly where off-by-one bugs creep in.
344
+ let insertAt, insertText;
345
+
346
+ if (trimmedArgs === '') {
347
+ // react() — bare call, safe to fill in from scratch.
348
+ insertAt = openParenIndex + 1;
349
+ insertText = `{\n babel: {\n plugins: [${devlinkPluginCall}].filter(Boolean),\n },\n }`;
350
+ // This is a full replacement of the (empty) args, not an insertion —
351
+ // handle it as a range replace below via closeParenIndex.
352
+ const patchedBare = original.slice(0, insertAt) + insertText + original.slice(closeParenIndex);
353
+ finishPatch(patchedBare);
354
+ return;
355
+ }
356
+
357
+ if (!trimmedArgs.startsWith('{')) {
358
+ // react(someVariable) or react(...spread) — a bare identifier/expression
359
+ // we can't safely rewrite without risking breaking the config.
360
+ warn(`${reactLocalName}(...) in ${configFile} is called with something other than a literal object, so it can't be auto-patched safely. Add devlink manually:\n\n babel: { plugins: [${devlinkPluginCall}].filter(Boolean) }\n\ninside whatever object is passed to ${reactLocalName}().`);
361
+ return;
362
+ }
363
+
364
+ // react({ ...existing options... }) — edit the object in place instead of
365
+ // clobbering whatever's already there (fast refresh options, other babel
366
+ // plugins, jsxImportSource, etc). All positions below are absolute
367
+ // offsets into `original`.
368
+ const objStart = openParenIndex + 1 + argsText.indexOf('{');
369
+ const objEnd = findMatchingBracket(original, objStart, '{', '}');
370
+ if (objEnd === -1) {
371
+ warn(`Could not parse the options object passed to ${reactLocalName}(...) in ${configFile}. Add devlink manually — see README.`);
372
+ return;
373
+ }
374
+
375
+ const babelKeyMatch = original.slice(objStart, objEnd).match(/\bbabel\s*:\s*\{/);
376
+ if (!babelKeyMatch) {
377
+ // No babel key at all — insert one right after the object's opening brace.
378
+ insertAt = objStart + 1;
379
+ insertText = `\n babel: {\n plugins: [${devlinkPluginCall}].filter(Boolean),\n },`;
380
+ finishPatch(original.slice(0, insertAt) + insertText + original.slice(insertAt));
381
+ return;
382
+ }
383
+
384
+ const babelObjStart = objStart + babelKeyMatch.index + babelKeyMatch[0].length - 1;
385
+ const babelObjEnd = findMatchingBracket(original, babelObjStart, '{', '}');
386
+ if (babelObjEnd === -1) {
387
+ warn(`Could not parse the existing babel option in ${configFile}. Add devlink manually — see README.`);
388
+ return;
389
+ }
390
+
391
+ const pluginsKeyMatch = original.slice(babelObjStart, babelObjEnd).match(/\bplugins\s*:\s*\[/);
392
+ if (!pluginsKeyMatch) {
393
+ // babel: { ... } exists but has no plugins array yet.
394
+ insertAt = babelObjStart + 1;
395
+ insertText = `\n plugins: [${devlinkPluginCall}].filter(Boolean),`;
396
+ finishPatch(original.slice(0, insertAt) + insertText + original.slice(insertAt));
397
+ return;
398
+ }
399
+
400
+ // babel.plugins already exists (e.g. another tool's babel plugin) —
401
+ // append devlink alongside it rather than overwrite it.
402
+ const arrStart = babelObjStart + pluginsKeyMatch.index + pluginsKeyMatch[0].length - 1;
403
+ const arrEnd = findMatchingBracket(original, arrStart, '[', ']');
404
+ if (arrEnd === -1) {
405
+ warn(`Could not parse the existing babel.plugins array in ${configFile}. Add devlink manually — see README.`);
406
+ return;
407
+ }
408
+ const arrInner = original.slice(arrStart + 1, arrEnd);
409
+ insertAt = arrEnd; // insert just before the closing ']'
410
+ insertText = arrInner.trim() === ''
411
+ ? `${devlinkPluginCall}`
412
+ : `${arrInner.trimEnd().endsWith(',') ? '' : ','}\n ${devlinkPluginCall},`;
413
+ finishPatch(original.slice(0, insertAt) + insertText + original.slice(insertAt));
414
+ return;
415
+
416
+ function finishPatch(patched) {
417
+
418
+ // Only now that the react() call itself is confirmed patchable do we add
419
+ // the import — avoids leaving a dangling unused import if patching fails.
420
+ // Match the file's existing module style: mixing a top-level `import`
421
+ // into an otherwise-CommonJS (`require`/`module.exports`) config forces
422
+ // Node to load the whole file as ESM, at which point `require` is
423
+ // undefined and the config throws at load time — a worse outcome than
424
+ // not patching at all.
425
+ let insertAt = 0;
426
+ let importLine;
427
+ if (isCjsConfig) {
428
+ importLine = `const { devlinkBabelConfig } = require('@rahul_ur/devlink-vite-plugin');\n`;
429
+ const requireLines = [...patched.matchAll(/^.*\brequire\(['"][^'"]+['"]\).*$/gm)];
430
+ if (requireLines.length > 0) {
431
+ const last = requireLines[requireLines.length - 1];
432
+ insertAt = last.index + last[0].length + 1;
433
+ }
434
+ } else {
435
+ importLine = `import { devlinkBabelConfig } from '@rahul_ur/devlink-vite-plugin';\n`;
436
+ const lastImport = patched.lastIndexOf('\nimport ');
437
+ insertAt = lastImport === -1 ? 0 : patched.indexOf('\n', lastImport) + 1;
438
+ }
439
+ patched = patched.slice(0, insertAt) + importLine + patched.slice(insertAt);
440
+
441
+ fs.writeFileSync(configPath, patched);
442
+ ok(`patched ${configFile} — ${reactLocalName}() now runs devlink's babel plugin`);
443
+ }
203
444
  }
204
445
 
205
446
  // ─── Setup Next.js ────────────────────────────────────────────────────────────
@@ -261,21 +502,27 @@ function setupCra() {
261
502
  // ─── Generate devlink.config.ts ───────────────────────────────────────────────
262
503
 
263
504
  function generateConfig() {
264
- const configPath = path.join(projectRoot, 'devlink.config.ts');
265
- if (exists(configPath)) { ok('devlink.config.ts already exists'); return; }
505
+ const candidates = ['devlink.config.ts', 'devlink.config.js', 'devlink.config.mts', 'devlink.config.mjs', 'devlink.config.cts', 'devlink.config.cjs'];
506
+ const existingConfig = candidates.find(f => exists(path.join(projectRoot, f)));
507
+ if (existingConfig) { ok(`${existingConfig} already exists`); return; }
266
508
 
267
- const root = projectRoot.replace(/\\/g, '/');
268
- const srcDir = exists(path.join(projectRoot, 'src')) ? `${root}/src` : root;
509
+ const ext = projectUsesTypeScript() ? 'ts' : 'js';
510
+ const configPath = path.join(projectRoot, `devlink.config.${ext}`);
269
511
 
270
- fs.writeFileSync(configPath, `// devlink.config.ts edit projectRoot and rootPath for your project
512
+ // Use relative paths, not absolute filesystem paths this file is meant
513
+ // to be committed, and an absolute path baked in here would be wrong the
514
+ // moment anyone else on the team (or CI) checks the project out.
515
+ const rootPath = exists(path.join(projectRoot, 'src')) ? './src' : '.';
516
+
517
+ fs.writeFileSync(configPath, `// devlink.config.${ext} — edit rootPath if your source lives somewhere other than ./src
271
518
  export const devlinkConfig = {
272
- projectRoot: '${root}',
273
- rootPath: '${srcDir}',
519
+ projectRoot: '.',
520
+ rootPath: '${rootPath}',
274
521
  defaultDocked: true,
275
522
  dockedWidth: 460,
276
523
  };
277
524
  `);
278
- ok('created devlink.config.ts');
525
+ ok(`created devlink.config.${ext}`);
279
526
  }
280
527
 
281
528
  // ─── Print main.tsx instructions ──────────────────────────────────────────────
@@ -343,9 +590,15 @@ function printInstructions(framework) {
343
590
  )
344
591
  }
345
592
 
346
- ReactDOM.createRoot(document.getElementById('root')!).render(
347
- <React.StrictMode><Root /></React.StrictMode>
348
- )`);
593
+ const rootElement = document.getElementById("root");
594
+
595
+ if (rootElement) {
596
+ ReactDOM.createRoot(rootElement).render(
597
+ <React.StrictMode>
598
+ <Root />
599
+ </React.StrictMode>,
600
+ );
601
+ }`);
349
602
  }
350
603
 
351
604
  console.log('\nThis keeps the AI companion, editor, and inspector out of your production');
@@ -363,6 +616,21 @@ async function runSetup() {
363
616
  const framework = detectFramework();
364
617
  log(`detected framework: ${framework}`);
365
618
 
619
+ if (framework === 'unknown') {
620
+ const candidates = findFrontendSubdirectories();
621
+ if (candidates.length > 0) {
622
+ warn(`No frontend framework detected at ${projectRoot} — this looks like it might be a monorepo root.`);
623
+ if (candidates.length === 1) {
624
+ warn(`Found what looks like a frontend project in ./${candidates[0]}. Try running devlink setup from there instead:\n\n cd ${candidates[0]} && npx devlink setup\n`);
625
+ } else {
626
+ warn(`Found possible frontend projects in: ${candidates.map(c => `./${c}`).join(', ')}. Re-run devlink setup from inside the right one.`);
627
+ }
628
+ // Deliberately stop here rather than writing devlink.config.js /
629
+ // package.json scripts at a root that isn't actually the app.
630
+ return;
631
+ }
632
+ }
633
+
366
634
  addScripts();
367
635
 
368
636
  if (framework === 'vite') setupVite();
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@rahul_ur/devlink",
3
- "version": "1.0.10",
4
- "description": "Click any element in your browser \u2192 jump to source code \u2192 edit \u2192 UI updates live. One command setup for Vite, Next.js and CRA.",
3
+ "version": "1.0.12",
4
+ "description": "Click any element in your browser jump to source code edit UI updates live. One command setup for Vite, Next.js and CRA.",
5
5
  "main": "index.js",
6
+ "type": "module",
6
7
  "bin": {
7
8
  "devlink": "./bin/cli.mjs",
8
9
  "devlink-setup": "./bin/cli.mjs"
9
10
  },
10
11
  "files": [
11
12
  "bin",
12
- "templates",
13
13
  "index.js",
14
14
  "index.d.ts",
15
15
  "README.md",
@@ -42,10 +42,10 @@
42
42
  },
43
43
  "homepage": "https://github.com/rahul_ur/devlink#readme",
44
44
  "dependencies": {
45
- "@rahul_ur/devlink-bridge": "^1.0.3",
46
- "@rahul_ur/devlink-studio": "^1.0.6",
47
- "@rahul_ur/devlink-babel-plugin": "^1.0.5",
48
- "@rahul_ur/devlink-vite-plugin": "^1.0.7"
45
+ "@rahul_ur/devlink-babel-plugin":"1.0.7",
46
+ "@rahul_ur/devlink-bridge": "1.0.4",
47
+ "@rahul_ur/devlink-studio": "1.0.7",
48
+ "@rahul_ur/devlink-vite-plugin": "1.0.7"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=18.0.0"