@norman-else/dsh-claude 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +26 -1
- package/lib/client.js +1212 -218
- package/lib/client.js.map +1 -1
- package/lib/{command-bridge-C10-lz6A.mjs → command-bridge-Br25ODdm.mjs} +2 -2
- package/lib/{command-bridge-C10-lz6A.mjs.map → command-bridge-Br25ODdm.mjs.map} +1 -1
- package/lib/{events-BdDs9ebF.mjs → events-DhrAr5gh.mjs} +3 -2
- package/lib/events-DhrAr5gh.mjs.map +1 -0
- package/lib/index.mjs +511 -100
- package/lib/index.mjs.map +1 -1
- package/lib/{preset-installer-DMANIjwu.mjs → preset-installer-B_Cb0RG6.mjs} +2 -2
- package/lib/{preset-installer-DMANIjwu.mjs.map → preset-installer-B_Cb0RG6.mjs.map} +1 -1
- package/lib/preset-route.mjs +2 -2
- package/package.json +1 -1
- package/lib/events-BdDs9ebF.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { C as CLAUDE_UPDATE_PATH, S as CLAUDE_UPDATE_CHECK_PATH, T as TASK_TOOL_NAMES, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_REPOSITORY_SETUP_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_ACTION_PATH, x as CLAUDE_REVIEW_COMMENT_PATH, y as CLAUDE_REPOSITORY_FEEDBACK_PATH } from "./events-DhrAr5gh.mjs";
|
|
2
|
+
import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-Br25ODdm.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-B_Cb0RG6.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
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";
|
|
@@ -25,7 +25,7 @@ function emptyProjection() {
|
|
|
25
25
|
activities: []
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
function record$
|
|
28
|
+
function record$6(value) {
|
|
29
29
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
30
30
|
}
|
|
31
31
|
function finiteInteger(value) {
|
|
@@ -35,7 +35,7 @@ function string$3(value, max) {
|
|
|
35
35
|
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
36
36
|
}
|
|
37
37
|
function binding(value) {
|
|
38
|
-
const input = record$
|
|
38
|
+
const input = record$6(value);
|
|
39
39
|
if (input === void 0 || !string$3(input.claudeSessionId, 512) || !string$3(input.sdkVersion, 128) || !string$3(input.cwd, 4096) || input.cliVersion !== void 0 && !string$3(input.cliVersion, 128)) return void 0;
|
|
40
40
|
return {
|
|
41
41
|
claudeSessionId: input.claudeSessionId,
|
|
@@ -65,22 +65,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
|
|
|
65
65
|
"failed"
|
|
66
66
|
]);
|
|
67
67
|
function activity(value) {
|
|
68
|
-
const input = record$
|
|
68
|
+
const input = record$6(value);
|
|
69
69
|
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;
|
|
70
70
|
return normalizeActivity(input);
|
|
71
71
|
}
|
|
72
72
|
function contextUsage(value) {
|
|
73
|
-
const input = record$
|
|
73
|
+
const input = record$6(value);
|
|
74
74
|
if (input === void 0 || !Array.isArray(input.categories)) return void 0;
|
|
75
75
|
return normalizeContextUsage(input);
|
|
76
76
|
}
|
|
77
77
|
function tasks(value) {
|
|
78
|
-
const input = record$
|
|
78
|
+
const input = record$6(value);
|
|
79
79
|
if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
|
|
80
80
|
return normalizeTasksEvent(input.tasks);
|
|
81
81
|
}
|
|
82
82
|
function parseClaudeSidecar(value) {
|
|
83
|
-
const input = record$
|
|
83
|
+
const input = record$6(value);
|
|
84
84
|
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");
|
|
85
85
|
const activities = input.activities.map(activity);
|
|
86
86
|
if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
|
|
@@ -653,7 +653,7 @@ function createUserQuestionBridge(userQuestions, activeContext) {
|
|
|
653
653
|
}
|
|
654
654
|
//#endregion
|
|
655
655
|
//#region src/sdk-messages.ts
|
|
656
|
-
function record$
|
|
656
|
+
function record$5(value) {
|
|
657
657
|
return value !== null && typeof value === "object" ? value : void 0;
|
|
658
658
|
}
|
|
659
659
|
function string$2(value) {
|
|
@@ -668,7 +668,7 @@ function taskUsageOf(usage) {
|
|
|
668
668
|
return Object.keys(normalized).length === 0 ? void 0 : normalized;
|
|
669
669
|
}
|
|
670
670
|
function resultUsage(message) {
|
|
671
|
-
const usage = record$
|
|
671
|
+
const usage = record$5(message.usage);
|
|
672
672
|
const normalized = {};
|
|
673
673
|
if (usage !== void 0) {
|
|
674
674
|
if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
|
|
@@ -680,7 +680,7 @@ function resultUsage(message) {
|
|
|
680
680
|
return normalized;
|
|
681
681
|
}
|
|
682
682
|
function normalizeAssistant(message) {
|
|
683
|
-
const content = record$
|
|
683
|
+
const content = record$5(message.message)?.content;
|
|
684
684
|
if (!Array.isArray(content)) return [{
|
|
685
685
|
kind: "protocol-error",
|
|
686
686
|
title: "Malformed Claude assistant message",
|
|
@@ -689,7 +689,7 @@ function normalizeAssistant(message) {
|
|
|
689
689
|
const parentToolUseId = string$2(message.parent_tool_use_id);
|
|
690
690
|
const normalized = [];
|
|
691
691
|
for (const item of content) {
|
|
692
|
-
const block = record$
|
|
692
|
+
const block = record$5(item);
|
|
693
693
|
if (block === void 0) continue;
|
|
694
694
|
if (block.type === "text") {
|
|
695
695
|
const text = string$2(block.text);
|
|
@@ -722,7 +722,7 @@ function normalizeAssistant(message) {
|
|
|
722
722
|
}
|
|
723
723
|
function normalizeUser(message) {
|
|
724
724
|
if (message.isReplay === true) return [];
|
|
725
|
-
const content = record$
|
|
725
|
+
const content = record$5(message.message)?.content;
|
|
726
726
|
if (typeof content === "string") return [];
|
|
727
727
|
if (!Array.isArray(content)) return [{
|
|
728
728
|
kind: "protocol-error",
|
|
@@ -732,7 +732,7 @@ function normalizeUser(message) {
|
|
|
732
732
|
const parentToolUseId = string$2(message.parent_tool_use_id);
|
|
733
733
|
const normalized = [];
|
|
734
734
|
for (const item of content) {
|
|
735
|
-
const block = record$
|
|
735
|
+
const block = record$5(item);
|
|
736
736
|
if (block?.type !== "tool_result") continue;
|
|
737
737
|
const toolUseId = string$2(block.tool_use_id);
|
|
738
738
|
if (toolUseId === void 0) continue;
|
|
@@ -813,7 +813,7 @@ function normalizeSystem(message) {
|
|
|
813
813
|
const summary = string$2(message.summary);
|
|
814
814
|
const subagentType = string$2(message.subagent_type);
|
|
815
815
|
const lastToolName = string$2(message.last_tool_name);
|
|
816
|
-
const usage = taskUsageOf(record$
|
|
816
|
+
const usage = taskUsageOf(record$5(message.usage));
|
|
817
817
|
return [{
|
|
818
818
|
kind: "subagent",
|
|
819
819
|
title: summary ?? description ?? "Claude subagent update",
|
|
@@ -829,7 +829,7 @@ function normalizeSystem(message) {
|
|
|
829
829
|
}];
|
|
830
830
|
}
|
|
831
831
|
if (subtype === "task_updated") {
|
|
832
|
-
const patch = record$
|
|
832
|
+
const patch = record$5(message.patch);
|
|
833
833
|
const status = string$2(patch?.status);
|
|
834
834
|
const taskId = string$2(message.task_id);
|
|
835
835
|
const description = string$2(patch?.description);
|
|
@@ -852,7 +852,7 @@ function normalizeSystem(message) {
|
|
|
852
852
|
const taskId = string$2(message.task_id);
|
|
853
853
|
const summary = string$2(message.summary);
|
|
854
854
|
const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
|
|
855
|
-
const usage = taskUsageOf(record$
|
|
855
|
+
const usage = taskUsageOf(record$5(message.usage));
|
|
856
856
|
return [{
|
|
857
857
|
kind: "subagent",
|
|
858
858
|
title: summary ?? taskId ?? "Claude subagent finished",
|
|
@@ -867,7 +867,7 @@ function normalizeSystem(message) {
|
|
|
867
867
|
if (subtype === "background_tasks_changed") return [{
|
|
868
868
|
kind: "background-tasks",
|
|
869
869
|
tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
|
|
870
|
-
const entry = record$
|
|
870
|
+
const entry = record$5(item);
|
|
871
871
|
const taskId = string$2(entry?.task_id);
|
|
872
872
|
const description = string$2(entry?.description);
|
|
873
873
|
const taskType = string$2(entry?.task_type);
|
|
@@ -910,10 +910,10 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
|
|
|
910
910
|
function normalizeSdkMessage(message) {
|
|
911
911
|
const value = message;
|
|
912
912
|
if (value.type === "stream_event") {
|
|
913
|
-
const event = record$
|
|
913
|
+
const event = record$5(value.event);
|
|
914
914
|
const parentToolUseId = string$2(value.parent_tool_use_id);
|
|
915
915
|
if (event?.type === "content_block_delta") {
|
|
916
|
-
const delta = record$
|
|
916
|
+
const delta = record$5(event.delta);
|
|
917
917
|
if (delta?.type === "text_delta") {
|
|
918
918
|
const text = string$2(delta.text);
|
|
919
919
|
return text === void 0 ? [] : [{
|
|
@@ -948,7 +948,7 @@ function normalizeSdkMessage(message) {
|
|
|
948
948
|
const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
|
|
949
949
|
const terminalReason = string$2(value.terminal_reason);
|
|
950
950
|
const userMessageUuid = string$2(value.user_message_uuid);
|
|
951
|
-
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$
|
|
951
|
+
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$5(item)).filter((item) => item !== void 0).map((item) => {
|
|
952
952
|
const toolName = string$2(item.tool_name);
|
|
953
953
|
const toolUseId = string$2(item.tool_use_id);
|
|
954
954
|
return toolName === void 0 || toolUseId === void 0 ? void 0 : {
|
|
@@ -974,7 +974,7 @@ function normalizeSdkMessage(message) {
|
|
|
974
974
|
detail: value.error ?? value.output
|
|
975
975
|
}];
|
|
976
976
|
if (value.type === "rate_limit_event") {
|
|
977
|
-
const status = string$2(record$
|
|
977
|
+
const status = string$2(record$5(value.rate_limit_info)?.status);
|
|
978
978
|
return [{
|
|
979
979
|
kind: "status",
|
|
980
980
|
title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
|
|
@@ -2510,7 +2510,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
2510
2510
|
}
|
|
2511
2511
|
//#endregion
|
|
2512
2512
|
//#region src/projection-routes.ts
|
|
2513
|
-
const MAX_SESSION_ID_CHARS$
|
|
2513
|
+
const MAX_SESSION_ID_CHARS$3 = 1024;
|
|
2514
2514
|
/** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
|
|
2515
2515
|
* the transcript hot path so git/gh latency never delays visible text. */
|
|
2516
2516
|
const META_REFRESH_MS = 5e3;
|
|
@@ -2524,7 +2524,7 @@ function targetFromUrl(rawUrl) {
|
|
|
2524
2524
|
if (stream) encoded = encoded.slice(0, -7);
|
|
2525
2525
|
if (encoded.length === 0 || encoded.includes("/")) return void 0;
|
|
2526
2526
|
const sessionId = decodeURIComponent(encoded);
|
|
2527
|
-
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$
|
|
2527
|
+
if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$3) return void 0;
|
|
2528
2528
|
return {
|
|
2529
2529
|
sessionId,
|
|
2530
2530
|
stream
|
|
@@ -2684,17 +2684,17 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
2684
2684
|
}
|
|
2685
2685
|
//#endregion
|
|
2686
2686
|
//#region src/repository-status.ts
|
|
2687
|
-
const MAX_OUTPUT_BYTES$
|
|
2687
|
+
const MAX_OUTPUT_BYTES$3 = 65536;
|
|
2688
2688
|
const MAX_DIFF_BYTES = 262144;
|
|
2689
2689
|
const MAX_UNTRACKED_DIFFS = 50;
|
|
2690
|
-
const GIT_TIMEOUT_MS$
|
|
2691
|
-
const GH_TIMEOUT_MS = 8e3;
|
|
2690
|
+
const GIT_TIMEOUT_MS$3 = 5e3;
|
|
2691
|
+
const GH_TIMEOUT_MS$1 = 8e3;
|
|
2692
2692
|
const CACHE_TTL_MS = 5e3;
|
|
2693
2693
|
const MAX_TEXT_CHARS = 1024;
|
|
2694
2694
|
function bounded(value) {
|
|
2695
2695
|
return value.trim().slice(0, MAX_TEXT_CHARS);
|
|
2696
2696
|
}
|
|
2697
|
-
async function collect$
|
|
2697
|
+
async function collect$3(handle) {
|
|
2698
2698
|
const outcome = await handle.done;
|
|
2699
2699
|
const stdout = handle.collected.stdout?.readFrom(0);
|
|
2700
2700
|
return {
|
|
@@ -2703,15 +2703,15 @@ async function collect$2(handle) {
|
|
|
2703
2703
|
lossy: stdout?.lossy === true
|
|
2704
2704
|
};
|
|
2705
2705
|
}
|
|
2706
|
-
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$
|
|
2706
|
+
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$3) {
|
|
2707
2707
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
2708
|
-
return collect$
|
|
2708
|
+
return collect$3(runtime.spawn({
|
|
2709
2709
|
argv: [executable, ...args],
|
|
2710
2710
|
cwd,
|
|
2711
2711
|
stdio: {
|
|
2712
2712
|
stdin: "ignore",
|
|
2713
2713
|
stdout: { maxBytes },
|
|
2714
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
2714
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$3 }
|
|
2715
2715
|
},
|
|
2716
2716
|
graceMs: 1e3,
|
|
2717
2717
|
signal,
|
|
@@ -2781,14 +2781,14 @@ function parseGitHubRemote(value) {
|
|
|
2781
2781
|
if (match?.[1] === void 0 || match[2] === void 0) return void 0;
|
|
2782
2782
|
return `${match[1]}/${match[2]}`;
|
|
2783
2783
|
}
|
|
2784
|
-
function record$
|
|
2784
|
+
function record$4(value) {
|
|
2785
2785
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2786
2786
|
}
|
|
2787
2787
|
function aggregateChecks(value) {
|
|
2788
2788
|
if (!Array.isArray(value) || value.length === 0) return "none";
|
|
2789
2789
|
let pending = false;
|
|
2790
2790
|
for (const item of value) {
|
|
2791
|
-
const check = record$
|
|
2791
|
+
const check = record$4(item);
|
|
2792
2792
|
if (check === void 0) continue;
|
|
2793
2793
|
const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
|
|
2794
2794
|
const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
|
|
@@ -2812,7 +2812,7 @@ function reviewState(value) {
|
|
|
2812
2812
|
return "none";
|
|
2813
2813
|
}
|
|
2814
2814
|
function parsePullRequest(value) {
|
|
2815
|
-
const input = record$
|
|
2815
|
+
const input = record$4(value);
|
|
2816
2816
|
if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
|
|
2817
2817
|
let url;
|
|
2818
2818
|
try {
|
|
@@ -2833,7 +2833,7 @@ function parsePullRequest(value) {
|
|
|
2833
2833
|
review: reviewState(input.reviewDecision),
|
|
2834
2834
|
checks: aggregateChecks(input.statusCheckRollup),
|
|
2835
2835
|
...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
|
|
2836
|
-
...typeof record$
|
|
2836
|
+
...typeof record$4(input.author)?.login === "string" ? { author: bounded(String(record$4(input.author)?.login)) } : {},
|
|
2837
2837
|
...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
|
|
2838
2838
|
...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
|
|
2839
2839
|
...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
|
|
@@ -2906,7 +2906,7 @@ var RepositoryStatusService = class {
|
|
|
2906
2906
|
"--show-toplevel",
|
|
2907
2907
|
"--absolute-git-dir",
|
|
2908
2908
|
"--git-common-dir"
|
|
2909
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2909
|
+
], cwd, GIT_TIMEOUT_MS$3);
|
|
2910
2910
|
if (paths.exitCode !== 0) return {
|
|
2911
2911
|
status: "not-repository",
|
|
2912
2912
|
cwd: safeCwd
|
|
@@ -2924,7 +2924,7 @@ var RepositoryStatusService = class {
|
|
|
2924
2924
|
"--porcelain=v2",
|
|
2925
2925
|
"--branch",
|
|
2926
2926
|
"--untracked-files=normal"
|
|
2927
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2927
|
+
], cwd, GIT_TIMEOUT_MS$3);
|
|
2928
2928
|
if (statusResult.exitCode !== 0) return {
|
|
2929
2929
|
status: "unavailable",
|
|
2930
2930
|
cwd: safeCwd
|
|
@@ -2934,7 +2934,7 @@ var RepositoryStatusService = class {
|
|
|
2934
2934
|
"remote",
|
|
2935
2935
|
"get-url",
|
|
2936
2936
|
"origin"
|
|
2937
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2937
|
+
], cwd, GIT_TIMEOUT_MS$3);
|
|
2938
2938
|
const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
|
|
2939
2939
|
const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
|
|
2940
2940
|
const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
|
|
@@ -2944,6 +2944,7 @@ var RepositoryStatusService = class {
|
|
|
2944
2944
|
files: 0,
|
|
2945
2945
|
truncated: false
|
|
2946
2946
|
};
|
|
2947
|
+
const baseBehind = pullRequest?.state === "open" && pullRequest.baseBranch !== void 0 ? await this.#baseBehind(cwd, git, pullRequest.baseBranch) : void 0;
|
|
2947
2948
|
return {
|
|
2948
2949
|
status: "ready",
|
|
2949
2950
|
cwd: safeCwd,
|
|
@@ -2952,7 +2953,8 @@ var RepositoryStatusService = class {
|
|
|
2952
2953
|
worktree: normalizedPath(gitDir) !== normalizedPath(commonDir),
|
|
2953
2954
|
...remote === void 0 ? {} : { remote },
|
|
2954
2955
|
...pullRequest === void 0 ? {} : { pullRequest },
|
|
2955
|
-
...diff === void 0 ? {} : { diff }
|
|
2956
|
+
...diff === void 0 ? {} : { diff },
|
|
2957
|
+
...baseBehind === void 0 ? {} : { baseBehind }
|
|
2956
2958
|
};
|
|
2957
2959
|
} catch {
|
|
2958
2960
|
return {
|
|
@@ -2961,13 +2963,28 @@ var RepositoryStatusService = class {
|
|
|
2961
2963
|
};
|
|
2962
2964
|
}
|
|
2963
2965
|
}
|
|
2966
|
+
async #baseBehind(cwd, git, baseBranch) {
|
|
2967
|
+
try {
|
|
2968
|
+
const result = await run(this.#runtime, git, [
|
|
2969
|
+
"rev-list",
|
|
2970
|
+
"--count",
|
|
2971
|
+
`HEAD..refs/remotes/origin/${baseBranch}`,
|
|
2972
|
+
"--"
|
|
2973
|
+
], cwd, GIT_TIMEOUT_MS$3);
|
|
2974
|
+
if (result.exitCode !== 0 || result.lossy) return void 0;
|
|
2975
|
+
const count = Number(bounded(result.stdout));
|
|
2976
|
+
return Number.isSafeInteger(count) && count >= 0 ? count : void 0;
|
|
2977
|
+
} catch {
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2964
2981
|
async #mergeBase(cwd, git, baseBranch) {
|
|
2965
2982
|
try {
|
|
2966
2983
|
const result = await run(this.#runtime, git, [
|
|
2967
2984
|
"merge-base",
|
|
2968
2985
|
"HEAD",
|
|
2969
2986
|
`refs/remotes/origin/${baseBranch}`
|
|
2970
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
2987
|
+
], cwd, GIT_TIMEOUT_MS$3);
|
|
2971
2988
|
if (result.exitCode !== 0 || result.lossy) return void 0;
|
|
2972
2989
|
const oid = bounded(result.stdout);
|
|
2973
2990
|
return /^[0-9a-f]{40}$/iu.test(oid) ? oid : void 0;
|
|
@@ -2983,7 +3000,7 @@ var RepositoryStatusService = class {
|
|
|
2983
3000
|
"--numstat",
|
|
2984
3001
|
base,
|
|
2985
3002
|
"--"
|
|
2986
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
3003
|
+
], cwd, GIT_TIMEOUT_MS$3);
|
|
2987
3004
|
if (numstat.exitCode !== 0 || numstat.lossy) return void 0;
|
|
2988
3005
|
const summary = parseDiffNumstat(numstat.stdout);
|
|
2989
3006
|
const patch = await run(this.#runtime, git, [
|
|
@@ -2993,7 +3010,7 @@ var RepositoryStatusService = class {
|
|
|
2993
3010
|
"--unified=3",
|
|
2994
3011
|
base,
|
|
2995
3012
|
"--"
|
|
2996
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
3013
|
+
], cwd, GIT_TIMEOUT_MS$3, MAX_DIFF_BYTES);
|
|
2997
3014
|
if (patch.exitCode !== 0) return {
|
|
2998
3015
|
...summary,
|
|
2999
3016
|
truncated: true
|
|
@@ -3017,7 +3034,7 @@ var RepositoryStatusService = class {
|
|
|
3017
3034
|
"--others",
|
|
3018
3035
|
"--exclude-standard",
|
|
3019
3036
|
"-z"
|
|
3020
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
3037
|
+
], cwd, GIT_TIMEOUT_MS$3);
|
|
3021
3038
|
if (listed.exitCode !== 0 || listed.lossy) return {
|
|
3022
3039
|
additions: 0,
|
|
3023
3040
|
files: 0,
|
|
@@ -3040,7 +3057,7 @@ var RepositoryStatusService = class {
|
|
|
3040
3057
|
"--",
|
|
3041
3058
|
"/dev/null",
|
|
3042
3059
|
path
|
|
3043
|
-
], cwd, GIT_TIMEOUT_MS$
|
|
3060
|
+
], cwd, GIT_TIMEOUT_MS$3, MAX_DIFF_BYTES);
|
|
3044
3061
|
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
3045
3062
|
truncated = true;
|
|
3046
3063
|
continue;
|
|
@@ -3069,7 +3086,7 @@ var RepositoryStatusService = class {
|
|
|
3069
3086
|
repository,
|
|
3070
3087
|
"--json",
|
|
3071
3088
|
"number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergedAt,statusCheckRollup,author,createdAt,baseRefName"
|
|
3072
|
-
], cwd, GH_TIMEOUT_MS);
|
|
3089
|
+
], cwd, GH_TIMEOUT_MS$1);
|
|
3073
3090
|
if (result.exitCode !== 0) return void 0;
|
|
3074
3091
|
return parsePullRequest(JSON.parse(result.stdout));
|
|
3075
3092
|
} catch {
|
|
@@ -3079,12 +3096,13 @@ var RepositoryStatusService = class {
|
|
|
3079
3096
|
};
|
|
3080
3097
|
//#endregion
|
|
3081
3098
|
//#region src/repository-setup.ts
|
|
3082
|
-
const MAX_OUTPUT_BYTES$
|
|
3083
|
-
const GIT_TIMEOUT_MS$
|
|
3099
|
+
const MAX_OUTPUT_BYTES$2 = 131072;
|
|
3100
|
+
const GIT_TIMEOUT_MS$2 = 1e4;
|
|
3084
3101
|
const GIT_FETCH_TIMEOUT_MS = 6e4;
|
|
3085
3102
|
const MAX_PATH_CHARS = 4096;
|
|
3086
3103
|
const MAX_BRANCH_CHARS = 512;
|
|
3087
3104
|
const LEASE_SCHEMA_VERSION = 1;
|
|
3105
|
+
const CLEANUP_GRACE_MS = 12e4;
|
|
3088
3106
|
var RepositorySetupError = class extends Error {
|
|
3089
3107
|
code;
|
|
3090
3108
|
constructor(code, message) {
|
|
@@ -3093,7 +3111,7 @@ var RepositorySetupError = class extends Error {
|
|
|
3093
3111
|
this.code = code;
|
|
3094
3112
|
}
|
|
3095
3113
|
};
|
|
3096
|
-
async function collect$
|
|
3114
|
+
async function collect$2(handle) {
|
|
3097
3115
|
const outcome = await handle.done;
|
|
3098
3116
|
const stdout = handle.collected.stdout?.readFrom(0);
|
|
3099
3117
|
const stderr = handle.collected.stderr?.readFrom(0);
|
|
@@ -3127,6 +3145,14 @@ function parseWorktreeBranches(value) {
|
|
|
3127
3145
|
function slug(value, fallback) {
|
|
3128
3146
|
return value.toLocaleLowerCase("en-US").replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48) || fallback;
|
|
3129
3147
|
}
|
|
3148
|
+
/** Comparable form for path identity: resolved, forward slashes, case-folded
|
|
3149
|
+
* so Windows drive-letter or case spelling differences cannot hide a match. */
|
|
3150
|
+
function comparablePath(value) {
|
|
3151
|
+
return resolve(value).replaceAll("\\", "/").toLocaleLowerCase("en-US");
|
|
3152
|
+
}
|
|
3153
|
+
function pathExists(value) {
|
|
3154
|
+
return stat(value).then(() => true, () => false);
|
|
3155
|
+
}
|
|
3130
3156
|
function lease(value) {
|
|
3131
3157
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3132
3158
|
const input = value;
|
|
@@ -3146,6 +3172,7 @@ var RepositorySetupService = class {
|
|
|
3146
3172
|
#leasePath;
|
|
3147
3173
|
#worktreeRoot;
|
|
3148
3174
|
#branchPrefix;
|
|
3175
|
+
#cleanupGraceMs;
|
|
3149
3176
|
#gitPath;
|
|
3150
3177
|
#pending = Promise.resolve();
|
|
3151
3178
|
constructor(runtime, options = {}) {
|
|
@@ -3153,6 +3180,7 @@ var RepositorySetupService = class {
|
|
|
3153
3180
|
this.#leasePath = options.leasePath ?? dshHomePath("plugins", "dsh-claude", "worktrees.json");
|
|
3154
3181
|
this.#worktreeRoot = options.worktreeRoot ?? dshHomePath("plugins", "dsh-claude", "worktrees");
|
|
3155
3182
|
this.#branchPrefix = options.branchPrefix ?? (async () => "claude");
|
|
3183
|
+
this.#cleanupGraceMs = options.cleanupGraceMs ?? CLEANUP_GRACE_MS;
|
|
3156
3184
|
}
|
|
3157
3185
|
async listBranches(cwd) {
|
|
3158
3186
|
const git = await this.#git();
|
|
@@ -3215,14 +3243,22 @@ var RepositorySetupService = class {
|
|
|
3215
3243
|
await this.#writeLeases(leases);
|
|
3216
3244
|
});
|
|
3217
3245
|
}
|
|
3218
|
-
|
|
3246
|
+
/** Reconcile leases against the set of directories still referenced by a
|
|
3247
|
+
* workspace: an unreferenced lease's clean worktree is removed. Fresh
|
|
3248
|
+
* leases are retained for a grace period so a worktree created moments ago
|
|
3249
|
+
* cannot be swept before its workspace registration lands, and dirty
|
|
3250
|
+
* worktrees are always retained to avoid losing uncommitted work. */
|
|
3251
|
+
cleanupOrphans(activePaths) {
|
|
3252
|
+
const active = new Set(activePaths.map(comparablePath));
|
|
3219
3253
|
return this.#serialize(async () => {
|
|
3220
3254
|
const leases = await this.#readLeases();
|
|
3221
3255
|
const retained = [];
|
|
3222
3256
|
let changed = false;
|
|
3223
3257
|
const git = await this.#git();
|
|
3258
|
+
const now = Date.now();
|
|
3224
3259
|
for (const item of leases) {
|
|
3225
|
-
|
|
3260
|
+
const age = now - Date.parse(item.createdAt);
|
|
3261
|
+
if (active.has(comparablePath(item.path)) || !(age >= this.#cleanupGraceMs)) {
|
|
3226
3262
|
retained.push(item);
|
|
3227
3263
|
continue;
|
|
3228
3264
|
}
|
|
@@ -3232,7 +3268,15 @@ var RepositorySetupService = class {
|
|
|
3232
3268
|
"--porcelain=v1",
|
|
3233
3269
|
"--untracked-files=normal"
|
|
3234
3270
|
], item.path);
|
|
3235
|
-
if (status.exitCode !== 0 || status.lossy
|
|
3271
|
+
if (status.exitCode !== 0 || status.lossy) {
|
|
3272
|
+
if (await pathExists(item.path)) retained.push(item);
|
|
3273
|
+
else {
|
|
3274
|
+
await this.#run(git, ["worktree", "prune"], item.root).catch(() => void 0);
|
|
3275
|
+
changed = true;
|
|
3276
|
+
}
|
|
3277
|
+
continue;
|
|
3278
|
+
}
|
|
3279
|
+
if (status.stdout.trim().length > 0) {
|
|
3236
3280
|
retained.push(item);
|
|
3237
3281
|
continue;
|
|
3238
3282
|
}
|
|
@@ -3253,7 +3297,8 @@ var RepositorySetupService = class {
|
|
|
3253
3297
|
], item.root).catch(() => void 0);
|
|
3254
3298
|
changed = true;
|
|
3255
3299
|
} catch {
|
|
3256
|
-
retained.push(item);
|
|
3300
|
+
if (await pathExists(item.root)) retained.push(item);
|
|
3301
|
+
else changed = true;
|
|
3257
3302
|
}
|
|
3258
3303
|
}
|
|
3259
3304
|
if (changed) await this.#writeLeases(retained);
|
|
@@ -3386,14 +3431,14 @@ var RepositorySetupService = class {
|
|
|
3386
3431
|
this.#gitPath ??= this.#runtime.resolveExecutable("git");
|
|
3387
3432
|
return this.#gitPath;
|
|
3388
3433
|
}
|
|
3389
|
-
async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$
|
|
3390
|
-
return collect$
|
|
3434
|
+
async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$2) {
|
|
3435
|
+
return collect$2(this.#runtime.spawn({
|
|
3391
3436
|
argv: [executable, ...args],
|
|
3392
3437
|
cwd,
|
|
3393
3438
|
stdio: {
|
|
3394
3439
|
stdin: "ignore",
|
|
3395
|
-
stdout: { maxBytes: MAX_OUTPUT_BYTES$
|
|
3396
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
3440
|
+
stdout: { maxBytes: MAX_OUTPUT_BYTES$2 },
|
|
3441
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
|
|
3397
3442
|
},
|
|
3398
3443
|
graceMs: 1e3,
|
|
3399
3444
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -3434,12 +3479,12 @@ var RepositorySetupService = class {
|
|
|
3434
3479
|
};
|
|
3435
3480
|
//#endregion
|
|
3436
3481
|
//#region src/repository-actions.ts
|
|
3437
|
-
const MAX_OUTPUT_BYTES = 262144;
|
|
3482
|
+
const MAX_OUTPUT_BYTES$1 = 262144;
|
|
3438
3483
|
const MAX_PATCH_CHARS = 65536;
|
|
3439
3484
|
const MAX_MESSAGE_CHARS = 512;
|
|
3440
3485
|
const MAX_PR_TEXT_CHARS = 8192;
|
|
3441
3486
|
const MAX_UNPUSHED_COMMITS = 20;
|
|
3442
|
-
const GIT_TIMEOUT_MS = 15e3;
|
|
3487
|
+
const GIT_TIMEOUT_MS$1 = 15e3;
|
|
3443
3488
|
const REMOTE_TIMEOUT_MS = 6e4;
|
|
3444
3489
|
const GENERATE_TIMEOUT_MS = 6e4;
|
|
3445
3490
|
var RepositoryActionError = class extends Error {
|
|
@@ -3452,7 +3497,7 @@ var RepositoryActionError = class extends Error {
|
|
|
3452
3497
|
if (commit !== void 0) this.commit = commit;
|
|
3453
3498
|
}
|
|
3454
3499
|
};
|
|
3455
|
-
async function collect(handle) {
|
|
3500
|
+
async function collect$1(handle) {
|
|
3456
3501
|
const outcome = await handle.done;
|
|
3457
3502
|
const stdout = handle.collected.stdout?.readFrom(0);
|
|
3458
3503
|
const stderr = handle.collected.stderr?.readFrom(0);
|
|
@@ -3579,6 +3624,77 @@ var RepositoryActionService = class {
|
|
|
3579
3624
|
pushed: true
|
|
3580
3625
|
};
|
|
3581
3626
|
}
|
|
3627
|
+
if (request.action === "merge-pr") {
|
|
3628
|
+
const method = request.mergeMethod;
|
|
3629
|
+
if (method !== "merge" && method !== "squash" && method !== "rebase") throw new RepositoryActionError("invalid-request", "The merge method is invalid.");
|
|
3630
|
+
let gh;
|
|
3631
|
+
try {
|
|
3632
|
+
gh = await this.#gh();
|
|
3633
|
+
} catch (error) {
|
|
3634
|
+
throw new RepositoryActionError("gh-unavailable", error instanceof Error ? error.message : "GitHub CLI is unavailable.");
|
|
3635
|
+
}
|
|
3636
|
+
const merged = await this.#run(gh, [
|
|
3637
|
+
"pr",
|
|
3638
|
+
"merge",
|
|
3639
|
+
`--${method}`
|
|
3640
|
+
], before.root, REMOTE_TIMEOUT_MS);
|
|
3641
|
+
if (merged.exitCode !== 0 || merged.lossy) {
|
|
3642
|
+
const reason = merged.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
|
|
3643
|
+
throw new RepositoryActionError("merge-failed", reason === void 0 || reason.length === 0 ? "The pull request could not be merged." : reason);
|
|
3644
|
+
}
|
|
3645
|
+
this.#invalidate(before.root);
|
|
3646
|
+
return {
|
|
3647
|
+
commit: before.head,
|
|
3648
|
+
pushed: true
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
if (request.action === "update-branch") {
|
|
3652
|
+
const base = safeText(request.baseBranch ?? "", 512, "Base branch");
|
|
3653
|
+
if (before.files.length > 0) throw new RepositoryActionError("dirty-workspace", "Commit or stash workspace changes before updating the branch.");
|
|
3654
|
+
const git = await this.#git();
|
|
3655
|
+
await this.#mustRun(git, [
|
|
3656
|
+
"fetch",
|
|
3657
|
+
"origin",
|
|
3658
|
+
"--",
|
|
3659
|
+
base
|
|
3660
|
+
], before.root, REMOTE_TIMEOUT_MS, "fetch-failed", "Git could not fetch the base branch.");
|
|
3661
|
+
const merged = await this.#run(git, [
|
|
3662
|
+
"merge",
|
|
3663
|
+
"--no-edit",
|
|
3664
|
+
"--",
|
|
3665
|
+
`origin/${base}`
|
|
3666
|
+
], before.root, REMOTE_TIMEOUT_MS);
|
|
3667
|
+
if (merged.exitCode !== 0 || merged.lossy) {
|
|
3668
|
+
const conflicted = await this.#run(git, [
|
|
3669
|
+
"diff",
|
|
3670
|
+
"--name-only",
|
|
3671
|
+
"--diff-filter=U",
|
|
3672
|
+
"--"
|
|
3673
|
+
], before.root, GIT_TIMEOUT_MS$1);
|
|
3674
|
+
const conflicts = conflicted.exitCode === 0 && !conflicted.lossy ? conflicted.stdout.split(/\r?\n/u).filter((line) => line.length > 0).slice(0, 100) : [];
|
|
3675
|
+
if (conflicts.length === 0) {
|
|
3676
|
+
await this.#run(git, ["merge", "--abort"], before.root, GIT_TIMEOUT_MS$1).catch(() => void 0);
|
|
3677
|
+
throw new RepositoryActionError("merge-failed", "Git could not merge the base branch.");
|
|
3678
|
+
}
|
|
3679
|
+
this.#invalidate(before.root);
|
|
3680
|
+
return {
|
|
3681
|
+
commit: before.head,
|
|
3682
|
+
pushed: false,
|
|
3683
|
+
conflicts
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
const mergedHead = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS$1, "merge-failed", "The merge commit could not be verified.")).stdout.trim();
|
|
3687
|
+
try {
|
|
3688
|
+
await this.#push(git, before.root, before.branch);
|
|
3689
|
+
} catch (error) {
|
|
3690
|
+
throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.", mergedHead);
|
|
3691
|
+
}
|
|
3692
|
+
this.#invalidate(before.root);
|
|
3693
|
+
return {
|
|
3694
|
+
commit: mergedHead,
|
|
3695
|
+
pushed: true
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3582
3698
|
const message = safeText(request.message, MAX_MESSAGE_CHARS, "Commit message");
|
|
3583
3699
|
if (before.files.length === 0 && request.action !== "create-pr") throw new RepositoryActionError("nothing-to-commit", "There are no changes to commit.");
|
|
3584
3700
|
const git = await this.#git();
|
|
@@ -3591,7 +3707,7 @@ var RepositoryActionService = class {
|
|
|
3591
3707
|
"add",
|
|
3592
3708
|
"--",
|
|
3593
3709
|
...paths
|
|
3594
|
-
], before.root, GIT_TIMEOUT_MS, "stage-failed", "Changes could not be staged.");
|
|
3710
|
+
], before.root, GIT_TIMEOUT_MS$1, "stage-failed", "Changes could not be staged.");
|
|
3595
3711
|
}
|
|
3596
3712
|
await this.#rejectStagedWarp(git, before.root);
|
|
3597
3713
|
const staged = await this.#run(git, [
|
|
@@ -3600,7 +3716,7 @@ var RepositoryActionService = class {
|
|
|
3600
3716
|
"--quiet",
|
|
3601
3717
|
"--exit-code",
|
|
3602
3718
|
"--"
|
|
3603
|
-
], before.root, GIT_TIMEOUT_MS);
|
|
3719
|
+
], before.root, GIT_TIMEOUT_MS$1);
|
|
3604
3720
|
if (staged.exitCode === 0) throw new RepositoryActionError("nothing-to-commit", "There are no staged changes to commit.");
|
|
3605
3721
|
if (staged.exitCode !== 1) throw new RepositoryActionError("repository-unavailable", "The staged changes could not be verified.");
|
|
3606
3722
|
await this.#mustRun(git, [
|
|
@@ -3608,8 +3724,8 @@ var RepositoryActionService = class {
|
|
|
3608
3724
|
"-m",
|
|
3609
3725
|
message,
|
|
3610
3726
|
"--"
|
|
3611
|
-
], before.root, GIT_TIMEOUT_MS, "commit-failed", "Git commit failed.");
|
|
3612
|
-
oid = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS, "commit-failed", "The new commit could not be verified.")).stdout.trim();
|
|
3727
|
+
], before.root, GIT_TIMEOUT_MS$1, "commit-failed", "Git commit failed.");
|
|
3728
|
+
oid = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS$1, "commit-failed", "The new commit could not be verified.")).stdout.trim();
|
|
3613
3729
|
this.#invalidate(before.root);
|
|
3614
3730
|
if (request.action === "commit") return {
|
|
3615
3731
|
commit: oid,
|
|
@@ -3660,21 +3776,21 @@ var RepositoryActionService = class {
|
|
|
3660
3776
|
"rev-parse",
|
|
3661
3777
|
"--path-format=absolute",
|
|
3662
3778
|
"--show-toplevel"
|
|
3663
|
-
], cwd, GIT_TIMEOUT_MS, "not-repository", "The session directory is not a Git repository.")).stdout.trim();
|
|
3779
|
+
], cwd, GIT_TIMEOUT_MS$1, "not-repository", "The session directory is not a Git repository.")).stdout.trim();
|
|
3664
3780
|
const [branchResult, headResult, statusResult, stagedPatch, unstagedPatch] = await Promise.all([
|
|
3665
3781
|
this.#run(git, [
|
|
3666
3782
|
"symbolic-ref",
|
|
3667
3783
|
"--quiet",
|
|
3668
3784
|
"--short",
|
|
3669
3785
|
"HEAD"
|
|
3670
|
-
], root, GIT_TIMEOUT_MS),
|
|
3671
|
-
this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS),
|
|
3786
|
+
], root, GIT_TIMEOUT_MS$1),
|
|
3787
|
+
this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS$1),
|
|
3672
3788
|
this.#run(git, [
|
|
3673
3789
|
"status",
|
|
3674
3790
|
"--porcelain=v1",
|
|
3675
3791
|
"-z",
|
|
3676
3792
|
"--untracked-files=all"
|
|
3677
|
-
], root, GIT_TIMEOUT_MS),
|
|
3793
|
+
], root, GIT_TIMEOUT_MS$1),
|
|
3678
3794
|
this.#run(git, [
|
|
3679
3795
|
"diff",
|
|
3680
3796
|
"--cached",
|
|
@@ -3684,7 +3800,7 @@ var RepositoryActionService = class {
|
|
|
3684
3800
|
"--",
|
|
3685
3801
|
":(exclude)WARP.md",
|
|
3686
3802
|
":(exclude)**/WARP.md"
|
|
3687
|
-
], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES),
|
|
3803
|
+
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1),
|
|
3688
3804
|
this.#run(git, [
|
|
3689
3805
|
"diff",
|
|
3690
3806
|
"--no-ext-diff",
|
|
@@ -3693,7 +3809,7 @@ var RepositoryActionService = class {
|
|
|
3693
3809
|
"--",
|
|
3694
3810
|
":(exclude)WARP.md",
|
|
3695
3811
|
":(exclude)**/WARP.md"
|
|
3696
|
-
], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES)
|
|
3812
|
+
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1)
|
|
3697
3813
|
]);
|
|
3698
3814
|
if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
|
|
3699
3815
|
if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
|
|
@@ -3713,7 +3829,7 @@ var RepositoryActionService = class {
|
|
|
3713
3829
|
"--abbrev-ref",
|
|
3714
3830
|
"--symbolic-full-name",
|
|
3715
3831
|
"@{upstream}"
|
|
3716
|
-
], root, GIT_TIMEOUT_MS);
|
|
3832
|
+
], root, GIT_TIMEOUT_MS$1);
|
|
3717
3833
|
const upstream = upstreamResult.exitCode === 0 && !upstreamResult.lossy ? upstreamResult.stdout.trim() : void 0;
|
|
3718
3834
|
const logResult = await this.#run(git, [
|
|
3719
3835
|
"log",
|
|
@@ -3722,7 +3838,7 @@ var RepositoryActionService = class {
|
|
|
3722
3838
|
String(21),
|
|
3723
3839
|
upstream === void 0 ? "HEAD" : "@{upstream}..HEAD",
|
|
3724
3840
|
"--"
|
|
3725
|
-
], root, GIT_TIMEOUT_MS);
|
|
3841
|
+
], root, GIT_TIMEOUT_MS$1);
|
|
3726
3842
|
const commitLines = logResult.exitCode === 0 && !logResult.lossy ? logResult.stdout.split(/\r?\n/u).filter((line) => line.includes(" ")) : [];
|
|
3727
3843
|
const unpushedCommits = commitLines.slice(0, MAX_UNPUSHED_COMMITS).flatMap((line) => {
|
|
3728
3844
|
const tab = line.indexOf(" ");
|
|
@@ -3754,7 +3870,7 @@ var RepositoryActionService = class {
|
|
|
3754
3870
|
"--cached",
|
|
3755
3871
|
"--name-only",
|
|
3756
3872
|
"--"
|
|
3757
|
-
], cwd, GIT_TIMEOUT_MS);
|
|
3873
|
+
], cwd, GIT_TIMEOUT_MS$1);
|
|
3758
3874
|
if (staged.exitCode !== 0 || staged.lossy) throw new RepositoryActionError("repository-unavailable", "Staged files could not be verified.");
|
|
3759
3875
|
if (staged.stdout.split(/\r?\n/u).some((path) => path.length > 0 && isProtectedWarpPath(path))) throw new RepositoryActionError("protected-warp-file", "WARP.md files cannot be committed. Unstage them before continuing.");
|
|
3760
3876
|
}
|
|
@@ -3764,7 +3880,7 @@ var RepositoryActionService = class {
|
|
|
3764
3880
|
"--abbrev-ref",
|
|
3765
3881
|
"--symbolic-full-name",
|
|
3766
3882
|
"@{upstream}"
|
|
3767
|
-
], cwd, GIT_TIMEOUT_MS)).exitCode === 0 ? ["push"] : [
|
|
3883
|
+
], cwd, GIT_TIMEOUT_MS$1)).exitCode === 0 ? ["push"] : [
|
|
3768
3884
|
"push",
|
|
3769
3885
|
"--set-upstream",
|
|
3770
3886
|
"origin",
|
|
@@ -3787,14 +3903,14 @@ var RepositoryActionService = class {
|
|
|
3787
3903
|
if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
|
|
3788
3904
|
return result;
|
|
3789
3905
|
}
|
|
3790
|
-
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES) {
|
|
3791
|
-
return collect(this.#runtime.spawn({
|
|
3906
|
+
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
|
|
3907
|
+
return collect$1(this.#runtime.spawn({
|
|
3792
3908
|
argv: [executable, ...args],
|
|
3793
3909
|
cwd,
|
|
3794
3910
|
stdio: {
|
|
3795
3911
|
stdin: "ignore",
|
|
3796
3912
|
stdout: { maxBytes },
|
|
3797
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES }
|
|
3913
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
|
|
3798
3914
|
},
|
|
3799
3915
|
graceMs: 1e3,
|
|
3800
3916
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -3805,7 +3921,7 @@ var RepositoryActionService = class {
|
|
|
3805
3921
|
//#endregion
|
|
3806
3922
|
//#region src/repository-setup-routes.ts
|
|
3807
3923
|
const MAX_BODY_BYTES$2 = 16384;
|
|
3808
|
-
function record$
|
|
3924
|
+
function record$3(value) {
|
|
3809
3925
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3810
3926
|
}
|
|
3811
3927
|
async function readJson$2(req) {
|
|
@@ -3817,7 +3933,7 @@ async function readJson$2(req) {
|
|
|
3817
3933
|
if (size > MAX_BODY_BYTES$2) throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
3818
3934
|
chunks.push(buffer);
|
|
3819
3935
|
}
|
|
3820
|
-
const value = record$
|
|
3936
|
+
const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3821
3937
|
if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
|
|
3822
3938
|
return value;
|
|
3823
3939
|
}
|
|
@@ -3911,14 +4027,16 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
3911
4027
|
//#endregion
|
|
3912
4028
|
//#region src/repository-action-routes.ts
|
|
3913
4029
|
const MAX_BODY_BYTES$1 = 16384;
|
|
3914
|
-
const MAX_SESSION_ID_CHARS$
|
|
4030
|
+
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
3915
4031
|
const ACTIONS = /* @__PURE__ */ new Set([
|
|
3916
4032
|
"commit",
|
|
3917
4033
|
"commit-push",
|
|
3918
4034
|
"push",
|
|
3919
|
-
"create-pr"
|
|
4035
|
+
"create-pr",
|
|
4036
|
+
"merge-pr",
|
|
4037
|
+
"update-branch"
|
|
3920
4038
|
]);
|
|
3921
|
-
function record$
|
|
4039
|
+
function record$2(value) {
|
|
3922
4040
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3923
4041
|
}
|
|
3924
4042
|
async function readJson$1(req) {
|
|
@@ -3930,13 +4048,13 @@ async function readJson$1(req) {
|
|
|
3930
4048
|
if (size > MAX_BODY_BYTES$1) throw new RepositoryActionError("body-too-large", "The request body is too large.");
|
|
3931
4049
|
chunks.push(buffer);
|
|
3932
4050
|
}
|
|
3933
|
-
const value = record$
|
|
4051
|
+
const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
3934
4052
|
if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
|
|
3935
4053
|
return value;
|
|
3936
4054
|
}
|
|
3937
|
-
function sessionId(url) {
|
|
4055
|
+
function sessionId$1(url) {
|
|
3938
4056
|
const value = url.searchParams.get("sessionId");
|
|
3939
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
4057
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new RepositoryActionError("invalid-session", "The session is invalid.");
|
|
3940
4058
|
return value;
|
|
3941
4059
|
}
|
|
3942
4060
|
function string(input, key) {
|
|
@@ -3956,13 +4074,16 @@ function actionRequest(input) {
|
|
|
3956
4074
|
return {
|
|
3957
4075
|
action,
|
|
3958
4076
|
fingerprint: string(input, "fingerprint"),
|
|
3959
|
-
message: action === "push" ? optionalString(input, "message") ?? "" : string(input, "message"),
|
|
4077
|
+
message: action === "push" || action === "merge-pr" || action === "update-branch" ? optionalString(input, "message") ?? "" : string(input, "message"),
|
|
3960
4078
|
includeUnstaged: input.includeUnstaged,
|
|
3961
4079
|
...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
|
|
3962
4080
|
...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
|
|
3963
4081
|
...optionalString(input, "baseBranch") === void 0 ? {} : { baseBranch: optionalString(input, "baseBranch") },
|
|
3964
4082
|
...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
|
|
3965
4083
|
throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
|
|
4084
|
+
})(),
|
|
4085
|
+
...input.mergeMethod === void 0 ? {} : input.mergeMethod === "merge" || input.mergeMethod === "squash" || input.mergeMethod === "rebase" ? { mergeMethod: input.mergeMethod } : (() => {
|
|
4086
|
+
throw new RepositoryActionError("invalid-request", "The mergeMethod field is invalid.");
|
|
3966
4087
|
})()
|
|
3967
4088
|
};
|
|
3968
4089
|
}
|
|
@@ -3974,7 +4095,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
3974
4095
|
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
3975
4096
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
3976
4097
|
try {
|
|
3977
|
-
const cwd = cwdForSession(sessionId(url));
|
|
4098
|
+
const cwd = cwdForSession(sessionId$1(url));
|
|
3978
4099
|
if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
|
|
3979
4100
|
if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
|
|
3980
4101
|
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
@@ -4006,6 +4127,218 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
4006
4127
|
}), "dsh-claude: repository action route");
|
|
4007
4128
|
}
|
|
4008
4129
|
//#endregion
|
|
4130
|
+
//#region src/pr-feedback.ts
|
|
4131
|
+
const MAX_OUTPUT_BYTES = 524288;
|
|
4132
|
+
const GIT_TIMEOUT_MS = 1e4;
|
|
4133
|
+
const GH_TIMEOUT_MS = 6e4;
|
|
4134
|
+
const MAX_COMMENTS = 100;
|
|
4135
|
+
const MAX_COMMENT_CHARS = 4096;
|
|
4136
|
+
const MAX_FAILING_CHECKS = 3;
|
|
4137
|
+
const MAX_LOG_CHARS = 8192;
|
|
4138
|
+
var PullRequestFeedbackError = class extends Error {
|
|
4139
|
+
code;
|
|
4140
|
+
constructor(code, message) {
|
|
4141
|
+
super(message);
|
|
4142
|
+
this.name = "PullRequestFeedbackError";
|
|
4143
|
+
this.code = code;
|
|
4144
|
+
}
|
|
4145
|
+
};
|
|
4146
|
+
async function collect(handle) {
|
|
4147
|
+
const outcome = await handle.done;
|
|
4148
|
+
const stdout = handle.collected.stdout?.readFrom(0);
|
|
4149
|
+
return {
|
|
4150
|
+
exitCode: outcome.exitCode,
|
|
4151
|
+
stdout: stdout?.text ?? "",
|
|
4152
|
+
lossy: stdout?.lossy === true
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
function record$1(value) {
|
|
4156
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4157
|
+
}
|
|
4158
|
+
function parseReviewComments(value) {
|
|
4159
|
+
if (!Array.isArray(value)) return [];
|
|
4160
|
+
const comments = [];
|
|
4161
|
+
for (const item of value) {
|
|
4162
|
+
const input = record$1(item);
|
|
4163
|
+
if (input === void 0 || !Number.isSafeInteger(input.id)) continue;
|
|
4164
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
4165
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
4166
|
+
const url = typeof input.html_url === "string" ? input.html_url : "";
|
|
4167
|
+
if (body.length === 0 || path.length === 0) continue;
|
|
4168
|
+
const line = Number.isSafeInteger(input.line) ? Number(input.line) : Number.isSafeInteger(input.original_line) ? Number(input.original_line) : void 0;
|
|
4169
|
+
comments.push({
|
|
4170
|
+
id: Number(input.id),
|
|
4171
|
+
path,
|
|
4172
|
+
...line === void 0 ? {} : { line },
|
|
4173
|
+
side: input.side === "LEFT" ? "old" : "new",
|
|
4174
|
+
author: typeof record$1(input.user)?.login === "string" ? String(record$1(input.user)?.login) : "unknown",
|
|
4175
|
+
body: body.slice(0, MAX_COMMENT_CHARS),
|
|
4176
|
+
url
|
|
4177
|
+
});
|
|
4178
|
+
if (comments.length >= MAX_COMMENTS) break;
|
|
4179
|
+
}
|
|
4180
|
+
return comments;
|
|
4181
|
+
}
|
|
4182
|
+
/** Extract the Actions job id from a check run link, when it is one. */
|
|
4183
|
+
function actionsJobId(link) {
|
|
4184
|
+
if (link === void 0) return void 0;
|
|
4185
|
+
return /\/actions\/runs\/\d+\/jobs?\/(\d+)/u.exec(link)?.[1];
|
|
4186
|
+
}
|
|
4187
|
+
function parseFailingChecks(value) {
|
|
4188
|
+
if (!Array.isArray(value)) return [];
|
|
4189
|
+
const failing = [];
|
|
4190
|
+
for (const item of value) {
|
|
4191
|
+
const input = record$1(item);
|
|
4192
|
+
if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
|
|
4193
|
+
failing.push({
|
|
4194
|
+
name: input.name,
|
|
4195
|
+
...typeof input.link === "string" && input.link.length > 0 ? { link: input.link } : {},
|
|
4196
|
+
...typeof input.description === "string" && input.description.length > 0 ? { description: input.description } : {}
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
return failing;
|
|
4200
|
+
}
|
|
4201
|
+
/** Keep the informative end of a failed-job log within the size budget. */
|
|
4202
|
+
function boundedLogTail(value) {
|
|
4203
|
+
const text = value.replaceAll("\0", "").trimEnd();
|
|
4204
|
+
return text.length <= MAX_LOG_CHARS ? text : text.slice(text.length - MAX_LOG_CHARS);
|
|
4205
|
+
}
|
|
4206
|
+
var PullRequestFeedbackService = class {
|
|
4207
|
+
#runtime;
|
|
4208
|
+
#gitExecutable;
|
|
4209
|
+
#ghExecutable;
|
|
4210
|
+
constructor(runtime) {
|
|
4211
|
+
this.#runtime = runtime;
|
|
4212
|
+
}
|
|
4213
|
+
async comments(cwd, pullNumber) {
|
|
4214
|
+
const repository = await this.#repository(cwd);
|
|
4215
|
+
const gh = await this.#gh();
|
|
4216
|
+
const result = await this.#run(gh, [
|
|
4217
|
+
"api",
|
|
4218
|
+
`repos/${repository}/pulls/${pullNumber}/comments`,
|
|
4219
|
+
"--paginate"
|
|
4220
|
+
], cwd, GH_TIMEOUT_MS);
|
|
4221
|
+
if (result.exitCode !== 0) throw new PullRequestFeedbackError("comments-unavailable", "Pull request comments could not be loaded.");
|
|
4222
|
+
try {
|
|
4223
|
+
const pages = result.stdout.replaceAll("][", ",");
|
|
4224
|
+
return parseReviewComments(JSON.parse(pages));
|
|
4225
|
+
} catch {
|
|
4226
|
+
throw new PullRequestFeedbackError("comments-unavailable", "Pull request comments could not be parsed.");
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
async failingChecks(cwd, pullNumber) {
|
|
4230
|
+
const gh = await this.#gh();
|
|
4231
|
+
const result = await this.#run(gh, [
|
|
4232
|
+
"pr",
|
|
4233
|
+
"checks",
|
|
4234
|
+
String(pullNumber),
|
|
4235
|
+
"--json",
|
|
4236
|
+
"name,state,link,description,bucket"
|
|
4237
|
+
], cwd, GH_TIMEOUT_MS);
|
|
4238
|
+
let checks;
|
|
4239
|
+
try {
|
|
4240
|
+
checks = parseFailingChecks(JSON.parse(result.stdout));
|
|
4241
|
+
} catch {
|
|
4242
|
+
throw new PullRequestFeedbackError("checks-unavailable", "Pull request checks could not be loaded.");
|
|
4243
|
+
}
|
|
4244
|
+
const detailed = [];
|
|
4245
|
+
for (const check of checks) {
|
|
4246
|
+
const jobId = detailed.length < MAX_FAILING_CHECKS ? actionsJobId(check.link) : void 0;
|
|
4247
|
+
if (jobId === void 0) {
|
|
4248
|
+
detailed.push(check);
|
|
4249
|
+
continue;
|
|
4250
|
+
}
|
|
4251
|
+
const log = await this.#run(gh, [
|
|
4252
|
+
"run",
|
|
4253
|
+
"view",
|
|
4254
|
+
"--job",
|
|
4255
|
+
jobId,
|
|
4256
|
+
"--log-failed"
|
|
4257
|
+
], cwd, GH_TIMEOUT_MS);
|
|
4258
|
+
detailed.push(log.exitCode === 0 && log.stdout.trim().length > 0 ? {
|
|
4259
|
+
...check,
|
|
4260
|
+
log: boundedLogTail(log.stdout)
|
|
4261
|
+
} : check);
|
|
4262
|
+
}
|
|
4263
|
+
return detailed;
|
|
4264
|
+
}
|
|
4265
|
+
async #repository(cwd) {
|
|
4266
|
+
const git = await this.#git();
|
|
4267
|
+
const remote = await this.#run(git, [
|
|
4268
|
+
"remote",
|
|
4269
|
+
"get-url",
|
|
4270
|
+
"origin"
|
|
4271
|
+
], cwd, GIT_TIMEOUT_MS);
|
|
4272
|
+
const repository = remote.exitCode === 0 ? parseGitHubRemote(remote.stdout) : void 0;
|
|
4273
|
+
if (repository === void 0) throw new PullRequestFeedbackError("no-github-remote", "The repository has no GitHub origin remote.");
|
|
4274
|
+
return repository;
|
|
4275
|
+
}
|
|
4276
|
+
#git() {
|
|
4277
|
+
this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
|
|
4278
|
+
return this.#gitExecutable;
|
|
4279
|
+
}
|
|
4280
|
+
#gh() {
|
|
4281
|
+
this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => {
|
|
4282
|
+
throw new PullRequestFeedbackError("gh-unavailable", "GitHub CLI is unavailable.");
|
|
4283
|
+
});
|
|
4284
|
+
return this.#ghExecutable;
|
|
4285
|
+
}
|
|
4286
|
+
#run(executable, args, cwd, timeoutMs) {
|
|
4287
|
+
return collect(this.#runtime.spawn({
|
|
4288
|
+
argv: [executable, ...args],
|
|
4289
|
+
cwd,
|
|
4290
|
+
stdio: {
|
|
4291
|
+
stdin: "ignore",
|
|
4292
|
+
stdout: { maxBytes: MAX_OUTPUT_BYTES },
|
|
4293
|
+
stderr: { maxBytes: 65536 }
|
|
4294
|
+
},
|
|
4295
|
+
graceMs: 1e3,
|
|
4296
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
4297
|
+
env: {}
|
|
4298
|
+
}));
|
|
4299
|
+
}
|
|
4300
|
+
};
|
|
4301
|
+
//#endregion
|
|
4302
|
+
//#region src/pr-feedback-routes.ts
|
|
4303
|
+
const MAX_SESSION_ID_CHARS$1 = 1024;
|
|
4304
|
+
function sessionId(url) {
|
|
4305
|
+
const value = url.searchParams.get("sessionId");
|
|
4306
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
|
|
4307
|
+
return value;
|
|
4308
|
+
}
|
|
4309
|
+
function pullNumber(url) {
|
|
4310
|
+
const value = Number(url.searchParams.get("number"));
|
|
4311
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 1e9) throw new PullRequestFeedbackError("invalid-request", "The pull request number is invalid.");
|
|
4312
|
+
return value;
|
|
4313
|
+
}
|
|
4314
|
+
function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
|
|
4315
|
+
ctx.effect(() => ctx.webServer.register({
|
|
4316
|
+
kind: "prefix",
|
|
4317
|
+
path: CLAUDE_REPOSITORY_FEEDBACK_PATH,
|
|
4318
|
+
handler: async (req, res) => {
|
|
4319
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4320
|
+
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
4321
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
4322
|
+
try {
|
|
4323
|
+
const cwd = cwdForSession(sessionId(url));
|
|
4324
|
+
if (cwd === void 0) throw new PullRequestFeedbackError("session-unavailable", "The Claude session is unavailable.");
|
|
4325
|
+
if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`) return json(res, 200, { comments: await service.comments(cwd, pullNumber(url)) });
|
|
4326
|
+
if (url.pathname === `/plugins/dsh-claude/repository/feedback/checks`) return json(res, 200, { checks: await service.failingChecks(cwd, pullNumber(url)) });
|
|
4327
|
+
return json(res, 404, { error: "not found" });
|
|
4328
|
+
} catch (error) {
|
|
4329
|
+
if (error instanceof PullRequestFeedbackError) return json(res, 409, {
|
|
4330
|
+
error: error.code,
|
|
4331
|
+
message: error.message
|
|
4332
|
+
});
|
|
4333
|
+
return json(res, 500, {
|
|
4334
|
+
error: "pr-feedback-unavailable",
|
|
4335
|
+
message: "Pull request feedback is unavailable."
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
}), "dsh-claude: pull request feedback route");
|
|
4340
|
+
}
|
|
4341
|
+
//#endregion
|
|
4009
4342
|
//#region src/review-comment-routes.ts
|
|
4010
4343
|
const MAX_BODY_BYTES = 16384;
|
|
4011
4344
|
const MAX_SESSION_ID_CHARS = 1024;
|
|
@@ -4346,6 +4679,12 @@ const BUILTIN_OUTPUT_STYLES = [
|
|
|
4346
4679
|
const STYLE_NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._()\[\]-]{0,127}$/u;
|
|
4347
4680
|
const DEFAULT_WORKTREE_BRANCH_PREFIX = "claude";
|
|
4348
4681
|
const MAX_BRANCH_PREFIX_CHARS = 128;
|
|
4682
|
+
const MAX_PROCESSES_LIMIT = 16;
|
|
4683
|
+
const MAX_IDLE_TIMEOUT_MINUTES = 1440;
|
|
4684
|
+
const DEFAULT_LIMITS = {
|
|
4685
|
+
maxProcesses: 4,
|
|
4686
|
+
idleTimeoutMs: 18e5
|
|
4687
|
+
};
|
|
4349
4688
|
function pathsFor(deps) {
|
|
4350
4689
|
const root = join(homedir(), ".claude");
|
|
4351
4690
|
return {
|
|
@@ -4451,19 +4790,45 @@ const WORKTREE_BRANCH_PREFIX = {
|
|
|
4451
4790
|
else document.worktreeBranchPrefix = value;
|
|
4452
4791
|
}
|
|
4453
4792
|
};
|
|
4454
|
-
|
|
4793
|
+
function isBoundedInteger(value, min, max) {
|
|
4794
|
+
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
4795
|
+
}
|
|
4796
|
+
function integerSetting(key, min, max, defaultFor) {
|
|
4797
|
+
return {
|
|
4798
|
+
key,
|
|
4799
|
+
kind: "text",
|
|
4800
|
+
document: "plugin",
|
|
4801
|
+
effect: "new-session",
|
|
4802
|
+
maxLength: String(max).length,
|
|
4803
|
+
read(document, defaults = DEFAULT_LIMITS) {
|
|
4804
|
+
const value = document[key];
|
|
4805
|
+
return String(isBoundedInteger(value, min, max) ? value : defaultFor(defaults));
|
|
4806
|
+
},
|
|
4807
|
+
apply(document, value) {
|
|
4808
|
+
const parsed = typeof value === "string" && /^\d{1,6}$/u.test(value.trim()) ? Number(value.trim()) : value;
|
|
4809
|
+
if (!isBoundedInteger(parsed, min, max)) throw new Error(`Invalid value for global setting ${key}`);
|
|
4810
|
+
document[key] = parsed;
|
|
4811
|
+
}
|
|
4812
|
+
};
|
|
4813
|
+
}
|
|
4814
|
+
const DESCRIPTORS = [
|
|
4815
|
+
OUTPUT_STYLE,
|
|
4816
|
+
WORKTREE_BRANCH_PREFIX,
|
|
4817
|
+
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
|
|
4818
|
+
integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
|
|
4819
|
+
];
|
|
4455
4820
|
const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
|
|
4456
4821
|
let pendingWrite = Promise.resolve();
|
|
4457
4822
|
function documentFor(descriptor, documents) {
|
|
4458
4823
|
return documents[descriptor.document];
|
|
4459
4824
|
}
|
|
4460
|
-
async function views(documents, paths) {
|
|
4825
|
+
async function views(documents, paths, defaults) {
|
|
4461
4826
|
return { settings: await Promise.all(DESCRIPTORS.map(async (descriptor) => {
|
|
4462
4827
|
const document = documentFor(descriptor, documents);
|
|
4463
4828
|
if (descriptor.kind === "text") return {
|
|
4464
4829
|
key: descriptor.key,
|
|
4465
4830
|
kind: descriptor.kind,
|
|
4466
|
-
value: descriptor.read(document),
|
|
4831
|
+
value: descriptor.read(document, defaults),
|
|
4467
4832
|
maxLength: descriptor.maxLength,
|
|
4468
4833
|
effect: descriptor.effect
|
|
4469
4834
|
};
|
|
@@ -4492,7 +4857,22 @@ async function readDocuments(paths) {
|
|
|
4492
4857
|
}
|
|
4493
4858
|
async function readGlobalSettings(deps = {}) {
|
|
4494
4859
|
const paths = pathsFor(deps);
|
|
4495
|
-
return views(await readDocuments(paths), paths);
|
|
4860
|
+
return views(await readDocuments(paths), paths, deps.defaultLimits ?? DEFAULT_LIMITS);
|
|
4861
|
+
}
|
|
4862
|
+
/** Supervisor limits the user overrode in Settings; absent keys fall back to the plugin config. */
|
|
4863
|
+
async function readSupervisorLimitOverrides(deps = {}) {
|
|
4864
|
+
let document;
|
|
4865
|
+
try {
|
|
4866
|
+
document = await readDocument(pathsFor(deps).pluginSettingsFile);
|
|
4867
|
+
} catch {
|
|
4868
|
+
return {};
|
|
4869
|
+
}
|
|
4870
|
+
const maxProcesses = document.maxProcesses;
|
|
4871
|
+
const idleTimeoutMinutes = document.idleTimeoutMinutes;
|
|
4872
|
+
return {
|
|
4873
|
+
...isBoundedInteger(maxProcesses, 1, MAX_PROCESSES_LIMIT) ? { maxProcesses } : {},
|
|
4874
|
+
...isBoundedInteger(idleTimeoutMinutes, 1, MAX_IDLE_TIMEOUT_MINUTES) ? { idleTimeoutMs: idleTimeoutMinutes * 6e4 } : {}
|
|
4875
|
+
};
|
|
4496
4876
|
}
|
|
4497
4877
|
async function readWorktreeBranchPrefix(deps = {}) {
|
|
4498
4878
|
const paths = pathsFor(deps);
|
|
@@ -4534,7 +4914,7 @@ function updateGlobalSettings(changes, deps = {}) {
|
|
|
4534
4914
|
}
|
|
4535
4915
|
if (changedDocuments.has("claude")) await atomicWrite(paths.settingsFile, documents.claude);
|
|
4536
4916
|
if (changedDocuments.has("plugin")) await atomicWrite(paths.pluginSettingsFile, documents.plugin);
|
|
4537
|
-
return views(documents, paths);
|
|
4917
|
+
return views(documents, paths, deps.defaultLimits ?? DEFAULT_LIMITS);
|
|
4538
4918
|
});
|
|
4539
4919
|
pendingWrite = operation;
|
|
4540
4920
|
return operation;
|
|
@@ -4562,7 +4942,9 @@ function registerClaudeGlobalSettingsRoute(ctx, deps = {}) {
|
|
|
4562
4942
|
if (req.method !== "GET" && req.method !== "PATCH") return json(res, 405, { error: "method not allowed" });
|
|
4563
4943
|
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
4564
4944
|
try {
|
|
4565
|
-
|
|
4945
|
+
const result = req.method === "GET" ? await readGlobalSettings(deps) : await updateGlobalSettings(await requestJson(req), deps);
|
|
4946
|
+
if (req.method === "PATCH") await deps.onUpdated?.();
|
|
4947
|
+
json(res, 200, result);
|
|
4566
4948
|
} catch (error) {
|
|
4567
4949
|
json(res, 400, { error: error instanceof Error ? error.message : "Invalid global settings request" });
|
|
4568
4950
|
}
|
|
@@ -4692,12 +5074,21 @@ async function installManagedPresetCompatibility(logger, install = ensureManaged
|
|
|
4692
5074
|
}
|
|
4693
5075
|
async function apply(ctx, config) {
|
|
4694
5076
|
await installManagedPresetCompatibility(ctx.logger);
|
|
5077
|
+
const defaultLimits = {
|
|
5078
|
+
idleTimeoutMs: config.idleTimeoutMs ?? 18e5,
|
|
5079
|
+
maxProcesses: config.maxProcesses ?? 4
|
|
5080
|
+
};
|
|
4695
5081
|
const supervisorConfig = {
|
|
4696
5082
|
executablePath: "",
|
|
4697
5083
|
defaultModel: config.model ?? "default",
|
|
4698
|
-
|
|
4699
|
-
maxProcesses: config.maxProcesses ?? 4
|
|
5084
|
+
...defaultLimits
|
|
4700
5085
|
};
|
|
5086
|
+
const applyLimitOverrides = async () => {
|
|
5087
|
+
const overrides = await readSupervisorLimitOverrides();
|
|
5088
|
+
supervisorConfig.idleTimeoutMs = overrides.idleTimeoutMs ?? defaultLimits.idleTimeoutMs;
|
|
5089
|
+
supervisorConfig.maxProcesses = overrides.maxProcesses ?? defaultLimits.maxProcesses;
|
|
5090
|
+
};
|
|
5091
|
+
await applyLimitOverrides();
|
|
4701
5092
|
const sidecar = new ClaudeSidecarRepository();
|
|
4702
5093
|
const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
|
|
4703
5094
|
const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
|
|
@@ -4777,7 +5168,21 @@ async function apply(ctx, config) {
|
|
|
4777
5168
|
ctx.on("agent/disposed", async ({ agent }) => {
|
|
4778
5169
|
reviewComments.disposeSession(agent.id);
|
|
4779
5170
|
await supervisor.disposeSession(agent.id);
|
|
4780
|
-
|
|
5171
|
+
});
|
|
5172
|
+
const injectWorkspaceRegistry = ctx.inject;
|
|
5173
|
+
injectWorkspaceRegistry(["workspaceRegistry"], (sweepCtx) => {
|
|
5174
|
+
const sweep = () => {
|
|
5175
|
+
try {
|
|
5176
|
+
const paths = sweepCtx.workspaceRegistry.list().map((workspace) => workspace.path);
|
|
5177
|
+
repositorySetup.cleanupOrphans(paths).catch(() => void 0);
|
|
5178
|
+
} catch {}
|
|
5179
|
+
};
|
|
5180
|
+
sweepCtx.effect(() => {
|
|
5181
|
+
sweep();
|
|
5182
|
+
const timer = setInterval(sweep, 6e4);
|
|
5183
|
+
timer.unref?.();
|
|
5184
|
+
return () => clearInterval(timer);
|
|
5185
|
+
}, "dsh-claude: worktree reconciliation");
|
|
4781
5186
|
});
|
|
4782
5187
|
ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
|
|
4783
5188
|
ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
|
|
@@ -4786,13 +5191,19 @@ async function apply(ctx, config) {
|
|
|
4786
5191
|
registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
|
|
4787
5192
|
const desktopActions = webCtx.get("desktopActions");
|
|
4788
5193
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
4789
|
-
registerClaudeGlobalSettingsRoute(webCtx
|
|
5194
|
+
registerClaudeGlobalSettingsRoute(webCtx, {
|
|
5195
|
+
defaultLimits,
|
|
5196
|
+
onUpdated: applyLimitOverrides
|
|
5197
|
+
});
|
|
4790
5198
|
registerRepositorySetupRoute(webCtx, repositorySetup);
|
|
4791
|
-
|
|
5199
|
+
const repositoryActions = new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
|
|
5200
|
+
const cwdForClaudeSession = (sessionId) => {
|
|
4792
5201
|
const agent = webCtx.agents.get(sessionId);
|
|
4793
5202
|
if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
|
|
4794
5203
|
return agent.session.header.cwd;
|
|
4795
|
-
}
|
|
5204
|
+
};
|
|
5205
|
+
registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
|
|
5206
|
+
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
|
|
4796
5207
|
const ownsClaudeSession = (sessionId) => {
|
|
4797
5208
|
const agent = webCtx.agents.get(sessionId);
|
|
4798
5209
|
return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
|