mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-19 14:01:22 +00:00
Stabilize SDK mode integration test
This commit is contained in:
parent
24e99ac5e7
commit
ec8b6afea9
274 changed files with 5412 additions and 7893 deletions
|
|
@ -52,9 +52,7 @@ export class DaytonaClient {
|
|||
image: options.image,
|
||||
envVars: options.envVars,
|
||||
labels: options.labels,
|
||||
...(options.autoStopInterval !== undefined
|
||||
? { autoStopInterval: options.autoStopInterval }
|
||||
: {}),
|
||||
...(options.autoStopInterval !== undefined ? { autoStopInterval: options.autoStopInterval } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -32,18 +32,10 @@ function commandLabel(cmd: SpiceCommand): string {
|
|||
|
||||
function looksMissing(error: unknown): boolean {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
return (
|
||||
detail.includes("ENOENT") ||
|
||||
detail.includes("not a git command") ||
|
||||
detail.includes("command not found")
|
||||
);
|
||||
return detail.includes("ENOENT") || detail.includes("not a git command") || detail.includes("command not found");
|
||||
}
|
||||
|
||||
async function tryRun(
|
||||
repoPath: string,
|
||||
cmd: SpiceCommand,
|
||||
args: string[]
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
async function tryRun(repoPath: string, cmd: SpiceCommand, args: string[]): Promise<{ stdout: string; stderr: string }> {
|
||||
return await execFileAsync(cmd.command, [...cmd.prefix, ...args], {
|
||||
cwd: repoPath,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
|
|
@ -51,8 +43,8 @@ async function tryRun(
|
|||
env: {
|
||||
...process.env,
|
||||
NO_COLOR: "1",
|
||||
FORCE_COLOR: "0"
|
||||
}
|
||||
FORCE_COLOR: "0",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -140,14 +132,7 @@ export async function gitSpiceAvailable(repoPath: string): Promise<boolean> {
|
|||
|
||||
export async function gitSpiceListStack(repoPath: string): Promise<SpiceStackEntry[]> {
|
||||
try {
|
||||
const { stdout } = await runSpice(repoPath, [
|
||||
"log",
|
||||
"short",
|
||||
"--all",
|
||||
"--json",
|
||||
"--no-cr-status",
|
||||
"--no-prompt"
|
||||
]);
|
||||
const { stdout } = await runSpice(repoPath, ["log", "short", "--all", "--json", "--no-cr-status", "--no-prompt"]);
|
||||
return parseLogJson(stdout);
|
||||
} catch {
|
||||
return [];
|
||||
|
|
@ -160,9 +145,9 @@ export async function gitSpiceSyncRepo(repoPath: string): Promise<void> {
|
|||
[
|
||||
["repo", "sync", "--restack", "--no-prompt"],
|
||||
["repo", "sync", "--restack"],
|
||||
["repo", "sync"]
|
||||
["repo", "sync"],
|
||||
],
|
||||
"git-spice repo sync failed"
|
||||
"git-spice repo sync failed",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -171,9 +156,9 @@ export async function gitSpiceRestackRepo(repoPath: string): Promise<void> {
|
|||
repoPath,
|
||||
[
|
||||
["repo", "restack", "--no-prompt"],
|
||||
["repo", "restack"]
|
||||
["repo", "restack"],
|
||||
],
|
||||
"git-spice repo restack failed"
|
||||
"git-spice repo restack failed",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -184,9 +169,9 @@ export async function gitSpiceRestackSubtree(repoPath: string, branchName: strin
|
|||
["upstack", "restack", "--branch", branchName, "--no-prompt"],
|
||||
["upstack", "restack", "--branch", branchName],
|
||||
["branch", "restack", "--branch", branchName, "--no-prompt"],
|
||||
["branch", "restack", "--branch", branchName]
|
||||
["branch", "restack", "--branch", branchName],
|
||||
],
|
||||
`git-spice restack subtree failed for ${branchName}`
|
||||
`git-spice restack subtree failed for ${branchName}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -195,41 +180,33 @@ export async function gitSpiceRebaseBranch(repoPath: string, branchName: string)
|
|||
repoPath,
|
||||
[
|
||||
["branch", "restack", "--branch", branchName, "--no-prompt"],
|
||||
["branch", "restack", "--branch", branchName]
|
||||
["branch", "restack", "--branch", branchName],
|
||||
],
|
||||
`git-spice branch restack failed for ${branchName}`
|
||||
`git-spice branch restack failed for ${branchName}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function gitSpiceReparentBranch(
|
||||
repoPath: string,
|
||||
branchName: string,
|
||||
parentBranch: string
|
||||
): Promise<void> {
|
||||
export async function gitSpiceReparentBranch(repoPath: string, branchName: string, parentBranch: string): Promise<void> {
|
||||
await runFallbacks(
|
||||
repoPath,
|
||||
[
|
||||
["upstack", "onto", "--branch", branchName, parentBranch, "--no-prompt"],
|
||||
["upstack", "onto", "--branch", branchName, parentBranch],
|
||||
["branch", "onto", "--branch", branchName, parentBranch, "--no-prompt"],
|
||||
["branch", "onto", "--branch", branchName, parentBranch]
|
||||
["branch", "onto", "--branch", branchName, parentBranch],
|
||||
],
|
||||
`git-spice reparent failed for ${branchName} -> ${parentBranch}`
|
||||
`git-spice reparent failed for ${branchName} -> ${parentBranch}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function gitSpiceTrackBranch(
|
||||
repoPath: string,
|
||||
branchName: string,
|
||||
parentBranch: string
|
||||
): Promise<void> {
|
||||
export async function gitSpiceTrackBranch(repoPath: string, branchName: string, parentBranch: string): Promise<void> {
|
||||
await runFallbacks(
|
||||
repoPath,
|
||||
[
|
||||
["branch", "track", branchName, "--base", parentBranch, "--no-prompt"],
|
||||
["branch", "track", branchName, "--base", parentBranch]
|
||||
["branch", "track", branchName, "--base", parentBranch],
|
||||
],
|
||||
`git-spice track failed for ${branchName}`
|
||||
`git-spice track failed for ${branchName}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,7 @@ const DEFAULT_GIT_FETCH_TIMEOUT_MS = 2 * 60_000;
|
|||
const DEFAULT_GIT_CLONE_TIMEOUT_MS = 5 * 60_000;
|
||||
|
||||
function resolveGithubToken(): string | null {
|
||||
const token =
|
||||
process.env.GH_TOKEN ??
|
||||
process.env.GITHUB_TOKEN ??
|
||||
process.env.HF_GITHUB_TOKEN ??
|
||||
process.env.HF_GH_TOKEN ??
|
||||
null;
|
||||
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? process.env.HF_GITHUB_TOKEN ?? process.env.HF_GH_TOKEN ?? null;
|
||||
if (!token) return null;
|
||||
const trimmed = token.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
|
|
@ -33,19 +28,18 @@ function ensureAskpassScript(): string {
|
|||
|
||||
// Git invokes $GIT_ASKPASS with the prompt string as argv[1]. Provide both username and password.
|
||||
// We avoid embedding the token in this file; it is read from env at runtime.
|
||||
const content =
|
||||
[
|
||||
"#!/bin/sh",
|
||||
'prompt="$1"',
|
||||
// Prefer GH_TOKEN/GITHUB_TOKEN but support HF_* aliases too.
|
||||
'token="${GH_TOKEN:-${GITHUB_TOKEN:-${HF_GITHUB_TOKEN:-${HF_GH_TOKEN:-}}}}"',
|
||||
'case "$prompt" in',
|
||||
' *Username*) echo "x-access-token" ;;',
|
||||
' *Password*) echo "$token" ;;',
|
||||
' *) echo "" ;;',
|
||||
"esac",
|
||||
"",
|
||||
].join("\n");
|
||||
const content = [
|
||||
"#!/bin/sh",
|
||||
'prompt="$1"',
|
||||
// Prefer GH_TOKEN/GITHUB_TOKEN but support HF_* aliases too.
|
||||
'token="${GH_TOKEN:-${GITHUB_TOKEN:-${HF_GITHUB_TOKEN:-${HF_GH_TOKEN:-}}}}"',
|
||||
'case "$prompt" in',
|
||||
' *Username*) echo "x-access-token" ;;',
|
||||
' *Password*) echo "$token" ;;',
|
||||
' *) echo "" ;;',
|
||||
"esac",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
writeFileSync(path, content, "utf8");
|
||||
chmodSync(path, 0o700);
|
||||
|
|
@ -141,12 +135,7 @@ export async function ensureCloned(remoteUrl: string, targetPath: string): Promi
|
|||
|
||||
export async function remoteDefaultBaseRef(repoPath: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync("git", [
|
||||
"-C",
|
||||
repoPath,
|
||||
"symbolic-ref",
|
||||
"refs/remotes/origin/HEAD",
|
||||
], { env: gitEnv() });
|
||||
const { stdout } = await execFileAsync("git", ["-C", repoPath, "symbolic-ref", "refs/remotes/origin/HEAD"], { env: gitEnv() });
|
||||
const ref = stdout.trim(); // refs/remotes/origin/main
|
||||
const match = ref.match(/^refs\/remotes\/(.+)$/);
|
||||
if (match?.[1]) {
|
||||
|
|
@ -169,17 +158,10 @@ export async function remoteDefaultBaseRef(repoPath: string): Promise<string> {
|
|||
}
|
||||
|
||||
export async function listRemoteBranches(repoPath: string): Promise<BranchSnapshot[]> {
|
||||
const { stdout } = await execFileAsync(
|
||||
"git",
|
||||
[
|
||||
"-C",
|
||||
repoPath,
|
||||
"for-each-ref",
|
||||
"--format=%(refname:short) %(objectname)",
|
||||
"refs/remotes/origin",
|
||||
],
|
||||
{ maxBuffer: 1024 * 1024, env: gitEnv() }
|
||||
);
|
||||
const { stdout } = await execFileAsync("git", ["-C", repoPath, "for-each-ref", "--format=%(refname:short) %(objectname)", "refs/remotes/origin"], {
|
||||
maxBuffer: 1024 * 1024,
|
||||
env: gitEnv(),
|
||||
});
|
||||
|
||||
return stdout
|
||||
.trim()
|
||||
|
|
@ -191,24 +173,12 @@ export async function listRemoteBranches(repoPath: string): Promise<BranchSnapsh
|
|||
const branchName = short.replace(/^origin\//, "");
|
||||
return { branchName, commitSha: commitSha ?? "" };
|
||||
})
|
||||
.filter(
|
||||
(row) =>
|
||||
row.branchName.length > 0 &&
|
||||
row.branchName !== "HEAD" &&
|
||||
row.branchName !== "origin" &&
|
||||
row.commitSha.length > 0,
|
||||
);
|
||||
.filter((row) => row.branchName.length > 0 && row.branchName !== "HEAD" && row.branchName !== "origin" && row.commitSha.length > 0);
|
||||
}
|
||||
|
||||
async function remoteBranchExists(repoPath: string, branchName: string): Promise<boolean> {
|
||||
try {
|
||||
await execFileAsync("git", [
|
||||
"-C",
|
||||
repoPath,
|
||||
"show-ref",
|
||||
"--verify",
|
||||
`refs/remotes/origin/${branchName}`,
|
||||
], { env: gitEnv() });
|
||||
await execFileAsync("git", ["-C", repoPath, "show-ref", "--verify", `refs/remotes/origin/${branchName}`], { env: gitEnv() });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
|
@ -233,11 +203,10 @@ export async function diffStatForBranch(repoPath: string, branchName: string): P
|
|||
try {
|
||||
const baseRef = await remoteDefaultBaseRef(repoPath);
|
||||
const headRef = `origin/${branchName}`;
|
||||
const { stdout } = await execFileAsync(
|
||||
"git",
|
||||
["-C", repoPath, "diff", "--shortstat", `${baseRef}...${headRef}`],
|
||||
{ maxBuffer: 1024 * 1024, env: gitEnv() }
|
||||
);
|
||||
const { stdout } = await execFileAsync("git", ["-C", repoPath, "diff", "--shortstat", `${baseRef}...${headRef}`], {
|
||||
maxBuffer: 1024 * 1024,
|
||||
env: gitEnv(),
|
||||
});
|
||||
const trimmed = stdout.trim();
|
||||
if (!trimmed) {
|
||||
return "+0/-0";
|
||||
|
|
@ -252,20 +221,13 @@ export async function diffStatForBranch(repoPath: string, branchName: string): P
|
|||
}
|
||||
}
|
||||
|
||||
export async function conflictsWithMain(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<boolean> {
|
||||
export async function conflictsWithMain(repoPath: string, branchName: string): Promise<boolean> {
|
||||
try {
|
||||
const baseRef = await remoteDefaultBaseRef(repoPath);
|
||||
const headRef = `origin/${branchName}`;
|
||||
// Use merge-tree (git 2.38+) for a clean conflict check.
|
||||
try {
|
||||
await execFileAsync(
|
||||
"git",
|
||||
["-C", repoPath, "merge-tree", "--write-tree", "--no-messages", baseRef, headRef],
|
||||
{ env: gitEnv() }
|
||||
);
|
||||
await execFileAsync("git", ["-C", repoPath, "merge-tree", "--write-tree", "--no-messages", baseRef, headRef], { env: gitEnv() });
|
||||
// If merge-tree exits 0, no conflicts. Non-zero exit means conflicts.
|
||||
return false;
|
||||
} catch {
|
||||
|
|
@ -279,11 +241,7 @@ export async function conflictsWithMain(
|
|||
|
||||
export async function getOriginOwner(repoPath: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
"git",
|
||||
["-C", repoPath, "remote", "get-url", "origin"],
|
||||
{ env: gitEnv() }
|
||||
);
|
||||
const { stdout } = await execFileAsync("git", ["-C", repoPath, "remote", "get-url", "origin"], { env: gitEnv() });
|
||||
const url = stdout.trim();
|
||||
// Handle SSH: git@github.com:owner/repo.git
|
||||
const sshMatch = url.match(/[:\/]([^\/]+)\/[^\/]+(?:\.git)?$/);
|
||||
|
|
|
|||
|
|
@ -36,9 +36,7 @@ interface GhPrListItem {
|
|||
}>;
|
||||
}
|
||||
|
||||
function parseCiStatus(
|
||||
checks: GhPrListItem["statusCheckRollup"]
|
||||
): string | null {
|
||||
function parseCiStatus(checks: GhPrListItem["statusCheckRollup"]): string | null {
|
||||
if (!checks || checks.length === 0) return null;
|
||||
|
||||
let total = 0;
|
||||
|
|
@ -53,12 +51,7 @@ function parseCiStatus(
|
|||
|
||||
if (conclusion === "SUCCESS" || state === "SUCCESS") {
|
||||
successes++;
|
||||
} else if (
|
||||
status === "IN_PROGRESS" ||
|
||||
status === "QUEUED" ||
|
||||
status === "PENDING" ||
|
||||
state === "PENDING"
|
||||
) {
|
||||
} else if (status === "IN_PROGRESS" || status === "QUEUED" || status === "PENDING" || state === "PENDING") {
|
||||
hasRunning = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -70,9 +63,7 @@ function parseCiStatus(
|
|||
return `${successes}/${total}`;
|
||||
}
|
||||
|
||||
function parseReviewStatus(
|
||||
reviews: GhPrListItem["reviews"]
|
||||
): { status: string | null; reviewer: string | null } {
|
||||
function parseReviewStatus(reviews: GhPrListItem["reviews"]): { status: string | null; reviewer: string | null } {
|
||||
if (!reviews || reviews.length === 0) {
|
||||
return { status: null, reviewer: null };
|
||||
}
|
||||
|
|
@ -120,35 +111,21 @@ function snapshotFromGhItem(item: GhPrListItem): PullRequestSnapshot {
|
|||
isDraft: item.isDraft ?? false,
|
||||
ciStatus: parseCiStatus(item.statusCheckRollup),
|
||||
reviewStatus,
|
||||
reviewer
|
||||
reviewer,
|
||||
};
|
||||
}
|
||||
|
||||
const PR_JSON_FIELDS =
|
||||
"number,headRefName,state,title,url,author,isDraft,statusCheckRollup,reviews";
|
||||
const PR_JSON_FIELDS = "number,headRefName,state,title,url,author,isDraft,statusCheckRollup,reviews";
|
||||
|
||||
export async function listPullRequests(repoPath: string): Promise<PullRequestSnapshot[]> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
"gh",
|
||||
[
|
||||
"pr",
|
||||
"list",
|
||||
"--json",
|
||||
PR_JSON_FIELDS,
|
||||
"--limit",
|
||||
"200"
|
||||
],
|
||||
{ maxBuffer: 1024 * 1024 * 4, cwd: repoPath }
|
||||
);
|
||||
const { stdout } = await execFileAsync("gh", ["pr", "list", "--json", PR_JSON_FIELDS, "--limit", "200"], { maxBuffer: 1024 * 1024 * 4, cwd: repoPath });
|
||||
|
||||
const parsed = JSON.parse(stdout) as GhPrListItem[];
|
||||
|
||||
return parsed.map((item) => {
|
||||
// Handle fork PRs where headRefName may contain "owner:branch"
|
||||
const headRefName = item.headRefName.includes(":")
|
||||
? item.headRefName.split(":").pop() ?? item.headRefName
|
||||
: item.headRefName;
|
||||
const headRefName = item.headRefName.includes(":") ? (item.headRefName.split(":").pop() ?? item.headRefName) : item.headRefName;
|
||||
|
||||
return snapshotFromGhItem({ ...item, headRefName });
|
||||
});
|
||||
|
|
@ -157,22 +134,9 @@ export async function listPullRequests(repoPath: string): Promise<PullRequestSna
|
|||
}
|
||||
}
|
||||
|
||||
export async function getPrInfo(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<PullRequestSnapshot | null> {
|
||||
export async function getPrInfo(repoPath: string, branchName: string): Promise<PullRequestSnapshot | null> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
"gh",
|
||||
[
|
||||
"pr",
|
||||
"view",
|
||||
branchName,
|
||||
"--json",
|
||||
PR_JSON_FIELDS
|
||||
],
|
||||
{ maxBuffer: 1024 * 1024 * 4, cwd: repoPath }
|
||||
);
|
||||
const { stdout } = await execFileAsync("gh", ["pr", "view", branchName, "--json", PR_JSON_FIELDS], { maxBuffer: 1024 * 1024 * 4, cwd: repoPath });
|
||||
|
||||
const item = JSON.parse(stdout) as GhPrListItem;
|
||||
return snapshotFromGhItem(item);
|
||||
|
|
@ -181,12 +145,7 @@ export async function getPrInfo(
|
|||
}
|
||||
}
|
||||
|
||||
export async function createPr(
|
||||
repoPath: string,
|
||||
headBranch: string,
|
||||
title: string,
|
||||
body?: string
|
||||
): Promise<{ number: number; url: string }> {
|
||||
export async function createPr(repoPath: string, headBranch: string, title: string, body?: string): Promise<{ number: number; url: string }> {
|
||||
const args = ["pr", "create", "--title", title, "--head", headBranch];
|
||||
if (body) {
|
||||
args.push("--body", body);
|
||||
|
|
@ -196,7 +155,7 @@ export async function createPr(
|
|||
|
||||
const { stdout } = await execFileAsync("gh", args, {
|
||||
maxBuffer: 1024 * 1024,
|
||||
cwd: repoPath
|
||||
cwd: repoPath,
|
||||
});
|
||||
|
||||
// gh pr create outputs the PR URL on success
|
||||
|
|
@ -208,29 +167,17 @@ export async function createPr(
|
|||
return { number, url };
|
||||
}
|
||||
|
||||
export async function getAllowedMergeMethod(
|
||||
repoPath: string
|
||||
): Promise<"squash" | "rebase" | "merge"> {
|
||||
export async function getAllowedMergeMethod(repoPath: string): Promise<"squash" | "rebase" | "merge"> {
|
||||
try {
|
||||
// Get the repo owner/name from gh
|
||||
const { stdout: repoJson } = await execFileAsync(
|
||||
"gh",
|
||||
["repo", "view", "--json", "owner,name"],
|
||||
{ cwd: repoPath }
|
||||
);
|
||||
const { stdout: repoJson } = await execFileAsync("gh", ["repo", "view", "--json", "owner,name"], { cwd: repoPath });
|
||||
const repo = JSON.parse(repoJson) as { owner: { login: string }; name: string };
|
||||
const repoFullName = `${repo.owner.login}/${repo.name}`;
|
||||
|
||||
const { stdout } = await execFileAsync(
|
||||
"gh",
|
||||
[
|
||||
"api",
|
||||
`repos/${repoFullName}`,
|
||||
"--jq",
|
||||
".allow_squash_merge, .allow_rebase_merge, .allow_merge_commit"
|
||||
],
|
||||
{ maxBuffer: 1024 * 1024, cwd: repoPath }
|
||||
);
|
||||
const { stdout } = await execFileAsync("gh", ["api", `repos/${repoFullName}`, "--jq", ".allow_squash_merge, .allow_rebase_merge, .allow_merge_commit"], {
|
||||
maxBuffer: 1024 * 1024,
|
||||
cwd: repoPath,
|
||||
});
|
||||
|
||||
const lines = stdout.trim().split("\n");
|
||||
const allowSquash = lines[0]?.trim() === "true";
|
||||
|
|
@ -248,23 +195,12 @@ export async function getAllowedMergeMethod(
|
|||
|
||||
export async function mergePr(repoPath: string, prNumber: number): Promise<void> {
|
||||
const method = await getAllowedMergeMethod(repoPath);
|
||||
await execFileAsync(
|
||||
"gh",
|
||||
["pr", "merge", String(prNumber), `--${method}`, "--delete-branch"],
|
||||
{ cwd: repoPath }
|
||||
);
|
||||
await execFileAsync("gh", ["pr", "merge", String(prNumber), `--${method}`, "--delete-branch"], { cwd: repoPath });
|
||||
}
|
||||
|
||||
export async function isPrMerged(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<boolean> {
|
||||
export async function isPrMerged(repoPath: string, branchName: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
"gh",
|
||||
["pr", "view", branchName, "--json", "state"],
|
||||
{ cwd: repoPath }
|
||||
);
|
||||
const { stdout } = await execFileAsync("gh", ["pr", "view", branchName, "--json", "state"], { cwd: repoPath });
|
||||
const parsed = JSON.parse(stdout) as { state: string };
|
||||
return parsed.state.toUpperCase() === "MERGED";
|
||||
} catch {
|
||||
|
|
@ -272,16 +208,9 @@ export async function isPrMerged(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getPrTitle(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<string | null> {
|
||||
export async function getPrTitle(repoPath: string, branchName: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
"gh",
|
||||
["pr", "view", branchName, "--json", "title"],
|
||||
{ cwd: repoPath }
|
||||
);
|
||||
const { stdout } = await execFileAsync("gh", ["pr", "view", branchName, "--json", "title"], { cwd: repoPath });
|
||||
const parsed = JSON.parse(stdout) as { title: string };
|
||||
return parsed.title;
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -21,17 +21,11 @@ export async function graphiteGet(repoPath: string, branchName: string): Promise
|
|||
}
|
||||
}
|
||||
|
||||
export async function graphiteCreateBranch(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<void> {
|
||||
export async function graphiteCreateBranch(repoPath: string, branchName: string): Promise<void> {
|
||||
await execFileAsync("gt", ["create", branchName], { cwd: repoPath });
|
||||
}
|
||||
|
||||
export async function graphiteCheckout(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<void> {
|
||||
export async function graphiteCheckout(repoPath: string, branchName: string): Promise<void> {
|
||||
await execFileAsync("gt", ["checkout", branchName], { cwd: repoPath });
|
||||
}
|
||||
|
||||
|
|
@ -39,17 +33,11 @@ export async function graphiteSubmit(repoPath: string): Promise<void> {
|
|||
await execFileAsync("gt", ["submit", "--no-edit"], { cwd: repoPath });
|
||||
}
|
||||
|
||||
export async function graphiteMergeBranch(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<void> {
|
||||
export async function graphiteMergeBranch(repoPath: string, branchName: string): Promise<void> {
|
||||
await execFileAsync("gt", ["merge", branchName], { cwd: repoPath });
|
||||
}
|
||||
|
||||
export async function graphiteAbandon(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<void> {
|
||||
export async function graphiteAbandon(repoPath: string, branchName: string): Promise<void> {
|
||||
await execFileAsync("gt", ["abandon", branchName], { cwd: repoPath });
|
||||
}
|
||||
|
||||
|
|
@ -58,14 +46,12 @@ export interface GraphiteStackEntry {
|
|||
parentBranch: string | null;
|
||||
}
|
||||
|
||||
export async function graphiteGetStack(
|
||||
repoPath: string
|
||||
): Promise<GraphiteStackEntry[]> {
|
||||
export async function graphiteGetStack(repoPath: string): Promise<GraphiteStackEntry[]> {
|
||||
try {
|
||||
// Try JSON output first
|
||||
const { stdout } = await execFileAsync("gt", ["log", "--json"], {
|
||||
cwd: repoPath,
|
||||
maxBuffer: 1024 * 1024
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(stdout) as Array<{
|
||||
|
|
@ -77,14 +63,14 @@ export async function graphiteGetStack(
|
|||
|
||||
return parsed.map((entry) => ({
|
||||
branchName: entry.branch ?? entry.name ?? "",
|
||||
parentBranch: entry.parent ?? entry.parentBranch ?? null
|
||||
parentBranch: entry.parent ?? entry.parentBranch ?? null,
|
||||
}));
|
||||
} catch {
|
||||
// Fall back to text parsing of `gt log`
|
||||
try {
|
||||
const { stdout } = await execFileAsync("gt", ["log"], {
|
||||
cwd: repoPath,
|
||||
maxBuffer: 1024 * 1024
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const entries: GraphiteStackEntry[] = [];
|
||||
|
|
@ -113,9 +99,7 @@ export async function graphiteGetStack(
|
|||
branchStack.pop();
|
||||
}
|
||||
|
||||
const parentBranch = branchStack.length > 0
|
||||
? branchStack[branchStack.length - 1] ?? null
|
||||
: null;
|
||||
const parentBranch = branchStack.length > 0 ? (branchStack[branchStack.length - 1] ?? null) : null;
|
||||
|
||||
entries.push({ branchName, parentBranch });
|
||||
branchStack.push(branchName);
|
||||
|
|
@ -128,15 +112,12 @@ export async function graphiteGetStack(
|
|||
}
|
||||
}
|
||||
|
||||
export async function graphiteGetParent(
|
||||
repoPath: string,
|
||||
branchName: string
|
||||
): Promise<string | null> {
|
||||
export async function graphiteGetParent(repoPath: string, branchName: string): Promise<string | null> {
|
||||
try {
|
||||
// Try `gt get <branchName>` to see parent info
|
||||
const { stdout } = await execFileAsync("gt", ["get", branchName], {
|
||||
cwd: repoPath,
|
||||
maxBuffer: 1024 * 1024
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
// Parse output for parent branch reference
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
import type { AgentType } from "@openhandoff/shared";
|
||||
import type {
|
||||
ListEventsRequest,
|
||||
ListPage,
|
||||
ListPageRequest,
|
||||
SessionEvent,
|
||||
SessionPersistDriver,
|
||||
SessionRecord
|
||||
} from "sandbox-agent";
|
||||
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from "sandbox-agent";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
export type AgentId = AgentType | "opencode";
|
||||
|
|
@ -118,18 +111,11 @@ export class SandboxAgentClient {
|
|||
const message = err instanceof Error ? err.message : String(err);
|
||||
const lowered = message.toLowerCase();
|
||||
// sandbox-agent server times out long-running ACP prompts and returns a 504-like error.
|
||||
return (
|
||||
lowered.includes("timeout waiting for agent response") ||
|
||||
lowered.includes("timed out waiting for agent response") ||
|
||||
lowered.includes("504")
|
||||
);
|
||||
return lowered.includes("timeout waiting for agent response") || lowered.includes("timed out waiting for agent response") || lowered.includes("504");
|
||||
}
|
||||
|
||||
async createSession(request: string | SandboxSessionCreateRequest): Promise<SandboxSession> {
|
||||
const normalized: SandboxSessionCreateRequest =
|
||||
typeof request === "string"
|
||||
? { prompt: request }
|
||||
: request;
|
||||
const normalized: SandboxSessionCreateRequest = typeof request === "string" ? { prompt: request } : request;
|
||||
const sdk = await this.sdk();
|
||||
// Do not wrap createSession in a local Promise.race timeout. The underlying SDK
|
||||
// call is not abortable, so local timeout races create overlapping ACP requests and
|
||||
|
|
@ -343,18 +329,14 @@ export class SandboxAgentClient {
|
|||
} while (cursor);
|
||||
}
|
||||
|
||||
async generateCommitMessage(
|
||||
dir: string,
|
||||
spec: string,
|
||||
task: string
|
||||
): Promise<string> {
|
||||
async generateCommitMessage(dir: string, spec: string, task: string): Promise<string> {
|
||||
const prompt = [
|
||||
"Generate a conventional commit message for the following changes.",
|
||||
"Return ONLY the commit message, no explanation or markdown formatting.",
|
||||
"",
|
||||
`Task: ${task}`,
|
||||
"",
|
||||
`Spec/diff:\n${spec}`
|
||||
`Spec/diff:\n${spec}`,
|
||||
].join("\n");
|
||||
|
||||
const sdk = await this.sdk();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue