@rahul_ur/devlink 1.0.10 → 1.0.11
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.mjs +307 -39
- 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
|
|
86
|
-
console.log('Leave blank and press Enter to skip
|
|
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('
|
|
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
|
|
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,
|
|
172
|
-
ok(
|
|
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
|
-
|
|
280
|
+
const original = fs.readFileSync(configPath, 'utf8');
|
|
178
281
|
|
|
179
|
-
if (
|
|
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
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
|
265
|
-
|
|
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
|
|
268
|
-
const
|
|
509
|
+
const ext = projectUsesTypeScript() ? 'ts' : 'js';
|
|
510
|
+
const configPath = path.join(projectRoot, `devlink.config.${ext}`);
|
|
269
511
|
|
|
270
|
-
|
|
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: '
|
|
273
|
-
rootPath: '${
|
|
519
|
+
projectRoot: '.',
|
|
520
|
+
rootPath: '${rootPath}',
|
|
274
521
|
defaultDocked: true,
|
|
275
522
|
dockedWidth: 460,
|
|
276
523
|
};
|
|
277
524
|
`);
|
|
278
|
-
ok(
|
|
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
|
-
|
|
347
|
-
|
|
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.
|
|
4
|
-
"description": "Click any element in your browser
|
|
3
|
+
"version": "1.0.11",
|
|
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-
|
|
46
|
-
"@rahul_ur/devlink-
|
|
47
|
-
"@rahul_ur/devlink-
|
|
48
|
-
"@rahul_ur/devlink-vite-plugin": "
|
|
45
|
+
"@rahul_ur/devlink-babel-plugin":"1.0.6",
|
|
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"
|