oxlint-tui 1.0.13 → 1.0.15
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 -492
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,319 +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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
});
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
function reducer(state, action) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
case "MOVE_RIGHT":
|
|
168
|
-
if (activePane !== 1) return { ...state, activePane: activePane + 1 };
|
|
169
|
-
return state;
|
|
170
|
-
|
|
171
|
-
case "MOVE_LEFT":
|
|
172
|
-
if (activePane !== 0) return { ...state, activePane: activePane - 1 };
|
|
173
|
-
return state;
|
|
174
|
-
case "MOVE_UP":
|
|
175
|
-
if (activePane === 0) {
|
|
176
|
-
const nextIdx =
|
|
177
|
-
selectedCatIdx === 0 ? categories.length - 1 : selectedCatIdx - 1;
|
|
178
|
-
return {
|
|
179
|
-
...state,
|
|
180
|
-
selectedCatIdx: nextIdx,
|
|
181
|
-
selectedRuleIdx: 0,
|
|
182
|
-
scrollRule: 0,
|
|
183
|
-
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight),
|
|
184
|
-
};
|
|
185
|
-
} else if (activePane === 1) {
|
|
186
|
-
const nextIdx =
|
|
187
|
-
selectedRuleIdx === 0 ? currentRules.length - 1 : selectedRuleIdx - 1;
|
|
188
|
-
return {
|
|
189
|
-
...state,
|
|
190
|
-
selectedRuleIdx: nextIdx,
|
|
191
|
-
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
return state;
|
|
195
|
-
case "MOVE_DOWN":
|
|
196
|
-
if (activePane === 0) {
|
|
197
|
-
const nextIdx =
|
|
198
|
-
selectedCatIdx === categories.length - 1 ? 0 : selectedCatIdx + 1;
|
|
199
|
-
return {
|
|
200
|
-
...state,
|
|
201
|
-
selectedCatIdx: nextIdx,
|
|
202
|
-
selectedRuleIdx: 0,
|
|
203
|
-
scrollRule: 0,
|
|
204
|
-
scrollCat: updateScroll(nextIdx, state.scrollCat, catViewHeight),
|
|
205
|
-
};
|
|
206
|
-
} else if (activePane === 1) {
|
|
207
|
-
const nextIdx =
|
|
208
|
-
selectedRuleIdx === currentRules.length - 1 ? 0 : selectedRuleIdx + 1;
|
|
209
|
-
return {
|
|
210
|
-
...state,
|
|
211
|
-
selectedRuleIdx: nextIdx,
|
|
212
|
-
scrollRule: updateScroll(nextIdx, state.scrollRule, viewHeight),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
return state;
|
|
216
|
-
default:
|
|
217
|
-
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
|
+
};
|
|
218
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
|
+
}
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
function getRuleStatus(ruleName, category, config) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
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];
|
|
231
246
|
}
|
|
232
|
-
return (
|
|
247
|
+
if (val !== undefined) return Array.isArray(val) ? val[0] : val;
|
|
248
|
+
}
|
|
249
|
+
return (config.categories && config.categories[category]) || "off";
|
|
233
250
|
}
|
|
234
251
|
|
|
235
252
|
function stripJsonComments(json) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
253
|
+
return json.replace(
|
|
254
|
+
/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
|
|
255
|
+
(m, g) => (g ? "" : m),
|
|
256
|
+
);
|
|
240
257
|
}
|
|
241
258
|
|
|
242
259
|
function loadRules() {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const userConfigPath = argv[2];
|
|
267
|
-
if (userConfigPath && fs.existsSync(userConfigPath)) {
|
|
268
|
-
configPath = userConfigPath;
|
|
269
|
-
} else if (fs.existsSync(".oxlintrc.json")) {
|
|
270
|
-
configPath = ".oxlintrc.json";
|
|
271
|
-
}
|
|
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
|
+
}
|
|
272
282
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
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
|
+
}
|
|
280
289
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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",
|
|
291
307
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const categories = Object.keys(map).sort();
|
|
311
|
+
return {
|
|
312
|
+
categories,
|
|
313
|
+
rulesByCategory: map,
|
|
314
|
+
config,
|
|
315
|
+
configPath,
|
|
316
|
+
};
|
|
300
317
|
}
|
|
301
318
|
|
|
302
319
|
function updateScroll(idx, currentScroll, viewHeight) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
320
|
+
if (idx < currentScroll) return idx;
|
|
321
|
+
if (idx >= currentScroll + viewHeight) return idx - viewHeight + 1;
|
|
322
|
+
return currentScroll;
|
|
306
323
|
}
|
|
307
324
|
|
|
308
325
|
function openUrl(url) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
326
|
+
if (!url) return;
|
|
327
|
+
const cmd =
|
|
328
|
+
platform === "darwin"
|
|
329
|
+
? "open"
|
|
330
|
+
: platform === "win32"
|
|
331
|
+
? "start"
|
|
332
|
+
: "xdg-open";
|
|
333
|
+
exec(`${cmd} "${url}"`);
|
|
317
334
|
}
|
|
318
335
|
|
|
319
336
|
function chunkString(str, len) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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;
|
|
325
342
|
}
|
|
326
343
|
|
|
327
344
|
const write = (str) => stdout.write(str);
|
|
@@ -329,250 +346,250 @@ const enterAltScreen = () => write("\x1b[?1049h\x1b[?25l");
|
|
|
329
346
|
const exitAltScreen = () => write("\x1b[?1049l\x1b[?25h");
|
|
330
347
|
|
|
331
348
|
function drawBox(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
349
|
+
buffer,
|
|
350
|
+
x,
|
|
351
|
+
y,
|
|
352
|
+
width,
|
|
353
|
+
height,
|
|
354
|
+
title,
|
|
355
|
+
items,
|
|
356
|
+
selectedIdx,
|
|
357
|
+
scrollOffset,
|
|
358
|
+
isActive,
|
|
342
359
|
) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
buffer.push(
|
|
354
|
-
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
355
|
-
);
|
|
356
|
-
}
|
|
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++) {
|
|
357
370
|
buffer.push(
|
|
358
|
-
|
|
371
|
+
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
359
372
|
);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
});
|
|
385
402
|
}
|
|
386
403
|
|
|
387
404
|
function drawStats(buffer, x, y, width, height, rules) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
buffer.push(
|
|
396
|
-
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
397
|
-
);
|
|
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++)
|
|
398
412
|
buffer.push(
|
|
399
|
-
|
|
413
|
+
`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`,
|
|
400
414
|
);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
});
|
|
424
441
|
}
|
|
425
442
|
|
|
426
443
|
function drawDetails(buffer, x, y, width, height, rule, isActive) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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}`,
|
|
431
453
|
);
|
|
432
|
-
|
|
433
|
-
|
|
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) {
|
|
434
490
|
buffer.push(
|
|
435
|
-
|
|
491
|
+
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${chunk}`,
|
|
436
492
|
);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
);
|
|
440
|
-
|
|
441
|
-
if (!rule) return;
|
|
442
|
-
|
|
443
|
-
let statusDisplay = rule.configStatus.toUpperCase();
|
|
444
|
-
if (rule.configStatus === "error")
|
|
445
|
-
statusDisplay = `${COLORS.error}${statusDisplay}${COLORS.reset}`;
|
|
446
|
-
else if (rule.configStatus === "warn")
|
|
447
|
-
statusDisplay = `${COLORS.warn}${statusDisplay}${COLORS.reset}`;
|
|
448
|
-
else statusDisplay = `${COLORS.dim}${statusDisplay}${COLORS.reset}`;
|
|
449
|
-
|
|
450
|
-
const labels = [
|
|
451
|
-
["Name", rule.value],
|
|
452
|
-
["Status", statusDisplay],
|
|
453
|
-
["Category", rule.category],
|
|
454
|
-
["Scope", rule.scope],
|
|
455
|
-
["Fix", rule.fix],
|
|
456
|
-
["Default", rule.default ? "Yes" : "No"],
|
|
457
|
-
["Type-aware", rule.type_aware ? "Yes" : "No"],
|
|
458
|
-
["Docs", `Hit ${COLORS.highlight}ENTER${COLORS.reset} to open docs`],
|
|
459
|
-
];
|
|
460
|
-
|
|
461
|
-
let line = 0;
|
|
462
|
-
labels.forEach(([lbl, val]) => {
|
|
463
|
-
if (lbl === "Status" && line < height - 2) {
|
|
464
|
-
buffer.push(
|
|
465
|
-
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${val}`,
|
|
466
|
-
);
|
|
467
|
-
line++;
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
const chunks = chunkString(String(val || "N/A"), width - 15);
|
|
471
|
-
chunks.forEach((chunk) => {
|
|
472
|
-
if (line < height - 2) {
|
|
473
|
-
buffer.push(
|
|
474
|
-
`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(10)} ${COLORS.reset}${chunk}`,
|
|
475
|
-
);
|
|
476
|
-
line++;
|
|
477
|
-
}
|
|
478
|
-
});
|
|
493
|
+
line++;
|
|
494
|
+
}
|
|
479
495
|
});
|
|
496
|
+
});
|
|
480
497
|
}
|
|
481
498
|
|
|
482
499
|
function render() {
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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(""));
|
|
542
559
|
}
|
|
543
560
|
|
|
544
561
|
readline.emitKeypressEvents(stdin);
|
|
545
562
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
546
563
|
|
|
547
564
|
stdin.on("keypress", (_, key) => {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
+
}
|
|
573
590
|
|
|
574
|
-
|
|
575
|
-
|
|
591
|
+
state = reducer(state, action);
|
|
592
|
+
render();
|
|
576
593
|
});
|
|
577
594
|
|
|
578
595
|
stdout.on("resize", render);
|