@rahul_ur/devlink 1.0.9 → 1.0.10

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 (4) hide show
  1. package/README.md +4 -0
  2. package/bin/cli.mjs +152 -16
  3. package/logo.png +0 -0
  4. package/package.json +12 -18
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/rahul_ur/devlink/main/logo.png" alt="devlink logo" width="120" />
3
+ </p>
4
+
1
5
  # devlink
2
6
 
3
7
  Click any element in your browser → jump to its source code → edit it → UI updates live.
package/bin/cli.mjs CHANGED
@@ -2,10 +2,12 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { execSync } from 'node:child_process';
5
+ import { createInterface } from 'node:readline/promises';
5
6
 
6
7
  const projectRoot = process.cwd();
7
8
  const args = process.argv.slice(2);
8
9
  const command = args[0] || 'setup';
10
+ const skipKeys = args.includes('--skip-keys');
9
11
 
10
12
  const log = (msg) => console.log(`\x1b[36m[devlink]\x1b[0m ${msg}`);
11
13
  const ok = (msg) => console.log(`\x1b[32m[devlink] ✓\x1b[0m ${msg}`);
@@ -16,6 +18,104 @@ function exists(p) { return fs.existsSync(p); }
16
18
  function readJson(p) { return JSON.parse(fs.readFileSync(p, 'utf8')); }
17
19
  function writeJson(p, v) { fs.writeFileSync(p, `${JSON.stringify(v, null, 2)}\n`); }
18
20
 
