nodewise 1.0.2 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodewise",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Node.js error explainer with AI-powered clarity",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -8,7 +8,14 @@ const chalk = require('chalk');
8
8
  const readline = require('readline');
9
9
  const { explain } = require('./index');
10
10
 
11
+ // Global state for cancellation
12
+ let cancelPrompt = false;
13
+ let activePromptResolver = null;
14
+
11
15
  async function showInteractiveExplainer(errorText, config) {
16
+ // Reset cancel flag
17
+ cancelPrompt = false;
18
+
12
19
  console.log('\n');
13
20
  const summary = (errorText || '').toString().split('\n')[0] || 'Unknown error';
14
21
 
@@ -20,34 +27,105 @@ async function showInteractiveExplainer(errorText, config) {
20
27
  console.log(chalk.white(' ' + summary));
21
28
  console.log();
22
29
 
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
30
  const answer = await getUserInput();
29
31
 
30
- if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes' || answer === '') {
32
+ if (answer === 'yes') {
31
33
  await explainErrorInteractively(errorText, config);
34
+ } else if (answer === 'cancelled') {
35
+ // Silently skip on cancellation
32
36
  } else {
33
37
  console.log(chalk.gray(' Skipped.\n'));
34
38
  }
35
39
  }
36
40
 
37
- function getUserInput() {
41
+ async function getUserInput() {
38
42
  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
- });
43
+ const options = [
44
+ { label: chalk.green('✓ Yes, explain it'), value: 'yes' },
45
+ { label: chalk.gray('✗ No, just skip'), value: 'no' }
46
+ ];
47
+
48
+ let selectedIndex = 0;
49
+ let isResolved = false;
50
+
51
+ // Store resolver for cancellation
52
+ activePromptResolver = () => {
53
+ if (!isResolved) {
54
+ cleanup();
55
+ isResolved = true;
56
+ resolve('cancelled');
57
+ }
58
+ };
59
+
60
+ // Display the question
61
+ console.log(chalk.cyan(' ? ') + chalk.white('Would you like an AI explanation?'));
62
+
63
+ const renderOptions = () => {
64
+ options.forEach((option, index) => {
65
+ const prefix = index === selectedIndex ? chalk.cyan('❯ ') : ' ';
66
+ console.log(` ${prefix}${option.label}`);
67
+ });
68
+ };
69
+
70
+ const cleanup = () => {
71
+ process.stdin.removeListener('keypress', onKeypress);
72
+ if (process.stdin.isTTY) {
73
+ process.stdin.setRawMode(false);
74
+ }
75
+ process.stdin.pause();
76
+ activePromptResolver = null;
77
+ };
78
+
79
+ // Initial render
80
+ renderOptions();
81
+
82
+ // Setup readline for keypress
83
+ readline.emitKeypressEvents(process.stdin);
84
+ if (process.stdin.isTTY) {
85
+ process.stdin.setRawMode(true);
86
+ }
87
+
88
+ const onKeypress = (str, key) => {
89
+ // Check if cancelled
90
+ if (cancelPrompt) {
91
+ cleanup();
92
+ if (!isResolved) {
93
+ isResolved = true;
94
+ console.log(chalk.yellow('\n (Skipped due to restart)\n'));
95
+ resolve('cancelled');
96
+ }
97
+ return;
98
+ }
99
+
100
+ if (key.name === 'up') {
101
+ // Clear previous options
102
+ process.stdout.write('\x1b[' + options.length + 'A'); // Move cursor up
103
+ process.stdout.write('\x1b[0J'); // Clear from cursor down
104
+
105
+ selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
106
+ renderOptions();
107
+ } else if (key.name === 'down') {
108
+ // Clear previous options
109
+ process.stdout.write('\x1b[' + options.length + 'A'); // Move cursor up
110
+ process.stdout.write('\x1b[0J'); // Clear from cursor down
111
+
112
+ selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0;
113
+ renderOptions();
114
+ } else if (key.name === 'return') {
115
+ cleanup();
116
+ if (!isResolved) {
117
+ isResolved = true;
118
+ console.log(); // Add newline after selection
119
+ resolve(options[selectedIndex].value);
120
+ }
121
+ } else if (key.ctrl && key.name === 'c') {
122
+ cleanup();
123
+ process.exit(0);
124
+ }
125
+ };
126
+
127
+ process.stdin.on('keypress', onKeypress);
128
+ process.stdin.resume();
51
129
  });
