scanwarp 0.2.0 → 0.3.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.
@@ -0,0 +1,290 @@
1
+ "use strict";
2
+ /**
3
+ * MCP server for scanwarp dev mode.
4
+ *
5
+ * Connects to the running scanwarp dev instance via its local HTTP API
6
+ * and exposes monitoring data to AI coding tools (Cursor, Claude Code).
7
+ *
8
+ * Transport: stdio (Cursor and Claude Code connect via command execution).
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.startDevMcpServer = startDevMcpServer;
12
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
13
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
14
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
15
+ // ─── HTTP client for the dev instance API ───
16
+ async function fetchDevApi(port, path) {
17
+ const response = await fetch(`http://localhost:${port}${path}`);
18
+ if (!response.ok) {
19
+ throw new Error(`Dev API returned ${response.status}: ${await response.text()}`);
20
+ }
21
+ return response.json();
22
+ }
23
+ // ─── Tool implementations ───
24
+ function formatUptime(ms) {
25
+ const seconds = Math.floor(ms / 1000);
26
+ if (seconds < 60)
27
+ return `${seconds} seconds`;
28
+ const minutes = Math.floor(seconds / 60);
29
+ if (minutes < 60)
30
+ return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
31
+ const hours = Math.floor(minutes / 60);
32
+ const remainingMinutes = minutes % 60;
33
+ return `${hours}h ${remainingMinutes}m`;
34
+ }
35
+ async function getDevStatus(port) {
36
+ const data = await fetchDevApi(port, '/api/status');
37
+ const lines = [];
38
+ lines.push(`## Dev Mode Status\n`);
39
+ lines.push(`**Uptime:** ${formatUptime(data.uptime_ms)}`);
40
+ lines.push(`**Routes:** ${data.total_routes} total (${data.pages} pages, ${data.api_routes} API routes)`);
41
+ lines.push(`**Traces:** ${data.total_traces} traces, ${data.total_spans} spans`);
42
+ if (data.active_issues > 0) {
43
+ lines.push(`**Issues:** ${data.active_issues} active issues detected`);
44
+ }
45
+ else {
46
+ lines.push(`**Issues:** None detected`);
47
+ }
48
+ if (data.error_count > 0) {
49
+ lines.push(`**Errors:** ${data.error_count} errors recorded`);
50
+ }
51
+ const health = data.active_issues === 0 && data.error_count === 0 ? 'Healthy' : 'Issues detected';
52
+ lines.push(`\n**Overall:** ${health}`);
53
+ return lines.join('\n');
54
+ }
55
+ async function getDevIssues(port) {
56
+ const data = await fetchDevApi(port, '/api/issues');
57
+ if (data.issues.length === 0) {
58
+ return 'No active issues detected. Your app is running clean.';
59
+ }
60
+ const lines = [];
61
+ lines.push(`## Active Issues (${data.issues.length})\n`);
62
+ for (let i = 0; i < data.issues.length; i++) {
63
+ const issue = data.issues[i];
64
+ const icon = issue.severity === 'error' ? '🔴' : issue.severity === 'warning' ? '🟡' : '🔵';
65
+ lines.push(`### ${i + 1}. ${icon} [${issue.severity.toUpperCase()}] ${issue.message}`);
66
+ lines.push(`**Rule:** ${issue.rule}`);
67
+ if (issue.detail) {
68
+ lines.push(`**Detail:** ${issue.detail}`);
69
+ }
70
+ if (issue.suggestion) {
71
+ lines.push(`**Suggestion:** ${issue.suggestion}`);
72
+ }
73
+ lines.push('');
74
+ }
75
+ return lines.join('\n');
76
+ }
77
+ async function getDevRoutes(port) {
78
+ const data = await fetchDevApi(port, '/api/routes');
79
+ if (data.routes.length === 0) {
80
+ return 'No routes discovered. Make sure your dev server is running.';
81
+ }
82
+ const lines = [];
83
+ lines.push(`## Discovered Routes (${data.routes.length})\n`);
84
+ const statusIcon = {
85
+ healthy: '✅',
86
+ error: '❌',
87
+ slow: '⚠️',
88
+ unknown: '❓',
89
+ };
90
+ // Group by type
91
+ const pages = data.routes.filter((r) => r.type === 'page');
92
+ const apiRoutes = data.routes.filter((r) => r.type === 'api');
93
+ if (pages.length > 0) {
94
+ lines.push(`### Pages (${pages.length})\n`);
95
+ lines.push('| Route | Status | Response Time | Baseline |');
96
+ lines.push('|-------|--------|--------------|----------|');
97
+ for (const r of pages) {
98
+ const icon = statusIcon[r.status] || '❓';
99
+ const time = r.last_time_ms !== null ? `${r.last_time_ms}ms` : '-';
100
+ const baseline = r.baseline_ms !== null ? `${r.baseline_ms}ms` : '-';
101
+ const errNote = r.error_text ? ` (${r.error_text})` : '';
102
+ lines.push(`| ${r.path} | ${icon} ${r.status}${errNote} | ${time} | ${baseline} |`);
103
+ }
104
+ lines.push('');
105
+ }
106
+ if (apiRoutes.length > 0) {
107
+ lines.push(`### API Routes (${apiRoutes.length})\n`);
108
+ lines.push('| Route | Status | Response Time | Baseline |');
109
+ lines.push('|-------|--------|--------------|----------|');
110
+ for (const r of apiRoutes) {
111
+ const icon = statusIcon[r.status] || '❓';
112
+ const time = r.last_time_ms !== null ? `${r.last_time_ms}ms` : '-';
113
+ const baseline = r.baseline_ms !== null ? `${r.baseline_ms}ms` : '-';
114
+ const errNote = r.error_text ? ` (${r.error_text})` : '';
115
+ lines.push(`| ${r.path} | ${icon} ${r.status}${errNote} | ${time} | ${baseline} |`);
116
+ }
117
+ lines.push('');
118
+ }
119
+ return lines.join('\n');
120
+ }
121
+ async function getSlowRoutes(port) {
122
+ const data = await fetchDevApi(port, '/api/slow-routes');
123
+ if (data.slow_routes.length === 0) {
124
+ return 'No slow routes detected. All routes are within their baseline performance.';
125
+ }
126
+ const lines = [];
127
+ lines.push(`## Slow Routes (${data.slow_routes.length})\n`);
128
+ for (const r of data.slow_routes) {
129
+ lines.push(`### ⚠️ ${r.path}`);
130
+ lines.push(`- **Current:** ${r.current_ms}ms`);
131
+ lines.push(`- **Baseline:** ${r.baseline_ms}ms`);
132
+ lines.push(`- **Slowdown:** ${r.ratio}x slower`);
133
+ if (r.bottleneck) {
134
+ lines.push(`- **Bottleneck:** \`${r.bottleneck.name}\` (${r.bottleneck.duration_ms}ms)`);
135
+ }
136
+ lines.push('');
137
+ }
138
+ return lines.join('\n');
139
+ }
140
+ async function getRouteTraces(port, routePath, limit) {
141
+ const params = new URLSearchParams({ path: routePath, limit: String(limit) });
142
+ const data = await fetchDevApi(port, `/api/route-traces?${params}`);
143
+ if (data.traces.length === 0) {
144
+ return `No traces found for route \`${routePath}\`. The route may not have been hit yet.`;
145
+ }
146
+ const lines = [];
147
+ lines.push(`## Traces for \`${routePath}\` (${data.traces.length} most recent)\n`);
148
+ for (const trace of data.traces) {
149
+ const time = new Date(trace.timestamp).toLocaleTimeString();
150
+ const statusIcon = (trace.status_code && trace.status_code < 400) ? '✅' : '❌';
151
+ lines.push(`### ${statusIcon} ${trace.method} ${trace.route} — ${trace.duration_ms}ms (${time})`);
152
+ lines.push(`Trace ID: \`${trace.trace_id}\`\n`);
153
+ // Build waterfall
154
+ lines.push('```');
155
+ renderWaterfall(trace.spans, null, 0, lines, trace.spans[0]?.attributes?.['start_time'] || 0);
156
+ lines.push('```');
157
+ lines.push('');
158
+ }
159
+ return lines.join('\n');
160
+ }
161
+ function renderWaterfall(spans, parentId, depth, lines, _baseTime) {
162
+ const children = spans.filter((s) => s.parent_span_id === parentId);
163
+ for (const span of children) {
164
+ const indent = ' '.repeat(depth);
165
+ const statusMark = span.status === 'ERROR' ? '✗' : '·';
166
+ const kindLabel = span.kind !== 'INTERNAL' ? ` [${span.kind}]` : '';
167
+ const dbStmt = span.attributes['db.statement'];
168
+ const detail = typeof dbStmt === 'string' ? ` — ${dbStmt.substring(0, 80)}` : '';
169
+ lines.push(`${indent}${statusMark} ${span.operation}${kindLabel} ${span.duration_ms}ms (${span.service})${detail}`);
170
+ renderWaterfall(spans, span.span_id, depth + 1, lines, _baseTime);
171
+ }
172
+ }
173
+ // ─── MCP Server setup ───
174
+ async function startDevMcpServer(port) {
175
+ // Verify connection to dev instance
176
+ try {
177
+ await fetchDevApi(port, '/health');
178
+ }
179
+ catch {
180
+ console.error(`Error: Cannot connect to ScanWarp dev server on port ${port}`);
181
+ console.error(`Make sure 'scanwarp dev' is running first.`);
182
+ process.exit(1);
183
+ }
184
+ const server = new index_js_1.Server({
185
+ name: 'scanwarp-dev',
186
+ version: '0.1.0',
187
+ }, {
188
+ capabilities: {
189
+ tools: {},
190
+ },
191
+ });
192
+ // Register tools
193
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
194
+ return {
195
+ tools: [
196
+ {
197
+ name: 'get_dev_status',
198
+ description: 'Get the current status of the ScanWarp dev mode monitoring — health, route count, issue count, uptime. Call this first to get an overview.',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {},
202
+ },
203
+ },
204
+ {
205
+ name: 'get_dev_issues',
206
+ description: 'Get all active issues detected by the real-time analyzers — N+1 queries, unhandled errors, slow queries, missing error handling, slow external calls. Each issue includes severity, message, and a suggested fix.',
207
+ inputSchema: {
208
+ type: 'object',
209
+ properties: {},
210
+ },
211
+ },
212
+ {
213
+ name: 'get_dev_routes',
214
+ description: 'Get all discovered routes with their current status (healthy/error/slow), last response time, and baseline comparison.',
215
+ inputSchema: {
216
+ type: 'object',
217
+ properties: {},
218
+ },
219
+ },
220
+ {
221
+ name: 'get_slow_routes',
222
+ description: 'Get routes that are currently slower than their baseline, with bottleneck span info from traces. Useful for finding performance regressions.',
223
+ inputSchema: {
224
+ type: 'object',
225
+ properties: {},
226
+ },
227
+ },
228
+ {
229
+ name: 'get_route_traces',
230
+ description: 'Get recent traces for a specific route with the full span waterfall. Shows exactly what happens when a request hits a route — database queries, external API calls, processing time, errors.',
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {
234
+ path: {
235
+ type: 'string',
236
+ description: 'The route path to get traces for (e.g., "/api/products", "/")',
237
+ },
238
+ limit: {
239
+ type: 'number',
240
+ description: 'Maximum number of recent traces to return (default: 5)',
241
+ },
242
+ },
243
+ required: ['path'],
244
+ },
245
+ },
246
+ ],
247
+ };
248
+ });
249
+ // Handle tool calls
250
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
251
+ const { name, arguments: args } = request.params;
252
+ try {
253
+ switch (name) {
254
+ case 'get_dev_status': {
255
+ const result = await getDevStatus(port);
256
+ return { content: [{ type: 'text', text: result }] };
257
+ }
258
+ case 'get_dev_issues': {
259
+ const result = await getDevIssues(port);
260
+ return { content: [{ type: 'text', text: result }] };
261
+ }
262
+ case 'get_dev_routes': {
263
+ const result = await getDevRoutes(port);
264
+ return { content: [{ type: 'text', text: result }] };
265
+ }
266
+ case 'get_slow_routes': {
267
+ const result = await getSlowRoutes(port);
268
+ return { content: [{ type: 'text', text: result }] };
269
+ }
270
+ case 'get_route_traces': {
271
+ const { path: routePath, limit } = args;
272
+ const result = await getRouteTraces(port, routePath, limit ?? 5);
273
+ return { content: [{ type: 'text', text: result }] };
274
+ }
275
+ default:
276
+ throw new Error(`Unknown tool: ${name}`);
277
+ }
278
+ }
279
+ catch (error) {
280
+ const errorMessage = error instanceof Error ? error.message : String(error);
281
+ return {
282
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
283
+ isError: true,
284
+ };
285
+ }
286
+ });
287
+ const transport = new stdio_js_1.StdioServerTransport();
288
+ await server.connect(transport);
289
+ console.error('ScanWarp dev MCP server running on stdio');
290
+ }
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  "use strict";
2
3
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
4
  if (k2 === undefined) k2 = k;
@@ -14,6 +15,143 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
15
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
16
  };
16
17
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./types.js"), exports);
18
- __exportStar(require("./diagnoser.js"), exports);
19
- __exportStar(require("./correlator.js"), exports);
18
+ const commander_1 = require("commander");
19
+ const init_js_1 = require("./commands/init.js");
20
+ const status_js_1 = require("./commands/status.js");
21
+ const logs_js_1 = require("./commands/logs.js");
22
+ const dev_js_1 = require("./commands/dev.js");
23
+ const dev_mcp_js_1 = require("./commands/dev-mcp.js");
24
+ const server_js_1 = require("./commands/server.js");
25
+ const mcp_js_1 = require("./commands/mcp.js");
26
+ const config_js_1 = require("./config.js");
27
+ const program = new commander_1.Command();
28
+ program
29
+ .name('scanwarp')
30
+ .description('Your AI writes your code. ScanWarp keeps it running.')
31
+ .version('0.3.0');
32
+ program
33
+ .command('init')
34
+ .description('Initialize ScanWarp monitoring for your project')
35
+ .option('-s, --server <url>', 'ScanWarp server URL')
36
+ .option('-u, --url <url>', 'Production URL to monitor')
37
+ .option('--skip-vercel', 'Skip Vercel integration setup')
38
+ .option('--skip-mcp', 'Skip MCP configuration')
39
+ .option('--skip-instrumentation', 'Skip production instrumentation setup')
40
+ .action(async (options) => {
41
+ try {
42
+ // Use config as fallback for server URL
43
+ options.server = options.server || config_js_1.config.getServerUrl();
44
+ await (0, init_js_1.initCommand)(options);
45
+ }
46
+ catch (error) {
47
+ console.error('Error:', error instanceof Error ? error.message : error);
48
+ process.exit(1);
49
+ }
50
+ });
51
+ program
52
+ .command('status')
53
+ .description('Check monitoring status')
54
+ .option('-s, --server <url>', 'ScanWarp server URL')
55
+ .action(async (options) => {
56
+ try {
57
+ options.server = options.server || config_js_1.config.getServerUrl();
58
+ await (0, status_js_1.statusCommand)(options);
59
+ }
60
+ catch (error) {
61
+ console.error('Error:', error instanceof Error ? error.message : error);
62
+ process.exit(1);
63
+ }
64
+ });
65
+ program
66
+ .command('logs')
67
+ .alias('events')
68
+ .description('View recent events')
69
+ .option('-s, --server <url>', 'ScanWarp server URL')
70
+ .option('-f, --follow', 'Follow log output (live streaming)')
71
+ .option('-t, --type <type>', 'Filter by event type (error/slow/down/up)')
72
+ .option('--source <source>', 'Filter by source (monitor/vercel/stripe/supabase/github)')
73
+ .option('-l, --limit <number>', 'Number of events to show', '50')
74
+ .action(async (options) => {
75
+ try {
76
+ options.server = options.server || config_js_1.config.getServerUrl();
77
+ options.limit = parseInt(options.limit);
78
+ await (0, logs_js_1.logsCommand)(options);
79
+ }
80
+ catch (error) {
81
+ console.error('Error:', error instanceof Error ? error.message : error);
82
+ process.exit(1);
83
+ }
84
+ });
85
+ program
86
+ .command('dev')
87
+ .description('Start your dev server with ScanWarp monitoring')
88
+ .option('-c, --command <cmd>', 'Dev server command to run (auto-detected if omitted)')
89
+ .option('-p, --port <number>', 'Port for local ScanWarp server')
90
+ .action(async (options) => {
91
+ try {
92
+ if (options.port)
93
+ options.port = parseInt(options.port);
94
+ await (0, dev_js_1.devCommand)(options);
95
+ }
96
+ catch (error) {
97
+ console.error('Error:', error instanceof Error ? error.message : error);
98
+ process.exit(1);
99
+ }
100
+ });
101
+ program
102
+ .command('dev-mcp')
103
+ .description('Start MCP server for AI coding tools (connects to running scanwarp dev)')
104
+ .option('-p, --port <number>', 'Port of the running ScanWarp dev server', '3456')
105
+ .action(async (options) => {
106
+ try {
107
+ if (options.port)
108
+ options.port = parseInt(options.port);
109
+ await (0, dev_mcp_js_1.devMcpCommand)(options);
110
+ }
111
+ catch (error) {
112
+ console.error('Error:', error instanceof Error ? error.message : error);
113
+ process.exit(1);
114
+ }
115
+ });
116
+ program
117
+ .command('server')
118
+ .alias('serve')
119
+ .description('Start ScanWarp server locally with SQLite (no Docker required)')
120
+ .option('-p, --port <number>', 'Port to listen on', '3000')
121
+ .option('--db-path <path>', 'SQLite database file path')
122
+ .action(async (options) => {
123
+ try {
124
+ if (options.port)
125
+ options.port = parseInt(options.port);
126
+ await (0, server_js_1.serverCommand)(options);
127
+ }
128
+ catch (error) {
129
+ console.error('Error:', error instanceof Error ? error.message : error);
130
+ process.exit(1);
131
+ }
132
+ });
133
+ program
134
+ .command('incidents')
135
+ .description('View open incidents')
136
+ .option('-s, --server <url>', 'ScanWarp server URL', 'http://localhost:3000')
137
+ .action(() => {
138
+ console.log('Incidents command coming soon...');
139
+ });
140
+ program
141
+ .command('mcp')
142
+ .description('Start MCP server for AI coding tools (production monitoring)')
143
+ .option('-s, --server <url>', 'ScanWarp server URL')
144
+ .option('-t, --token <token>', 'API token for authentication')
145
+ .option('-p, --project <id>', 'Default project ID')
146
+ .action(async (options) => {
147
+ try {
148
+ await (0, mcp_js_1.mcpCommand)(options);
149
+ }
150
+ catch (error) {
151
+ console.error('Error:', error instanceof Error ? error.message : error);
152
+ process.exit(1);
153
+ }
154
+ });
155
+ program.parse();
156
+ // Re-export from @scanwarp/core for backward compatibility
157
+ __exportStar(require("@scanwarp/core"), exports);
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setupInstrumentation = setupInstrumentation;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const ora_1 = __importDefault(require("ora"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const child_process_1 = require("child_process");
13
+ const NEXTJS_INSTRUMENTATION = `export async function register() {
14
+ if (process.env.NEXT_RUNTIME === "nodejs") {
15
+ await import("@scanwarp/instrument");
16
+ }
17
+ }
18
+ `;
19
+ async function setupInstrumentation(detected, serverUrl, projectId, shouldPrompt = true) {
20
+ let enableTracing = true;
21
+ if (shouldPrompt) {
22
+ const response = await inquirer_1.default.prompt([
23
+ {
24
+ type: 'confirm',
25
+ name: 'enableTracing',
26
+ message: 'Enable request tracing? (recommended)',
27
+ default: true,
28
+ },
29
+ ]);
30
+ enableTracing = response.enableTracing;
31
+ }
32
+ if (!enableTracing) {
33
+ console.log(chalk_1.default.gray(' Skipped\n'));
34
+ return;
35
+ }
36
+ console.log(chalk_1.default.gray(' Setting up production instrumentation for monitoring...\n'));
37
+ const cwd = process.cwd();
38
+ const isNextJs = detected.framework === 'Next.js';
39
+ const packageManager = detectPackageManager(cwd);
40
+ const serviceName = detected.projectName || 'my-app';
41
+ // Step 1: Install @scanwarp/instrument
42
+ const installSpinner = (0, ora_1.default)('Installing @scanwarp/instrument...').start();
43
+ try {
44
+ const installCmd = getInstallCommand(packageManager);
45
+ (0, child_process_1.execSync)(installCmd, { cwd, stdio: 'pipe' });
46
+ installSpinner.succeed('Installed @scanwarp/instrument');
47
+ }
48
+ catch {
49
+ installSpinner.fail('Failed to install @scanwarp/instrument');
50
+ console.log(chalk_1.default.yellow(' Run manually:'));
51
+ console.log(chalk_1.default.cyan(` ${getInstallCommand(packageManager)}\n`));
52
+ }
53
+ // Step 2: Framework-specific setup
54
+ if (isNextJs) {
55
+ await setupNextJs(cwd);
56
+ }
57
+ else {
58
+ setupGenericNode();
59
+ }
60
+ // Step 3: Add env vars to .env.local or .env
61
+ const envPath = path_1.default.join(cwd, '.env');
62
+ const envLocalPath = path_1.default.join(cwd, '.env.local');
63
+ const targetEnv = fs_1.default.existsSync(envLocalPath) ? envLocalPath : envPath;
64
+ const envVars = {
65
+ SCANWARP_PROJECT_ID: projectId,
66
+ SCANWARP_SERVER: serverUrl,
67
+ SCANWARP_SERVICE_NAME: serviceName,
68
+ };
69
+ console.log(chalk_1.default.bold('\n Environment variables:\n'));
70
+ for (const [key, val] of Object.entries(envVars)) {
71
+ console.log(chalk_1.default.cyan(` ${key}=${val}`));
72
+ }
73
+ if (fs_1.default.existsSync(targetEnv)) {
74
+ const { writeEnv } = await inquirer_1.default.prompt([
75
+ {
76
+ type: 'confirm',
77
+ name: 'writeEnv',
78
+ message: `Append to ${path_1.default.basename(targetEnv)}?`,
79
+ default: true,
80
+ },
81
+ ]);
82
+ if (writeEnv) {
83
+ appendEnvVars(targetEnv, envVars);
84
+ }
85
+ }
86
+ else {
87
+ // No .env file exists — create .env.local for Next.js, .env otherwise
88
+ const newEnvPath = isNextJs ? envLocalPath : envPath;
89
+ const { createEnv } = await inquirer_1.default.prompt([
90
+ {
91
+ type: 'confirm',
92
+ name: 'createEnv',
93
+ message: `Create ${path_1.default.basename(newEnvPath)}?`,
94
+ default: true,
95
+ },
96
+ ]);
97
+ if (createEnv) {
98
+ const lines = Object.entries(envVars)
99
+ .map(([k, v]) => `${k}=${v}`)
100
+ .join('\n');
101
+ fs_1.default.writeFileSync(newEnvPath, `# ScanWarp tracing\n${lines}\n`);
102
+ console.log(chalk_1.default.green(` ✓ Created ${path_1.default.basename(newEnvPath)}`));
103
+ }
104
+ }
105
+ // Step 4: Success message
106
+ console.log(chalk_1.default.bold.green('\n ✓ Request tracing configured!\n'));
107
+ if (isNextJs) {
108
+ console.log(chalk_1.default.gray(' Traces will be sent automatically when your Next.js app starts.'));
109
+ }
110
+ else {
111
+ console.log(chalk_1.default.gray(' Add NODE_OPTIONS to your start command to enable tracing:'));
112
+ console.log(chalk_1.default.cyan(`\n NODE_OPTIONS="--require @scanwarp/instrument" node ./dist/server.js\n`));
113
+ console.log(chalk_1.default.gray(' Or set it in your environment / package.json scripts:'));
114
+ console.log(chalk_1.default.cyan(` "start": "NODE_OPTIONS='--require @scanwarp/instrument' node ./dist/server.js"`));
115
+ }
116
+ console.log('');
117
+ }
118
+ async function setupNextJs(cwd) {
119
+ // Step 2a: Create instrumentation.ts
120
+ const tsPath = path_1.default.join(cwd, 'instrumentation.ts');
121
+ const srcTsPath = path_1.default.join(cwd, 'src', 'instrumentation.ts');
122
+ const hasSrcDir = fs_1.default.existsSync(path_1.default.join(cwd, 'src'));
123
+ const targetPath = hasSrcDir ? srcTsPath : tsPath;
124
+ if (fs_1.default.existsSync(tsPath) || fs_1.default.existsSync(srcTsPath)) {
125
+ console.log(chalk_1.default.yellow('\n ⚠ instrumentation.ts already exists'));
126
+ console.log(chalk_1.default.gray(' Add this to your register() function:\n'));
127
+ console.log(chalk_1.default.cyan(' await import("@scanwarp/instrument");'));
128
+ }
129
+ else {
130
+ const spinner = (0, ora_1.default)('Creating instrumentation.ts...').start();
131
+ fs_1.default.writeFileSync(targetPath, NEXTJS_INSTRUMENTATION);
132
+ const relPath = path_1.default.relative(cwd, targetPath);
133
+ spinner.succeed(`Created ${relPath}`);
134
+ }
135
+ // Step 2b: Patch next.config to enable instrumentationHook
136
+ await patchNextConfig(cwd);
137
+ }
138
+ async function patchNextConfig(cwd) {
139
+ // Find the next.config file
140
+ const candidates = [
141
+ 'next.config.ts',
142
+ 'next.config.mjs',
143
+ 'next.config.js',
144
+ ];
145
+ let configPath = null;
146
+ for (const name of candidates) {
147
+ const fullPath = path_1.default.join(cwd, name);
148
+ if (fs_1.default.existsSync(fullPath)) {
149
+ configPath = fullPath;
150
+ break;
151
+ }
152
+ }
153
+ if (!configPath) {
154
+ console.log(chalk_1.default.yellow(' ⚠ No next.config file found'));
155
+ console.log(chalk_1.default.gray(' Add this to your next.config.js:\n'));
156
+ console.log(chalk_1.default.cyan(' experimental: { instrumentationHook: true }'));
157
+ return;
158
+ }
159
+ const content = fs_1.default.readFileSync(configPath, 'utf-8');
160
+ const configName = path_1.default.basename(configPath);
161
+ // Check if instrumentationHook is already set
162
+ if (content.includes('instrumentationHook')) {
163
+ console.log(chalk_1.default.green(` ✓ ${configName} already has instrumentationHook`));
164
+ return;
165
+ }
166
+ // Try to patch the config automatically
167
+ // Look for an existing `experimental` block
168
+ if (content.includes('experimental')) {
169
+ // There's an existing experimental block — try to add instrumentationHook to it
170
+ const patched = content.replace(/experimental\s*:\s*\{/, 'experimental: {\n instrumentationHook: true,');
171
+ if (patched !== content) {
172
+ fs_1.default.writeFileSync(configPath, patched);
173
+ console.log(chalk_1.default.green(` ✓ Added instrumentationHook to ${configName}`));
174
+ return;
175
+ }
176
+ }
177
+ // Look for the config object to inject experimental block
178
+ // Match patterns like: `const nextConfig = {` or `module.exports = {` or `export default {`
179
+ const configObjectPattern = /((?:const\s+\w+\s*=|module\.exports\s*=|export\s+default)\s*\{)/;
180
+ const match = content.match(configObjectPattern);
181
+ if (match) {
182
+ const patched = content.replace(match[1], `${match[1]}\n experimental: { instrumentationHook: true },`);
183
+ fs_1.default.writeFileSync(configPath, patched);
184
+ console.log(chalk_1.default.green(` ✓ Added experimental.instrumentationHook to ${configName}`));
185
+ return;
186
+ }
187
+ // Could not auto-patch — instruct user
188
+ console.log(chalk_1.default.yellow(` ⚠ Could not auto-patch ${configName}`));
189
+ console.log(chalk_1.default.gray(' Add this to your Next.js config:\n'));
190
+ console.log(chalk_1.default.cyan(' experimental: { instrumentationHook: true }\n'));
191
+ }
192
+ function setupGenericNode() {
193
+ console.log(chalk_1.default.bold('\n Add tracing to your start command:\n'));
194
+ console.log(chalk_1.default.cyan(` NODE_OPTIONS="--require @scanwarp/instrument" node ./dist/server.js`));
195
+ console.log('');
196
+ console.log(chalk_1.default.gray(' Or add as the first import in your entrypoint:'));
197
+ console.log(chalk_1.default.cyan(' import "@scanwarp/instrument";'));
198
+ }
199
+ function appendEnvVars(filePath, vars) {
200
+ const existing = fs_1.default.readFileSync(filePath, 'utf-8');
201
+ const additions = [];
202
+ for (const [key, val] of Object.entries(vars)) {
203
+ if (!existing.includes(key)) {
204
+ additions.push(`${key}=${val}`);
205
+ }
206
+ }
207
+ if (additions.length > 0) {
208
+ const separator = existing.endsWith('\n') ? '' : '\n';
209
+ fs_1.default.appendFileSync(filePath, `${separator}\n# ScanWarp tracing\n${additions.join('\n')}\n`);
210
+ console.log(chalk_1.default.green(` ✓ Updated ${path_1.default.basename(filePath)}`));
211
+ }
212
+ else {
213
+ console.log(chalk_1.default.gray(` ✓ Variables already present in ${path_1.default.basename(filePath)}`));
214
+ }
215
+ }
216
+ function detectPackageManager(cwd) {
217
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'bun.lockb')) || fs_1.default.existsSync(path_1.default.join(cwd, 'bun.lock')))
218
+ return 'bun';
219
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'pnpm-lock.yaml')))
220
+ return 'pnpm';
221
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'yarn.lock')))
222
+ return 'yarn';
223
+ return 'npm';
224
+ }
225
+ function getInstallCommand(pm) {
226
+ switch (pm) {
227
+ case 'bun':
228
+ return 'bun add @scanwarp/instrument';
229
+ case 'pnpm':
230
+ return 'pnpm add @scanwarp/instrument';
231
+ case 'yarn':
232
+ return 'yarn add @scanwarp/instrument';
233
+ case 'npm':
234
+ return 'npm install @scanwarp/instrument';
235
+ }
236
+ }