oxlint-tui 1.0.12 → 1.0.14
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 +1 -1
- package/tui.js +509 -479
package/package.json
CHANGED
package/tui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execSync, exec } from "node:child_process";
|
|
3
|
+
import { execSync, exec, spawn } from "node:child_process";
|
|
4
4
|
import { stdout, stdin, exit, platform, argv } from "node:process";
|
|
5
5
|
import readline from "readline";
|
|
6
6
|
import fs from "node:fs";
|
|
@@ -9,306 +9,336 @@ const OXLINT_VERSION = "1.41.0";
|
|
|
9
9
|
const TSGOLINT_VERSION = "0.11.1";
|
|
10
10
|
|
|
11
11
|
const COLORS = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
reset: "\x1b[0m",
|
|
13
|
+
dim: "\x1b[90m",
|
|
14
|
+
highlight: "\x1b[38;5;110m",
|
|
15
|
+
selectedBg: "\x1b[47m\x1b[30m",
|
|
16
|
+
borderActive: "\x1b[36m",
|
|
17
|
+
borderInactive: "\x1b[90m",
|
|
18
|
+
error: "\x1b[31m",
|
|
19
|
+
warn: "\x1b[33m",
|
|
20
|
+
success: "\x1b[32m",
|
|
21
|
+
info: "\x1b[34m",
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const KEY_MAP = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
k: { type: "MOVE_UP" },
|
|
26
|
+
up: { type: "MOVE_UP" },
|
|
27
|
+
down: { type: "MOVE_DOWN" },
|
|
28
|
+
j: { type: "MOVE_DOWN" },
|
|
29
|
+
left: { type: "MOVE_LEFT" },
|
|
30
|
+
h: { type: "MOVE_LEFT" },
|
|
31
|
+
right: { type: "MOVE_RIGHT" },
|
|
32
|
+
l: { type: "MOVE_RIGHT" },
|
|
33
|
+
return: { type: "OPEN_DOCS" },
|
|
34
|
+
enter: { type: "OPEN_DOCS" },
|
|
35
|
+
1: { type: "SET_STATUS", value: "off" },
|
|
36
|
+
2: { type: "SET_STATUS", value: "warn" },
|
|
37
|
+
3: { type: "SET_STATUS", value: "error" },
|
|
38
|
+
q: { type: "EXIT" },
|
|
39
|
+
r: { type: "RUN_LINT" },
|
|
40
|
+
t: { type: "RUN_TYPE_AWARE_LINT" },
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
let state = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
activePane: 0,
|
|
45
|
+
selectedCatIdx: 0,
|
|
46
|
+
selectedRuleIdx: 0,
|
|
47
|
+
scrollCat: 0,
|
|
48
|
+
scrollRule: 0,
|
|
49
|
+
isLinting: false,
|
|
50
|
+
message: "oxlint-tui",
|
|
51
|
+
messageType: "dim",
|
|
52
|
+
...loadRules(),
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
// TODO: Nukes comments in the json file. Find the most minimal way to avoid this, while preserving formatting
|
|
56
56
|
function updateConfig(rule, newStatus) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
57
|
+
if (!state.configPath) return;
|
|
58
|
+
try {
|
|
59
|
+
if (!state.config.rules) state.config.rules = {};
|
|
60
|
+
const ruleName = rule.value;
|
|
61
|
+
const canonicalKey =
|
|
62
|
+
rule.scope === "oxc2" || rule.scope === "eslint"
|
|
63
|
+
? ruleName
|
|
64
|
+
: `${rule.scope}/${ruleName}`;
|
|
65
|
+
const existingKey = Object.keys(state.config.rules).find(
|
|
66
|
+
(key) =>
|
|
67
|
+
key === canonicalKey ||
|
|
68
|
+
key === ruleName ||
|
|
69
|
+
key.endsWith(`/${ruleName}`),
|
|
70
|
+
);
|
|
71
|
+
const targetKey = existingKey || canonicalKey;
|
|
72
|
+
state.config.rules[targetKey] = newStatus;
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
state.configPath,
|
|
75
|
+
JSON.stringify(state.config, null, 2),
|
|
76
|
+
"utf8",
|
|
77
|
+
);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
state.message = "Failed to write config file";
|
|
80
|
+
state.messageType = "error";
|
|
81
|
+
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function runLint(type_aware) {
|
|
85
|
-
|
|
85
|
+
if (state.isLinting) return;
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
state.messageType = "info";
|
|
92
|
-
render();
|
|
87
|
+
state.isLinting = true;
|
|
88
|
+
state.message = type_aware ? "Linting with --type-aware)..." : "Linting...";
|
|
89
|
+
state.messageType = "info";
|
|
90
|
+
render();
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
92
|
+
const npxCmd = platform === "win32" ? "npx.cmd" : "npx";
|
|
93
|
+
|
|
94
|
+
const args = type_aware
|
|
95
|
+
? ["-q", "--yes", "--package", `oxlint@${OXLINT_VERSION}`, "--package", `oxlint-tsgolint@${TSGOLINT_VERSION}`, "--", "oxlint", "--type-aware",]
|
|
96
|
+
: ["-q", "--yes", "--package", `oxlint@${OXLINT_VERSION}`, "--", "oxlint"];
|
|
97
|
+
|
|
98
|
+
const child = spawn(npxCmd, args);
|
|
99
|
+
|
|
100
|
+
let stdoutData = "";
|
|
101
|
+
let stderrData = "";
|
|
102
|
+
|
|
103
|
+
child.stdout.on("data", (data) => {
|
|
104
|
+
stdoutData += data;
|
|
105
|
+
});
|
|
106
|
+
child.stderr.on("data", (data) => {
|
|
107
|
+
stderrData += data;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
child.on("close", (code) => {
|
|
111
|
+
state.isLinting = false;
|
|
112
|
+
|
|
113
|
+
const fullOutput = stdoutData + stderrData;
|
|
114
|
+
const summaryMatch = fullOutput.match(
|
|
115
|
+
/Found (\d+) warnings? and (\d+) errors?/i,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (summaryMatch) {
|
|
119
|
+
state.message = summaryMatch[0];
|
|
120
|
+
const errors = parseInt(summaryMatch[2]);
|
|
121
|
+
state.messageType = errors > 0 ? "error" : "warn";
|
|
122
|
+
} else if (
|
|
123
|
+
stdoutData.toLowerCase().includes("finished") ||
|
|
124
|
+
(code === 0 && stdoutData.length < 200)
|
|
125
|
+
) {
|
|
126
|
+
state.message = "Linting passed! 0 issues found.";
|
|
127
|
+
state.messageType = "success";
|
|
128
|
+
} else {
|
|
129
|
+
const cleanError = stderrData
|
|
130
|
+
.split("\n")
|
|
131
|
+
.filter(
|
|
132
|
+
(l) =>
|
|
133
|
+
!l.includes("experimental") &&
|
|
134
|
+
!l.includes("Breaking changes") &&
|
|
135
|
+
l.trim() !== "",
|
|
136
|
+
)
|
|
137
|
+
.join(" ");
|
|
138
|
+
|
|
139
|
+
state.message = cleanError
|
|
140
|
+
? `Error: ${cleanError.substring(0, 50)}...`
|
|
141
|
+
: "Lint failed";
|
|
142
|
+
state.messageType = "error";
|
|
143
|
+
}
|
|
144
|
+
render();
|
|
145
|
+
});
|
|
116
146
|
}
|
|
117
147
|
|
|
118
148
|
function reducer(state, action) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
case "MOVE_RIGHT":
|
|
155
|
-
if (activePane !== 1) return { ...state, activePane: activePane + 1 };
|
|
156
|
-
return state;
|
|
157
|
-
|
|
158
|
-
case "MOVE_LEFT":
|
|
159
|
-
if (activePane !== 0) return { ...state, activePane: activePane - 1 };
|
|
160
|
-
return state;
|
|
161
|
-
case "MOVE_UP":
|
|
162
|
-
if (activePane === 0) {
|
|
163
|
-
const nextIdx =
|
|
164
|
-
selectedCatIdx === 0 ? categories.length - 1 : selectedCatIdx - 1;
|
|
165
|
-
return {
|
|
166
|
-
...state,
|
|
167
|
-
selectedCatIdx: nextIdx,
|
|
168
|
-
selectedRuleIdx: 0,
|
|
169
|
-
scrollRule: 0,
|
|
170
|
-
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight),
|
|
171
|
-
};
|
|
172
|
-
} else if (activePane === 1) {
|
|
173
|
-
const nextIdx =
|
|
174
|
-
selectedRuleIdx === 0 ? currentRules.length - 1 : selectedRuleIdx - 1;
|
|
175
|
-
return {
|
|
176
|
-
...state,
|
|
177
|
-
selectedRuleIdx: nextIdx,
|
|
178
|
-
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
return state;
|
|
182
|
-
case "MOVE_DOWN":
|
|
183
|
-
if (activePane === 0) {
|
|
184
|
-
const nextIdx =
|
|
185
|
-
selectedCatIdx === categories.length - 1 ? 0 : selectedCatIdx + 1;
|
|
186
|
-
return {
|
|
187
|
-
...state,
|
|
188
|
-
selectedCatIdx: nextIdx,
|
|
189
|
-
selectedRuleIdx: 0,
|
|
190
|
-
scrollRule: 0,
|
|
191
|
-
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight),
|
|
192
|
-
};
|
|
193
|
-
} else if (activePane === 1) {
|
|
194
|
-
const nextIdx =
|
|
195
|
-
selectedRuleIdx === currentRules.length - 1 ? 0 : selectedRuleIdx + 1;
|
|
196
|
-
return {
|
|
197
|
-
...state,
|
|
198
|
-
selectedRuleIdx: nextIdx,
|
|
199
|
-
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
return state;
|
|
203
|
-
default:
|
|
204
|
-
return state;
|
|
149
|
+
const {
|
|
150
|
+
categories,
|
|
151
|
+
rulesByCategory,
|
|
152
|
+
selectedCatIdx,
|
|
153
|
+
selectedRuleIdx,
|
|
154
|
+
activePane,
|
|
155
|
+
} = state;
|
|
156
|
+
const currentCat = categories[selectedCatIdx];
|
|
157
|
+
const currentRules = rulesByCategory[currentCat] || [];
|
|
158
|
+
const viewHeight = stdout.rows - 8;
|
|
159
|
+
const statsHeight = 7;
|
|
160
|
+
const catViewHeight = viewHeight - statsHeight;
|
|
161
|
+
|
|
162
|
+
switch (action.type) {
|
|
163
|
+
case "SET_STATUS": {
|
|
164
|
+
if (activePane !== 1) return state;
|
|
165
|
+
const rule = currentRules[selectedRuleIdx];
|
|
166
|
+
if (!rule) return state;
|
|
167
|
+
updateConfig(rule, action.value);
|
|
168
|
+
const updatedRules = [...currentRules];
|
|
169
|
+
updatedRules[selectedRuleIdx] = {
|
|
170
|
+
...rule,
|
|
171
|
+
configStatus: action.value,
|
|
172
|
+
isActive: action.value === "error" || action.value === "warn",
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
...state,
|
|
176
|
+
message: `Rule '${rule.value}' set to: ${action.value}`,
|
|
177
|
+
messageType: "info",
|
|
178
|
+
rulesByCategory: {
|
|
179
|
+
...rulesByCategory,
|
|
180
|
+
[currentCat]: updatedRules,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
205
183
|
}
|
|
184
|
+
case "MOVE_RIGHT":
|
|
185
|
+
if (activePane !== 1) return { ...state, activePane: activePane + 1 };
|
|
186
|
+
return state;
|
|
187
|
+
|
|
188
|
+
case "MOVE_LEFT":
|
|
189
|
+
if (activePane !== 0) return { ...state, activePane: activePane - 1 };
|
|
190
|
+
return state;
|
|
191
|
+
case "MOVE_UP":
|
|
192
|
+
if (activePane === 0) {
|
|
193
|
+
const nextIdx =
|
|
194
|
+
selectedCatIdx === 0 ? categories.length - 1 : selectedCatIdx - 1;
|
|
195
|
+
return {
|
|
196
|
+
...state,
|
|
197
|
+
selectedCatIdx: nextIdx,
|
|
198
|
+
selectedRuleIdx: 0,
|
|
199
|
+
scrollRule: 0,
|
|
200
|
+
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight),
|
|
201
|
+
};
|
|
202
|
+
} else if (activePane === 1) {
|
|
203
|
+
const nextIdx =
|
|
204
|
+
selectedRuleIdx === 0 ? currentRules.length - 1 : selectedRuleIdx - 1;
|
|
205
|
+
return {
|
|
206
|
+
...state,
|
|
207
|
+
selectedRuleIdx: nextIdx,
|
|
208
|
+
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return state;
|
|
212
|
+
case "MOVE_DOWN":
|
|
213
|
+
if (activePane === 0) {
|
|
214
|
+
const nextIdx =
|
|
215
|
+
selectedCatIdx === categories.length - 1 ? 0 : selectedCatIdx + 1;
|
|
216
|
+
return {
|
|
217
|
+
...state,
|
|
218
|
+
selectedCatIdx: nextIdx,
|
|
219
|
+
selectedRuleIdx: 0,
|
|
220
|
+
scrollRule: 0,
|
|
221
|
+
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight),
|
|
222
|
+
};
|
|
223
|
+
} else if (activePane === 1) {
|
|
224
|
+
const nextIdx =
|
|
225
|
+
selectedRuleIdx === currentRules.length - 1 ? 0 : selectedRuleIdx + 1;
|
|
226
|
+
return {
|
|
227
|
+
...state,
|
|
228
|
+
selectedRuleIdx: nextIdx,
|
|
229
|
+
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return state;
|
|
233
|
+
default:
|
|
234
|
+
return state;
|
|
235
|
+
}
|
|
206
236
|
}
|
|
207
237
|
|
|
208
238
|
function getRuleStatus(ruleName, category, config) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
if (val !== undefined) return Array.isArray(val) ? val[0] : val;
|
|
239
|
+
if (config.rules) {
|
|
240
|
+
let val = config.rules[ruleName];
|
|
241
|
+
if (val === undefined) {
|
|
242
|
+
const foundKey = Object.keys(config.rules).find((key) =>
|
|
243
|
+
key.endsWith(`/${ruleName}`),
|
|
244
|
+
);
|
|
245
|
+
if (foundKey) val = config.rules[foundKey];
|
|
218
246
|
}
|
|
219
|
-
return (
|
|
247
|
+
if (val !== undefined) return Array.isArray(val) ? val[0] : val;
|
|
248
|
+
}
|
|
249
|
+
return (config.categories && config.categories[category]) || "off";
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
function stripJsonComments(json) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
253
|
+
return json.replace(
|
|
254
|
+
/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
|
|
255
|
+
(m, g) => (g ? "" : m),
|
|
256
|
+
);
|
|
227
257
|
}
|
|
228
258
|
|
|
229
259
|
function loadRules() {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const userConfigPath = argv[2];
|
|
254
|
-
if (userConfigPath && fs.existsSync(userConfigPath)) {
|
|
255
|
-
configPath = userConfigPath;
|
|
256
|
-
} else if (fs.existsSync(".oxlintrc.json")) {
|
|
257
|
-
configPath = ".oxlintrc.json";
|
|
258
|
-
}
|
|
260
|
+
let rulesData;
|
|
261
|
+
let config = {
|
|
262
|
+
rules: {},
|
|
263
|
+
categories: {},
|
|
264
|
+
};
|
|
265
|
+
let configPath = null;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const raw = execSync(
|
|
269
|
+
`npx -q --yes oxlint@${OXLINT_VERSION} --rules --format=json`,
|
|
270
|
+
{
|
|
271
|
+
encoding: "utf8",
|
|
272
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
rulesData = JSON.parse(raw);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.error(
|
|
278
|
+
`${COLORS.error}Error: Could not run 'npx oxlint'.${COLORS.reset}`,
|
|
279
|
+
);
|
|
280
|
+
exit(1);
|
|
281
|
+
}
|
|
259
282
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
283
|
+
const userConfigPath = argv[2];
|
|
284
|
+
if (userConfigPath && fs.existsSync(userConfigPath)) {
|
|
285
|
+
configPath = userConfigPath;
|
|
286
|
+
} else if (fs.existsSync(".oxlintrc.json")) {
|
|
287
|
+
configPath = ".oxlintrc.json";
|
|
288
|
+
}
|
|
267
289
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
290
|
+
if (configPath) {
|
|
291
|
+
try {
|
|
292
|
+
config = JSON.parse(
|
|
293
|
+
stripJsonComments(fs.readFileSync(configPath, "utf8")),
|
|
294
|
+
);
|
|
295
|
+
} catch (e) { }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const map = {};
|
|
299
|
+
rulesData.forEach((rule) => {
|
|
300
|
+
const cat = rule.category || "Uncategorized";
|
|
301
|
+
if (!map[cat]) map[cat] = [];
|
|
302
|
+
const status = getRuleStatus(rule.value, cat, config);
|
|
303
|
+
map[cat].push({
|
|
304
|
+
...rule,
|
|
305
|
+
configStatus: status,
|
|
306
|
+
isActive: status === "error" || status === "warn",
|
|
278
307
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const categories = Object.keys(map).sort();
|
|
311
|
+
return {
|
|
312
|
+
categories,
|
|
313
|
+
rulesByCategory: map,
|
|
314
|
+
config,
|
|
315
|
+
configPath,
|
|
316
|
+
};
|
|
287
317
|
}
|
|
288
318
|
|
|
289
319
|
function updateScroll(idx, currentScroll, viewHeight) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
320
|
+
if (idx < currentScroll) return idx;
|
|
321
|
+
if (idx >= currentScroll + viewHeight) return idx - viewHeight + 1;
|
|
322
|
+
return currentScroll;
|
|
293
323
|
}
|
|
294
324
|
|
|
295
325
|
function openUrl(url) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
326
|
+
if (!url) return;
|
|
327
|
+
const cmd =
|
|
328
|
+
platform === "darwin"
|
|
329
|
+
? "open"
|
|
330
|
+
: platform === "win32"
|
|
331
|
+
? "start"
|
|
332
|
+
: "xdg-open";
|
|
333
|
+
exec(`${cmd} "${url}"`);
|
|
304
334
|
}
|
|
305
335
|
|
|
306
336
|
function chunkString(str, len) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
337
|
+
if (!str) return [];
|
|
338
|
+
const size = Math.ceil(str.length / len);
|
|
339
|
+
const r = Array(size);
|
|
340
|
+
for (let i = 0; i < size; i++) r[i] = str.substring(i * len, (i + 1) * len);
|
|
341
|
+
return r;
|
|
312
342
|
}
|
|
313
343
|
|
|
314
344
|
const write = (str) => stdout.write(str);
|
|
@@ -316,250 +346,250 @@ const enterAltScreen = () => write("\x1b[?1049h\x1b[?25l");
|
|
|
316
346
|
const exitAltScreen = () => write("\x1b[?1049l\x1b[?25h");
|
|
317
347
|
|
|
318
348
|
function drawBox(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
349
|
+
buffer,
|
|
350
|
+
x,
|
|
351
|
+
y,
|
|
352
|
+
width,
|
|
353
|
+
height,
|
|
354
|
+
title,
|
|
355
|
+
items,
|
|
356
|
+
selectedIdx,
|
|
357
|
+
scrollOffset,
|
|
358
|
+
isActive,
|
|
329
359
|
) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
buffer.push(
|
|
341
|
-
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
342
|
-
);
|
|
343
|
-
}
|
|
360
|
+
const borderColor = isActive ? COLORS.borderActive : COLORS.borderInactive;
|
|
361
|
+
const titleClean =
|
|
362
|
+
title.length > width - 6 ? title.substring(0, width - 7) + "…" : title;
|
|
363
|
+
const topBorder = `${borderColor}┌─ ${titleClean} `.padEnd(
|
|
364
|
+
width + borderColor.length - 1,
|
|
365
|
+
"─",
|
|
366
|
+
);
|
|
367
|
+
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
368
|
+
|
|
369
|
+
for (let i = 1; i < height - 1; i++) {
|
|
344
370
|
buffer.push(
|
|
345
|
-
|
|
371
|
+
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
346
372
|
);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
373
|
+
}
|
|
374
|
+
buffer.push(
|
|
375
|
+
`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const innerHeight = height - 2;
|
|
379
|
+
items.slice(scrollOffset, scrollOffset + innerHeight).forEach((item, i) => {
|
|
380
|
+
const absIdx = scrollOffset + i;
|
|
381
|
+
const rawText = (item.value || item).toString();
|
|
382
|
+
let display =
|
|
383
|
+
rawText.length > width - 4
|
|
384
|
+
? rawText.substring(0, width - 5) + "…"
|
|
385
|
+
: rawText.padEnd(width - 4);
|
|
386
|
+
let itemColor = COLORS.dim;
|
|
387
|
+
if (item.configStatus === "error") itemColor = COLORS.error;
|
|
388
|
+
else if (item.configStatus === "warn") itemColor = COLORS.warn;
|
|
389
|
+
else if (item.isActive) itemColor = COLORS.success;
|
|
390
|
+
|
|
391
|
+
buffer.push(`\x1b[${y + 1 + i};${x + 2}H`);
|
|
392
|
+
if (absIdx === selectedIdx) {
|
|
393
|
+
buffer.push(
|
|
394
|
+
isActive
|
|
395
|
+
? `${COLORS.selectedBg}${display}${COLORS.reset}`
|
|
396
|
+
: `${COLORS.dim}\x1b[7m${display}${COLORS.reset}`,
|
|
397
|
+
);
|
|
398
|
+
} else {
|
|
399
|
+
buffer.push(`${itemColor}${display}${COLORS.reset}`);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
372
402
|
}
|
|
373
403
|
|
|
374
404
|
function drawStats(buffer, x, y, width, height, rules) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
buffer.push(
|
|
383
|
-
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
384
|
-
);
|
|
405
|
+
const borderColor = COLORS.borderInactive;
|
|
406
|
+
const topBorder = `${borderColor}┌─ STATS `.padEnd(
|
|
407
|
+
width + borderColor.length - 1,
|
|
408
|
+
"─",
|
|
409
|
+
);
|
|
410
|
+
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
411
|
+
for (let i = 1; i < height - 1; i++)
|
|
385
412
|
buffer.push(
|
|
386
|
-
|
|
413
|
+
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
387
414
|
);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
415
|
+
buffer.push(
|
|
416
|
+
`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
let counts = { error: 0, warn: 0, off: 0 };
|
|
420
|
+
rules.forEach((r) => {
|
|
421
|
+
if (r.configStatus === "error") counts.error++;
|
|
422
|
+
else if (r.configStatus === "warn") counts.warn++;
|
|
423
|
+
else counts.off++;
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const lines = [
|
|
427
|
+
{ label: "Error", count: counts.error, color: COLORS.error },
|
|
428
|
+
{ label: "Warn", count: counts.warn, color: COLORS.warn },
|
|
429
|
+
{ label: "Off", count: counts.off, color: COLORS.dim },
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
lines.forEach((line, i) => {
|
|
433
|
+
if (i < height - 2) {
|
|
434
|
+
const numStr = String(line.count).padStart(3);
|
|
435
|
+
const labelStr = line.label.padEnd(width - 8);
|
|
436
|
+
buffer.push(
|
|
437
|
+
`\x1b[${y + 1 + i};${x + 2}H${line.color}${labelStr}${numStr}${COLORS.reset}`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
411
441
|
}
|
|
412
442
|
|
|
413
443
|
function drawDetails(buffer, x, y, width, height, rule, isActive) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
444
|
+
const borderColor = isActive ? COLORS.borderActive : COLORS.borderInactive;
|
|
445
|
+
const topBorder = `${borderColor}┌─ DETAILS `.padEnd(
|
|
446
|
+
width + borderColor.length - 1,
|
|
447
|
+
"─",
|
|
448
|
+
);
|
|
449
|
+
buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
|
|
450
|
+
for (let i = 1; i < height - 1; i++)
|
|
451
|
+
buffer.push(
|
|
452
|
+
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
418
453
|
);
|
|
419
|
-
|
|
420
|
-
|
|
454
|
+
buffer.push(
|
|
455
|
+
`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
if (!rule) return;
|
|
459
|
+
|
|
460
|
+
let statusDisplay = rule.configStatus.toUpperCase();
|
|
461
|
+
if (rule.configStatus === "error")
|
|
462
|
+
statusDisplay = `${COLORS.error}${statusDisplay}${COLORS.reset}`;
|
|
463
|
+
else if (rule.configStatus === "warn")
|
|
464
|
+
statusDisplay = `${COLORS.warn}${statusDisplay}${COLORS.reset}`;
|
|
465
|
+
else statusDisplay = `${COLORS.dim}${statusDisplay}${COLORS.reset}`;
|
|
466
|
+
|
|
467
|
+
const labels = [
|
|
468
|
+
["Name", rule.value],
|
|
469
|
+
["Status", statusDisplay],
|
|
470
|
+
["Category", rule.category],
|
|
471
|
+
["Scope", rule.scope],
|
|
472
|
+
["Fix", rule.fix],
|
|
473
|
+
["Default", rule.default ? "Yes" : "No"],
|
|
474
|
+
["Type-aware", rule.type_aware ? "Yes" : "No"],
|
|
475
|
+
["Docs", `Hit ${COLORS.highlight}ENTER${COLORS.reset} to open docs`],
|
|
476
|
+
];
|
|
477
|
+
|
|
478
|
+
let line = 0;
|
|
479
|
+
labels.forEach(([lbl, val]) => {
|
|
480
|
+
if (lbl === "Status" && line < height - 2) {
|
|
481
|
+
buffer.push(
|
|
482
|
+
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${val}`,
|
|
483
|
+
);
|
|
484
|
+
line++;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const chunks = chunkString(String(val || "N/A"), width - 15);
|
|
488
|
+
chunks.forEach((chunk) => {
|
|
489
|
+
if (line < height - 2) {
|
|
421
490
|
buffer.push(
|
|
422
|
-
|
|
491
|
+
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${chunk}`,
|
|
423
492
|
);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
if (!rule) return;
|
|
429
|
-
|
|
430
|
-
let statusDisplay = rule.configStatus.toUpperCase();
|
|
431
|
-
if (rule.configStatus === "error")
|
|
432
|
-
statusDisplay = `${COLORS.error}${statusDisplay}${COLORS.reset}`;
|
|
433
|
-
else if (rule.configStatus === "warn")
|
|
434
|
-
statusDisplay = `${COLORS.warn}${statusDisplay}${COLORS.reset}`;
|
|
435
|
-
else statusDisplay = `${COLORS.dim}${statusDisplay}${COLORS.reset}`;
|
|
436
|
-
|
|
437
|
-
const labels = [
|
|
438
|
-
["Name", rule.value],
|
|
439
|
-
["Status", statusDisplay],
|
|
440
|
-
["Category", rule.category],
|
|
441
|
-
["Scope", rule.scope],
|
|
442
|
-
["Fix", rule.fix],
|
|
443
|
-
["Default", rule.default ? "Yes" : "No"],
|
|
444
|
-
["Type-aware", rule.type_aware ? "Yes" : "No"],
|
|
445
|
-
["Docs", `Hit ${COLORS.highlight}ENTER${COLORS.reset} to open docs`],
|
|
446
|
-
];
|
|
447
|
-
|
|
448
|
-
let line = 0;
|
|
449
|
-
labels.forEach(([lbl, val]) => {
|
|
450
|
-
if (lbl === "Status" && line < height - 2) {
|
|
451
|
-
buffer.push(
|
|
452
|
-
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${val}`,
|
|
453
|
-
);
|
|
454
|
-
line++;
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
const chunks = chunkString(String(val || "N/A"), width - 15);
|
|
458
|
-
chunks.forEach((chunk) => {
|
|
459
|
-
if (line < height - 2) {
|
|
460
|
-
buffer.push(
|
|
461
|
-
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${chunk}`,
|
|
462
|
-
);
|
|
463
|
-
line++;
|
|
464
|
-
}
|
|
465
|
-
});
|
|
493
|
+
line++;
|
|
494
|
+
}
|
|
466
495
|
});
|
|
496
|
+
});
|
|
467
497
|
}
|
|
468
498
|
|
|
469
499
|
function render() {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
500
|
+
const { columns, rows } = stdout;
|
|
501
|
+
const currentCat = state.categories[state.selectedCatIdx];
|
|
502
|
+
const rules = state.rulesByCategory[currentCat] || [];
|
|
503
|
+
const rule = rules[state.selectedRuleIdx];
|
|
504
|
+
const boxHeight = rows - 5;
|
|
505
|
+
const col1W = Math.floor(columns * 0.2);
|
|
506
|
+
const col2W = Math.floor(columns * 0.3);
|
|
507
|
+
const col3W = columns - col1W - col2W - 2;
|
|
508
|
+
const statsHeight = 6;
|
|
509
|
+
const catListHeight = boxHeight - statsHeight;
|
|
510
|
+
|
|
511
|
+
const buffer = ["\x1b[H\x1b[J"];
|
|
512
|
+
drawBox(
|
|
513
|
+
buffer,
|
|
514
|
+
1,
|
|
515
|
+
1,
|
|
516
|
+
col1W,
|
|
517
|
+
catListHeight,
|
|
518
|
+
"CATEGORIES",
|
|
519
|
+
state.categories,
|
|
520
|
+
state.selectedCatIdx,
|
|
521
|
+
state.scrollCat,
|
|
522
|
+
state.activePane === 0,
|
|
523
|
+
);
|
|
524
|
+
drawStats(buffer, 1, 1 + catListHeight, col1W, statsHeight, rules);
|
|
525
|
+
drawBox(
|
|
526
|
+
buffer,
|
|
527
|
+
col1W + 1,
|
|
528
|
+
1,
|
|
529
|
+
col2W,
|
|
530
|
+
boxHeight,
|
|
531
|
+
`RULES (${rules.length})`,
|
|
532
|
+
rules,
|
|
533
|
+
state.selectedRuleIdx,
|
|
534
|
+
state.scrollRule,
|
|
535
|
+
state.activePane === 1,
|
|
536
|
+
);
|
|
537
|
+
drawDetails(
|
|
538
|
+
buffer,
|
|
539
|
+
col1W + col2W + 1,
|
|
540
|
+
1,
|
|
541
|
+
col3W,
|
|
542
|
+
boxHeight,
|
|
543
|
+
rule,
|
|
544
|
+
state.activePane === 2,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const msgColor = COLORS[state.messageType] || COLORS.reset;
|
|
548
|
+
buffer.push(
|
|
549
|
+
`\x1b[${rows - 3};2H${msgColor}● ${state.message}${COLORS.reset}`,
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
const footerConfig = state.configPath
|
|
553
|
+
? `Config: ${state.configPath}`
|
|
554
|
+
: "No config loaded";
|
|
555
|
+
buffer.push(
|
|
556
|
+
`\x1b[${rows - 1};2H${COLORS.dim}Arrows/HJKL: Nav | 1-3: Status | R: Lint | T: Lint with --type-aware | Enter: Docs | Q: Quit | ${footerConfig}${COLORS.reset}`,
|
|
557
|
+
);
|
|
558
|
+
write(buffer.join(""));
|
|
529
559
|
}
|
|
530
560
|
|
|
531
561
|
readline.emitKeypressEvents(stdin);
|
|
532
562
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
533
563
|
|
|
534
564
|
stdin.on("keypress", (_, key) => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
565
|
+
const action =
|
|
566
|
+
KEY_MAP[key.name] ||
|
|
567
|
+
(key.ctrl && key.name === "c"
|
|
568
|
+
? { type: "EXIT" }
|
|
569
|
+
: KEY_MAP[key.sequence] || null);
|
|
570
|
+
if (!action) return;
|
|
571
|
+
|
|
572
|
+
if (action.type === "EXIT") {
|
|
573
|
+
exitAltScreen();
|
|
574
|
+
exit(0);
|
|
575
|
+
}
|
|
576
|
+
if (action.type === "RUN_LINT") {
|
|
577
|
+
runLint(false);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (action.type === "RUN_TYPE_AWARE_LINT") {
|
|
581
|
+
runLint(true);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (action.type === "OPEN_DOCS" && state.activePane === 1) {
|
|
585
|
+
const currentCat = state.categories[state.selectedCatIdx];
|
|
586
|
+
const rule = state.rulesByCategory[currentCat]?.[state.selectedRuleIdx];
|
|
587
|
+
if (rule) openUrl(rule.docs_url || rule.url);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
560
590
|
|
|
561
|
-
|
|
562
|
-
|
|
591
|
+
state = reducer(state, action);
|
|
592
|
+
render();
|
|
563
593
|
});
|
|
564
594
|
|
|
565
595
|
stdout.on("resize", render);
|