oxlint-tui 1.2.0 → 1.3.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/dist/index.js DELETED
@@ -1,360 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import readline from "readline";
4
- import { execSync, spawn } from "node:child_process";
5
- import { stdout, stdin, exit, platform, argv } from "node:process";
6
- import { join, dirname } from "node:path";
7
- import { fileURLToPath } from "node:url";
8
- import { render } from "./rendering.js";
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
- const OXLINT_VERSION = "1.41.0";
12
- const TSGOLINT_VERSION = "0.11.1";
13
- const KEY_MAP = {
14
- k: { type: "MOVE_UP" },
15
- up: { type: "MOVE_UP" },
16
- down: { type: "MOVE_DOWN" },
17
- j: { type: "MOVE_DOWN" },
18
- left: { type: "MOVE_LEFT" },
19
- h: { type: "MOVE_LEFT" },
20
- right: { type: "MOVE_RIGHT" },
21
- l: { type: "MOVE_RIGHT" },
22
- return: { type: "OPEN_DOCS" },
23
- enter: { type: "OPEN_DOCS" },
24
- "1": { type: "SET_STATUS", value: "off" },
25
- "2": { type: "SET_STATUS", value: "warn" },
26
- "3": { type: "SET_STATUS", value: "error" },
27
- q: { type: "EXIT" },
28
- r: { type: "RUN_LINT" },
29
- x: { type: "RUN_SINGLE_RULE" },
30
- };
31
- export let state = {
32
- activePane: 0,
33
- selectedCategoryIndex: 0,
34
- selectedRuleIndex: 0,
35
- categoryScroll: 0,
36
- ruleScroll: 0,
37
- isLintInProgress: false,
38
- message: "oxlint-tui",
39
- messageType: "dim",
40
- ...loadRules(),
41
- };
42
- function updateConfig(rule, newStatus) {
43
- if (!state.configPath || !state.config)
44
- return;
45
- try {
46
- if (!state.config.rules)
47
- state.config.rules = {};
48
- const ruleName = rule.value;
49
- const canonicalKey = rule.scope === "oxc2" || rule.scope === "eslint" ? ruleName : `${rule.scope}/${ruleName}`;
50
- const rules = state.config.rules;
51
- const existingKey = Object.keys(rules).find((key) => key === canonicalKey || key === ruleName || key.endsWith(`/${ruleName}`));
52
- const targetKey = existingKey || canonicalKey;
53
- rules[targetKey] = newStatus;
54
- fs.writeFileSync(state.configPath, JSON.stringify(state.config, null, 2), "utf8");
55
- }
56
- catch {
57
- state.message = "Failed to write config file";
58
- state.messageType = "error";
59
- }
60
- }
61
- function runLint({ rule = null } = {}) {
62
- if (state.isLintInProgress)
63
- return;
64
- state.isLintInProgress = true;
65
- let ruleName = rule ? `${rule.scope}/${rule.value}` : null;
66
- const typeAware = rule
67
- ? rule.type_aware
68
- : Object.values(state.rulesByCategory)
69
- .flat()
70
- .some((ruleItem) => ruleItem.isActive && ruleItem.type_aware === true);
71
- state.message = "Linting";
72
- if (ruleName)
73
- state.message += ` [${ruleName}]`;
74
- if (typeAware)
75
- state.message += " with --type-aware";
76
- state.message += "...";
77
- state.messageType = "info";
78
- render();
79
- const npxCmd = platform === "win32" ? "npx.cmd" : "npx";
80
- const args = ["-q", "--yes", "--package", `oxlint@${OXLINT_VERSION}`];
81
- if (typeAware) {
82
- args.push("--package", `oxlint-tsgolint@${TSGOLINT_VERSION}`);
83
- }
84
- args.push("--", "oxlint");
85
- if (typeAware) {
86
- args.push("--type-aware");
87
- }
88
- if (ruleName) {
89
- args.push("-A", "all", "-D", ruleName);
90
- }
91
- const child = spawn(npxCmd, args);
92
- let stdoutData = "";
93
- let stderrData = "";
94
- child.stdout.on("data", (data) => {
95
- stdoutData += data;
96
- });
97
- child.stderr.on("data", (data) => {
98
- stderrData += data;
99
- });
100
- child.on("close", (code) => {
101
- state.isLintInProgress = false;
102
- const fullOutput = stdoutData + stderrData;
103
- const summaryMatch = fullOutput.match(/Found (\d+) warnings? and (\d+) errors?/i);
104
- if (summaryMatch) {
105
- const errors = parseInt(summaryMatch[2]);
106
- state.message = ruleName
107
- ? `[${ruleName}] Found ${errors} issue${errors === 1 ? "" : "s"}`
108
- : summaryMatch[0];
109
- state.messageType = errors > 0 ? "error" : "warn";
110
- }
111
- else if (stdoutData.toLowerCase().includes("finished") ||
112
- (code === 0 && stdoutData.length < 200)) {
113
- state.message = "Linting passed! 0 issues found.";
114
- if (ruleName)
115
- state.message = `[${ruleName}] ${state.message}`;
116
- state.messageType = "success";
117
- }
118
- else {
119
- const cleanError = stderrData
120
- .split("\n")
121
- .filter((l) => !l.includes("experimental") && !l.includes("Breaking changes") && l.trim() !== "")
122
- .join(" ");
123
- state.message = cleanError ? `Error: ${cleanError.substring(0, 50)}...` : "Lint failed";
124
- state.messageType = "error";
125
- }
126
- render();
127
- });
128
- }
129
- function execute(action) {
130
- if (!action)
131
- return;
132
- const { categories, rulesByCategory, selectedCategoryIndex, selectedRuleIndex, activePane } = state;
133
- const currentCategory = categories[selectedCategoryIndex];
134
- const currentCategoryRules = rulesByCategory[currentCategory] || [];
135
- const viewportHeight = stdout.rows - 8;
136
- const statsBoxHeight = 7;
137
- const categoryListHeight = viewportHeight - statsBoxHeight;
138
- switch (action.type) {
139
- case "EXIT":
140
- exitAltScreen();
141
- exit(0);
142
- return;
143
- case "RUN_LINT":
144
- runLint();
145
- return;
146
- case "RUN_SINGLE_RULE": {
147
- const rule = currentCategoryRules[selectedRuleIndex];
148
- if (rule)
149
- runLint({ rule });
150
- return;
151
- }
152
- case "OPEN_DOCS": {
153
- if (activePane === 1) {
154
- const rule = currentCategoryRules[selectedRuleIndex];
155
- if (rule)
156
- openUrl(rule.docs_url || rule.url);
157
- }
158
- return;
159
- }
160
- case "SET_STATUS": {
161
- if (activePane !== 1 || !action.value)
162
- return;
163
- const rule = currentCategoryRules[selectedRuleIndex];
164
- if (!rule)
165
- return;
166
- updateConfig(rule, action.value);
167
- const updatedRules = [...currentCategoryRules];
168
- updatedRules[selectedRuleIndex] = {
169
- ...rule,
170
- configStatus: action.value,
171
- isActive: action.value === "error" || action.value === "warn",
172
- };
173
- state = {
174
- ...state,
175
- message: `Rule '${rule.value}' set to: ${action.value}`,
176
- messageType: "info",
177
- rulesByCategory: {
178
- ...rulesByCategory,
179
- [currentCategory]: updatedRules,
180
- },
181
- };
182
- render();
183
- return;
184
- }
185
- case "MOVE_RIGHT":
186
- if (activePane !== 1) {
187
- state = { ...state, activePane: activePane + 1 };
188
- render();
189
- }
190
- return;
191
- case "MOVE_LEFT":
192
- if (activePane !== 0) {
193
- state = { ...state, activePane: activePane - 1 };
194
- render();
195
- }
196
- return;
197
- case "MOVE_UP":
198
- if (activePane === 0) {
199
- const nextIndex = selectedCategoryIndex === 0 ? categories.length - 1 : selectedCategoryIndex - 1;
200
- state = {
201
- ...state,
202
- selectedCategoryIndex: nextIndex,
203
- selectedRuleIndex: 0,
204
- ruleScroll: 0,
205
- categoryScroll: updateScroll(nextIndex, state.categoryScroll, categoryListHeight),
206
- };
207
- }
208
- else if (activePane === 1) {
209
- const nextIndex = selectedRuleIndex === 0 ? currentCategoryRules.length - 1 : selectedRuleIndex - 1;
210
- state = {
211
- ...state,
212
- selectedRuleIndex: nextIndex,
213
- ruleScroll: updateScroll(nextIndex, state.ruleScroll, viewportHeight),
214
- };
215
- }
216
- render();
217
- return;
218
- case "MOVE_DOWN":
219
- if (activePane === 0) {
220
- const nextIndex = selectedCategoryIndex === categories.length - 1 ? 0 : selectedCategoryIndex + 1;
221
- state = {
222
- ...state,
223
- selectedCategoryIndex: nextIndex,
224
- selectedRuleIndex: 0,
225
- ruleScroll: 0,
226
- categoryScroll: updateScroll(nextIndex, state.categoryScroll, categoryListHeight),
227
- };
228
- }
229
- else if (activePane === 1) {
230
- const nextIndex = selectedRuleIndex === currentCategoryRules.length - 1 ? 0 : selectedRuleIndex + 1;
231
- state = {
232
- ...state,
233
- selectedRuleIndex: nextIndex,
234
- ruleScroll: updateScroll(nextIndex, state.ruleScroll, viewportHeight),
235
- };
236
- }
237
- render();
238
- return;
239
- }
240
- }
241
- function getRuleStatus(ruleName, category, config) {
242
- if (config.rules) {
243
- let val = config.rules[ruleName];
244
- if (val === undefined) {
245
- const foundKey = Object.keys(config.rules).find((key) => key.endsWith(`/${ruleName}`));
246
- if (foundKey)
247
- val = config.rules[foundKey];
248
- }
249
- if (val !== undefined) {
250
- const status = Array.isArray(val) ? val[0] : val;
251
- return status;
252
- }
253
- }
254
- if (config.categories && config.categories[category]) {
255
- return config.categories[category];
256
- }
257
- return "off";
258
- }
259
- function stripJsonComments(json) {
260
- return json.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? "" : m));
261
- }
262
- function loadRules() {
263
- let rulesData = [];
264
- let config = {
265
- rules: {},
266
- categories: {},
267
- };
268
- let configPath = null;
269
- let descriptions = {};
270
- const descriptionsPath = join(__dirname, "./", "rule-descriptions.json");
271
- try {
272
- if (fs.existsSync(descriptionsPath)) {
273
- descriptions = JSON.parse(fs.readFileSync(descriptionsPath, "utf8"));
274
- }
275
- }
276
- catch {
277
- state.message = "Error: Couldn't find description.";
278
- state.messageType = "error";
279
- }
280
- try {
281
- const raw = execSync(`npx -q --yes oxlint@${OXLINT_VERSION} --rules --format=json`, {
282
- encoding: "utf8",
283
- stdio: ["ignore", "pipe", "ignore"],
284
- });
285
- rulesData = JSON.parse(raw);
286
- }
287
- catch {
288
- state.message = "Error: Couldn't run 'npx oxlint'.";
289
- state.messageType = "error";
290
- exit(1);
291
- }
292
- const userConfigPath = argv[2];
293
- if (userConfigPath && fs.existsSync(userConfigPath)) {
294
- configPath = userConfigPath;
295
- }
296
- else if (fs.existsSync(".oxlintrc.json")) {
297
- configPath = ".oxlintrc.json";
298
- }
299
- if (configPath) {
300
- try {
301
- config = JSON.parse(stripJsonComments(fs.readFileSync(configPath, "utf8")));
302
- }
303
- catch {
304
- state.message = "Error: Couldn't parse config.";
305
- state.messageType = "error";
306
- }
307
- }
308
- const map = {};
309
- rulesData.forEach((rule) => {
310
- const cat = rule.category || "Uncategorized";
311
- if (!map[cat])
312
- map[cat] = [];
313
- const status = getRuleStatus(rule.value, cat, config);
314
- const description = descriptions[rule.scope]?.[rule.value];
315
- map[cat].push({
316
- ...rule,
317
- description,
318
- configStatus: status,
319
- isActive: status === "error" || status === "warn",
320
- });
321
- });
322
- const categories = Object.keys(map).toSorted();
323
- return {
324
- categories,
325
- rulesByCategory: map,
326
- config,
327
- configPath,
328
- };
329
- }
330
- function updateScroll(idx, currentScroll, viewHeight) {
331
- if (idx < currentScroll)
332
- return idx;
333
- if (idx >= currentScroll + viewHeight)
334
- return idx - viewHeight + 1;
335
- return currentScroll;
336
- }
337
- function openUrl(url) {
338
- if (!url)
339
- return;
340
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "explorer" : "xdg-open";
341
- const process = spawn(cmd, [url], {
342
- detached: true,
343
- stdio: "ignore",
344
- });
345
- process.unref();
346
- }
347
- const write = (str) => stdout.write(str);
348
- const enterAltScreen = () => write("\x1b[?1049h\x1b[?25l");
349
- const exitAltScreen = () => write("\x1b[?1049l\x1b[?25h");
350
- readline.emitKeypressEvents(stdin);
351
- if (stdin.isTTY)
352
- stdin.setRawMode(true);
353
- stdin.on("keypress", (_, key) => {
354
- const action = KEY_MAP[key.name] ||
355
- (key.ctrl && key.name === "c" ? { type: "EXIT" } : KEY_MAP[key.sequence] || null);
356
- execute(action);
357
- });
358
- stdout.on("resize", render);
359
- enterAltScreen();
360
- render();
package/dist/rendering.js DELETED
@@ -1,166 +0,0 @@
1
- import { state } from "./index.js";
2
- import { stdout } from "node:process";
3
- export const COLORS = {
4
- reset: "\x1b[0m",
5
- dim: "\x1b[38;5;242m",
6
- highlight: "\x1b[38;5;111m",
7
- selectedBg: "\x1b[48;5;24m\x1b[38;5;255m\x1b[1m",
8
- borderActive: "\x1b[38;5;111m",
9
- borderInactive: "\x1b[38;5;237m",
10
- error: "\x1b[38;5;203m",
11
- warn: "\x1b[38;5;215m",
12
- success: "\x1b[38;5;114m",
13
- info: "\x1b[38;5;75m",
14
- };
15
- function chunkString(str, len) {
16
- if (!str)
17
- return [];
18
- const size = Math.ceil(str.length / len);
19
- const r = Array(size);
20
- for (let i = 0; i < size; i++)
21
- r[i] = str.substring(i * len, (i + 1) * len);
22
- return r;
23
- }
24
- function drawBox(buffer, x, y, width, height, title, items, selectedIndex, scrollOffset, isActive) {
25
- const borderColor = isActive ? COLORS.borderActive : COLORS.borderInactive;
26
- const titleClean = title.length > width - 6 ? title.substring(0, width - 7) + "…" : title;
27
- const topBorder = `${borderColor}┌─ ${titleClean} `.padEnd(width + borderColor.length - 1, "─");
28
- buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
29
- for (let i = 1; i < height - 1; i++) {
30
- buffer.push(`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`);
31
- }
32
- buffer.push(`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`);
33
- const innerHeight = height - 2;
34
- items.slice(scrollOffset, scrollOffset + innerHeight).forEach((item, i) => {
35
- const absoluteIndex = scrollOffset + i;
36
- const isRule = typeof item !== "string";
37
- const rawText = isRule ? item.value : item;
38
- let display = rawText.length > width - 4
39
- ? rawText.substring(0, width - 5) + "…"
40
- : rawText.padEnd(width - 4);
41
- let itemColor = COLORS.dim;
42
- if (isRule) {
43
- const ruleItem = item;
44
- if (ruleItem.configStatus === "error")
45
- itemColor = COLORS.error;
46
- else if (ruleItem.configStatus === "warn")
47
- itemColor = COLORS.warn;
48
- else if (ruleItem.isActive)
49
- itemColor = COLORS.success;
50
- }
51
- buffer.push(`\x1b[${y + 1 + i};${x + 2}H`);
52
- if (absoluteIndex === selectedIndex) {
53
- buffer.push(isActive
54
- ? `${COLORS.selectedBg}${display}${COLORS.reset}`
55
- : `${COLORS.dim}\x1b[7m${display}${COLORS.reset}`);
56
- }
57
- else {
58
- buffer.push(`${itemColor}${display}${COLORS.reset}`);
59
- }
60
- });
61
- }
62
- function drawStats(buffer, x, y, width, height, rules) {
63
- const borderColor = COLORS.borderInactive;
64
- const topBorder = `${borderColor}┌─ STATS `.padEnd(width + borderColor.length - 1, "─");
65
- buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
66
- for (let i = 1; i < height - 1; i++)
67
- buffer.push(`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`);
68
- buffer.push(`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`);
69
- let counts = { error: 0, warn: 0, off: 0 };
70
- rules.forEach((ruleItem) => {
71
- if (ruleItem.configStatus === "error")
72
- counts.error++;
73
- else if (ruleItem.configStatus === "warn")
74
- counts.warn++;
75
- else
76
- counts.off++;
77
- });
78
- const lines = [
79
- { label: "Error", count: counts.error, color: COLORS.error },
80
- { label: "Warn", count: counts.warn, color: COLORS.warn },
81
- { label: "Off", count: counts.off, color: COLORS.dim },
82
- ];
83
- lines.forEach((line, i) => {
84
- if (i < height - 2) {
85
- const numStr = String(line.count).padStart(3);
86
- const labelStr = line.label.padEnd(width - 8);
87
- buffer.push(`\x1b[${y + 1 + i};${x + 2}H${line.color}${labelStr}${numStr}${COLORS.reset}`);
88
- }
89
- });
90
- }
91
- function drawDetails(buffer, x, y, width, height, rule, isActive) {
92
- const borderColor = isActive ? COLORS.borderActive : COLORS.borderInactive;
93
- const topBorder = `${borderColor}┌─ DETAILS `.padEnd(width + borderColor.length - 1, "─");
94
- buffer.push(`\x1b[${y};${x}H${topBorder}┐${COLORS.reset}`);
95
- for (let i = 1; i < height - 1; i++)
96
- buffer.push(`\x1b[${y + i};${x}H${borderColor}│${" ".repeat(width - 2)}│${COLORS.reset}`);
97
- buffer.push(`\x1b[${y + height - 1};${x}H${borderColor}└${"─".repeat(width - 2)}┘${COLORS.reset}`);
98
- if (!rule)
99
- return;
100
- let statusDisplay = rule.configStatus.toUpperCase();
101
- if (rule.configStatus === "error")
102
- statusDisplay = `${COLORS.error}${statusDisplay}${COLORS.reset}`;
103
- else if (rule.configStatus === "warn")
104
- statusDisplay = `${COLORS.warn}${statusDisplay}${COLORS.reset}`;
105
- else
106
- statusDisplay = `${COLORS.dim}${statusDisplay}${COLORS.reset}`;
107
- const metadata = [
108
- ["Name", rule.value],
109
- ["Status", statusDisplay],
110
- ["Category", rule.category],
111
- ["Scope", rule.scope],
112
- ["Fix", rule.fix || "N/A"],
113
- ["Default", rule.default ? "Yes" : "No"],
114
- ["Type-aware", rule.type_aware ? "Yes" : "No"],
115
- ];
116
- let line = 0;
117
- metadata.forEach(([lbl, val]) => {
118
- if (line < height - 2) {
119
- buffer.push(`\x1b[${y + 1 + line};${x + 2}H${COLORS.highlight}${lbl.padEnd(12)} ${COLORS.reset}${val}`);
120
- line++;
121
- }
122
- });
123
- if (line < height - 3)
124
- line++;
125
- if (line < height - 2) {
126
- buffer.push(`\x1b[${y + 1 + line};${x + 2}H${COLORS.reset}Description:${COLORS.reset}`);
127
- line++;
128
- const cleanDesc = (rule.description ?? "N/A").replace(/\s+/g, " ").trim();
129
- const chunks = chunkString(cleanDesc, width - 6);
130
- chunks.forEach((chunk) => {
131
- if (line < height - 2) {
132
- buffer.push(`\x1b[${y + 1 + line};${x + 2}H${COLORS.dim}${chunk}${COLORS.reset}`);
133
- line++;
134
- }
135
- });
136
- }
137
- const footerLine = Math.max(line + 1, height - 3);
138
- if (footerLine < height - 1) {
139
- buffer.push(`\x1b[${y + 1 + footerLine};${x + 2}HHit ${COLORS.highlight}ENTER${COLORS.reset} to open docs`);
140
- }
141
- }
142
- export function render() {
143
- if (!state || !state.categories) {
144
- return;
145
- }
146
- const { columns = 80, rows = 24 } = stdout;
147
- const currentCategory = state.categories[state.selectedCategoryIndex];
148
- const rules = state.rulesByCategory[currentCategory] || [];
149
- const rule = rules[state.selectedRuleIndex];
150
- const boxHeight = rows - 5;
151
- const categoriesColumnWidth = Math.floor(columns * 0.2);
152
- const rulesColumnWidth = Math.floor(columns * 0.3);
153
- const detailsColumnWidth = columns - categoriesColumnWidth - rulesColumnWidth - 2;
154
- const statsHeight = 6;
155
- const categoryListHeight = boxHeight - statsHeight;
156
- const buffer = ["\x1b[H\x1b[J"];
157
- drawBox(buffer, 1, 1, categoriesColumnWidth, categoryListHeight, "CATEGORIES", state.categories, state.selectedCategoryIndex, state.categoryScroll, state.activePane === 0);
158
- drawStats(buffer, 1, 1 + categoryListHeight, categoriesColumnWidth, statsHeight, rules);
159
- drawBox(buffer, categoriesColumnWidth + 1, 1, rulesColumnWidth, boxHeight, `RULES (${rules.length})`, rules, state.selectedRuleIndex, state.ruleScroll, state.activePane === 1);
160
- drawDetails(buffer, categoriesColumnWidth + rulesColumnWidth + 1, 1, detailsColumnWidth, boxHeight, rule, state.activePane === 2);
161
- const msgColor = COLORS[state.messageType] || COLORS.reset;
162
- buffer.push(`\x1b[${rows - 3};2H${msgColor}● ${state.message}${COLORS.reset}`);
163
- const footerConfig = state.configPath ? `Config: ${state.configPath}` : "No config loaded";
164
- buffer.push(`\x1b[${rows - 1};2H${COLORS.dim}Arrows/HJKL: Nav | 1-3: Status | R: Lint | X: Run rule | Enter: Docs | Q: Quit | ${footerConfig}${COLORS.reset}`);
165
- stdout.write(buffer.join(""));
166
- }