@norman-else/dsh-claude 0.1.18 → 0.1.19

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/lib/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_UPDATE_CHECK_PATH, x as TASK_TOOL_NAMES, y as CLAUDE_UPDATE_PATH } from "./events-6xTApIQw.mjs";
2
- import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BWXp9d3p.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-B0NrA4Hi.mjs";
1
+ import { S as TASK_TOOL_NAMES, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_UPDATE_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_REPOSITORY_SETUP_PATH, y as CLAUDE_UPDATE_CHECK_PATH } from "./events-DPJaBReT.mjs";
2
+ import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-DBRIOgJs.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DnoSEKGm.mjs";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { chmod, mkdir, opendir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
7
- import { dirname, extname, join, resolve } from "node:path";
7
+ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
8
8
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
9
9
  import { homedir } from "node:os";
10
10
  import { query } from "@anthropic-ai/claude-agent-sdk";
@@ -22,18 +22,18 @@ function emptyProjection() {
22
22
  activities: []
23
23
  };
24
24
  }
25
- function record$1(value) {
25
+ function record$3(value) {
26
26
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
27
27
  }
28
28
  function finiteInteger(value) {
29
29
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
30
30
  }
31
- function string$1(value, max) {
31
+ function string$2(value, max) {
32
32
  return typeof value === "string" && value.length > 0 && value.length <= max;
33
33
  }
34
34
  function binding(value) {
35
- const input = record$1(value);
36
- if (input === void 0 || !string$1(input.claudeSessionId, 512) || !string$1(input.sdkVersion, 128) || !string$1(input.cwd, 4096) || input.cliVersion !== void 0 && !string$1(input.cliVersion, 128)) return void 0;
35
+ const input = record$3(value);
36
+ if (input === void 0 || !string$2(input.claudeSessionId, 512) || !string$2(input.sdkVersion, 128) || !string$2(input.cwd, 4096) || input.cliVersion !== void 0 && !string$2(input.cliVersion, 128)) return void 0;
37
37
  return {
38
38
  claudeSessionId: input.claudeSessionId,
39
39
  sdkVersion: input.sdkVersion,
@@ -61,22 +61,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
61
61
  "failed"
62
62
  ]);
63
63
  function activity(value) {
64
- const input = record$1(value);
64
+ const input = record$3(value);
65
65
  if (input === void 0 || !finiteInteger(input.turn) || !finiteInteger(input.step) || !finiteInteger(input.ordinal) || typeof input.kind !== "string" || !ACTIVITY_KINDS.has(input.kind) || input.phase !== void 0 && (typeof input.phase !== "string" || !ACTIVITY_PHASES.has(input.phase))) return void 0;
66
66
  return normalizeActivity(input);
67
67
  }
68
68
  function contextUsage(value) {
69
- const input = record$1(value);
69
+ const input = record$3(value);
70
70
  if (input === void 0 || !Array.isArray(input.categories)) return void 0;
71
71
  return normalizeContextUsage(input);
72
72
  }
73
73
  function tasks(value) {
74
- const input = record$1(value);
74
+ const input = record$3(value);
75
75
  if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
76
76
  return normalizeTasksEvent(input.tasks);
77
77
  }
78
78
  function parseClaudeSidecar(value) {
79
- const input = record$1(value);
79
+ const input = record$3(value);
80
80
  if (input === void 0 || input.schemaVersion !== SIDECAR_SCHEMA_VERSION || !finiteInteger(input.revision) || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("dsh-claude: invalid sidecar document");
81
81
  const activities = input.activities.map(activity);
82
82
  if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
@@ -525,10 +525,10 @@ function createUserQuestionBridge(userQuestions, activeContext) {
525
525
  }
526
526
  //#endregion
527
527
  //#region src/sdk-messages.ts
528
- function record(value) {
528
+ function record$2(value) {
529
529
  return value !== null && typeof value === "object" ? value : void 0;
530
530
  }
531
- function string(value) {
531
+ function string$1(value) {
532
532
  return typeof value === "string" ? value : void 0;
533
533
  }
534
534
  function taskUsageOf(usage) {
@@ -540,7 +540,7 @@ function taskUsageOf(usage) {
540
540
  return Object.keys(normalized).length === 0 ? void 0 : normalized;
541
541
  }
542
542
  function resultUsage(message) {
543
- const usage = record(message.usage);
543
+ const usage = record$2(message.usage);
544
544
  const normalized = {};
545
545
  if (usage !== void 0) {
546
546
  if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
@@ -552,26 +552,26 @@ function resultUsage(message) {
552
552
  return normalized;
553
553
  }
554
554
  function normalizeAssistant(message) {
555
- const content = record(message.message)?.content;
555
+ const content = record$2(message.message)?.content;
556
556
  if (!Array.isArray(content)) return [{
557
557
  kind: "protocol-error",
558
558
  title: "Malformed Claude assistant message",
559
559
  detail: message
560
560
  }];
561
- const parentToolUseId = string(message.parent_tool_use_id);
561
+ const parentToolUseId = string$1(message.parent_tool_use_id);
562
562
  const normalized = [];
563
563
  for (const item of content) {
564
- const block = record(item);
564
+ const block = record$2(item);
565
565
  if (block === void 0) continue;
566
566
  if (block.type === "text") {
567
- const text = string(block.text);
567
+ const text = string$1(block.text);
568
568
  if (text !== void 0 && text.length > 0) normalized.push({
569
569
  kind: "assistant-text",
570
570
  text,
571
571
  ...parentToolUseId === void 0 ? {} : { parentToolUseId }
572
572
  });
573
573
  } else if (block.type === "thinking") {
574
- const text = string(block.thinking);
574
+ const text = string$1(block.thinking);
575
575
  if (text !== void 0 && text.length > 0) normalized.push({
576
576
  kind: "thinking",
577
577
  text,
@@ -579,8 +579,8 @@ function normalizeAssistant(message) {
579
579
  ...parentToolUseId === void 0 ? {} : { parentToolUseId }
580
580
  });
581
581
  } else if (block.type === "tool_use") {
582
- const toolUseId = string(block.id);
583
- const toolName = string(block.name);
582
+ const toolUseId = string$1(block.id);
583
+ const toolName = string$1(block.name);
584
584
  if (toolUseId !== void 0 && toolName !== void 0) normalized.push({
585
585
  kind: "tool-call",
586
586
  toolUseId,
@@ -593,18 +593,18 @@ function normalizeAssistant(message) {
593
593
  return normalized;
594
594
  }
595
595
  function normalizeUser(message) {
596
- const content = record(message.message)?.content;
596
+ const content = record$2(message.message)?.content;
597
597
  if (!Array.isArray(content)) return [{
598
598
  kind: "protocol-error",
599
599
  title: "Malformed Claude user message",
600
600
  detail: message
601
601
  }];
602
- const parentToolUseId = string(message.parent_tool_use_id);
602
+ const parentToolUseId = string$1(message.parent_tool_use_id);
603
603
  const normalized = [];
604
604
  for (const item of content) {
605
- const block = record(item);
605
+ const block = record$2(item);
606
606
  if (block?.type !== "tool_result") continue;
607
- const toolUseId = string(block.tool_use_id);
607
+ const toolUseId = string$1(block.tool_use_id);
608
608
  if (toolUseId === void 0) continue;
609
609
  normalized.push({
610
610
  kind: "tool-result",
@@ -617,11 +617,11 @@ function normalizeUser(message) {
617
617
  return normalized;
618
618
  }
619
619
  function normalizeSystem(message) {
620
- const subtype = string(message.subtype);
620
+ const subtype = string$1(message.subtype);
621
621
  if (subtype === "init") {
622
- const sessionId = string(message.session_id);
623
- const cliVersion = string(message.claude_code_version);
624
- const cwd = string(message.cwd);
622
+ const sessionId = string$1(message.session_id);
623
+ const cliVersion = string$1(message.claude_code_version);
624
+ const cwd = string$1(message.cwd);
625
625
  return sessionId !== void 0 && cliVersion !== void 0 && cwd !== void 0 ? [{
626
626
  kind: "init",
627
627
  sessionId,
@@ -650,20 +650,20 @@ function normalizeSystem(message) {
650
650
  title: `Claude session ${String(message.state)}`
651
651
  }];
652
652
  if (subtype === "permission_denied") {
653
- const toolUseId = string(message.tool_use_id);
654
- const toolName = string(message.tool_name);
653
+ const toolUseId = string$1(message.tool_use_id);
654
+ const toolName = string$1(message.tool_name);
655
655
  if (toolUseId !== void 0 && toolName !== void 0) return [{
656
656
  kind: "permission-denied",
657
657
  toolUseId,
658
658
  toolName,
659
- summary: string(message.message) ?? "Claude Code denied the action"
659
+ summary: string$1(message.message) ?? "Claude Code denied the action"
660
660
  }];
661
661
  }
662
662
  if (subtype === "task_started") {
663
- const taskId = string(message.task_id);
664
- const description = string(message.description);
665
- const subagentType = string(message.subagent_type);
666
- const taskType = string(message.task_type);
663
+ const taskId = string$1(message.task_id);
664
+ const description = string$1(message.description);
665
+ const subagentType = string$1(message.subagent_type);
666
+ const taskType = string$1(message.task_type);
667
667
  return [{
668
668
  kind: "subagent",
669
669
  title: description ?? taskId ?? "Claude subagent started",
@@ -678,12 +678,12 @@ function normalizeSystem(message) {
678
678
  }];
679
679
  }
680
680
  if (subtype === "task_progress") {
681
- const taskId = string(message.task_id);
682
- const description = string(message.description);
683
- const summary = string(message.summary);
684
- const subagentType = string(message.subagent_type);
685
- const lastToolName = string(message.last_tool_name);
686
- const usage = taskUsageOf(record(message.usage));
681
+ const taskId = string$1(message.task_id);
682
+ const description = string$1(message.description);
683
+ const summary = string$1(message.summary);
684
+ const subagentType = string$1(message.subagent_type);
685
+ const lastToolName = string$1(message.last_tool_name);
686
+ const usage = taskUsageOf(record$2(message.usage));
687
687
  return [{
688
688
  kind: "subagent",
689
689
  title: summary ?? description ?? "Claude subagent update",
@@ -699,11 +699,11 @@ function normalizeSystem(message) {
699
699
  }];
700
700
  }
701
701
  if (subtype === "task_updated") {
702
- const patch = record(message.patch);
703
- const status = string(patch?.status);
704
- const taskId = string(message.task_id);
705
- const description = string(patch?.description);
706
- const error = string(patch?.error);
702
+ const patch = record$2(message.patch);
703
+ const status = string$1(patch?.status);
704
+ const taskId = string$1(message.task_id);
705
+ const description = string$1(patch?.description);
706
+ const error = string$1(patch?.error);
707
707
  const taskStatus = status === void 0 ? void 0 : status === "killed" ? "killed" : status === "completed" ? "completed" : status === "failed" ? "failed" : "running";
708
708
  return [{
709
709
  kind: "subagent",
@@ -719,10 +719,10 @@ function normalizeSystem(message) {
719
719
  if (subtype === "task_notification") {
720
720
  const failed = message.status === "failed";
721
721
  const stopped = message.status === "stopped" || message.status === "cancelled";
722
- const taskId = string(message.task_id);
723
- const summary = string(message.summary);
722
+ const taskId = string$1(message.task_id);
723
+ const summary = string$1(message.summary);
724
724
  const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
725
- const usage = taskUsageOf(record(message.usage));
725
+ const usage = taskUsageOf(record$2(message.usage));
726
726
  return [{
727
727
  kind: "subagent",
728
728
  title: summary ?? taskId ?? "Claude subagent finished",
@@ -737,10 +737,10 @@ function normalizeSystem(message) {
737
737
  if (subtype === "background_tasks_changed") return [{
738
738
  kind: "background-tasks",
739
739
  tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
740
- const entry = record(item);
741
- const taskId = string(entry?.task_id);
742
- const description = string(entry?.description);
743
- const taskType = string(entry?.task_type);
740
+ const entry = record$2(item);
741
+ const taskId = string$1(entry?.task_id);
742
+ const description = string$1(entry?.description);
743
+ const taskType = string$1(entry?.task_type);
744
744
  if (taskId === void 0 || description === void 0) return [];
745
745
  return [{
746
746
  taskId,
@@ -756,7 +756,7 @@ function normalizeSystem(message) {
756
756
  }];
757
757
  if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
758
758
  kind: message.level === "warning" ? "warning" : "status",
759
- title: string(message.content) ?? string(message.text) ?? "Claude Code notice",
759
+ title: string$1(message.content) ?? string$1(message.text) ?? "Claude Code notice",
760
760
  detail: message
761
761
  }];
762
762
  if (subtype?.startsWith("hook_") === true || subtype === "plugin_install") return [{
@@ -780,12 +780,12 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
780
780
  function normalizeSdkMessage(message) {
781
781
  const value = message;
782
782
  if (value.type === "stream_event") {
783
- const event = record(value.event);
784
- const parentToolUseId = string(value.parent_tool_use_id);
783
+ const event = record$2(value.event);
784
+ const parentToolUseId = string$1(value.parent_tool_use_id);
785
785
  if (event?.type === "content_block_delta") {
786
- const delta = record(event.delta);
786
+ const delta = record$2(event.delta);
787
787
  if (delta?.type === "text_delta") {
788
- const text = string(delta.text);
788
+ const text = string$1(delta.text);
789
789
  return text === void 0 ? [] : [{
790
790
  kind: "text-delta",
791
791
  text,
@@ -793,7 +793,7 @@ function normalizeSdkMessage(message) {
793
793
  }];
794
794
  }
795
795
  if (delta?.type === "thinking_delta") {
796
- const text = string(delta.thinking);
796
+ const text = string$1(delta.thinking);
797
797
  return text === void 0 ? [] : [{
798
798
  kind: "thinking",
799
799
  text,
@@ -808,7 +808,7 @@ function normalizeSdkMessage(message) {
808
808
  if (value.type === "user") return normalizeUser(value);
809
809
  if (value.type === "system") return normalizeSystem(value);
810
810
  if (value.type === "result") {
811
- const sessionId = string(value.session_id);
811
+ const sessionId = string$1(value.session_id);
812
812
  if (sessionId === void 0 || value.subtype !== "success" && !RESULT_ERROR_SUBTYPES.has(String(value.subtype))) return [{
813
813
  kind: "protocol-error",
814
814
  title: "Malformed Claude result message",
@@ -816,11 +816,11 @@ function normalizeSdkMessage(message) {
816
816
  }];
817
817
  const success = value.subtype === "success" && value.is_error !== true;
818
818
  const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
819
- const terminalReason = string(value.terminal_reason);
820
- const userMessageUuid = string(value.user_message_uuid);
821
- const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record(item)).filter((item) => item !== void 0).map((item) => {
822
- const toolName = string(item.tool_name);
823
- const toolUseId = string(item.tool_use_id);
819
+ const terminalReason = string$1(value.terminal_reason);
820
+ const userMessageUuid = string$1(value.user_message_uuid);
821
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$2(item)).filter((item) => item !== void 0).map((item) => {
822
+ const toolName = string$1(item.tool_name);
823
+ const toolUseId = string$1(item.tool_use_id);
824
824
  return toolName === void 0 || toolUseId === void 0 ? void 0 : {
825
825
  toolName,
826
826
  toolUseId
@@ -844,7 +844,7 @@ function normalizeSdkMessage(message) {
844
844
  detail: value.error ?? value.output
845
845
  }];
846
846
  if (value.type === "rate_limit_event") {
847
- const status = string(record(value.rate_limit_info)?.status);
847
+ const status = string$1(record$2(value.rate_limit_info)?.status);
848
848
  const blocking = status !== void 0 && status !== "allowed";
849
849
  return [{
850
850
  kind: blocking ? "warning" : "status",
@@ -2033,7 +2033,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2033
2033
  inputModalities: ["text", "image"]
2034
2034
  }));
2035
2035
  }
2036
- async resolveModel(provider, model) {
2036
+ async resolveModel(provider, model, _signal) {
2037
2037
  const known = MODELS.find((item) => item.id === model);
2038
2038
  const contextWindow = this.#supervisor.contextWindow(model) ?? (known !== void 0 && "contextWindow" in known ? known.contextWindow : void 0);
2039
2039
  return {
@@ -2050,6 +2050,12 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2050
2050
  })) }
2051
2051
  };
2052
2052
  }
2053
+ async prepareCall(provider, model, signal) {
2054
+ return {
2055
+ model: await this.resolveModel(provider, model, signal),
2056
+ stream: (options) => this.stream(options)
2057
+ };
2058
+ }
2053
2059
  async *stream(options) {
2054
2060
  if (options.purpose !== void 0) throw new Error(`dsh-claude: auxiliary ${options.purpose} calls are not routed into the Claude session`);
2055
2061
  const agent = resolveAgent(this.#agents, options);
@@ -2304,7 +2310,7 @@ function sessionIdFromUrl(rawUrl) {
2304
2310
  }
2305
2311
  }
2306
2312
  /** Register the browser-readable, credential-free sidecar projection endpoint. */
2307
- function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => []) {
2313
+ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0) {
2308
2314
  ctx.effect(() => ctx.webServer.register({
2309
2315
  kind: "prefix",
2310
2316
  path: CLAUDE_PROJECTION_PATH,
@@ -2315,14 +2321,17 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2315
2321
  if (sessionId === void 0) return json(res, 400, { error: "invalid session id" });
2316
2322
  try {
2317
2323
  const projection = await sidecar.read(sessionId);
2324
+ const owned = ownsSession(sessionId);
2325
+ const repository = owned ? await repositoryForSession(sessionId) : void 0;
2318
2326
  return json(res, 200, {
2319
2327
  schemaVersion: projection.schemaVersion,
2320
2328
  revision: projection.revision,
2321
- owned: ownsSession(sessionId),
2329
+ owned,
2322
2330
  commands: commandsForSession(sessionId),
2323
2331
  activities: projection.activities,
2324
2332
  ...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
2325
- ...projection.tasks === void 0 ? {} : { tasks: projection.tasks }
2333
+ ...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
2334
+ ...repository === void 0 ? {} : { repository }
2326
2335
  });
2327
2336
  } catch {
2328
2337
  return json(res, 500, { error: "projection unavailable" });
@@ -2331,6 +2340,788 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2331
2340
  }), "dsh-claude: sidecar projection route");
2332
2341
  }
2333
2342
  //#endregion
2343
+ //#region src/repository-status.ts
2344
+ const MAX_OUTPUT_BYTES$1 = 65536;
2345
+ const MAX_DIFF_BYTES = 262144;
2346
+ const GIT_TIMEOUT_MS$1 = 5e3;
2347
+ const GH_TIMEOUT_MS = 8e3;
2348
+ const CACHE_TTL_MS = 5e3;
2349
+ const MAX_TEXT_CHARS = 1024;
2350
+ function bounded(value) {
2351
+ return value.trim().slice(0, MAX_TEXT_CHARS);
2352
+ }
2353
+ async function collect$1(handle) {
2354
+ const outcome = await handle.done;
2355
+ const stdout = handle.collected.stdout?.readFrom(0);
2356
+ return {
2357
+ exitCode: outcome.exitCode,
2358
+ stdout: stdout?.text ?? "",
2359
+ lossy: stdout?.lossy === true
2360
+ };
2361
+ }
2362
+ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
2363
+ const signal = AbortSignal.timeout(timeoutMs);
2364
+ return collect$1(runtime.spawn({
2365
+ argv: [executable, ...args],
2366
+ cwd,
2367
+ stdio: {
2368
+ stdin: "ignore",
2369
+ stdout: { maxBytes },
2370
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
2371
+ },
2372
+ graceMs: 1e3,
2373
+ signal,
2374
+ env: {}
2375
+ }));
2376
+ }
2377
+ function normalizedPath(value) {
2378
+ return value.replaceAll("\\", "/").replace(/\/+$/u, "").toLocaleLowerCase("en-US");
2379
+ }
2380
+ function parseGitStatus(output) {
2381
+ let branch;
2382
+ let detached = false;
2383
+ let dirty = false;
2384
+ for (const line of output.split(/\r?\n/u)) {
2385
+ if (line.startsWith("# branch.head ")) {
2386
+ const head = bounded(line.slice(14));
2387
+ if (head === "(detached)") detached = true;
2388
+ else if (head.length > 0 && head !== "(unknown)") branch = head;
2389
+ continue;
2390
+ }
2391
+ if (line.length > 0 && !line.startsWith("# ")) dirty = true;
2392
+ }
2393
+ return {
2394
+ ...branch === void 0 ? {} : { branch },
2395
+ detached,
2396
+ dirty
2397
+ };
2398
+ }
2399
+ function parseDiffNumstat(value) {
2400
+ let additions = 0;
2401
+ let deletions = 0;
2402
+ let files = 0;
2403
+ for (const line of value.split(/\r?\n/u)) {
2404
+ if (line.length === 0) continue;
2405
+ const [added, deleted] = line.split(" ");
2406
+ files += 1;
2407
+ if (added !== void 0 && /^\d+$/u.test(added)) additions += Number(added);
2408
+ if (deleted !== void 0 && /^\d+$/u.test(deleted)) deletions += Number(deleted);
2409
+ }
2410
+ return {
2411
+ additions,
2412
+ deletions,
2413
+ files
2414
+ };
2415
+ }
2416
+ function parseGitHubRemote(value) {
2417
+ const remote = bounded(value);
2418
+ const match = /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?$/iu.exec(remote);
2419
+ if (match?.[1] === void 0 || match[2] === void 0) return void 0;
2420
+ return `${match[1]}/${match[2]}`;
2421
+ }
2422
+ function record$1(value) {
2423
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2424
+ }
2425
+ function aggregateChecks(value) {
2426
+ if (!Array.isArray(value) || value.length === 0) return "none";
2427
+ let pending = false;
2428
+ for (const item of value) {
2429
+ const check = record$1(item);
2430
+ if (check === void 0) continue;
2431
+ const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
2432
+ const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
2433
+ const state = typeof check.state === "string" ? check.state.toUpperCase() : void 0;
2434
+ if ([
2435
+ "FAILURE",
2436
+ "ERROR",
2437
+ "CANCELLED",
2438
+ "TIMED_OUT",
2439
+ "ACTION_REQUIRED"
2440
+ ].includes(conclusion ?? state ?? "")) return "failing";
2441
+ if (status !== void 0 && status !== "COMPLETED") pending = true;
2442
+ if (["PENDING", "EXPECTED"].includes(state ?? "")) pending = true;
2443
+ }
2444
+ return pending ? "pending" : "passing";
2445
+ }
2446
+ function reviewState(value) {
2447
+ if (value === "APPROVED") return "approved";
2448
+ if (value === "CHANGES_REQUESTED") return "changes-requested";
2449
+ if (value === "REVIEW_REQUIRED") return "review-required";
2450
+ return "none";
2451
+ }
2452
+ function parsePullRequest(value) {
2453
+ const input = record$1(value);
2454
+ if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
2455
+ let url;
2456
+ try {
2457
+ url = new URL(input.url);
2458
+ } catch {
2459
+ return;
2460
+ }
2461
+ if (url.protocol !== "https:" || url.hostname !== "github.com") return void 0;
2462
+ const rawState = typeof input.state === "string" ? input.state.toUpperCase() : "";
2463
+ const state = input.mergedAt !== void 0 && input.mergedAt !== null ? "merged" : rawState === "OPEN" ? "open" : rawState === "CLOSED" ? "closed" : void 0;
2464
+ if (state === void 0) return void 0;
2465
+ return {
2466
+ number: Number(input.number),
2467
+ title: bounded(input.title),
2468
+ url: url.href,
2469
+ state,
2470
+ draft: input.isDraft === true,
2471
+ review: reviewState(input.reviewDecision),
2472
+ checks: aggregateChecks(input.statusCheckRollup),
2473
+ ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
2474
+ ...typeof record$1(input.author)?.login === "string" ? { author: bounded(String(record$1(input.author)?.login)) } : {},
2475
+ ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
2476
+ ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
2477
+ };
2478
+ }
2479
+ var RepositoryStatusService = class {
2480
+ #runtime;
2481
+ #cacheTtlMs;
2482
+ #cache = /* @__PURE__ */ new Map();
2483
+ #lastReady = /* @__PURE__ */ new Map();
2484
+ #gitExecutable;
2485
+ #ghExecutable;
2486
+ constructor(runtime, cacheTtlMs = CACHE_TTL_MS) {
2487
+ this.#runtime = runtime;
2488
+ this.#cacheTtlMs = cacheTtlMs;
2489
+ }
2490
+ inspect(cwd) {
2491
+ const current = this.#cache.get(cwd);
2492
+ if (current !== void 0 && current.expiresAt > Date.now()) return current.value;
2493
+ const value = this.#inspect(cwd).then((next) => this.#stabilize(cwd, next));
2494
+ this.#cache.set(cwd, {
2495
+ expiresAt: Date.now() + this.#cacheTtlMs,
2496
+ value
2497
+ });
2498
+ value.catch(() => this.#cache.delete(cwd));
2499
+ return value;
2500
+ }
2501
+ dispose() {
2502
+ this.#cache.clear();
2503
+ this.#lastReady.clear();
2504
+ }
2505
+ #stabilize(cwd, next) {
2506
+ const previous = this.#lastReady.get(cwd);
2507
+ if (next.status !== "ready") return previous ?? next;
2508
+ const stable = previous?.status === "ready" && previous.root === next.root && previous.branch === next.branch ? {
2509
+ ...next,
2510
+ ...next.pullRequest === void 0 && previous.pullRequest !== void 0 ? { pullRequest: previous.pullRequest } : {},
2511
+ ...next.pullRequest === void 0 && previous.pullRequest !== void 0 && previous.diff !== void 0 ? { diff: previous.diff } : next.diff === void 0 && previous.diff !== void 0 ? { diff: previous.diff } : {}
2512
+ } : next;
2513
+ this.#lastReady.set(cwd, stable);
2514
+ return stable;
2515
+ }
2516
+ async #git() {
2517
+ this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
2518
+ return this.#gitExecutable;
2519
+ }
2520
+ async #gh() {
2521
+ this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => void 0);
2522
+ return this.#ghExecutable;
2523
+ }
2524
+ async #inspect(cwd) {
2525
+ const safeCwd = bounded(cwd);
2526
+ let git;
2527
+ try {
2528
+ git = await this.#git();
2529
+ } catch {
2530
+ return {
2531
+ status: "unavailable",
2532
+ cwd: safeCwd
2533
+ };
2534
+ }
2535
+ try {
2536
+ const paths = await run(this.#runtime, git, [
2537
+ "rev-parse",
2538
+ "--path-format=absolute",
2539
+ "--show-toplevel",
2540
+ "--absolute-git-dir",
2541
+ "--git-common-dir"
2542
+ ], cwd, GIT_TIMEOUT_MS$1);
2543
+ if (paths.exitCode !== 0) return {
2544
+ status: "not-repository",
2545
+ cwd: safeCwd
2546
+ };
2547
+ const [rootValue, gitDirValue, commonDirValue] = paths.stdout.split(/\r?\n/u);
2548
+ const root = bounded(rootValue ?? "");
2549
+ const gitDir = bounded(gitDirValue ?? "");
2550
+ const commonDir = bounded(commonDirValue ?? "");
2551
+ if (root.length === 0 || gitDir.length === 0 || commonDir.length === 0) return {
2552
+ status: "unavailable",
2553
+ cwd: safeCwd
2554
+ };
2555
+ const statusResult = await run(this.#runtime, git, [
2556
+ "status",
2557
+ "--porcelain=v2",
2558
+ "--branch",
2559
+ "--untracked-files=no"
2560
+ ], cwd, GIT_TIMEOUT_MS$1);
2561
+ if (statusResult.exitCode !== 0) return {
2562
+ status: "unavailable",
2563
+ cwd: safeCwd
2564
+ };
2565
+ const status = parseGitStatus(statusResult.stdout);
2566
+ const remoteResult = await run(this.#runtime, git, [
2567
+ "remote",
2568
+ "get-url",
2569
+ "origin"
2570
+ ], cwd, GIT_TIMEOUT_MS$1);
2571
+ const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
2572
+ const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
2573
+ const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
2574
+ const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase) : {
2575
+ additions: 0,
2576
+ deletions: 0,
2577
+ files: 0,
2578
+ truncated: false
2579
+ };
2580
+ return {
2581
+ status: "ready",
2582
+ cwd: safeCwd,
2583
+ root,
2584
+ ...status,
2585
+ worktree: normalizedPath(gitDir) !== normalizedPath(commonDir),
2586
+ ...remote === void 0 ? {} : { remote },
2587
+ ...pullRequest === void 0 ? {} : { pullRequest },
2588
+ ...diff === void 0 ? {} : { diff }
2589
+ };
2590
+ } catch {
2591
+ return {
2592
+ status: "unavailable",
2593
+ cwd: safeCwd
2594
+ };
2595
+ }
2596
+ }
2597
+ async #mergeBase(cwd, git, baseBranch) {
2598
+ try {
2599
+ const result = await run(this.#runtime, git, [
2600
+ "merge-base",
2601
+ "HEAD",
2602
+ `refs/remotes/origin/${baseBranch}`
2603
+ ], cwd, GIT_TIMEOUT_MS$1);
2604
+ if (result.exitCode !== 0 || result.lossy) return void 0;
2605
+ const oid = bounded(result.stdout);
2606
+ return /^[0-9a-f]{40}$/iu.test(oid) ? oid : void 0;
2607
+ } catch {
2608
+ return;
2609
+ }
2610
+ }
2611
+ async #diff(cwd, git, base) {
2612
+ try {
2613
+ const numstat = await run(this.#runtime, git, [
2614
+ "diff",
2615
+ "--no-ext-diff",
2616
+ "--numstat",
2617
+ base,
2618
+ "--"
2619
+ ], cwd, GIT_TIMEOUT_MS$1);
2620
+ if (numstat.exitCode !== 0 || numstat.lossy) return void 0;
2621
+ const summary = parseDiffNumstat(numstat.stdout);
2622
+ const patch = await run(this.#runtime, git, [
2623
+ "diff",
2624
+ "--no-ext-diff",
2625
+ "--no-color",
2626
+ "--unified=3",
2627
+ base,
2628
+ "--"
2629
+ ], cwd, GIT_TIMEOUT_MS$1, MAX_DIFF_BYTES);
2630
+ if (patch.exitCode !== 0) return {
2631
+ ...summary,
2632
+ truncated: true
2633
+ };
2634
+ return {
2635
+ ...summary,
2636
+ ...patch.lossy ? {} : { patch: patch.stdout },
2637
+ truncated: patch.lossy
2638
+ };
2639
+ } catch {
2640
+ return;
2641
+ }
2642
+ }
2643
+ async #pullRequest(cwd, repository, branch) {
2644
+ const gh = await this.#gh();
2645
+ if (gh === void 0) return void 0;
2646
+ try {
2647
+ const result = await run(this.#runtime, gh, [
2648
+ "pr",
2649
+ "view",
2650
+ branch,
2651
+ "--repo",
2652
+ repository,
2653
+ "--json",
2654
+ "number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergedAt,statusCheckRollup,author,createdAt,baseRefName"
2655
+ ], cwd, GH_TIMEOUT_MS);
2656
+ if (result.exitCode !== 0) return void 0;
2657
+ return parsePullRequest(JSON.parse(result.stdout));
2658
+ } catch {
2659
+ return;
2660
+ }
2661
+ }
2662
+ };
2663
+ //#endregion
2664
+ //#region src/repository-setup.ts
2665
+ const MAX_OUTPUT_BYTES = 131072;
2666
+ const GIT_TIMEOUT_MS = 1e4;
2667
+ const GIT_FETCH_TIMEOUT_MS = 6e4;
2668
+ const MAX_PATH_CHARS = 4096;
2669
+ const MAX_BRANCH_CHARS = 512;
2670
+ const LEASE_SCHEMA_VERSION = 1;
2671
+ var RepositorySetupError = class extends Error {
2672
+ code;
2673
+ constructor(code, message) {
2674
+ super(message);
2675
+ this.name = "RepositorySetupError";
2676
+ this.code = code;
2677
+ }
2678
+ };
2679
+ async function collect(handle) {
2680
+ const outcome = await handle.done;
2681
+ const stdout = handle.collected.stdout?.readFrom(0);
2682
+ const stderr = handle.collected.stderr?.readFrom(0);
2683
+ return {
2684
+ exitCode: outcome.exitCode,
2685
+ stdout: stdout?.text ?? "",
2686
+ stderr: stderr?.text ?? "",
2687
+ lossy: stdout?.lossy === true || stderr?.lossy === true
2688
+ };
2689
+ }
2690
+ function safePath(value) {
2691
+ if (value.length === 0 || value.length > MAX_PATH_CHARS || !isAbsolute(value) || value.includes("\0")) throw new RepositorySetupError("invalid-path", "The workspace path is invalid.");
2692
+ return resolve(value);
2693
+ }
2694
+ function safeBranch(value) {
2695
+ if (value.length === 0 || value.length > MAX_BRANCH_CHARS || value.trim() !== value || /[\0\r\n]/u.test(value)) throw new RepositorySetupError("invalid-branch", "The branch name is invalid.");
2696
+ return value;
2697
+ }
2698
+ function parseCurrentBranch(value) {
2699
+ const branch = value.trim();
2700
+ return branch.length === 0 || branch === "HEAD" ? void 0 : branch;
2701
+ }
2702
+ function parseWorktreeBranches(value) {
2703
+ const occupied = /* @__PURE__ */ new Map();
2704
+ let path;
2705
+ for (const line of `${value}\n`.split(/\r?\n/u)) if (line.startsWith("worktree ")) path = line.slice(9);
2706
+ else if (line.startsWith("branch refs/heads/") && path !== void 0) occupied.set(line.slice(18), path);
2707
+ else if (line.length === 0) path = void 0;
2708
+ return occupied;
2709
+ }
2710
+ function slug(value, fallback) {
2711
+ return value.toLocaleLowerCase("en-US").replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48) || fallback;
2712
+ }
2713
+ function lease(value) {
2714
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
2715
+ const input = value;
2716
+ if (typeof input.id !== "string" || typeof input.root !== "string" || typeof input.path !== "string" || typeof input.branch !== "string" || typeof input.createdAt !== "string" || input.pluginGeneratedBranch !== void 0 && typeof input.pluginGeneratedBranch !== "boolean" || input.sessionId !== void 0 && typeof input.sessionId !== "string") return void 0;
2717
+ return {
2718
+ id: input.id,
2719
+ root: input.root,
2720
+ path: input.path,
2721
+ branch: input.branch,
2722
+ createdAt: input.createdAt,
2723
+ pluginGeneratedBranch: input.pluginGeneratedBranch === true,
2724
+ ...input.sessionId === void 0 ? {} : { sessionId: input.sessionId }
2725
+ };
2726
+ }
2727
+ var RepositorySetupService = class {
2728
+ #runtime;
2729
+ #leasePath;
2730
+ #worktreeRoot;
2731
+ #branchPrefix;
2732
+ #gitPath;
2733
+ #pending = Promise.resolve();
2734
+ constructor(runtime, options = {}) {
2735
+ this.#runtime = runtime;
2736
+ this.#leasePath = options.leasePath ?? dshHomePath("plugins", "dsh-claude", "worktrees.json");
2737
+ this.#worktreeRoot = options.worktreeRoot ?? dshHomePath("plugins", "dsh-claude", "worktrees");
2738
+ this.#branchPrefix = options.branchPrefix ?? (async () => "claude");
2739
+ }
2740
+ async listBranches(cwd) {
2741
+ const git = await this.#git();
2742
+ const root = await this.#repositoryRoot(git, safePath(cwd));
2743
+ const [status, refs, remoteRefs] = await Promise.all([
2744
+ this.#run(git, [
2745
+ "status",
2746
+ "--porcelain=v2",
2747
+ "--branch",
2748
+ "--untracked-files=normal"
2749
+ ], root),
2750
+ this.#run(git, [
2751
+ "for-each-ref",
2752
+ "--format=%(refname:short)",
2753
+ "refs/heads"
2754
+ ], root),
2755
+ this.#run(git, [
2756
+ "for-each-ref",
2757
+ "--format=%(refname:short) %(symref)",
2758
+ "refs/remotes"
2759
+ ], root)
2760
+ ]);
2761
+ if (status.exitCode !== 0 || status.lossy || refs.exitCode !== 0 || refs.lossy || remoteRefs.exitCode !== 0 || remoteRefs.lossy) throw new RepositorySetupError("repository-unavailable", "The repository state is unavailable.");
2762
+ const current = status.stdout.split(/\r?\n/u).find((line) => line.startsWith("# branch.head "))?.slice(14);
2763
+ const branches = refs.stdout.split(/\r?\n/u).filter(Boolean).sort((left, right) => left.localeCompare(right));
2764
+ const remoteBranches = remoteRefs.stdout.split(/\r?\n/u).map((line) => line.trim().split(/\s+/u)).filter((parts) => parts[0] !== void 0 && parts[0].length > 0 && parts.length === 1).map((parts) => parts[0]).sort((left, right) => left.localeCompare(right));
2765
+ const currentBranch = current === void 0 ? void 0 : parseCurrentBranch(current);
2766
+ return {
2767
+ root,
2768
+ ...currentBranch === void 0 ? {} : { current: currentBranch },
2769
+ dirty: status.stdout.split(/\r?\n/u).some((line) => line.length > 0 && !line.startsWith("# ")),
2770
+ branches,
2771
+ remoteBranches
2772
+ };
2773
+ }
2774
+ async setup(cwd, branchValue, useWorktree, explicitBranchName, progress = () => {}) {
2775
+ progress("inspecting");
2776
+ const branch = safeBranch(branchValue);
2777
+ const info = await this.listBranches(cwd);
2778
+ const local = info.branches.includes(branch);
2779
+ const remote = info.remoteBranches.includes(branch);
2780
+ if (!local && !remote) throw new RepositorySetupError("branch-not-found", "The selected local or remote-tracking branch does not exist.");
2781
+ const requestedBranch = explicitBranchName === void 0 ? void 0 : safeBranch(explicitBranchName);
2782
+ if (useWorktree) return this.#createWorktree(info.root, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, progress);
2783
+ progress("switching-branch");
2784
+ return !local && remote ? this.#checkoutRemote(info, branch) : this.#checkout(info, branch);
2785
+ }
2786
+ bindLease(leaseId, sessionId) {
2787
+ if (leaseId.length === 0 || leaseId.length > 128 || sessionId.length === 0 || sessionId.length > 1024) return Promise.reject(new RepositorySetupError("invalid-lease", "The worktree lease is invalid."));
2788
+ return this.#serialize(async () => {
2789
+ const leases = await this.#readLeases();
2790
+ const index = leases.findIndex((item) => item.id === leaseId);
2791
+ if (index < 0) throw new RepositorySetupError("lease-not-found", "The worktree lease no longer exists.");
2792
+ const existing = leases[index];
2793
+ if (existing === void 0) throw new RepositorySetupError("lease-not-found", "The worktree lease no longer exists.");
2794
+ leases[index] = {
2795
+ ...existing,
2796
+ sessionId
2797
+ };
2798
+ await this.#writeLeases(leases);
2799
+ });
2800
+ }
2801
+ cleanupSession(sessionId) {
2802
+ return this.#serialize(async () => {
2803
+ const leases = await this.#readLeases();
2804
+ const retained = [];
2805
+ let changed = false;
2806
+ const git = await this.#git();
2807
+ for (const item of leases) {
2808
+ if (item.sessionId !== sessionId) {
2809
+ retained.push(item);
2810
+ continue;
2811
+ }
2812
+ try {
2813
+ const status = await this.#run(git, [
2814
+ "status",
2815
+ "--porcelain=v1",
2816
+ "--untracked-files=normal"
2817
+ ], item.path);
2818
+ if (status.exitCode !== 0 || status.lossy || status.stdout.trim().length > 0) {
2819
+ retained.push(item);
2820
+ continue;
2821
+ }
2822
+ if ((await this.#run(git, [
2823
+ "worktree",
2824
+ "remove",
2825
+ "--",
2826
+ item.path
2827
+ ], item.root)).exitCode !== 0) {
2828
+ retained.push(item);
2829
+ continue;
2830
+ }
2831
+ if (item.pluginGeneratedBranch) await this.#run(git, [
2832
+ "branch",
2833
+ "-D",
2834
+ "--",
2835
+ item.branch
2836
+ ], item.root).catch(() => void 0);
2837
+ changed = true;
2838
+ } catch {
2839
+ retained.push(item);
2840
+ }
2841
+ }
2842
+ if (changed) await this.#writeLeases(retained);
2843
+ });
2844
+ }
2845
+ async #checkoutRemote(info, remoteBranch) {
2846
+ const separator = remoteBranch.indexOf("/");
2847
+ if (separator <= 0 || separator === remoteBranch.length - 1) throw new RepositorySetupError("invalid-branch", "The remote-tracking branch name is invalid.");
2848
+ const localBranch = safeBranch(remoteBranch.slice(separator + 1));
2849
+ if (info.branches.includes(localBranch)) {
2850
+ const git = await this.#git();
2851
+ const upstream = await this.#run(git, [
2852
+ "for-each-ref",
2853
+ "--format=%(upstream:short)",
2854
+ `refs/heads/${localBranch}`
2855
+ ], info.root);
2856
+ if (upstream.exitCode !== 0 || upstream.lossy) throw new RepositorySetupError("repository-unavailable", "The local branch tracking state is unavailable.");
2857
+ if (upstream.stdout.trim() !== remoteBranch) throw new RepositorySetupError("branch-tracking-conflict", `The local branch ${localBranch} does not track ${remoteBranch}.`);
2858
+ return this.#checkout(info, localBranch);
2859
+ }
2860
+ if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
2861
+ const git = await this.#git();
2862
+ if ((await this.#run(git, [
2863
+ "switch",
2864
+ "--track",
2865
+ "-c",
2866
+ localBranch,
2867
+ `refs/remotes/${remoteBranch}`
2868
+ ], info.root)).exitCode !== 0) throw new RepositorySetupError("checkout-failed", "Git could not create the local tracking branch.");
2869
+ return {
2870
+ mode: "checkout",
2871
+ root: info.root,
2872
+ path: info.root,
2873
+ branch: localBranch
2874
+ };
2875
+ }
2876
+ async #checkout(info, branch) {
2877
+ if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
2878
+ if (info.current !== branch) {
2879
+ const git = await this.#git();
2880
+ const worktrees = await this.#run(git, [
2881
+ "worktree",
2882
+ "list",
2883
+ "--porcelain"
2884
+ ], info.root);
2885
+ if (worktrees.exitCode !== 0 || worktrees.lossy) throw new RepositorySetupError("repository-unavailable", "Worktree state is unavailable.");
2886
+ const occupiedPath = parseWorktreeBranches(worktrees.stdout).get(branch);
2887
+ if (occupiedPath !== void 0 && resolve(occupiedPath) !== info.root) throw new RepositorySetupError("branch-occupied", "The selected branch is already checked out in another worktree.");
2888
+ if ((await this.#run(git, [
2889
+ "switch",
2890
+ "--",
2891
+ branch
2892
+ ], info.root)).exitCode !== 0) throw new RepositorySetupError("checkout-failed", "Git could not switch to the selected branch.");
2893
+ }
2894
+ return {
2895
+ mode: "checkout",
2896
+ root: info.root,
2897
+ path: info.root,
2898
+ branch
2899
+ };
2900
+ }
2901
+ async #createWorktree(root, baseBranch, baseRef, explicitBranchName, progress) {
2902
+ const git = await this.#git();
2903
+ progress("fetching");
2904
+ const fetched = await this.#run(git, [
2905
+ "-c",
2906
+ "credential.interactive=never",
2907
+ "fetch",
2908
+ "--all",
2909
+ "--prune"
2910
+ ], root, GIT_FETCH_TIMEOUT_MS);
2911
+ if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references before creating the worktree.");
2912
+ const suffix = randomUUID().slice(0, 8);
2913
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
2914
+ const branch = explicitBranchName ?? `${safeBranch(await this.#branchPrefix())}/${slug(baseBranch, "branch")}-${stamp}-${suffix}`;
2915
+ const path = join(this.#worktreeRoot, `${slug(basename(root), "repository")}-${stamp}-${suffix}`);
2916
+ progress("creating-worktree");
2917
+ await mkdir(this.#worktreeRoot, { recursive: true });
2918
+ if ((await this.#run(git, [
2919
+ "worktree",
2920
+ "add",
2921
+ "-b",
2922
+ branch,
2923
+ path,
2924
+ baseRef
2925
+ ], root)).exitCode !== 0) throw new RepositorySetupError("worktree-failed", "Git could not create the worktree.");
2926
+ const item = {
2927
+ id: randomUUID(),
2928
+ root,
2929
+ path,
2930
+ branch,
2931
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2932
+ pluginGeneratedBranch: explicitBranchName === void 0
2933
+ };
2934
+ progress("saving-worktree");
2935
+ try {
2936
+ await this.#serialize(async () => {
2937
+ const leases = await this.#readLeases();
2938
+ await this.#writeLeases([...leases, item]);
2939
+ });
2940
+ } catch (error) {
2941
+ await this.#run(git, [
2942
+ "worktree",
2943
+ "remove",
2944
+ "--force",
2945
+ "--",
2946
+ path
2947
+ ], root).catch(() => void 0);
2948
+ throw error;
2949
+ }
2950
+ return {
2951
+ mode: "worktree",
2952
+ root,
2953
+ path,
2954
+ branch,
2955
+ leaseId: item.id
2956
+ };
2957
+ }
2958
+ async #repositoryRoot(git, cwd) {
2959
+ const result = await this.#run(git, [
2960
+ "rev-parse",
2961
+ "--path-format=absolute",
2962
+ "--show-toplevel"
2963
+ ], cwd);
2964
+ const root = result.stdout.trim();
2965
+ if (result.exitCode !== 0 || result.lossy || root.length === 0 || !isAbsolute(root)) throw new RepositorySetupError("not-repository", "The workspace is not a Git repository.");
2966
+ return resolve(root);
2967
+ }
2968
+ #git() {
2969
+ this.#gitPath ??= this.#runtime.resolveExecutable("git");
2970
+ return this.#gitPath;
2971
+ }
2972
+ async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
2973
+ return collect(this.#runtime.spawn({
2974
+ argv: [executable, ...args],
2975
+ cwd,
2976
+ stdio: {
2977
+ stdin: "ignore",
2978
+ stdout: { maxBytes: MAX_OUTPUT_BYTES },
2979
+ stderr: { maxBytes: MAX_OUTPUT_BYTES }
2980
+ },
2981
+ graceMs: 1e3,
2982
+ signal: AbortSignal.timeout(timeoutMs),
2983
+ env: {}
2984
+ }));
2985
+ }
2986
+ async #readLeases() {
2987
+ try {
2988
+ const parsed = JSON.parse(await readFile(this.#leasePath, "utf8"));
2989
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return [];
2990
+ const input = parsed;
2991
+ if (input.schemaVersion !== LEASE_SCHEMA_VERSION || !Array.isArray(input.leases)) return [];
2992
+ return input.leases.map(lease).filter((item) => item !== void 0);
2993
+ } catch (error) {
2994
+ if (error.code === "ENOENT") return [];
2995
+ throw error;
2996
+ }
2997
+ }
2998
+ async #writeLeases(leases) {
2999
+ await mkdir(resolve(this.#leasePath, ".."), { recursive: true });
3000
+ const temporary = `${this.#leasePath}.${process.pid}.${randomUUID()}.tmp`;
3001
+ const document = {
3002
+ schemaVersion: LEASE_SCHEMA_VERSION,
3003
+ leases
3004
+ };
3005
+ try {
3006
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 384 });
3007
+ await rename(temporary, this.#leasePath);
3008
+ } finally {
3009
+ await rm(temporary, { force: true }).catch(() => void 0);
3010
+ }
3011
+ }
3012
+ #serialize(operation) {
3013
+ const result = this.#pending.then(operation, operation);
3014
+ this.#pending = result.then(() => void 0, () => void 0);
3015
+ return result;
3016
+ }
3017
+ };
3018
+ //#endregion
3019
+ //#region src/repository-setup-routes.ts
3020
+ const MAX_BODY_BYTES = 16384;
3021
+ function record(value) {
3022
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3023
+ }
3024
+ async function readJson(req) {
3025
+ const chunks = [];
3026
+ let size = 0;
3027
+ for await (const chunk of req) {
3028
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3029
+ size += buffer.length;
3030
+ if (size > MAX_BODY_BYTES) throw new RepositorySetupError("body-too-large", "The request body is too large.");
3031
+ chunks.push(buffer);
3032
+ }
3033
+ const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3034
+ if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
3035
+ return value;
3036
+ }
3037
+ function string(input, key) {
3038
+ const value = input[key];
3039
+ if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field is required.`);
3040
+ return value;
3041
+ }
3042
+ function optionalString(input, key) {
3043
+ const value = input[key];
3044
+ if (value === void 0) return void 0;
3045
+ if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field must be a string.`);
3046
+ return value;
3047
+ }
3048
+ function ndjsonHeaders(res) {
3049
+ res.writeHead(200, {
3050
+ "content-type": "application/x-ndjson; charset=utf-8",
3051
+ "cache-control": "no-store",
3052
+ "x-content-type-options": "nosniff"
3053
+ });
3054
+ res.flushHeaders?.();
3055
+ }
3056
+ function ndjson(res, value) {
3057
+ res.write(`${JSON.stringify(value)}\n`);
3058
+ }
3059
+ async function streamSetup(res, service, input) {
3060
+ ndjsonHeaders(res);
3061
+ let ended = false;
3062
+ try {
3063
+ ndjson(res, {
3064
+ type: "complete",
3065
+ result: await service.setup(string(input, "cwd"), string(input, "branch"), input.worktree, optionalString(input, "branchName"), (stage) => {
3066
+ if (!ended) ndjson(res, {
3067
+ type: "progress",
3068
+ stage
3069
+ });
3070
+ })
3071
+ });
3072
+ } catch (error) {
3073
+ const setupError = error instanceof RepositorySetupError ? error : void 0;
3074
+ ndjson(res, {
3075
+ type: "error",
3076
+ code: setupError?.code ?? "repository-setup-unavailable",
3077
+ message: setupError?.message ?? "Repository setup is unavailable."
3078
+ });
3079
+ } finally {
3080
+ ended = true;
3081
+ res.end();
3082
+ }
3083
+ }
3084
+ /** Register trusted browser routes for safe pre-session Git setup. */
3085
+ function registerRepositorySetupRoute(ctx, service) {
3086
+ ctx.effect(() => ctx.webServer.register({
3087
+ kind: "prefix",
3088
+ path: CLAUDE_REPOSITORY_SETUP_PATH,
3089
+ handler: async (req, res) => {
3090
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
3091
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
3092
+ try {
3093
+ if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
3094
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
3095
+ const cwd = new URL(req.url ?? "/", "http://localhost").searchParams.get("cwd");
3096
+ if (cwd === null) throw new RepositorySetupError("invalid-request", "The cwd query parameter is required.");
3097
+ return json(res, 200, await service.listBranches(cwd));
3098
+ }
3099
+ if (pathname === "/plugins/dsh-claude/repository/setup") {
3100
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3101
+ const input = await readJson(req);
3102
+ if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
3103
+ await streamSetup(res, service, input);
3104
+ return;
3105
+ }
3106
+ if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
3107
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3108
+ const input = await readJson(req);
3109
+ await service.bindLease(string(input, "leaseId"), string(input, "sessionId"));
3110
+ return json(res, 200, { ok: true });
3111
+ }
3112
+ return json(res, 404, { error: "not found" });
3113
+ } catch (error) {
3114
+ if (error instanceof RepositorySetupError) return json(res, 409, {
3115
+ error: error.code,
3116
+ message: error.message
3117
+ });
3118
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid json" });
3119
+ return json(res, 500, { error: "repository setup unavailable" });
3120
+ }
3121
+ }
3122
+ }), "dsh-claude: repository setup route");
3123
+ }
3124
+ //#endregion
2334
3125
  //#region src/update-routes.ts
