scanwarp 0.3.4 → 0.4.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.
@@ -14,7 +14,23 @@ const chokidar_1 = require("chokidar");
14
14
  const detector_js_1 = require("../detector.js");
15
15
  const analysis_engine_js_1 = require("../dev/analysis-engine.js");
16
16
  const schema_drift_js_1 = require("../dev/analyzers/schema-drift.js");
17
+ const browser_monitor_script_js_1 = require("../dev/browser-monitor-script.js");
17
18
  // ─── Main dev command ───
19
+ // ─── Helper functions ───
20
+ function hasInstrumentInPackageJson(cwd) {
21
+ const packageJsonPath = path_1.default.join(cwd, 'package.json');
22
+ if (!fs_1.default.existsSync(packageJsonPath)) {
23
+ return false;
24
+ }
25
+ try {
26
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
27
+ return !!(packageJson.dependencies?.['@scanwarp/instrument'] ||
28
+ packageJson.devDependencies?.['@scanwarp/instrument']);
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
18
34
  // ─── Production setup detection ───
19
35
  function checkProductionSetup(cwd) {
20
36
  // Check if they've deployed or are using hosting platforms
@@ -29,19 +45,7 @@ function checkProductionSetup(cwd) {
29
45
  // Check if instrumentation is set up
30
46
  const hasInstrumentationFile = fs_1.default.existsSync(path_1.default.join(cwd, 'instrumentation.ts')) ||
31
47
  fs_1.default.existsSync(path_1.default.join(cwd, 'instrumentation.js'));
32
- // Check if @scanwarp/instrument is in package.json
33
- let hasInstrumentPackage = false;
34
- const packageJsonPath = path_1.default.join(cwd, 'package.json');
35
- if (fs_1.default.existsSync(packageJsonPath)) {
36
- try {
37
- const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
38
- hasInstrumentPackage = !!(packageJson.dependencies?.['@scanwarp/instrument'] ||
39
- packageJson.devDependencies?.['@scanwarp/instrument']);
40
- }
41
- catch {
42
- // Ignore parse errors
43
- }
44
- }
48
+ const hasInstrumentPackage = hasInstrumentInPackageJson(cwd);
45
49
  return hasInstrumentationFile || hasInstrumentPackage;
46
50
  }
47
51
  async function devCommand(options = {}) {
@@ -87,12 +91,18 @@ async function devCommand(options = {}) {
87
91
  console.log(chalk_1.default.white(` }`));
88
92
  console.log(chalk_1.default.gray(` }`));
89
93
  console.log(chalk_1.default.gray(` }\n`));
94
+ // Print browser monitoring instructions
95
+ console.log(chalk_1.default.bold(' 🔍 Browser error monitoring:\n'));
96
+ console.log(chalk_1.default.gray(` Add this script tag to your HTML <head> for frontend error capture:`));
97
+ console.log(chalk_1.default.cyan(` <script src="http://localhost:${scanwarpPort}/monitor.js"></script>\n`));
98
+ console.log(chalk_1.default.gray(` This will detect blank screens, console errors, and React issues.\n`));
90
99
  // Step 5: Start the user's dev server
91
100
  const isNextJs = detected.framework === 'Next.js';
101
+ const hasInstrumentPackage = hasInstrumentInPackageJson(cwd);
92
102
  const devServerPort = detectDevServerPort(devCmd);
93
103
  console.log(chalk_1.default.bold(` Starting: ${devCmd}\n`));
94
104
  console.log(chalk_1.default.gray('─'.repeat(60)));
95
- const child = startDevServer(devCmd, cwd, scanwarpPort, isNextJs);
105
+ const child = startDevServer(devCmd, cwd, scanwarpPort, isNextJs, hasInstrumentPackage);
96
106
  // Use store's maps for tracking (shared with MCP API)
97
107
  const previousResults = store.previousResults;
98
108
  const baselines = store.baselines;
@@ -313,6 +323,7 @@ async function startLocalServer(port) {
313
323
  const store = {
314
324
  spans: [],
315
325
  events: [],
326
+ browserErrors: [],
316
327
  liveLogEnabled: false,
317
328
  analysisEngine: new analysis_engine_js_1.AnalysisEngine(),
318
329
  routes: { pages: [], apiRoutes: [] },
@@ -351,6 +362,55 @@ async function startLocalServer(port) {
351
362
  res.end(JSON.stringify({ partialSuccess: {} }));
352
363
  return;
353
364
  }
365
+ // POST /dev/errors - Browser error reporting
366
+ if (req.method === 'POST' && req.url === '/dev/errors') {
367
+ try {
368
+ const data = JSON.parse(body);
369
+ const error = {
370
+ type: data.error?.type || 'unknown',
371
+ message: data.error?.message || 'No message',
372
+ stack: data.error?.stack,
373
+ timestamp: data.error?.timestamp || Date.now(),
374
+ url: data.url,
375
+ userAgent: data.userAgent,
376
+ filename: data.error?.filename,
377
+ lineno: data.error?.lineno,
378
+ colno: data.error?.colno,
379
+ html: data.error?.html,
380
+ };
381
+ store.browserErrors.push(error);
382
+ // Keep only last 100 errors
383
+ if (store.browserErrors.length > 100) {
384
+ store.browserErrors.shift();
385
+ }
386
+ // Print to console for visibility
387
+ if (error.type === 'blank_screen') {
388
+ console.log(chalk_1.default.red('\n🚨 [Browser] Blank screen detected!'));
389
+ console.log(chalk_1.default.yellow(` URL: ${error.url}`));
390
+ }
391
+ else {
392
+ console.log(chalk_1.default.red(`\n🚨 [Browser] ${error.type}: ${error.message}`));
393
+ if (error.filename) {
394
+ console.log(chalk_1.default.gray(` ${error.filename}:${error.lineno}:${error.colno}`));
395
+ }
396
+ }
397
+ res.writeHead(200);
398
+ res.end(JSON.stringify({ success: true }));
399
+ }
400
+ catch (e) {
401
+ res.writeHead(400);
402
+ res.end(JSON.stringify({ error: 'Invalid request' }));
403
+ }
404
+ return;
405
+ }
406
+ // GET /monitor.js - Serve browser monitoring script
407
+ if (req.method === 'GET' && req.url === '/monitor.js') {
408
+ const monitorScript = browser_monitor_script_js_1.BROWSER_MONITOR_SCRIPT.replace('__SCANWARP_SERVER__', `'http://localhost:${port}'`);
409
+ res.setHeader('Content-Type', 'application/javascript');
410
+ res.writeHead(200);
411
+ res.end(monitorScript);
412
+ return;
413
+ }
354
414
  // GET /health
355
415
  if (req.method === 'GET' && req.url === '/health') {
356
416
  res.writeHead(200);
@@ -384,6 +444,15 @@ async function startLocalServer(port) {
384
444
  res.end(JSON.stringify({ issues }));
385
445
  return;
386
446
  }
447
+ // GET /api/browser-errors
448
+ if (req.method === 'GET' && req.url === '/api/browser-errors') {
449
+ res.writeHead(200);
450
+ res.end(JSON.stringify({
451
+ errors: store.browserErrors.slice(-20), // Last 20 errors
452
+ total: store.browserErrors.length
453
+ }));
454
+ return;
455
+ }
387
456
  // GET /api/routes
388
457
  if (req.method === 'GET' && req.url === '/api/routes') {
389
458
  const allRoutes = [
@@ -686,7 +755,7 @@ function flattenAttributes(attrs) {
686
755
  return result;
687
756
  }
688
757
  // ─── Child process management ───
689
- function startDevServer(devCmd, cwd, scanwarpPort, isNextJs) {
758
+ function startDevServer(devCmd, cwd, scanwarpPort, isNextJs, hasInstrumentPackage) {
690
759
  const [cmd, ...args] = parseCommand(devCmd);
691
760
  // Build env vars
692
761
  const env = {
@@ -695,8 +764,8 @@ function startDevServer(devCmd, cwd, scanwarpPort, isNextJs) {
695
764
  SCANWARP_PROJECT_ID: 'local-dev',
696
765
  SCANWARP_SERVICE_NAME: 'dev',
697
766
  };
698
- // For non-Next.js, inject NODE_OPTIONS to auto-load instrumentation
699
- if (!isNextJs) {
767
+ // For non-Next.js, inject NODE_OPTIONS to auto-load instrumentation (if installed)
768
+ if (!isNextJs && hasInstrumentPackage) {
700
769
  const existingNodeOpts = process.env.NODE_OPTIONS || '';
701
770
  env.NODE_OPTIONS = `--require @scanwarp/instrument ${existingNodeOpts}`.trim();
702
771
  }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ /**
3
+ * Browser monitoring script - embedded as string for easy serving
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BROWSER_MONITOR_SCRIPT = void 0;
7
+ exports.BROWSER_MONITOR_SCRIPT = `
8
+ /**
9
+ * ScanWarp Browser Monitor
10
+ * Injected into user's app during dev to capture frontend errors
11
+ * Reports to local ScanWarp dev server
12
+ */
13
+
14
+ (function() {
15
+ const SCANWARP_SERVER = '__SCANWARP_SERVER__';
16
+ const errors = [];
17
+
18
+ // Capture console.error
19
+ const originalError = console.error;
20
+ console.error = function(...args) {
21
+ captureError({
22
+ type: 'console.error',
23
+ message: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '),
24
+ timestamp: Date.now(),
25
+ stack: new Error().stack
26
+ });
27
+ originalError.apply(console, args);
28
+ };
29
+
30
+ // Capture unhandled errors
31
+ window.addEventListener('error', (event) => {
32
+ captureError({
33
+ type: 'unhandled_error',
34
+ message: event.message,
35
+ filename: event.filename,
36
+ lineno: event.lineno,
37
+ colno: event.colno,
38
+ stack: event.error?.stack,
39
+ timestamp: Date.now()
40
+ });
41
+ });
42
+
43
+ // Capture unhandled promise rejections
44
+ window.addEventListener('unhandledrejection', (event) => {
45
+ captureError({
46
+ type: 'unhandled_rejection',
47
+ message: event.reason?.message || String(event.reason),
48
+ stack: event.reason?.stack,
49
+ timestamp: Date.now()
50
+ });
51
+ });
52
+
53
+ // Capture React errors (if React is present)
54
+ setTimeout(() => {
55
+ if (window.React && window.ReactDOM) {
56
+ const originalErrorHandler = window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactDebugCurrentFrame?.setExtraStackFrame;
57
+ if (originalErrorHandler) {
58
+ window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactDebugCurrentFrame.setExtraStackFrame = function(stack) {
59
+ captureError({
60
+ type: 'react_error',
61
+ message: 'React rendering error',
62
+ stack: stack,
63
+ timestamp: Date.now()
64
+ });
65
+ originalErrorHandler.call(this, stack);
66
+ };
67
+ }
68
+ }
69
+ }, 100);
70
+
71
+ // Check if app rendered (detect blank screen)
72
+ setTimeout(() => {
73
+ const body = document.body;
74
+ const hasContent = body && (body.children.length > 1 || body.textContent.trim().length > 0);
75
+
76
+ if (!hasContent) {
77
+ captureError({
78
+ type: 'blank_screen',
79
+ message: 'Page rendered but appears blank - no content in body',
80
+ timestamp: Date.now(),
81
+ html: document.documentElement.outerHTML.slice(0, 500)
82
+ });
83
+ }
84
+ }, 2000);
85
+
86
+ // Send error to ScanWarp server
87
+ function captureError(error) {
88
+ errors.push(error);
89
+
90
+ // Send to server
91
+ fetch(SCANWARP_SERVER + '/dev/errors', {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({
95
+ url: window.location.href,
96
+ userAgent: navigator.userAgent,
97
+ error: error
98
+ })
99
+ }).catch(() => {
100
+ // Silently fail if server is down
101
+ });
102
+ }
103
+
104
+ // Expose for debugging
105
+ window.__scanwarp = {
106
+ errors: errors,
107
+ getErrors: () => errors,
108
+ clearErrors: () => errors.length = 0
109
+ };
110
+
111
+ console.log('%c[ScanWarp] %cMonitoring for errors...', 'color: #3b82f6; font-weight: bold', 'color: #6b7280');
112
+ })();
113
+ `;
@@ -95,7 +95,9 @@ async function setupMCP(serverUrl) {
95
95
  fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
96
96
  console.log(chalk_1.default.green(`✓ MCP server added to ${toolName} configuration`));
97
97
  console.log(chalk_1.default.gray(` Config: ${configPath}\n`));
98
- console.log(chalk_1.default.yellow(` Restart ${toolName} to load the MCP server\n`));
98
+ console.log(chalk_1.default.bold.yellow(' 📌 One more step:'));
99
+ console.log(chalk_1.default.yellow(` Restart ${toolName} to activate the MCP connection`));
100
+ console.log(chalk_1.default.gray(` (${toolName} loads MCP servers at startup)\n`));
99
101
  }
100
102
  catch (error) {
101
103
  console.log(chalk_1.default.red('✗ Failed to update MCP config'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scanwarp",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Production monitoring built for developers who ship fast. Auto-diagnoses issues with Claude AI.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {