diff options
| author | YurenHao0426 <blackhao0426@gmail.com> | 2026-09-13 04:05:38 +0000 |
|---|---|---|
| committer | YurenHao0426 <blackhao0426@gmail.com> | 2026-09-13 04:05:38 +0000 |
| commit | f3b2fc01082e754f7dfe73caa4f0a4f207a2adfb (patch) | |
| tree | f6083a5abddc3541ad7996f71695e559b4b41ece /src/cli/localState.ts | |
| parent | 34f22876ab9e15e9f14300a8c21cd7109800d1ab (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/localState.ts')
| -rw-r--r-- | src/cli/localState.ts | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/src/cli/localState.ts b/src/cli/localState.ts new file mode 100644 index 0000000..73a1060 --- /dev/null +++ b/src/cli/localState.ts @@ -0,0 +1,76 @@ +// Manages per-clone state stored in <dir>/.lattex-cli.json +// and the global auth credential at ~/.config/lattex/auth.json + +import { join } from 'path' +import { readFile, writeFile, mkdir, chmod } from 'fs/promises' +import { existsSync } from 'fs' +import { homedir } from 'os' + +const CONFIG_DIR = join(homedir(), '.config', 'lattex') +const AUTH_PATH = join(CONFIG_DIR, 'auth.json') +const STATE_FILE = '.lattex-cli.json' + +// ── Global auth ── + +export async function saveCookie(cookie: string): Promise<void> { + await mkdir(CONFIG_DIR, { recursive: true }) + await writeFile(AUTH_PATH, JSON.stringify({ cookie }), { mode: 0o600 }) + // Ensure directory and file are user-only + await chmod(CONFIG_DIR, 0o700).catch(() => {}) + await chmod(AUTH_PATH, 0o600).catch(() => {}) +} + +export async function loadCookie(): Promise<string | null> { + try { + const data = JSON.parse(await readFile(AUTH_PATH, 'utf-8')) + return data.cookie || null + } catch { + return null + } +} + +// ── Per-clone state ── + +export interface CloneState { + projectId: string + projectName: string + /** docId → relPath */ + docPathMap: Record<string, string> + /** relPath → docId */ + pathDocMap: Record<string, string> + /** fileRefId → relPath */ + fileRefPathMap: Record<string, string> + /** relPath → fileRefId */ + pathFileRefMap: Record<string, string> + /** folderId → relDirPath */ + folderMap: Record<string, string> + /** relDirPath → folderId */ + pathFolderMap: Record<string, string> + rootFolderId: string + rootDocId: string + /** ISO timestamp of last pull */ + lastPull: string + /** sha256 hash of each file at last pull (relPath → hash) */ + fileHashes: Record<string, string> +} + +export function statePath(dir: string): string { + return join(dir, STATE_FILE) +} + +export async function loadState(dir: string): Promise<CloneState | null> { + const p = statePath(dir) + try { + return JSON.parse(await readFile(p, 'utf-8')) + } catch { + return null + } +} + +export async function saveState(dir: string, state: CloneState): Promise<void> { + await writeFile(statePath(dir), JSON.stringify(state, null, 2)) +} + +export function hasState(dir: string): boolean { + return existsSync(statePath(dir)) +} |
