dsh-git-ui 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -20
- package/README.zh.md +28 -21
- package/cordis.patch.yml +1 -1
- package/lib/client.js +36 -35
- package/lib/client.js.map +4 -4
- package/lib/host/actions.d.ts +22 -2
- package/lib/host/core.d.ts +7 -1
- package/lib/host/index.d.ts +12 -10
- package/lib/host/index.js +366 -21
- package/lib/host/index.js.map +4 -4
- package/lib/host/parser.d.ts +37 -1
- package/lib/host/queries.d.ts +17 -0
- package/lib/host/types.d.ts +131 -1
- package/package.json +1 -1
- package/src/client/GitCenter.tsx +1411 -148
- package/src/client/GitPill.tsx +280 -55
- package/src/client/changes-diff.ts +63 -0
- package/src/client/controller.ts +36 -1
- package/src/client/error-text.ts +21 -0
- package/src/client/file-tree.ts +101 -0
- package/src/client/git-graph.ts +188 -0
- package/src/client/icons.tsx +292 -0
- package/src/client/index.ts +2 -1
- package/src/client/locales.ts +124 -0
- package/src/client/popup-close.ts +19 -0
- package/src/client/remote.ts +83 -1
- package/src/client/select-menu.tsx +113 -0
- package/src/client/side-by-side.ts +150 -0
- package/src/client/styles.ts +1375 -86
- package/src/client/time-format.ts +32 -0
- package/src/host/actions.ts +66 -15
- package/src/host/core.ts +9 -2
- package/src/host/index.ts +21 -10
- package/src/host/parser.ts +155 -12
- package/src/host/queries.ts +289 -0
- package/src/host/types.ts +104 -0
package/lib/host/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/host/index.ts", "../../src/host/git.ts", "../../src/host/parser.ts", "../../src/host/core.ts", "../../src/host/actions.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * dsh-git-ui host half: the `gitInfo` Remote service.\n *\n * Cordis shell only \u2014 every behavior lives in `core.ts`/`actions.ts` behind\n * injected structural faces, so tests never need a cordis runtime. The class\n * is a plugin in its own right (class form), mounted by the bundle patch row\n * with the package name; the gateway exposes `gitInfo/snapshot` and\n * `gitInfo/run` through SRC discovery (`typertRemote` binding + `@Remote`\n * marker).\n */\nimport { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { realpath, stat } from 'node:fs/promises'\nimport { createGitRunner, type SubprocessLike } from './git.ts'\nimport { normalizeConfig, snapshotForSession, type GitStatusConfig, type SnapshotDeps } from './core.ts'\nimport { runAction } from './actions.ts'\nimport type { GitActionResult, GitActionRequest, GitSnapshotRequest, GitSnapshotResult } from './types.ts'\n\nexport type { GitSnapshot, GitSnapshotResult, GitSnapshotFailure, GitSnapshotRequest, GitCommit, GitChange, GitAction, GitActionResult, GitActionRequest } from './types.ts'\nexport { normalizeConfig, DEFAULT_CONFIG } from './core.ts'\nexport { parseStatusOutput, parseLogOutput, parseBranchOutput } from './parser.ts'\nexport { isSafePath, runAction } from './actions.ts'\n\n/** Structural face of a live session header. */\ninterface SessionLike {\n readonly header?: { readonly cwd?: string }\n}\n\n/** Structural face of the sessions service. */\ninterface SessionsLike {\n get(id: string): SessionLike | undefined\n}\n\n/** Structural face of the session-persistence service. */\ninterface SessionPersistenceLike {\n inspect(id: string): Promise<{ readonly meta: { readonly cwd?: string } }>\n}\n\n/** The `gitInfo` service: `snapshot` (read) and `run` (management) endpoints. */\nexport class GitStatusService extends TypertRemoteService {\n static inject = ['subprocess', 'sessions', 'sessionPersistence']\n\n private readonly config: GitStatusConfig\n\n constructor(ctx: Context, config: unknown) {\n super(ctx, 'gitInfo')\n this.config = normalizeConfig(config)\n }\n\n /** Adapter face shared by both endpoints (injected services + runner). */\n private deps(signal?: AbortSignal): { readonly deps: SnapshotDeps } | { readonly failure: { readonly code: 'git-unavailable'; readonly detail: string } } {\n const subprocess = this.ctx.get('subprocess') as SubprocessLike | undefined\n if (subprocess === undefined) {\n return { failure: { code: 'git-unavailable', detail: 'subprocess service unavailable' } }\n }\n const sessions = this.ctx.get('sessions') as SessionsLike | undefined\n const persistence = this.ctx.get('sessionPersistence') as SessionPersistenceLike | undefined\n const runner = createGitRunner(subprocess, this.config.timeoutMs, this.config.maxStatusBytes)\n return {\n deps: {\n run: runner,\n fs: { realpath, stat },\n sessions: {\n liveCwd: (id) => sessions?.get(id)?.header?.cwd,\n persistedMeta: async (id) => {\n if (persistence === undefined) return undefined\n try {\n const inspection = await persistence.inspect(id)\n return { cwd: inspection.meta.cwd }\n } catch {\n return undefined\n }\n },\n },\n signal,\n },\n }\n }\n\n @Remote('snapshot')\n async snapshot(request: GitSnapshotRequest, signal?: AbortSignal): Promise<GitSnapshotResult> {\n const adapted = this.deps(signal)\n if ('failure' in adapted) return { ok: false, error: adapted.failure }\n return snapshotForSession(adapted.deps, this.config, request.sessionId)\n }\n\n @Remote('run')\n async run(request: GitActionRequest, signal?: AbortSignal): Promise<GitActionResult> {\n const adapted = this.deps(signal)\n if ('failure' in adapted) {\n return { ok: false, error: { code: 'git-error', message: adapted.failure.detail } }\n }\n return runAction(adapted.deps, this.config, request)\n }\n}\n\nexport default GitStatusService\n", "/**\n * Git command execution adapter over the host subprocess service.\n *\n * The widget only needs a tiny slice of the subprocess contract; declaring it\n * structurally here (instead of depending on the npm package, whose registry\n * chain is incomplete) keeps the plugin buildable standalone while remaining\n * wire-compatible with the host's `subprocess` service.\n */\nimport { readFile } from 'node:fs/promises'\n\n/** One collected stream disposition (matches the host SubprocessCollect). */\ninterface CollectDisposition {\n readonly collect: {\n readonly maxBytes: number\n /**\n * Spill disposition: when the stream overflows the in-memory tail, the\n * host appends the COMPLETE stream to a private spill file (up to this\n * cap) and `readFrom` reports its path. Without it, only the tail is\n * ever retained and the head (and its change counts) is lost.\n */\n readonly spill?: { readonly maxBytes: number }\n }\n}\n\n/** Structural slice of the host subprocess spawn spec. */\ninterface SpawnSpec {\n readonly argv: readonly string[]\n readonly cwd: string\n readonly stdio: {\n readonly stdout: CollectDisposition\n readonly stderr: CollectDisposition\n }\n readonly graceMs: number\n readonly signal?: AbortSignal\n}\n\n/** Structural slice of the host subprocess handle (collect-mode output). */\ninterface SpawnHandle {\n readonly done: Promise<{ readonly exitCode: number | null; readonly signal: NodeJS.Signals | null }>\n readonly collected: {\n readonly stdout?: {\n readFrom(fromByte: number): { readonly text: string; readonly lossy: boolean; readonly spillPath?: string }\n }\n readonly stderr?: {\n readFrom(fromByte: number): { readonly text: string; readonly lossy: boolean; readonly spillPath?: string }\n }\n }\n}\n\n/** Minimal subprocess-service face the adapter consumes. */\nexport interface SubprocessLike {\n spawn(spec: SpawnSpec): SpawnHandle\n}\n\n/** One git command outcome. */\nexport interface GitRunResult {\n /** Process exit code; null when terminated by a signal. */\n readonly exitCode: number | null\n readonly stdout: string\n readonly stderr: string\n /** True when the run was killed by our timeout (or the caller's signal). */\n readonly timedOut: boolean\n /**\n * True when the final stdout text is still incomplete: the collected\n * output overflowed its byte cap AND the spill file was unavailable (no\n * spill configured on the host, or the spill cap also overflowed).\n */\n readonly stdoutLossy: boolean\n}\n\n/** The run primitive the snapshot orchestration uses. */\nexport interface GitRunner {\n run(argv: readonly string[], opts: { readonly cwd: string; readonly signal?: AbortSignal }): Promise<GitRunResult>\n}\n\n/**\n * Adapt the host subprocess service into a `GitRunner` with a per-command\n * timeout. A timed-out run resolves (never rejects) with `timedOut: true`;\n * only spawn-level failures (e.g. git not installed) reject.\n *\n * Overflow handling: stdout/stderr collect with a spill cap of\n * `maxBytes * 16` (default 4 MiB memory tail \u2192 64 MiB spill file). When the\n * tail overflowed but the spill file holds the complete stream, the runner\n * reads the file and reports `stdoutLossy: false` \u2014 the change COUNTS stay\n * exact. `stdoutLossy: true` is reserved for the doubly-overflowed case\n * (spill also exceeded), where the head is genuinely lost.\n */\nexport function createGitRunner(subprocess: SubprocessLike, timeoutMs: number, maxBytes: number): GitRunner {\n const spillMaxBytes = maxBytes * 16\n return {\n async run(argv, opts) {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const signal = opts.signal === undefined\n ? controller.signal\n : AbortSignal.any([controller.signal, opts.signal])\n const handle = subprocess.spawn({\n argv,\n cwd: opts.cwd,\n stdio: {\n stdout: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },\n stderr: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },\n },\n graceMs: 200,\n signal,\n })\n let outcome: Awaited<SpawnHandle['done']>\n try {\n // `done` rejects for spawn-level failures; an abort-triggered\n // rejection is the timeout path and resolves as timedOut.\n outcome = await handle.done\n } catch (error) {\n if (controller.signal.aborted || opts.signal?.aborted === true) {\n return { exitCode: null, stdout: '', stderr: '', timedOut: true, stdoutLossy: false }\n }\n throw error\n }\n const stdout = handle.collected.stdout?.readFrom(0)\n const stderr = handle.collected.stderr?.readFrom(0)\n const stdoutResolved = await resolveStdout(stdout)\n return {\n exitCode: outcome.exitCode,\n stdout: stdoutResolved.text,\n stderr: stderr?.text ?? '',\n timedOut: controller.signal.aborted || opts.signal?.aborted === true,\n stdoutLossy: stdoutResolved.lossy,\n }\n } finally {\n clearTimeout(timer)\n }\n },\n }\n}\n\n/**\n * Resolve the stdout text from a collect read: the in-memory tail, or \u2014 when\n * the read is lossy and the host spilled the complete stream to a file \u2014 the\n * spill file contents (so change COUNTS stay exact). A failed spill read\n * falls back to the tail and keeps `lossy: true` (head genuinely lost).\n */\nasync function resolveStdout(\n read: { readonly text: string; readonly lossy: boolean; readonly spillPath?: string } | undefined,\n): Promise<{ readonly text: string; readonly lossy: boolean }> {\n if (read === undefined) return { text: '', lossy: false }\n if (!read.lossy || read.spillPath === undefined) return { text: read.text, lossy: read.lossy }\n try {\n return { text: await readFile(read.spillPath, 'utf8'), lossy: false }\n } catch {\n return { text: read.text, lossy: true }\n }\n}\n", "/**\n * Pure parsers for the git porcelain/log output shapes used by the widget.\n * No side effects and no I/O \u2014 fully unit-testable against literal fixtures\n * (verified against real `git status --porcelain=v1 -z --branch` output).\n */\nimport type { GitChange, GitChangeStatus, GitCommit } from './types.ts'\n\n/** Parsed status counts plus the (possibly capped) change list. */\nexport interface ParsedStatus {\n readonly branch: string | null\n readonly unborn: boolean\n readonly staged: number\n readonly modified: number\n readonly untracked: number\n readonly ahead: number\n readonly behind: number\n readonly changes: readonly GitChange[]\n readonly truncated: boolean\n}\n\n/** The NUL byte separating porcelain v1 -z entries. */\nconst NUL = '\\u0000'\n/** The unit separator used by the log --format payload. */\nconst LOG_SEP = '\\u001f'\n\ninterface StatusHeader {\n readonly branch: string | null\n readonly unborn: boolean\n readonly ahead: number\n readonly behind: number\n}\n\n/**\n * Parse the `## ` header line of `git status --porcelain=v1 -z --branch`.\n * Recognized shapes (verified against git 2.x):\n * `## main`\n * `## main...origin/main`\n * `## main...origin/main [ahead 1]`\n * `## main...origin/main [behind 2]`\n * `## main...origin/main [ahead 1, behind 2]`\n * `## HEAD (no branch)` (detached)\n * `## HEAD (detached at <hash>)` (detached, older git)\n * `## No commits yet on main` (unborn)\n * `## Initial commit on main` (unborn, older git)\n */\nexport function parseStatusHeader(line: string): StatusHeader {\n const body = line.startsWith('## ') ? line.slice(3) : line\n if (body === '') return { branch: null, unborn: false, ahead: 0, behind: 0 }\n\n const unbornMatch = /^(?:No commits yet on|Initial commit on)\\s+(.+)$/.exec(body)\n if (unbornMatch !== null) {\n return { branch: unbornMatch[1] ?? null, unborn: true, ahead: 0, behind: 0 }\n }\n\n const detached = /^HEAD(?:\\s+\\([^)]*\\))?$/.exec(body)\n if (detached !== null) {\n return { branch: null, unborn: false, ahead: 0, behind: 0 }\n }\n\n const bracketMatch = /^(.*?)\\s*\\[([^\\]]+)\\]$/.exec(body)\n const core = bracketMatch?.[1] ?? body\n let ahead = 0\n let behind = 0\n if (bracketMatch?.[2] !== undefined) {\n for (const part of bracketMatch[2].split(',')) {\n const trimmed = part.trim()\n const aheadMatch = /^ahead (\\d+)$/.exec(trimmed)\n const behindMatch = /^behind (\\d+)$/.exec(trimmed)\n if (aheadMatch !== null) ahead = Number(aheadMatch[1])\n if (behindMatch !== null) behind = Number(behindMatch[1])\n }\n }\n // The core is `<branch>...<upstream>` \u2014 the branch never contains `...`.\n const branch = core.split('...', 1)[0] ?? core\n return { branch: branch === '' ? null : branch, unborn: false, ahead, behind }\n}\n\n/** Map one porcelain XY pair to a change status. */\nfunction changeStatus(x: string, y: string): GitChangeStatus {\n if (x === '?' && y === '?') return 'untracked'\n if (x === 'U' || y === 'U' || (x !== ' ' && y !== ' ')) return 'conflicted'\n switch (x) {\n case 'A': return 'added'\n case 'M': return 'modified'\n case 'D': return 'deleted'\n case 'R': return 'renamed'\n case 'T': return 'typechange'\n case 'C': return 'added'\n default: return 'modified'\n }\n}\n\n/**\n * Parse the full `git status --porcelain=v1 -z --branch` output.\n * -z format: every entry (header and each `XY path`) is NUL-terminated; a\n * rename/copy entry emits `R <new>\\0<old>\\0` so the following item is the\n * source path and must be consumed without becoming a change itself.\n */\nexport function parseStatusOutput(output: string, maxChanges: number): ParsedStatus {\n const raw = output.split(NUL)\n // Trailing NUL produces a final empty segment; drop it.\n const segments = raw[raw.length - 1] === '' ? raw.slice(0, -1) : raw\n const header = parseStatusHeader(segments[0] ?? '')\n\n let staged = 0\n let modified = 0\n let untracked = 0\n const changes: GitChange[] = []\n let truncated = false\n\n for (let index = 1; index < segments.length; index += 1) {\n const entry = segments[index] ?? ''\n const x = entry[0] ?? ' '\n const y = entry[1] ?? ' '\n const path = entry.slice(3)\n if (x === ' ' && y === ' ') continue\n if (x === 'R' || x === 'C') {\n // -z: the source path is the next segment \u2014 consume it.\n index += 1\n }\n if (x === '?' && y === '?') {\n untracked += 1\n } else {\n if (x !== ' ' && x !== '?') staged += 1\n if (y !== ' ' && y !== '?') modified += 1\n }\n if (changes.length < maxChanges) {\n changes.push({ path, status: changeStatus(x, y), staged: x !== ' ' && x !== '?' })\n } else {\n truncated = true\n }\n }\n\n return {\n branch: header.branch,\n unborn: header.unborn,\n staged,\n modified,\n untracked,\n ahead: header.ahead,\n behind: header.behind,\n changes,\n truncated,\n }\n}\n\n/**\n * Parse `git log -n 5 --format=%H%x1f%h%x1f%s%x1f%an%x1f%aI` output.\n * One commit per line, fields separated by the unit separator; empty output\n * (unborn repository) yields `[]`.\n */\nexport function parseLogOutput(output: string): readonly GitCommit[] {\n const commits: GitCommit[] = []\n for (const line of output.split('\\n')) {\n if (line === '') continue\n const [hash, shortHash, subject, author, dateIso] = line.split(LOG_SEP)\n if (hash === undefined || hash === '') continue\n commits.push({\n hash,\n shortHash: shortHash ?? '',\n subject: subject ?? '',\n author: author ?? '',\n dateIso: dateIso ?? '',\n })\n }\n return commits\n}\n\n/**\n * Parse `git branch --show-current` output: the branch name, or null when\n * empty (detached HEAD).\n */\nexport function parseBranchOutput(output: string): string | null {\n const trimmed = output.trim()\n return trimmed === '' ? null : trimmed\n}\n", "/**\n * Framework-free snapshot orchestration: session cwd resolution + git command\n * sequence + frozen GitSnapshot assembly. Every dependency is injected\n * structurally, so the whole flow is testable without a cordis runtime; the\n * cordis shell (GitStatusService) only adapts host services into these faces.\n */\nimport { parseBranchOutput, parseLogOutput, parseStatusOutput } from './parser.ts'\nimport type { GitRunner } from './git.ts'\nimport type { GitSnapshot, GitSnapshotResult } from './types.ts'\n\n/** Resolved plugin config (already normalized; see normalizeConfig). */\nexport interface GitStatusConfig {\n readonly timeoutMs: number\n readonly maxStatusBytes: number\n readonly maxChanges: number\n readonly defaultRefreshIntervalMs: number\n}\n\n/** Session identity lookup: live first, persisted fallback. */\nexport interface SessionLookup {\n /** Live session cwd; undefined when the session is cold or absent in memory. */\n liveCwd(sessionId: string): string | undefined\n /**\n * Persisted session metadata; resolves to undefined when no persisted\n * session exists, and to `{ cwd }` (cwd possibly undefined) otherwise.\n */\n persistedMeta(sessionId: string): Promise<{ readonly cwd?: string } | undefined>\n}\n\n/** Filesystem primitives (node:fs/promises slices). */\nexport interface FsLike {\n realpath(path: string): Promise<string>\n stat(path: string): Promise<{ isDirectory(): boolean }>\n}\n\n/** Everything the snapshot flow needs beyond the session lookup. */\nexport interface SnapshotDeps {\n readonly run: GitRunner\n readonly fs: FsLike\n readonly sessions: SessionLookup\n /** Injectable clock for deterministic tests. */\n readonly now?: () => number\n /** Caller-side cancellation (Remote `signal` slot): aborts in-flight git runs. */\n readonly signal?: AbortSignal\n}\n\n/** Defaults applied by normalizeConfig when a value is absent or invalid. */\nexport const DEFAULT_CONFIG: GitStatusConfig = {\n timeoutMs: 5000,\n maxStatusBytes: 4 * 1024 * 1024,\n maxChanges: 100,\n defaultRefreshIntervalMs: 30_000,\n}\n\n/** Coerce a raw patch config value into a validated GitStatusConfig. */\nexport function normalizeConfig(raw: unknown): GitStatusConfig {\n const value = (raw ?? {}) as Record<string, unknown>\n const numberOr = (key: string, fallback: number): number => {\n const candidate = value[key]\n return typeof candidate === 'number' && Number.isFinite(candidate) && candidate >= 0\n ? candidate\n : fallback\n }\n return {\n timeoutMs: numberOr('timeoutMs', DEFAULT_CONFIG.timeoutMs) || DEFAULT_CONFIG.timeoutMs,\n maxStatusBytes: numberOr('maxStatusBytes', DEFAULT_CONFIG.maxStatusBytes) || DEFAULT_CONFIG.maxStatusBytes,\n maxChanges: Math.floor(numberOr('maxChanges', DEFAULT_CONFIG.maxChanges) || DEFAULT_CONFIG.maxChanges),\n defaultRefreshIntervalMs: numberOr('defaultRefreshIntervalMs', DEFAULT_CONFIG.defaultRefreshIntervalMs),\n }\n}\n\n/** Outcome of the cwd resolution step. */\ntype CwdResolution =\n | { readonly ok: true; readonly cwd: string }\n | { readonly ok: false; readonly error: Extract<GitSnapshotResult, { ok: false }>['error'] }\n\nasync function resolveCwd(sessions: SessionLookup, sessionId: string): Promise<CwdResolution> {\n const live = sessions.liveCwd(sessionId)\n if (live !== undefined) return { ok: true, cwd: live }\n const persisted = await sessions.persistedMeta(sessionId)\n if (persisted === undefined) return { ok: false, error: { code: 'session-not-found', sessionId } }\n if (persisted.cwd === undefined) return { ok: false, error: { code: 'cwd-unavailable', sessionId } }\n return { ok: true, cwd: persisted.cwd }\n}\n\n/** Classify a failed run outcome into a snapshot failure. */\nfunction runFailure(result: { readonly timedOut: boolean }, detail: string): Extract<GitSnapshotResult, { ok: false }>['error'] {\n return result.timedOut ? { code: 'timeout' } : { code: 'git-unavailable', detail }\n}\n\n/** Run one command, mapping a spawn-level failure to a snapshot failure. */\nexport async function runCommand(\n runner: GitRunner,\n argv: readonly string[],\n cwd: string,\n label: string,\n signal?: AbortSignal,\n): Promise<{ readonly run: Awaited<ReturnType<GitRunner['run']>> } | { readonly failure: Extract<GitSnapshotResult, { ok: false }>['error'] }> {\n try {\n return { run: await runner.run(argv, { cwd, ...(signal === undefined ? {} : { signal }) }) }\n } catch (error) {\n return { failure: { code: 'git-unavailable', detail: `${label}: ${error instanceof Error ? error.message : String(error)}` } }\n }\n}\n\n/**\n * Resolve a session's repository workspace: cwd (live or persisted), the\n * realpath'd directory, and the git work-tree root via `rev-parse\n * --show-toplevel`. Shared by the snapshot flow and the operation runner.\n */\nexport type WorkspaceResolution =\n | { readonly ok: true; readonly cwd: string; readonly root: string }\n | { readonly ok: false; readonly error: Extract<GitSnapshotResult, { ok: false }>['error'] }\n\nexport async function resolveWorkspace(\n deps: SnapshotDeps,\n sessionId: string,\n): Promise<WorkspaceResolution> {\n const resolved = await resolveCwd(deps.sessions, sessionId)\n if (!resolved.ok) return { ok: false, error: resolved.error }\n\n let realCwd: string\n try {\n realCwd = await deps.fs.realpath(resolved.cwd)\n const stat = await deps.fs.stat(realCwd)\n if (!stat.isDirectory()) {\n return { ok: false, error: { code: 'path-not-found', path: realCwd } }\n }\n } catch {\n return { ok: false, error: { code: 'path-not-found', path: resolved.cwd } }\n }\n\n const toplevel = await runCommand(deps.run, ['git', 'rev-parse', '--show-toplevel'], realCwd, 'rev-parse', deps.signal)\n if ('failure' in toplevel) return { ok: false, error: toplevel.failure }\n if (toplevel.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (toplevel.run.exitCode !== 0) {\n // exit 128 covers both \"not a git repository\" (plain directory) and\n // other git failures (dubious ownership, unreadable work tree, \u2026).\n // Only the former is a stable non-repo state; everything else surfaces\n // as git-unavailable with the actual reason instead of a misleading\n // \"no git repository\" pill.\n const stderr = toplevel.run.stderr\n if (!stderr.includes('not a git repository')) {\n return { ok: false, error: runFailure(toplevel.run, `git rev-parse failed: ${stderr.trim() || `exit ${String(toplevel.run.exitCode)}`}`) }\n }\n return { ok: false, error: { code: 'not-a-git-repo' } }\n }\n const root = toplevel.run.stdout.trim()\n if (root === '') return { ok: false, error: { code: 'not-a-git-repo' } }\n return { ok: true, cwd: realCwd, root }\n}\n\n/**\n * Build one frozen GitSnapshot for a session working directory.\n * Command sequence (all read-only; every command after the first runs with\n * the repository root as cwd):\n * 1. `git rev-parse --show-toplevel` \u2014 repo detection (exit 128 \u2192 not-a-git-repo)\n * 2. `git branch --show-current` \u2014 null when detached\n * 3. `git rev-parse --short HEAD` \u2014 null + unborn when the repo has no commits\n * 4. `git status --porcelain=v1 -z --branch`\n * 5. `git log -n 5 --format=%H%x1f%h%x1f%s%x1f%an%x1f%aI`\n */\nexport async function snapshotForSession(\n deps: SnapshotDeps,\n config: GitStatusConfig,\n sessionId: string,\n): Promise<GitSnapshotResult> {\n const workspace = await resolveWorkspace(deps, sessionId)\n if (!workspace.ok) return { ok: false, error: workspace.error }\n const root = workspace.root\n\n const branchRun = await runCommand(deps.run, ['git', 'branch', '--show-current'], root, 'branch', deps.signal)\n if ('failure' in branchRun) return { ok: false, error: branchRun.failure }\n if (branchRun.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n const branch = branchRun.run.exitCode === 0 ? parseBranchOutput(branchRun.run.stdout) : null\n\n const headRun = await runCommand(deps.run, ['git', 'rev-parse', '--short', 'HEAD'], root, 'rev-parse HEAD', deps.signal)\n if ('failure' in headRun) return { ok: false, error: headRun.failure }\n if (headRun.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n // A failed HEAD read (non-timeout) only nulls the hash: the authoritative\n // unborn flag comes from the status header below (`## No commits yet on\n // main`), so a corrupt repo is never misreported as \"no commits\".\n const head = headRun.run.exitCode === 0 ? (headRun.run.stdout.trim() || null) : null\n\n const status = await runCommand(deps.run, ['git', 'status', '--porcelain=v1', '-z', '--branch'], root, 'status', deps.signal)\n if ('failure' in status) return { ok: false, error: status.failure }\n if (status.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (status.run.exitCode !== 0) {\n return { ok: false, error: runFailure(status.run, `git status exited ${String(status.run.exitCode)}`) }\n }\n const parsed = parseStatusOutput(status.run.stdout, config.maxChanges)\n\n const log = await runCommand(deps.run, ['git', 'log', '-n', '5', '--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI'], root, 'log', deps.signal)\n if ('failure' in log) return { ok: false, error: log.failure }\n if (log.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n const recentCommits = log.run.exitCode === 0 ? parseLogOutput(log.run.stdout) : []\n\n const checkedAt = deps.now?.() ?? Date.now()\n const snapshot: GitSnapshot = {\n root,\n branch,\n head,\n unborn: parsed.unborn,\n dirty: parsed.staged + parsed.modified + parsed.untracked > 0,\n staged: parsed.staged,\n modified: parsed.modified,\n untracked: parsed.untracked,\n ahead: parsed.ahead,\n behind: parsed.behind,\n lastCommit: recentCommits[0] ?? null,\n recentCommits,\n changes: parsed.changes,\n truncated: parsed.truncated || ('run' in status && status.run.stdoutLossy),\n refreshIntervalMs: config.defaultRefreshIntervalMs,\n checkedAt,\n }\n return { ok: true, value: snapshot }\n}\n", "/**\n * Framework-free git management operation runner.\n *\n * Same layering as `core.ts`: every dependency is injected structurally, the\n * whole flow is testable against real temporary git repositories without a\n * cordis runtime, and `GitStatusService` only adapts host services into the\n * `SnapshotDeps` face.\n *\n * Security model: the browser only ever sends a `sessionId` plus\n * repository-relative paths (as listed in a snapshot's `changes`). Paths are\n * validated against the work-tree root (absolute paths and `..` escapes are\n * rejected) and every git invocation uses `--` so a path can never be\n * interpreted as an option. Commands run through the same subprocess adapter\n * as the read-only snapshot flow \u2014 no shell is involved.\n */\nimport { resolve, sep } from 'node:path'\nimport { resolveWorkspace, runCommand, snapshotForSession, type GitStatusConfig, type SnapshotDeps } from './core.ts'\nimport type { GitAction, GitActionResult, GitActionRequest } from './types.ts'\n\n/** Build the command sequence for one action, validating every path against the root. */\nfunction buildArgv(action: GitAction, root: string): { readonly argv: readonly (readonly string[])[] } | { readonly error: string } {\n switch (action.kind) {\n case 'stage':\n return withPaths(['git', 'add', '--'], action.paths, root)\n case 'stage-all':\n return { argv: [['git', 'add', '-A']] }\n case 'unstage':\n return withPaths(['git', 'restore', '--staged', '--'], action.paths, root)\n case 'unstage-all':\n return { argv: [['git', 'restore', '--staged', '--', '.']] }\n case 'discard':\n return withPaths(['git', 'restore', '--'], action.paths, root)\n case 'discard-all':\n // Reset the index to HEAD first, then the work tree to the index \u2014 the\n // IDE-style \"roll back everything tracked\" semantics.\n return { argv: [['git', 'restore', '--staged', '--', '.'], ['git', 'restore', '--', '.']] }\n case 'commit': {\n // Message emptiness is validated by runAction (git-error), not here.\n const message = action.message.trim()\n if (action.paths === undefined || action.paths.length === 0) {\n return { argv: [['git', 'commit', '-m', message]] }\n }\n // git commit -- <paths> stages those paths from the work tree and\n // commits only them (index state of other paths is ignored) \u2014 the\n // IDE-style \"commit selected files\" semantics.\n return withPaths(['git', 'commit', '-m', message, '--'], action.paths, root)\n }\n }\n}\n\n/** Append validated repo-relative paths behind `--`. */\nfunction withPaths(prefix: readonly string[], paths: readonly string[], root: string): { readonly argv: readonly (readonly string[])[] } | { readonly error: string } {\n if (paths.length === 0) return { error: 'no paths given' }\n for (const path of paths) {\n if (!isSafePath(path, root)) return { error: `unsafe path: ${path}` }\n }\n return { argv: [[...prefix, ...paths]] }\n}\n\n/**\n * A path is safe when it is repo-relative and stays inside the work tree:\n * reject absolute paths, drive letters / backslashes, and `..` escapes\n * (checked via path resolution against the realpath'd root).\n */\nexport function isSafePath(path: string, root: string): boolean {\n if (path === '') return false\n if (path.startsWith('/') || path.startsWith('\\\\') || /^[A-Za-z]:/.test(path)) return false\n const resolved = resolve(root, path)\n const prefix = root.endsWith(sep) ? root : `${root}${sep}`\n return resolved === root || resolved.startsWith(prefix)\n}\n\n/** Map a snapshot-flow failure (which may carry git-unavailable) onto the operation error shape. */\nfunction operationError(failure: Extract<Awaited<ReturnType<typeof resolveWorkspace>>, { ok: false }>['error']): GitActionResult & { ok: false } {\n if (failure.code === 'git-unavailable') {\n return { ok: false, error: { code: 'git-error', message: failure.detail } }\n }\n return { ok: false, error: failure }\n}\n\n/**\n * Execute one management action against the session's repository and return\n * the refreshed snapshot on success (the caller re-renders from it, so the\n * UI never waits for the next poll).\n */\nexport async function runAction(\n deps: SnapshotDeps,\n config: GitStatusConfig,\n request: GitActionRequest,\n): Promise<GitActionResult> {\n const workspace = await resolveWorkspace(deps, request.sessionId)\n if (!workspace.ok) return operationError(workspace.error)\n const root = workspace.root\n\n if (request.action.kind === 'commit' && request.action.message.trim() === '') {\n return { ok: false, error: { code: 'git-error', message: 'commit message is empty' } }\n }\n\n const built = buildArgv(request.action, root)\n if ('error' in built) return { ok: false, error: { code: 'invalid-path', message: built.error } }\n\n // Run the command sequence; a failure stops the rest (the first commands\n // may already have taken effect \u2014 they are all idempotent restores).\n let lastStdout = ''\n for (const argv of built.argv) {\n const outcome = await runCommand(deps.run, argv, root, `action ${request.action.kind}`, deps.signal)\n if ('failure' in outcome) return operationError(outcome.failure)\n if (outcome.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (outcome.run.exitCode !== 0) {\n // git writes user-facing failures to stderr OR stdout (e.g. a clean\n // repo's `git commit` reports \"nothing to commit\" on stdout).\n const message = outcome.run.stderr.trim() || outcome.run.stdout.trim()\n return {\n ok: false,\n error: {\n code: 'git-error',\n message: message !== '' ? message : `git ${request.action.kind} exited ${String(outcome.run.exitCode)}`,\n },\n }\n }\n lastStdout = outcome.run.stdout.trim()\n }\n\n const snapshot = await snapshotForSession(deps, config, request.sessionId)\n if (!snapshot.ok) return operationError(snapshot.error)\n return { ok: true, snapshot: snapshot.value, ...(lastStdout === '' ? {} : { output: lastStdout }) }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,SAAS,QAAQ,2BAA2B;AAE5C,SAAS,UAAU,YAAY;;;ACJ/B,SAAS,gBAAgB;AA+ElB,SAAS,gBAAgB,YAA4B,WAAmB,UAA6B;AAC1G,QAAM,gBAAgB,WAAW;AACjC,SAAO;AAAA,IACL,MAAM,IAAI,MAAM,MAAM;AACpB,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC5D,UAAI;AACF,cAAM,SAAS,KAAK,WAAW,SAC3B,WAAW,SACX,YAAY,IAAI,CAAC,WAAW,QAAQ,KAAK,MAAM,CAAC;AACpD,cAAM,SAAS,WAAW,MAAM;AAAA,UAC9B;AAAA,UACA,KAAK,KAAK;AAAA,UACV,OAAO;AAAA,YACL,QAAQ,EAAE,SAAS,EAAE,UAAU,OAAO,EAAE,UAAU,cAAc,EAAE,EAAE;AAAA,YACpE,QAAQ,EAAE,SAAS,EAAE,UAAU,OAAO,EAAE,UAAU,cAAc,EAAE,EAAE;AAAA,UACtE;AAAA,UACA,SAAS;AAAA,UACT;AAAA,QACF,CAAC;AACD,YAAI;AACJ,YAAI;AAGF,oBAAU,MAAM,OAAO;AAAA,QACzB,SAAS,OAAO;AACd,cAAI,WAAW,OAAO,WAAW,KAAK,QAAQ,YAAY,MAAM;AAC9D,mBAAO,EAAE,UAAU,MAAM,QAAQ,IAAI,QAAQ,IAAI,UAAU,MAAM,aAAa,MAAM;AAAA,UACtF;AACA,gBAAM;AAAA,QACR;AACA,cAAM,SAAS,OAAO,UAAU,QAAQ,SAAS,CAAC;AAClD,cAAM,SAAS,OAAO,UAAU,QAAQ,SAAS,CAAC;AAClD,cAAM,iBAAiB,MAAM,cAAc,MAAM;AACjD,eAAO;AAAA,UACL,UAAU,QAAQ;AAAA,UAClB,QAAQ,eAAe;AAAA,UACvB,QAAQ,QAAQ,QAAQ;AAAA,UACxB,UAAU,WAAW,OAAO,WAAW,KAAK,QAAQ,YAAY;AAAA,UAChE,aAAa,eAAe;AAAA,QAC9B;AAAA,MACF,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;AAQA,eAAe,cACb,MAC6D;AAC7D,MAAI,SAAS,OAAW,QAAO,EAAE,MAAM,IAAI,OAAO,MAAM;AACxD,MAAI,CAAC,KAAK,SAAS,KAAK,cAAc,OAAW,QAAO,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM;AAC7F,MAAI;AACF,WAAO,EAAE,MAAM,MAAM,SAAS,KAAK,WAAW,MAAM,GAAG,OAAO,MAAM;AAAA,EACtE,QAAQ;AACN,WAAO,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK;AAAA,EACxC;AACF;;;AClIA,IAAM,MAAM;AAEZ,IAAM,UAAU;AAsBT,SAAS,kBAAkB,MAA4B;AAC5D,QAAM,OAAO,KAAK,WAAW,KAAK,IAAI,KAAK,MAAM,CAAC,IAAI;AACtD,MAAI,SAAS,GAAI,QAAO,EAAE,QAAQ,MAAM,QAAQ,OAAO,OAAO,GAAG,QAAQ,EAAE;AAE3E,QAAM,cAAc,mDAAmD,KAAK,IAAI;AAChF,MAAI,gBAAgB,MAAM;AACxB,WAAO,EAAE,QAAQ,YAAY,CAAC,KAAK,MAAM,QAAQ,MAAM,OAAO,GAAG,QAAQ,EAAE;AAAA,EAC7E;AAEA,QAAM,WAAW,0BAA0B,KAAK,IAAI;AACpD,MAAI,aAAa,MAAM;AACrB,WAAO,EAAE,QAAQ,MAAM,QAAQ,OAAO,OAAO,GAAG,QAAQ,EAAE;AAAA,EAC5D;AAEA,QAAM,eAAe,yBAAyB,KAAK,IAAI;AACvD,QAAM,OAAO,eAAe,CAAC,KAAK;AAClC,MAAI,QAAQ;AACZ,MAAI,SAAS;AACb,MAAI,eAAe,CAAC,MAAM,QAAW;AACnC,eAAW,QAAQ,aAAa,CAAC,EAAE,MAAM,GAAG,GAAG;AAC7C,YAAM,UAAU,KAAK,KAAK;AAC1B,YAAM,aAAa,gBAAgB,KAAK,OAAO;AAC/C,YAAM,cAAc,iBAAiB,KAAK,OAAO;AACjD,UAAI,eAAe,KAAM,SAAQ,OAAO,WAAW,CAAC,CAAC;AACrD,UAAI,gBAAgB,KAAM,UAAS,OAAO,YAAY,CAAC,CAAC;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,SAAS,KAAK,MAAM,OAAO,CAAC,EAAE,CAAC,KAAK;AAC1C,SAAO,EAAE,QAAQ,WAAW,KAAK,OAAO,QAAQ,QAAQ,OAAO,OAAO,OAAO;AAC/E;AAGA,SAAS,aAAa,GAAW,GAA4B;AAC3D,MAAI,MAAM,OAAO,MAAM,IAAK,QAAO;AACnC,MAAI,MAAM,OAAO,MAAM,OAAQ,MAAM,OAAO,MAAM,IAAM,QAAO;AAC/D,UAAQ,GAAG;AAAA,IACT,KAAK;AAAK,aAAO;AAAA,IACjB,KAAK;AAAK,aAAO;AAAA,IACjB,KAAK;AAAK,aAAO;AAAA,IACjB,KAAK;AAAK,aAAO;AAAA,IACjB,KAAK;AAAK,aAAO;AAAA,IACjB,KAAK;AAAK,aAAO;AAAA,IACjB;AAAS,aAAO;AAAA,EAClB;AACF;AAQO,SAAS,kBAAkB,QAAgB,YAAkC;AAClF,QAAM,MAAM,OAAO,MAAM,GAAG;AAE5B,QAAM,WAAW,IAAI,IAAI,SAAS,CAAC,MAAM,KAAK,IAAI,MAAM,GAAG,EAAE,IAAI;AACjE,QAAM,SAAS,kBAAkB,SAAS,CAAC,KAAK,EAAE;AAElD,MAAI,SAAS;AACb,MAAI,WAAW;AACf,MAAI,YAAY;AAChB,QAAM,UAAuB,CAAC;AAC9B,MAAI,YAAY;AAEhB,WAAS,QAAQ,GAAG,QAAQ,SAAS,QAAQ,SAAS,GAAG;AACvD,UAAM,QAAQ,SAAS,KAAK,KAAK;AACjC,UAAM,IAAI,MAAM,CAAC,KAAK;AACtB,UAAM,IAAI,MAAM,CAAC,KAAK;AACtB,UAAM,OAAO,MAAM,MAAM,CAAC;AAC1B,QAAI,MAAM,OAAO,MAAM,IAAK;AAC5B,QAAI,MAAM,OAAO,MAAM,KAAK;AAE1B,eAAS;AAAA,IACX;AACA,QAAI,MAAM,OAAO,MAAM,KAAK;AAC1B,mBAAa;AAAA,IACf,OAAO;AACL,UAAI,MAAM,OAAO,MAAM,IAAK,WAAU;AACtC,UAAI,MAAM,OAAO,MAAM,IAAK,aAAY;AAAA,IAC1C;AACA,QAAI,QAAQ,SAAS,YAAY;AAC/B,cAAQ,KAAK,EAAE,MAAM,QAAQ,aAAa,GAAG,CAAC,GAAG,QAAQ,MAAM,OAAO,MAAM,IAAI,CAAC;AAAA,IACnF,OAAO;AACL,kBAAY;AAAA,IACd;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,QAAQ,OAAO;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO;AAAA,IACf;AAAA,IACA;AAAA,EACF;AACF;AAOO,SAAS,eAAe,QAAsC;AACnE,QAAM,UAAuB,CAAC;AAC9B,aAAW,QAAQ,OAAO,MAAM,IAAI,GAAG;AACrC,QAAI,SAAS,GAAI;AACjB,UAAM,CAAC,MAAM,WAAW,SAAS,QAAQ,OAAO,IAAI,KAAK,MAAM,OAAO;AACtE,QAAI,SAAS,UAAa,SAAS,GAAI;AACvC,YAAQ,KAAK;AAAA,MACX;AAAA,MACA,WAAW,aAAa;AAAA,MACxB,SAAS,WAAW;AAAA,MACpB,QAAQ,UAAU;AAAA,MAClB,SAAS,WAAW;AAAA,IACtB,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAMO,SAAS,kBAAkB,QAA+B;AAC/D,QAAM,UAAU,OAAO,KAAK;AAC5B,SAAO,YAAY,KAAK,OAAO;AACjC;;;AChIO,IAAM,iBAAkC;AAAA,EAC7C,WAAW;AAAA,EACX,gBAAgB,IAAI,OAAO;AAAA,EAC3B,YAAY;AAAA,EACZ,0BAA0B;AAC5B;AAGO,SAAS,gBAAgB,KAA+B;AAC7D,QAAM,QAAS,OAAO,CAAC;AACvB,QAAM,WAAW,CAAC,KAAa,aAA6B;AAC1D,UAAM,YAAY,MAAM,GAAG;AAC3B,WAAO,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,KAAK,aAAa,IAC/E,YACA;AAAA,EACN;AACA,SAAO;AAAA,IACL,WAAW,SAAS,aAAa,eAAe,SAAS,KAAK,eAAe;AAAA,IAC7E,gBAAgB,SAAS,kBAAkB,eAAe,cAAc,KAAK,eAAe;AAAA,IAC5F,YAAY,KAAK,MAAM,SAAS,cAAc,eAAe,UAAU,KAAK,eAAe,UAAU;AAAA,IACrG,0BAA0B,SAAS,4BAA4B,eAAe,wBAAwB;AAAA,EACxG;AACF;AAOA,eAAe,WAAW,UAAyB,WAA2C;AAC5F,QAAM,OAAO,SAAS,QAAQ,SAAS;AACvC,MAAI,SAAS,OAAW,QAAO,EAAE,IAAI,MAAM,KAAK,KAAK;AACrD,QAAM,YAAY,MAAM,SAAS,cAAc,SAAS;AACxD,MAAI,cAAc,OAAW,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,qBAAqB,UAAU,EAAE;AACjG,MAAI,UAAU,QAAQ,OAAW,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,mBAAmB,UAAU,EAAE;AACnG,SAAO,EAAE,IAAI,MAAM,KAAK,UAAU,IAAI;AACxC;AAGA,SAAS,WAAW,QAAwC,QAAoE;AAC9H,SAAO,OAAO,WAAW,EAAE,MAAM,UAAU,IAAI,EAAE,MAAM,mBAAmB,OAAO;AACnF;AAGA,eAAsB,WACpB,QACA,MACA,KACA,OACA,QAC6I;AAC7I,MAAI;AACF,WAAO,EAAE,KAAK,MAAM,OAAO,IAAI,MAAM,EAAE,KAAK,GAAI,WAAW,SAAY,CAAC,IAAI,EAAE,OAAO,EAAG,CAAC,EAAE;AAAA,EAC7F,SAAS,OAAO;AACd,WAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,QAAQ,GAAG,KAAK,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC,GAAG,EAAE;AAAA,EAC/H;AACF;AAWA,eAAsB,iBACpB,MACA,WAC8B;AAC9B,QAAM,WAAW,MAAM,WAAW,KAAK,UAAU,SAAS;AAC1D,MAAI,CAAC,SAAS,GAAI,QAAO,EAAE,IAAI,OAAO,OAAO,SAAS,MAAM;AAE5D,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,KAAK,GAAG,SAAS,SAAS,GAAG;AAC7C,UAAMA,QAAO,MAAM,KAAK,GAAG,KAAK,OAAO;AACvC,QAAI,CAACA,MAAK,YAAY,GAAG;AACvB,aAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,kBAAkB,MAAM,QAAQ,EAAE;AAAA,IACvE;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,kBAAkB,MAAM,SAAS,IAAI,EAAE;AAAA,EAC5E;AAEA,QAAM,WAAW,MAAM,WAAW,KAAK,KAAK,CAAC,OAAO,aAAa,iBAAiB,GAAG,SAAS,aAAa,KAAK,MAAM;AACtH,MAAI,aAAa,SAAU,QAAO,EAAE,IAAI,OAAO,OAAO,SAAS,QAAQ;AACvE,MAAI,SAAS,IAAI,SAAU,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,UAAU,EAAE;AAC1E,MAAI,SAAS,IAAI,aAAa,GAAG;AAM/B,UAAM,SAAS,SAAS,IAAI;AAC5B,QAAI,CAAC,OAAO,SAAS,sBAAsB,GAAG;AAC5C,aAAO,EAAE,IAAI,OAAO,OAAO,WAAW,SAAS,KAAK,yBAAyB,OAAO,KAAK,KAAK,QAAQ,OAAO,SAAS,IAAI,QAAQ,CAAC,EAAE,EAAE,EAAE;AAAA,IAC3I;AACA,WAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,iBAAiB,EAAE;AAAA,EACxD;AACA,QAAM,OAAO,SAAS,IAAI,OAAO,KAAK;AACtC,MAAI,SAAS,GAAI,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,iBAAiB,EAAE;AACvE,SAAO,EAAE,IAAI,MAAM,KAAK,SAAS,KAAK;AACxC;AAYA,eAAsB,mBACpB,MACA,QACA,WAC4B;AAC5B,QAAM,YAAY,MAAM,iBAAiB,MAAM,SAAS;AACxD,MAAI,CAAC,UAAU,GAAI,QAAO,EAAE,IAAI,OAAO,OAAO,UAAU,MAAM;AAC9D,QAAM,OAAO,UAAU;AAEvB,QAAM,YAAY,MAAM,WAAW,KAAK,KAAK,CAAC,OAAO,UAAU,gBAAgB,GAAG,MAAM,UAAU,KAAK,MAAM;AAC7G,MAAI,aAAa,UAAW,QAAO,EAAE,IAAI,OAAO,OAAO,UAAU,QAAQ;AACzE,MAAI,UAAU,IAAI,SAAU,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,UAAU,EAAE;AAC3E,QAAM,SAAS,UAAU,IAAI,aAAa,IAAI,kBAAkB,UAAU,IAAI,MAAM,IAAI;AAExF,QAAM,UAAU,MAAM,WAAW,KAAK,KAAK,CAAC,OAAO,aAAa,WAAW,MAAM,GAAG,MAAM,kBAAkB,KAAK,MAAM;AACvH,MAAI,aAAa,QAAS,QAAO,EAAE,IAAI,OAAO,OAAO,QAAQ,QAAQ;AACrE,MAAI,QAAQ,IAAI,SAAU,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,UAAU,EAAE;AAIzE,QAAM,OAAO,QAAQ,IAAI,aAAa,IAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,OAAQ;AAEhF,QAAM,SAAS,MAAM,WAAW,KAAK,KAAK,CAAC,OAAO,UAAU,kBAAkB,MAAM,UAAU,GAAG,MAAM,UAAU,KAAK,MAAM;AAC5H,MAAI,aAAa,OAAQ,QAAO,EAAE,IAAI,OAAO,OAAO,OAAO,QAAQ;AACnE,MAAI,OAAO,IAAI,SAAU,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,UAAU,EAAE;AACxE,MAAI,OAAO,IAAI,aAAa,GAAG;AAC7B,WAAO,EAAE,IAAI,OAAO,OAAO,WAAW,OAAO,KAAK,qBAAqB,OAAO,OAAO,IAAI,QAAQ,CAAC,EAAE,EAAE;AAAA,EACxG;AACA,QAAM,SAAS,kBAAkB,OAAO,IAAI,QAAQ,OAAO,UAAU;AAErE,QAAM,MAAM,MAAM,WAAW,KAAK,KAAK,CAAC,OAAO,OAAO,MAAM,KAAK,uCAAuC,GAAG,MAAM,OAAO,KAAK,MAAM;AACnI,MAAI,aAAa,IAAK,QAAO,EAAE,IAAI,OAAO,OAAO,IAAI,QAAQ;AAC7D,MAAI,IAAI,IAAI,SAAU,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,UAAU,EAAE;AACrE,QAAM,gBAAgB,IAAI,IAAI,aAAa,IAAI,eAAe,IAAI,IAAI,MAAM,IAAI,CAAC;AAEjF,QAAM,YAAY,KAAK,MAAM,KAAK,KAAK,IAAI;AAC3C,QAAM,WAAwB;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,OAAO,OAAO,SAAS,OAAO,WAAW,OAAO,YAAY;AAAA,IAC5D,QAAQ,OAAO;AAAA,IACf,UAAU,OAAO;AAAA,IACjB,WAAW,OAAO;AAAA,IAClB,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO;AAAA,IACf,YAAY,cAAc,CAAC,KAAK;AAAA,IAChC;AAAA,IACA,SAAS,OAAO;AAAA,IAChB,WAAW,OAAO,aAAc,SAAS,UAAU,OAAO,IAAI;AAAA,IAC9D,mBAAmB,OAAO;AAAA,IAC1B;AAAA,EACF;AACA,SAAO,EAAE,IAAI,MAAM,OAAO,SAAS;AACrC;;;AC1MA,SAAS,SAAS,WAAW;AAK7B,SAAS,UAAU,QAAmB,MAA8F;AAClI,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO,UAAU,CAAC,OAAO,OAAO,IAAI,GAAG,OAAO,OAAO,IAAI;AAAA,IAC3D,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,CAAC,OAAO,OAAO,IAAI,CAAC,EAAE;AAAA,IACxC,KAAK;AACH,aAAO,UAAU,CAAC,OAAO,WAAW,YAAY,IAAI,GAAG,OAAO,OAAO,IAAI;AAAA,IAC3E,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,CAAC,OAAO,WAAW,YAAY,MAAM,GAAG,CAAC,EAAE;AAAA,IAC7D,KAAK;AACH,aAAO,UAAU,CAAC,OAAO,WAAW,IAAI,GAAG,OAAO,OAAO,IAAI;AAAA,IAC/D,KAAK;AAGH,aAAO,EAAE,MAAM,CAAC,CAAC,OAAO,WAAW,YAAY,MAAM,GAAG,GAAG,CAAC,OAAO,WAAW,MAAM,GAAG,CAAC,EAAE;AAAA,IAC5F,KAAK,UAAU;AAEb,YAAM,UAAU,OAAO,QAAQ,KAAK;AACpC,UAAI,OAAO,UAAU,UAAa,OAAO,MAAM,WAAW,GAAG;AAC3D,eAAO,EAAE,MAAM,CAAC,CAAC,OAAO,UAAU,MAAM,OAAO,CAAC,EAAE;AAAA,MACpD;AAIA,aAAO,UAAU,CAAC,OAAO,UAAU,MAAM,SAAS,IAAI,GAAG,OAAO,OAAO,IAAI;AAAA,IAC7E;AAAA,EACF;AACF;AAGA,SAAS,UAAU,QAA2B,OAA0B,MAA8F;AACpK,MAAI,MAAM,WAAW,EAAG,QAAO,EAAE,OAAO,iBAAiB;AACzD,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,WAAW,MAAM,IAAI,EAAG,QAAO,EAAE,OAAO,gBAAgB,IAAI,GAAG;AAAA,EACtE;AACA,SAAO,EAAE,MAAM,CAAC,CAAC,GAAG,QAAQ,GAAG,KAAK,CAAC,EAAE;AACzC;AAOO,SAAS,WAAW,MAAc,MAAuB;AAC9D,MAAI,SAAS,GAAI,QAAO;AACxB,MAAI,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,IAAI,KAAK,aAAa,KAAK,IAAI,EAAG,QAAO;AACrF,QAAM,WAAW,QAAQ,MAAM,IAAI;AACnC,QAAM,SAAS,KAAK,SAAS,GAAG,IAAI,OAAO,GAAG,IAAI,GAAG,GAAG;AACxD,SAAO,aAAa,QAAQ,SAAS,WAAW,MAAM;AACxD;AAGA,SAAS,eAAe,SAAyH;AAC/I,MAAI,QAAQ,SAAS,mBAAmB;AACtC,WAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,aAAa,SAAS,QAAQ,OAAO,EAAE;AAAA,EAC5E;AACA,SAAO,EAAE,IAAI,OAAO,OAAO,QAAQ;AACrC;AAOA,eAAsB,UACpB,MACA,QACA,SAC0B;AAC1B,QAAM,YAAY,MAAM,iBAAiB,MAAM,QAAQ,SAAS;AAChE,MAAI,CAAC,UAAU,GAAI,QAAO,eAAe,UAAU,KAAK;AACxD,QAAM,OAAO,UAAU;AAEvB,MAAI,QAAQ,OAAO,SAAS,YAAY,QAAQ,OAAO,QAAQ,KAAK,MAAM,IAAI;AAC5E,WAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,aAAa,SAAS,0BAA0B,EAAE;AAAA,EACvF;AAEA,QAAM,QAAQ,UAAU,QAAQ,QAAQ,IAAI;AAC5C,MAAI,WAAW,MAAO,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,gBAAgB,SAAS,MAAM,MAAM,EAAE;AAIhG,MAAI,aAAa;AACjB,aAAW,QAAQ,MAAM,MAAM;AAC7B,UAAM,UAAU,MAAM,WAAW,KAAK,KAAK,MAAM,MAAM,UAAU,QAAQ,OAAO,IAAI,IAAI,KAAK,MAAM;AACnG,QAAI,aAAa,QAAS,QAAO,eAAe,QAAQ,OAAO;AAC/D,QAAI,QAAQ,IAAI,SAAU,QAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,UAAU,EAAE;AACzE,QAAI,QAAQ,IAAI,aAAa,GAAG;AAG9B,YAAM,UAAU,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK;AACrE,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO;AAAA,UACL,MAAM;AAAA,UACN,SAAS,YAAY,KAAK,UAAU,OAAO,QAAQ,OAAO,IAAI,WAAW,OAAO,QAAQ,IAAI,QAAQ,CAAC;AAAA,QACvG;AAAA,MACF;AAAA,IACF;AACA,iBAAa,QAAQ,IAAI,OAAO,KAAK;AAAA,EACvC;AAEA,QAAM,WAAW,MAAM,mBAAmB,MAAM,QAAQ,QAAQ,SAAS;AACzE,MAAI,CAAC,SAAS,GAAI,QAAO,eAAe,SAAS,KAAK;AACtD,SAAO,EAAE,IAAI,MAAM,UAAU,SAAS,OAAO,GAAI,eAAe,KAAK,CAAC,IAAI,EAAE,QAAQ,WAAW,EAAG;AACpG;;;AJ9HA;AAuCO,IAAM,mBAAN,eAA+B,0BAwCpC,iBAAC,OAAO,UAAU,IAOlB,YAAC,OAAO,KAAK,IA/CuB,IAAoB;AAAA,EAKxD,YAAY,KAAc,QAAiB;AACzC,UAAM,KAAK,SAAS;AANjB;AAGL,wBAAiB;AAIf,SAAK,SAAS,gBAAgB,MAAM;AAAA,EACtC;AAAA;AAAA,EAGQ,KAAK,QAA6I;AACxJ,UAAM,aAAa,KAAK,IAAI,IAAI,YAAY;AAC5C,QAAI,eAAe,QAAW;AAC5B,aAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,QAAQ,iCAAiC,EAAE;AAAA,IAC1F;AACA,UAAM,WAAW,KAAK,IAAI,IAAI,UAAU;AACxC,UAAM,cAAc,KAAK,IAAI,IAAI,oBAAoB;AACrD,UAAM,SAAS,gBAAgB,YAAY,KAAK,OAAO,WAAW,KAAK,OAAO,cAAc;AAC5F,WAAO;AAAA,MACL,MAAM;AAAA,QACJ,KAAK;AAAA,QACL,IAAI,EAAE,UAAU,KAAK;AAAA,QACrB,UAAU;AAAA,UACR,SAAS,CAAC,OAAO,UAAU,IAAI,EAAE,GAAG,QAAQ;AAAA,UAC5C,eAAe,OAAO,OAAO;AAC3B,gBAAI,gBAAgB,OAAW,QAAO;AACtC,gBAAI;AACF,oBAAM,aAAa,MAAM,YAAY,QAAQ,EAAE;AAC/C,qBAAO,EAAE,KAAK,WAAW,KAAK,IAAI;AAAA,YACpC,QAAQ;AACN,qBAAO;AAAA,YACT;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAGA,MAAM,SAAS,SAA6B,QAAkD;AAC5F,UAAM,UAAU,KAAK,KAAK,MAAM;AAChC,QAAI,aAAa,QAAS,QAAO,EAAE,IAAI,OAAO,OAAO,QAAQ,QAAQ;AACrE,WAAO,mBAAmB,QAAQ,MAAM,KAAK,QAAQ,QAAQ,SAAS;AAAA,EACxE;AAAA,EAGA,MAAM,IAAI,SAA2B,QAAgD;AACnF,UAAM,UAAU,KAAK,KAAK,MAAM;AAChC,QAAI,aAAa,SAAS;AACxB,aAAO,EAAE,IAAI,OAAO,OAAO,EAAE,MAAM,aAAa,SAAS,QAAQ,QAAQ,OAAO,EAAE;AAAA,IACpF;AACA,WAAO,UAAU,QAAQ,MAAM,KAAK,QAAQ,OAAO;AAAA,EACrD;AACF;AAvDO;AAyCL,4BAAM,YADN,eAxCW;AAgDX,4BAAM,OADN,UA/CW;AAAN,2BAAM;AACX,cADW,kBACJ,UAAS,CAAC,cAAc,YAAY,oBAAoB;AAwDjE,IAAO,gBAAQ;",
|
|
6
|
-
"names": ["stat"]
|
|
3
|
+
"sources": ["../../src/host/index.ts", "../../src/host/git.ts", "../../src/host/parser.ts", "../../src/host/core.ts", "../../src/host/actions.ts", "../../src/host/queries.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * dsh-git-ui host half: the `gitInfo` Remote service.\n *\n * Cordis shell only \u2014 every behavior lives in `core.ts`/`actions.ts`/\n * `queries.ts` behind injected structural faces, so tests never need a\n * cordis runtime. The class is a plugin in its own right (class form),\n * mounted by the bundle patch row with the package name; the gateway exposes\n * `gitInfo/snapshot`, `gitInfo/run` and `gitInfo/query` through SRC\n * discovery (`typertRemote` binding + `@Remote` marker).\n */\nimport { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { realpath, stat } from 'node:fs/promises'\nimport { createGitRunner, type SubprocessLike } from './git.ts'\nimport { normalizeConfig, snapshotForSession, type GitStatusConfig, type SnapshotDeps } from './core.ts'\nimport { runAction } from './actions.ts'\nimport { runQuery } from './queries.ts'\nimport type { GitActionResult, GitActionRequest, GitQueryRequest, GitQueryResponse, GitSnapshotRequest, GitSnapshotResult } from './types.ts'\n\nexport type { GitSnapshot, GitSnapshotResult, GitSnapshotFailure, GitSnapshotRequest, GitCommit, GitChange, GitAction, GitActionResult, GitActionRequest, GitQuery, GitQueryResult, GitQueryRequest, GitQueryResponse, GitBranch, GitFileStat, GitRef } from './types.ts'\nexport { normalizeConfig, DEFAULT_CONFIG } from './core.ts'\nexport { parseStatusOutput, parseLogOutput, parseBranchOutput, parseNameStatusOutput } from './parser.ts'\nexport { isSafePath, isValidBranchName, runAction } from './actions.ts'\nexport { runQuery } from './queries.ts'\n\n/** Structural face of a live session header. */\ninterface SessionLike {\n readonly header?: { readonly cwd?: string }\n}\n\n/** Structural face of the sessions service. */\ninterface SessionsLike {\n get(id: string): SessionLike | undefined\n}\n\n/** Structural face of the session-persistence service. */\ninterface SessionPersistenceLike {\n inspect(id: string): Promise<{ readonly meta: { readonly cwd?: string } }>\n}\n\n/** The `gitInfo` service: `snapshot` (read) and `run` (management) endpoints. */\nexport class GitStatusService extends TypertRemoteService {\n static inject = ['subprocess', 'sessions', 'sessionPersistence']\n\n private readonly config: GitStatusConfig\n\n constructor(ctx: Context, config: unknown) {\n super(ctx, 'gitInfo')\n this.config = normalizeConfig(config)\n }\n\n /** Adapter face shared by both endpoints (injected services + runner). */\n private deps(signal?: AbortSignal): { readonly deps: SnapshotDeps } | { readonly failure: { readonly code: 'git-unavailable'; readonly detail: string } } {\n const subprocess = this.ctx.get('subprocess') as SubprocessLike | undefined\n if (subprocess === undefined) {\n return { failure: { code: 'git-unavailable', detail: 'subprocess service unavailable' } }\n }\n const sessions = this.ctx.get('sessions') as SessionsLike | undefined\n const persistence = this.ctx.get('sessionPersistence') as SessionPersistenceLike | undefined\n const runner = createGitRunner(subprocess, this.config.timeoutMs, this.config.maxStatusBytes)\n return {\n deps: {\n run: runner,\n fs: { realpath, stat },\n sessions: {\n liveCwd: (id) => sessions?.get(id)?.header?.cwd,\n persistedMeta: async (id) => {\n if (persistence === undefined) return undefined\n try {\n const inspection = await persistence.inspect(id)\n return { cwd: inspection.meta.cwd }\n } catch {\n return undefined\n }\n },\n },\n signal,\n },\n }\n }\n\n @Remote('snapshot')\n async snapshot(request: GitSnapshotRequest, signal?: AbortSignal): Promise<GitSnapshotResult> {\n const adapted = this.deps(signal)\n if ('failure' in adapted) return { ok: false, error: adapted.failure }\n return snapshotForSession(adapted.deps, this.config, request.sessionId)\n }\n\n @Remote('run')\n async run(request: GitActionRequest, signal?: AbortSignal): Promise<GitActionResult> {\n const adapted = this.deps(signal)\n if ('failure' in adapted) {\n return { ok: false, error: { code: 'git-error', message: adapted.failure.detail } }\n }\n return runAction(adapted.deps, this.config, request)\n }\n\n @Remote('query')\n async query(request: GitQueryRequest, signal?: AbortSignal): Promise<GitQueryResponse> {\n const adapted = this.deps(signal)\n if ('failure' in adapted) {\n return { ok: false, error: { code: 'git-error', message: adapted.failure.detail } }\n }\n return runQuery(adapted.deps, this.config, request)\n }\n}\n\nexport default GitStatusService\n", "/**\n * Git command execution adapter over the host subprocess service.\n *\n * The widget only needs a tiny slice of the subprocess contract; declaring it\n * structurally here (instead of depending on the npm package, whose registry\n * chain is incomplete) keeps the plugin buildable standalone while remaining\n * wire-compatible with the host's `subprocess` service.\n */\nimport { readFile } from 'node:fs/promises'\n\n/** One collected stream disposition (matches the host SubprocessCollect). */\ninterface CollectDisposition {\n readonly collect: {\n readonly maxBytes: number\n /**\n * Spill disposition: when the stream overflows the in-memory tail, the\n * host appends the COMPLETE stream to a private spill file (up to this\n * cap) and `readFrom` reports its path. Without it, only the tail is\n * ever retained and the head (and its change counts) is lost.\n */\n readonly spill?: { readonly maxBytes: number }\n }\n}\n\n/** Structural slice of the host subprocess spawn spec. */\ninterface SpawnSpec {\n readonly argv: readonly string[]\n readonly cwd: string\n readonly stdio: {\n readonly stdout: CollectDisposition\n readonly stderr: CollectDisposition\n }\n readonly graceMs: number\n readonly signal?: AbortSignal\n}\n\n/** Structural slice of the host subprocess handle (collect-mode output). */\ninterface SpawnHandle {\n readonly done: Promise<{ readonly exitCode: number | null; readonly signal: NodeJS.Signals | null }>\n readonly collected: {\n readonly stdout?: {\n readFrom(fromByte: number): { readonly text: string; readonly lossy: boolean; readonly spillPath?: string }\n }\n readonly stderr?: {\n readFrom(fromByte: number): { readonly text: string; readonly lossy: boolean; readonly spillPath?: string }\n }\n }\n}\n\n/** Minimal subprocess-service face the adapter consumes. */\nexport interface SubprocessLike {\n spawn(spec: SpawnSpec): SpawnHandle\n}\n\n/** One git command outcome. */\nexport interface GitRunResult {\n /** Process exit code; null when terminated by a signal. */\n readonly exitCode: number | null\n readonly stdout: string\n readonly stderr: string\n /** True when the run was killed by our timeout (or the caller's signal). */\n readonly timedOut: boolean\n /**\n * True when the final stdout text is still incomplete: the collected\n * output overflowed its byte cap AND the spill file was unavailable (no\n * spill configured on the host, or the spill cap also overflowed).\n */\n readonly stdoutLossy: boolean\n}\n\n/** The run primitive the snapshot orchestration uses. */\nexport interface GitRunner {\n run(argv: readonly string[], opts: { readonly cwd: string; readonly signal?: AbortSignal }): Promise<GitRunResult>\n}\n\n/**\n * Adapt the host subprocess service into a `GitRunner` with a per-command\n * timeout. A timed-out run resolves (never rejects) with `timedOut: true`;\n * only spawn-level failures (e.g. git not installed) reject.\n *\n * Overflow handling: stdout/stderr collect with a spill cap of\n * `maxBytes * 16` (default 4 MiB memory tail \u2192 64 MiB spill file). When the\n * tail overflowed but the spill file holds the complete stream, the runner\n * reads the file and reports `stdoutLossy: false` \u2014 the change COUNTS stay\n * exact. `stdoutLossy: true` is reserved for the doubly-overflowed case\n * (spill also exceeded), where the head is genuinely lost.\n */\nexport function createGitRunner(subprocess: SubprocessLike, timeoutMs: number, maxBytes: number): GitRunner {\n const spillMaxBytes = maxBytes * 16\n return {\n async run(argv, opts) {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const signal = opts.signal === undefined\n ? controller.signal\n : AbortSignal.any([controller.signal, opts.signal])\n const handle = subprocess.spawn({\n argv,\n cwd: opts.cwd,\n stdio: {\n stdout: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },\n stderr: { collect: { maxBytes, spill: { maxBytes: spillMaxBytes } } },\n },\n graceMs: 200,\n signal,\n })\n let outcome: Awaited<SpawnHandle['done']>\n try {\n // `done` rejects for spawn-level failures; an abort-triggered\n // rejection is the timeout path and resolves as timedOut.\n outcome = await handle.done\n } catch (error) {\n if (controller.signal.aborted || opts.signal?.aborted === true) {\n return { exitCode: null, stdout: '', stderr: '', timedOut: true, stdoutLossy: false }\n }\n throw error\n }\n const stdout = handle.collected.stdout?.readFrom(0)\n const stderr = handle.collected.stderr?.readFrom(0)\n const stdoutResolved = await resolveStdout(stdout)\n return {\n exitCode: outcome.exitCode,\n stdout: stdoutResolved.text,\n stderr: stderr?.text ?? '',\n timedOut: controller.signal.aborted || opts.signal?.aborted === true,\n stdoutLossy: stdoutResolved.lossy,\n }\n } finally {\n clearTimeout(timer)\n }\n },\n }\n}\n\n/**\n * Resolve the stdout text from a collect read: the in-memory tail, or \u2014 when\n * the read is lossy and the host spilled the complete stream to a file \u2014 the\n * spill file contents (so change COUNTS stay exact). A failed spill read\n * falls back to the tail and keeps `lossy: true` (head genuinely lost).\n */\nasync function resolveStdout(\n read: { readonly text: string; readonly lossy: boolean; readonly spillPath?: string } | undefined,\n): Promise<{ readonly text: string; readonly lossy: boolean }> {\n if (read === undefined) return { text: '', lossy: false }\n if (!read.lossy || read.spillPath === undefined) return { text: read.text, lossy: read.lossy }\n try {\n return { text: await readFile(read.spillPath, 'utf8'), lossy: false }\n } catch {\n return { text: read.text, lossy: true }\n }\n}\n", "/**\n * Pure parsers for the git porcelain/log output shapes used by the widget.\n * No side effects and no I/O \u2014 fully unit-testable against literal fixtures\n * (verified against real `git status --porcelain=v1 -z --branch` output).\n */\nimport type { GitChange, GitChangeStatus, GitCommit, GitRef, GraphCommit } from './types.ts'\n\n/** Parsed status counts plus the (possibly capped) change list. */\nexport interface ParsedStatus {\n readonly branch: string | null\n readonly unborn: boolean\n readonly staged: number\n readonly modified: number\n readonly untracked: number\n readonly ahead: number\n readonly behind: number\n readonly changes: readonly GitChange[]\n readonly truncated: boolean\n}\n\n/** The NUL byte separating porcelain v1 -z entries. */\nconst NUL = '\\u0000'\n/** The unit separator used by the log --format payload. */\nconst LOG_SEP = '\\u001f'\n\ninterface StatusHeader {\n readonly branch: string | null\n readonly unborn: boolean\n readonly ahead: number\n readonly behind: number\n}\n\n/**\n * Parse the `## ` header line of `git status --porcelain=v1 -z --branch`.\n * Recognized shapes (verified against git 2.x):\n * `## main`\n * `## main...origin/main`\n * `## main...origin/main [ahead 1]`\n * `## main...origin/main [behind 2]`\n * `## main...origin/main [ahead 1, behind 2]`\n * `## HEAD (no branch)` (detached)\n * `## HEAD (detached at <hash>)` (detached, older git)\n * `## No commits yet on main` (unborn)\n * `## Initial commit on main` (unborn, older git)\n */\nexport function parseStatusHeader(line: string): StatusHeader {\n const body = line.startsWith('## ') ? line.slice(3) : line\n if (body === '') return { branch: null, unborn: false, ahead: 0, behind: 0 }\n\n const unbornMatch = /^(?:No commits yet on|Initial commit on)\\s+(.+)$/.exec(body)\n if (unbornMatch !== null) {\n return { branch: unbornMatch[1] ?? null, unborn: true, ahead: 0, behind: 0 }\n }\n\n const detached = /^HEAD(?:\\s+\\([^)]*\\))?$/.exec(body)\n if (detached !== null) {\n return { branch: null, unborn: false, ahead: 0, behind: 0 }\n }\n\n const bracketMatch = /^(.*?)\\s*\\[([^\\]]+)\\]$/.exec(body)\n const core = bracketMatch?.[1] ?? body\n let ahead = 0\n let behind = 0\n if (bracketMatch?.[2] !== undefined) {\n for (const part of bracketMatch[2].split(',')) {\n const trimmed = part.trim()\n const aheadMatch = /^ahead (\\d+)$/.exec(trimmed)\n const behindMatch = /^behind (\\d+)$/.exec(trimmed)\n if (aheadMatch !== null) ahead = Number(aheadMatch[1])\n if (behindMatch !== null) behind = Number(behindMatch[1])\n }\n }\n // The core is `<branch>...<upstream>` \u2014 the branch never contains `...`.\n const branch = core.split('...', 1)[0] ?? core\n return { branch: branch === '' ? null : branch, unborn: false, ahead, behind }\n}\n\n/** \u5355\u5217\u72B6\u6001\u7801 \u2192 \u53D8\u66F4\u72B6\u6001\u6620\u5C04\uFF08\u771F\u5B9E\u51B2\u7A81\u7531 isConflicted \u5355\u72EC\u5224\u5B9A\uFF09\u3002 */\nfunction singleStatus(code: string): GitChangeStatus {\n switch (code) {\n case 'A': return 'added'\n case 'M': return 'modified'\n case 'D': return 'deleted'\n case 'R': return 'renamed'\n case 'T': return 'typechange'\n case 'C': return 'added'\n default: return 'modified'\n }\n}\n\n/**\n * \u771F\u5B9E\u5408\u5E76\u51B2\u7A81\uFF1A\u4EFB\u4E00\u4FA7\u4E3A U\uFF08UU/AU/UD/UA/DU\uFF09\uFF0C\u6216\u53CC\u65B9\u540C\u6DFB/\u540C\u5220\uFF08AA/DD\uFF09\u3002\n * \u6CE8\u610F MM/AM/MD \u7B49\u300C\u5DF2\u6682\u5B58 + \u5DE5\u4F5C\u533A\u518D\u6539\u300D\u662F\u5408\u6CD5\u6DF7\u5408\u6001\u800C\u975E\u51B2\u7A81\n * \uFF08\u65E7\u89C4\u5219\u300C\u53CC\u5217\u5747\u975E\u7A7A\u5373\u51B2\u7A81\u300D\u4F1A\u628A MM \u8BEF\u62A5\u4E3A\u51B2\u7A81\uFF09\u3002\n */\nfunction isConflicted(x: string, y: string): boolean {\n return x === 'U' || y === 'U' || (x === 'A' && y === 'A') || (x === 'D' && y === 'D')\n}\n\n/**\n * Parse the full `git status --porcelain=v1 -z --branch` output.\n * -z format: every entry (header and each `XY path`) is NUL-terminated; a\n * rename/copy entry emits `R <new>\\0<old>\\0` so the following item is the\n * source path and must be consumed without becoming a change itself.\n *\n * \u6DF7\u5408\u72B6\u6001\u62C6\u5206\uFF08IDEA \u5F0F\uFF09\uFF1AX\u3001Y \u5747\u975E\u7A7A\u7684\u5408\u6CD5\u5BF9\uFF08MM/AM/MD/RM\u2026\uFF09\u62C6\u4E3A\n * \u300C\u5DF2\u6682\u5B58\u4FA7 + \u672A\u6682\u5B58\u4FA7\u300D\u4E24\u6761 GitChange\u2014\u2014UI \u636E\u6B64\u628A\u540C\u4E00\u6587\u4EF6\u5206\u522B\u5217\u5165\n * \u300C\u5DF2\u6682\u5B58\u66F4\u6539\u300D\u4E0E\u300C\u66F4\u6539\u300D\u4E24\u7EC4\uFF0C\u4E24\u4FA7\u53EF\u72EC\u7ACB\u64CD\u4F5C\u3001\u5DEE\u5F02\u57FA\u7EBF\u552F\u4E00\u3002\n * \u771F\u5B9E\u51B2\u7A81\uFF08isConflicted\uFF09\u4FDD\u6301\u5355\u6761 conflicted \u6761\u76EE\u3002\n */\nexport function parseStatusOutput(output: string, maxChanges: number): ParsedStatus {\n const raw = output.split(NUL)\n // Trailing NUL produces a final empty segment; drop it.\n const segments = raw[raw.length - 1] === '' ? raw.slice(0, -1) : raw\n const header = parseStatusHeader(segments[0] ?? '')\n\n let staged = 0\n let modified = 0\n let untracked = 0\n const changes: GitChange[] = []\n let truncated = false\n\n /** \u6536\u5F55\u4E00\u6761\u53D8\u66F4\u6761\u76EE\uFF1B\u8D85\u51FA\u4E0A\u9650\u4EC5\u7F6E\u622A\u65AD\u6807\u8BB0\uFF08\u8BA1\u6570\u4E0D\u53D7\u5F71\u54CD\uFF09\u3002\n * isDirectory \u7531 git \u8F93\u51FA\u6743\u5A01\u6807\u8BB0\uFF08\u672A\u8DDF\u8E2A\u76EE\u5F55\u6761\u76EE\u4E3A `dir/` \u5C3E\u659C\u6760\uFF09\uFF0C\n * \u5C55\u793A\u5C42\u4F9D\u8D56\u6B64\u5B57\u6BB5\uFF0C\u4E0D\u518D\u81EA\u884C\u89E3\u6790\u8DEF\u5F84\u5B57\u7B26\u4E32\u3002 */\n const pushChange = (path: string, status: GitChangeStatus, isStaged: boolean): void => {\n if (changes.length < maxChanges) {\n changes.push({ path, status, staged: isStaged, isDirectory: path.endsWith('/') })\n } else {\n truncated = true\n }\n }\n\n for (let index = 1; index < segments.length; index += 1) {\n const entry = segments[index] ?? ''\n const x = entry[0] ?? ' '\n const y = entry[1] ?? ' '\n const path = entry.slice(3)\n if (x === ' ' && y === ' ') continue\n if (x === 'R' || x === 'C') {\n // -z: the source path is the next segment \u2014 consume it.\n index += 1\n }\n if (x === '?' && y === '?') {\n untracked += 1\n pushChange(path, 'untracked', false)\n continue\n }\n // \u8BA1\u6570\u4ECD\u6309 X/Y \u4E24\u5217\u5206\u522B\u7D2F\u8BA1\uFF1B\u62C6\u53CC\u6761\u76EE\u4E0D\u6539\u53D8\u603B\u6570\u3002\n if (x !== ' ' && x !== '?') staged += 1\n if (y !== ' ' && y !== '?') modified += 1\n\n if (isConflicted(x, y)) {\n // \u51B2\u7A81\u6587\u4EF6\u6309\u5355\u6761\u5C55\u793A\uFF08IDEA \u51B2\u7A81\u6761\u76EE\u5F62\u6001\uFF09\uFF0C\u5F52\u5165\u5DF2\u6682\u5B58\u7EC4\u3002\n pushChange(path, 'conflicted', true)\n } else if (x !== ' ' && y !== ' ') {\n // \u6DF7\u5408\u6001\uFF1A\u5DF2\u6682\u5B58\u4FA7\u72B6\u6001\u53D6 X\uFF0C\u672A\u6682\u5B58\u4FA7\u72B6\u6001\u53D6 Y\u3002\n pushChange(path, singleStatus(x), true)\n pushChange(path, singleStatus(y), false)\n } else {\n const isStaged = x !== ' '\n pushChange(path, singleStatus(isStaged ? x : y), isStaged)\n }\n }\n\n return {\n branch: header.branch,\n unborn: header.unborn,\n staged,\n modified,\n untracked,\n ahead: header.ahead,\n behind: header.behind,\n changes,\n truncated,\n }\n}\n\n/**\n * Parse `git log -n 5 --format=%H%x1f%h%x1f%s%x1f%an%x1f%aI` output.\n * One commit per line, fields separated by the unit separator; empty output\n * (unborn repository) yields `[]`.\n */\nexport function parseLogOutput(output: string): readonly GitCommit[] {\n const commits: GitCommit[] = []\n for (const line of output.split('\\n')) {\n if (line === '') continue\n const [hash, shortHash, subject, author, dateIso] = line.split(LOG_SEP)\n if (hash === undefined || hash === '') continue\n commits.push({\n hash,\n shortHash: shortHash ?? '',\n subject: subject ?? '',\n author: author ?? '',\n dateIso: dateIso ?? '',\n })\n }\n return commits\n}\n\n/**\n * \u89E3\u6790\u5E26\u56FE\u7684 log \u8F93\u51FA\uFF1A\n * `%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%P%x1f%D`\n * \u5176\u4E2D `%P` \u4E3A\u7A7A\u683C\u5206\u9694\u7684\u7236\u63D0\u4EA4\u54C8\u5E0C\uFF08\u6839\u63D0\u4EA4\u4E3A\u7A7A\uFF09\uFF0C`%D` \u4E3A ref \u88C5\u9970\u3002\n * \u8FD4\u56DE\u9002\u5408\u5206\u652F\u56FE\u6E32\u67D3\u5668\u7684 `GraphCommit[]`\u3002\n */\nexport function parseGraphLogOutput(output: string, remotes: readonly string[] = []): readonly GraphCommit[] {\n const commits: GraphCommit[] = []\n for (const line of output.split('\\n')) {\n if (line === '') continue\n const [hash, shortHash, subject, author, dateIso, parentField, decoField] = line.split(LOG_SEP)\n if (hash === undefined || hash === '') continue\n const parents = (parentField ?? '')\n .split(' ')\n .filter((p) => p !== '')\n commits.push({\n hash,\n shortHash: shortHash ?? '',\n subject: subject ?? '',\n author: author ?? '',\n dateIso: dateIso ?? '',\n parents,\n refs: parseDecorations(decoField ?? '', remotes),\n })\n }\n return commits\n}\n\n/**\n * \u89E3\u6790 `%D` \u88C5\u9970\u4E32\uFF0C\u5F62\u5982 `HEAD -> main, origin/main, tag: v1.0`\uFF1B\u7A7A\u4E32\u65E0 refs\u3002\n * \u5206\u7C7B\u89C4\u5219\uFF1A`HEAD -> x` \u4E3A\u5F53\u524D\u5206\u652F\uFF1B`tag: t` \u4E3A\u6807\u7B7E\uFF1B\n * \u5E26\u8FDC\u7A0B\u524D\u7F00\uFF08`<remote>/\u2026`\uFF09\u4E3A\u8FDC\u7A0B\u5206\u652F\uFF1B\u5176\u4F59\u4E3A\u672C\u5730\u5206\u652F\u3002\n */\nexport function parseDecorations(decorations: string, remotes: readonly string[]): readonly GitRef[] {\n const trimmed = decorations.trim()\n if (trimmed === '') return []\n const refs: GitRef[] = []\n for (const token of trimmed.split(', ')) {\n if (token.startsWith('HEAD -> ')) {\n refs.push({ kind: 'branch', name: token.slice(8), head: true })\n } else if (token.startsWith('tag: ')) {\n refs.push({ kind: 'tag', name: token.slice(5), head: false })\n } else if (remotes.some((remote) => token === remote || token.startsWith(`${remote}/`))) {\n refs.push({ kind: 'remote', name: token, head: false })\n } else {\n refs.push({ kind: 'branch', name: token, head: false })\n }\n }\n return refs\n}\n\n/**\n * \u89E3\u6790 `git show -s --format=%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%b` \u8F93\u51FA\uFF1A\n * \u524D\u4E94\u4E2A\u5B57\u6BB5\u4E3A\u673A\u5668\u53EF\u8BFB\u5143\u6570\u636E\uFF0C\u7B2C\u516D\u5B57\u6BB5\u8D77\u4E3A %b \u6B63\u6587\n * \uFF08%b \u5DF2\u6392\u9664 subject \u9996\u6BB5\u843D\uFF0C\u5929\u7136\u65E0\u91CD\u590D\u5C55\u793A\u95EE\u9898\uFF09\u3002\n */\nexport function parseShowMeta(output: string): { readonly commit: GitCommit; readonly body: string } | null {\n const trimmed = output.trimEnd()\n if (trimmed === '') return null\n const [hash, shortHash, subject, author, dateIso, ...bodyParts] = trimmed.split(LOG_SEP)\n if (hash === undefined || hash === '') return null\n return {\n commit: {\n hash,\n shortHash: shortHash ?? '',\n subject: subject ?? '',\n author: author ?? '',\n dateIso: dateIso ?? '',\n },\n body: bodyParts.join(LOG_SEP).trimEnd(),\n }\n}\n\n/** `--name-status` \u72B6\u6001\u7801 \u2192 \u53D8\u66F4\u72B6\u6001\u6620\u5C04\u3002 */\nfunction nameStatusCode(code: string): GitChangeStatus {\n switch (code) {\n case 'A': return 'added'\n case 'D': return 'deleted'\n case 'R': return 'renamed'\n case 'T': return 'typechange'\n case 'U': return 'conflicted'\n default: return 'modified'\n }\n}\n\n/**\n * \u89E3\u6790 `git show --format= --name-status -z` \u8F93\u51FA\uFF1ANUL \u5206\u9694\uFF0C\n * `X\\0path\\0`\uFF0Crename/copy \u4E3A `R100\\0old\\0new\\0`\uFF08\u53D6\u65B0\u8DEF\u5F84\uFF09\u3002\n * -z \u539F\u59CB\u8F93\u51FA\u4E0D\u5F15\u53F7\u5316\uFF0C\u975E ASCII \u8DEF\u5F84\u5929\u7136\u514D\u75AB\u4E71\u7801\uFF08\u65E7 --stat \u516B\u8FDB\u5236\u8F6C\u4E49\u95EE\u9898\u7684\u6839\u56E0\u6D88\u9664\uFF09\u3002\n */\nexport function parseNameStatusOutput(output: string): readonly { readonly path: string; readonly status: GitChangeStatus }[] {\n const raw = output.split(NUL)\n const segments = raw[raw.length - 1] === '' ? raw.slice(0, -1) : raw\n const rows: { path: string; status: GitChangeStatus }[] = []\n for (let i = 0; i < segments.length; i += 1) {\n const entry = segments[i] ?? ''\n if (entry === '') continue\n const code = entry[0] ?? ' '\n if (code === 'R' || code === 'C') {\n // rename/copy\uFF1Aold \u5728 i+1\u3001new \u5728 i+2\uFF0C\u5C55\u793A\u53D6\u65B0\u8DEF\u5F84\u3002\n rows.push({ path: segments[i + 2] ?? '', status: nameStatusCode(code) })\n i += 2\n } else {\n rows.push({ path: segments[i + 1] ?? '', status: nameStatusCode(code) })\n i += 1\n }\n }\n return rows\n}\n\n/**\n * Parse `git branch --show-current` output: the branch name, or null when\n * empty (detached HEAD).\n */\nexport function parseBranchOutput(output: string): string | null {\n const trimmed = output.trim()\n return trimmed === '' ? null : trimmed\n}\n\n", "/**\n * Framework-free snapshot orchestration: session cwd resolution + git command\n * sequence + frozen GitSnapshot assembly. Every dependency is injected\n * structurally, so the whole flow is testable without a cordis runtime; the\n * cordis shell (GitStatusService) only adapts host services into these faces.\n */\nimport { parseBranchOutput, parseLogOutput, parseStatusOutput } from './parser.ts'\nimport type { GitRunner } from './git.ts'\nimport type { GitSnapshot, GitSnapshotResult } from './types.ts'\n\n/** Resolved plugin config (already normalized; see normalizeConfig). */\nexport interface GitStatusConfig {\n readonly timeoutMs: number\n readonly maxStatusBytes: number\n readonly maxChanges: number\n readonly defaultRefreshIntervalMs: number\n}\n\n/** Session identity lookup: live first, persisted fallback. */\nexport interface SessionLookup {\n /** Live session cwd; undefined when the session is cold or absent in memory. */\n liveCwd(sessionId: string): string | undefined\n /**\n * Persisted session metadata; resolves to undefined when no persisted\n * session exists, and to `{ cwd }` (cwd possibly undefined) otherwise.\n */\n persistedMeta(sessionId: string): Promise<{ readonly cwd?: string } | undefined>\n}\n\n/** Filesystem primitives (node:fs/promises slices). */\nexport interface FsLike {\n realpath(path: string): Promise<string>\n stat(path: string): Promise<{ isDirectory(): boolean }>\n}\n\n/** Everything the snapshot flow needs beyond the session lookup. */\nexport interface SnapshotDeps {\n readonly run: GitRunner\n readonly fs: FsLike\n readonly sessions: SessionLookup\n /** Injectable clock for deterministic tests. */\n readonly now?: () => number\n /** Caller-side cancellation (Remote `signal` slot): aborts in-flight git runs. */\n readonly signal?: AbortSignal\n}\n\n/** Defaults applied by normalizeConfig when a value is absent or invalid. */\nexport const DEFAULT_CONFIG: GitStatusConfig = {\n timeoutMs: 5000,\n maxStatusBytes: 4 * 1024 * 1024,\n maxChanges: 100,\n defaultRefreshIntervalMs: 30_000,\n}\n\n/** Coerce a raw patch config value into a validated GitStatusConfig. */\nexport function normalizeConfig(raw: unknown): GitStatusConfig {\n const value = (raw ?? {}) as Record<string, unknown>\n const numberOr = (key: string, fallback: number): number => {\n const candidate = value[key]\n return typeof candidate === 'number' && Number.isFinite(candidate) && candidate >= 0\n ? candidate\n : fallback\n }\n return {\n timeoutMs: numberOr('timeoutMs', DEFAULT_CONFIG.timeoutMs) || DEFAULT_CONFIG.timeoutMs,\n maxStatusBytes: numberOr('maxStatusBytes', DEFAULT_CONFIG.maxStatusBytes) || DEFAULT_CONFIG.maxStatusBytes,\n maxChanges: Math.floor(numberOr('maxChanges', DEFAULT_CONFIG.maxChanges) || DEFAULT_CONFIG.maxChanges),\n defaultRefreshIntervalMs: numberOr('defaultRefreshIntervalMs', DEFAULT_CONFIG.defaultRefreshIntervalMs),\n }\n}\n\n/** Outcome of the cwd resolution step. */\ntype CwdResolution =\n | { readonly ok: true; readonly cwd: string }\n | { readonly ok: false; readonly error: Extract<GitSnapshotResult, { ok: false }>['error'] }\n\nasync function resolveCwd(sessions: SessionLookup, sessionId: string): Promise<CwdResolution> {\n const live = sessions.liveCwd(sessionId)\n if (live !== undefined) return { ok: true, cwd: live }\n const persisted = await sessions.persistedMeta(sessionId)\n if (persisted === undefined) return { ok: false, error: { code: 'session-not-found', sessionId } }\n if (persisted.cwd === undefined) return { ok: false, error: { code: 'cwd-unavailable', sessionId } }\n return { ok: true, cwd: persisted.cwd }\n}\n\n/** Classify a failed run outcome into a snapshot failure. */\nfunction runFailure(result: { readonly timedOut: boolean }, detail: string): Extract<GitSnapshotResult, { ok: false }>['error'] {\n return result.timedOut ? { code: 'timeout' } : { code: 'git-unavailable', detail }\n}\n\n/** Run one command, mapping a spawn-level failure to a snapshot failure. */\nexport async function runCommand(\n runner: GitRunner,\n argv: readonly string[],\n cwd: string,\n label: string,\n signal?: AbortSignal,\n): Promise<{ readonly run: Awaited<ReturnType<GitRunner['run']>> } | { readonly failure: Extract<GitSnapshotResult, { ok: false }>['error'] }> {\n try {\n return { run: await runner.run(argv, { cwd, ...(signal === undefined ? {} : { signal }) }) }\n } catch (error) {\n return { failure: { code: 'git-unavailable', detail: `${label}: ${error instanceof Error ? error.message : String(error)}` } }\n }\n}\n\n/**\n * Resolve a session's repository workspace: cwd (live or persisted), the\n * realpath'd directory, and the git work-tree root via `rev-parse\n * --show-toplevel`. Shared by the snapshot flow and the operation runner.\n */\nexport type WorkspaceResolution =\n | { readonly ok: true; readonly cwd: string; readonly root: string }\n | { readonly ok: false; readonly error: Extract<GitSnapshotResult, { ok: false }>['error'] }\n\nexport async function resolveWorkspace(\n deps: SnapshotDeps,\n sessionId: string,\n): Promise<WorkspaceResolution> {\n const resolved = await resolveCwd(deps.sessions, sessionId)\n if (!resolved.ok) return { ok: false, error: resolved.error }\n\n let realCwd: string\n try {\n realCwd = await deps.fs.realpath(resolved.cwd)\n const stat = await deps.fs.stat(realCwd)\n if (!stat.isDirectory()) {\n return { ok: false, error: { code: 'path-not-found', path: realCwd } }\n }\n } catch {\n return { ok: false, error: { code: 'path-not-found', path: resolved.cwd } }\n }\n\n const toplevel = await runCommand(deps.run, ['git', 'rev-parse', '--show-toplevel'], realCwd, 'rev-parse', deps.signal)\n if ('failure' in toplevel) return { ok: false, error: toplevel.failure }\n if (toplevel.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (toplevel.run.exitCode !== 0) {\n // exit 128 covers both \"not a git repository\" (plain directory) and\n // other git failures (dubious ownership, unreadable work tree, \u2026).\n // Only the former is a stable non-repo state; everything else surfaces\n // as git-unavailable with the actual reason instead of a misleading\n // \"no git repository\" pill.\n const stderr = toplevel.run.stderr\n if (!stderr.includes('not a git repository')) {\n return { ok: false, error: runFailure(toplevel.run, `git rev-parse failed: ${stderr.trim() || `exit ${String(toplevel.run.exitCode)}`}`) }\n }\n return { ok: false, error: { code: 'not-a-git-repo' } }\n }\n const root = toplevel.run.stdout.trim()\n if (root === '') return { ok: false, error: { code: 'not-a-git-repo' } }\n return { ok: true, cwd: realCwd, root }\n}\n\n/**\n * Build one frozen GitSnapshot for a session working directory.\n * Command sequence (all read-only; every command after the first runs with\n * the repository root as cwd):\n * 1. `git rev-parse --show-toplevel` \u2014 repo detection (exit 128 \u2192 not-a-git-repo)\n * 2. `git branch --show-current` \u2014 null when detached\n * 3. `git rev-parse --short HEAD` \u2014 null + unborn when the repo has no commits\n * 4. `git status --porcelain=v1 -z --branch --untracked-files=all`\n * 5. `git log -n 5 --format=%H%x1f%h%x1f%s%x1f%an%x1f%aI`\n *\n * --untracked-files=all\uFF1Agit \u9ED8\u8BA4 normal \u6A21\u5F0F\u4F1A\u628A\u6574\u76EE\u5F55\u672A\u8DDF\u8E2A\u6298\u53E0\u4E3A\u5355\u6761\n * `?? dir/`\uFF08\u5C3E\u659C\u6760\uFF09\u4E14\u4E0D\u679A\u4E3E\u5176\u5185\u90E8\u6587\u4EF6\u2014\u2014\u9690\u85CF\u76EE\u5F55\uFF08.agent/.tianqi \u7B49\uFF09\u7684\n * \u53D8\u66F4\u56E0\u6B64\u4ECE\u4E0D\u8FDB\u5165\u53D8\u66F4\u6E05\u5355\u3002`all` \u5F3A\u5236\u9010\u6587\u4EF6\u679A\u4E3E\uFF08\u4E0E IDEA / VSCode \u4E00\u81F4\uFF09\uFF0C\n * \u5185\u90E8\u6587\u4EF6\u5F97\u4EE5\u5C55\u793A\uFF1BmaxChanges \u622A\u65AD\u5217\u8868\u3001maxStatusBytes spill \u4FDD\u8BA1\u6570\u7CBE\u786E\uFF0C\n * \u8D85\u5927\u672A\u8DDF\u8E2A\u6811\uFF08\u5982\u672A gitignore \u7684\u6784\u5EFA\u4EA7\u7269\uFF09\u7ECF\u6B64\u8DEF\u5F84\u4F18\u96C5\u964D\u7EA7\u3002\n */\nexport async function snapshotForSession(\n deps: SnapshotDeps,\n config: GitStatusConfig,\n sessionId: string,\n): Promise<GitSnapshotResult> {\n const workspace = await resolveWorkspace(deps, sessionId)\n if (!workspace.ok) return { ok: false, error: workspace.error }\n const root = workspace.root\n\n const branchRun = await runCommand(deps.run, ['git', 'branch', '--show-current'], root, 'branch', deps.signal)\n if ('failure' in branchRun) return { ok: false, error: branchRun.failure }\n if (branchRun.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n const branch = branchRun.run.exitCode === 0 ? parseBranchOutput(branchRun.run.stdout) : null\n\n const headRun = await runCommand(deps.run, ['git', 'rev-parse', '--short', 'HEAD'], root, 'rev-parse HEAD', deps.signal)\n if ('failure' in headRun) return { ok: false, error: headRun.failure }\n if (headRun.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n // A failed HEAD read (non-timeout) only nulls the hash: the authoritative\n // unborn flag comes from the status header below (`## No commits yet on\n // main`), so a corrupt repo is never misreported as \"no commits\".\n const head = headRun.run.exitCode === 0 ? (headRun.run.stdout.trim() || null) : null\n\n // --untracked-files=all\uFF1A\u5F3A\u5236\u679A\u4E3E\u672A\u8DDF\u8E2A\u76EE\u5F55\u5185\u90E8\u6587\u4EF6\uFF08\u6839\u56E0\u4FEE\u590D\u2014\u2014\u89C1\u6A21\u5757\u6CE8\u91CA\uFF09\u3002\n const status = await runCommand(deps.run, ['git', 'status', '--porcelain=v1', '-z', '--branch', '--untracked-files=all'], root, 'status', deps.signal)\n if ('failure' in status) return { ok: false, error: status.failure }\n if (status.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (status.run.exitCode !== 0) {\n return { ok: false, error: runFailure(status.run, `git status exited ${String(status.run.exitCode)}`) }\n }\n const parsed = parseStatusOutput(status.run.stdout, config.maxChanges)\n\n const log = await runCommand(deps.run, ['git', 'log', '-n', '5', '--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI'], root, 'log', deps.signal)\n if ('failure' in log) return { ok: false, error: log.failure }\n if (log.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n const recentCommits = log.run.exitCode === 0 ? parseLogOutput(log.run.stdout) : []\n\n const checkedAt = deps.now?.() ?? Date.now()\n const snapshot: GitSnapshot = {\n root,\n branch,\n head,\n unborn: parsed.unborn,\n dirty: parsed.staged + parsed.modified + parsed.untracked > 0,\n staged: parsed.staged,\n modified: parsed.modified,\n untracked: parsed.untracked,\n ahead: parsed.ahead,\n behind: parsed.behind,\n lastCommit: recentCommits[0] ?? null,\n recentCommits,\n changes: parsed.changes,\n truncated: parsed.truncated || ('run' in status && status.run.stdoutLossy),\n refreshIntervalMs: config.defaultRefreshIntervalMs,\n checkedAt,\n }\n return { ok: true, value: snapshot }\n}\n", "/**\n * Framework-free git management operation runner.\n *\n * Same layering as `core.ts`: every dependency is injected structurally, the\n * whole flow is testable against real temporary git repositories without a\n * cordis runtime, and `GitStatusService` only adapts host services into the\n * `SnapshotDeps` face.\n *\n * Security model: the browser only ever sends a `sessionId` plus\n * repository-relative paths (as listed in a snapshot's `changes`). Paths are\n * validated against the work-tree root (absolute paths and `..` escapes are\n * rejected) and every git invocation uses `--` so a path can never be\n * interpreted as an option. Commands run through the same subprocess adapter\n * as the read-only snapshot flow \u2014 no shell is involved.\n */\nimport { resolve, sep } from 'node:path'\nimport { resolveWorkspace, runCommand, snapshotForSession, type GitStatusConfig, type SnapshotDeps } from './core.ts'\nimport type { GitAction, GitActionResult, GitActionRequest, GitOperationErrorCode } from './types.ts'\n\n/** Build the command sequence for one action, validating every path against the root. */\nfunction buildArgv(action: GitAction, root: string): { readonly argv: readonly (readonly string[])[] } | { readonly error: string } {\n switch (action.kind) {\n case 'stage':\n return withPaths([['git', 'add', '--']], action.paths, root)\n case 'stage-all':\n return { argv: [['git', 'add', '-A']] }\n case 'unstage':\n return withPaths([['git', 'restore', '--staged', '--']], action.paths, root)\n case 'unstage-all':\n return { argv: [['git', 'restore', '--staged', '--', '.']] }\n case 'discard':\n return withPaths([['git', 'restore', '--']], action.paths, root)\n case 'discard-all':\n // Reset the index to HEAD first, then the work tree to the index \u2014 the\n // IDE-style \"roll back everything tracked\" semantics.\n return { argv: [['git', 'restore', '--staged', '--', '.'], ['git', 'restore', '--', '.']] }\n case 'commit': {\n // Message emptiness is validated by runAction (git-error), not here.\n const message = action.message.trim()\n if (action.paths === undefined || action.paths.length === 0) {\n return { argv: [['git', 'commit', '-m', message]] }\n }\n // \u4E24\u6B65\u5E8F\u5217\uFF08IDE \u5F0F\u300C\u63D0\u4EA4\u6240\u9009\u6587\u4EF6\u300D\u8BED\u4E49\uFF0C\u542B\u672A\u8DDF\u8E2A\u6587\u4EF6\uFF09\uFF1A\n // 1. `git add -- <paths>` \u5148\u628A\u6240\u9009\u8DEF\u5F84\u7EB3\u5165\u7D22\u5F15\u2014\u2014\u88F8\u7684\n // `git commit -- <\u672A\u8DDF\u8E2A\u8DEF\u5F84>` \u4F1A\u62A5 pathspec \u9519\u8BEF\uFF0C\u5148\u884C\u6682\u5B58\u4F7F\u5176\u53EF\u5339\u914D\uFF1B\n // 2. `git commit -m <msg> -- <paths>` \u6309\u8DEF\u5F84\u9650\u5B9A\u63D0\u4EA4\u8FD9\u4E9B\u8DEF\u5F84\u7684\u5DE5\u4F5C\u533A\u5185\u5BB9\uFF0C\n // \u5176\u4F59\u5DF2\u6682\u5B58\u6587\u4EF6\u4E0D\u53D7\u5F71\u54CD\u3002\u5BF9\u5DF2\u8DDF\u8E2A\u8DEF\u5F84\u4E0E\u5355\u547D\u4EE4\u5B8C\u5168\u7B49\u4EF7\uFF08\u5DF2\u5B9E\u6D4B\u9A8C\u8BC1\uFF09\u3002\n return withPaths([['git', 'add', '--'], ['git', 'commit', '-m', message, '--']], action.paths, root)\n }\n case 'branch-create': {\n // Name validity is validated by runAction (invalid-name), not here.\n const from = action.from === undefined || action.from === '' ? [] : [action.from]\n return { argv: [['git', 'branch', action.name, ...from]] }\n }\n case 'branch-checkout':\n return { argv: [['git', 'checkout', action.name]] }\n case 'branch-delete':\n return { argv: [['git', 'branch', action.force === true ? '-D' : '-d', action.name]] }\n case 'fetch':\n // fetch --all --prune\uFF1A\u62C9\u53D6\u6240\u6709\u8FDC\u7A0B\u5F15\u7528\u66F4\u65B0 + \u6E05\u7406\u5DF2\u5220\u9664\u7684\u8FDC\u7A0B\u8DDF\u8E2A\u5206\u652F\u3002\n return { argv: [['git', 'fetch', '--all', '--prune']] }\n }\n}\n\n/**\n * A branch name is valid when it matches git's ref-name grammar at the level\n * we care about: non-empty, ASCII ref chars only, no leading `-` (option\n * injection guard, though argv never shells out), no `..` (path traversal of\n * refs), no trailing `/`, and no double slashes.\n */\nexport function isValidBranchName(name: string): boolean {\n if (name === '' || name.startsWith('-') || name.includes('..') || name.endsWith('/') || name.includes('//')) return false\n return /^[A-Za-z0-9._/-]+$/.test(name)\n}\n\n/**\n * \u6821\u9A8C\u4ED3\u5E93\u76F8\u5BF9\u8DEF\u5F84\u540E\u8FFD\u52A0\u5230 `--` \u4E4B\u540E\uFF1B`prefixes` \u53EF\u7ED9\u51FA\u591A\u6761\u547D\u4EE4\u5E8F\u5217\uFF0C\n * \u6821\u9A8C\u540E\u7684\u8DEF\u5F84\u9010\u4E00\u9644\u52A0\u5230\u6BCF\u6761\u5E8F\u5217\uFF08commit \u6240\u9009\u8DEF\u5F84\u5373\u4E24\u6B65\u5E8F\u5217\uFF09\u3002\n */\nfunction withPaths(prefixes: readonly (readonly string[])[], paths: readonly string[], root: string): { readonly argv: readonly (readonly string[])[] } | { readonly error: string } {\n if (paths.length === 0) return { error: 'no paths given' }\n for (const path of paths) {\n if (!isSafePath(path, root)) return { error: `unsafe path: ${path}` }\n }\n return { argv: prefixes.map((prefix) => [...prefix, ...paths]) }\n}\n\n/**\n * A path is safe when it is repo-relative and stays inside the work tree:\n * reject absolute paths, drive letters / backslashes, and `..` escapes\n * (checked via path resolution against the realpath'd root).\n */\nexport function isSafePath(path: string, root: string): boolean {\n if (path === '') return false\n if (path.startsWith('/') || path.startsWith('\\\\') || /^[A-Za-z]:/.test(path)) return false\n const resolved = resolve(root, path)\n const prefix = root.endsWith(sep) ? root : `${root}${sep}`\n return resolved === root || resolved.startsWith(prefix)\n}\n\n/** Map a snapshot-flow failure (which may carry git-unavailable) onto the operation error shape. */\nexport function operationError(failure: Extract<Awaited<ReturnType<typeof resolveWorkspace>>, { ok: false }>['error']): GitActionResult & { ok: false } {\n if (failure.code === 'git-unavailable') {\n return { ok: false, error: { code: 'git-error', message: failure.detail } }\n }\n return { ok: false, error: failure }\n}\n\n/**\n * \u628A git \u547D\u4EE4\u5931\u8D25\u5F52\u7C7B\u4E3A\u53EF\u9884\u671F\u7684\u4E1A\u52A1\u9519\u8BEF\uFF08\u5176\u4F59\u4FDD\u6301 git-error\uFF09\u3002\n * \u5207\u5206\u652F\u88AB\u5DE5\u4F5C\u533A\u672A\u63D0\u4EA4\u53D8\u66F4\u963B\u6B62\u662F\u6700\u5E38\u89C1\u7684\u53EF\u9884\u671F\u5931\u8D25\uFF1Agit \u8F93\u51FA\n * \"would be overwritten by checkout\"\uFF08\u6216\u4E2D\u6587\u672C\u5730\u5316 \"\u5C06\u88AB checkout \u8986\u76D6\"\uFF09\uFF0C\n * \u5F52\u4E00\u5316\u4E3A local-changes-block\uFF0Cclient \u636E\u6B64\u7ED9\u53CB\u597D\u63D0\u793A + \u5904\u7406\u53D8\u66F4\u5F15\u5BFC\u3002\n */\nexport function classifyOperationError(kind: GitAction['kind'], message: string): GitOperationErrorCode {\n if (kind === 'branch-checkout' && /would be overwritten by checkout|\u5C06\u88AB checkout \u8986\u76D6|\u6709\u672A\u8DDF\u8E2A\u5DE5\u4F5C\u533A\u6587\u4EF6\u5C06\u4F1A\u88AB checkout \u8986\u76D6/i.test(message)) {\n return 'local-changes-block'\n }\n return 'git-error'\n}\n\n/**\n * Execute one management action against the session's repository and return\n * the refreshed snapshot on success (the caller re-renders from it, so the\n * UI never waits for the next poll).\n */\nexport async function runAction(\n deps: SnapshotDeps,\n config: GitStatusConfig,\n request: GitActionRequest,\n): Promise<GitActionResult> {\n const workspace = await resolveWorkspace(deps, request.sessionId)\n if (!workspace.ok) return operationError(workspace.error)\n const root = workspace.root\n\n if (request.action.kind === 'commit' && request.action.message.trim() === '') {\n return { ok: false, error: { code: 'git-error', message: 'commit message is empty' } }\n }\n\n const kind = request.action.kind\n if (kind === 'branch-create' || kind === 'branch-checkout' || kind === 'branch-delete') {\n const name = request.action.name\n if (!isValidBranchName(name)) {\n return { ok: false, error: { code: 'invalid-name', message: `invalid branch name: ${name}` } }\n }\n }\n\n const built = buildArgv(request.action, root)\n if ('error' in built) return { ok: false, error: { code: 'invalid-path', message: built.error } }\n\n // Run the command sequence; a failure stops the rest. \u5148\u884C\u547D\u4EE4\u53EF\u80FD\u5DF2\u751F\u6548\uFF1A\n // restore \u7C7B\u547D\u4EE4\u5E42\u7B49\u53EF\u91CD\u5165\uFF1B\u4E24\u6B65\u63D0\u4EA4\u82E5 add \u6210\u529F\u540E commit \u5931\u8D25\uFF0C\u6240\u9009\u8DEF\u5F84\n // \u7559\u5728\u6682\u5B58\u533A\uFF08IDE \u884C\u4E3A\u76F8\u540C\uFF0C\u4E0B\u6B21\u91CD\u8BD5\u5373\u53EF\u6210\u529F\uFF09\u3002\n let lastStdout = ''\n for (const argv of built.argv) {\n const outcome = await runCommand(deps.run, argv, root, `action ${request.action.kind}`, deps.signal)\n if ('failure' in outcome) return operationError(outcome.failure)\n if (outcome.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (outcome.run.exitCode !== 0) {\n // git writes user-facing failures to stderr OR stdout (e.g. a clean\n // repo's `git commit` reports \"nothing to commit\" on stdout).\n const message = outcome.run.stderr.trim() || outcome.run.stdout.trim()\n const code = classifyOperationError(request.action.kind, message)\n return {\n ok: false,\n error: {\n code,\n message: message !== '' ? message : `git ${request.action.kind} exited ${String(outcome.run.exitCode)}`,\n },\n }\n }\n lastStdout = outcome.run.stdout.trim()\n }\n\n const snapshot = await snapshotForSession(deps, config, request.sessionId)\n if (!snapshot.ok) return operationError(snapshot.error)\n return { ok: true, snapshot: snapshot.value, ...(lastStdout === '' ? {} : { output: lastStdout }) }\n}\n", "/**\n * Framework-free read-only query runner (history / diff / show / branches).\n *\n * Same layering as `core.ts`/`actions.ts`: structural injection, testable\n * against real temporary repositories without a cordis runtime. Every query\n * resolves the workspace once, then runs one or two read-only git commands\n * against the repository root.\n */\nimport { resolveWorkspace, runCommand, type GitStatusConfig, type SnapshotDeps } from './core.ts'\nimport { parseBranchOutput, parseGraphLogOutput, parseNameStatusOutput, parseShowMeta } from './parser.ts'\nimport { isSafePath, operationError } from './actions.ts'\nimport type { GitBranch, GitQueryRequest, GitQueryResponse } from './types.ts'\n\n/** Machine-readable log format for show queries (no parents). */\nconst LOG_FORMAT = '%H%x1f%h%x1f%s%x1f%an%x1f%aI'\n/** \u5E26\u56FE\u7684 log \u683C\u5F0F\uFF08%P = \u7236\u63D0\u4EA4\uFF0C%D = ref \u88C5\u9970\uFF09\u3002 */\nconst GRAPH_FORMAT = '%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%P%x1f%D'\n\n/** History page size cap (and default). \u5343\u6761\u7EA7 + \u65E0\u9650\u6EDA\u52A8\u3002 */\nconst MAX_HISTORY_LIMIT = 1000\n\n/** A ref is acceptable when non-empty and free of whitespace. */\nfunction isValidRef(ref: string): boolean {\n return ref !== '' && !/\\s/.test(ref)\n}\n\n/**\n * \u6267\u884C\u4E00\u6761\u53EA\u8BFB\u67E5\u8BE2\u3002\u7ED3\u679C\u5747\u4E3A JSON \u7EAF\u6570\u636E\u4E14\u6709\u754C\n * \uFF08history \u5206\u9875\uFF1Bdiff \u6587\u672C\u53D7 runner \u7684 spill/\u622A\u65AD\u7EA6\u675F\uFF09\u3002\n * `config` \u5F53\u524D\u672A\u7528\uFF1A\u4FDD\u7559\u4EE5\u4E0E runAction \u5171\u4EAB runner \u7B7E\u540D\u5951\u7EA6\n * \uFF08deps, config, request\uFF09\uFF0C\u540E\u7EED\u67E5\u8BE2\u9650\u6D41\u7B49\u8C03\u4F18\u53EF\u76F4\u63A5\u542F\u7528\u3002\n */\nexport async function runQuery(\n deps: SnapshotDeps,\n config: GitStatusConfig,\n request: GitQueryRequest,\n): Promise<GitQueryResponse> {\n void config\n const workspace = await resolveWorkspace(deps, request.sessionId)\n if (!workspace.ok) return { ok: false, error: operationError(workspace.error).error }\n const root = workspace.root\n const query = request.query\n\n switch (query.kind) {\n case 'history':\n return historyQuery(deps, root, query)\n case 'diff':\n return diffQuery(deps, root, query.path, query.base)\n case 'show':\n return showQuery(deps, root, query.ref)\n case 'branches':\n return branchesQuery(deps, root)\n case 'tags':\n return tagsQuery(deps, root)\n case 'authors':\n return authorsQuery(deps, root)\n }\n}\n\nasync function historyQuery(\n deps: SnapshotDeps,\n root: string,\n query: Extract<GitQueryRequest['query'], { kind: 'history' }>,\n): Promise<GitQueryResponse> {\n const safeLimit = Math.min(Math.max(Math.floor(query.limit), 0), MAX_HISTORY_LIMIT)\n const safeSkip = Math.max(Math.floor(query.skip), 0)\n if (query.ref !== undefined && !isValidRef(query.ref)) {\n return { ok: false, error: { code: 'invalid-name', message: `invalid ref: ${query.ref}` } }\n }\n const search = query.search?.trim() ?? ''\n const hexLike = /^[0-9a-f]{7,40}$/i.test(search)\n // \u54C8\u5E0C\u7CBE\u51C6\u68C0\u7D22\uFF1A\u4EC5\u5B9A\u4F4D\u76EE\u6807\u63D0\u4EA4\u81EA\u8EAB\uFF08--no-walk \u4E0D\u904D\u5386\u7956\u5148\uFF09\u2192 \u5355\u6761\u76EE\uFF0C\n // \u4E0D\u518D\u5217\u51FA\u8BE5\u63D0\u4EA4\u7684\u5168\u90E8\u7956\u5148\uFF1B\u6587\u672C\u641C\u7D22\u8D70 --grep\uFF08-i -E\uFF0C\u8DE8\u5F15\u7528\u5339\u914D\uFF09\u3002\n const scope = hexLike ? [] : query.ref === undefined ? ['--all'] : [query.ref]\n const noWalk = hexLike ? ['--no-walk', search] : []\n const filters: string[] = []\n if (search !== '' && !hexLike) filters.push('--regexp-ignore-case', '--extended-regexp', `--grep=${search}`)\n const author = query.author?.trim() ?? ''\n if (author !== '') filters.push(`--author=${author}`)\n const since = query.since?.trim() ?? ''\n if (since !== '') filters.push(`--since=${since}`)\n\n const log = await runCommand(\n deps.run,\n // -n/--skip \u524D\u7F6E\uFF1Agit \u7684 `-n N` \u51FA\u73B0\u5728 `--no-walk` \u4E4B\u540E\u4F1A\u91CD\u7F6E no-walk\n // \uFF08hexLike \u4F1A\u9519\u8BEF\u5217\u51FA\u5168\u90E8\u7956\u5148\uFF09\uFF0C\u524D\u7F6E\u5219 `-n 1000 --no-walk x` \u6052\u8FD4\u56DE\u5355\u6761\u3002\n ['git', 'log', ...filters, `--skip=${String(safeSkip)}`, '-n', String(safeLimit), ...noWalk, ...scope, `--format=${GRAPH_FORMAT}`],\n root,\n 'log',\n deps.signal,\n )\n if ('failure' in log) return { ok: false, error: operationError(log.failure).error }\n if (log.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (log.run.exitCode !== 0) {\n // \u672A\u51FA\u751F\u4ED3\u5E93\u65E0\u63D0\u4EA4\uFF1Agit log \u4EE5 128 \u6B64\u4FE1\u606F\u9000\u51FA\u2014\u2014\u7A33\u5B9A\u7A7A\u5386\u53F2\uFF0C\u975E\u9519\u8BEF\u3002\n if (log.run.stderr.includes('does not have any commits')) {\n return { ok: true, value: { kind: 'history', commits: [], total: 0 } }\n }\n // \u54C8\u5E0C\u65E0\u89E3\u6790\uFF08\u672A\u547D\u4E2D\uFF09\u6216\u524D\u7F00\u4E0D\u552F\u4E00\uFF08ambiguous\uFF09\uFF1A\u7A33\u5B9A\u7A7A\u7ED3\u679C\uFF08\u8BA9\u7528\u6237\u8F93\u5165\u66F4\u957F\u524D\u7F00\uFF09\u3002\n if (hexLike && /unknown revision|bad revision|ambiguous/i.test(log.run.stderr)) {\n return { ok: true, value: { kind: 'history', commits: [], total: 0 } }\n }\n return gitError('log', log.run.stderr, log.run.stdout)\n }\n\n // \u8FC7\u6EE4\u8303\u56F4\u5185\u7684\u63D0\u4EA4\u603B\u6570\uFF08best-effort\uFF09\u3002\n let total = 0\n const count = await runCommand(deps.run, ['git', 'rev-list', '--count', ...noWalk, ...scope, ...filters], root, 'rev-list', deps.signal)\n if ('run' in count && count.run.exitCode === 0) {\n const parsed = Number(count.run.stdout.trim())\n if (Number.isFinite(parsed) && parsed >= 0) total = parsed\n }\n\n // \u8FDC\u7A0B\u540D\u7528\u4E8E %D \u88C5\u9970\u7684\u8FDC\u7A0B\u5206\u652F\u5206\u7C7B\uFF1B\u5931\u8D25\u65F6\u964D\u7EA7\u4E3A\u7A7A\u5217\u8868\uFF08\u5176\u4F59\u6309\u672C\u5730\u5206\u652F\u5904\u7406\uFF09\u3002\n let remotes: readonly string[] = []\n const remoteRun = await runCommand(deps.run, ['git', 'remote'], root, 'remote', deps.signal)\n if ('run' in remoteRun && remoteRun.run.exitCode === 0) {\n remotes = remoteRun.run.stdout.split('\\n').map((s) => s.trim()).filter((s) => s !== '')\n }\n\n return { ok: true, value: { kind: 'history', commits: parseGraphLogOutput(log.run.stdout, remotes), total } }\n}\n\n/**\n * \u5355\u6587\u4EF6\u5DEE\u5F02\uFF08\u53D8\u66F4\u754C\u9762\u5BF9\u7167\u67E5\u770B\u7528\uFF09\u3002\n * staged = --cached\uFF1Bworktree = \u5DE5\u4F5C\u533A\u5BF9\u7D22\u5F15\uFF1B\n * \u672A\u7248\u672C\u7BA1\u7406\u6587\u4EF6 worktree \u5DEE\u5F02\u4E3A\u7A7A \u2192 \u56DE\u9000 --no-index \u4E0E /dev/null \u5BF9\u6BD4\uFF08\u9000\u51FA\u7801 1 \u89C6\u4E3A\u6709\u5DEE\u5F02\u7684\u6210\u529F\uFF09\u3002\n */\nasync function diffQuery(\n deps: SnapshotDeps,\n root: string,\n path: string,\n base: 'worktree' | 'staged',\n): Promise<GitQueryResponse> {\n if (!isSafePath(path, root)) return { ok: false, error: { code: 'invalid-path', message: `unsafe path: ${path}` } }\n // \u4F7F\u7528 -U999999 \u663E\u793A\u5B8C\u6574\u6587\u6863\u4E0A\u4E0B\u6587\uFF08\u800C\u975E\u4EC5\u53D8\u66F4 hunk\uFF09\uFF0C\u652F\u6301\u6587\u6863\u6D4F\u89C8\u4F53\u9A8C\u3002\n const argv = base === 'staged'\n ? ['git', 'diff', '--cached', '-U999999', '--', path]\n : ['git', 'diff', '-U999999', '--', path]\n const run = await runCommand(deps.run, argv, root, 'diff', deps.signal)\n if ('failure' in run) return { ok: false, error: operationError(run.failure).error }\n if (run.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (run.run.exitCode !== 0) return gitError('diff', run.run.stderr, run.run.stdout)\n if (run.run.stdout !== '' || base === 'staged') {\n return { ok: true, value: { kind: 'diff', path, text: run.run.stdout } }\n }\n // \u7A7A\u5DEE\u5F02\uFF1A\u53EF\u80FD\u662F\u672A\u7248\u672C\u7BA1\u7406\u6587\u4EF6\u2014\u2014\u4E0E /dev/null \u5BF9\u6BD4\u751F\u6210\u5168\u589E\u5DEE\u5F02\u3002\n const ni = await runCommand(deps.run, ['git', 'diff', '--no-index', '-U999999', '--', '/dev/null', path], root, 'diff --no-index', deps.signal)\n if ('failure' in ni) return { ok: false, error: operationError(ni.failure).error }\n if (ni.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (ni.run.exitCode !== 0 && ni.run.exitCode !== 1) return gitError('diff', ni.run.stderr, ni.run.stdout)\n return { ok: true, value: { kind: 'diff', path, text: ni.run.stdout } }\n}\n\nasync function showQuery(deps: SnapshotDeps, root: string, ref: string): Promise<GitQueryResponse> {\n if (!isValidRef(ref)) return { ok: false, error: { code: 'invalid-name', message: `invalid ref: ${ref}` } }\n // -s \u4EC5\u8F93\u51FA\u683C\u5F0F\u5757\uFF1A%b \u4E3A\u6392\u9664\u9996\u6BB5\u843D\u540E\u7684\u6B63\u6587\uFF0C\u72EC\u7ACB\u8C03\u7528\u907F\u514D\u89E3\u6790\u6B67\u4E49\u3002\n const meta = await runCommand(\n deps.run,\n ['git', 'show', '-s', `--format=${LOG_FORMAT}%x1f%b`, ref],\n root,\n 'show',\n deps.signal,\n )\n if ('failure' in meta) return { ok: false, error: operationError(meta.failure).error }\n if (meta.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (meta.run.exitCode !== 0) return gitError('show', meta.run.stderr, meta.run.stdout)\n\n const stat = await runCommand(\n deps.run,\n ['git', '-c', 'core.quotePath=false', 'show', '--format=', '--name-status', '-z', ref],\n root,\n 'show --name-status',\n deps.signal,\n )\n if ('failure' in stat) return { ok: false, error: operationError(stat.failure).error }\n if (stat.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (stat.run.exitCode !== 0) return gitError('show', stat.run.stderr, stat.run.stdout)\n\n const parsed = parseShowMeta(meta.run.stdout)\n return {\n ok: true,\n value: {\n kind: 'show',\n ref,\n commit: parsed?.commit ?? null,\n body: parsed?.body ?? '',\n stats: parseNameStatusOutput(stat.run.stdout),\n },\n }\n}\n\n/** \u4F5C\u8005\u5217\u8868\uFF08\u5DE5\u5177\u680F\u7528\u6237\u9009\u62E9\u7528\uFF09\uFF0C\u53BB\u91CD\u6392\u5E8F\u622A\u65AD 100\u3002 */\nasync function authorsQuery(deps: SnapshotDeps, root: string): Promise<GitQueryResponse> {\n const run = await runCommand(deps.run, ['git', 'log', '--all', '-n', '1000', '--format=%an'], root, 'log authors', deps.signal)\n if ('failure' in run) return { ok: false, error: operationError(run.failure).error }\n if (run.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (run.run.exitCode !== 0) return { ok: true, value: { kind: 'authors', authors: [] } }\n const authors = [...new Set(run.run.stdout.split('\\n').map((s) => s.trim()).filter((s) => s !== ''))].sort().slice(0, 100)\n return { ok: true, value: { kind: 'authors', authors } }\n}\n\n/** \u6807\u7B7E\u5217\u8868\uFF08\u5DE6\u680F\u8FC7\u6EE4\u6811\u7528\uFF09\uFF0C\u590D\u7528 tab \u5206\u9694\u89E3\u6790\u3002 */\nasync function tagsQuery(deps: SnapshotDeps, root: string): Promise<GitQueryResponse> {\n const FORMAT = '--format=%(refname:short)%09%(objectname:short)'\n const run = await runCommand(deps.run, ['git', 'tag', FORMAT], root, 'tag', deps.signal)\n if ('failure' in run) return { ok: false, error: operationError(run.failure).error }\n if (run.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (run.run.exitCode !== 0) return gitError('tag', run.run.stderr, run.run.stdout)\n return { ok: true, value: { kind: 'tags', tags: parseBranchList(run.run.stdout) } }\n}\n\nasync function branchesQuery(deps: SnapshotDeps, root: string): Promise<GitQueryResponse> {\n // \u672C\u5730\u5206\u652F\u683C\u5F0F\uFF1Aname\\thash\\tupstream\\ttrack\uFF08track \u5982 [ahead 2, behind 1]\uFF09\u3002\n // \u8FDC\u7A0B\u5206\u652F\u65E0\u4E0A\u6E38 \u2192 upstream/track \u4E3A\u7A7A\u3002\n const LOCAL_FORMAT = '--format=%(refname:short)%09%(objectname:short)%09%(upstream:short)%09%(upstream:track)'\n const REMOTE_FORMAT = '--format=%(refname:short)%09%(objectname:short)'\n const local = await runCommand(deps.run, ['git', 'branch', LOCAL_FORMAT], root, 'branch', deps.signal)\n if ('failure' in local) return { ok: false, error: operationError(local.failure).error }\n if (local.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (local.run.exitCode !== 0) return gitError('branch', local.run.stderr, local.run.stdout)\n\n const remote = await runCommand(deps.run, ['git', 'branch', '-r', REMOTE_FORMAT], root, 'branch -r', deps.signal)\n if ('failure' in remote) return { ok: false, error: operationError(remote.failure).error }\n if (remote.run.timedOut) return { ok: false, error: { code: 'timeout' } }\n if (remote.run.exitCode !== 0) return gitError('branch -r', remote.run.stderr, remote.run.stdout)\n\n const current = await runCommand(deps.run, ['git', 'branch', '--show-current'], root, 'branch', deps.signal)\n const currentName = 'run' in current && current.run.exitCode === 0 ? parseBranchOutput(current.run.stdout) : null\n\n // \u9ED8\u8BA4\u5206\u652F\uFF1Aorigin/HEAD \u7B26\u53F7\u5F15\u7528\uFF08\u5982 origin/main\uFF09\uFF1B\u5931\u8D25\u964D\u7EA7 null\u3002\n let defaultBranch: string | null = null\n const def = await runCommand(deps.run, ['git', 'symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], root, 'symbolic-ref', deps.signal)\n if ('run' in def && def.run.exitCode === 0) {\n const value = def.run.stdout.trim()\n const slash = value.indexOf('/')\n defaultBranch = value === '' ? null : slash === -1 ? value : value.slice(slash + 1)\n }\n\n return {\n ok: true,\n value: {\n kind: 'branches',\n current: currentName,\n defaultBranch,\n local: parseBranchList(local.run.stdout),\n remote: parseBranchList(remote.run.stdout).filter((branch) => !branch.name.endsWith('/HEAD')),\n },\n }\n}\n\n/**\n * \u89E3\u6790 `%(refname:short)%09%(objectname:short)[%09%(upstream:short)%09%(upstream:track)]` \u884C\u3002\n * \u672C\u5730\u5206\u652F\u542B 4 \u5B57\u6BB5\uFF08upstream + track\uFF09\uFF0C\u8FDC\u7A0B\u5206\u652F\u4EC5 2 \u5B57\u6BB5\uFF08\u65E0\u4E0A\u6E38\uFF09\u3002\n * track \u683C\u5F0F\uFF1A`[ahead N]`\u3001`[behind N]`\u3001`[ahead N, behind N]` \u6216\u7A7A\uFF08\u65E0\u4E0A\u6E38/\u5DF2\u540C\u6B65\uFF09\u3002\n */\nfunction parseBranchList(output: string): readonly GitBranch[] {\n const branches: GitBranch[] = []\n for (const line of output.split('\\n')) {\n if (line === '') continue\n const parts = line.split('\\t')\n const name = parts[0]\n const hash = parts[1]\n if (name === undefined || name === '') continue\n const track = parts[3] ?? ''\n const aheadMatch = /ahead (\\d+)/.exec(track)\n const behindMatch = /behind (\\d+)/.exec(track)\n const ahead = aheadMatch ? Number(aheadMatch[1]) : 0\n const behind = behindMatch ? Number(behindMatch[1]) : 0\n branches.push({\n name,\n shortHash: hash === undefined || hash === '' ? null : hash,\n ...(ahead > 0 ? { ahead } : {}),\n ...(behind > 0 ? { behind } : {}),\n })\n }\n return branches\n}\n\nfunction gitError(label: string, stderr: string, stdout: string): GitQueryResponse {\n const message = stderr.trim() || stdout.trim()\n return {\n ok: false,\n error: {\n code: 'git-error',\n message: message !== '' ? message : `git ${label} failed`,\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["stat", "stat"]
|
|
7
7
|
}
|
package/lib/host/parser.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* No side effects and no I/O — fully unit-testable against literal fixtures
|
|
4
4
|
* (verified against real `git status --porcelain=v1 -z --branch` output).
|
|
5
5
|
*/
|
|
6
|
-
import type { GitChange, GitCommit } from './types.ts';
|
|
6
|
+
import type { GitChange, GitChangeStatus, GitCommit, GitRef, GraphCommit } from './types.ts';
|
|
7
7
|
/** Parsed status counts plus the (possibly capped) change list. */
|
|
8
8
|
export interface ParsedStatus {
|
|
9
9
|
readonly branch: string | null;
|
|
@@ -41,6 +41,11 @@ export declare function parseStatusHeader(line: string): StatusHeader;
|
|
|
41
41
|
* -z format: every entry (header and each `XY path`) is NUL-terminated; a
|
|
42
42
|
* rename/copy entry emits `R <new>\0<old>\0` so the following item is the
|
|
43
43
|
* source path and must be consumed without becoming a change itself.
|
|
44
|
+
*
|
|
45
|
+
* 混合状态拆分(IDEA 式):X、Y 均非空的合法对(MM/AM/MD/RM…)拆为
|
|
46
|
+
* 「已暂存侧 + 未暂存侧」两条 GitChange——UI 据此把同一文件分别列入
|
|
47
|
+
* 「已暂存更改」与「更改」两组,两侧可独立操作、差异基线唯一。
|
|
48
|
+
* 真实冲突(isConflicted)保持单条 conflicted 条目。
|
|
44
49
|
*/
|
|
45
50
|
export declare function parseStatusOutput(output: string, maxChanges: number): ParsedStatus;
|
|
46
51
|
/**
|
|
@@ -49,6 +54,37 @@ export declare function parseStatusOutput(output: string, maxChanges: number): P
|
|
|
49
54
|
* (unborn repository) yields `[]`.
|
|
50
55
|
*/
|
|
51
56
|
export declare function parseLogOutput(output: string): readonly GitCommit[];
|
|
57
|
+
/**
|
|
58
|
+
* 解析带图的 log 输出:
|
|
59
|
+
* `%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%P%x1f%D`
|
|
60
|
+
* 其中 `%P` 为空格分隔的父提交哈希(根提交为空),`%D` 为 ref 装饰。
|
|
61
|
+
* 返回适合分支图渲染器的 `GraphCommit[]`。
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseGraphLogOutput(output: string, remotes?: readonly string[]): readonly GraphCommit[];
|
|
64
|
+
/**
|
|
65
|
+
* 解析 `%D` 装饰串,形如 `HEAD -> main, origin/main, tag: v1.0`;空串无 refs。
|
|
66
|
+
* 分类规则:`HEAD -> x` 为当前分支;`tag: t` 为标签;
|
|
67
|
+
* 带远程前缀(`<remote>/…`)为远程分支;其余为本地分支。
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseDecorations(decorations: string, remotes: readonly string[]): readonly GitRef[];
|
|
70
|
+
/**
|
|
71
|
+
* 解析 `git show -s --format=%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%b` 输出:
|
|
72
|
+
* 前五个字段为机器可读元数据,第六字段起为 %b 正文
|
|
73
|
+
* (%b 已排除 subject 首段落,天然无重复展示问题)。
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseShowMeta(output: string): {
|
|
76
|
+
readonly commit: GitCommit;
|
|
77
|
+
readonly body: string;
|
|
78
|
+
} | null;
|
|
79
|
+
/**
|
|
80
|
+
* 解析 `git show --format= --name-status -z` 输出:NUL 分隔,
|
|
81
|
+
* `X\0path\0`,rename/copy 为 `R100\0old\0new\0`(取新路径)。
|
|
82
|
+
* -z 原始输出不引号化,非 ASCII 路径天然免疫乱码(旧 --stat 八进制转义问题的根因消除)。
|
|
83
|
+
*/
|
|
84
|
+
export declare function parseNameStatusOutput(output: string): readonly {
|
|
85
|
+
readonly path: string;
|
|
86
|
+
readonly status: GitChangeStatus;
|
|
87
|
+
}[];
|
|
52
88
|
/**
|
|
53
89
|
* Parse `git branch --show-current` output: the branch name, or null when
|
|
54
90
|
* empty (detached HEAD).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-free read-only query runner (history / diff / show / branches).
|
|
3
|
+
*
|
|
4
|
+
* Same layering as `core.ts`/`actions.ts`: structural injection, testable
|
|
5
|
+
* against real temporary repositories without a cordis runtime. Every query
|
|
6
|
+
* resolves the workspace once, then runs one or two read-only git commands
|
|
7
|
+
* against the repository root.
|
|
8
|
+
*/
|
|
9
|
+
import { type GitStatusConfig, type SnapshotDeps } from './core.ts';
|
|
10
|
+
import type { GitQueryRequest, GitQueryResponse } from './types.ts';
|
|
11
|
+
/**
|
|
12
|
+
* 执行一条只读查询。结果均为 JSON 纯数据且有界
|
|
13
|
+
* (history 分页;diff 文本受 runner 的 spill/截断约束)。
|
|
14
|
+
* `config` 当前未用:保留以与 runAction 共享 runner 签名契约
|
|
15
|
+
* (deps, config, request),后续查询限流等调优可直接启用。
|
|
16
|
+
*/
|
|
17
|
+
export declare function runQuery(deps: SnapshotDeps, config: GitStatusConfig, request: GitQueryRequest): Promise<GitQueryResponse>;
|
package/lib/host/types.d.ts
CHANGED
|
@@ -66,10 +66,38 @@ export interface GitCommit {
|
|
|
66
66
|
readonly author: string;
|
|
67
67
|
readonly dateIso: string;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* 带父引用的提交(图渲染用)。
|
|
71
|
+
* `parents` 为完整 SHA(线上格式以空格分隔);根提交为空数组。
|
|
72
|
+
* `refs` 为 `%D` 装饰(分支 / 远程 / 标签)。
|
|
73
|
+
*/
|
|
74
|
+
export interface GraphCommit extends GitCommit {
|
|
75
|
+
readonly parents: readonly string[];
|
|
76
|
+
readonly refs: readonly GitRef[];
|
|
77
|
+
}
|
|
78
|
+
/** 提交上挂载的一个 ref 装饰(分支 / 远程 / 标签)。 */
|
|
79
|
+
export interface GitRef {
|
|
80
|
+
readonly kind: 'branch' | 'remote' | 'tag';
|
|
81
|
+
readonly name: string;
|
|
82
|
+
/** `HEAD -> name` 的当前分支为 true。 */
|
|
83
|
+
readonly head: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 一条变更文件条目。混合状态(porcelain XY 双列均非空,如 MM/AM)会被
|
|
87
|
+
* 拆为两条:`staged: true` 一侧(状态取 X 列)与 `staged: false` 一侧
|
|
88
|
+
* (状态取 Y 列),UI 据此分列「已暂存更改 / 更改」两组(IDEA 式)。
|
|
89
|
+
* 真实冲突(UU/AA/DD 等)保持单条 `conflicted`。
|
|
90
|
+
*/
|
|
69
91
|
export interface GitChange {
|
|
70
92
|
readonly path: string;
|
|
71
93
|
readonly status: GitChangeStatus;
|
|
72
94
|
readonly staged: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* 目录条目(git status 对未跟踪目录输出 `dir/`,host 解析时权威标记)。
|
|
97
|
+
* 展示层必须以此字段判断目录而非字符串派生——任何路径规范化剥离尾斜杠
|
|
98
|
+
* 都不会丢失目录性(`.agent/` 曾被当文件展示的根因防护)。
|
|
99
|
+
*/
|
|
100
|
+
readonly isDirectory: boolean;
|
|
73
101
|
}
|
|
74
102
|
export type GitChangeStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked' | 'conflicted' | 'typechange';
|
|
75
103
|
/**
|
|
@@ -98,8 +126,25 @@ export type GitAction = {
|
|
|
98
126
|
/** Commit only these paths (git commit -- <paths> semantics); absent or
|
|
99
127
|
* empty commits everything already staged. */
|
|
100
128
|
readonly paths?: readonly string[];
|
|
129
|
+
} | {
|
|
130
|
+
readonly kind: 'branch-create';
|
|
131
|
+
readonly name: string;
|
|
132
|
+
readonly from?: string;
|
|
133
|
+
} | {
|
|
134
|
+
readonly kind: 'branch-checkout';
|
|
135
|
+
readonly name: string;
|
|
136
|
+
} | {
|
|
137
|
+
readonly kind: 'branch-delete';
|
|
138
|
+
readonly name: string;
|
|
139
|
+
readonly force?: boolean;
|
|
140
|
+
} | {
|
|
141
|
+
readonly kind: 'fetch';
|
|
101
142
|
};
|
|
102
|
-
export type GitOperationErrorCode = 'session-not-found' | 'cwd-unavailable' | 'path-not-found' | 'not-a-git-repo' | 'invalid-path' | 'git-error' | 'timeout'
|
|
143
|
+
export type GitOperationErrorCode = 'session-not-found' | 'cwd-unavailable' | 'path-not-found' | 'not-a-git-repo' | 'invalid-path' | 'invalid-name' | 'git-error' | 'timeout'
|
|
144
|
+
/** 切分支被工作区未提交变更阻止(git: "would be overwritten by checkout")。
|
|
145
|
+
* host 归一化为业务错误:client 用友好文案 +「处理变更」引导,不再直接
|
|
146
|
+
* 抛原始多行 git stderr。原始信息保留在 message。 */
|
|
147
|
+
| 'local-changes-block';
|
|
103
148
|
export type GitActionResult = {
|
|
104
149
|
readonly ok: true;
|
|
105
150
|
readonly snapshot: GitSnapshot;
|
|
@@ -116,3 +161,88 @@ export interface GitActionRequest {
|
|
|
116
161
|
readonly sessionId: string;
|
|
117
162
|
readonly action: GitAction;
|
|
118
163
|
}
|
|
164
|
+
/** 一条只读查询,对应 `gitInfo/query` 端点。 */
|
|
165
|
+
export type GitQuery = {
|
|
166
|
+
readonly kind: 'history';
|
|
167
|
+
readonly limit: number;
|
|
168
|
+
readonly skip: number;
|
|
169
|
+
/** 可选 ref 过滤(分支/远程/标签);缺省为 --all 全分支。 */
|
|
170
|
+
readonly ref?: string;
|
|
171
|
+
/** 文本搜索:7+ 位十六进制视为哈希前缀跳转;否则提交信息正则搜索(-i -E)。 */
|
|
172
|
+
readonly search?: string;
|
|
173
|
+
/** 作者过滤(--author)。 */
|
|
174
|
+
readonly author?: string;
|
|
175
|
+
/** 日期下限(--since,如 '7 days ago')。 */
|
|
176
|
+
readonly since?: string;
|
|
177
|
+
} | {
|
|
178
|
+
readonly kind: 'diff';
|
|
179
|
+
readonly path: string;
|
|
180
|
+
readonly base: 'worktree' | 'staged';
|
|
181
|
+
} | {
|
|
182
|
+
readonly kind: 'show';
|
|
183
|
+
readonly ref: string;
|
|
184
|
+
} | {
|
|
185
|
+
readonly kind: 'branches';
|
|
186
|
+
} | {
|
|
187
|
+
readonly kind: 'tags';
|
|
188
|
+
} | {
|
|
189
|
+
readonly kind: 'authors';
|
|
190
|
+
};
|
|
191
|
+
/** 提交变更文件行(`--name-status` 源:状态 + 路径,不再携带 +/- 行数)。 */
|
|
192
|
+
export interface GitFileStat {
|
|
193
|
+
readonly path: string;
|
|
194
|
+
readonly status: GitChangeStatus;
|
|
195
|
+
}
|
|
196
|
+
/** One branch row from `git branch --format`. Local branches may carry
|
|
197
|
+
* ahead/behind counts relative to their upstream (from `%(upstream:track)`). */
|
|
198
|
+
export interface GitBranch {
|
|
199
|
+
readonly name: string;
|
|
200
|
+
readonly shortHash: string | null;
|
|
201
|
+
/** 本地分支领先上游的提交数(仅本地有上游时存在)。 */
|
|
202
|
+
readonly ahead?: number;
|
|
203
|
+
/** 本地分支落后上游的提交数(仅本地有上游时存在)。 */
|
|
204
|
+
readonly behind?: number;
|
|
205
|
+
}
|
|
206
|
+
export type GitQueryResult = {
|
|
207
|
+
readonly kind: 'history';
|
|
208
|
+
readonly commits: readonly GraphCommit[];
|
|
209
|
+
readonly total: number;
|
|
210
|
+
} | {
|
|
211
|
+
readonly kind: 'diff';
|
|
212
|
+
readonly path: string;
|
|
213
|
+
readonly text: string;
|
|
214
|
+
} | {
|
|
215
|
+
readonly kind: 'show';
|
|
216
|
+
readonly ref: string;
|
|
217
|
+
readonly commit: GitCommit | null;
|
|
218
|
+
/** 提交完整正文(不含 subject 行);IDEA 式右栏展示用。 */
|
|
219
|
+
readonly body: string;
|
|
220
|
+
readonly stats: readonly GitFileStat[];
|
|
221
|
+
} | {
|
|
222
|
+
readonly kind: 'branches';
|
|
223
|
+
readonly current: string | null;
|
|
224
|
+
readonly defaultBranch: string | null;
|
|
225
|
+
readonly local: readonly GitBranch[];
|
|
226
|
+
readonly remote: readonly GitBranch[];
|
|
227
|
+
} | {
|
|
228
|
+
readonly kind: 'tags';
|
|
229
|
+
readonly tags: readonly GitBranch[];
|
|
230
|
+
} | {
|
|
231
|
+
readonly kind: 'authors';
|
|
232
|
+
readonly authors: readonly string[];
|
|
233
|
+
};
|
|
234
|
+
export type GitQueryResponse = {
|
|
235
|
+
readonly ok: true;
|
|
236
|
+
readonly value: GitQueryResult;
|
|
237
|
+
} | {
|
|
238
|
+
readonly ok: false;
|
|
239
|
+
readonly error: {
|
|
240
|
+
readonly code: GitOperationErrorCode;
|
|
241
|
+
readonly message?: string;
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
/** Wire request of the `query` endpoint. */
|
|
245
|
+
export interface GitQueryRequest {
|
|
246
|
+
readonly sessionId: string;
|
|
247
|
+
readonly query: GitQuery;
|
|
248
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-git-ui",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "DeepSeek Harness (dsh) plugin: visualize Git status in the Web UI — current branch, HEAD, staged/modified/untracked counts, ahead/behind, recent commits and changed files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|