21
+ // ─── .env merging ───────────────────────────────────────────────────────────
22
+ // Sets one or more keys in .env at the project root without touching any
23
+ // other lines already in there (e.g. keys someone set by hand, or ones
24
+ // added by a previous run of this command). Parses just enough to find and
25
+ // replace an existing KEY=... line, or append a new one.
26
+ function setEnvVars(envPath, vars) {
27
+ let lines = exists(envPath) ? fs.readFileSync(envPath, 'utf8').split('\n') : [];
28
+ // Trim trailing blanks up front — otherwise a file that already ends in
29
+ // a newline leaves an empty '' element that a newly-appended key then
30
+ // lands after, producing a stray blank line in the middle of the file.
31
+ while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
32
+ for (const [key, value] of Object.entries(vars)) {
33
+ if (!value) continue;
34
+ const lineIndex = lines.findIndex(l => l.trim().startsWith(`${key}=`));
35
+ const newLine = `${key}=${value}`;
36
+ if (lineIndex !== -1) lines[lineIndex] = newLine;
37
+ else lines.push(newLine);
38
+ }
39
+ fs.writeFileSync(envPath, lines.join('\n') + '\n');
40
+ }
41
+
42
+ // Makes sure .env (and variants) are excluded from git in *this* project
43
+ // specifically — never assume a .gitignore fix applied somewhere else
44
+ // (e.g. in devlink's own monorepo) carried over to whatever project
45
+ // `devlink setup` is being run in.
46
+ function ensureEnvGitignored() {
47
+ const gitignorePath = path.join(projectRoot, '.gitignore');
48
+ const entry = '.env';
49
+ if (!exists(gitignorePath)) {
50
+ fs.writeFileSync(gitignorePath, `${entry}\n.env.*\n!.env.example\n`);
51
+ ok('created .gitignore with .env excluded');
52
+ return;
53
+ }
54
+ const content = fs.readFileSync(gitignorePath, 'utf8');
55
+ const alreadyCovered = content.split('\n').some(l => {
56
+ const t = l.trim();
57
+ return t === '.env' || t === '.env*' || t === '**/.env' || t === '*.env';
58
+ });
59
+ if (alreadyCovered) {
60
+ ok('.env already excluded in .gitignore');
61
+ return;
62
+ }
63
+ fs.writeFileSync(gitignorePath, content.replace(/\n?$/, '\n') + '\n# API keys — added by devlink setup\n.env\n.env.*\n!.env.example\n');
64
+ ok('added .env to .gitignore');
65
+ }
66
+
67
+ // ─── API key setup ──────────────────────────────────────────────────────────
68
+ async function promptForApiKeys() {
69
+ if (skipKeys) {
70
+ log('skipping API key setup (--skip-keys)');
71
+ return;
72
+ }
73
+ // Non-interactive environments (CI, piped input, etc.) have no TTY to
74
+ // prompt against — skip rather than hang waiting for input that will
75
+ // never come.
76
+ if (!process.stdin.isTTY) {
77
+ log('no interactive terminal detected — skipping API key setup (run with a TTY, or set ANTHROPIC_API_KEY / OPENAI_API_KEY yourself in .env)');
78
+ return;
79
+ }
80
+
81
+ console.log('\n\x1b[36m─────────────────────────────────────────────────\x1b[0m');
82
+ console.log('\x1b[36m Optional: cloud model API keys\x1b[0m');
83
+ console.log('\x1b[36m─────────────────────────────────────────────────\x1b[0m');
84
+ 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');
87
+
88
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
89
+ let anthropicKey, openaiKey;
90
+ try {
91
+ anthropicKey = (await rl.question(' Anthropic API key (Claude) — from https://console.anthropic.com/settings/keys: ')).trim();
92
+ openaiKey = (await rl.question(' OpenAI API key (GPT/Codex) — from https://platform.openai.com/api-keys: ')).trim();
93
+ } finally {
94
+ rl.close();
95
+ }
96
+
97
+ if (!anthropicKey && !openaiKey) {
98
+ log('no keys entered — skipping. You can rerun `devlink setup` anytime to add them.');
99
+ return;
100
+ }
101
+
102
+ const envPath = path.join(projectRoot, '.env');
103
+ setEnvVars(envPath, {
104
+ ANTHROPIC_API_KEY: anthropicKey,
105
+ OPENAI_API_KEY: openaiKey,
106
+ });
107
+ ensureEnvGitignored();
108
+
109
+ const examplePath = path.join(projectRoot, '.env.example');
110
+ if (!exists(examplePath)) {
111
+ fs.writeFileSync(examplePath, 'ANTHROPIC_API_KEY=\nOPENAI_API_KEY=\n');
112
+ ok('created .env.example template');
113
+ }
114
+
115
+ const added = [anthropicKey && 'Claude', openaiKey && 'GPT/Codex'].filter(Boolean).join(' and ');
116
+ ok(`saved ${added} API key(s) to .env — restart devlink:bridge to pick them up`);
117
+ }
118
+
19
119
  // ─── Detect framework ─────────────────────────────────────────────────────────
20
120
 
