thincoder 0.10.0 → 0.11.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/tools/git.mjs CHANGED
@@ -6,101 +6,133 @@ import {
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { join } from "node:path";
8
8
 
9
- export const gitDiffTool = {
10
- name: "git_diff",
11
- description: DESC("git_diff"),
9
+ export const gitTool = {
10
+ name: "git",
11
+ description:
12
+ "Run a git command. Use this to see uncommitted changes, staged changes, diff against a ref, recent commits, or manage checkpoints. Only works inside a git repository.\n" +
13
+ "- action='diff': Show unified diff — what changed since last commit. Set staged=true for staged-only diff, ref=<ref> to compare against a specific commit/branch, path=<dir> to scope to a file or directory.\n" +
14
+ "- action='status': Show working tree state — staged, unstaged, untracked files, and conflicts. Returns categorized lists.\n" +
15
+ "- action='log': Show recent commit history. Set count to limit, oneline=true for compact format, path=<file> to see history of one file.\n" +
16
+ "- action='checkpoint': Manage git-based snapshots. Use checkpointAction to choose: list (overview), create (snapshot now), rewind (restore snapshot by id), cat (read a file from a snapshot).",
12
17
  parameters: {
13
18
  type: "object",
14
19
  properties: {
15
- staged: { type: "boolean", description: "Show staged changes (default false)" },
16
- path: { type: "string", description: "File or directory to diff (default all)" },
17
- ref: { type: "string", description: "Compare against this ref (default HEAD)" },
20
+ action: { type: "string", enum: ["diff", "status", "log", "checkpoint"], description: "diff / status / log / checkpoint" },
21
+ // diff/log params
22
+ staged: { type: "boolean", description: "(diff) Show staged changes instead of working tree" },
23
+ path: { type: "string", description: "(diff/log/checkpoint:cat/checkpoint:rewind) File or directory to scope to" },
24
+ ref: { type: "string", description: "(diff) Compare against this ref (default HEAD)" },
25
+ count: { type: "number", description: "(log) Number of commits (default 10)" },
26
+ oneline: { type: "boolean", description: "(log) One-line-per-commit format" },
27
+ // checkpoint params
28
+ checkpointAction: { type: "string", enum: ["list", "create", "rewind", "cat"], description: "(checkpoint) list snapshots / create one / restore by id / read file from snapshot" },
29
+ checkpointId: { type: "string", description: "(checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)" },
18
30
  },
31
+ required: ["action"],
19
32
  },
20
- readonly: true,
21
- execute(args, ctx) {
22
- const ref = args.ref ?? "HEAD"
23
- // ref supplied by model and placed before "--": validate charset, prevent "--output=..." etc. from being treated as git options
24
- if (!/^[A-Za-z0-9._\/~^][A-Za-z0-9._\/~^-]*$/.test(ref)) throw new Error(`Invalid git ref: ${ref}`)
25
- const flags = args.staged ? ["--staged"] : []
26
- const paths = args.path ? [args.path] : []
27
- const out = runGit(ctx.cwd, ["diff", ...flags, ref, "--", ...paths])
28
- return truncate(out || "(no changes)")
29
- },
30
- }
31
-
32
- // ---------------------------------------------------------------- git_status
33
-
34
- export const gitStatusTool = {
35
- name: "git_status",
36
- description: DESC("git_status"),
37
- parameters: {
38
- type: "object",
39
- properties: {},
40
- },
41
- readonly: true,
42
- execute(_args, ctx) {
43
- const porcelain = runGit(ctx.cwd, ["status", "--porcelain"])
44
- if (!porcelain) return "(clean no changes)"
45
-
46
- const staged = []
47
- const unstaged = []
48
- const untracked = []
49
- const conflicts = []
50
- for (const line of porcelain.split("\n")) {
51
- if (!line) continue
52
- // porcelain: XY path — 2 status chars + space + file path (some environments have only 1 space)
53
- // Strip possible CR (execFileSync on some Windows git leaves \r at end of line but not in newline)
54
- const clean = line.replace(/\r/g, "")
55
- // Try matching "XY path" or "XY path" (variable spacing)
56
- const m = clean.match(/^(..?)\s+(.+)$/)
57
- if (!m) continue
58
- const [, status, rawFile] = m
59
- // Rename entries in porcelain output are "R old -> new", split for clarity instead of treating as a literal filename
60
- const file = status.includes("R") && rawFile.includes(" -> ") ? rawFile.replace(" -> ", " → ") : rawFile
61
- const idx = status[0] ?? " "
62
- const wt = status[1] ?? " "
63
- if (idx === "U" || wt === "U" || (idx === "A" && wt === "A")) {
64
- conflicts.push(file)
65
- } else if (idx === "?" && wt === "?") {
66
- untracked.push(file)
67
- } else {
68
- if (idx !== " " && idx !== "?") staged.push(idx + " " + file)
69
- if (wt !== " " && wt !== "?") unstaged.push(wt + " " + file)
33
+ readonly: false,
34
+ async execute(args, ctx) {
35
+ switch (args.action) {
36
+ case "diff": {
37
+ const ref = args.ref ?? "HEAD"
38
+ if (!/^[A-Za-z0-9._\/~^][A-Za-z0-9._\/~^-]*$/.test(ref)) throw new Error(`Invalid git ref: ${ref}`)
39
+ const flags = args.staged ? ["--staged"] : []
40
+ const paths = args.path ? [args.path] : []
41
+ const out = runGit(ctx.cwd, ["diff", ...flags, ref, "--", ...paths])
42
+ return truncate(out || "(no changes)")
43
+ }
44
+ case "status": {
45
+ const porcelain = runGit(ctx.cwd, ["status", "--porcelain"])
46
+ if (!porcelain) return "(clean — no changes)"
47
+
48
+ const staged = []
49
+ const unstaged = []
50
+ const untracked = []
51
+ const conflicts = []
52
+ for (const line of porcelain.split("\n")) {
53
+ if (!line) continue
54
+ const clean = line.replace(/\r/g, "")
55
+ const m = clean.match(/^(..?)\s+(.+)$/)
56
+ if (!m) continue
57
+ const [, status, rawFile] = m
58
+ const file = status.includes("R") && rawFile.includes(" -> ") ? rawFile.replace(" -> ", " → ") : rawFile
59
+ const idx = status[0] ?? " "
60
+ const wt = status[1] ?? " "
61
+ if (idx === "U" || wt === "U" || (idx === "A" && wt === "A")) {
62
+ conflicts.push(file)
63
+ } else if (idx === "?" && wt === "?") {
64
+ untracked.push(file)
65
+ } else {
66
+ if (idx !== " " && idx !== "?") staged.push(idx + " " + file)
67
+ if (wt !== " " && wt !== "?") unstaged.push(wt + " " + file)
68
+ }
69
+ }
70
+ const parts = []
71
+ if (staged.length) parts.push("Staged (" + staged.length + "):\n" + staged.join("\n"))
72
+ if (unstaged.length) parts.push("Unstaged (" + unstaged.length + "):\n" + unstaged.join("\n"))
73
+ if (untracked.length) parts.push("Untracked (" + untracked.length + "):\n" + untracked.join("\n"))
74
+ if (conflicts.length) parts.push("Conflicts (" + conflicts.length + "):\n" + conflicts.join("\n"))
75
+ return truncate(parts.join("\n\n"))
76
+ }
77
+ case "log": {
78
+ const n = Math.min(Math.max(1, args.count ?? 10), 200)
79
+ const isOneline = args.oneline
80
+ const cmdArgs = isOneline
81
+ ? ["log", "-" + n, "--oneline"]
82
+ : ["log", "-" + n, "--format=%h %ad %an %s", "--date=short"]
83
+ if (args.path) cmdArgs.push("--", args.path)
84
+ const out = runGit(ctx.cwd, cmdArgs)
85
+ return truncate(out || "(no commits)")
70
86
  }
87
+ case "checkpoint": {
88
+ const { createCheckpoint, listCheckpoints, rewind, isGitRepo } = await import("../git/checkpoint.mjs")
89
+ if (!isGitRepo(ctx.cwd)) throw new Error("Not a git repository — checkpoints unavailable")
90
+
91
+ const sub = args.checkpointAction
92
+ if (!sub) return "checkpoint: missing checkpointAction — use: list | create | rewind | cat"
93
+
94
+ if (sub === "create") {
95
+ const cp = await createCheckpoint(ctx.cwd)
96
+ return `Checkpoint ${cp.id} created (${cp.files} file(s): ${cp.tracked.length} tracked, ${cp.untracked.length} untracked)`
97
+ }
98
+ if (sub === "rewind") {
99
+ if (!args.checkpointId) throw new Error("checkpointId is required for rewind — use checkpointAction=list to see snapshot ids")
100
+ const s = await rewind(ctx.cwd, args.checkpointId, { path: args.path })
101
+ if (args.path) {
102
+ return `Restored "${args.path}" (${s.type}) from checkpoint ${args.checkpointId}.\n(The pre-rewind state was snapshotted first — you can rewind again to go back.)`
103
+ }
104
+ return `Rewound to checkpoint ${args.checkpointId}: patch ${s.patchApplied ? "applied" : "(empty)"}, ${s.restored ?? 0} untracked file(s) restored, ${s.deleted ?? 0} file(s) deleted.\n(The pre-rewind state was snapshotted first — you can rewind again to go back.)`
105
+ }
106
+ if (sub === "cat") {
107
+ if (!args.checkpointId) throw new Error("checkpointId is required for cat — use checkpointAction=list to see snapshot ids")
108
+ if (!args.path) throw new Error("path is required for cat — specify which file to read")
109
+ const { catFile } = await import("../git/checkpoint.mjs")
110
+ return await catFile(ctx.cwd, args.checkpointId, args.path)
111
+ }
112
+ if (sub === "list") {
113
+ const cps = await listCheckpoints(ctx.cwd)
114
+ if (cps.length === 0) return "(no checkpoints yet — one is auto-created before each user task)"
115
+
116
+ // Specific id: show the file tree within that snapshot
117
+ if (args.checkpointId) {
118
+ const cp = cps.find((c) => c.id === args.checkpointId)
119
+ if (!cp) throw new Error(`checkpoint ${args.checkpointId} not found`)
120
+ return formatFileTree(cp)
121
+ }
122
+
123
+ // Overview: list of all snapshots
124
+ return cps.map((c) => {
125
+ const parts = [`${c.id} ${new Date(c.time).toISOString()}`]
126
+ if (c.tracked.length) parts.push(`${c.tracked.length} tracked: ${c.tracked.join(", ")}`)
127
+ if (c.untracked.length) parts.push(`${c.untracked.length} untracked: ${c.untracked.join(", ")}`)
128
+ return parts.join(" ")
129
+ }).join("\n")
130
+ }
131
+ throw new Error(`Unknown checkpoint action: ${sub}. Use: list | create | rewind | cat`)
132
+ }
133
+ default:
134
+ return `Unknown action '${args.action}'. Use: diff | status | log | checkpoint`
71
135
  }
72
- const parts = []
73
- if (staged.length) parts.push("Staged (" + staged.length + "):\n" + staged.join("\n"))
74
- if (unstaged.length) parts.push("Unstaged (" + unstaged.length + "):\n" + unstaged.join("\n"))
75
- if (untracked.length) parts.push("Untracked (" + untracked.length + "):\n" + untracked.join("\n"))
76
- if (conflicts.length) parts.push("Conflicts (" + conflicts.length + "):\n" + conflicts.join("\n"))
77
- return truncate(parts.join("\n\n"))
78
- },
79
- }
80
-
81
- // ---------------------------------------------------------------- git_log
82
-
83
- export const gitLogTool = {
84
- name: "git_log",
85
- description: DESC("git_log"),
86
- parameters: {
87
- type: "object",
88
- properties: {
89
- count: { type: "number", description: "Number of commits (default 10)" },
90
- path: { type: "string", description: "File or directory (default all)" },
91
- oneline: { type: "boolean", description: "One-line-per-commit format (default false)" },
92
- },
93
- },
94
- readonly: true,
95
- execute(args, ctx) {
96
- const n = Math.min(Math.max(1, args.count ?? 10), 200)
97
- const isOneline = args.oneline
98
- const cmdArgs = isOneline
99
- ? ["log", "-" + n, "--oneline"]
100
- : ["log", "-" + n, "--format=%h %ad %an %s", "--date=short"]
101
- if (args.path) cmdArgs.push("--", args.path)
102
- const out = runGit(ctx.cwd, cmdArgs)
103
- return truncate(out || "(no commits)")
104
136
  },
105
137
  }
106
138
 
@@ -128,67 +160,6 @@ export const questionTool = {
128
160
  },
129
161
  }
130
162
 
131
- /** Execute a git command; returns empty string when not a git repo / git unavailable */
132
-
133
- // ---------------------------------------------------------------- checkpoint
134
-
135
- export const checkpointTool = {
136
- name: "checkpoint",
137
- description: DESC("checkpoint"),
138
- parameters: {
139
- type: "object",
140
- properties: {
141
- action: { type: "string", enum: ["list", "create", "rewind", "cat"], description: "list snapshots / create one now / restore a snapshot by id / read a file's content from a snapshot" },
142
- id: { type: "string", description: "Snapshot id (required for rewind and cat; optional for list — shows file tree of that snapshot)" },
143
- path: { type: "string", description: "Restore only this single file from the checkpoint (tracked or untracked). Other files are left untouched." },
144
- },
145
- required: ["action"],
146
- },
147
- readonly: false,
148
- async execute(args, ctx) {
149
- const { createCheckpoint, listCheckpoints, rewind, isGitRepo } = await import("../git/checkpoint.mjs")
150
- if (!isGitRepo(ctx.cwd)) throw new Error("Not a git repository — checkpoints unavailable")
151
- if (args.action === "create") {
152
- const cp = await createCheckpoint(ctx.cwd)
153
- return `Checkpoint ${cp.id} created (${cp.files} file(s): ${cp.tracked.length} tracked, ${cp.untracked.length} untracked)`
154
- }
155
- if (args.action === "rewind") {
156
- if (!args.id) throw new Error("id is required for rewind — use action=list to see snapshot ids")
157
- const s = await rewind(ctx.cwd, args.id, { path: args.path })
158
- if (args.path) {
159
- return `Restored "${args.path}" (${s.type}) from checkpoint ${args.id}.\n(The pre-rewind state was snapshotted first — you can rewind again to go back.)`
160
- }
161
- return `Rewound to checkpoint ${args.id}: patch ${s.patchApplied ? "applied" : "(empty)"}, ${s.restored ?? 0} untracked file(s) restored, ${s.deleted ?? 0} file(s) deleted.\n(The pre-rewind state was snapshotted first — you can rewind again to go back.)`
162
- }
163
- if (args.action === "cat") {
164
- if (!args.id) throw new Error("id is required for cat — use action=list to see snapshot ids")
165
- if (!args.path) throw new Error("path is required for cat — specify which file to read")
166
- const { catFile } = await import("../git/checkpoint.mjs")
167
- return await catFile(ctx.cwd, args.id, args.path)
168
- }
169
- if (args.action === "list") {
170
- const cps = await listCheckpoints(ctx.cwd)
171
- if (cps.length === 0) return "(no checkpoints yet — one is auto-created before each user task)"
172
-
173
- // Specific id: show the file tree within that snapshot
174
- if (args.id) {
175
- const cp = cps.find((c) => c.id === args.id)
176
- if (!cp) throw new Error(`checkpoint ${args.id} not found`)
177
- return formatFileTree(cp)
178
- }
179
-
180
- // Overview: list of all snapshots
181
- return cps.map((c) => {
182
- const parts = [`${c.id} ${new Date(c.time).toISOString()}`]
183
- if (c.tracked.length) parts.push(`${c.tracked.length} tracked: ${c.tracked.join(", ")}`)
184
- if (c.untracked.length) parts.push(`${c.untracked.length} untracked: ${c.untracked.join(", ")}`)
185
- return parts.join(" ")
186
- }).join("\n")
187
- }
188
- throw new Error(`Unknown action: ${args.action}`)
189
- },
190
- }
191
-
192
163
  /** Format a checkpoint's file list as a directory tree (directories first, indented display) */
193
164
  function formatFileTree(cp) {
194
165
  const all = [
@@ -197,10 +168,8 @@ function formatFileTree(cp) {
197
168
  ]
198
169
  if (all.length === 0) return "(empty checkpoint)"
199
170
 
200
- // Sort by path (directories group naturally)
201
171
  all.sort((a, b) => a.path.localeCompare(b.path))
202
172
 
203
- // Build directory → file list map
204
173
  const tree = new Map()
205
174
  for (const { path, type } of all) {
206
175
  const dir = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "."
@@ -208,12 +177,10 @@ function formatFileTree(cp) {
208
177
  tree.get(dir).push({ name: path.slice(dir === "." ? 0 : dir.length + 1), type })
209
178
  }
210
179
 
211
- // Output sorted by directory
212
180
  const lines = []
213
181
  const dirs = [...tree.keys()].sort()
214
182
  for (const dir of dirs) {
215
183
  if (dir !== "." && !lines.includes(dir + "/")) {
216
- // Parent directory before child directory
217
184
  const parts = dir.split("/")
218
185
  for (let i = 1; i <= parts.length; i++) {
219
186
  const prefix = parts.slice(0, i).join("/") + "/"
@@ -221,7 +188,6 @@ function formatFileTree(cp) {
221
188
  }
222
189
  }
223
190
  }
224
- // Ensure directories precede files
225
191
  for (const dir of dirs) {
226
192
  if (dir !== ".") {
227
193
  for (const { name, type } of tree.get(dir)) {
@@ -2,27 +2,27 @@
2
2
  export { toOpenAISchema } from "./shared.mjs";
3
3
 
4
4
  import { readTool, writeTool, editTool, insertAfterTool, readImageTool, hashlineEditTool } from "./file.mjs";
5
- import { applyPatchTool, syntaxCheckTool, deleteTool } from "./patch.mjs";
5
+ import { applyPatchTool, deleteTool } from "./patch.mjs";
6
6
  import { bashTool, globTool, grepTool, lsTool } from "./system.mjs";
7
7
  import { websearchTool, fetchTool } from "./web.mjs";
8
- import { gitDiffTool, gitStatusTool, gitLogTool, questionTool, checkpointTool } from "./git.mjs";
8
+ import { gitTool, questionTool } from "./git.mjs";
9
9
  import { checklistTool } from "./checklist.mjs";
10
- import { linterTool } from "./linter.mjs";
10
+ import { lintTool } from "./linter.mjs";
11
11
  import { lspTool } from "./lsp.mjs";
12
12
  import { codeModeTool } from "./codemode.mjs";
13
13
 
14
14
  export const builtinTools = [
15
15
  readTool, writeTool, editTool, insertAfterTool, hashlineEditTool, applyPatchTool,
16
- syntaxCheckTool, readImageTool, bashTool, globTool, grepTool,
16
+ readImageTool, bashTool, globTool, grepTool,
17
17
  websearchTool, lsTool, fetchTool, deleteTool,
18
- gitDiffTool, gitStatusTool, gitLogTool, questionTool, checkpointTool,
19
- checklistTool, linterTool, lspTool, codeModeTool,
18
+ gitTool, questionTool,
19
+ checklistTool, lintTool, lspTool, codeModeTool,
20
20
  ];
21
21
 
22
22
  export {
23
23
  readTool, writeTool, editTool, insertAfterTool, hashlineEditTool, applyPatchTool,
24
- syntaxCheckTool, readImageTool, bashTool, globTool, grepTool,
24
+ readImageTool, bashTool, globTool, grepTool,
25
25
  websearchTool, lsTool, fetchTool, deleteTool,
26
- gitDiffTool, gitStatusTool, gitLogTool, questionTool, checkpointTool,
27
- checklistTool, linterTool, lspTool, codeModeTool,
26
+ gitTool, questionTool,
27
+ checklistTool, lintTool, lspTool, codeModeTool,
28
28
  };
@@ -1,38 +1,66 @@
1
1
  import { DESC, resolveInCwd } from "./shared.mjs"
2
+ import { execFileSync } from "node:child_process"
3
+ import { existsSync } from "node:fs"
4
+ import { join, relative } from "node:path"
2
5
 
3
- export const linterTool = {
4
- name: "linter",
5
- description: DESC("linter"),
6
+ export const lintTool = {
7
+ name: "lint",
8
+ description:
9
+ "Run the appropriate linter/checker for a file. Auto-detects based on file extension and project config.\n" +
10
+ "Without 'full', runs a fast node --check (JS/TS syntax only, catches parse errors in milliseconds).\n" +
11
+ "With 'full', runs the language-aware cascade: eslint → tsc –noEmit → node --check (JS/TS/TSX); ruff (Python); cargo check (Rust); go vet (Go).\n" +
12
+ "Use the fast default after every write/edit; use 'full' before declaring a task complete.",
6
13
  parameters: {
7
14
  type: "object",
8
15
  properties: {
9
- path: { type: "string", description: "File path (default: most recently modified file)" },
16
+ path: { type: "string", description: "File to check (default: most recently modified file)" },
17
+ full: { type: "boolean", description: "Run the full language-aware cascade instead of just node --check (default false)" },
10
18
  },
19
+ required: [],
11
20
  },
12
21
  readonly: true,
13
22
  async execute(args, ctx) {
14
- const { execFileSync } = await import("node:child_process")
15
- const { existsSync } = await import("node:fs")
16
- const { join, relative } = await import("node:path")
17
- const abs = args.path ? resolveInCwd(ctx, args.path) : (ctx.agent?._touchedFiles?.at(-1) || null)
18
- if (!abs) return "linter: no file specified and no recently modified file to check"
23
+ const abs = args.path
24
+ ? resolveInCwd(ctx, args.path)
25
+ : (ctx.agent?._touchedFiles?.at(-1) || null)
26
+ if (!abs) return "lint: no file specified and no recently modified file to check"
19
27
 
28
+ if (!args.full) {
29
+ // Fast path: node --check only
30
+ return nodeCheckResult(abs)
31
+ }
32
+
33
+ // Full cascade: language-aware (eslint → tsc → node --check, etc.)
20
34
  const ext = abs.split(".").pop()?.toLowerCase()
21
35
  const checkers = LANG_CHECKERS[ext]
22
- if (!checkers) return `linter: no linter configured for .${ext} files. Supported: ${Object.keys(LANG_CHECKERS).map(e => `.${e}`).join(", ")}`
36
+ if (!checkers) return nodeCheckResult(abs) // fall back to node --check
23
37
 
24
38
  for (const checker of checkers) {
25
39
  const result = await checker(abs, { cwd: ctx.cwd, existsSync, execFileSync, join, relative })
26
40
  if (result !== null) return result
27
41
  }
28
- return `linter: no linter available for ${args.path || abs}. Install one?`
42
+ return `lint: no linter available for ${abs}. Install one?`
29
43
  },
30
44
  }
31
45
 
32
- // ─── Checker definitions ──────────────────────
46
+ function nodeCheckResult(abs) {
47
+ if (!/\.(?:m?js|cjs|m?ts|cts|jsx|tsx)$/.test(abs)) {
48
+ return `lint (check): only JS/TS-family files supported for fast syntax check; use full=true for other languages. Path: ${abs}`
49
+ }
50
+ try {
51
+ execFileSync(process.execPath, ["--check", abs], {
52
+ encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"],
53
+ })
54
+ return `Syntax OK: ${abs}`
55
+ } catch (e) {
56
+ const msg = (e.stderr || e.stdout || e.message || "").trim()
57
+ return `Syntax error in ${abs}:\n${msg || "(unknown)"}`
58
+ }
59
+ }
60
+
61
+ // ─── Full-check cascade checkers ──────────────────────
33
62
 
34
63
  async function eslintCheck(file, { cwd, existsSync, execFileSync, join, relative }) {
35
- // Walk up to find eslint config
36
64
  let dir = file.split(/[\\/]/).slice(0, -1).join("/") || "."
37
65
  while (true) {
38
66
  for (const cfg of [".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yaml", ".eslintrc.yml", "eslint.config.js", "eslint.config.mjs"]) {
@@ -73,18 +101,6 @@ async function tscCheck(file, { cwd, existsSync, execFileSync, join }) {
73
101
  }
74
102
  }
75
103
 
76
- async function nodeCheck(file, { execFileSync, cwd }) {
77
- if (!/\.(m?js|cjs)$/.test(file)) return null
78
- try {
79
- execFileSync(process.execPath, ["--check", file], {
80
- cwd, encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"],
81
- })
82
- return "✓ node --check: Syntax OK"
83
- } catch (e) {
84
- return `✗ node --check: ${(e.stderr || e.message).slice(0, 500)}`
85
- }
86
- }
87
-
88
104
  async function ruffCheck(file, { cwd, execFileSync }) {
89
105
  if (!/\.py$/.test(file)) return null
90
106
  try {
@@ -93,7 +109,7 @@ async function ruffCheck(file, { cwd, execFileSync }) {
93
109
  })
94
110
  return "✓ ruff: no issues"
95
111
  } catch (e) {
96
- if (e.code === "ENOENT") return "linter: ruff not installed. Run: pip install ruff"
112
+ if (e.code === "ENOENT") return "lint: ruff not installed. Run: pip install ruff"
97
113
  const stdout = (e.stdout || "").trim()
98
114
  if (stdout) return stdout
99
115
  return `✗ ruff: ${(e.stderr || e.message).slice(0, 500)}`
@@ -129,13 +145,11 @@ async function goVet(file, { cwd, execFileSync }) {
129
145
  }
130
146
  }
131
147
 
132
- // ─── Language → checkers (first available wins) ──
133
-
134
148
  const LANG_CHECKERS = {
135
- js: [eslintCheck, nodeCheck],
136
- mjs: [eslintCheck, nodeCheck],
137
- cjs: [eslintCheck, nodeCheck],
138
- jsx: [eslintCheck, nodeCheck],
149
+ js: [eslintCheck],
150
+ mjs: [eslintCheck],
151
+ cjs: [eslintCheck],
152
+ jsx: [eslintCheck],
139
153
  ts: [eslintCheck, tscCheck],
140
154
  tsx: [eslintCheck, tscCheck],
141
155
  mts: [eslintCheck, tscCheck],