From f3b2fc01082e754f7dfe73caa4f0a4f207a2adfb Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Sun, 13 Sep 2026 04:05:38 +0000 Subject: Add headless CLI for Overleaf project management (lattex-cli) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/cli/fileTree.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/cli/fileTree.ts (limited to 'src/cli/fileTree.ts') diff --git a/src/cli/fileTree.ts b/src/cli/fileTree.ts new file mode 100644 index 0000000..736b1e1 --- /dev/null +++ b/src/cli/fileTree.ts @@ -0,0 +1,69 @@ +// Walk Overleaf project root folder into flat maps (same logic as main/index.ts walkRootFolder) + +export interface FileTreeEntry { + name: string + path: string // relative path, forward slashes + isDir: boolean + docId?: string // text docs + fileRefId?: string // binary files +} + +export interface FileTreeResult { + entries: FileTreeEntry[] + docPathMap: Record // docId → relPath + pathDocMap: Record // relPath → docId + fileRefs: Array<{ id: string; path: string }> + folderMap: Record // folderId → relPath ('' for root) + pathFolderMap: Record // relPath → folderId + rootFolderId: string +} + +interface FolderLike { + _id: string + name: string + docs?: Array<{ _id: string; name: string }> + fileRefs?: Array<{ _id: string; name: string }> + folders?: FolderLike[] +} + +export function walkRootFolder(rootFolder: FolderLike[]): FileTreeResult { + const docPathMap: Record = {} + const pathDocMap: Record = {} + const fileRefs: Array<{ id: string; path: string }> = [] + const folderMap: Record = {} + const pathFolderMap: Record = {} + const entries: FileTreeEntry[] = [] + const rootFolderId = rootFolder[0]?._id || '' + + function walk(f: FolderLike, prefix: string): void { + // Register this folder + const folderPath = prefix ? prefix.slice(0, -1) : '' // remove trailing / + folderMap[f._id] = folderPath + pathFolderMap[folderPath] = f._id + + for (const doc of f.docs || []) { + const relPath = prefix + doc.name + docPathMap[doc._id] = relPath + pathDocMap[relPath] = doc._id + entries.push({ name: doc.name, path: relPath, isDir: false, docId: doc._id }) + } + + for (const ref of f.fileRefs || []) { + const relPath = prefix + ref.name + fileRefs.push({ id: ref._id, path: relPath }) + entries.push({ name: ref.name, path: relPath, isDir: false, fileRefId: ref._id }) + } + + for (const sub of f.folders || []) { + const subPrefix = prefix + sub.name + '/' + entries.push({ name: sub.name, path: subPrefix.slice(0, -1), isDir: true }) + walk(sub, subPrefix) + } + } + + if (rootFolder[0]) { + walk(rootFolder[0], '') + } + + return { entries, docPathMap, pathDocMap, fileRefs, folderMap, pathFolderMap, rootFolderId } +} -- cgit v1.2.3