@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/INSTALL.md +55 -55
- package/LICENSE +21 -21
- package/README.md +191 -191
- package/cordis.patch.yml +12 -12
- package/legacy-preset/agent.cordis.yml +11 -11
- package/legacy-preset/preset.yml +4 -4
- package/lib/bin.mjs +1 -1
- package/lib/bin.mjs.map +1 -1
- package/lib/client.d.ts +70 -2
- package/lib/client.js +2620 -412
- package/lib/client.js.map +1 -1
- package/lib/{events-6xTApIQw.mjs → events-DPJaBReT.mjs} +3 -2
- package/lib/events-DPJaBReT.mjs.map +1 -0
- package/lib/index.mjs +934 -82
- package/lib/index.mjs.map +1 -1
- package/lib/{presenters-BWXp9d3p.mjs → presenters-DBRIOgJs.mjs} +2 -2
- package/lib/presenters-DBRIOgJs.mjs.map +1 -0
- package/lib/{preset-installer-B0NrA4Hi.mjs → preset-installer-DnoSEKGm.mjs} +2 -2
- package/lib/preset-installer-DnoSEKGm.mjs.map +1 -0
- package/lib/preset-route.mjs +2 -2
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +181 -134
- package/preset/claude/agent.cordis.yml +11 -11
- package/preset/claude/preset.yml +4 -4
- package/lib/events-6xTApIQw.mjs.map +0 -1
- package/lib/presenters-BWXp9d3p.mjs.map +0 -1
- package/lib/preset-installer-B0NrA4Hi.mjs.map +0 -1
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
|
|
2
|
-
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
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$
|
|
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$
|
|
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$
|
|
36
|
-
if (input === void 0 || !string$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
2746
|
-
|
|
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
|