Add configurable worktree root
* Add configurable worktree root Allow new Paseo worktrees to be created under a custom worktrees.root from config.json while preserving the existing PASEO_HOME/worktrees default. Thread the resolved root through create/list/archive/ownership flows, MCP/session paths, checkout metadata, schema, docs, and targeted tests. * Address worktree root review feedback Use the resolved Paseo worktree base when a custom PASEO_HOME is provided, and remove an unused worktreesRoot dependency from worktree list handling. * Fix checkout worktree path test on Windows Use platform-native paths in the custom PASEO_HOME regression test so Windows CI compares against the same resolved root shape as production code. * Clarify optional worktrees config docs --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
8c6abcb41f
commit
d406c29e16
@@ -147,6 +147,9 @@ Single file, validated with `PersistedConfigSchema`.
|
|||||||
app: {
|
app: {
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
},
|
},
|
||||||
|
worktrees?: {
|
||||||
|
root?: string // optional root for new worktrees; defaults to $PASEO_HOME/worktrees
|
||||||
|
},
|
||||||
providers: {
|
providers: {
|
||||||
openai: { apiKey: string },
|
openai: { apiKey: string },
|
||||||
local: { modelsDir: string }
|
local: { modelsDir: string }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type { AgentStorage } from "./agent-storage.js";
|
|||||||
|
|
||||||
interface CreateAgentLifecycleDispatchDependencies {
|
interface CreateAgentLifecycleDispatchDependencies {
|
||||||
paseoHome: string;
|
paseoHome: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
agentStorage: AgentStorage;
|
agentStorage: AgentStorage;
|
||||||
github: GitHubService;
|
github: GitHubService;
|
||||||
@@ -106,6 +107,7 @@ export class CreateAgentLifecycleDispatch {
|
|||||||
firstAgentContext,
|
firstAgentContext,
|
||||||
runSetup: false,
|
runSetup: false,
|
||||||
paseoHome: this.dependencies.paseoHome,
|
paseoHome: this.dependencies.paseoHome,
|
||||||
|
worktreesRoot: this.dependencies.worktreesRoot,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
switch (target.mode) {
|
switch (target.mode) {
|
||||||
@@ -191,6 +193,7 @@ export class CreateAgentLifecycleDispatch {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const ownership = await isPaseoOwnedWorktreeCwd(options.worktreePath, {
|
const ownership = await isPaseoOwnedWorktreeCwd(options.worktreePath, {
|
||||||
paseoHome: this.dependencies.paseoHome,
|
paseoHome: this.dependencies.paseoHome,
|
||||||
|
worktreesRoot: this.dependencies.worktreesRoot,
|
||||||
});
|
});
|
||||||
if (!ownership.allowed) {
|
if (!ownership.allowed) {
|
||||||
throw new Error("Auto-created worktree is not a Paseo-owned worktree");
|
throw new Error("Auto-created worktree is not a Paseo-owned worktree");
|
||||||
@@ -199,6 +202,7 @@ export class CreateAgentLifecycleDispatch {
|
|||||||
await archivePaseoWorktree(
|
await archivePaseoWorktree(
|
||||||
{
|
{
|
||||||
paseoHome: this.dependencies.paseoHome,
|
paseoHome: this.dependencies.paseoHome,
|
||||||
|
worktreesRoot: this.dependencies.worktreesRoot,
|
||||||
github: this.dependencies.github,
|
github: this.dependencies.github,
|
||||||
workspaceGitService: this.dependencies.workspaceGitService,
|
workspaceGitService: this.dependencies.workspaceGitService,
|
||||||
agentManager: this.dependencies.agentManager,
|
agentManager: this.dependencies.agentManager,
|
||||||
@@ -215,6 +219,7 @@ export class CreateAgentLifecycleDispatch {
|
|||||||
targetPath: options.worktreePath,
|
targetPath: options.worktreePath,
|
||||||
repoRoot: options.repoRoot ?? ownership.repoRoot ?? null,
|
repoRoot: options.repoRoot ?? ownership.repoRoot ?? null,
|
||||||
worktreesRoot: ownership.worktreeRoot,
|
worktreesRoot: ownership.worktreeRoot,
|
||||||
|
worktreesBaseRoot: this.dependencies.worktreesRoot,
|
||||||
requestId: randomUUID(),
|
requestId: randomUUID(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ interface CreateAgentCommandDependencies {
|
|||||||
agentStorage: AgentStorage;
|
agentStorage: AgentStorage;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
workspaceGitService?: Pick<
|
workspaceGitService?: Pick<
|
||||||
WorkspaceGitService,
|
WorkspaceGitService,
|
||||||
"getSnapshot" | "listWorktrees" | "resolveRepoRoot"
|
"getSnapshot" | "listWorktrees" | "resolveRepoRoot"
|
||||||
@@ -419,6 +420,7 @@ async function resolveMcpCwd(params: {
|
|||||||
...(params.initialPrompt ? { firstAgentContext: { prompt: params.initialPrompt } } : {}),
|
...(params.initialPrompt ? { firstAgentContext: { prompt: params.initialPrompt } } : {}),
|
||||||
runSetup: false,
|
runSetup: false,
|
||||||
paseoHome: dependencies.paseoHome,
|
paseoHome: dependencies.paseoHome,
|
||||||
|
worktreesRoot: dependencies.worktreesRoot,
|
||||||
},
|
},
|
||||||
createPaseoWorktree: dependencies.createPaseoWorktree,
|
createPaseoWorktree: dependencies.createPaseoWorktree,
|
||||||
resolveDefaultBranch: baseBranch ? async () => baseBranch : undefined,
|
resolveDefaultBranch: baseBranch ? async () => baseBranch : undefined,
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export interface AgentMcpServerOptions {
|
|||||||
clearWorkspaceArchiving?: ArchivePaseoWorktreeDependencies["clearWorkspaceArchiving"];
|
clearWorkspaceArchiving?: ArchivePaseoWorktreeDependencies["clearWorkspaceArchiving"];
|
||||||
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
|
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
/**
|
/**
|
||||||
* ID of the agent that is connecting to this MCP server.
|
* ID of the agent that is connecting to this MCP server.
|
||||||
* Used for cwd/mode inheritance when agents spawn child agents.
|
* Used for cwd/mode inheritance when agents spawn child agents.
|
||||||
@@ -918,6 +919,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
agentStorage,
|
agentStorage,
|
||||||
logger: childLogger,
|
logger: childLogger,
|
||||||
paseoHome: options.paseoHome,
|
paseoHome: options.paseoHome,
|
||||||
|
worktreesRoot: options.worktreesRoot,
|
||||||
workspaceGitService: options.workspaceGitService,
|
workspaceGitService: options.workspaceGitService,
|
||||||
terminalManager,
|
terminalManager,
|
||||||
providerSnapshotManager,
|
providerSnapshotManager,
|
||||||
@@ -2152,6 +2154,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
const commandResult = await createPaseoWorktreeCommand(
|
const commandResult = await createPaseoWorktreeCommand(
|
||||||
{
|
{
|
||||||
paseoHome: options.paseoHome,
|
paseoHome: options.paseoHome,
|
||||||
|
worktreesRoot: options.worktreesRoot,
|
||||||
createPaseoWorktreeWorkflow: options.createPaseoWorktree,
|
createPaseoWorktreeWorkflow: options.createPaseoWorktree,
|
||||||
},
|
},
|
||||||
createMcpWorktreeCommandInput(repoRoot, target),
|
createMcpWorktreeCommandInput(repoRoot, target),
|
||||||
@@ -2414,6 +2417,7 @@ function archiveWorktreeDependencies(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
paseoHome: options.paseoHome,
|
paseoHome: options.paseoHome,
|
||||||
|
worktreesRoot: options.worktreesRoot,
|
||||||
github: options.github,
|
github: options.github,
|
||||||
workspaceGitService: options.workspaceGitService,
|
workspaceGitService: options.workspaceGitService,
|
||||||
agentManager: context.agentManager,
|
agentManager: context.agentManager,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { isPaseoOwnedWorktreeCwd } from "../../utils/worktree.js";
|
|||||||
|
|
||||||
export interface AutoArchiveArchiveOptions {
|
export interface AutoArchiveArchiveOptions {
|
||||||
paseoHome: string;
|
paseoHome: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
daemonConfigStore: DaemonConfigStore;
|
daemonConfigStore: DaemonConfigStore;
|
||||||
workspaceGitService: WorkspaceGitServiceImpl;
|
workspaceGitService: WorkspaceGitServiceImpl;
|
||||||
github: GitHubService;
|
github: GitHubService;
|
||||||
@@ -81,7 +82,10 @@ export async function archiveIfSafe(input: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownership = await deps.isPaseoOwnedWorktreeCwd(cwd, { paseoHome: options.paseoHome });
|
const ownership = await deps.isPaseoOwnedWorktreeCwd(cwd, {
|
||||||
|
paseoHome: options.paseoHome,
|
||||||
|
worktreesRoot: options.worktreesRoot,
|
||||||
|
});
|
||||||
if (!ownership.allowed) {
|
if (!ownership.allowed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,6 +94,7 @@ export async function archiveIfSafe(input: {
|
|||||||
await deps.archivePaseoWorktree(
|
await deps.archivePaseoWorktree(
|
||||||
{
|
{
|
||||||
paseoHome: options.paseoHome,
|
paseoHome: options.paseoHome,
|
||||||
|
worktreesRoot: options.worktreesRoot,
|
||||||
github: options.github,
|
github: options.github,
|
||||||
workspaceGitService: options.workspaceGitService,
|
workspaceGitService: options.workspaceGitService,
|
||||||
agentManager: options.agentManager,
|
agentManager: options.agentManager,
|
||||||
@@ -115,6 +120,7 @@ export async function archiveIfSafe(input: {
|
|||||||
targetPath: cwd,
|
targetPath: cwd,
|
||||||
repoRoot: ownership.repoRoot ?? null,
|
repoRoot: ownership.repoRoot ?? null,
|
||||||
worktreesRoot: ownership.worktreeRoot,
|
worktreesRoot: ownership.worktreeRoot,
|
||||||
|
worktreesBaseRoot: options.worktreesRoot,
|
||||||
requestId: "auto-archive-on-merge",
|
requestId: "auto-archive-on-merge",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ export type DaemonLifecycleIntent =
|
|||||||
export interface PaseoDaemonConfig {
|
export interface PaseoDaemonConfig {
|
||||||
listen: string;
|
listen: string;
|
||||||
paseoHome: string;
|
paseoHome: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
corsAllowedOrigins: string[];
|
corsAllowedOrigins: string[];
|
||||||
allowedHosts?: HostnamesConfig;
|
allowedHosts?: HostnamesConfig;
|
||||||
hostnames?: HostnamesConfig;
|
hostnames?: HostnamesConfig;
|
||||||
@@ -518,6 +519,7 @@ export async function createPaseoDaemon(
|
|||||||
const workspaceGitService = new WorkspaceGitServiceImpl({
|
const workspaceGitService = new WorkspaceGitServiceImpl({
|
||||||
logger,
|
logger,
|
||||||
paseoHome: config.paseoHome,
|
paseoHome: config.paseoHome,
|
||||||
|
worktreesRoot: config.worktreesRoot,
|
||||||
deps: {
|
deps: {
|
||||||
github,
|
github,
|
||||||
},
|
},
|
||||||
@@ -657,6 +659,7 @@ export async function createPaseoDaemon(
|
|||||||
|
|
||||||
setupAutoArchiveOnMerge({
|
setupAutoArchiveOnMerge({
|
||||||
paseoHome: config.paseoHome,
|
paseoHome: config.paseoHome,
|
||||||
|
worktreesRoot: config.worktreesRoot,
|
||||||
daemonConfigStore,
|
daemonConfigStore,
|
||||||
workspaceGitService,
|
workspaceGitService,
|
||||||
github,
|
github,
|
||||||
@@ -694,6 +697,7 @@ export async function createPaseoDaemon(
|
|||||||
return createPaseoWorktreeWorkflow(
|
return createPaseoWorktreeWorkflow(
|
||||||
{
|
{
|
||||||
paseoHome: config.paseoHome,
|
paseoHome: config.paseoHome,
|
||||||
|
worktreesRoot: config.worktreesRoot,
|
||||||
createPaseoWorktree: async (workflowInput, workflowOptions) => {
|
createPaseoWorktree: async (workflowInput, workflowOptions) => {
|
||||||
return createRegisteredPaseoWorktree(workflowInput, {
|
return createRegisteredPaseoWorktree(workflowInput, {
|
||||||
github,
|
github,
|
||||||
@@ -740,6 +744,7 @@ export async function createPaseoDaemon(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
paseoHome: config.paseoHome,
|
paseoHome: config.paseoHome,
|
||||||
|
worktreesRoot: config.worktreesRoot,
|
||||||
callerAgentId,
|
callerAgentId,
|
||||||
enableVoiceTools: false,
|
enableVoiceTools: false,
|
||||||
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
|
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
|
||||||
@@ -965,6 +970,7 @@ export async function createPaseoDaemon(
|
|||||||
providerSnapshotManager,
|
providerSnapshotManager,
|
||||||
{
|
{
|
||||||
listen: formatListenTarget(boundListenTarget ?? listenTarget),
|
listen: formatListenTarget(boundListenTarget ?? listenTarget),
|
||||||
|
worktreesRoot: config.worktreesRoot,
|
||||||
relay: {
|
relay: {
|
||||||
enabled: relayEnabled,
|
enabled: relayEnabled,
|
||||||
endpoint: relayEndpoint,
|
endpoint: relayEndpoint,
|
||||||
|
|||||||
@@ -83,3 +83,29 @@ describe("daemon relay config", () => {
|
|||||||
expect(config.relayPublicUseTls).toBe(true);
|
expect(config.relayPublicUseTls).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("daemon worktree root config", () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves relative worktrees.root against PASEO_HOME", async () => {
|
||||||
|
const home = await createPaseoHome({
|
||||||
|
version: 1,
|
||||||
|
worktrees: { root: "custom-worktrees" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(loadConfig(home, { env: {} }).worktreesRoot).toBe(path.join(home, "custom-worktrees"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps absolute worktrees.root absolute", async () => {
|
||||||
|
const home = await createPaseoHome({
|
||||||
|
version: 1,
|
||||||
|
worktrees: { root: path.join(os.tmpdir(), "paseo-custom-worktrees") },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(loadConfig(home, { env: {} }).worktreesRoot).toBe(
|
||||||
|
path.join(os.tmpdir(), "paseo-custom-worktrees"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { resolvePaseoNodeEnv } from "./paseo-env.js";
|
import { resolvePaseoNodeEnv } from "./paseo-env.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { expandTilde } from "../utils/path.js";
|
||||||
|
|
||||||
import type { PaseoDaemonConfig } from "./bootstrap.js";
|
import type { PaseoDaemonConfig } from "./bootstrap.js";
|
||||||
import {
|
import {
|
||||||
@@ -256,6 +257,21 @@ function resolveAuthConfig(
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveWorktreesRoot(
|
||||||
|
paseoHome: string,
|
||||||
|
persisted: ReturnType<typeof loadPersistedConfig>,
|
||||||
|
): string | undefined {
|
||||||
|
const configuredRoot = persisted.worktrees?.root?.trim();
|
||||||
|
if (!configuredRoot) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedRoot = expandTilde(configuredRoot);
|
||||||
|
return path.isAbsolute(expandedRoot)
|
||||||
|
? path.resolve(expandedRoot)
|
||||||
|
: path.resolve(paseoHome, expandedRoot);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAppendSystemPrompt(persisted: ReturnType<typeof loadPersistedConfig>): string {
|
function resolveAppendSystemPrompt(persisted: ReturnType<typeof loadPersistedConfig>): string {
|
||||||
return persisted.daemon?.appendSystemPrompt ?? "";
|
return persisted.daemon?.appendSystemPrompt ?? "";
|
||||||
}
|
}
|
||||||
@@ -321,6 +337,7 @@ export function loadConfig(
|
|||||||
return {
|
return {
|
||||||
listen,
|
listen,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
|
worktreesRoot: resolveWorktreesRoot(paseoHome, persisted),
|
||||||
corsAllowedOrigins: resolveCorsAllowedOrigins(env, persisted),
|
corsAllowedOrigins: resolveCorsAllowedOrigins(env, persisted),
|
||||||
hostnames,
|
hostnames,
|
||||||
mcpEnabled,
|
mcpEnabled,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type { TerminalManager } from "../terminal/terminal-manager.js";
|
|||||||
|
|
||||||
export interface ArchivePaseoWorktreeDependencies {
|
export interface ArchivePaseoWorktreeDependencies {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
github: GitHubService;
|
github: GitHubService;
|
||||||
workspaceGitService: Pick<WorkspaceGitService, "getSnapshot">;
|
workspaceGitService: Pick<WorkspaceGitService, "getSnapshot">;
|
||||||
agentManager: Pick<AgentManager, "listAgents" | "archiveAgent" | "archiveSnapshot">;
|
agentManager: Pick<AgentManager, "listAgents" | "archiveAgent" | "archiveSnapshot">;
|
||||||
@@ -41,12 +42,14 @@ export async function archivePaseoWorktree(
|
|||||||
targetPath: string;
|
targetPath: string;
|
||||||
repoRoot: string | null;
|
repoRoot: string | null;
|
||||||
worktreesRoot?: string;
|
worktreesRoot?: string;
|
||||||
|
worktreesBaseRoot?: string;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
},
|
},
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
let targetPath = options.targetPath;
|
let targetPath = options.targetPath;
|
||||||
const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
|
const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
|
||||||
paseoHome: dependencies.paseoHome,
|
paseoHome: dependencies.paseoHome,
|
||||||
|
worktreesRoot: options.worktreesBaseRoot ?? dependencies.worktreesRoot,
|
||||||
});
|
});
|
||||||
if (resolvedWorktree) {
|
if (resolvedWorktree) {
|
||||||
targetPath = resolvedWorktree.worktreePath;
|
targetPath = resolvedWorktree.worktreePath;
|
||||||
@@ -115,6 +118,7 @@ export async function archivePaseoWorktree(
|
|||||||
worktreePath: targetPath,
|
worktreePath: targetPath,
|
||||||
worktreesRoot: options.worktreesRoot,
|
worktreesRoot: options.worktreesRoot,
|
||||||
paseoHome: dependencies.paseoHome,
|
paseoHome: dependencies.paseoHome,
|
||||||
|
worktreesBaseRoot: options.worktreesBaseRoot ?? dependencies.worktreesRoot,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof WorktreeTeardownError) {
|
if (error instanceof WorktreeTeardownError) {
|
||||||
|
|||||||
@@ -63,6 +63,18 @@ describe("PersistedConfigSchema daemon relay config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("PersistedConfigSchema worktrees config", () => {
|
||||||
|
test("accepts optional worktree root", () => {
|
||||||
|
const parsed = PersistedConfigSchema.parse({
|
||||||
|
worktrees: {
|
||||||
|
root: "/mnt/fast/paseo-worktrees",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parsed.worktrees?.root).toBe("/mnt/fast/paseo-worktrees");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("PersistedConfigSchema daemon append system prompt", () => {
|
describe("PersistedConfigSchema daemon append system prompt", () => {
|
||||||
test("accepts optional append system prompt", () => {
|
test("accepts optional append system prompt", () => {
|
||||||
const parsed = PersistedConfigSchema.parse({
|
const parsed = PersistedConfigSchema.parse({
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ const ProvidersSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const WorktreesConfigSchema = z
|
||||||
|
.object({
|
||||||
|
root: z.string().min(1).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const BcryptHashSchema = z.string().regex(/^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/, {
|
const BcryptHashSchema = z.string().regex(/^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/, {
|
||||||
message: "Expected a bcrypt hash",
|
message: "Expected a bcrypt hash",
|
||||||
});
|
});
|
||||||
@@ -248,6 +254,7 @@ export const PersistedConfigSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
providers: ProvidersSchema.optional(),
|
providers: ProvidersSchema.optional(),
|
||||||
|
worktrees: WorktreesConfigSchema.optional(),
|
||||||
agents: z
|
agents: z
|
||||||
.object({
|
.object({
|
||||||
providers: z.preprocess(normalizeAgentProviders, ProviderOverridesSchema).optional(),
|
providers: z.preprocess(normalizeAgentProviders, ProviderOverridesSchema).optional(),
|
||||||
|
|||||||
@@ -569,6 +569,7 @@ export interface SessionOptions {
|
|||||||
downloadTokenStore: DownloadTokenStore;
|
downloadTokenStore: DownloadTokenStore;
|
||||||
pushTokenStore: PushTokenStore;
|
pushTokenStore: PushTokenStore;
|
||||||
paseoHome: string;
|
paseoHome: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
agentStorage: AgentStorage;
|
agentStorage: AgentStorage;
|
||||||
projectRegistry: ProjectRegistry;
|
projectRegistry: ProjectRegistry;
|
||||||
@@ -742,6 +743,7 @@ export class Session {
|
|||||||
private readonly onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | null;
|
private readonly onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | null;
|
||||||
private readonly sessionLogger: pino.Logger;
|
private readonly sessionLogger: pino.Logger;
|
||||||
private readonly paseoHome: string;
|
private readonly paseoHome: string;
|
||||||
|
private readonly worktreesRoot: string | undefined;
|
||||||
|
|
||||||
// State machine
|
// State machine
|
||||||
private abortController: AbortController;
|
private abortController: AbortController;
|
||||||
@@ -859,6 +861,7 @@ export class Session {
|
|||||||
downloadTokenStore,
|
downloadTokenStore,
|
||||||
pushTokenStore,
|
pushTokenStore,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
|
worktreesRoot,
|
||||||
agentManager,
|
agentManager,
|
||||||
agentStorage,
|
agentStorage,
|
||||||
projectRegistry,
|
projectRegistry,
|
||||||
@@ -900,6 +903,7 @@ export class Session {
|
|||||||
this.downloadTokenStore = downloadTokenStore;
|
this.downloadTokenStore = downloadTokenStore;
|
||||||
this.pushTokenStore = pushTokenStore;
|
this.pushTokenStore = pushTokenStore;
|
||||||
this.paseoHome = paseoHome;
|
this.paseoHome = paseoHome;
|
||||||
|
this.worktreesRoot = worktreesRoot;
|
||||||
this.sessionLogger = logger.child({
|
this.sessionLogger = logger.child({
|
||||||
module: "session",
|
module: "session",
|
||||||
clientId: this.clientId,
|
clientId: this.clientId,
|
||||||
@@ -930,6 +934,7 @@ export class Session {
|
|||||||
});
|
});
|
||||||
this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
|
this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
agentManager: this.agentManager,
|
agentManager: this.agentManager,
|
||||||
agentStorage: this.agentStorage,
|
agentStorage: this.agentStorage,
|
||||||
github: this.github,
|
github: this.github,
|
||||||
@@ -3111,6 +3116,7 @@ export class Session {
|
|||||||
agentStorage: this.agentStorage,
|
agentStorage: this.agentStorage,
|
||||||
logger: this.sessionLogger,
|
logger: this.sessionLogger,
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
workspaceGitService: this.workspaceGitService,
|
workspaceGitService: this.workspaceGitService,
|
||||||
providerSnapshotManager: this.providerSnapshotManager,
|
providerSnapshotManager: this.providerSnapshotManager,
|
||||||
daemonConfig: this.readStructuredGenerationDaemonConfig(),
|
daemonConfig: this.readStructuredGenerationDaemonConfig(),
|
||||||
@@ -3491,6 +3497,7 @@ export class Session {
|
|||||||
return buildWorktreeAgentSessionConfig(
|
return buildWorktreeAgentSessionConfig(
|
||||||
{
|
{
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
sessionLogger: this.sessionLogger,
|
sessionLogger: this.sessionLogger,
|
||||||
workspaceGitService: this.workspaceGitService,
|
workspaceGitService: this.workspaceGitService,
|
||||||
createPaseoWorktree: (input, serviceOptions) =>
|
createPaseoWorktree: (input, serviceOptions) =>
|
||||||
@@ -5230,7 +5237,7 @@ export class Session {
|
|||||||
baseRef,
|
baseRef,
|
||||||
mode: msg.strategy === "squash" ? "squash" : "merge",
|
mode: msg.strategy === "squash" ? "squash" : "merge",
|
||||||
},
|
},
|
||||||
{ paseoHome: this.paseoHome },
|
{ paseoHome: this.paseoHome, worktreesRoot: this.worktreesRoot },
|
||||||
);
|
);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.notifyGitMutation(mutatedCwd, "merge-to-base", { invalidateGithub: true }),
|
this.notifyGitMutation(mutatedCwd, "merge-to-base", { invalidateGithub: true }),
|
||||||
@@ -5711,6 +5718,7 @@ export class Session {
|
|||||||
return handleWorktreeArchiveRequest(
|
return handleWorktreeArchiveRequest(
|
||||||
{
|
{
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
github: this.github,
|
github: this.github,
|
||||||
workspaceGitService: this.workspaceGitService,
|
workspaceGitService: this.workspaceGitService,
|
||||||
agentManager: this.agentManager,
|
agentManager: this.agentManager,
|
||||||
@@ -7298,6 +7306,7 @@ export class Session {
|
|||||||
return handleCreateWorktreeRequest(
|
return handleCreateWorktreeRequest(
|
||||||
{
|
{
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
describeWorkspaceRecord: (result) => this.describeCreatedWorktreeWorkspace(result),
|
describeWorkspaceRecord: (result) => this.describeCreatedWorktreeWorkspace(result),
|
||||||
emit: (message) => this.emit(message),
|
emit: (message) => this.emit(message),
|
||||||
sessionLogger: this.sessionLogger,
|
sessionLogger: this.sessionLogger,
|
||||||
@@ -7317,6 +7326,7 @@ export class Session {
|
|||||||
return createWorktreeWorkflow(
|
return createWorktreeWorkflow(
|
||||||
{
|
{
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
createPaseoWorktree: (workflowInput, serviceOptions) =>
|
createPaseoWorktree: (workflowInput, serviceOptions) =>
|
||||||
this.createPaseoWorktree(workflowInput, serviceOptions),
|
this.createPaseoWorktree(workflowInput, serviceOptions),
|
||||||
warmWorkspaceGitData: (workspace) => this.warmWorkspaceGitDataForWorkspace(workspace),
|
warmWorkspaceGitData: (workspace) => this.warmWorkspaceGitDataForWorkspace(workspace),
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
private readonly workspaceGitService: WorkspaceGitService;
|
private readonly workspaceGitService: WorkspaceGitService;
|
||||||
private readonly downloadTokenStore: DownloadTokenStore;
|
private readonly downloadTokenStore: DownloadTokenStore;
|
||||||
private readonly paseoHome: string;
|
private readonly paseoHome: string;
|
||||||
|
private readonly worktreesRoot: string | undefined;
|
||||||
private readonly daemonConfigStore: DaemonConfigStore;
|
private readonly daemonConfigStore: DaemonConfigStore;
|
||||||
private readonly pushTokenStore: PushTokenStore;
|
private readonly pushTokenStore: PushTokenStore;
|
||||||
private readonly pushNotificationSender: PushNotificationSender;
|
private readonly pushNotificationSender: PushNotificationSender;
|
||||||
@@ -419,6 +420,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
providerSnapshotManager?: ProviderSnapshotManager,
|
providerSnapshotManager?: ProviderSnapshotManager,
|
||||||
daemonRuntimeConfig?: {
|
daemonRuntimeConfig?: {
|
||||||
listen: string | null;
|
listen: string | null;
|
||||||
|
worktreesRoot?: string;
|
||||||
relay: {
|
relay: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
@@ -453,6 +455,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
this.workspaceGitService = workspaceGitService ?? createFallbackWorkspaceGitService();
|
this.workspaceGitService = workspaceGitService ?? createFallbackWorkspaceGitService();
|
||||||
this.downloadTokenStore = downloadTokenStore;
|
this.downloadTokenStore = downloadTokenStore;
|
||||||
this.paseoHome = paseoHome;
|
this.paseoHome = paseoHome;
|
||||||
|
this.worktreesRoot = daemonRuntimeConfig?.worktreesRoot;
|
||||||
this.daemonConfigStore = daemonConfigStore;
|
this.daemonConfigStore = daemonConfigStore;
|
||||||
this.mcpBaseUrl = mcpBaseUrl;
|
this.mcpBaseUrl = mcpBaseUrl;
|
||||||
this.assignOptionalServices({
|
this.assignOptionalServices({
|
||||||
@@ -858,6 +861,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
downloadTokenStore: this.downloadTokenStore,
|
downloadTokenStore: this.downloadTokenStore,
|
||||||
pushTokenStore: this.pushTokenStore,
|
pushTokenStore: this.pushTokenStore,
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
agentManager: this.agentManager,
|
agentManager: this.agentManager,
|
||||||
agentStorage: this.agentStorage,
|
agentStorage: this.agentStorage,
|
||||||
projectRegistry: this.projectRegistry,
|
projectRegistry: this.projectRegistry,
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ interface WorkspaceGitServiceDependencies {
|
|||||||
interface WorkspaceGitServiceOptions {
|
interface WorkspaceGitServiceOptions {
|
||||||
logger: pino.Logger;
|
logger: pino.Logger;
|
||||||
paseoHome: string;
|
paseoHome: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
deps?: Partial<WorkspaceGitServiceDependencies>;
|
deps?: Partial<WorkspaceGitServiceDependencies>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +348,7 @@ function resolveWorkspaceGitServiceDeps(
|
|||||||
export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||||
private readonly logger: pino.Logger;
|
private readonly logger: pino.Logger;
|
||||||
private readonly paseoHome: string;
|
private readonly paseoHome: string;
|
||||||
|
private readonly worktreesRoot: string | undefined;
|
||||||
private readonly deps: WorkspaceGitServiceDependencies;
|
private readonly deps: WorkspaceGitServiceDependencies;
|
||||||
private readonly snapshotUpdatedListeners = new Set<WorkspaceGitSnapshotUpdatedListener>();
|
private readonly snapshotUpdatedListeners = new Set<WorkspaceGitSnapshotUpdatedListener>();
|
||||||
private readonly workspaceTargets = new Map<string, WorkspaceGitTarget>();
|
private readonly workspaceTargets = new Map<string, WorkspaceGitTarget>();
|
||||||
@@ -385,6 +387,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
constructor(options: WorkspaceGitServiceOptions) {
|
constructor(options: WorkspaceGitServiceOptions) {
|
||||||
this.logger = options.logger.child({ module: "workspace-git-service" });
|
this.logger = options.logger.child({ module: "workspace-git-service" });
|
||||||
this.paseoHome = options.paseoHome;
|
this.paseoHome = options.paseoHome;
|
||||||
|
this.worktreesRoot = options.worktreesRoot;
|
||||||
this.deps = resolveWorkspaceGitServiceDeps(options.deps);
|
this.deps = resolveWorkspaceGitServiceDeps(options.deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +441,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
try {
|
try {
|
||||||
const status = await this.deps.getCheckoutStatus(normalizedCwd, {
|
const status = await this.deps.getCheckoutStatus(normalizedCwd, {
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
});
|
});
|
||||||
if (!status.isGit) {
|
if (!status.isGit) {
|
||||||
@@ -484,7 +488,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
const normalizedOptions = this.normalizeCheckoutDiffOptions(options);
|
const normalizedOptions = this.normalizeCheckoutDiffOptions(options);
|
||||||
const key = this.buildCheckoutDiffCacheKey(normalizedCwd, normalizedOptions);
|
const key = this.buildCheckoutDiffCacheKey(normalizedCwd, normalizedOptions);
|
||||||
return this.readAuxiliaryCache(this.checkoutDiffCache, key, readOptions, () =>
|
return this.readAuxiliaryCache(this.checkoutDiffCache, key, readOptions, () =>
|
||||||
this.deps.getCheckoutDiff(normalizedCwd, normalizedOptions, { paseoHome: this.paseoHome }),
|
this.deps.getCheckoutDiff(normalizedCwd, normalizedOptions, {
|
||||||
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,6 +588,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
this.deps.listPaseoWorktrees({
|
this.deps.listPaseoWorktrees({
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1566,7 +1574,11 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
|
|
||||||
const cwd = target.cwd;
|
const cwd = target.cwd;
|
||||||
const previousGitHubPollKey = this.getGitHubPollKey(target);
|
const previousGitHubPollKey = this.getGitHubPollKey(target);
|
||||||
const baseContext: CheckoutContext = { paseoHome: this.paseoHome, logger: this.logger };
|
const baseContext: CheckoutContext = {
|
||||||
|
paseoHome: this.paseoHome,
|
||||||
|
worktreesRoot: this.worktreesRoot,
|
||||||
|
logger: this.logger,
|
||||||
|
};
|
||||||
const facts = await this.loadCheckoutFacts(target, {
|
const facts = await this.loadCheckoutFacts(target, {
|
||||||
...baseContext,
|
...baseContext,
|
||||||
allowRecent: !request.force,
|
allowRecent: !request.force,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface CreateWorktreeCoreInput {
|
|||||||
githubPrNumber?: number;
|
githubPrNumber?: number;
|
||||||
firstAgentContext?: FirstAgentContext;
|
firstAgentContext?: FirstAgentContext;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
runSetup?: boolean;
|
runSetup?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ export async function createWorktreeCore(
|
|||||||
slug: normalizedSlug,
|
slug: normalizedSlug,
|
||||||
repoRoot,
|
repoRoot,
|
||||||
paseoHome: input.paseoHome,
|
paseoHome: input.paseoHome,
|
||||||
|
worktreesRoot: input.worktreesRoot,
|
||||||
});
|
});
|
||||||
if (existingWorktree) {
|
if (existingWorktree) {
|
||||||
return { worktree: existingWorktree, intent, repoRoot, created: false };
|
return { worktree: existingWorktree, intent, repoRoot, created: false };
|
||||||
@@ -110,6 +112,7 @@ export async function createWorktreeCore(
|
|||||||
source: intent,
|
source: intent,
|
||||||
runSetup: input.runSetup ?? true,
|
runSetup: input.runSetup ?? true,
|
||||||
paseoHome: input.paseoHome,
|
paseoHome: input.paseoHome,
|
||||||
|
worktreesRoot: input.worktreesRoot,
|
||||||
}),
|
}),
|
||||||
intent,
|
intent,
|
||||||
repoRoot,
|
repoRoot,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ type AgentWorktreeSetupTimelineWriter = (input: {
|
|||||||
|
|
||||||
interface BuildAgentSessionConfigDependencies {
|
interface BuildAgentSessionConfigDependencies {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
sessionLogger: Logger;
|
sessionLogger: Logger;
|
||||||
workspaceGitService?: WorkspaceGitService;
|
workspaceGitService?: WorkspaceGitService;
|
||||||
createPaseoWorktree: (
|
createPaseoWorktree: (
|
||||||
@@ -95,6 +96,7 @@ interface BuildAgentSessionConfigDependencies {
|
|||||||
|
|
||||||
interface CreatePaseoWorktreeInBackgroundDependencies {
|
interface CreatePaseoWorktreeInBackgroundDependencies {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
emitWorkspaceUpdateForCwd: (cwd: string, options?: { dedupeGitState?: boolean }) => Promise<void>;
|
emitWorkspaceUpdateForCwd: (cwd: string, options?: { dedupeGitState?: boolean }) => Promise<void>;
|
||||||
cacheWorkspaceSetupSnapshot: (workspaceId: string, snapshot: WorkspaceSetupSnapshot) => void;
|
cacheWorkspaceSetupSnapshot: (workspaceId: string, snapshot: WorkspaceSetupSnapshot) => void;
|
||||||
emit: EmitSessionMessage;
|
emit: EmitSessionMessage;
|
||||||
@@ -158,6 +160,7 @@ interface HandleWorkspaceSetupStatusRequestDependencies {
|
|||||||
|
|
||||||
interface HandleCreatePaseoWorktreeRequestDependencies {
|
interface HandleCreatePaseoWorktreeRequestDependencies {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
describeWorkspaceRecord: (
|
describeWorkspaceRecord: (
|
||||||
result: CreatePaseoWorktreeResult,
|
result: CreatePaseoWorktreeResult,
|
||||||
) => Promise<WorkspaceDescriptorPayload>;
|
) => Promise<WorkspaceDescriptorPayload>;
|
||||||
@@ -224,6 +227,7 @@ export async function buildAgentSessionConfig(
|
|||||||
firstAgentContext,
|
firstAgentContext,
|
||||||
runSetup: false,
|
runSetup: false,
|
||||||
paseoHome: dependencies.paseoHome,
|
paseoHome: dependencies.paseoHome,
|
||||||
|
worktreesRoot: dependencies.worktreesRoot,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
resolveDefaultBranch: normalized.baseBranch
|
resolveDefaultBranch: normalized.baseBranch
|
||||||
@@ -491,6 +495,7 @@ export async function handleCreatePaseoWorktreeRequest(
|
|||||||
const commandResult = await createPaseoWorktreeCommand(
|
const commandResult = await createPaseoWorktreeCommand(
|
||||||
{
|
{
|
||||||
paseoHome: dependencies.paseoHome,
|
paseoHome: dependencies.paseoHome,
|
||||||
|
worktreesRoot: dependencies.worktreesRoot,
|
||||||
createPaseoWorktreeWorkflow: dependencies.createPaseoWorktreeWorkflow,
|
createPaseoWorktreeWorkflow: dependencies.createPaseoWorktreeWorkflow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -572,6 +577,7 @@ export async function createPaseoWorktreeWorkflow(
|
|||||||
...input,
|
...input,
|
||||||
runSetup: false,
|
runSetup: false,
|
||||||
paseoHome: input.paseoHome ?? dependencies.paseoHome,
|
paseoHome: input.paseoHome ?? dependencies.paseoHome,
|
||||||
|
worktreesRoot: input.worktreesRoot ?? dependencies.worktreesRoot,
|
||||||
},
|
},
|
||||||
options?.resolveDefaultBranch
|
options?.resolveDefaultBranch
|
||||||
? { resolveDefaultBranch: options.resolveDefaultBranch }
|
? { resolveDefaultBranch: options.resolveDefaultBranch }
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface CreatePaseoWorktreeCommandDependencies<
|
|||||||
Result extends CreatePaseoWorktreeResult = CreatePaseoWorktreeResult,
|
Result extends CreatePaseoWorktreeResult = CreatePaseoWorktreeResult,
|
||||||
> {
|
> {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
createPaseoWorktreeWorkflow?: CreatePaseoWorktreeWorkflow<Result>;
|
createPaseoWorktreeWorkflow?: CreatePaseoWorktreeWorkflow<Result>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ export type CreatePaseoWorktreeCommandInput = Omit<
|
|||||||
"paseoHome" | "runSetup"
|
"paseoHome" | "runSetup"
|
||||||
> & {
|
> & {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreatePaseoWorktreeCommandResult<Result extends CreatePaseoWorktreeResult> =
|
export type CreatePaseoWorktreeCommandResult<Result extends CreatePaseoWorktreeResult> =
|
||||||
@@ -73,6 +75,7 @@ export async function createPaseoWorktreeCommand<Result extends CreatePaseoWorkt
|
|||||||
...input,
|
...input,
|
||||||
runSetup: false,
|
runSetup: false,
|
||||||
paseoHome: input.paseoHome ?? dependencies.paseoHome,
|
paseoHome: input.paseoHome ?? dependencies.paseoHome,
|
||||||
|
worktreesRoot: input.worktreesRoot ?? dependencies.worktreesRoot,
|
||||||
});
|
});
|
||||||
return { ok: true, createdWorktree };
|
return { ok: true, createdWorktree };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -118,6 +121,7 @@ export async function archivePaseoWorktreeCommand(
|
|||||||
const resolvedTarget = await resolveArchiveTarget(dependencies, input);
|
const resolvedTarget = await resolveArchiveTarget(dependencies, input);
|
||||||
const ownership = await isPaseoOwnedWorktreeCwd(resolvedTarget.targetPath, {
|
const ownership = await isPaseoOwnedWorktreeCwd(resolvedTarget.targetPath, {
|
||||||
paseoHome: dependencies.paseoHome,
|
paseoHome: dependencies.paseoHome,
|
||||||
|
worktreesRoot: dependencies.worktreesRoot,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ownership.allowed) {
|
if (!ownership.allowed) {
|
||||||
@@ -134,6 +138,7 @@ export async function archivePaseoWorktreeCommand(
|
|||||||
targetPath: resolvedTarget.targetPath,
|
targetPath: resolvedTarget.targetPath,
|
||||||
repoRoot,
|
repoRoot,
|
||||||
worktreesRoot: ownership.worktreeRoot,
|
worktreesRoot: ownership.worktreeRoot,
|
||||||
|
worktreesBaseRoot: dependencies.worktreesRoot,
|
||||||
requestId: input.requestId,
|
requestId: input.requestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,6 +189,10 @@ async function resolveWorktreeSlugPath(
|
|||||||
repoRoot: string,
|
repoRoot: string,
|
||||||
worktreeSlug: string,
|
worktreeSlug: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const worktreesRoot = await getPaseoWorktreesRoot(repoRoot, dependencies.paseoHome);
|
const worktreesRoot = await getPaseoWorktreesRoot(
|
||||||
|
repoRoot,
|
||||||
|
dependencies.paseoHome,
|
||||||
|
dependencies.worktreesRoot,
|
||||||
|
);
|
||||||
return join(worktreesRoot, worktreeSlug);
|
return join(worktreesRoot, worktreeSlug);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
mkdirSync,
|
mkdirSync,
|
||||||
} from "fs";
|
} from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { win32 } from "node:path";
|
||||||
import { tmpdir } from "os";
|
import { tmpdir } from "os";
|
||||||
import {
|
import {
|
||||||
__resetCheckoutShortstatCacheForTests,
|
__resetCheckoutShortstatCacheForTests,
|
||||||
@@ -2355,6 +2356,20 @@ const x = 1;
|
|||||||
expect(isPaseoWorktreePath("C:\\Users\\dev\\.paseo\\worktrees\\feature")).toBe(true);
|
expect(isPaseoWorktreePath("C:\\Users\\dev\\.paseo\\worktrees\\feature")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("matches worktrees under a custom PASEO_HOME", () => {
|
||||||
|
const customPaseoHome = process.platform === "win32" ? "C:\\paseo" : "/var/lib/paseo";
|
||||||
|
const worktreePath =
|
||||||
|
process.platform === "win32"
|
||||||
|
? win32.join(customPaseoHome, "worktrees", "project", "feature")
|
||||||
|
: `${customPaseoHome}/worktrees/project/feature`;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isPaseoWorktreePath(worktreePath, {
|
||||||
|
paseoHome: customPaseoHome,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects paths without .paseo/worktrees segment", () => {
|
it("rejects paths without .paseo/worktrees segment", () => {
|
||||||
expect(isPaseoWorktreePath("/home/user/repo")).toBe(false);
|
expect(isPaseoWorktreePath("/home/user/repo")).toBe(false);
|
||||||
expect(isPaseoWorktreePath("C:\\Users\\dev\\repo")).toBe(false);
|
expect(isPaseoWorktreePath("C:\\Users\\dev\\repo")).toBe(false);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "../services/github-service.js";
|
} from "../services/github-service.js";
|
||||||
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
||||||
import { runGitCommand } from "./run-git-command.js";
|
import { runGitCommand } from "./run-git-command.js";
|
||||||
import { isPaseoOwnedWorktreeCwd } from "./worktree.js";
|
import { isPaseoOwnedWorktreeCwd, resolvePaseoWorktreesBaseRoot } from "./worktree.js";
|
||||||
import { readPaseoWorktreeMetadata } from "./worktree-metadata.js";
|
import { readPaseoWorktreeMetadata } from "./worktree-metadata.js";
|
||||||
const READ_ONLY_GIT_ENV = {
|
const READ_ONLY_GIT_ENV = {
|
||||||
GIT_OPTIONAL_LOCKS: "0",
|
GIT_OPTIONAL_LOCKS: "0",
|
||||||
@@ -779,6 +779,7 @@ export interface MergeFromBaseOptions {
|
|||||||
|
|
||||||
export interface CheckoutContext {
|
export interface CheckoutContext {
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
logger?: Pick<Logger, "trace">;
|
logger?: Pick<Logger, "trace">;
|
||||||
facts?: CheckoutSnapshotFacts | null;
|
facts?: CheckoutSnapshotFacts | null;
|
||||||
}
|
}
|
||||||
@@ -882,6 +883,7 @@ export async function getMainRepoRoot(cwd: string): Promise<string> {
|
|||||||
async function getMainRepoRootFromCommonDir(
|
async function getMainRepoRootFromCommonDir(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
commonDir: string | null,
|
commonDir: string | null,
|
||||||
|
context?: CheckoutContext,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!commonDir) {
|
if (!commonDir) {
|
||||||
throw new Error("Not in a git repository");
|
throw new Error("Not in a git repository");
|
||||||
@@ -897,7 +899,14 @@ async function getMainRepoRootFromCommonDir(
|
|||||||
envOverlay: READ_ONLY_GIT_ENV,
|
envOverlay: READ_ONLY_GIT_ENV,
|
||||||
});
|
});
|
||||||
const worktrees = parseWorktreeList(worktreeOut);
|
const worktrees = parseWorktreeList(worktreeOut);
|
||||||
const nonBareNonPaseo = worktrees.filter((wt) => !wt.isBare && !isPaseoWorktreePath(wt.path));
|
const nonBareNonPaseo = worktrees.filter(
|
||||||
|
(wt) =>
|
||||||
|
!wt.isBare &&
|
||||||
|
!isPaseoWorktreePath(wt.path, {
|
||||||
|
paseoHome: context?.paseoHome,
|
||||||
|
worktreesRoot: context?.worktreesRoot,
|
||||||
|
}),
|
||||||
|
);
|
||||||
const childrenOfBareRepo = nonBareNonPaseo.filter((wt) => isDescendantPath(wt.path, normalized));
|
const childrenOfBareRepo = nonBareNonPaseo.filter((wt) => isDescendantPath(wt.path, normalized));
|
||||||
const mainChild = childrenOfBareRepo.find((wt) => basename(wt.path) === "main");
|
const mainChild = childrenOfBareRepo.find((wt) => basename(wt.path) === "main");
|
||||||
return mainChild?.path ?? childrenOfBareRepo[0]?.path ?? nonBareNonPaseo[0]?.path ?? normalized;
|
return mainChild?.path ?? childrenOfBareRepo[0]?.path ?? nonBareNonPaseo[0]?.path ?? normalized;
|
||||||
@@ -909,8 +918,14 @@ export interface GitWorktreeEntry {
|
|||||||
isBare?: boolean;
|
isBare?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check whether a path contains a `.paseo/worktrees/` segment (both `/` and `\`). */
|
/** Check whether a path is under Paseo's worktree root. */
|
||||||
export function isPaseoWorktreePath(p: string): boolean {
|
export function isPaseoWorktreePath(
|
||||||
|
p: string,
|
||||||
|
options?: { paseoHome?: string; worktreesRoot?: string },
|
||||||
|
): boolean {
|
||||||
|
if (options?.worktreesRoot || options?.paseoHome) {
|
||||||
|
return isDescendantPath(p, resolvePaseoWorktreesBaseRoot(options));
|
||||||
|
}
|
||||||
return /[/\\]\.paseo[/\\]worktrees[/\\]/.test(p);
|
return /[/\\]\.paseo[/\\]worktrees[/\\]/.test(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1008,7 +1023,10 @@ async function getPaseoWorktreeForCwd(
|
|||||||
return { isPaseoOwnedWorktree: false };
|
return { isPaseoOwnedWorktree: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownership = await isPaseoOwnedWorktreeCwd(cwd, { paseoHome: context?.paseoHome });
|
const ownership = await isPaseoOwnedWorktreeCwd(cwd, {
|
||||||
|
paseoHome: context?.paseoHome,
|
||||||
|
worktreesRoot: context?.worktreesRoot,
|
||||||
|
});
|
||||||
if (!ownership.allowed) {
|
if (!ownership.allowed) {
|
||||||
return { isPaseoOwnedWorktree: false };
|
return { isPaseoOwnedWorktree: false };
|
||||||
}
|
}
|
||||||
@@ -1548,9 +1566,11 @@ export async function getCheckoutSnapshotFacts(
|
|||||||
? readPaseoWorktreeBaseRef(inspected.paseoWorktree.worktreeRoot)
|
? readPaseoWorktreeBaseRef(inspected.paseoWorktree.worktreeRoot)
|
||||||
: null;
|
: null;
|
||||||
const resolvedBaseRef = storedBaseRef ?? (await resolveBaseRef(cwd));
|
const resolvedBaseRef = storedBaseRef ?? (await resolveBaseRef(cwd));
|
||||||
const mainRepoRoot = await getMainRepoRootFromCommonDir(cwd, inspected.gitCommonDir).catch(
|
const mainRepoRoot = await getMainRepoRootFromCommonDir(
|
||||||
() => null,
|
cwd,
|
||||||
);
|
inspected.gitCommonDir,
|
||||||
|
context,
|
||||||
|
).catch(() => null);
|
||||||
let comparisonBaseRef: string | null = null;
|
let comparisonBaseRef: string | null = null;
|
||||||
if (
|
if (
|
||||||
resolvedBaseRef &&
|
resolvedBaseRef &&
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ interface LegacyCreateWorktreeTestOptions {
|
|||||||
worktreeSlug: string;
|
worktreeSlug: string;
|
||||||
runSetup?: boolean;
|
runSetup?: boolean;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLegacyWorktreeForTest(
|
function createLegacyWorktreeForTest(
|
||||||
@@ -68,6 +69,7 @@ function createLegacyWorktreeForTest(
|
|||||||
},
|
},
|
||||||
runSetup: options.runSetup ?? true,
|
runSetup: options.runSetup ?? true,
|
||||||
paseoHome: options.paseoHome,
|
paseoHome: options.paseoHome,
|
||||||
|
worktreesRoot: options.worktreesRoot,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +120,38 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => {
|
|||||||
expect(metadata).toMatchObject({ version: 1, baseRefName: "main" });
|
expect(metadata).toMatchObject({ version: 1, baseRefName: "main" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates and owns worktrees under a configured root", async () => {
|
||||||
|
const worktreesRoot = join(tempDir, "custom-worktrees");
|
||||||
|
const projectHash = await deriveWorktreeProjectHash(repoDir);
|
||||||
|
const result = await createLegacyWorktreeForTest({
|
||||||
|
branchName: "custom-root",
|
||||||
|
cwd: repoDir,
|
||||||
|
baseBranch: "main",
|
||||||
|
worktreeSlug: "custom-root",
|
||||||
|
paseoHome,
|
||||||
|
worktreesRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.worktreePath).toBe(join(worktreesRoot, projectHash, "custom-root"));
|
||||||
|
await expect(
|
||||||
|
isPaseoOwnedWorktreeCwd(result.worktreePath, { paseoHome, worktreesRoot }),
|
||||||
|
).resolves.toMatchObject({ allowed: true, worktreeRoot: join(worktreesRoot, projectHash) });
|
||||||
|
await expect(
|
||||||
|
isPaseoOwnedWorktreeCwd(result.worktreePath, { paseoHome }),
|
||||||
|
).resolves.toMatchObject({ allowed: false });
|
||||||
|
|
||||||
|
const worktrees = await listPaseoWorktrees({ cwd: repoDir, paseoHome, worktreesRoot });
|
||||||
|
expect(worktrees.map((entry) => entry.path)).toContain(result.worktreePath);
|
||||||
|
|
||||||
|
await deletePaseoWorktree({
|
||||||
|
cwd: repoDir,
|
||||||
|
worktreePath: result.worktreePath,
|
||||||
|
paseoHome,
|
||||||
|
worktreesBaseRoot: worktreesRoot,
|
||||||
|
});
|
||||||
|
expect(existsSync(result.worktreePath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it.skip("detects paseo-owned worktrees across realpath differences (macOS /var vs /private/var)", async () => {
|
it.skip("detects paseo-owned worktrees across realpath differences (macOS /var vs /private/var)", async () => {
|
||||||
// Intentionally create repo using the non-realpath tmpdir() variant (often /var/... on macOS).
|
// Intentionally create repo using the non-realpath tmpdir() variant (often /var/... on macOS).
|
||||||
const varTempDir = mkdtempSync(join(tmpdir(), "worktree-realpath-test-"));
|
const varTempDir = mkdtempSync(join(tmpdir(), "worktree-realpath-test-"));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { execFile } from "child_process";
|
|||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { existsSync, mkdirSync, realpathSync, rmSync, statSync } from "fs";
|
import { existsSync, mkdirSync, realpathSync, rmSync, statSync } from "fs";
|
||||||
import { copyFile, rm, stat } from "fs/promises";
|
import { copyFile, rm, stat } from "fs/promises";
|
||||||
import { join, basename, dirname, resolve, sep } from "path";
|
import { join, basename, dirname, isAbsolute, resolve, sep } from "path";
|
||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import stripAnsi from "strip-ansi";
|
import stripAnsi from "strip-ansi";
|
||||||
@@ -31,6 +31,7 @@ import { resolvePaseoHome } from "../server/paseo-home.js";
|
|||||||
import { createExternalProcessEnv } from "../server/paseo-env.js";
|
import { createExternalProcessEnv } from "../server/paseo-env.js";
|
||||||
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
||||||
import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
||||||
|
import { expandTilde } from "./path.js";
|
||||||
|
|
||||||
export { slugify, validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
export { slugify, validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
||||||
|
|
||||||
@@ -150,6 +151,11 @@ export interface PaseoWorktreeOwnership {
|
|||||||
worktreePath?: string;
|
worktreePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorktreeRootOptions {
|
||||||
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type WorktreeSource =
|
export type WorktreeSource =
|
||||||
| { kind: "branch-off"; baseBranch: string; branchName: string }
|
| { kind: "branch-off"; baseBranch: string; branchName: string }
|
||||||
| { kind: "checkout-branch"; branchName: string }
|
| { kind: "checkout-branch"; branchName: string }
|
||||||
@@ -168,12 +174,14 @@ export interface CreateWorktreeOptions {
|
|||||||
source: WorktreeSource;
|
source: WorktreeSource;
|
||||||
runSetup: boolean;
|
runSetup: boolean;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolveExistingWorktreeForSlugOptions {
|
interface ResolveExistingWorktreeForSlugOptions {
|
||||||
slug: string;
|
slug: string;
|
||||||
repoRoot: string;
|
repoRoot: string;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BranchAlreadyCheckedOutError extends Error {
|
export class BranchAlreadyCheckedOutError extends Error {
|
||||||
@@ -766,19 +774,38 @@ export async function deriveWorktreeProjectHash(cwd: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPaseoWorktreesRoot(cwd: string, paseoHome?: string): Promise<string> {
|
export function resolvePaseoWorktreesBaseRoot(options?: WorktreeRootOptions): string {
|
||||||
const home = paseoHome ? resolve(paseoHome) : resolvePaseoHome();
|
if (options?.worktreesRoot) {
|
||||||
|
const expandedRoot = expandTilde(options.worktreesRoot);
|
||||||
|
if (isAbsolute(expandedRoot)) {
|
||||||
|
return resolve(expandedRoot);
|
||||||
|
}
|
||||||
|
const home = options.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
|
||||||
|
return resolve(home, expandedRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
const home = options?.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
|
||||||
|
return join(home, "worktrees");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPaseoWorktreesRoot(
|
||||||
|
cwd: string,
|
||||||
|
paseoHome?: string,
|
||||||
|
worktreesRoot?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const baseRoot = resolvePaseoWorktreesBaseRoot({ paseoHome, worktreesRoot });
|
||||||
const projectHash = await deriveWorktreeProjectHash(cwd);
|
const projectHash = await deriveWorktreeProjectHash(cwd);
|
||||||
return join(home, "worktrees", projectHash);
|
return join(baseRoot, projectHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function computeWorktreePath(
|
export async function computeWorktreePath(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
paseoHome?: string,
|
paseoHome?: string,
|
||||||
|
worktreesRoot?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const worktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome);
|
const projectWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot);
|
||||||
return join(worktreesRoot, slug);
|
return join(projectWorktreesRoot, slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePathForOwnership(input: string): string {
|
function normalizePathForOwnership(input: string): string {
|
||||||
@@ -798,7 +825,7 @@ function resolveRepoRootFromGitCommonDir(commonDir: string): string {
|
|||||||
|
|
||||||
export async function isPaseoOwnedWorktreeCwd(
|
export async function isPaseoOwnedWorktreeCwd(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
options?: { paseoHome?: string },
|
options?: WorktreeRootOptions,
|
||||||
): Promise<PaseoWorktreeOwnership> {
|
): Promise<PaseoWorktreeOwnership> {
|
||||||
const resolvedCwd = normalizePathForOwnership(cwd);
|
const resolvedCwd = normalizePathForOwnership(cwd);
|
||||||
|
|
||||||
@@ -813,10 +840,10 @@ export async function isPaseoOwnedWorktreeCwd(
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
const paseoHome = options?.paseoHome ? resolve(options.paseoHome) : resolvePaseoHome();
|
const worktreesBaseRoot = resolvePaseoWorktreesBaseRoot(options);
|
||||||
const paseoWorktreesPrefix = normalizePathForOwnership(join(paseoHome, "worktrees")) + sep;
|
const paseoWorktreesPrefix = normalizePathForOwnership(worktreesBaseRoot) + sep;
|
||||||
|
|
||||||
// Ownership is defined by the path living under $PASEO_HOME/worktrees/<hash>/<slug>[/...].
|
// Ownership is defined by the path living under <worktrees-root>/<hash>/<slug>[/...].
|
||||||
// The <hash>/<slug> prefix is Paseo-private — nothing else writes there — so the
|
// The <hash>/<slug> prefix is Paseo-private — nothing else writes there — so the
|
||||||
// path shape alone is sufficient proof of ownership, even when git has already
|
// path shape alone is sufficient proof of ownership, even when git has already
|
||||||
// forgotten about the worktree.
|
// forgotten about the worktree.
|
||||||
@@ -838,7 +865,7 @@ export async function isPaseoOwnedWorktreeCwd(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const worktreesRoot = join(paseoHome, "worktrees", parts[0]);
|
const worktreesRoot = join(worktreesBaseRoot, parts[0]);
|
||||||
return {
|
return {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
...(repoRoot !== undefined ? { repoRoot } : {}),
|
...(repoRoot !== undefined ? { repoRoot } : {}),
|
||||||
@@ -901,17 +928,19 @@ function resolveWorktreeCreatedAtIso(worktreePath: string): string {
|
|||||||
export async function listPaseoWorktrees({
|
export async function listPaseoWorktrees({
|
||||||
cwd,
|
cwd,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
|
worktreesRoot,
|
||||||
}: {
|
}: {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesRoot?: string;
|
||||||
}): Promise<PaseoWorktreeInfo[]> {
|
}): Promise<PaseoWorktreeInfo[]> {
|
||||||
const worktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome);
|
const projectWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot);
|
||||||
const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
|
const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
|
||||||
cwd,
|
cwd,
|
||||||
envOverlay: READ_ONLY_GIT_ENV,
|
envOverlay: READ_ONLY_GIT_ENV,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootPrefix = normalizePathForOwnership(worktreesRoot) + sep;
|
const rootPrefix = normalizePathForOwnership(projectWorktreesRoot) + sep;
|
||||||
return parseWorktreeList(stdout)
|
return parseWorktreeList(stdout)
|
||||||
.map((entry) => Object.assign({}, entry, { path: normalizePathForOwnership(entry.path) }))
|
.map((entry) => Object.assign({}, entry, { path: normalizePathForOwnership(entry.path) }))
|
||||||
.filter((entry) => entry.path.startsWith(rootPrefix))
|
.filter((entry) => entry.path.startsWith(rootPrefix))
|
||||||
@@ -924,10 +953,12 @@ export async function resolveExistingWorktreeForSlug({
|
|||||||
slug,
|
slug,
|
||||||
repoRoot,
|
repoRoot,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
|
worktreesRoot,
|
||||||
}: ResolveExistingWorktreeForSlugOptions): Promise<WorktreeConfig | null> {
|
}: ResolveExistingWorktreeForSlugOptions): Promise<WorktreeConfig | null> {
|
||||||
const worktrees = await listPaseoWorktrees({
|
const worktrees = await listPaseoWorktrees({
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
|
worktreesRoot,
|
||||||
});
|
});
|
||||||
const slugSuffix = `${sep}${slug}`;
|
const slugSuffix = `${sep}${slug}`;
|
||||||
const existingWorktree = worktrees.find((worktree) => worktree.path.endsWith(slugSuffix));
|
const existingWorktree = worktrees.find((worktree) => worktree.path.endsWith(slugSuffix));
|
||||||
@@ -952,7 +983,7 @@ export async function resolveExistingWorktreeForSlug({
|
|||||||
|
|
||||||
export async function resolvePaseoWorktreeRootForCwd(
|
export async function resolvePaseoWorktreeRootForCwd(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
options?: { paseoHome?: string },
|
options?: WorktreeRootOptions,
|
||||||
): Promise<{ repoRoot: string; worktreeRoot: string; worktreePath: string } | null> {
|
): Promise<{ repoRoot: string; worktreeRoot: string; worktreePath: string } | null> {
|
||||||
let gitCommonDir: string;
|
let gitCommonDir: string;
|
||||||
try {
|
try {
|
||||||
@@ -961,7 +992,11 @@ export async function resolvePaseoWorktreeRootForCwd(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const worktreesRoot = await getPaseoWorktreesRoot(cwd, options?.paseoHome);
|
const worktreesRoot = await getPaseoWorktreesRoot(
|
||||||
|
cwd,
|
||||||
|
options?.paseoHome,
|
||||||
|
options?.worktreesRoot,
|
||||||
|
);
|
||||||
const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
|
const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
|
||||||
|
|
||||||
let worktreeRoot: string | null = null;
|
let worktreeRoot: string | null = null;
|
||||||
@@ -987,6 +1022,7 @@ export async function resolvePaseoWorktreeRootForCwd(
|
|||||||
const knownWorktrees = await listPaseoWorktrees({
|
const knownWorktrees = await listPaseoWorktrees({
|
||||||
cwd,
|
cwd,
|
||||||
paseoHome: options?.paseoHome,
|
paseoHome: options?.paseoHome,
|
||||||
|
worktreesRoot: options?.worktreesRoot,
|
||||||
});
|
});
|
||||||
const match = knownWorktrees.find((entry) => entry.path === resolvedWorktreeRoot);
|
const match = knownWorktrees.find((entry) => entry.path === resolvedWorktreeRoot);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
@@ -1006,12 +1042,14 @@ export async function deletePaseoWorktree({
|
|||||||
worktreeSlug,
|
worktreeSlug,
|
||||||
worktreesRoot,
|
worktreesRoot,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
|
worktreesBaseRoot,
|
||||||
}: {
|
}: {
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
worktreePath?: string;
|
worktreePath?: string;
|
||||||
worktreeSlug?: string;
|
worktreeSlug?: string;
|
||||||
worktreesRoot?: string;
|
worktreesRoot?: string;
|
||||||
paseoHome?: string;
|
paseoHome?: string;
|
||||||
|
worktreesBaseRoot?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (!worktreePath && !worktreeSlug) {
|
if (!worktreePath && !worktreeSlug) {
|
||||||
throw new Error("worktreePath or worktreeSlug is required");
|
throw new Error("worktreePath or worktreeSlug is required");
|
||||||
@@ -1024,7 +1062,7 @@ export async function deletePaseoWorktree({
|
|||||||
if (worktreesRoot) {
|
if (worktreesRoot) {
|
||||||
resolvedWorktreesRoot = worktreesRoot;
|
resolvedWorktreesRoot = worktreesRoot;
|
||||||
} else if (cwd) {
|
} else if (cwd) {
|
||||||
resolvedWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome);
|
resolvedWorktreesRoot = await getPaseoWorktreesRoot(cwd, paseoHome, worktreesBaseRoot);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("cwd or worktreesRoot is required to delete a Paseo worktree");
|
throw new Error("cwd or worktreesRoot is required to delete a Paseo worktree");
|
||||||
}
|
}
|
||||||
@@ -1033,8 +1071,12 @@ export async function deletePaseoWorktree({
|
|||||||
const requestedPath = worktreePath ?? join(resolvedWorktreesRoot, worktreeSlug!);
|
const requestedPath = worktreePath ?? join(resolvedWorktreesRoot, worktreeSlug!);
|
||||||
const resolvedRequested = normalizePathForOwnership(requestedPath);
|
const resolvedRequested = normalizePathForOwnership(requestedPath);
|
||||||
const resolvedWorktree =
|
const resolvedWorktree =
|
||||||
(await resolvePaseoWorktreeRootForCwd(requestedPath, { paseoHome }))?.worktreePath ??
|
(
|
||||||
resolvedRequested;
|
await resolvePaseoWorktreeRootForCwd(requestedPath, {
|
||||||
|
paseoHome,
|
||||||
|
worktreesRoot: worktreesBaseRoot,
|
||||||
|
})
|
||||||
|
)?.worktreePath ?? resolvedRequested;
|
||||||
|
|
||||||
if (!resolvedWorktree.startsWith(resolvedRoot)) {
|
if (!resolvedWorktree.startsWith(resolvedRoot)) {
|
||||||
throw new Error("Refusing to delete non-Paseo worktree");
|
throw new Error("Refusing to delete non-Paseo worktree");
|
||||||
@@ -1121,9 +1163,10 @@ export const createWorktree = async ({
|
|||||||
worktreeSlug,
|
worktreeSlug,
|
||||||
runSetup,
|
runSetup,
|
||||||
paseoHome,
|
paseoHome,
|
||||||
|
worktreesRoot,
|
||||||
}: CreateWorktreeOptions): Promise<WorktreeConfig> => {
|
}: CreateWorktreeOptions): Promise<WorktreeConfig> => {
|
||||||
const sourcePlan = await resolveWorktreeSourcePlan({ cwd, source, desiredSlug: worktreeSlug });
|
const sourcePlan = await resolveWorktreeSourcePlan({ cwd, source, desiredSlug: worktreeSlug });
|
||||||
let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome), worktreeSlug);
|
let worktreePath = join(await getPaseoWorktreesRoot(cwd, paseoHome, worktreesRoot), worktreeSlug);
|
||||||
mkdirSync(dirname(worktreePath), { recursive: true });
|
mkdirSync(dirname(worktreePath), { recursive: true });
|
||||||
|
|
||||||
// Also handle worktree path collision
|
// Also handle worktree path collision
|
||||||
|
|||||||
@@ -115,6 +115,16 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"worktrees": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"root": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"providers": {
|
"providers": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -54,6 +54,20 @@ Agent providers, both the first-class ones Paseo ships with and custom entries y
|
|||||||
|
|
||||||
See [Providers](/docs/providers) for the mental model and [Supported providers](/docs/supported-providers) for the full list of agents Paseo can launch. For pointing Claude at Anthropic-compatible endpoints (Z.AI, Alibaba/Qwen), multiple profiles, custom binaries, ACP agents, and the `additionalModels` merge behavior, see [Custom providers](/docs/custom-providers). The full field reference lives on GitHub at [docs/custom-providers.md](https://github.com/getpaseo/paseo/blob/main/docs/custom-providers.md).
|
See [Providers](/docs/providers) for the mental model and [Supported providers](/docs/supported-providers) for the full list of agents Paseo can launch. For pointing Claude at Anthropic-compatible endpoints (Z.AI, Alibaba/Qwen), multiple profiles, custom binaries, ACP agents, and the `additionalModels` merge behavior, see [Custom providers](/docs/custom-providers). The full field reference lives on GitHub at [docs/custom-providers.md](https://github.com/getpaseo/paseo/blob/main/docs/custom-providers.md).
|
||||||
|
|
||||||
|
## Worktrees
|
||||||
|
|
||||||
|
New worktrees are created under `$PASEO_HOME/worktrees` by default. To place new worktrees somewhere else, set `worktrees.root`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"worktrees": {
|
||||||
|
"root": "/mnt/fast/paseo-worktrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Relative paths are resolved against `PASEO_HOME`. Existing worktrees remain where they are; changing this setting only changes where Paseo creates and discovers Paseo-managed worktrees going forward.
|
||||||
|
|
||||||
## Voice
|
## Voice
|
||||||
|
|
||||||
Voice is configured through `features.dictation` and `features.voiceMode`, with provider credentials under `providers`.
|
Voice is configured through `features.dictation` and `features.voiceMode`, with provider credentials under `providers`.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Each agent runs in its own git worktree, a separate directory on a separate bran
|
|||||||
|
|
||||||
## Layout and workflow
|
## Layout and workflow
|
||||||
|
|
||||||
Worktrees live under `$PASEO_HOME/worktrees/`, grouped by a hash of the source checkout path. Each worktree gets a random slug; the branch name is chosen when you first launch an agent.
|
Worktrees live under `$PASEO_HOME/worktrees/` by default, grouped by a hash of the source checkout path. You can change the base directory with `worktrees.root` in `config.json`. Each worktree gets a random slug; the branch name is chosen when you first launch an agent.
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.paseo/worktrees/
|
~/.paseo/worktrees/
|
||||||
@@ -20,6 +20,16 @@ Worktrees live under `$PASEO_HOME/worktrees/`, grouped by a hash of the source c
|
|||||||
└── bold-owl/
|
└── bold-owl/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
With a custom root, Paseo keeps the same hashed layout under that directory:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"worktrees": {
|
||||||
|
"root": "/mnt/fast/paseo-worktrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
1. Create a worktree, Paseo runs your setup hooks
|
1. Create a worktree, Paseo runs your setup hooks
|
||||||
2. Launch an agent, a branch is created or assigned
|
2. Launch an agent, a branch is created or assigned
|
||||||
3. Review the diff against the base branch
|
3. Review the diff against the base branch
|
||||||
|
|||||||
@@ -124,15 +124,15 @@ The desktop app's first-run hook (`installCli`) symlinks this to `~/.local/bin/p
|
|||||||
|
|
||||||
Daemon-client architecture: the daemon owns agent lifecycle, state, and the WebSocket API. Tools, CLI, mobile, and desktop apps are all clients.
|
Daemon-client architecture: the daemon owns agent lifecycle, state, and the WebSocket API. Tools, CLI, mobile, and desktop apps are all clients.
|
||||||
|
|
||||||
| | Default |
|
| | Default |
|
||||||
| -------------- | ------------------------------------------ |
|
| -------------- | --------------------------------------------------------------- |
|
||||||
| Listen address | `127.0.0.1:6767` (override `PASEO_LISTEN`) |
|
| Listen address | `127.0.0.1:6767` (override `PASEO_LISTEN`) |
|
||||||
| Home | `~/.paseo` (override `PASEO_HOME`) |
|
| Home | `~/.paseo` (override `PASEO_HOME`) |
|
||||||
| Daemon log | `$PASEO_HOME/daemon.log` |
|
| Daemon log | `$PASEO_HOME/daemon.log` |
|
||||||
| Agent state | `$PASEO_HOME/agents/<id>.json` |
|
| Agent state | `$PASEO_HOME/agents/<id>.json` |
|
||||||
| Worktrees | `$PASEO_HOME/worktrees/` |
|
| Worktrees | `$PASEO_HOME/worktrees/` (or `worktrees.root` in `config.json`) |
|
||||||
| PID file | `$PASEO_HOME/paseo.pid` |
|
| PID file | `$PASEO_HOME/paseo.pid` |
|
||||||
| Health | `GET http://127.0.0.1:6767/api/health` |
|
| Health | `GET http://127.0.0.1:6767/api/health` |
|
||||||
|
|
||||||
Debug order:
|
Debug order:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user