2335
3126
  const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
2336
3127
  const UPDATE_TIMEOUT_MS = 3e4;
@@ -2604,11 +3395,14 @@ const BUILTIN_OUTPUT_STYLES = [
2604
3395
  "Learning"
2605
3396
  ];
2606
3397
  const STYLE_NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._()\[\]-]{0,127}$/u;
3398
+ const DEFAULT_WORKTREE_BRANCH_PREFIX = "claude";
3399
+ const MAX_BRANCH_PREFIX_CHARS = 128;
2607
3400
  function pathsFor(deps) {
2608
3401
  const root = join(homedir(), ".claude");
2609
3402
  return {
2610
3403
  settingsFile: deps.paths?.settingsFile ?? join(root, "settings.json"),
2611
- outputStylesDir: deps.paths?.outputStylesDir ?? join(root, "output-styles")
3404
+ outputStylesDir: deps.paths?.outputStylesDir ?? join(root, "output-styles"),
3405
+ pluginSettingsFile: deps.paths?.pluginSettingsFile ?? dshHomePath("plugins", "dsh-claude", "settings.json")
2612
3406
  };
2613
3407
  }
2614
3408
  function object(value) {
@@ -2664,9 +3458,10 @@ async function userOutputStyleOptions(directory) {
2664
3458
  }
2665
3459
  return options;
2666
3460
  }
