แก้ปัญหาที่ Dev เจอทุกวัน: พอร์ตชน, .env ไม่ครบ, console.log ค้าง, TODO, ไฟล์ใหญ่ใน git, branch รก, cache บวม

แก้ปัญหาที่ Dev เจอทุกวันด้วยคำสั่งเดียว: พอร์ตชน ("Port 3000 is already in use"), .env หายหรือไม่ครบ, console.log ที่ลืมลบ, TODO ที่ค้าง, ไฟล์ใหญ่ที่หลุดเข้า git, branch ที่ merge แล้วแต่ยังไม่ลบ, merge conflict ค้าง และ cache ของ build ที่บวม ทุกคำสั่งที่ลบหรือปิดอะไรจะแสดงให้ดูก่อน แล้วทำจริงเมื่อพิมพ์ confirm ต่อท้ายเท่านั้น
| คำสั่ง | ทำอะไร |
|---|---|
/pain scan | สแกนทุกอย่างในครั้งเดียว |
/pain ports | พอร์ต dev ที่ถูกใช้ และโปรแกรมที่ใช้อยู่ |
/pain kill 3000 | ดูว่าโปรแกรมไหนใช้พอร์ต 3000 |
/pain kill 3000 confirm | ปิดโปรแกรมนั้นจริง |
/pain env / env fix | ตรวจ .env / สร้าง .env จาก .env.example และเพิ่ม .env ใน .gitignore |
/pain logs / logs fix | หา console.log และ debugger / ให้ Claude ช่วยลบ (ขออนุญาตก่อนแก้ทุกไฟล์) |
/pain todos | หา TODO FIXME HACK XXX |
/pain todos rank | ให้ Claude จัดลำดับความสำคัญของ TODO (สูง/กลาง/ต่ำ) |
/pain deps | แพ็กเกจ npm ที่ตกรุ่น แยกตัวที่ข้ามเวอร์ชันหลัก (เช็คเองสัปดาห์ละครั้งตอนเปิด Claude Code) |
/pain git | ไฟล์ใหญ่กว่า 5 MB ใน git, branch ที่ merge แล้ว, conflict ค้าง |
/pain clean / clean confirm | ดูรายการ cache ที่ลบได้ / ลบจริง |
/pain report | ผลสแกนล่าสุด |
/plugin install dev-painkiller-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
netstat บน Windows, lsof หรือ ss บน macOS/Linux และหาชื่อโปรแกรมจาก PID.env กับ .env.example โดยไม่แสดงค่า$.process.run(['git', ...])confirm และส่งงานแก้ไฟล์ให้ Claude ด้วย $.prompt.submitkill ให้ดูชื่อโปรแกรมให้ดี พอร์ต 5432 / 6379 มักเป็นฐานข้อมูล (PostgreSQL / Redis) ที่ไม่ควรปิดenv fix ไม่เขียนทับ .env ที่มีอยู่แล้วhooks/register.tsx 424 lines1/**
2 * ============================================================================
3 * MOD 18: Dev Painkiller TH - แก้ปัญหาที่ Dev เจอทุกวัน (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: ปัญหาจริงที่เจอทุกวัน "Port 3000 is already in use", ลืมลบ console.log, .env ไม่ครบ
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. /pain scan สแกนทีเดียวครบ: พอร์ตที่ถูกใช้, .env, console.log, TODO, ไฟล์ใหญ่ใน git, git, cache
9 * 2. /pain kill <port> ปิดโปรแกรมที่ยึดพอร์ต (แสดงชื่อโปรแกรมก่อน ต้องยืนยันด้วย confirm)
10 * 3. /pain env fix สร้าง .env จาก .env.example และเพิ่ม .env ลง .gitignore
11 * 4. /pain clean ลบ cache ของ build (แสดงรายการก่อน ต้องยืนยันด้วย confirm)
12 * 5. /pain logs fix ให้ Claude ช่วยลบ console.log (ผ่านการอนุญาตแก้ไฟล์ตามปกติ)
13 * 6. /pain deps แพ็กเกจตกรุ่น แยกตัวที่ข้ามเวอร์ชันหลัก (เช็คเองสัปดาห์ละครั้ง)
14 * 7. /pain todos rank ให้ Claude จัดลำดับความสำคัญของ TODO
15 *
16 * หลักความปลอดภัย: คำสั่งที่ลบหรือปิดอะไร แสดงก่อนเสมอ และทำจริงเมื่อพิมพ์ confirm ต่อท้าย
17 */
18
19import type { EngineInterface, FsEntry, Register } from 'claude-code'
20
21type Level = 'high' | 'medium' | 'info'
22interface Issue {
23 area: string
24 level: Level
25 title: string
26 fix?: string
27}
28
29const ICON: Record<Level, string> = { high: '🔴', medium: '🟡', info: '🔵' }
30const DEV_PORTS = [3000, 3001, 4200, 5000, 5173, 5432, 6379, 8000, 8080, 8888, 9000]
31const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', 'vendor', '.venv', 'venv', '__pycache__', '.turbo'])
32const CODE_FILE = /\.(js|jsx|ts|tsx|mjs|cjs|vue|svelte|py)$/
33const CACHE_DIRS = ['node_modules/.cache', '.next/cache', '.turbo', '.parcel-cache', '.eslintcache', '.vite', '__pycache__']
34
35let projectRoot = ''
36let isWindows = false
37let issues: Issue[] = []
38
39const sep = () => (isWindows ? '\\' : '/')
40const join = (...parts: string[]) => parts.join(sep()).replace(/[\\/]+/g, sep())
41
42// ==========================================================================
43// บทที่ 1: หาพอร์ตที่ถูกใช้ - Windows ใช้ netstat, macOS/Linux ใช้ lsof หรือ ss
44// ==========================================================================
45interface PortUse {
46 port: number
47 pid: number
48 name: string
49}
50
51async function listeningPorts($: EngineInterface): Promise<PortUse[]> {
52 const found = new Map<number, number>() // port -> pid
53 if (isWindows) {
54 const run = await $.process.run(['netstat', '-ano', '-p', 'TCP'])
55 for (const line of run.stdout.split('\n')) {
56 // ฟังอยู่ = foreign address เป็น 0.0.0.0:0 หรือ [::]:0 (ไม่พึ่งคำว่า LISTENING เพราะ Windows ภาษาอื่นแปลคำนี้)
57 const m = /TCP\s+\S+:(\d+)\s+(0\.0\.0\.0:0|\[::\]:0)\s+\S+\s+(\d+)/.exec(line)
58 if (m) found.set(Number(m[1]), Number(m[3]))
59 }
60 } else {
61 const run = await $.process.run(['lsof', '-nP', '-iTCP', '-sTCP:LISTEN']).catch(() => undefined)
62 if (run && run.exitCode === 0) {
63 for (const line of run.stdout.split('\n').slice(1)) {
64 const m = /^\S+\s+(\d+)\s.*:(\d+)\s+\(LISTEN\)/.exec(line)
65 if (m) found.set(Number(m[2]), Number(m[1]))
66 }
67 } else {
68 const ss = await $.process.run(['ss', '-ltnp']).catch(() => undefined)
69 for (const line of ss?.stdout.split('\n') ?? []) {
70 const m = /:(\d+)\s+\S+.*pid=(\d+)/.exec(line)
71 if (m) found.set(Number(m[1]), Number(m[2]))
72 }
73 }
74 }
75
76 const result: PortUse[] = []
77 for (const port of DEV_PORTS) {
78 const pid = found.get(port)
79 if (pid !== undefined) result.push({ port, pid, name: await processName($, pid) })
80 }
81 return result
82}
83
84async function processName($: EngineInterface, pid: number): Promise<string> {
85 const run = isWindows
86 ? await $.process.run(['tasklist', '/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH']).catch(() => undefined)
87 : await $.process.run(['ps', '-p', String(pid), '-o', 'comm=']).catch(() => undefined)
88 const out = run?.stdout.trim() ?? ''
89 return (isWindows ? /^"([^"]+)"/.exec(out)?.[1] : out.split('/').pop()) || 'ไม่ทราบชื่อ'
90}
91
92// ==========================================================================
93// บทที่ 2: เดินอ่านไฟล์โค้ด หา console.log / debugger / TODO
94// ==========================================================================
95async function walkCode($: EngineInterface, visit: (file: string, text: string) => void, limit = 1500): Promise<void> {
96 const queue = [projectRoot]
97 let count = 0
98 while (queue.length && count < limit) {
99 const dir = queue.shift() ?? projectRoot
100 let entries: FsEntry[] = []
101 try {
102 entries = await $.fs.list(dir)
103 } catch {
104 continue
105 }
106 for (const e of entries) {
107 const path = join(dir, e.name)
108 if (e.kind === 'dir') {
109 if (!SKIP_DIRS.has(e.name)) queue.push(path)
110 } else if (CODE_FILE.test(e.name) && e.size < 512 * 1024) {
111 count++
112 visit(path.slice(projectRoot.length + 1), await $.fs.read(path).catch(() => ''))
113 }
114 }
115 }
116}
117
118type Hit = { file: string; line: number; text: string }
119
120async function findPatterns($: EngineInterface, regex: RegExp): Promise<Hit[]> {
121 const hits: Hit[] = []
122 await walkCode($, (file, text) => {
123 // ข้ามไฟล์ test และ script ที่ตั้งใจพิมพ์ออกจอ
124 if (/(^|[\\/])(test|tests|__tests__|scripts?)[\\/]|\.(test|spec)\./.test(file)) return
125 text.split('\n').forEach((line, i) => {
126 if (regex.test(line)) hits.push({ file, line: i + 1, text: line.trim().slice(0, 80) })
127 })
128 })
129 return hits
130}
131
132// ==========================================================================
133// บทที่ 3: ตรวจ .env - เทียบชื่อตัวแปรกับ .env.example (ไม่แสดงค่า)
134// ==========================================================================
135function envKeys(text: string): string[] {
136 return text
137 .split('\n')
138 .map(l => /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(l)?.[1])
139 .filter((k): k is string => Boolean(k))
140}
141
142async function checkEnv($: EngineInterface): Promise<{ text: string; issues: Issue[] }> {
143 const env = await $.fs.read(join(projectRoot, '.env')).catch(() => undefined)
144 const example = (await $.fs.read(join(projectRoot, '.env.example')).catch(() => undefined)) ?? (await $.fs.read(join(projectRoot, '.env.sample')).catch(() => undefined))
145 const ignore = (await $.fs.read(join(projectRoot, '.gitignore')).catch(() => '')) ?? ''
146 const found: Issue[] = []
147 const lines = ['🔑 .env']
148
149 if (env === undefined) {
150 lines.push(example ? ' ❌ ไม่มี .env แต่มี .env.example' : ' ⚪ ไม่มี .env และ .env.example')
151 if (example) found.push({ area: 'env', level: 'high', title: 'ไม่มี .env (มี .env.example)', fix: '/pain env fix' })
152 } else {
153 lines.push(` ✅ มี .env (${envKeys(env).length} ตัวแปร)`)
154 }
155 if (env !== undefined && example) {
156 const missing = envKeys(example).filter(k => !envKeys(env).includes(k))
157 lines.push(missing.length ? ` ⚠️ ขาดตัวแปรที่ .env.example มี: ${missing.join(', ')}` : ' ✅ ตัวแปรครบตาม .env.example')
158 if (missing.length) found.push({ area: 'env', level: 'medium', title: `.env ขาด ${missing.join(', ')}`, fix: 'เพิ่มค่าเองใน .env' })
159 }
160 const ignored = /^\s*\/?\.env(\s|$|\*)/m.test(ignore)
161 lines.push(ignored ? ' ✅ .gitignore มี .env' : ' ⚠️ .gitignore ไม่มี .env (เสี่ยง commit ความลับ)')
162 if (!ignored) found.push({ area: 'env', level: 'high', title: '.gitignore ไม่มี .env', fix: '/pain env fix' })
163 return { text: lines.join('\n'), issues: found }
164}
165
166// ==========================================================================
167// บทที่ 4: ตรวจ git - ไฟล์ใหญ่, branch ที่ merge แล้ว, conflict ค้าง
168// ==========================================================================
169async function git($: EngineInterface, args: string[]): Promise<string | undefined> {
170 const run = await $.process.run(['git', ...args], { timeoutMs: 20_000 }).catch(() => undefined)
171 return run && run.exitCode === 0 ? run.stdout : undefined
172}
173
174async function checkGit($: EngineInterface): Promise<{ text: string; issues: Issue[] }> {
175 const files = await git($, ['ls-files'])
176 if (files === undefined) return { text: '🌿 Git: ไม่ใช่ git repo', issues: [] }
177 const found: Issue[] = []
178 const lines = ['🌿 Git']
179
180 const big: string[] = []
181 for (const f of files.split('\n').filter(Boolean)) {
182 const st = await $.fs.stat(join(projectRoot, f)).catch(() => undefined)
183 if (st && st.size > 5 * 1024 * 1024) big.push(`${f} (${(st.size / 1024 / 1024).toFixed(1)} MB)`)
184 }
185 lines.push(big.length ? ` 🐘 ไฟล์ใหญ่กว่า 5 MB ใน git: ${big.slice(0, 5).join(', ')}` : ' ✅ ไม่มีไฟล์ใหญ่กว่า 5 MB ใน git')
186 if (big.length) found.push({ area: 'git', level: 'medium', title: `ไฟล์ใหญ่ใน git ${big.length} ไฟล์`, fix: 'ใช้ Git LFS หรือย้ายออกแล้วเพิ่มใน .gitignore' })
187
188 const current = (await git($, ['branch', '--show-current']))?.trim() ?? ''
189 const merged = (await git($, ['branch', '--merged']))?.split('\n').map(b => b.replace('*', '').trim()).filter(b => b && b !== current && !['main', 'master', 'develop'].includes(b)) ?? []
190 const all = (await git($, ['branch']))?.split('\n').filter(Boolean).length ?? 0
191 lines.push(` 🌿 branch ทั้งหมด ${all} อัน, merge แล้วลบได้ ${merged.length} อัน${merged.length ? `: ${merged.slice(0, 5).join(', ')}` : ''}`)
192 if (merged.length >= 5) found.push({ area: 'git', level: 'info', title: `branch ที่ merge แล้ว ${merged.length} อัน`, fix: 'git branch -d <ชื่อ>' })
193
194 const changes = (await git($, ['status', '--porcelain']))?.split('\n').filter(Boolean) ?? []
195 const conflicts = changes.filter(l => /^(UU|AA|DD|AU|UA|DU|UD) /.test(l))
196 lines.push(` 📝 ไฟล์ที่ยังไม่ commit ${changes.length} ไฟล์${conflicts.length ? `, 💥 conflict ค้าง ${conflicts.length} ไฟล์` : ''}`)
197 if (conflicts.length) found.push({ area: 'git', level: 'high', title: `merge conflict ค้าง ${conflicts.length} ไฟล์`, fix: 'แก้ conflict แล้ว git add' })
198 return { text: lines.join('\n'), issues: found }
199}
200
201// ==========================================================================
202// บทที่ 5: cache ที่ลบได้อย่างปลอดภัย (build ใหม่ก็สร้างกลับมาเอง)
203// ==========================================================================
204async function existingCaches($: EngineInterface): Promise<string[]> {
205 const result: string[] = []
206 for (const c of CACHE_DIRS) if (await $.fs.exists(join(projectRoot, c))) result.push(c)
207 return result
208}
209
210async function removePath($: EngineInterface, path: string): Promise<void> {
211 const argv = isWindows
212 ? ['powershell', '-NoProfile', '-NonInteractive', '-Command', "$ErrorActionPreference='Stop'; Remove-Item -LiteralPath $env:PK_TARGET -Recurse -Force"]
213 : ['sh', '-c', 'rm -rf -- "$PK_TARGET"']
214 const run = await $.process.run(argv, { env: { PK_TARGET: path }, timeoutMs: 120_000 })
215 if (run.exitCode !== 0) throw new Error(run.stderr.trim() || `exit ${run.exitCode}`)
216}
217
218function report(): string {
219 if (issues.length === 0) return '🩹 ไม่พบปัญหา 🎉 (หรือยังไม่ได้ /pain scan)'
220 const lines = [`🩹 Dev Painkiller - พบ ${issues.length} ปัญหา`, '----------------------------------------']
221 for (const level of ['high', 'medium', 'info'] as const) {
222 for (const i of issues.filter(x => x.level === level)) lines.push(`${ICON[level]} [${i.area}] ${i.title}${i.fix ? `\n 💡 ${i.fix}` : ''}`)
223 }
224 return lines.join('\n')
225}
226
227// ==========================================================================
228// บทที่ 5.5: แพ็กเกจที่ตกรุ่น - npm outdated (exit code 1 เมื่อมีของตกรุ่น ไม่ใช่ error)
229// ==========================================================================
230interface Outdated {
231 name: string
232 current: string
233 latest: string
234 major: boolean
235}
236
237async function outdated($: EngineInterface): Promise<Outdated[] | string> {
238 if (!(await $.fs.exists(`${projectRoot}${sep()}package.json`))) return 'ไม่พบ package.json (ตอนนี้รองรับโปรเจกต์ Node)'
239 // บน Windows npm เป็นไฟล์ .cmd ต้องเรียกผ่าน cmd /c
240 const argv = isWindows ? ['cmd', '/c', 'npm', 'outdated', '--json'] : ['npm', 'outdated', '--json']
241 const r = await $.process.run(argv, { timeoutMs: 120_000 }).catch(() => undefined)
242 if (!r) return 'ไม่พบคำสั่ง npm'
243 let data: Record<string, { current?: string; latest?: string }> = {}
244 try {
245 data = JSON.parse(r.stdout || '{}') as typeof data
246 } catch {
247 return `อ่านผล npm outdated ไม่ได้: ${r.stderr.trim().split('\n')[0] ?? ''}`
248 }
249 const major = (v?: string) => Number(/\d+/.exec(v ?? '')?.[0] ?? NaN)
250 return Object.entries(data)
251 .filter(([, v]) => v.latest && v.current !== v.latest)
252 .map(([name, v]) => ({ name, current: v.current ?? '(ยังไม่ติดตั้ง)', latest: v.latest ?? '', major: major(v.latest) > major(v.current) || !v.current }))
253 .sort((a, b) => Number(b.major) - Number(a.major) || a.name.localeCompare(b.name))
254}
255
256function outdatedText(list: Outdated[]): string {
257 if (list.length === 0) return '✅ แพ็กเกจเป็นเวอร์ชันล่าสุดหมดแล้ว'
258 const majors = list.filter(p => p.major)
259 const minors = list.filter(p => !p.major)
260 return [
261 `📦 แพ็กเกจตกรุ่น ${list.length} ตัว`,
262 ...(majors.length ? ['', `🔴 ข้ามเวอร์ชันหลัก ${majors.length} ตัว (อาจมี breaking change อ่าน changelog ก่อน)`, ...majors.map(p => ` ${p.name.padEnd(28)} ${p.current} → ${p.latest}`)] : []),
263 ...(minors.length ? ['', `🟡 อัปเดตย่อย ${minors.length} ตัว (ปกติปลอดภัย: npm update)`, ...minors.slice(0, 30).map(p => ` ${p.name.padEnd(28)} ${p.current} → ${p.latest}`)] : []),
264 ].join('\n')
265}
266
267const HELP = `🩹 Dev Painkiller TH (Mod 18)
268========================================
269/pain scan สแกนทุกอย่าง
270/pain ports พอร์ต dev ที่ถูกใช้อยู่ และโปรแกรมที่ใช้
271/pain kill <port> ดูว่าจะปิดโปรแกรมไหน -> /pain kill <port> confirm ปิดจริง
272/pain env ตรวจ .env | /pain env fix สร้าง .env + เพิ่มใน .gitignore
273/pain logs หา console.log / debugger | /pain logs fix ให้ Claude ช่วยลบ
274/pain todos หา TODO FIXME HACK XXX | /pain todos rank ให้ Claude จัดลำดับความสำคัญ
275/pain deps แพ็กเกจตกรุ่น (เช็คเองสัปดาห์ละครั้งตอนเปิด Claude Code)
276/pain git ไฟล์ใหญ่ใน git, branch ที่ merge แล้ว, conflict
277/pain clean ดู cache ที่ลบได้ -> /pain clean confirm ลบจริง
278/pain report ผลสแกนล่าสุด`
279
280export const register: Register = on => {
281 on('session.start', async ($, e, next) => {
282 projectRoot = e.cwd.replace(/[\\/]$/, '')
283 isWindows = /^[A-Za-z]:/.test(projectRoot)
284 issues = []
285 await $.command.register({ name: 'pain', description: 'แก้ปัญหา Dev ทุกวัน: พอร์ตชน, .env, console.log, TODO, แพ็กเกจตกรุ่น (/pain help)', argumentHint: 'scan|ports|kill|env|logs|todos|deps|git|clean' })
286 // เช็คแพ็กเกจตกรุ่นสัปดาห์ละครั้งต่อโปรเจกต์ (ไม่รอ ไม่ขวางการเริ่ม session)
287 const key = `deps:${projectRoot.toLowerCase()}`
288 const last = ((await $.store.get(key)) as number | undefined) ?? 0
289 const now = await $.clock.now()
290 if (now - last > 7 * 24 * 3600_000) {
291 void outdated($).then(async list => {
292 if (typeof list === 'string') return
293 await $.store.set(key, now)
294 const majors = list.filter(p => p.major).length
295 if (list.length) $.ui.toast(`📦 มีแพ็กเกจตกรุ่น ${list.length} ตัว${majors ? ` (ข้ามเวอร์ชันหลัก ${majors})` : ''} ดู: /pain deps`, { timeoutMs: 12_000 })
296 })
297 }
298 return next(e)
299 })
300
301 // ==========================================================================
302 // บทที่ 6: คำสั่ง /pain
303 // ==========================================================================
304 on('command.run', { command: 'pain' }, async ($, e) => {
305 const [sub = 'help', arg, confirm] = e.args.trim().split(/\s+/).filter(Boolean)
306
307 switch (sub) {
308 case 'scan': {
309 issues = []
310 const ports = await listeningPorts($).catch(() => [])
311 for (const p of ports) issues.push({ area: 'port', level: 'info', title: `พอร์ต ${p.port} ถูกใช้โดย ${p.name} (PID ${p.pid})`, fix: `/pain kill ${p.port} confirm` })
312 const env = await checkEnv($)
313 issues.push(...env.issues)
314 const logs = await findPatterns($, /\bconsole\.log\s*\(|^\s*debugger\s*;?\s*$/)
315 if (logs.length) issues.push({ area: 'code', level: 'medium', title: `console.log / debugger ${logs.length} จุด`, fix: '/pain logs แล้ว /pain logs fix' })
316 const todos = await findPatterns($, /\b(TODO|FIXME|HACK|XXX)\b/)
317 if (todos.length) issues.push({ area: 'code', level: 'info', title: `TODO/FIXME ${todos.length} จุด`, fix: '/pain todos' })
318 const g = await checkGit($)
319 issues.push(...g.issues)
320 const caches = await existingCaches($)
321 if (caches.length) issues.push({ area: 'cache', level: 'info', title: `มี cache ที่ลบได้: ${caches.join(', ')}`, fix: '/pain clean confirm' })
322 return { text: report() }
323 }
324 case 'ports': {
325 const ports = await listeningPorts($)
326 if (ports.length === 0) return { text: `🔌 ไม่มีโปรแกรมใช้พอร์ต dev ยอดนิยม (${DEV_PORTS.join(', ')})` }
327 return { text: ['🔌 พอร์ตที่ถูกใช้', ...ports.map(p => ` :${p.port} ${p.name} (PID ${p.pid})`), '', 'ปิดด้วย /pain kill <port> confirm'].join('\n') }
328 }
329 case 'kill': {
330 const port = Number(arg)
331 if (!port) return { text: 'วิธีใช้: /pain kill 3000 (ดูก่อน) แล้ว /pain kill 3000 confirm (ปิดจริง)' }
332 const use = (await listeningPorts($)).find(p => p.port === port)
333 if (!use) {
334 if (!DEV_PORTS.includes(port)) return { text: `ℹ️ ตรวจได้เฉพาะพอร์ต ${DEV_PORTS.join(', ')}` }
335 return { text: `✅ พอร์ต ${port} ว่างอยู่แล้ว` }
336 }
337 if (confirm !== 'confirm') return { text: `⚠️ พอร์ต ${port} ถูกใช้โดย ${use.name} (PID ${use.pid})\nถ้าแน่ใจว่าปิดได้ พิมพ์: /pain kill ${port} confirm` }
338 const run = isWindows ? await $.process.run(['taskkill', '/PID', String(use.pid), '/F']) : await $.process.run(['kill', String(use.pid)])
339 return { text: run.exitCode === 0 ? `✅ ปิด ${use.name} (PID ${use.pid}) แล้ว พอร์ต ${port} ว่างแล้ว` : `❌ ปิดไม่ได้: ${run.stderr.trim() || run.stdout.trim()}` }
340 }
341 case 'env': {
342 if (arg !== 'fix') return { text: (await checkEnv($)).text }
343 const done: string[] = []
344 const envPath = join(projectRoot, '.env')
345 if (!(await $.fs.exists(envPath))) {
346 const example = (await $.fs.read(join(projectRoot, '.env.example')).catch(() => undefined)) ?? (await $.fs.read(join(projectRoot, '.env.sample')).catch(() => undefined))
347 if (example !== undefined) {
348 await $.fs.write(envPath, example)
349 done.push('✅ สร้าง .env จาก .env.example แล้ว (อย่าลืมใส่ค่าจริง)')
350 }
351 }
352 const ignorePath = join(projectRoot, '.gitignore')
353 const ignore = (await $.fs.read(ignorePath).catch(() => '')) ?? ''
354 if (!/^\s*\/?\.env(\s|$|\*)/m.test(ignore)) {
355 await $.fs.write(ignorePath, `${ignore}${ignore && !ignore.endsWith('\n') ? '\n' : ''}.env\n`)
356 done.push('✅ เพิ่ม .env ใน .gitignore แล้ว')
357 }
358 return { text: done.length ? done.join('\n') : '✅ ไม่มีอะไรต้องแก้' }
359 }
360 case 'logs': {
361 const logs = await findPatterns($, /\bconsole\.log\s*\(|^\s*debugger\s*;?\s*$/)
362 if (logs.length === 0) return { text: '✅ ไม่พบ console.log / debugger' }
363 if (arg === 'fix') {
364 const files = [...new Set(logs.map(l => l.file))].slice(0, 20)
365 $.clock.after(50, () => void $.prompt.submit({ text: `ช่วยลบ console.log และ debugger ที่ใช้ debug ออกจากไฟล์เหล่านี้ (เก็บ log ที่ตั้งใจให้ผู้ใช้เห็นไว้):\n${files.map(f => `- ${f}`).join('\n')}` }).catch(() => undefined)) // ส่งหลังคำสั่งตอบกลับแล้ว (ส่งระหว่าง command.run ไม่ได้)
366 return { text: `🧹 ส่งงานให้ Claude ลบ console.log ใน ${files.length} ไฟล์แล้ว (Claude จะขออนุญาตก่อนแก้ทุกไฟล์)` }
367 }
368 return { text: [`🐛 console.log / debugger ${logs.length} จุด`, ...logs.slice(0, 30).map(l => ` ${l.file}:${l.line} ${l.text}`), logs.length > 30 ? ` ... และอีก ${logs.length - 30} จุด` : '', '/pain logs fix = ให้ Claude ช่วยลบ'].filter(Boolean).join('\n') }
369 }
370 case 'todos': {
371 const todos = await findPatterns($, /\b(TODO|FIXME|HACK|XXX)\b/)
372 if (todos.length === 0) return { text: '✅ ไม่มี TODO ค้าง' }
373 const lines = todos.slice(0, 80).map(t => `${t.file}:${t.line} ${t.text}`)
374 if (arg === 'rank') {
375 // ให้ Haiku จัดกลุ่มตามความสำคัญ (อ่านเฉพาะบรรทัด TODO ไม่ส่งทั้งไฟล์)
376 const system = 'จัดลำดับความสำคัญของ TODO/FIXME ในโค้ด ตอบเป็นภาษาไทย 3 หัวข้อ: 🔴 สูง (บั๊ก ความปลอดภัย ข้อมูลเสียหาย), 🟡 กลาง (ฟีเจอร์ค้าง ประสิทธิภาพ), 🟢 ต่ำ (ความสวยงาม refactor) แต่ละข้อคงรูป "ไฟล์:บรรทัด" ไว้ ตามด้วยเหตุผลสั้นๆ ไม่เกิน 10 คำ'
377 for (const model of ['haiku', await $.session.model()]) {
378 const r = await $.model.complete({ model, system, prompt: lines.join('\n'), maxTokens: 1500 }).catch(() => undefined)
379 if (r?.isAnswered) return { text: `📝 TODO ${todos.length} จุด จัดลำดับโดย Claude\n\n${r.text.trim()}` }
380 }
381 return { text: '❌ ถาม Claude ไม่สำเร็จ' }
382 }
383 return { text: [`📝 TODO/FIXME ${todos.length} จุด`, ...lines.slice(0, 40).map(l => ` ${l}`), '', 'ให้ Claude จัดลำดับ: /pain todos rank'].join('\n') }
384 }
385 case 'deps': {
386 const list = await outdated($)
387 if (typeof list === 'string') return { text: `❌ ${list}` }
388 await $.store.set(`deps:${projectRoot.toLowerCase()}`, await $.clock.now())
389 return { text: outdatedText(list) }
390 }
391 case 'git':
392 return { text: (await checkGit($)).text }
393 case 'clean': {
394 const caches = await existingCaches($)
395 if (caches.length === 0) return { text: '✅ ไม่มี cache ให้ลบ' }
396 if (arg !== 'confirm') return { text: `🧹 cache ที่ลบได้ (build ใหม่จะสร้างกลับมาเอง):\n${caches.map(c => ` ${c}`).join('\n')}\nลบจริงด้วย /pain clean confirm` }
397 const results: string[] = []
398 for (const c of caches) {
399 try {
400 await removePath($, join(projectRoot, c))
401 results.push(` ✅ ${c}`)
402 } catch (err) {
403 results.push(` ❌ ${c}: ${err instanceof Error ? err.message : String(err)}`)
404 }
405 }
406 return { text: ['🧹 ลบ cache', ...results].join('\n') }
407 }
408 case 'report':
409 return { text: report() }
410 default:
411 return { text: HELP }
412 }
413 })
414}
415
416/**
417 * ========================================================================
418 * การบ้าน / ต่อยอด
419 * ========================================================================
420 * 1. ดัก tool.call ของ Bash: ถ้า npm run dev ล้มเหลวด้วย EADDRINUSE ให้ toast แนะนำ /pain kill
421 * 2. หา dependency ที่ไม่ได้ใช้ด้วย $.process.run(['npx', 'depcheck', '--json'])
422 * 3. ลบ branch ที่ merge แล้วทั้งหมด (แสดงรายการ + confirm แบบเดียวกับ /pain clean)
423 */
424