@norman-else/dsh-claude 0.1.51 → 0.1.53
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 +95 -30
- package/README.md +25 -15
- package/lib/client.d.ts +3 -3
- package/lib/client.js +386 -357
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +288 -27
- package/lib/index.mjs.map +1 -1
- package/package.json +79 -79
package/lib/index.mjs
CHANGED
|
@@ -3478,13 +3478,17 @@ async function summarizeSessionTitle(executablePath, request, factory = query) {
|
|
|
3478
3478
|
abortController: lifetime,
|
|
3479
3479
|
model: SESSION_TITLE_MODEL,
|
|
3480
3480
|
allowedTools: [],
|
|
3481
|
-
settingSources: [
|
|
3481
|
+
settingSources: [
|
|
3482
|
+
"user",
|
|
3483
|
+
"project",
|
|
3484
|
+
"local"
|
|
3485
|
+
],
|
|
3482
3486
|
maxTurns: 1,
|
|
3483
3487
|
...executablePath.length === 0 ? {} : { pathToClaudeCodeExecutable: executablePath }
|
|
3484
3488
|
}
|
|
3485
3489
|
});
|
|
3486
3490
|
for await (const message of query) {
|
|
3487
|
-
if (message.type !== "result" || message.subtype !== "success") continue;
|
|
3491
|
+
if (message.type !== "result" || message.subtype !== "success" || message.is_error === true) continue;
|
|
3488
3492
|
const title = sessionTitleLine(message.result);
|
|
3489
3493
|
if (title.length > 0) return title;
|
|
3490
3494
|
break;
|
|
@@ -5214,9 +5218,8 @@ function uniqueBranchName(candidate, taken) {
|
|
|
5214
5218
|
/** Compress a composer draft into a branch slug with a throwaway Claude turn.
|
|
5215
5219
|
*
|
|
5216
5220
|
* Deliberately NOT routed through the supervisor, for the same reasons as the
|
|
5217
|
-
* plan-usage probe: there is no session to borrow yet.
|
|
5218
|
-
*
|
|
5219
|
-
* reply in the user's language") turns the answer into an unusable slug. */
|
|
5221
|
+
* plan-usage probe: there is no session to borrow yet. Settings sources match
|
|
5222
|
+
* conversation turns so the CLI can resolve settings-based authentication. */
|
|
5220
5223
|
async function summarizeBranchSlug(executablePath, intent, factory = query) {
|
|
5221
5224
|
const task = intent.trim().slice(0, MAX_INTENT_CHARS);
|
|
5222
5225
|
if (task.length === 0) return void 0;
|
|
@@ -5231,12 +5234,16 @@ async function summarizeBranchSlug(executablePath, intent, factory = query) {
|
|
|
5231
5234
|
abortController: lifetime,
|
|
5232
5235
|
model: BRANCH_SUMMARY_MODEL,
|
|
5233
5236
|
allowedTools: [],
|
|
5234
|
-
settingSources: [
|
|
5237
|
+
settingSources: [
|
|
5238
|
+
"user",
|
|
5239
|
+
"project",
|
|
5240
|
+
"local"
|
|
5241
|
+
],
|
|
5235
5242
|
maxTurns: 1,
|
|
5236
5243
|
...executablePath.length === 0 ? {} : { pathToClaudeCodeExecutable: executablePath }
|
|
5237
5244
|
}
|
|
5238
5245
|
});
|
|
5239
|
-
for await (const message of query) if (message.type === "result" && message.subtype === "success") return branchSlug(message.result);
|
|
5246
|
+
for await (const message of query) if (message.type === "result" && message.subtype === "success" && message.is_error !== true) return branchSlug(message.result);
|
|
5240
5247
|
return;
|
|
5241
5248
|
} catch {
|
|
5242
5249
|
return;
|
|
@@ -5407,9 +5414,10 @@ var RepositorySetupService = class {
|
|
|
5407
5414
|
progress("switching-branch");
|
|
5408
5415
|
return !local && remote ? this.#checkoutRemote(info, branch) : this.#checkout(info, branch);
|
|
5409
5416
|
}
|
|
5410
|
-
/** Tear down a merged branch: remove a
|
|
5411
|
-
*
|
|
5412
|
-
* the
|
|
5417
|
+
/** Tear down a merged branch: remove a worktree (a plugin one with its
|
|
5418
|
+
* lease, or one another tool added) and its branch, or switch a plain
|
|
5419
|
+
* checkout back to the base branch and delete the merged branch. Refuses
|
|
5420
|
+
* dirty trees. */
|
|
5413
5421
|
/** `branch` names the merged branch when the checkout is no longer on it:
|
|
5414
5422
|
* a session that opened a pull request in another clone switched that
|
|
5415
5423
|
* clone back to base itself, and only the local branch is left to delete.
|
|
@@ -5453,6 +5461,42 @@ var RepositorySetupService = class {
|
|
|
5453
5461
|
};
|
|
5454
5462
|
});
|
|
5455
5463
|
}
|
|
5464
|
+
const dirs = await this.#run(git, [
|
|
5465
|
+
"rev-parse",
|
|
5466
|
+
"--path-format=absolute",
|
|
5467
|
+
"--git-dir",
|
|
5468
|
+
"--git-common-dir"
|
|
5469
|
+
], path);
|
|
5470
|
+
const [gitDir = "", commonDir = ""] = dirs.exitCode === 0 ? dirs.stdout.trim().split(/\r?\n/u) : [];
|
|
5471
|
+
if (gitDir.length > 0 && comparablePath(gitDir) !== comparablePath(commonDir)) {
|
|
5472
|
+
const mainRoot = resolve(commonDir, "..");
|
|
5473
|
+
const head = named === void 0 ? await this.#run(git, [
|
|
5474
|
+
"symbolic-ref",
|
|
5475
|
+
"--quiet",
|
|
5476
|
+
"--short",
|
|
5477
|
+
"HEAD"
|
|
5478
|
+
], path) : void 0;
|
|
5479
|
+
const branch = named ?? (head?.exitCode === 0 ? head.stdout.trim() : "");
|
|
5480
|
+
if (branch.length === 0) throw new RepositorySetupError("nothing-to-clean", "The worktree is not on a branch.");
|
|
5481
|
+
if (requirePushed) await this.#requirePushed(git, mainRoot, branch);
|
|
5482
|
+
if ((await this.#run(git, [
|
|
5483
|
+
"worktree",
|
|
5484
|
+
"remove",
|
|
5485
|
+
"--",
|
|
5486
|
+
path
|
|
5487
|
+
], mainRoot)).exitCode !== 0) throw new RepositorySetupError("worktree-remove-failed", "Git could not remove the worktree.");
|
|
5488
|
+
await this.#run(git, [
|
|
5489
|
+
"branch",
|
|
5490
|
+
"-D",
|
|
5491
|
+
"--",
|
|
5492
|
+
branch
|
|
5493
|
+
], mainRoot).catch(() => void 0);
|
|
5494
|
+
return {
|
|
5495
|
+
mode: "worktree",
|
|
5496
|
+
root: mainRoot,
|
|
5497
|
+
branch
|
|
5498
|
+
};
|
|
5499
|
+
}
|
|
5456
5500
|
const root = await this.#repositoryRoot(git, path);
|
|
5457
5501
|
if (base === void 0) throw new RepositorySetupError("nothing-to-clean", "A plain checkout needs the base branch to return to.");
|
|
5458
5502
|
const head = await this.#run(git, [
|
|
@@ -5849,9 +5893,19 @@ var RepositorySetupService = class {
|
|
|
5849
5893
|
//#region src/repository-actions.ts
|
|
5850
5894
|
const MAX_OUTPUT_BYTES$1 = 262144;
|
|
5851
5895
|
const MAX_PATCH_CHARS = 65536;
|
|
5852
|
-
const MAX_MESSAGE_CHARS =
|
|
5896
|
+
const MAX_MESSAGE_CHARS = 2048;
|
|
5853
5897
|
const MAX_PR_TEXT_CHARS = 8192;
|
|
5854
5898
|
const MAX_UNPUSHED_COMMITS = 20;
|
|
5899
|
+
/** What one generation shows the model: the whole patch when it fits, the
|
|
5900
|
+
* head of it otherwise, and the prompt says which. */
|
|
5901
|
+
const MAX_GENERATE_PATCH_CHARS = 49152;
|
|
5902
|
+
const MAX_SUBJECT_CHARS = 72;
|
|
5903
|
+
/** Past this the first line is not a subject at all; under it, a subject a
|
|
5904
|
+
* few characters over what the prompt asked for is the user's to trim. */
|
|
5905
|
+
const MAX_SUBJECT_KEPT_CHARS = 120;
|
|
5906
|
+
const MAX_PR_TITLE_CHARS = 100;
|
|
5907
|
+
const MAX_RECENT_SUBJECTS = 10;
|
|
5908
|
+
const MAX_PR_COMMITS = 50;
|
|
5855
5909
|
const GIT_TIMEOUT_MS$1 = 15e3;
|
|
5856
5910
|
const REMOTE_TIMEOUT_MS = 6e4;
|
|
5857
5911
|
const GENERATE_TIMEOUT_MS = 6e4;
|
|
@@ -5859,10 +5913,19 @@ const GENERATE_TIMEOUT_MS = 6e4;
|
|
|
5859
5913
|
* use is cost: MCP servers (which `ask` already skips for the same reason,
|
|
5860
5914
|
* and which stall for as long as an unreachable one takes to give up) and the
|
|
5861
5915
|
* user's own hooks and settings. Project settings stay: a repository's commit
|
|
5862
|
-
* conventions belong in the message.
|
|
5863
|
-
*
|
|
5916
|
+
* conventions belong in the message. The prompt itself goes in on stdin:
|
|
5917
|
+
* a 48 KB diff on argv is past what Windows lets a process be started with
|
|
5918
|
+
* (ENAMETOOLONG, and the fallback subject where a message should be).
|
|
5919
|
+
*
|
|
5920
|
+
* Sonnet rather than the session's default: describing a diff needs no
|
|
5921
|
+
* frontier model, but telling three unrelated changes apart in one diff is
|
|
5922
|
+
* where haiku starts to blur them, and sonnet costs only a second or two
|
|
5923
|
+
* more. Extended thinking is off (see {@link GENERATE_ENV}): the naming call
|
|
5924
|
+
* in prompts.ts measured it as most of a ten-second run. */
|
|
5864
5925
|
const GENERATE_ARGUMENTS = [
|
|
5865
5926
|
"-p",
|
|
5927
|
+
"--model",
|
|
5928
|
+
"sonnet",
|
|
5866
5929
|
"--strict-mcp-config",
|
|
5867
5930
|
"--mcp-config",
|
|
5868
5931
|
"{\"mcpServers\":{}}",
|
|
@@ -5873,6 +5936,8 @@ const GENERATE_ARGUMENTS = [
|
|
|
5873
5936
|
"--output-format",
|
|
5874
5937
|
"text"
|
|
5875
5938
|
];
|
|
5939
|
+
/** A CLI that stops honouring the variable is slow again, never wrong. */
|
|
5940
|
+
const GENERATE_ENV = { MAX_THINKING_TOKENS: "0" };
|
|
5876
5941
|
var RepositoryActionError = class extends Error {
|
|
5877
5942
|
code;
|
|
5878
5943
|
commit;
|
|
@@ -5932,11 +5997,82 @@ function fallbackCommitMessage(files) {
|
|
|
5932
5997
|
if (files.length === 1) return `Update ${files[0]?.path ?? "repository files"}`;
|
|
5933
5998
|
return `Update ${files.length} repository files`;
|
|
5934
5999
|
}
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
6000
|
+
/** The model's answer as lines: fences and a trailing prose apology dropped,
|
|
6001
|
+
* carriage returns and NULs (which git and the request validator refuse)
|
|
6002
|
+
* gone, leading blank lines gone. */
|
|
6003
|
+
function answerLines(value) {
|
|
6004
|
+
const kept = value.replace(/\0/gu, "").split(/\r?\n/u).map((line) => line.trimEnd()).filter((line) => !line.trim().startsWith("```"));
|
|
6005
|
+
while (kept.length > 0 && kept[0]?.trim() === "") kept.shift();
|
|
6006
|
+
while (kept.length > 0 && kept.at(-1)?.trim() === "") kept.pop();
|
|
6007
|
+
return kept;
|
|
6008
|
+
}
|
|
6009
|
+
function unquoted(line) {
|
|
6010
|
+
return line.trim().replace(/^['"`]+|['"`]+$/gu, "").trim();
|
|
6011
|
+
}
|
|
6012
|
+
/** Subject line, then a body when the model wrote one: the subject must at
|
|
6013
|
+
* least look like one, the body is kept as the bullets the model listed, and the
|
|
6014
|
+
* whole thing is cut at a line boundary under the commit-message cap. */
|
|
6015
|
+
function normalizeCommitMessage(value, fallback) {
|
|
6016
|
+
const lines = answerLines(value);
|
|
6017
|
+
const subject = unquoted(lines[0] ?? "");
|
|
6018
|
+
if (subject.length === 0 || subject.length > MAX_SUBJECT_KEPT_CHARS) return fallback;
|
|
6019
|
+
const body = lines.slice(1);
|
|
6020
|
+
while (body.length > 0 && body[0]?.trim() === "") body.shift();
|
|
6021
|
+
const kept = [subject];
|
|
6022
|
+
if (body.length > 0) kept.push("");
|
|
6023
|
+
let previousBlank = false;
|
|
6024
|
+
for (const line of body) {
|
|
6025
|
+
const blank = line.trim() === "";
|
|
6026
|
+
if (blank && previousBlank) continue;
|
|
6027
|
+
previousBlank = blank;
|
|
6028
|
+
if ([...kept, line].join("\n").length > MAX_MESSAGE_CHARS) break;
|
|
6029
|
+
kept.push(line);
|
|
6030
|
+
}
|
|
6031
|
+
while (kept.length > 1 && kept.at(-1)?.trim() === "") kept.pop();
|
|
6032
|
+
return kept.join("\n");
|
|
6033
|
+
}
|
|
6034
|
+
/** `Title:` on its own line, then the `Summary:` / `Changes:` body the
|
|
6035
|
+
* create-pr arm insists on. Anything else is the caller's fallback. */
|
|
6036
|
+
function parsePullRequestText(value) {
|
|
6037
|
+
const lines = answerLines(value);
|
|
6038
|
+
const titleAt = lines.findIndex((line) => /^title:/iu.test(line.trim()));
|
|
6039
|
+
if (titleAt < 0) return void 0;
|
|
6040
|
+
const title = unquoted(lines[titleAt].trim().replace(/^title:/iu, ""));
|
|
6041
|
+
if (title.length === 0 || title.length > MAX_PR_TITLE_CHARS) return void 0;
|
|
6042
|
+
const summaryAt = lines.findIndex((line, index) => index > titleAt && /^summary:/iu.test(line.trim()));
|
|
6043
|
+
if (summaryAt < 0) return void 0;
|
|
6044
|
+
const body = lines.slice(summaryAt).map((line) => line.trim() === "" ? "" : line).join("\n").trim();
|
|
6045
|
+
return validPullRequestBody(body) && body.length <= MAX_PR_TEXT_CHARS ? {
|
|
6046
|
+
title,
|
|
6047
|
+
body
|
|
6048
|
+
} : void 0;
|
|
6049
|
+
}
|
|
6050
|
+
function fallbackPullRequestText(commits, files) {
|
|
6051
|
+
const title = commits[0]?.subject ?? fallbackCommitMessage(files);
|
|
6052
|
+
const changes = commits.length > 0 ? commits.map((commit) => commit.subject) : files.map((file) => `Update ${file.path}`);
|
|
6053
|
+
return {
|
|
6054
|
+
title,
|
|
6055
|
+
body: `Summary: ${title}\n\nChanges:\n${(changes.length > 0 ? changes : [title]).map((item) => `- ${item}`).join("\n")}`
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
function parseBranchCommits(output) {
|
|
6059
|
+
return output.split("\0").flatMap((record) => {
|
|
6060
|
+
const [subject = "", ...rest] = record.replace(/^\r?\n/u, "").split(/\r?\n/u);
|
|
6061
|
+
if (subject.trim().length === 0) return [];
|
|
6062
|
+
return [{
|
|
6063
|
+
subject: subject.trim().slice(0, 140),
|
|
6064
|
+
body: rest.join("\n").trim()
|
|
6065
|
+
}];
|
|
6066
|
+
}).slice(0, MAX_PR_COMMITS);
|
|
6067
|
+
}
|
|
6068
|
+
function boundedPatch(patch) {
|
|
6069
|
+
return patch.length > MAX_GENERATE_PATCH_CHARS ? {
|
|
6070
|
+
text: patch.slice(0, MAX_GENERATE_PATCH_CHARS),
|
|
6071
|
+
truncated: true
|
|
6072
|
+
} : {
|
|
6073
|
+
text: patch,
|
|
6074
|
+
truncated: false
|
|
6075
|
+
};
|
|
5940
6076
|
}
|
|
5941
6077
|
function validPrUrl(value) {
|
|
5942
6078
|
try {
|
|
@@ -5973,19 +6109,126 @@ var RepositoryActionService = class {
|
|
|
5973
6109
|
const preview = await this.#preview(cwd);
|
|
5974
6110
|
if (preview.fingerprint !== fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
|
|
5975
6111
|
const fallback = fallbackCommitMessage(preview.files);
|
|
6112
|
+
const git = await this.#git();
|
|
6113
|
+
const recent = await this.#run(git, [
|
|
6114
|
+
"log",
|
|
6115
|
+
"--no-merges",
|
|
6116
|
+
"--format=%s",
|
|
6117
|
+
"-n",
|
|
6118
|
+
String(MAX_RECENT_SUBJECTS),
|
|
6119
|
+
"HEAD",
|
|
6120
|
+
"--"
|
|
6121
|
+
], preview.root, GIT_TIMEOUT_MS$1);
|
|
6122
|
+
const subjects = recent.exitCode === 0 && !recent.lossy ? recent.stdout.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0) : [];
|
|
6123
|
+
const patch = boundedPatch(preview.patch);
|
|
5976
6124
|
const prompt = [
|
|
5977
|
-
"Write
|
|
5978
|
-
|
|
6125
|
+
"Write a git commit message for the changes below, in English.",
|
|
6126
|
+
`Line 1 is the subject: imperative mood, at most ${MAX_SUBJECT_CHARS} characters, saying what the change does rather than which files it touches.`,
|
|
6127
|
+
"If the diff contains more than one independent change, leave line 2 blank and then list each change on its own line starting with \"- \", one sentence each, describing what changed as the diff shows it.",
|
|
6128
|
+
"A change with a single purpose gets the subject line only.",
|
|
6129
|
+
"Describe only what the diff shows. Do not invent motivation, do not summarise the file list, and do not mention that the diff is truncated.",
|
|
6130
|
+
"Return only the message: no quotes, no markdown fences, no explanation before or after it.",
|
|
6131
|
+
...subjects.length > 0 ? [`Recent commit subjects of this repository, as a style reference only:\n${subjects.map((subject) => `- ${subject}`).join("\n")}`] : [],
|
|
5979
6132
|
`Files: ${preview.files.map((file) => file.path).join(", ")}`,
|
|
5980
|
-
|
|
6133
|
+
...patch.truncated || preview.truncated ? ["The diff below is cut short; the file list above is complete."] : [],
|
|
6134
|
+
`Diff:\n${patch.text}`
|
|
5981
6135
|
].join("\n");
|
|
5982
6136
|
try {
|
|
5983
|
-
const result = await this.#run(this.#claudeExecutable, GENERATE_ARGUMENTS
|
|
5984
|
-
return result.exitCode === 0 && !result.lossy ?
|
|
6137
|
+
const result = await this.#run(this.#claudeExecutable, GENERATE_ARGUMENTS, preview.root, GENERATE_TIMEOUT_MS, MAX_OUTPUT_BYTES$1, GENERATE_ENV, prompt);
|
|
6138
|
+
return result.exitCode === 0 && !result.lossy ? normalizeCommitMessage(result.stdout, fallback) : fallback;
|
|
5985
6139
|
} catch {
|
|
5986
6140
|
return fallback;
|
|
5987
6141
|
}
|
|
5988
6142
|
}
|
|
6143
|
+
/** Title and description for the pull request the branch would open: the
|
|
6144
|
+
* commits and diff since the base, plus whatever is still uncommitted,
|
|
6145
|
+
* since create-pr commits that first. Base is the named branch on origin,
|
|
6146
|
+
* else origin's default; with neither, the tree alone. */
|
|
6147
|
+
async generatePullRequest(cwd, fingerprint, baseBranch) {
|
|
6148
|
+
const preview = await this.#preview(cwd);
|
|
6149
|
+
if (preview.fingerprint !== fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
|
|
6150
|
+
const git = await this.#git();
|
|
6151
|
+
const base = await this.#baseRef(git, preview.root, baseBranch);
|
|
6152
|
+
let commits = [];
|
|
6153
|
+
let branchPatch = "";
|
|
6154
|
+
let branchTruncated = false;
|
|
6155
|
+
if (base !== void 0) {
|
|
6156
|
+
const log = await this.#run(git, [
|
|
6157
|
+
"log",
|
|
6158
|
+
"--no-merges",
|
|
6159
|
+
"--format=%s%n%b%x00",
|
|
6160
|
+
"-n",
|
|
6161
|
+
String(51),
|
|
6162
|
+
`${base}..HEAD`,
|
|
6163
|
+
"--"
|
|
6164
|
+
], preview.root, GIT_TIMEOUT_MS$1);
|
|
6165
|
+
if (log.exitCode === 0 && !log.lossy) commits = parseBranchCommits(log.stdout);
|
|
6166
|
+
const funcname = await diffFuncnameArgs();
|
|
6167
|
+
const diff = await this.#run(git, [
|
|
6168
|
+
...funcname,
|
|
6169
|
+
"diff",
|
|
6170
|
+
"--no-ext-diff",
|
|
6171
|
+
"--no-color",
|
|
6172
|
+
"--unified=3",
|
|
6173
|
+
`${base}...HEAD`,
|
|
6174
|
+
"--",
|
|
6175
|
+
":(exclude)WARP.md",
|
|
6176
|
+
":(exclude)**/WARP.md"
|
|
6177
|
+
], preview.root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1);
|
|
6178
|
+
if (diff.exitCode === 0) {
|
|
6179
|
+
branchPatch = diff.stdout;
|
|
6180
|
+
branchTruncated = diff.lossy;
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
const fallback = fallbackPullRequestText(commits, preview.files);
|
|
6184
|
+
const patch = boundedPatch([branchPatch, preview.patch].filter((part) => part.length > 0).join("\n"));
|
|
6185
|
+
const prompt = [
|
|
6186
|
+
"Write the title and description of a GitHub pull request for the changes below, in English.",
|
|
6187
|
+
"Answer in exactly this shape and nothing else:",
|
|
6188
|
+
`Title: <imperative title, at most ${MAX_SUBJECT_CHARS} characters, saying what the pull request does>`,
|
|
6189
|
+
"Summary: <one sentence saying what the pull request achieves as a whole>",
|
|
6190
|
+
"",
|
|
6191
|
+
"Changes:",
|
|
6192
|
+
"- <one line per independent change, describing what changed in the code>",
|
|
6193
|
+
"",
|
|
6194
|
+
"List every independent change the diff contains as its own bullet. Do not merge unrelated changes into one bullet, and do not describe anything the diff does not show.",
|
|
6195
|
+
"No markdown headings, no quotes, no fences, no text before \"Title:\" or after the last bullet.",
|
|
6196
|
+
...commits.length > 0 ? [`Commits on this branch, newest first:\n${commits.map((commit) => commit.body.length > 0 ? `- ${commit.subject}\n${commit.body}` : `- ${commit.subject}`).join("\n")}`] : [],
|
|
6197
|
+
...preview.files.length > 0 ? [`Uncommitted files that will go into the same pull request: ${preview.files.map((file) => file.path).join(", ")}`] : [],
|
|
6198
|
+
...patch.truncated || branchTruncated || preview.truncated ? ["The diff below is cut short; the commit list is complete."] : [],
|
|
6199
|
+
`Diff:\n${patch.text}`
|
|
6200
|
+
].join("\n");
|
|
6201
|
+
try {
|
|
6202
|
+
const result = await this.#run(this.#claudeExecutable, GENERATE_ARGUMENTS, preview.root, GENERATE_TIMEOUT_MS, MAX_OUTPUT_BYTES$1, GENERATE_ENV, prompt);
|
|
6203
|
+
return (result.exitCode === 0 && !result.lossy ? parsePullRequestText(result.stdout) : void 0) ?? fallback;
|
|
6204
|
+
} catch {
|
|
6205
|
+
return fallback;
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
/** `origin/<branch>` when the named branch exists on origin, else the branch
|
|
6209
|
+
* origin's HEAD points at. Neither on a checkout that never fetched. */
|
|
6210
|
+
async #baseRef(git, root, baseBranch) {
|
|
6211
|
+
const named = baseBranch?.trim() ?? "";
|
|
6212
|
+
if (named.length > 0) {
|
|
6213
|
+
if (/[\0\r\n\s]|\.\.|^-/u.test(named)) return void 0;
|
|
6214
|
+
const verified = await this.#run(git, [
|
|
6215
|
+
"rev-parse",
|
|
6216
|
+
"--verify",
|
|
6217
|
+
"--quiet",
|
|
6218
|
+
"--symbolic-full-name",
|
|
6219
|
+
`refs/remotes/origin/${named}`
|
|
6220
|
+
], root, GIT_TIMEOUT_MS$1);
|
|
6221
|
+
return verified.exitCode === 0 && !verified.lossy && verified.stdout.trim() === `refs/remotes/origin/${named}` ? `origin/${named}` : void 0;
|
|
6222
|
+
}
|
|
6223
|
+
const head = await this.#run(git, [
|
|
6224
|
+
"symbolic-ref",
|
|
6225
|
+
"--quiet",
|
|
6226
|
+
"--short",
|
|
6227
|
+
"refs/remotes/origin/HEAD"
|
|
6228
|
+
], root, GIT_TIMEOUT_MS$1);
|
|
6229
|
+
const ref = head.stdout.trim();
|
|
6230
|
+
return head.exitCode === 0 && !head.lossy && /^origin\/[^\s]+$/u.test(ref) ? ref : void 0;
|
|
6231
|
+
}
|
|
5989
6232
|
execute(cwd, request) {
|
|
5990
6233
|
const operation = this.#pending.then(() => this.#execute(cwd, request));
|
|
5991
6234
|
this.#pending = operation.then(() => void 0, () => void 0);
|
|
@@ -6378,18 +6621,18 @@ var RepositoryActionService = class {
|
|
|
6378
6621
|
if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
|
|
6379
6622
|
return result;
|
|
6380
6623
|
}
|
|
6381
|
-
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
|
|
6624
|
+
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1, env = {}, stdin) {
|
|
6382
6625
|
return collect$1(this.#runtime.spawn({
|
|
6383
6626
|
argv: [executable, ...args],
|
|
6384
6627
|
cwd,
|
|
6385
6628
|
stdio: {
|
|
6386
|
-
stdin: "ignore",
|
|
6629
|
+
stdin: stdin === void 0 ? "ignore" : { data: stdin },
|
|
6387
6630
|
stdout: { maxBytes },
|
|
6388
6631
|
stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
|
|
6389
6632
|
},
|
|
6390
6633
|
graceMs: 1e3,
|
|
6391
6634
|
signal: AbortSignal.timeout(timeoutMs),
|
|
6392
|
-
env
|
|
6635
|
+
env
|
|
6393
6636
|
}));
|
|
6394
6637
|
}
|
|
6395
6638
|
};
|
|
@@ -6648,6 +6891,17 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
6648
6891
|
value: { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) }
|
|
6649
6892
|
};
|
|
6650
6893
|
}
|
|
6894
|
+
if (url.pathname === `/plugins/dsh-claude/repository/action/pull-request`) {
|
|
6895
|
+
if (io.method !== "POST") return {
|
|
6896
|
+
status: 405,
|
|
6897
|
+
value: { error: "method not allowed" }
|
|
6898
|
+
};
|
|
6899
|
+
const input = await readJson$5(io);
|
|
6900
|
+
return {
|
|
6901
|
+
status: 200,
|
|
6902
|
+
value: await service.generatePullRequest(cwd, string$1(input, "fingerprint"), optionalString(input, "baseBranch"))
|
|
6903
|
+
};
|
|
6904
|
+
}
|
|
6651
6905
|
if (url.pathname === "/plugins/dsh-claude/repository/action") {
|
|
6652
6906
|
if (io.method !== "POST") return {
|
|
6653
6907
|
status: 405,
|
|
@@ -8762,6 +9016,13 @@ async function isDirectory(path) {
|
|
|
8762
9016
|
}
|
|
8763
9017
|
/** Repository roots behind the touched paths, minus the session's own, in
|
|
8764
9018
|
* first-seen order. `rootOf` answers undefined outside any repository. */
|
|
9019
|
+
/** Whether `child` lies strictly under `parent`. Compared with one separator:
|
|
9020
|
+
* on Windows git prints forward slashes where Node resolves to backslashes,
|
|
9021
|
+
* and a root can arrive in either form. */
|
|
9022
|
+
function isInside(child, parent) {
|
|
9023
|
+
const slashed = (value) => value.replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
9024
|
+
return slashed(child).startsWith(`${slashed(parent)}/`);
|
|
9025
|
+
}
|
|
8765
9026
|
async function touchedRepositoryRoots(paths, sessionRoot, rootOf, max, directory = isDirectory) {
|
|
8766
9027
|
const roots = [];
|
|
8767
9028
|
const directories = /* @__PURE__ */ new Set();
|
|
@@ -8769,7 +9030,7 @@ async function touchedRepositoryRoots(paths, sessionRoot, rootOf, max, directory
|
|
|
8769
9030
|
for (const directory of directories) {
|
|
8770
9031
|
const root = await rootOf(directory);
|
|
8771
9032
|
if (root === void 0 || root === sessionRoot || roots.includes(root)) continue;
|
|
8772
|
-
if (sessionRoot
|
|
9033
|
+
if (isInside(sessionRoot, root)) continue;
|
|
8773
9034
|
roots.push(root);
|
|
8774
9035
|
if (roots.length >= max) break;
|
|
8775
9036
|
}
|