52
130
  }
53
131
 
@@ -69,6 +147,16 @@ function clearThinking(interval) {
69
147
  process.stdout.write('\r' + ' '.repeat(50) + '\r');
70
148
  }
71
149
 
150
+ /**
151
+ * Cancel any active prompt
152
+ */
153
+ function cancelActivePrompt() {
154
+ cancelPrompt = true;
155
+ if (activePromptResolver) {
156
+ activePromptResolver();
157
+ }
158
+ }
159
+
72
160
  function displayExplanation(explanation) {
73
161
  console.log('\n');
74
162
 
@@ -119,5 +207,6 @@ async function explainErrorInteractively(errorText, config) {
119
207
 
120
208
  module.exports = {
121
209
  showInteractiveExplainer,
122
- displayExplanation
210
+ displayExplanation,
211
+ cancelActivePrompt
123
212
  };
package/src/runner.js CHANGED
@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
12
12
  const chokidar = require('chokidar');
13
13
  const chalk = require('chalk');
14
14
  const { explain } = require('./explainer');
15
- const { showInteractiveExplainer } = require('./explainer/interactive');
15
+ const { showInteractiveExplainer, cancelActivePrompt } = require('./explainer/interactive');
16
16
 
17
17
  class Runner {
18
18
  constructor(scriptPath, args = [], config = {}) {
@@ -57,7 +57,7 @@ class Runner {
57
57
  const text = data.toString();
58
58
  process.stderr.write(text);
59
59
  errorBuffer += text;
60
-
60
+
61
61
  // Detect errors in stderr and explain them immediately
62
62
  if (this.isErrorOutput(text)) {
63
63
  this.explainErrorWithDebounce(text);
@@ -85,7 +85,7 @@ class Runner {
85
85
 
86
86
  this.child.on('exit', (code, signal) => {
87
87
  this.isProcessExiting = true;
88
-
88
+
89
89
  if (code !== 0 && code !== null) {
90
90
  // Process exited with error
91
91
  if (!this.isRestarting && errorBuffer) {
@@ -148,15 +148,18 @@ class Runner {
148
148
 
149
149
  this.isRestarting = true;
150
150
 
151
+ // Cancel any active interactive prompt
152
+ cancelActivePrompt();
153
+
151
154
  if (this.child) {
152
155
  this.child.kill('SIGTERM');
153
-
156
+
154
157
  // Wait a bit for graceful shutdown
155
158
  setTimeout(() => {
156
159
  if (this.child && !this.child.killed) {
157
160
  this.child.kill('SIGKILL');
158
161
  }
159
-
162
+
160
163
  setTimeout(() => {
161
164
  this.isRestarting = false;
162
165
  if (reason) {
@@ -175,6 +178,9 @@ class Runner {
175
178
  * Stop the process and watcher
176
179
  */
177
180
  stop() {
181
+ // Cancel any active interactive prompt
182
+ cancelActivePrompt();
183
+
178
184
  if (this.watcher) {
179
185
  this.watcher.close();
180
186
  this.watcher = null;
@@ -228,7 +234,7 @@ class Runner {
228
234
  return;
229
235
  }
230
236
  this.lastErrorExplanationTime = now;
231
-
237
+
232
238
  // Don't explain startup errors that already caused process exit
233
239
  if (this.isProcessExiting) {
234
240
  return;