dsh-git-ui 0.0.1 → 0.0.2
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 +22 -4
- package/README.zh.md +22 -4
- package/lib/client.js +34 -34
- package/lib/client.js.map +4 -4
- package/lib/host/actions.d.ts +14 -0
- package/lib/host/core.d.ts +24 -0
- package/lib/host/index.d.ts +13 -8
- package/lib/host/index.js +108 -12
- package/lib/host/index.js.map +3 -3
- package/lib/host/types.d.ts +44 -0
- package/package.json +1 -1
- package/src/client/GitCenter.tsx +261 -0
- package/src/client/GitPill.tsx +29 -5
- package/src/client/controller.ts +48 -1
- package/src/client/index.ts +2 -1
- package/src/client/locales.ts +38 -0
- package/src/client/remote.ts +58 -0
- package/src/client/styles.ts +153 -0
- package/src/host/actions.ts +127 -0
- package/src/host/core.ts +31 -12
- package/src/host/index.ts +34 -17
- package/src/host/types.ts +39 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type GitStatusConfig, type SnapshotDeps } from './core.ts';
|
|
2
|
+
import type { GitActionResult, GitActionRequest } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A path is safe when it is repo-relative and stays inside the work tree:
|
|
5
|
+
* reject absolute paths, drive letters / backslashes, and `..` escapes
|
|
6
|
+
* (checked via path resolution against the realpath'd root).
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSafePath(path: string, root: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Execute one management action against the session's repository and return
|
|
11
|
+
* the refreshed snapshot on success (the caller re-renders from it, so the
|
|
12
|
+
* UI never waits for the next poll).
|
|
13
|
+
*/
|
|
14
|
+
export declare function runAction(deps: SnapshotDeps, config: GitStatusConfig, request: GitActionRequest): Promise<GitActionResult>;
|
package/lib/host/core.d.ts
CHANGED
|
@@ -40,6 +40,30 @@ export interface SnapshotDeps {
|
|
|
40
40
|
export declare const DEFAULT_CONFIG: GitStatusConfig;
|
|
41
41
|
/** Coerce a raw patch config value into a validated GitStatusConfig. */
|
|
42
42
|
export declare function normalizeConfig(raw: unknown): GitStatusConfig;
|
|
43
|
+
/** Run one command, mapping a spawn-level failure to a snapshot failure. */
|
|
44
|
+
export declare function runCommand(runner: GitRunner, argv: readonly string[], cwd: string, label: string, signal?: AbortSignal): Promise<{
|
|
45
|
+
readonly run: Awaited<ReturnType<GitRunner['run']>>;
|
|
46
|
+
} | {
|
|
47
|
+
readonly failure: Extract<GitSnapshotResult, {
|
|
48
|
+
ok: false;
|
|
49
|
+
}>['error'];
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a session's repository workspace: cwd (live or persisted), the
|
|
53
|
+
* realpath'd directory, and the git work-tree root via `rev-parse
|
|
54
|
+
* --show-toplevel`. Shared by the snapshot flow and the operation runner.
|
|
55
|
+
*/
|
|
56
|
+
export type WorkspaceResolution = {
|
|
57
|
+
readonly ok: true;
|
|
58
|
+
readonly cwd: string;
|
|
59
|
+
readonly root: string;
|
|
60
|
+
} | {
|
|
61
|
+
readonly ok: false;
|
|
62
|
+
readonly error: Extract<GitSnapshotResult, {
|
|
63
|
+
ok: false;
|
|
64
|
+
}>['error'];
|
|
65
|
+
};
|
|
66
|
+
export declare function resolveWorkspace(deps: SnapshotDeps, sessionId: string): Promise<WorkspaceResolution>;
|
|
43
67
|
/**
|
|
44
68
|
* Build one frozen GitSnapshot for a session working directory.
|
|
45
69
|
* Command sequence (all read-only; every command after the first runs with
|
package/lib/host/index.d.ts
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-git-ui host half: the `gitInfo` Remote service.
|
|
3
3
|
*
|
|
4
|
-
* Cordis shell only — every behavior lives in `core.ts` behind
|
|
5
|
-
* structural faces, so tests never need a cordis runtime. The class
|
|
6
|
-
* plugin in its own right (class form), mounted by the bundle patch row
|
|
7
|
-
* the package name; the gateway exposes `gitInfo/snapshot`
|
|
8
|
-
* discovery (`typertRemote` binding + `@Remote`
|
|
4
|
+
* Cordis shell only — every behavior lives in `core.ts`/`actions.ts` behind
|
|
5
|
+
* injected structural faces, so tests never need a cordis runtime. The class
|
|
6
|
+
* is a plugin in its own right (class form), mounted by the bundle patch row
|
|
7
|
+
* with the package name; the gateway exposes `gitInfo/snapshot` and
|
|
8
|
+
* `gitInfo/run` through SRC discovery (`typertRemote` binding + `@Remote`
|
|
9
|
+
* marker).
|
|
9
10
|
*/
|
|
10
11
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
11
12
|
import type { Context } from '@deepseek-ai/cordis';
|
|
12
|
-
import type { GitSnapshotRequest, GitSnapshotResult } from './types.ts';
|
|
13
|
-
export type { GitSnapshot, GitSnapshotResult, GitSnapshotFailure, GitSnapshotRequest, GitCommit, GitChange } from './types.ts';
|
|
13
|
+
import type { GitActionResult, GitActionRequest, GitSnapshotRequest, GitSnapshotResult } from './types.ts';
|
|
14
|
+
export type { GitSnapshot, GitSnapshotResult, GitSnapshotFailure, GitSnapshotRequest, GitCommit, GitChange, GitAction, GitActionResult, GitActionRequest } from './types.ts';
|
|
14
15
|
export { normalizeConfig, DEFAULT_CONFIG } from './core.ts';
|
|
15
16
|
export { parseStatusOutput, parseLogOutput, parseBranchOutput } from './parser.ts';
|
|
16
|
-
|
|
17
|
+
export { isSafePath, runAction } from './actions.ts';
|
|
18
|
+
/** The `gitInfo` service: `snapshot` (read) and `run` (management) endpoints. */
|
|
17
19
|
export declare class GitStatusService extends TypertRemoteService {
|
|
18
20
|
static inject: string[];
|
|
19
21
|
private readonly config;
|
|
20
22
|
constructor(ctx: Context, config: unknown);
|
|
23
|
+
/** Adapter face shared by both endpoints (injected services + runner). */
|
|
24
|
+
private deps;
|
|
21
25
|
snapshot(request: GitSnapshotRequest, signal?: AbortSignal): Promise<GitSnapshotResult>;
|
|
26
|
+
run(request: GitActionRequest, signal?: AbortSignal): Promise<GitActionResult>;
|
|
22
27
|
}
|
|
23
28
|
export default GitStatusService;
|
package/lib/host/index.js
CHANGED
|
@@ -257,7 +257,7 @@ async function runCommand(runner, argv, cwd, label, signal) {
|
|
|
257
257
|
return { failure: { code: "git-unavailable", detail: `${label}: ${error instanceof Error ? error.message : String(error)}` } };
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
-
async function
|
|
260
|
+
async function resolveWorkspace(deps, sessionId) {
|
|
261
261
|
const resolved = await resolveCwd(deps.sessions, sessionId);
|
|
262
262
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
263
263
|
let realCwd;
|
|
@@ -282,6 +282,12 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
282
282
|
}
|
|
283
283
|
const root = toplevel.run.stdout.trim();
|
|
284
284
|
if (root === "") return { ok: false, error: { code: "not-a-git-repo" } };
|
|
285
|
+
return { ok: true, cwd: realCwd, root };
|
|
286
|
+
}
|
|
287
|
+
async function snapshotForSession(deps, config, sessionId) {
|
|
288
|
+
const workspace = await resolveWorkspace(deps, sessionId);
|
|
289
|
+
if (!workspace.ok) return { ok: false, error: workspace.error };
|
|
290
|
+
const root = workspace.root;
|
|
285
291
|
const branchRun = await runCommand(deps.run, ["git", "branch", "--show-current"], root, "branch", deps.signal);
|
|
286
292
|
if ("failure" in branchRun) return { ok: false, error: branchRun.failure };
|
|
287
293
|
if (branchRun.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
@@ -323,25 +329,102 @@ async function snapshotForSession(deps, config, sessionId) {
|
|
|
323
329
|
return { ok: true, value: snapshot };
|
|
324
330
|
}
|
|
325
331
|
|
|
332
|
+
// src/host/actions.ts
|
|
333
|
+
import { resolve, sep } from "node:path";
|
|
334
|
+
function buildArgv(action, root) {
|
|
335
|
+
switch (action.kind) {
|
|
336
|
+
case "stage":
|
|
337
|
+
return withPaths(["git", "add", "--"], action.paths, root);
|
|
338
|
+
case "stage-all":
|
|
339
|
+
return { argv: [["git", "add", "-A"]] };
|
|
340
|
+
case "unstage":
|
|
341
|
+
return withPaths(["git", "restore", "--staged", "--"], action.paths, root);
|
|
342
|
+
case "unstage-all":
|
|
343
|
+
return { argv: [["git", "restore", "--staged", "--", "."]] };
|
|
344
|
+
case "discard":
|
|
345
|
+
return withPaths(["git", "restore", "--"], action.paths, root);
|
|
346
|
+
case "discard-all":
|
|
347
|
+
return { argv: [["git", "restore", "--staged", "--", "."], ["git", "restore", "--", "."]] };
|
|
348
|
+
case "commit": {
|
|
349
|
+
const message = action.message.trim();
|
|
350
|
+
if (action.paths === void 0 || action.paths.length === 0) {
|
|
351
|
+
return { argv: [["git", "commit", "-m", message]] };
|
|
352
|
+
}
|
|
353
|
+
return withPaths(["git", "commit", "-m", message, "--"], action.paths, root);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function withPaths(prefix, paths, root) {
|
|
358
|
+
if (paths.length === 0) return { error: "no paths given" };
|
|
359
|
+
for (const path of paths) {
|
|
360
|
+
if (!isSafePath(path, root)) return { error: `unsafe path: ${path}` };
|
|
361
|
+
}
|
|
362
|
+
return { argv: [[...prefix, ...paths]] };
|
|
363
|
+
}
|
|
364
|
+
function isSafePath(path, root) {
|
|
365
|
+
if (path === "") return false;
|
|
366
|
+
if (path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:/.test(path)) return false;
|
|
367
|
+
const resolved = resolve(root, path);
|
|
368
|
+
const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
369
|
+
return resolved === root || resolved.startsWith(prefix);
|
|
370
|
+
}
|
|
371
|
+
function operationError(failure) {
|
|
372
|
+
if (failure.code === "git-unavailable") {
|
|
373
|
+
return { ok: false, error: { code: "git-error", message: failure.detail } };
|
|
374
|
+
}
|
|
375
|
+
return { ok: false, error: failure };
|
|
376
|
+
}
|
|
377
|
+
async function runAction(deps, config, request) {
|
|
378
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
379
|
+
if (!workspace.ok) return operationError(workspace.error);
|
|
380
|
+
const root = workspace.root;
|
|
381
|
+
if (request.action.kind === "commit" && request.action.message.trim() === "") {
|
|
382
|
+
return { ok: false, error: { code: "git-error", message: "commit message is empty" } };
|
|
383
|
+
}
|
|
384
|
+
const built = buildArgv(request.action, root);
|
|
385
|
+
if ("error" in built) return { ok: false, error: { code: "invalid-path", message: built.error } };
|
|
386
|
+
let lastStdout = "";
|
|
387
|
+
for (const argv of built.argv) {
|
|
388
|
+
const outcome = await runCommand(deps.run, argv, root, `action ${request.action.kind}`, deps.signal);
|
|
389
|
+
if ("failure" in outcome) return operationError(outcome.failure);
|
|
390
|
+
if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
391
|
+
if (outcome.run.exitCode !== 0) {
|
|
392
|
+
const message = outcome.run.stderr.trim() || outcome.run.stdout.trim();
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
error: {
|
|
396
|
+
code: "git-error",
|
|
397
|
+
message: message !== "" ? message : `git ${request.action.kind} exited ${String(outcome.run.exitCode)}`
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
lastStdout = outcome.run.stdout.trim();
|
|
402
|
+
}
|
|
403
|
+
const snapshot = await snapshotForSession(deps, config, request.sessionId);
|
|
404
|
+
if (!snapshot.ok) return operationError(snapshot.error);
|
|
405
|
+
return { ok: true, snapshot: snapshot.value, ...lastStdout === "" ? {} : { output: lastStdout } };
|
|
406
|
+
}
|
|
407
|
+
|
|
326
408
|
// src/host/index.ts
|
|
327
|
-
var _snapshot_dec, _a, _init;
|
|
328
|
-
var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _a) {
|
|
409
|
+
var _run_dec, _snapshot_dec, _a, _init;
|
|
410
|
+
var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _a) {
|
|
329
411
|
constructor(ctx, config) {
|
|
330
412
|
super(ctx, "gitInfo");
|
|
331
413
|
__runInitializers(_init, 5, this);
|
|
332
414
|
__publicField(this, "config");
|
|
333
415
|
this.config = normalizeConfig(config);
|
|
334
416
|
}
|
|
335
|
-
|
|
417
|
+
/** Adapter face shared by both endpoints (injected services + runner). */
|
|
418
|
+
deps(signal) {
|
|
336
419
|
const subprocess = this.ctx.get("subprocess");
|
|
337
420
|
if (subprocess === void 0) {
|
|
338
|
-
return {
|
|
421
|
+
return { failure: { code: "git-unavailable", detail: "subprocess service unavailable" } };
|
|
339
422
|
}
|
|
340
423
|
const sessions = this.ctx.get("sessions");
|
|
341
424
|
const persistence = this.ctx.get("sessionPersistence");
|
|
342
425
|
const runner = createGitRunner(subprocess, this.config.timeoutMs, this.config.maxStatusBytes);
|
|
343
|
-
return
|
|
344
|
-
{
|
|
426
|
+
return {
|
|
427
|
+
deps: {
|
|
345
428
|
run: runner,
|
|
346
429
|
fs: { realpath, stat },
|
|
347
430
|
sessions: {
|
|
@@ -357,14 +440,25 @@ var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec =
|
|
|
357
440
|
}
|
|
358
441
|
},
|
|
359
442
|
signal
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
async snapshot(request, signal) {
|
|
447
|
+
const adapted = this.deps(signal);
|
|
448
|
+
if ("failure" in adapted) return { ok: false, error: adapted.failure };
|
|
449
|
+
return snapshotForSession(adapted.deps, this.config, request.sessionId);
|
|
450
|
+
}
|
|
451
|
+
async run(request, signal) {
|
|
452
|
+
const adapted = this.deps(signal);
|
|
453
|
+
if ("failure" in adapted) {
|
|
454
|
+
return { ok: false, error: { code: "git-error", message: adapted.failure.detail } };
|
|
455
|
+
}
|
|
456
|
+
return runAction(adapted.deps, this.config, request);
|
|
364
457
|
}
|
|
365
458
|
};
|
|
366
459
|
_init = __decoratorStart(_a);
|
|
367
460
|
__decorateElement(_init, 1, "snapshot", _snapshot_dec, GitStatusService);
|
|
461
|
+
__decorateElement(_init, 1, "run", _run_dec, GitStatusService);
|
|
368
462
|
__decoratorMetadata(_init, GitStatusService);
|
|
369
463
|
__publicField(GitStatusService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
|
|
370
464
|
var index_default = GitStatusService;
|
|
@@ -372,9 +466,11 @@ export {
|
|
|
372
466
|
DEFAULT_CONFIG,
|
|
373
467
|
GitStatusService,
|
|
374
468
|
index_default as default,
|
|
469
|
+
isSafePath,
|
|
375
470
|
normalizeConfig,
|
|
376
471
|
parseBranchOutput,
|
|
377
472
|
parseLogOutput,
|
|
378
|
-
parseStatusOutput
|
|
473
|
+
parseStatusOutput,
|
|
474
|
+
runAction
|
|
379
475
|
};
|
|
380
476
|
//# sourceMappingURL=index.js.map
|
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"],
|
|
4
|
-
"sourcesContent": ["/**\n * dsh-git-ui host half: the `gitInfo` Remote service.\n *\n * Cordis shell only \u2014 every behavior lives in `core.ts` behind injected\n * structural faces, so tests never need a cordis runtime. The class is a\n * plugin in its own right (class form), mounted by the bundle patch row with\n * the package name; the gateway exposes `gitInfo/snapshot` 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 } from './core.ts'\nimport type { GitSnapshotRequest, GitSnapshotResult } from './types.ts'\n\nexport type { GitSnapshot, GitSnapshotResult, GitSnapshotFailure, GitSnapshotRequest, GitCommit, GitChange } from './types.ts'\nexport { normalizeConfig, DEFAULT_CONFIG } from './core.ts'\nexport { parseStatusOutput, parseLogOutput, parseBranchOutput } from './parser.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: one `snapshot` Remote endpoint. */\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 @Remote('snapshot')\n async snapshot(request: GitSnapshotRequest, signal?: AbortSignal): Promise<GitSnapshotResult> {\n const subprocess = this.ctx.get('subprocess') as SubprocessLike | undefined\n if (subprocess === undefined) {\n return { ok: false, error: { 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 snapshotForSession(\n {\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 this.config,\n request.sessionId,\n )\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. */\nasync 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 * 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 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\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"],
|
|
5
|
-
"mappings": "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",
|
|
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
6
|
"names": ["stat"]
|
|
7
7
|
}
|
package/lib/host/types.d.ts
CHANGED
|
@@ -72,3 +72,47 @@ export interface GitChange {
|
|
|
72
72
|
readonly staged: boolean;
|
|
73
73
|
}
|
|
74
74
|
export type GitChangeStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked' | 'conflicted' | 'typechange';
|
|
75
|
+
/**
|
|
76
|
+
* One git management operation addressed by the `run` endpoint. Paths are
|
|
77
|
+
* always repository-relative (as listed in a snapshot's `changes`), never
|
|
78
|
+
* absolute — the host validates them against the work tree.
|
|
79
|
+
*/
|
|
80
|
+
export type GitAction = {
|
|
81
|
+
readonly kind: 'stage';
|
|
82
|
+
readonly paths: readonly string[];
|
|
83
|
+
} | {
|
|
84
|
+
readonly kind: 'stage-all';
|
|
85
|
+
} | {
|
|
86
|
+
readonly kind: 'unstage';
|
|
87
|
+
readonly paths: readonly string[];
|
|
88
|
+
} | {
|
|
89
|
+
readonly kind: 'unstage-all';
|
|
90
|
+
} | {
|
|
91
|
+
readonly kind: 'discard';
|
|
92
|
+
readonly paths: readonly string[];
|
|
93
|
+
} | {
|
|
94
|
+
readonly kind: 'discard-all';
|
|
95
|
+
} | {
|
|
96
|
+
readonly kind: 'commit';
|
|
97
|
+
readonly message: string;
|
|
98
|
+
/** Commit only these paths (git commit -- <paths> semantics); absent or
|
|
99
|
+
* empty commits everything already staged. */
|
|
100
|
+
readonly paths?: readonly string[];
|
|
101
|
+
};
|
|
102
|
+
export type GitOperationErrorCode = 'session-not-found' | 'cwd-unavailable' | 'path-not-found' | 'not-a-git-repo' | 'invalid-path' | 'git-error' | 'timeout';
|
|
103
|
+
export type GitActionResult = {
|
|
104
|
+
readonly ok: true;
|
|
105
|
+
readonly snapshot: GitSnapshot;
|
|
106
|
+
readonly output?: string;
|
|
107
|
+
} | {
|
|
108
|
+
readonly ok: false;
|
|
109
|
+
readonly error: {
|
|
110
|
+
readonly code: GitOperationErrorCode;
|
|
111
|
+
readonly message?: string;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/** Wire request of the `run` endpoint. */
|
|
115
|
+
export interface GitActionRequest {
|
|
116
|
+
readonly sessionId: string;
|
|
117
|
+
readonly action: GitAction;
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-git-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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",
|