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/src/runner.js ADDED
@@ -0,0 +1,243 @@
1
+ /**
2
+ * runner.js
3
+ *
4
+ * Process runner and watcher
5
+ * - Spawns child process
6
+ * - Captures stderr for errors
7
+ * - Watches files for changes
8
+ * - Restarts on change
9
+ */
10
+
11
+ const { spawn } = require('child_process');
12
+ const chokidar = require('chokidar');
13
+ const chalk = require('chalk');
14
+ const { explain } = require('./explainer');
15
+ const { showInteractiveExplainer } = require('./explainer/interactive');
16
+
17
+ class Runner {
18
+ constructor(scriptPath, args = [], config = {}) {
19
+ this.scriptPath = scriptPath;
20
+ this.args = args;
21
+ this.config = config;
22
+ this.child = null;
23
+ this.watcher = null;
24
+ this.isRestarting = false;
25
+ this.lastErrorExplanationTime = 0;
26
+ this.isProcessExiting = false;
27
+ this.ignorePatterns = config.ignorePatterns || ['node_modules', '.git', '.env'];
28
+ }
29
+
30
+ /**
31
+ * Start the process
32
+ */
33
+ start() {
34
+ if (this.child) {
35
+ this.stop();
36
+ }
37
+
38
+ console.log(chalk.blue(`\n▶ Starting: ${this.scriptPath}\n`));
39
+
40
+ // Set environment variable to indicate nodewise is active
41
+ const env = {
42
+ ...process.env,
43
+ NODEWISE_ACTIVE: 'true'
44
+ };
45
+
46
+ this.child = spawn('node', [this.scriptPath, ...this.args], {
47
+ stdio: ['inherit', 'pipe', 'pipe'], // Capture both stdout and stderr
48
+ env: env
49
+ });
50
+
51
+ // Capture both stderr and stdout for errors
52
+ let errorBuffer = '';
53
+ let lastErrorTime = 0;
54
+
55
+ // Capture stderr
56
+ this.child.stderr.on('data', (data) => {
57
+ const text = data.toString();
58
+ process.stderr.write(text);
59
+ errorBuffer += text;
60
+
61
+ // Detect errors in stderr and explain them immediately
62
+ if (this.isErrorOutput(text)) {
63
+ this.explainErrorWithDebounce(text);
64
+ errorBuffer = ''; // Reset buffer after explanation
65
+ }
66
+ });
67
+
68
+ // Also capture stdout for runtime errors that get logged
69
+ this.child.stdout.on('data', (data) => {
70
+ const text = data.toString();
71
+ process.stdout.write(text);
72
+ // Check if this looks like an error logged to stdout
73
+ if (this.isErrorOutput(text)) {
74
+ errorBuffer += text;
75
+ // Detect errors in stdout and explain them immediately
76
+ this.explainErrorWithDebounce(text);
77
+ errorBuffer = ''; // Reset buffer after explanation
78
+ }
79
+ });
80
+
81
+ this.child.on('error', (err) => {
82
+ console.error(chalk.red(`\n✗ Error spawning process: ${err.message}\n`));
83
+ this.handleError(errorBuffer);
84
+ });
85
+
86
+ this.child.on('exit', (code, signal) => {
87
+ this.isProcessExiting = true;
88
+
89
+ if (code !== 0 && code !== null) {
90
+ // Process exited with error
91
+ if (!this.isRestarting && errorBuffer) {
92
+ this.handleError(errorBuffer);
93
+ }
94
+ } else if (signal) {
95
+ console.log(chalk.yellow(`\nProcess terminated by signal ${signal}\n`));
96
+ }
97
+
98
+ this.child = null;
99
+ errorBuffer = '';
100
+ this.isProcessExiting = false;
101
+ });
102
+
103
+ // Watch for file changes
104
+ this.setupWatcher();
105
+ }
106
+
107
+ /**
108
+ * Handle error with interactive explanation
109
+ */
110
+ async handleError(errorText) {
111
+ if (!errorText) return;
112
+ await showInteractiveExplainer(errorText, this.config);
113
+ }
114
+
115
+ /**
116
+ * Setup file watcher
117
+ */
118
+ setupWatcher() {
119
+ if (this.watcher) {
120
+ this.watcher.close();
121
+ }
122
+
123
+ const watchPatterns = [
124
+ '**/*.js',
125
+ '**/*.json',
126
+ '!node_modules/**',
127
+ '!.git/**'
128
+ ];
129
+
130
+ this.watcher = chokidar.watch(watchPatterns, {
131
+ ignored: this.ignorePatterns,
132
+ awaitWriteFinish: {
133
+ stabilityThreshold: 300,
134
+ pollInterval: 100
135
+ }
136
+ });
137
+
138
+ this.watcher.on('change', (path) => {
139
+ this.restart(`File changed: ${path}`);
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Restart the process
145
+ */
146
+ restart(reason = '') {
147
+ if (this.isRestarting) return;
148
+
149
+ this.isRestarting = true;
150
+
151
+ if (this.child) {
152
+ this.child.kill('SIGTERM');
153
+
154
+ // Wait a bit for graceful shutdown
155
+ setTimeout(() => {
156
+ if (this.child && !this.child.killed) {
157
+ this.child.kill('SIGKILL');
158
+ }
159
+
160
+ setTimeout(() => {
161
+ this.isRestarting = false;
162
+ if (reason) {
163
+ console.log(chalk.yellow(`\n↻ Restarting... (${reason})\n`));
164
+ }
165
+ this.start();
166
+ }, 100);
167
+ }, 500);
168
+ } else {
169
+ this.isRestarting = false;
170
+ this.start();
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Stop the process and watcher
176
+ */
177
+ stop() {
178
+ if (this.watcher) {
179
+ this.watcher.close();
180
+ this.watcher = null;
181
+ }
182
+
183
+ if (this.child) {
184
+ this.child.kill('SIGTERM');
185
+ this.child = null;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Handle graceful shutdown
191
+ */
192
+ handleShutdown() {
193
+ console.log(chalk.yellow('\nShutting down nodewise...\n'));
194
+ this.stop();
195
+ process.exit(0);
196
+ }
197
+
198
+ /**
199
+ * Check if output contains error indicators
200
+ */
201
+ isErrorOutput(text) {
202
+ const errorPatterns = [
203
+ /error:/i,
204
+ /Error\:/i,
205
+ /TypeError/i,
206
+ /ReferenceError/i,
207
+ /SyntaxError/i,
208
+ /warning:/i,
209
+ /failed/i,
210
+ /throw/i,
211
+ /stack trace/i,
212
+ /traceback/i,
213
+ /exception/i,
214
+ /crash/i,
215
+ /fatal/i
216
+ ];
217
+
218
+ return errorPatterns.some(pattern => pattern.test(text));
219
+ }
220
+
221
+ /**
222
+ * Explain error with debouncing to avoid duplicate explanations
223
+ */
224
+ async explainErrorWithDebounce(errorText) {
225
+ const now = Date.now();
226
+ // Debounce: only explain if at least 500ms has passed since last error
227
+ if (this.lastErrorExplanationTime && (now - this.lastErrorExplanationTime) < 500) {
228
+ return;
229
+ }
230
+ this.lastErrorExplanationTime = now;
231
+
232
+ // Don't explain startup errors that already caused process exit
233
+ if (this.isProcessExiting) {
234
+ return;
235
+ }
236
+
237
+ await showInteractiveExplainer(errorText, this.config);
238
+ }
239
+ }
240
+
241
+ module.exports = {
242
+ Runner
243
+ };
package/src/setup.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * setup.js
3
+ *
4
+ * Interactive setup wizard for first-time users
5
+ * Similar to Vite's setup experience
6
+ */
7
+
8
+ const inquirer = require('inquirer').default;
9
+ const chalk = require('chalk');
10
+ const config = require('./config');
11
+
12
+ /**
13
+ * Run interactive setup
14
+ */
15
+ async function runSetup() {
16
+ console.log('\n' + chalk.cyan('┌─ Welcome to Nodewise Setup ─┐'));
17
+ console.log(chalk.cyan('│') +
18
+ chalk.blue(' Node.js error explainer with AI-powered clarity') +
19
+ chalk.cyan(' │'));
20
+ console.log(chalk.cyan('└────────────────────────────────┘\n'));
21
+
22
+ // Step 1: Choose explanation mode
23
+ const modeAnswer = await inquirer.prompt([
24
+ {
25
+ type: 'list',
26
+ name: 'mode',
27
+ message: 'Select explanation mode:',
28
+ choices: [
29
+ {
30
+ name: 'Gemini Explainer (AI-powered)',
31
+ value: 'gemini',
32
+ description: 'Uses Google Gemini AI to explain errors intelligently'
33
+ },
34
+ {
35
+ name: 'Normal Detection (Pattern-based)',
36
+ value: 'normal',
37
+ description: 'Uses built-in pattern matching to explain common errors'
38
+ }
39
+ ],
40
+ default: 'normal'
41
+ }
42
+ ]);
43
+
44
+ let finalConfig;
45
+
46
+ if (modeAnswer.mode === 'gemini') {
47
+ console.log('\n' + chalk.cyan('┌─ Google Gemini Setup ─┐'));
48
+ console.log(chalk.cyan('│'));
49
+ console.log(chalk.cyan('│') + ' 1. Visit: ' + chalk.blue('https://makersuite.google.com/app/apikey'));
50
+ console.log(chalk.cyan('│') + ' 2. Click "Create API Key"');
51
+ console.log(chalk.cyan('│') + ' 3. Copy the key and paste below');
52
+ console.log(chalk.cyan('│'));
53
+ console.log(chalk.cyan('└──────────────────────────────┘\n'));
54
+
55
+ const geminiAnswers = await inquirer.prompt([
56
+ {
57
+ type: 'password',
58
+ name: 'apiKey',
59
+ message: 'Paste your Gemini API Key:',
60
+ mask: '*',
61
+ validate: (input) => {
62
+ const trimmed = input.trim();
63
+ if (!trimmed || trimmed.length < 10) {
64
+ return 'API key seems invalid (too short). Copy the full key from makersuite.google.com';
65
+ }
66
+ return true;
67
+ }
68
+ }
69
+ ]);
70
+
71
+ finalConfig = config.createConfig('gemini', {
72
+ apiKey: geminiAnswers.apiKey.trim()
73
+ });
74
+
75
+ console.log('\n' + chalk.green('✓ Gemini configuration saved!'));
76
+ console.log(chalk.gray(' Endpoint: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent'));
77
+ } else {
78
+ finalConfig = config.createConfig('normal');
79
+ console.log('\n' + chalk.green('✓ Normal mode configured'));
80
+ }
81
+
82
+ // Display next steps
83
+ console.log(chalk.cyan('\n┌─ Next Steps ─┐'));
84
+ console.log(chalk.cyan('│'));
85
+ console.log(chalk.cyan('│') + ' Run your app with nodewise:');
86
+ console.log(chalk.cyan('│') + ' ' + chalk.yellow('npx nodewise my-app.js'));
87
+ console.log(chalk.cyan('│'));
88
+ console.log(chalk.cyan('│') + ' Or with arguments:');
89
+ console.log(chalk.cyan('│') + ' ' + chalk.yellow('npx nodewise server.js --port 3000'));
90
+ console.log(chalk.cyan('│'));
91
+ console.log(chalk.cyan('└──────────────────────────────┘\n'));
92
+
93
+ return finalConfig;
94
+ }
95
+
96
+ module.exports = {
97
+ runSetup
98
+ };