@rahul_ur/devlink 1.0.8 → 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.
- package/README.md +4 -0
- package/bin/cli.mjs +152 -16
- package/logo.png +0 -0
- package/package.json +8 -7
package/README.md
CHANGED
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 {
|
|
192
|
-
import {
|
|
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
|
-
|
|
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
|
-
<
|
|
200
|
-
<
|
|
201
|
-
|
|
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 {
|
|
207
|
-
import {
|
|
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
|
-
|
|
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
|
-
<
|
|
215
|
-
<
|
|
216
|
-
|
|
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('\
|
|
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
|
|
258
|
-
devlink
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rahul_ur/devlink",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Click any element in your browser
|
|
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.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"devlink": "./bin/cli.mjs",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"templates",
|
|
13
13
|
"index.js",
|
|
14
14
|
"index.d.ts",
|
|
15
|
-
"README.md"
|
|
15
|
+
"README.md",
|
|
16
|
+
"logo.png"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"prepublishOnly": "npm pack --dry-run"
|
|
@@ -41,10 +42,10 @@
|
|
|
41
42
|
},
|
|
42
43
|
"homepage": "https://github.com/rahul_ur/devlink#readme",
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"@rahul_ur/devlink-bridge": "^1.0.
|
|
45
|
-
"@rahul_ur/devlink-studio": "^1.0.
|
|
46
|
-
"@rahul_ur/devlink-babel-plugin": "^1.0.
|
|
47
|
-
"@rahul_ur/devlink-vite-plugin": "^1.0.
|
|
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"
|
|
48
49
|
},
|
|
49
50
|
"engines": {
|
|
50
51
|
"node": ">=18.0.0"
|