nodewise 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/LICENSE +21 -0
- package/README.md +97 -0
- package/USER_MANUAL.md +106 -0
- package/bin/nodewise.js +166 -0
- package/examples/express-server.js +70 -0
- package/examples/express-with-errors.js +63 -0
- package/examples/module-not-found.js +9 -0
- package/examples/reference-error.js +9 -0
- package/examples/syntax-error.js +11 -0
- package/examples/type-error.js +10 -0
- package/examples/working-app.js +35 -0
- package/package.json +49 -0
- package/src/config.js +167 -0
- package/src/errorPatterns.js +3253 -0
- package/src/errorWrapper.js +43 -0
- package/src/explainer/gemini.js +212 -0
- package/src/explainer/index.js +51 -0
- package/src/explainer/interactive.js +123 -0
- package/src/explainer/normal.js +27 -0
- package/src/expressErrorHandler.js +68 -0
- package/src/index.js +14 -0
- package/src/runner.js +243 -0
- package/src/setup.js +98 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* errorWrapper.js
|
|
3
|
+
*
|
|
4
|
+
* Optional error wrapper that can be required in user's app
|
|
5
|
+
* to ensure all runtime errors are logged to stderr where nodewise can catch them
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Setup global error handlers
|
|
10
|
+
* Call this at the very start of your app:
|
|
11
|
+
*
|
|
12
|
+
* if (process.env.NODEWISE_ACTIVE) {
|
|
13
|
+
* require('./path/to/errorWrapper').setupErrorHandlers();
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
function setupErrorHandlers() {
|
|
17
|
+
// Catch all uncaught exceptions
|
|
18
|
+
process.on('uncaughtException', (error) => {
|
|
19
|
+
console.error('Uncaught Exception:');
|
|
20
|
+
console.error(error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Catch all unhandled promise rejections
|
|
25
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
26
|
+
console.error('Unhandled Rejection at:', promise);
|
|
27
|
+
console.error('Reason:', reason);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// For Express apps - catch all errors
|
|
32
|
+
if (global.app) {
|
|
33
|
+
global.app.use((err, req, res, next) => {
|
|
34
|
+
console.error('Express Error:');
|
|
35
|
+
console.error(err);
|
|
36
|
+
res.status(500).send('Server Error');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
setupErrorHandlers
|
|
43
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* explainer/gemini.js
|
|
3
|
+
*
|
|
4
|
+
* Gemini AI explainer - Uses Google Generative AI (Gemini)
|
|
5
|
+
* Sends errors to Google Gemini API for intelligent AI-powered explanations
|
|
6
|
+
*
|
|
7
|
+
* API Documentation: https://ai.google.dev/tutorials/rest_quickstart
|
|
8
|
+
* Model: gemini-1.5-flash (fast, efficient model)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const axios = require('axios');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default Gemini API endpoint - no need to change unless using custom proxy
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Clean markdown formatting from text
|
|
20
|
+
* Removes ###, **, `, etc to get plain text
|
|
21
|
+
*/
|
|
22
|
+
function cleanMarkdown(text) {
|
|
23
|
+
return text
|
|
24
|
+
// Remove markdown headings: ###, ##, #
|
|
25
|
+
.replace(/^#+\s+/gm, '')
|
|
26
|
+
// Remove bold: **text** → text
|
|
27
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
28
|
+
// Remove italic: *text* or _text_
|
|
29
|
+
.replace(/[*_]([^*_]+)[*_]/g, '$1')
|
|
30
|
+
// Remove inline code: `text` → text
|
|
31
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
32
|
+
// Remove code blocks: ```...```
|
|
33
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
34
|
+
// Remove links: [text](url) → text
|
|
35
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
|
|
36
|
+
// Remove multiple spaces, leading/trailing
|
|
37
|
+
.trim()
|
|
38
|
+
// Clean up extra blank lines
|
|
39
|
+
.split('\n')
|
|
40
|
+
.filter(line => line.trim() !== '')
|
|
41
|
+
.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create the prompt for Gemini - concise, focused on solutions, plain text
|
|
46
|
+
*/
|
|
47
|
+
function createSystemPrompt() {
|
|
48
|
+
return `You are a Node.js debugging assistant. Answer in plain text only. No markdown.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Truncate error text to a small, focused snippet to save tokens
|
|
53
|
+
*/
|
|
54
|
+
function truncateError(text, maxLines = 6, maxChars = 800) {
|
|
55
|
+
|
|
56
|
+
function truncateString(s, max = 1000) {
|
|
57
|
+
if (!s) return '';
|
|
58
|
+
const str = typeof s === 'string' ? s : JSON.stringify(s);
|
|
59
|
+
if (str.length <= max) return str;
|
|
60
|
+
return str.slice(0, max) + '... (truncated)';
|
|
61
|
+
}
|
|
62
|
+
if (!text) return '';
|
|
63
|
+
const cleaned = text.toString().trim();
|
|
64
|
+
const lines = cleaned.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
65
|
+
const selected = lines.slice(0, maxLines);
|
|
66
|
+
let out = selected.join('\n');
|
|
67
|
+
if (out.length > maxChars) out = out.slice(0, maxChars) + '... (truncated)';
|
|
68
|
+
if (lines.length > maxLines && out.indexOf('...') === -1) out += '\n... (truncated)';
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Explain error using Gemini API
|
|
74
|
+
*
|
|
75
|
+
* @param {string} errorText - The error message/stack trace
|
|
76
|
+
* @param {object} geminiConfig - Configuration object with { apiKey: 'your-api-key' }
|
|
77
|
+
* @returns {Promise<string>} - The explanation with problem analysis and exact solution
|
|
78
|
+
*
|
|
79
|
+
* Example:
|
|
80
|
+
* const config = { apiKey: 'AIzaSy...' }
|
|
81
|
+
* const explanation = await explainWithGemini('TypeError: Cannot read property', config)
|
|
82
|
+
*/
|
|
83
|
+
async function explainWithGemini(errorText, geminiConfig) {
|
|
84
|
+
// Validate API key is provided
|
|
85
|
+
if (!geminiConfig || !geminiConfig.apiKey) {
|
|
86
|
+
throw new Error('Gemini API key not configured. Run: nodewise --setup');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const apiKey = geminiConfig.apiKey.trim();
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Build the full API URL with just the API key
|
|
93
|
+
const url = `${DEFAULT_ENDPOINT}?key=${apiKey}`;
|
|
94
|
+
|
|
95
|
+
// Exact structure matching the curl example you provided
|
|
96
|
+
// Minimal, token-efficient prompt — send a truncated error to reduce tokens
|
|
97
|
+
const snippet = truncateError(errorText, 6, 800);
|
|
98
|
+
const payload = {
|
|
99
|
+
contents: [
|
|
100
|
+
{
|
|
101
|
+
parts: [
|
|
102
|
+
{
|
|
103
|
+
text: `${createSystemPrompt()}\n\nBriefly explain the error in plain text: one-line summary; cause; file:line to change; minimal code fix.\n\nError snippet:\n${snippet}`
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Make the exact same request structure as shown in curl
|
|
111
|
+
const response = await axios.post(url, payload, {
|
|
112
|
+
timeout: geminiConfig.timeout || 60000,
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'x-goog-api-key': apiKey // Also send as header (optional but explicit)
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Extract explanation from Gemini response
|
|
120
|
+
if (response.data?.candidates?.[0]?.content?.parts?.[0]?.text) {
|
|
121
|
+
let explanation = response.data.candidates[0].content.parts[0].text.trim();
|
|
122
|
+
// Clean any markdown formatting that might have been included
|
|
123
|
+
explanation = cleanMarkdown(explanation);
|
|
124
|
+
return explanation;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error('Empty response from Gemini API');
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Log detailed info to stderr for debugging (without exposing API key)
|
|
130
|
+
try {
|
|
131
|
+
const resp = error.response;
|
|
132
|
+
const cfg = error.config || {};
|
|
133
|
+
const reqBody = cfg.data || payload || {};
|
|
134
|
+
const respBody = resp?.data;
|
|
135
|
+
|
|
136
|
+
const logLines = [];
|
|
137
|
+
logLines.push('--- Gemini request failed (debug) ---');
|
|
138
|
+
if (resp) logLines.push(`Status: ${resp.status} ${resp.statusText || ''}`);
|
|
139
|
+
if (error.code) logLines.push(`Error code: ${error.code}`);
|
|
140
|
+
logLines.push('Request snippet:');
|
|
141
|
+
// show only the text portion of the payload to save tokens
|
|
142
|
+
const textPart = (reqBody && reqBody.contents && reqBody.contents[0] && reqBody.contents[0].parts && reqBody.contents[0].parts[0] && reqBody.contents[0].parts[0].text) ? reqBody.contents[0].parts[0].text : truncateString(reqBody, 600);
|
|
143
|
+
logLines.push(truncateString(textPart, 1000));
|
|
144
|
+
if (respBody) {
|
|
145
|
+
logLines.push('Response (truncated):');
|
|
146
|
+
logLines.push(truncateString(respBody, 1000));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Provide a curl equivalent without the API key value
|
|
150
|
+
const curl = `curl "${DEFAULT_ENDPOINT}" \\
|
|
151
|
+
-H "x-goog-api-key: $GEMINI_API_KEY" \\
|
|
152
|
+
-H 'Content-Type: application/json' \\
|
|
153
|
+
-X POST \\
|
|
154
|
+
-d '${truncateString(reqBody, 1000)}'`;
|
|
155
|
+
logLines.push('Curl (use env var GEMINI_API_KEY):');
|
|
156
|
+
logLines.push(curl);
|
|
157
|
+
|
|
158
|
+
// Print to stderr so normal output stays clean
|
|
159
|
+
console.error('\n' + logLines.join('\n') + '\n');
|
|
160
|
+
} catch (logErr) {
|
|
161
|
+
// ignore logging errors
|
|
162
|
+
}
|
|
163
|
+
// Provide helpful error messages for common issues
|
|
164
|
+
// Short, plain fallback errors (keeps CLI concise)
|
|
165
|
+
if (error.response?.status === 401 || error.response?.status === 403) {
|
|
166
|
+
throw new Error('Gemini API Key Error: invalid or expired key');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (error.response?.status === 429) {
|
|
170
|
+
throw new Error('Gemini rate limit (429)');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (error.response?.status === 503 || error.response?.status === 500) {
|
|
174
|
+
throw new Error('Gemini service unavailable');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
178
|
+
throw new Error('Network error: cannot reach Gemini');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (error.code === 'ETIMEDOUT' || error.code === 'EHOSTUNREACH') {
|
|
182
|
+
throw new Error('Timeout: Gemini did not respond');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Generic fallback
|
|
186
|
+
throw new Error(`Gemini API Error: ${error.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get API key setup instructions
|
|
192
|
+
*/
|
|
193
|
+
function getSetupInstructions() {
|
|
194
|
+
return `
|
|
195
|
+
🔑 Gemini API Setup:
|
|
196
|
+
1. Visit: https://makersuite.google.com/app/apikey
|
|
197
|
+
2. Click "Create API Key"
|
|
198
|
+
3. Copy the API key
|
|
199
|
+
4. Paste when prompted: nodewise --setup
|
|
200
|
+
|
|
201
|
+
That's it! You only need to provide the API key.
|
|
202
|
+
The endpoint is automatically configured.
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
explainWithGemini,
|
|
208
|
+
createSystemPrompt,
|
|
209
|
+
cleanMarkdown,
|
|
210
|
+
getSetupInstructions,
|
|
211
|
+
DEFAULT_ENDPOINT
|
|
212
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* explainer/index.js
|
|
3
|
+
*
|
|
4
|
+
* Main explainer abstraction layer
|
|
5
|
+
* Routes to appropriate explainer based on configuration
|
|
6
|
+
* Handles fallback from Gemini to Normal mode on failure
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { explainWithGemini } = require('./gemini');
|
|
10
|
+
const { explainWithNormal } = require('./normal');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Main explain function - routes based on config
|
|
15
|
+
*
|
|
16
|
+
* @param {string} errorText - The error message/stack
|
|
17
|
+
* @param {object} config - Configuration object with mode setting
|
|
18
|
+
* @returns {Promise<string>} - The explanation
|
|
19
|
+
*/
|
|
20
|
+
async function explain(errorText, config) {
|
|
21
|
+
if (!config) {
|
|
22
|
+
throw new Error('Configuration is required');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If no mode specified, default to normal
|
|
26
|
+
const mode = config.mode || 'normal';
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
if (mode === 'gemini') {
|
|
30
|
+
try {
|
|
31
|
+
const geminiOptions = { ...config.gemini, timeout: config.timeout };
|
|
32
|
+
return await explainWithGemini(errorText, geminiOptions);
|
|
33
|
+
} catch (geminiError) {
|
|
34
|
+
// Gemini failed — fall back to normal mode (concise)
|
|
35
|
+
const reason = (geminiError && geminiError.message) ? geminiError.message.split('\n')[0] : 'unknown';
|
|
36
|
+
console.warn(chalk.yellow(`Gemini failed, using normal mode: ${reason}`));
|
|
37
|
+
return await explainWithNormal(errorText);
|
|
38
|
+
}
|
|
39
|
+
} else if (mode === 'normal') {
|
|
40
|
+
return await explainWithNormal(errorText);
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(`Unknown explanation mode: ${mode}`);
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
explain
|
|
51
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* explainer/interactive.js
|
|
3
|
+
*
|
|
4
|
+
* Minimal interactive UI for error explanations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
const { explain } = require('./index');
|
|
10
|
+
|
|
11
|
+
async function showInteractiveExplainer(errorText, config) {
|
|
12
|
+
console.log('\n');
|
|
13
|
+
const summary = (errorText || '').toString().split('\n')[0] || 'Unknown error';
|
|
14
|
+
|
|
15
|
+
// Header with soft border
|
|
16
|
+
console.log(chalk.hex('#FF5F5F')(' ┌' + '─'.repeat(58)));
|
|
17
|
+
console.log(chalk.hex('#FF5F5F')(' │ ') + chalk.white.bold('CRASH DETECTED'));
|
|
18
|
+
console.log(chalk.hex('#FF5F5F')(' └' + '─'.repeat(58)));
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.white(' ' + summary));
|
|
21
|
+
console.log();
|
|
22
|
+
|
|
23
|
+
// Compact, modern prompt
|
|
24
|
+
console.log(chalk.cyan.bold(' ? ') + chalk.white('Would you like an AI explanation?'));
|
|
25
|
+
console.log(chalk.gray(' [y] Yes, explain it [n] No, just skip'));
|
|
26
|
+
console.log();
|
|
27
|
+
|
|
28
|
+
const answer = await getUserInput();
|
|
29
|
+
|
|
30
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes' || answer === '') {
|
|
31
|
+
await explainErrorInteractively(errorText, config);
|
|
32
|
+
} else {
|
|
33
|
+
console.log(chalk.gray(' Skipped.\n'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getUserInput() {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const rl = readline.createInterface({
|
|
40
|
+
input: process.stdin,
|
|
41
|
+
output: process.stdout,
|
|
42
|
+
terminal: true
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
process.stdout.write(chalk.cyan(' > '));
|
|
46
|
+
|
|
47
|
+
rl.on('line', (answer) => {
|
|
48
|
+
rl.close();
|
|
49
|
+
resolve(answer.trim());
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function showThinking() {
|
|
55
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
56
|
+
let frameIndex = 0;
|
|
57
|
+
|
|
58
|
+
process.stdout.write('\n');
|
|
59
|
+
const interval = setInterval(() => {
|
|
60
|
+
process.stdout.write(`\r ${chalk.hex('#00D7FF')(frames[frameIndex])} ${chalk.gray('Consulting Gemini...')}`);
|
|
61
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
62
|
+
}, 80);
|
|
63
|
+
|
|
64
|
+
return interval;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clearThinking(interval) {
|
|
68
|
+
clearInterval(interval);
|
|
69
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function displayExplanation(explanation) {
|
|
73
|
+
console.log('\n');
|
|
74
|
+
|
|
75
|
+
// Minimalist header with a simple horizontal rule
|
|
76
|
+
console.log(chalk.hex('#00FF87').bold(' ✦ GEMINI INTELLIGENCE'));
|
|
77
|
+
console.log(chalk.gray(' ' + '─'.repeat(45)));
|
|
78
|
+
console.log();
|
|
79
|
+
|
|
80
|
+
const lines = explanation.split('\n');
|
|
81
|
+
lines.forEach((line) => {
|
|
82
|
+
let content = line.trim();
|
|
83
|
+
if (!content) {
|
|
84
|
+
console.log(); // Add a gap for empty lines (double-spacing)
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// High-end minimalist styling
|
|
89
|
+
// If it starts with a keyword, add a line break before it to create gaps between sections
|
|
90
|
+
if (/^(Summary|Problem|Cause|Solution|Fix|Where|Why|File|Line|Note|Suggestion):/i.test(content)) {
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
content = content
|
|
95
|
+
.replace(/^(Summary|Problem|Cause|Solution|Fix|Where|Why|File|Line|Note|Suggestion):/i, (match) => chalk.hex('#FFAF00').bold(match))
|
|
96
|
+
.replace(/`([^`]+)`/g, (match) => chalk.hex('#00FF87')(match))
|
|
97
|
+
.replace(/'([^']+)'/g, (match) => chalk.hex('#00FF87')(match));
|
|
98
|
+
|
|
99
|
+
console.log(' ' + content);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function explainErrorInteractively(errorText, config) {
|
|
106
|
+
let thinkingInterval = showThinking();
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const explanation = await explain(errorText, config);
|
|
110
|
+
clearThinking(thinkingInterval);
|
|
111
|
+
displayExplanation(explanation);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
clearThinking(thinkingInterval);
|
|
114
|
+
console.log(chalk.red.bold(' ✗ Failed to explain'));
|
|
115
|
+
console.log(chalk.red(` ${error.message}`));
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
showInteractiveExplainer,
|
|
122
|
+
displayExplanation
|
|
123
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* explainer/normal.js
|
|
3
|
+
*
|
|
4
|
+
* Normal mode explainer using pattern-based detection
|
|
5
|
+
* Lightweight, offline, no API calls required
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { getErrorExplanation } = require('../errorPatterns');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Explain error using pattern matching
|
|
12
|
+
*
|
|
13
|
+
* @param {string} errorText - The error message/stack
|
|
14
|
+
* @returns {Promise<string>} - The explanation
|
|
15
|
+
*/
|
|
16
|
+
async function explainWithNormal(errorText) {
|
|
17
|
+
try {
|
|
18
|
+
const explanation = getErrorExplanation(errorText);
|
|
19
|
+
return explanation;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return `Unable to explain this error. Here's what we know:\n${errorText}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
explainWithNormal
|
|
27
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* expressErrorHandler.js
|
|
3
|
+
*
|
|
4
|
+
* Express error handling for nodewise
|
|
5
|
+
* Provides utilities to ensure all errors are logged to stderr where nodewise can catch them
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error handler middleware for Express
|
|
10
|
+
* Place this AFTER all other middleware and route handlers
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* const app = express();
|
|
14
|
+
* app.get('/', (req, res) => { ... });
|
|
15
|
+
* app.use(nodewiseErrorHandler); // <-- Add at the end
|
|
16
|
+
* app.listen(3000);
|
|
17
|
+
*/
|
|
18
|
+
function nodewiseErrorHandler(err, req, res, next) {
|
|
19
|
+
// Always log errors to stderr so nodewise captures them
|
|
20
|
+
console.error('Caught Error in Express:');
|
|
21
|
+
console.error(err.stack || err.toString());
|
|
22
|
+
|
|
23
|
+
// Send response to client
|
|
24
|
+
res.status(err.status || 500).json({
|
|
25
|
+
error: err.message,
|
|
26
|
+
status: err.status || 500
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Wrapper to catch errors in async route handlers
|
|
32
|
+
* Use this to wrap async route handlers to catch thrown errors
|
|
33
|
+
*
|
|
34
|
+
* Example:
|
|
35
|
+
* app.get('/', asyncHandler(async (req, res) => {
|
|
36
|
+
* const data = await risky();
|
|
37
|
+
* res.send(data);
|
|
38
|
+
* }));
|
|
39
|
+
*/
|
|
40
|
+
function asyncHandler(fn) {
|
|
41
|
+
return (req, res, next) => {
|
|
42
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Setup global uncaught error handlers
|
|
48
|
+
* Call this at app startup to catch all unhandled errors
|
|
49
|
+
*/
|
|
50
|
+
function setupGlobalErrorHandlers() {
|
|
51
|
+
process.on('uncaughtException', (err) => {
|
|
52
|
+
console.error('Uncaught Exception:');
|
|
53
|
+
console.error(err.stack || err);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
58
|
+
console.error('Unhandled Promise Rejection:');
|
|
59
|
+
console.error(reason);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
nodewiseErrorHandler,
|
|
66
|
+
asyncHandler,
|
|
67
|
+
setupGlobalErrorHandlers
|
|
68
|
+
};
|
package/src/index.js
ADDED