summaryrefslogtreecommitdiff
path: root/src/cli/localState.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/localState.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/localState.ts')
-rw-r--r--src/cli/localState.ts76
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))
+}