2667
- const DESCRIPTORS = [{
3461
+ const OUTPUT_STYLE = {
2668
3462
  key: "outputStyle",
2669
3463
  kind: "select",
3464
+ document: "claude",
2670
3465
  effect: "new-session",
2671
3466
  async options(paths) {
2672
3467
  const builtIn = BUILTIN_OUTPUT_STYLES.map((value) => ({
@@ -2687,11 +3482,42 @@ const DESCRIPTORS = [{
2687
3482
  if (value === "Default") delete document.outputStyle;
2688
3483
  else document.outputStyle = value;
2689
3484
  }
2690
- }];
3485
+ };
3486
+ function isValidWorktreeBranchPrefix(value) {
3487
+ return value.length > 0 && value.length <= MAX_BRANCH_PREFIX_CHARS && value.trim() === value && !value.startsWith("/") && !value.endsWith("/") && !value.includes("//") && !value.includes("..") && !value.includes("@{") && !/[\0-\x20\x7f~^:?*[\\]/u.test(value) && value.split("/").every((segment) => segment.length > 0 && !segment.startsWith(".") && !segment.endsWith(".lock"));
3488
+ }
3489
+ const WORKTREE_BRANCH_PREFIX = {
3490
+ key: "worktreeBranchPrefix",
3491
+ kind: "text",
3492
+ document: "plugin",
3493
+ effect: "next-worktree",
3494
+ maxLength: MAX_BRANCH_PREFIX_CHARS,
3495
+ read(document) {
3496
+ const value = document.worktreeBranchPrefix;
3497
+ return typeof value === "string" && isValidWorktreeBranchPrefix(value) ? value : DEFAULT_WORKTREE_BRANCH_PREFIX;
3498
+ },
3499
+ apply(document, value) {
3500
+ if (typeof value !== "string" || !isValidWorktreeBranchPrefix(value)) throw new Error("Invalid value for global setting worktreeBranchPrefix");
3501
+ if (value === DEFAULT_WORKTREE_BRANCH_PREFIX) delete document.worktreeBranchPrefix;
3502
+ else document.worktreeBranchPrefix = value;
3503
+ }
3504
+ };
3505
+ const DESCRIPTORS = [OUTPUT_STYLE, WORKTREE_BRANCH_PREFIX];
2691
3506
  const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
2692
3507
  let pendingWrite = Promise.resolve();
2693
- async function views(document, paths) {
3508
+ function documentFor(descriptor, documents) {
3509
+ return documents[descriptor.document];
3510
+ }
3511
+ async function views(documents, paths) {
2694
3512
  return { settings: await Promise.all(DESCRIPTORS.map(async (descriptor) => {
3513
+ const document = documentFor(descriptor, documents);
3514
+ if (descriptor.kind === "text") return {
3515
+ key: descriptor.key,
3516
+ kind: descriptor.kind,
3517
+ value: descriptor.read(document),
3518
+ maxLength: descriptor.maxLength,
3519
+ effect: descriptor.effect
3520
+ };
2695
3521
  const discovered = await descriptor.options(paths);
2696
3522
  const value = descriptor.read(document, discovered);
2697
3523
  const options = discovered.some((option) => option.value === value) ? discovered : [...discovered, {
@@ -2708,9 +3534,20 @@ async function views(document, paths) {
2708
3534
  };
2709
3535
  })) };
2710
3536
  }
3537
+ async function readDocuments(paths) {
3538
+ const [claude, plugin] = await Promise.all([readDocument(paths.settingsFile), readDocument(paths.pluginSettingsFile)]);
3539
+ return {
3540
+ claude,
3541
+ plugin
3542
+ };
3543
+ }
2711
3544
  async function readGlobalSettings(deps = {}) {
2712
3545
  const paths = pathsFor(deps);
2713
- return views(await readDocument(paths.settingsFile), paths);
3546
+ return views(await readDocuments(paths), paths);
3547
+ }
3548
+ async function readWorktreeBranchPrefix(deps = {}) {
3549
+ const paths = pathsFor(deps);
3550
+ return WORKTREE_BRANCH_PREFIX.read(await readDocument(paths.pluginSettingsFile));
2714
3551
  }
2715
3552
  async function atomicWrite(path, document) {
2716
3553
  await mkdir(dirname(path), {
@@ -2737,13 +3574,18 @@ function updateGlobalSettings(changes, deps = {}) {
2737
3574
  for (const key of Object.keys(changeObject)) if (!DESCRIPTOR_BY_KEY.has(key)) return Promise.reject(/* @__PURE__ */ new Error(`Unsupported global setting: ${key}`));
2738
3575
  const paths = pathsFor(deps);
2739
3576
  const operation = pendingWrite.catch(() => void 0).then(async () => {
2740
- const document = await readDocument(paths.settingsFile);
3577
+ const documents = await readDocuments(paths);
3578
+ const changedDocuments = /* @__PURE__ */ new Set();
2741
3579
  for (const [key, value] of Object.entries(changeObject)) {
2742
3580
  const descriptor = DESCRIPTOR_BY_KEY.get(key);
2743
- descriptor.apply(document, value, await descriptor.options(paths));
3581
+ const document = documentFor(descriptor, documents);
3582
+ if (descriptor.kind === "select") descriptor.apply(document, value, await descriptor.options(paths));
3583
+ else descriptor.apply(document, value);
3584
+ changedDocuments.add(descriptor.document);
2744
3585
  }
2745
- await atomicWrite(paths.settingsFile, document);
2746
- return views(document, paths);
3586
+ if (changedDocuments.has("claude")) await atomicWrite(paths.settingsFile, documents.claude);
3587
+ if (changedDocuments.has("plugin")) await atomicWrite(paths.pluginSettingsFile, documents.plugin);
3588
+ return views(documents, paths);
2747
3589
  });
2748
3590
  pendingWrite = operation;
2749
3591
  return operation;
@@ -2908,6 +3750,8 @@ async function apply(ctx, config) {
2908
3750
  maxProcesses: config.maxProcesses ?? 4
2909
3751
  };
2910
3752
  const sidecar = new ClaudeSidecarRepository();
3753
+ const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
3754
+ const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
2911
3755
  const commandCatalogs = /* @__PURE__ */ new Map();
2912
3756
  const supervisor = new ClaudeSupervisor({
2913
3757
  runtime: ctx.subprocess,
@@ -2982,17 +3826,25 @@ async function apply(ctx, config) {
2982
3826
  }
2983
3827
  ctx.on("agent/disposed", async ({ agent }) => {
2984
3828
  await supervisor.disposeSession(agent.id);
3829
+ await repositorySetup.cleanupSession(agent.id);
2985
3830
  });
2986
3831
  ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
3832
+ ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
2987
3833
  ctx.inject(["webServer"], (webCtx) => {
2988
3834
  registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
2989
3835
  const desktopActions = webCtx.get("desktopActions");
2990
3836
  registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
2991
3837
  registerClaudeGlobalSettingsRoute(webCtx);
3838
+ registerRepositorySetupRoute(webCtx, repositorySetup);
2992
3839
  registerClaudeProjectionRoute(webCtx, sidecar, (sessionId) => {
2993
3840
  const agent = webCtx.agents.get(sessionId);
2994
3841
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
2995
- }, (sessionId) => commandCatalogs.get(sessionId) ?? []);
3842
+ }, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
3843
+ const agent = webCtx.agents.get(sessionId);
3844
+ if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
3845
+ const cwd = agent.session.header.cwd;
3846
+ return cwd === void 0 ? void 0 : repositoryStatus.inspect(cwd);
3847
+ });
2996
3848
  });
2997
3849
  }
2998
3850
  //#endregion