Add configurable worktree target path and session run diagnostics
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, open, unlink, writeFile } from "node:fs/promises";
|
||||
import { mkdir, open, stat, unlink, writeFile } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
@@ -272,6 +272,7 @@ export class ResourceProvisioningOrchestrator {
|
||||
export type GitWorktreeProviderConfig = {
|
||||
rootDirectory: string;
|
||||
baseRef: string;
|
||||
targetPath?: string;
|
||||
};
|
||||
|
||||
export type PortRangeProviderConfig = {
|
||||
@@ -313,6 +314,10 @@ export function createGitWorktreeProvider(
|
||||
provision: async ({ sessionId, workspaceRoot, options }) => {
|
||||
const rootDirectory = readOptionalString(options, "rootDirectory", config.rootDirectory);
|
||||
const baseRef = readOptionalString(options, "baseRef", config.baseRef);
|
||||
const targetPath = normalizeWorktreeTargetPath(
|
||||
readOptionalStringOrUndefined(options, "targetPath") ?? config.targetPath,
|
||||
"targetPath",
|
||||
);
|
||||
|
||||
const repoRoot = await runGit(["-C", workspaceRoot, "rev-parse", "--show-toplevel"]);
|
||||
const worktreeRoot = resolvePath(repoRoot, rootDirectory);
|
||||
@@ -321,6 +326,18 @@ export function createGitWorktreeProvider(
|
||||
const worktreeName = buildScopedName(sessionId);
|
||||
const worktreePath = resolve(worktreeRoot, worktreeName);
|
||||
await runGit(["-C", repoRoot, "worktree", "add", "--detach", worktreePath, baseRef]);
|
||||
if (targetPath) {
|
||||
await runGit(["-C", worktreePath, "sparse-checkout", "init", "--cone"]);
|
||||
await runGit(["-C", worktreePath, "sparse-checkout", "set", targetPath]);
|
||||
}
|
||||
|
||||
const preferredWorkingDirectory = targetPath ? resolve(worktreePath, targetPath) : worktreePath;
|
||||
await assertDirectoryExists(
|
||||
preferredWorkingDirectory,
|
||||
targetPath
|
||||
? `Configured worktree target path "${targetPath}" is not a directory in ref "${baseRef}".`
|
||||
: `Provisioned worktree path "${preferredWorkingDirectory}" does not exist.`,
|
||||
);
|
||||
|
||||
return {
|
||||
kind: "git-worktree",
|
||||
@@ -329,6 +346,7 @@ export function createGitWorktreeProvider(
|
||||
worktreeRoot,
|
||||
worktreePath,
|
||||
baseRef,
|
||||
...(targetPath ? { targetPath } : {}),
|
||||
},
|
||||
soft: {
|
||||
env: {
|
||||
@@ -339,12 +357,14 @@ export function createGitWorktreeProvider(
|
||||
promptSections: [
|
||||
`Git worktree: ${worktreePath}`,
|
||||
`Worktree base ref: ${baseRef}`,
|
||||
...(targetPath ? [`Worktree target path: ${targetPath} (sparse-checkout enabled)`] : []),
|
||||
],
|
||||
metadata: {
|
||||
git_worktree_path: worktreePath,
|
||||
git_worktree_base_ref: baseRef,
|
||||
...(targetPath ? { git_worktree_target_path: targetPath } : {}),
|
||||
},
|
||||
preferredWorkingDirectory: worktreePath,
|
||||
preferredWorkingDirectory,
|
||||
},
|
||||
release: async () => {
|
||||
await runGit(["-C", repoRoot, "worktree", "remove", "--force", worktreePath]);
|
||||
@@ -576,6 +596,21 @@ function readOptionalString(
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function readOptionalStringOrUndefined(
|
||||
options: Record<string, unknown>,
|
||||
key: string,
|
||||
): string | undefined {
|
||||
const value = options[key];
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`Option "${key}" must be a string when provided.`);
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function readOptionalInteger(
|
||||
options: Record<string, unknown>,
|
||||
key: string,
|
||||
@@ -595,6 +630,46 @@ function readOptionalInteger(
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeWorktreeTargetPath(value: string | undefined, key: string): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const slashNormalized = value.replace(/\\/g, "/");
|
||||
if (isAbsolute(slashNormalized) || /^[a-zA-Z]:\//.test(slashNormalized)) {
|
||||
throw new Error(`Option "${key}" must be a relative path within the repository worktree.`);
|
||||
}
|
||||
|
||||
const normalizedSegments = slashNormalized
|
||||
.split("/")
|
||||
.map((segment) => segment.trim())
|
||||
.filter((segment) => segment.length > 0 && segment !== ".");
|
||||
|
||||
if (normalizedSegments.some((segment) => segment === "..")) {
|
||||
throw new Error(`Option "${key}" must not contain ".." path segments.`);
|
||||
}
|
||||
|
||||
if (normalizedSegments.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return normalizedSegments.join("/");
|
||||
}
|
||||
|
||||
async function assertDirectoryExists(path: string, errorMessage: string): Promise<void> {
|
||||
try {
|
||||
const stats = await stat(path);
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function readNumberFromAllocation(allocation: Record<string, JsonValue>, key: string): number {
|
||||
const value = allocation[key];
|
||||
if (typeof value !== "number" || !Number.isInteger(value)) {
|
||||
@@ -642,6 +717,8 @@ export function buildChildResourceRequests(input: ChildResourceSuballocationInpu
|
||||
const parentWorktreePath = readStringFromAllocation(parentGit, "worktreePath");
|
||||
const baseRefRaw = parentGit.baseRef;
|
||||
const baseRef = typeof baseRefRaw === "string" && baseRefRaw.trim().length > 0 ? baseRefRaw : "HEAD";
|
||||
const targetPathRaw = parentGit.targetPath;
|
||||
const targetPath = typeof targetPathRaw === "string" ? targetPathRaw.trim() : "";
|
||||
|
||||
requests.push({
|
||||
kind: "git-worktree",
|
||||
@@ -652,6 +729,7 @@ export function buildChildResourceRequests(input: ChildResourceSuballocationInpu
|
||||
buildScopedName(input.parentSnapshot.sessionId),
|
||||
),
|
||||
baseRef,
|
||||
...(targetPath ? { targetPath } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ function toProvisioningConfig(input: Readonly<AppConfig>): BuiltInProvisioningCo
|
||||
gitWorktree: {
|
||||
rootDirectory: input.provisioning.gitWorktree.rootDirectory,
|
||||
baseRef: input.provisioning.gitWorktree.baseRef,
|
||||
targetPath: input.provisioning.gitWorktree.targetPath,
|
||||
},
|
||||
portRange: {
|
||||
basePort: input.provisioning.portRange.basePort,
|
||||
|
||||
Reference in New Issue
Block a user