21
121
  function detectFramework() {
@@ -186,34 +286,60 @@ function printInstructions(framework) {
186
286
  console.log('\x1b[36m devlink setup complete — final step:\x1b[0m');
187
287
  console.log('\x1b[36m─────────────────────────────────────────────────\x1b[0m\n');
188
288
 
289
+ // A plain top-level `import { DevlinkStudio } from '@rahul_ur/devlink-studio'`
290
+ // bundles it (and its dependencies — Monaco, xterm) into every build,
291
+ // production included. DevlinkStudio's own `enabled` prop stops it from
292
+ // *rendering* outside dev, but that's a runtime check — it does nothing
293
+ // about bundle size or exposing the AI companion/inspector code to real
294
+ // users. A dynamic import gated on the DEV flag drops it during Rollup's
295
+ // production build entirely, as a separate chunk your production users
296
+ // never download.
189
297
  if (isNext) {
190
298
  console.log('Add to pages/_app.tsx:\n');
191
- console.log(` import { DevlinkBridge } from '@rahul_ur/devlink-bridge'
192
- import { DevlinkStudio } from '@rahul_ur/devlink-studio'
299
+ console.log(` import { lazy, Suspense } from 'react'
300
+ import { DevlinkBridge } from '@rahul_ur/devlink-bridge'
193
301
  import { devlinkConfig } from '../devlink.config'
194
302
 
195
- const bridge = new DevlinkBridge()
303
+ // Dynamic + DEV-gated: keeps devlink-studio (and Monaco/xterm) out of
304
+ // the production bundle entirely — not just inactive, actually absent.
305
+ const DevlinkStudio = process.env.NODE_ENV === 'development'
306
+ ? lazy(() => import('@rahul_ur/devlink-studio').then(m => ({ default: m.DevlinkStudio })))
307
+ : null
308
+
309
+ const bridge = process.env.NODE_ENV === 'development' ? new DevlinkBridge() : null
196
310
 
197
311
  export default function App({ Component, pageProps }) {
312
+ if (!DevlinkStudio) return <Component {...pageProps} />
198
313
  return (
199
- <DevlinkStudio bridge={bridge} {...devlinkConfig}>
200
- <Component {...pageProps} />
201
- </DevlinkStudio>
314
+ <Suspense fallback={<Component {...pageProps} />}>
315
+ <DevlinkStudio bridge={bridge} {...devlinkConfig}>
316
+ <Component {...pageProps} />
317
+ </DevlinkStudio>
318
+ </Suspense>
202
319
  )
203
320
  }`);
204
321
  } else {
205
322
  console.log('Add to src/main.tsx:\n');
206
- console.log(` import { DevlinkBridge } from '@rahul_ur/devlink-bridge'
207
- import { DevlinkStudio } from '@rahul_ur/devlink-studio'
323
+ console.log(` import { lazy, Suspense } from 'react'
324
+ import { DevlinkBridge } from '@rahul_ur/devlink-bridge'
208
325
  import { devlinkConfig } from '../devlink.config'
209
326
 
210
- const bridge = new DevlinkBridge()
327
+ // Dynamic + DEV-gated: keeps devlink-studio (and Monaco/xterm) out of
328
+ // the production bundle entirely — not just inactive, actually absent.
329
+ const DevlinkStudio = import.meta.env.DEV
330
+ ? lazy(() => import('@rahul_ur/devlink-studio').then(m => ({ default: m.DevlinkStudio })))
331
+ : null
332
+
333
+ const bridge = import.meta.env.DEV ? new DevlinkBridge() : null
211
334
 
212
335
  function Root() {
336
+ if (!DevlinkStudio) return <App />
213
337
  return (
214
- <DevlinkStudio bridge={bridge} {...devlinkConfig}>
215
- <App />
216
- </DevlinkStudio>
338
+ <Suspense fallback={<App />}>
339
+ <DevlinkStudio bridge={bridge} {...devlinkConfig}>
340
+ <App />
341
+ </DevlinkStudio>
342
+ </Suspense>
217
343
  )
218
344
  }
219
345
 
@@ -222,7 +348,9 @@ function printInstructions(framework) {
222
348
  )`);
223
349
  }
224
350
 
225
- console.log('\nThen start the bridge server:\n');
351
+ console.log('\nThis keeps the AI companion, editor, and inspector out of your production');
352
+ console.log('bundle entirely (dynamic import, dropped at build time) — not just hidden.\n');
353
+ console.log('Then start the bridge server:\n');
226
354
  console.log(' npm run devlink:bridge\n');
227
355
  console.log('Press \x1b[36mAlt+D\x1b[0m in the browser to activate inspect mode.\n');
228
356
  console.log('\x1b[36m─────────────────────────────────────────────────\x1b[0m\n');
@@ -230,7 +358,7 @@ function printInstructions(framework) {
230
358
 
231
359
  // ─── Commands ─────────────────────────────────────────────────────────────────
232
360
 
233
- function runSetup() {
361
+ async function runSetup() {
234
362
  log(`setting up devlink in ${projectRoot}`);
235
363
  const framework = detectFramework();
236
364
  log(`detected framework: ${framework}`);
@@ -246,6 +374,7 @@ function runSetup() {
246
374
  }
247
375
 
248
376
  generateConfig();
377
+ await promptForApiKeys();
249
378
  printInstructions(framework);
250
379
  }
251
380
 
@@ -254,8 +383,9 @@ function runHelp() {
254
383
  \x1b[36mdevlink\x1b[0m — click any element in your browser, jump to source, edit, UI updates live.
255
384
 
256
385
  \x1b[1mCommands:\x1b[0m
257
- devlink setup Auto-configure your project (Vite / Next.js / CRA)
258
- devlink help Show this help message
386
+ devlink setup Auto-configure your project (Vite / Next.js / CRA)
387
+ devlink setup --skip-keys Same, but skip the optional cloud API key prompt
388
+ devlink help Show this help message
259
389
 
260
390
  \x1b[1mQuick start:\x1b[0m
261
391
  npm install @rahul_ur/devlink
@@ -274,6 +404,12 @@ function runHelp() {
274
404
  @rahul_ur/devlink-babel-plugin Stamps data-source at build time
275
405
  @rahul_ur/devlink-vite-plugin Wires plugin into Vite automatically
276
406
 
407
+ \x1b[1mCloud model keys:\x1b[0m
408
+ \`devlink setup\` will offer to save an Anthropic and/or OpenAI API key to
409
+ .env (gitignored automatically) so Claude/GPT show up in the AI companion's
410
+ model picker alongside your local Ollama models. Fully optional — Ollama
411
+ keeps working with no keys set at all.
412
+
277
413
  \x1b[1mDocs:\x1b[0m https://github.com/rahul_ur/devlink
278
414
  `);
279
415
  }
package/logo.png ADDED
Binary file
package/package.json CHANGED
@@ -1,15 +1,8 @@
1
1
  {
2
2
  "name": "@rahul_ur/devlink",
3
- "version": "1.0.9",
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
- "type": "module",
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.",
6
5
  "main": "index.js",
7
- "exports": {
8
- ".": {
9
- "import": "./index.js",
10
- "types": "./index.d.ts"
11
- }
12
- },
13
6
  "bin": {
14
7
  "devlink": "./bin/cli.mjs",
15
8
  "devlink-setup": "./bin/cli.mjs"
@@ -19,7 +12,8 @@
19
12
  "templates",
20
13
  "index.js",
21
14
  "index.d.ts",
22
- "README.md"
15
+ "README.md",
16
+ "logo.png"
23
17
  ],
24
18
  "scripts": {
25
19
  "prepublishOnly": "npm pack --dry-run"
@@ -37,21 +31,21 @@
37
31
  "editor",
38
32
  "babel-plugin"
39
33
  ],
40
- "author": "Rahul Urmaliya",
34
+ "author": "Rahul <your@email.com>",
41
35
  "license": "MIT",
42
36
  "repository": {
43
37
  "type": "git",
44
- "url": "git+https://github.com/BiginerCoder/devlink.git"
38
+ "url": "https://github.com/rahul_ur/devlink"
45
39
  },
46
40
  "bugs": {
47
- "url": "https://github.com/BiginerCoder/devlink/issues"
41
+ "url": "https://github.com/rahul_ur/devlink/issues"
48
42
  },
49
- "homepage": "https://github.com/BiginerCoder/devlink#readme",
43
+ "homepage": "https://github.com/rahul_ur/devlink#readme",
50
44
  "dependencies": {
51
- "@rahul_ur/devlink-bridge": "^1.0.1",
52
- "@rahul_ur/devlink-studio": "^1.0.5",
53
- "@rahul_ur/devlink-babel-plugin": "^1.0.4",
54
- "@rahul_ur/devlink-vite-plugin": "^1.0.6"
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"
55
49
  },
56
50
  "engines": {
57
51
  "node": ">=18.0.0"