summaryrefslogtreecommitdiff
path: root/src/cli/logParser.ts
diff options
context:
space:
mode:
authorYurenHao0426 <blackhao0426@gmail.com>2026-09-13 04:05:38 +0000
committerYurenHao0426 <blackhao0426@gmail.com>2026-09-13 04:05:38 +0000
commitf3b2fc01082e754f7dfe73caa4f0a4f207a2adfb (patch)
treef6083a5abddc3541ad7996f71695e559b4b41ece /src/cli/logParser.ts
parent34f22876ab9e15e9f14300a8c21cd7109800d1ab (diff)
Add headless CLI for Overleaf project management (lattex-cli)
New src/cli/ module providing a headless CLI for AI agents to work with Overleaf projects from a server with no display. Commands: auth, projects, clone, pull, status, push, compile. Key design decisions: - Uses Node.js https module (no Electron dependency), same approach as the existing MCP server in src/mcp/lattex.mjs - Reuses overleafProtocol.ts for Socket.IO v0.9 parsing (pure, no Electron) - Separate esbuild bundle (out/cli/lattex-cli.mjs) — does not touch the Electron app build - 64 tests for arg parsing, file tree walking, diff logic, and log parsing Files: - src/cli/main.ts — CLI entry point with all commands - src/cli/args.ts — argument parser - src/cli/overleafApi.ts — Overleaf API client using https + ws - src/cli/fileTree.ts — project root folder walker - src/cli/localState.ts — per-clone state (.lattex-cli.json) and auth storage - src/cli/diff.ts — local vs remote diff logic - src/cli/logParser.ts — LaTeX compile log parser - src/cli/test.ts — unit tests - tsconfig.cli.json — TypeScript config for CLI Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/cli/logParser.ts')
-rw-r--r--src/cli/logParser.ts99
1 files changed, 99 insertions, 0 deletions
diff --git a/src/cli/logParser.ts b/src/cli/logParser.ts
new file mode 100644
index 0000000..d59d7df
--- /dev/null
+++ b/src/cli/logParser.ts
@@ -0,0 +1,99 @@
+// LaTeX compile log parser (extracted from src/mcp/lattex.mjs)
+
+export interface LogEntry {
+ level: 'error' | 'warning'
+ message: string
+ file?: string
+ line?: number
+}
+
+export function parseCompileLog(raw: string): LogEntry[] {
+ const entries: LogEntry[] = []
+ const lines = raw.split('\n')
+
+ for (let i = 0; i < lines.length; i++) {
+ const ln = lines[i]
+
+ if (/^!/.test(ln) || /LaTeX Error:/.test(ln)) {
+ let msg = ln.replace(/^!\s*/, '')
+ while (i + 1 < lines.length && lines[i + 1] && !lines[i + 1].startsWith('l.') && !lines[i + 1].startsWith('!')) {
+ i++
+ if (lines[i].trim()) msg += ' ' + lines[i].trim()
+ }
+ let lineNum: number | undefined
+ if (i + 1 < lines.length && /^l\.(\d+)/.test(lines[i + 1])) {
+ i++
+ lineNum = parseInt(lines[i].match(/^l\.(\d+)/)![1])
+ }
+ entries.push({ level: 'error', message: msg.trim(), line: lineNum })
+ continue
+ }
+
+ const fileLineErr = ln.match(/^\.\/(.+?):(\d+):\s*(.+)/)
+ if (fileLineErr) {
+ const msg = fileLineErr[3]
+ const isWarning = /warning/i.test(msg)
+ entries.push({
+ level: isWarning ? 'warning' : 'error',
+ message: msg,
+ file: fileLineErr[1],
+ line: parseInt(fileLineErr[2])
+ })
+ continue
+ }
+
+ const pkgWarn = ln.match(/Package (\S+) Warning:\s*(.*)/)
+ if (pkgWarn) {
+ let msg = `[${pkgWarn[1]}] ${pkgWarn[2]}`
+ let warnLine: number | undefined
+ while (i + 1 < lines.length && /^\(/.test(lines[i + 1])) {
+ i++
+ msg += ' ' + lines[i].replace(/^\([^)]*\)\s*/, '').trim()
+ const lineMatch = lines[i].match(/on input line (\d+)/)
+ if (lineMatch) warnLine = parseInt(lineMatch[1])
+ }
+ if (!warnLine) {
+ const lineMatch = msg.match(/on input line (\d+)/)
+ if (lineMatch) warnLine = parseInt(lineMatch[1])
+ }
+ entries.push({ level: 'warning', message: msg.trim(), line: warnLine })
+ continue
+ }
+
+ const latexWarn = ln.match(/LaTeX Warning:\s*(.*)/)
+ if (latexWarn) {
+ let msg = latexWarn[1]
+ while (i + 1 < lines.length && lines[i + 1] && !lines[i + 1].match(/^[(!.]/) && lines[i + 1].startsWith(' ')) {
+ i++
+ msg += ' ' + lines[i].trim()
+ }
+ const lineMatch = msg.match(/on input line (\d+)/)
+ entries.push({ level: 'warning', message: msg.trim(), line: lineMatch ? parseInt(lineMatch[1]) : undefined })
+ continue
+ }
+
+ if (/^(Overfull|Underfull)/.test(ln)) {
+ const paraMatch = ln.match(/at lines (\d+)--(\d+)/) || ln.match(/in paragraph at lines (\d+)--(\d+)/)
+ entries.push({ level: 'warning', message: ln.trim(), line: paraMatch ? parseInt(paraMatch[1]) : undefined })
+ continue
+ }
+
+ if (/File .* not found/.test(ln)) {
+ entries.push({ level: 'error', message: ln.trim() })
+ }
+ }
+
+ // Deduplicate
+ const seen = new Set<string>()
+ return entries.filter((e) => {
+ const key = `${e.level}:${e.message}`
+ if (seen.has(key)) return false
+ seen.add(key)
+ return true
+ })
+}
+
+export function formatEntry(e: LogEntry): string {
+ const loc = [e.file, e.line].filter(Boolean).join(':')
+ return `[${e.level.toUpperCase()}]${loc ? ` ${loc}:` : ''} ${e.message}`
+}