@rahul_ur/devlink-bridge 1.0.0

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/server.mjs ADDED
@@ -0,0 +1,234 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { watch } from 'fs';
5
+ import { exec } from 'child_process';
6
+
7
+ const PORT = 7100;
8
+ const WORKSPACE = process.cwd();
9
+ const watchers = new Map();
10
+
11
+ // ─── Security: resolve path inside workspace ──────────────────────────────────
12
+ function safeResolvePath(filePath) {
13
+ const resolved = path.resolve(filePath);
14
+ // Allow absolute paths anywhere on the machine (needed for cross-project use)
15
+ // but log them for awareness
16
+ return resolved;
17
+ }
18
+
19
+ // ─── Ignored directories for readDir ─────────────────────────────────────────
20
+ const IGNORE = new Set([
21
+ 'node_modules', '.git', 'dist', 'build', '.next',
22
+ 'coverage', '.DS_Store', 'thumbs.db', '.turbo',
23
+ ]);
24
+
25
+ async function readDir(dirPath, recursive) {
26
+ let entries;
27
+ try {
28
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
29
+ } catch {
30
+ return [];
31
+ }
32
+
33
+ const result = [];
34
+ for (const entry of entries) {
35
+ if (IGNORE.has(entry.name)) continue;
36
+ const fullPath = path.join(dirPath, entry.name).replace(/\\/g, '/');
37
+ if (entry.isDirectory()) {
38
+ result.push({
39
+ name: entry.name,
40
+ path: fullPath,
41
+ type: 'directory',
42
+ children: recursive ? await readDir(path.join(dirPath, entry.name), true) : undefined,
43
+ });
44
+ } else {
45
+ result.push({ name: entry.name, path: fullPath, type: 'file' });
46
+ }
47
+ }
48
+ return result.sort((a, b) => {
49
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
50
+ return a.name.localeCompare(b.name);
51
+ });
52
+ }
53
+
54
+ // ─── WebSocket server ─────────────────────────────────────────────────────────
55
+ const wss = new WebSocketServer({ host: '127.0.0.1', port: PORT });
56
+
57
+ wss.on('connection', (ws) => {
58
+ console.log('[devlink] client connected');
59
+
60
+ ws.on('message', async (raw) => {
61
+ let msg;
62
+ try {
63
+ msg = JSON.parse(raw.toString());
64
+ } catch {
65
+ console.error('[devlink] invalid JSON message');
66
+ return;
67
+ }
68
+
69
+ switch (msg.type) {
70
+
71
+ // ── Handshake ────────────────────────────────────────────────────────────
72
+ case 'handshake': {
73
+ ws.send(JSON.stringify({
74
+ type: 'handshakeAck',
75
+ protocolVersion: '1.0',
76
+ workspaceRoot: WORKSPACE.replace(/\\/g, '/'),
77
+ }));
78
+ break;
79
+ }
80
+
81
+ // ── Read file ────────────────────────────────────────────────────────────
82
+ case 'readFile': {
83
+ try {
84
+ const resolved = safeResolvePath(msg.path);
85
+ const content = await fs.readFile(resolved, 'utf-8');
86
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: content }));
87
+ } catch (e) {
88
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: e.message }));
89
+ }
90
+ break;
91
+ }
92
+
93
+ // ── Write file ───────────────────────────────────────────────────────────
94
+ case 'writeFile': {
95
+ try {
96
+ const resolved = safeResolvePath(msg.path);
97
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
98
+ await fs.writeFile(resolved, msg.content, 'utf-8');
99
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: null }));
100
+ // Push back with writeId so browser can dedup its own save
101
+ ws.send(JSON.stringify({
102
+ type: 'fileChanged',
103
+ path: msg.path,
104
+ content: msg.content,
105
+ writeId: msg.writeId,
106
+ }));
107
+ } catch (e) {
108
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: e.message }));
109
+ }
110
+ break;
111
+ }
112
+
113
+ // ── Read directory ───────────────────────────────────────────────────────
114
+ case 'readDir': {
115
+ try {
116
+ const resolved = safeResolvePath(msg.path);
117
+ const entries = await readDir(resolved, msg.recursive ?? false);
118
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: entries }));
119
+ } catch (e) {
120
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: e.message }));
121
+ }
122
+ break;
123
+ }
124
+
125
+ // ── Watch file ───────────────────────────────────────────────────────────
126
+ case 'watchFile': {
127
+ const filePath = msg.path;
128
+ if (!watchers.has(filePath)) {
129
+ try {
130
+ const watcher = watch(filePath, async () => {
131
+ try {
132
+ const content = await fs.readFile(filePath, 'utf-8');
133
+ if (ws.readyState === ws.OPEN) {
134
+ ws.send(JSON.stringify({ type: 'fileChanged', path: filePath, content }));
135
+ }
136
+ } catch { /* file deleted */ }
137
+ });
138
+ watchers.set(filePath, watcher);
139
+ } catch (e) {
140
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: e.message }));
141
+ break;
142
+ }
143
+ }
144
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: null }));
145
+ break;
146
+ }
147
+
148
+ // ── Unwatch file ─────────────────────────────────────────────────────────
149
+ case 'unwatchFile': {
150
+ const watcher = watchers.get(msg.path);
151
+ if (watcher) {
152
+ watcher.close();
153
+ watchers.delete(msg.path);
154
+ }
155
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: null }));
156
+ break;
157
+ }
158
+
159
+ // ── Execute shell command ─────────────────────────────────────────────────
160
+ // Used by terminal tab and AI codex commands
161
+ case 'execCommand': {
162
+ const { command, cwd } = msg;
163
+
164
+ // Safety: block obviously destructive commands
165
+ const blocked = ['rm -rf /', 'format c:', 'del /f /s /q c:\\'];
166
+ const lower = (command || '').toLowerCase();
167
+ if (blocked.some(b => lower.includes(b))) {
168
+ ws.send(JSON.stringify({
169
+ type: 'response', id: msg.id, ok: true,
170
+ result: { stdout: '', stderr: 'Command blocked by devlink safety rules.', code: 1 },
171
+ }));
172
+ break;
173
+ }
174
+
175
+ const resolvedCwd = cwd ? safeResolvePath(cwd) : WORKSPACE;
176
+
177
+ exec(command, { cwd: resolvedCwd, timeout: 60000 }, (err, stdout, stderr) => {
178
+ if (ws.readyState !== ws.OPEN) return;
179
+ ws.send(JSON.stringify({
180
+ type: 'response', id: msg.id, ok: true,
181
+ result: {
182
+ stdout: stdout || '',
183
+ stderr: stderr || '',
184
+ error: err?.message || null,
185
+ code: err?.code ?? 0,
186
+ },
187
+ }));
188
+ });
189
+ break;
190
+ }
191
+
192
+ // ── AI autocomplete ───────────────────────────────────────────────────────
193
+ // Stub — returns empty so Monaco doesn't error.
194
+ // Replace with your model adapter call here.
195
+ case 'ai-autocomplete': {
196
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, result: '' }));
197
+ break;
198
+ }
199
+
200
+ // ── Ping / pong ───────────────────────────────────────────────────────────
201
+ case 'ping': {
202
+ ws.send(JSON.stringify({ type: 'pong', id: msg.id }));
203
+ break;
204
+ }
205
+
206
+ default: {
207
+ console.warn('[devlink] unknown message type:', msg.type);
208
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: `Unknown type: ${msg.type}` }));
209
+ }
210
+ }
211
+ });
212
+
213
+ ws.on('close', () => {
214
+ console.log('[devlink] client disconnected');
215
+ // Clean up watchers for this client
216
+ for (const [filePath, watcher] of watchers) {
217
+ watcher.close();
218
+ watchers.delete(filePath);
219
+ }
220
+ });
221
+
222
+ ws.on('error', (e) => {
223
+ console.error('[devlink] client error:', e.message);
224
+ });
225
+ });
226
+
227
+ wss.on('error', (e) => {
228
+ console.error('[devlink] server error:', e.message);
229
+ });
230
+
231
+ console.log(`[devlink] bridge running on ws://127.0.0.1:${PORT}`);
232
+ console.log(`[devlink] workspace: ${WORKSPACE}`);
233
+ console.log(`[devlink] execCommand handler: enabled`);
234
+ console.log(`[devlink] ai-autocomplete handler: stub (replace with model adapter)`);
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const bridgePackageRoot = path.dirname(__filename);
8
+ const projectRoot = process.cwd();
9
+ const packageJsonPath = path.join(projectRoot, 'package.json');
10
+
11
+ const log = (msg) => console.log(`[devlink setup] ${msg}`);
12
+ const warn = (msg) => console.warn(`[devlink setup] ⚠ ${msg}`);
13
+ const ok = (msg) => console.log(`[devlink setup] ✓ ${msg}`);
14
+
15
+ function readJson(p) { return JSON.parse(fs.readFileSync(p, 'utf8')); }
16
+ function writeJson(p,v) { fs.writeFileSync(p, `${JSON.stringify(v, null, 2)}\n`); }
17
+ function exists(p) { return fs.existsSync(p); }
18
+
19
+ function maybeRelativeDep(targetPath) {
20
+ if (!exists(targetPath)) return null;
21
+ return `file:${path.relative(projectRoot, targetPath).replace(/\\/g, '/')}`;
22
+ }
23
+
24
+ // ─── Detect framework ─────────────────────────────────────────────────────────
25
+
26
+ function detectFramework(pkg) {
27
+ const allDeps = {
28
+ ...pkg.dependencies,
29
+ ...pkg.devDependencies,
30
+ };
31
+ if (allDeps['react-scripts']) return 'cra';
32
+ if (allDeps['next']) return 'next';
33
+ if (allDeps['vite']) return 'vite';
34
+ if (allDeps['@vitejs/plugin-react']) return 'vite';
35
+ return 'unknown';
36
+ }
37
+
38
+ // ─── Update package.json ──────────────────────────────────────────────────────
39
+
40
+ function updatePackageJson() {
41
+ if (!exists(packageJsonPath)) {
42
+ throw new Error(`package.json not found in ${projectRoot}`);
43
+ }
44
+
45
+ const pkg = readJson(packageJsonPath);
46
+ pkg.scripts = pkg.scripts || {};
47
+ pkg.dependencies = pkg.dependencies || {};
48
+ pkg.devDependencies = pkg.devDependencies || {};
49
+
50
+ let changed = false;
51
+
52
+ // Add bridge scripts
53
+ if (!pkg.scripts['devlink:bridge']) {
54
+ pkg.scripts['devlink:bridge'] = 'node node_modules/devlink-bridge/server.mjs';
55
+ changed = true; log('added script devlink:bridge');
56
+ }
57
+ if (!pkg.scripts['devlink:setup']) {
58
+ pkg.scripts['devlink:setup'] = 'node node_modules/devlink-bridge/setup-devlink.mjs';
59
+ changed = true; log('added script devlink:setup');
60
+ }
61
+
62
+ const alphaRoot = path.resolve(bridgePackageRoot, '..');
63
+ const deps = {
64
+ 'devlink-bridge': maybeRelativeDep(bridgePackageRoot),
65
+ 'devlink-studio': maybeRelativeDep(path.join(alphaRoot, 'devlink-v2', 'packages', 'devlink-studio')),
66
+ 'devlink-babel-plugin': maybeRelativeDep(path.join(alphaRoot, 'devlink-babel-plugin')),
67
+ 'devlink-vite-plugin': maybeRelativeDep(path.join(alphaRoot, 'devlink-vite-plugin')),
68
+ };
69
+
70
+ if (!pkg.dependencies['devlink-bridge'] && deps['devlink-bridge']) {
71
+ pkg.dependencies['devlink-bridge'] = deps['devlink-bridge'];
72
+ changed = true; log('added dependency devlink-bridge');
73
+ }
74
+ if (!pkg.dependencies['devlink-studio'] && deps['devlink-studio']) {
75
+ pkg.dependencies['devlink-studio'] = deps['devlink-studio'];
76
+ changed = true; log('added dependency devlink-studio');
77
+ }
78
+ if (!pkg.devDependencies['devlink-babel-plugin'] && deps['devlink-babel-plugin']) {
79
+ pkg.devDependencies['devlink-babel-plugin'] = deps['devlink-babel-plugin'];
80
+ changed = true; log('added devDependency devlink-babel-plugin');
81
+ }
82
+ if (!pkg.devDependencies['devlink-vite-plugin'] && deps['devlink-vite-plugin']) {
83
+ pkg.devDependencies['devlink-vite-plugin'] = deps['devlink-vite-plugin'];
84
+ changed = true; log('added devDependency devlink-vite-plugin');
85
+ }
86
+
87
+ if (changed) writeJson(packageJsonPath, pkg);
88
+ else log('package.json already up to date');
89
+
90
+ return pkg;
91
+ }
92
+
93
+ // ─── CRA setup ───────────────────────────────────────────────────────────────
94
+
95
+ function setupCra() {
96
+ const configPath = path.join(projectRoot, 'node_modules', 'react-scripts', 'config', 'webpack.config.js');
97
+ if (!exists(configPath)) {
98
+ warn('react-scripts webpack config not found. Run npm install first, then npm run devlink:setup again.');
99
+ return false;
100
+ }
101
+
102
+ let src = fs.readFileSync(configPath, 'utf8');
103
+ let changed = false;
104
+
105
+ if (!src.includes("require.resolve('devlink-babel-plugin')")) {
106
+ const marker = "const createEnvironmentHash = require('./webpack/persistentCache/createEnvironmentHash');";
107
+ if (src.includes(marker)) {
108
+ src = src.replace(marker, `${marker}\nconst devlinkBabelPlugin = require.resolve('devlink-babel-plugin');`);
109
+ changed = true;
110
+ }
111
+ }
112
+
113
+ if (!src.includes('devlinkBabelPlugin,')) {
114
+ const reactRefreshLine = " isEnvDevelopment &&\n shouldUseReactRefresh &&\n require.resolve('react-refresh/babel'),";
115
+ if (src.includes(reactRefreshLine)) {
116
+ const block =
117
+ " isEnvDevelopment && [\n" +
118
+ " devlinkBabelPlugin,\n" +
119
+ " { root: paths.appPath, envs: ['development'] },\n" +
120
+ " ],\n";
121
+ src = src.replace(reactRefreshLine, `${block}${reactRefreshLine}`);
122
+ changed = true;
123
+ }
124
+ }
125
+
126
+ if (changed) { fs.writeFileSync(configPath, src); ok('patched CRA webpack config'); }
127
+ else { ok('CRA webpack config already patched'); }
128
+ return true;
129
+ }
130
+
131
+ // ─── Vite setup ───────────────────────────────────────────────────────────────
132
+
133
+ function setupVite() {
134
+ // Find vite config — could be .ts, .js, .mts, .mjs
135
+ const candidates = [
136
+ 'vite.config.ts', 'vite.config.js',
137
+ 'vite.config.mts', 'vite.config.mjs',
138
+ ];
139
+ const configFile = candidates.find(f => exists(path.join(projectRoot, f)));
140
+
141
+ if (!configFile) {
142
+ warn('No vite.config file found. Creating vite.config.ts with devlink config...');
143
+ createViteConfig('vite.config.ts');
144
+ return true;
145
+ }
146
+
147
+ const configPath = path.join(projectRoot, configFile);
148
+ let src = fs.readFileSync(configPath, 'utf8');
149
+
150
+ if (src.includes('devlink-babel-plugin') || src.includes('devlink-vite-plugin')) {
151
+ ok('vite.config already has devlink config');
152
+ return true;
153
+ }
154
+
155
+ // Patch existing vite config
156
+ const isTs = configFile.endsWith('.ts') || configFile.endsWith('.mts');
157
+
158
+ // Add import at top
159
+ const importLine = `import { devlinkBabelConfig } from 'devlink-vite-plugin';\n`;
160
+ if (!src.includes("from 'devlink-vite-plugin'")) {
161
+ // Insert after last existing import
162
+ const lastImportIdx = src.lastIndexOf('\nimport ');
163
+ const insertAt = lastImportIdx === -1 ? 0 : src.indexOf('\n', lastImportIdx) + 1;
164
+ src = src.slice(0, insertAt) + importLine + src.slice(insertAt);
165
+ }
166
+
167
+ // Patch react() plugin call to add babel config
168
+ // Handles: react() and react({ ... })
169
+ if (src.includes('react()')) {
170
+ src = src.replace(
171
+ 'react()',
172
+ `react({\n babel: {\n plugins: [devlinkBabelConfig({ root: process.cwd() })].filter(Boolean),\n },\n })`
173
+ );
174
+ ok('patched react() in vite.config');
175
+ } else if (src.match(/react\(\s*\{/)) {
176
+ // react({ already has options — add babel inside
177
+ src = src.replace(
178
+ /react\(\s*\{/,
179
+ `react({\n babel: {\n plugins: [devlinkBabelConfig({ root: process.cwd() })].filter(Boolean),\n },\n `
180
+ );
181
+ ok('patched react({ in vite.config');
182
+ } else {
183
+ // Can't auto-patch safely — append comment instructions
184
+ src += `\n// TODO: add devlink babel plugin to your react() config:\n`;
185
+ src += `// import { devlinkBabelConfig } from 'devlink-vite-plugin'\n`;
186
+ src += `// react({ babel: { plugins: [devlinkBabelConfig({ root: process.cwd() })] } })\n`;
187
+ warn('Could not auto-patch vite.config. See TODO comment added at bottom of file.');
188
+ }
189
+
190
+ fs.writeFileSync(configPath, src);
191
+ ok(`patched ${configFile}`);
192
+ return true;
193
+ }
194
+
195
+ function createViteConfig(filename) {
196
+ const content = `import { defineConfig } from 'vite';
197
+ import react from '@vitejs/plugin-react';
198
+ import { devlinkBabelConfig } from 'devlink-vite-plugin';
199
+
200
+ export default defineConfig({
201
+ plugins: [
202
+ react({
203
+ babel: {
204
+ plugins: [
205
+ devlinkBabelConfig({ root: process.cwd() }),
206
+ ].filter(Boolean),
207
+ },
208
+ }),
209
+ ],
210
+ });
211
+ `;
212
+ fs.writeFileSync(path.join(projectRoot, filename), content);
213
+ ok(`created ${filename} with devlink config`);
214
+ }
215
+
216
+ // ─── Next.js setup ────────────────────────────────────────────────────────────
217
+
218
+ function setupNext() {
219
+ const candidates = ['next.config.ts', 'next.config.js', 'next.config.mjs'];
220
+ const configFile = candidates.find(f => exists(path.join(projectRoot, f)));
221
+
222
+ if (!configFile) {
223
+ warn('No next.config file found. Creating next.config.js with devlink config...');
224
+ createNextConfig('next.config.js');
225
+ return true;
226
+ }
227
+
228
+ const configPath = path.join(projectRoot, configFile);
229
+ let src = fs.readFileSync(configPath, 'utf8');
230
+
231
+ if (src.includes('devlink-babel-plugin')) {
232
+ ok('next.config already has devlink config');
233
+ return true;
234
+ }
235
+
236
+ // Next.js uses .babelrc or babel.config.js to add babel plugins
237
+ // We patch/create .babelrc
238
+ const babelRcPath = path.join(projectRoot, '.babelrc');
239
+ if (exists(babelRcPath)) {
240
+ const babelRc = readJson(babelRcPath);
241
+ const devEnv = babelRc.env?.development || {};
242
+ devEnv.plugins = devEnv.plugins || [];
243
+ if (!devEnv.plugins.some(p => (Array.isArray(p) ? p[0] : p) === 'devlink-babel-plugin')) {
244
+ devEnv.plugins.push(['devlink-babel-plugin', { root: '.', envs: ['development'] }]);
245
+ babelRc.env = babelRc.env || {};
246
+ babelRc.env.development = devEnv;
247
+ writeJson(babelRcPath, babelRc);
248
+ ok('patched .babelrc for Next.js');
249
+ } else {
250
+ ok('.babelrc already has devlink config');
251
+ }
252
+ } else {
253
+ // Create .babelrc
254
+ const babelRc = {
255
+ presets: ['next/babel'],
256
+ env: {
257
+ development: {
258
+ plugins: [
259
+ ['devlink-babel-plugin', { root: '.', envs: ['development'] }]
260
+ ]
261
+ }
262
+ }
263
+ };
264
+ writeJson(babelRcPath, babelRc);
265
+ ok('created .babelrc for Next.js with devlink config');
266
+ warn('Adding .babelrc disables Next.js SWC compiler. Only use in development.');
267
+ }
268
+
269
+ return true;
270
+ }
271
+
272
+ function createNextConfig(filename) {
273
+ const content = `/** @type {import('next').NextConfig} */
274
+ const nextConfig = {};
275
+ module.exports = nextConfig;
276
+ `;
277
+ fs.writeFileSync(path.join(projectRoot, filename), content);
278
+ }
279
+
280
+ // ─── Generate devlink.config.ts in user's project ─────────────────────────────
281
+
282
+ function generateDevlinkConfig(framework) {
283
+ const configPath = path.join(projectRoot, 'devlink.config.ts');
284
+ if (exists(configPath)) { ok('devlink.config.ts already exists'); return; }
285
+
286
+ // Try to auto-detect project src folder
287
+ const srcDir = exists(path.join(projectRoot, 'src')) ? './src' : '.';
288
+ const winRoot = projectRoot.replace(/\\/g, '/');
289
+
290
+ const content = `// devlink.config.ts
291
+ // Auto-generated by devlink setup. Edit projectRoot and rootPath for your project.
292
+
293
+ export const devlinkConfig = {
294
+ /** Absolute path to your project root */
295
+ projectRoot: '${winRoot}',
296
+
297
+ /** Path shown in the file tree — usually projectRoot/src */
298
+ rootPath: '${winRoot}/${srcDir.replace('./', '')}',
299
+
300
+ /** Start editor docked to the right side */
301
+ defaultDocked: true,
302
+
303
+ /** Width of the docked editor panel in px */
304
+ dockedWidth: 460,
305
+ };
306
+ `;
307
+ fs.writeFileSync(configPath, content);
308
+ ok('created devlink.config.ts — edit projectRoot if needed');
309
+ }
310
+
311
+ // ─── Generate main devlink entry patch instructions ───────────────────────────
312
+
313
+ function printMainInstructions(framework) {
314
+ console.log('');
315
+ console.log('─────────────────────────────────────────────────────');
316
+ console.log('[devlink setup] Final step — update your entry file:');
317
+ console.log('─────────────────────────────────────────────────────');
318
+ console.log('');
319
+ console.log('In your main.tsx / main.jsx / _app.tsx:');
320
+ console.log('');
321
+ if (framework === 'next') {
322
+ console.log(` // pages/_app.tsx
323
+ import { DevlinkBridge } from 'devlink-bridge'
324
+ import { DevlinkStudio } from 'devlink-studio'
325
+ import devlinkConfig from '../devlink.config'
326
+
327
+ const bridge = new DevlinkBridge()
328
+
329
+ export default function App({ Component, pageProps }) {
330
+ return (
331
+ <DevlinkStudio bridge={bridge} {...devlinkConfig}>
332
+ <Component {...pageProps} />
333
+ </DevlinkStudio>
334
+ )
335
+ }`);
336
+ } else {
337
+ console.log(` import { DevlinkBridge } from 'devlink-bridge'
338
+ import { DevlinkStudio } from 'devlink-studio'
339
+ import { devlinkConfig } from '../devlink.config'
340
+
341
+ const bridge = new DevlinkBridge()
342
+
343
+ function Root() {
344
+ return (
345
+ <DevlinkStudio bridge={bridge} {...devlinkConfig}>
346
+ <App />
347
+ </DevlinkStudio>
348
+ )
349
+ }`);
350
+ }
351
+ console.log('');
352
+ console.log('Then start the bridge server in a separate terminal:');
353
+ console.log(' npm run devlink:bridge');
354
+ console.log('');
355
+ console.log('─────────────────────────────────────────────────────');
356
+ }
357
+
358
+ // ─── Main ─────────────────────────────────────────────────────────────────────
359
+
360
+ function main() {
361
+ log(`configuring ${projectRoot}`);
362
+
363
+ const pkg = updatePackageJson();
364
+ const framework = detectFramework(pkg);
365
+ log(`detected framework: ${framework}`);
366
+
367
+ let configured = false;
368
+ if (framework === 'cra') configured = setupCra();
369
+ else if (framework === 'vite') configured = setupVite();
370
+ else if (framework === 'next') configured = setupNext();
371
+ else {
372
+ warn('Could not detect framework (CRA / Vite / Next.js).');
373
+ warn('Add devlink-babel-plugin manually to your build config.');
374
+ warn('See: https://github.com/your-org/devlink#manual-setup');
375
+ }
376
+
377
+ generateDevlinkConfig(framework);
378
+ printMainInstructions(framework);
379
+
380
+ ok('setup complete');
381
+ }
382
+
383
+ try {
384
+ main();
385
+ } catch (err) {
386
+ console.error(`[devlink setup] failed: ${err.message}`);
387
+ process.exitCode = 1;
388
+ }