fix(worktree): type expected errors (#27296)
This commit is contained in:
@@ -54,6 +54,22 @@ export const ToolListQuery = Schema.Struct({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const WorktreeList = Schema.Array(Schema.String)
|
const WorktreeList = Schema.Array(Schema.String)
|
||||||
|
const WorktreeErrorName = Schema.Union([
|
||||||
|
Schema.Literal("WorktreeNotGitError"),
|
||||||
|
Schema.Literal("WorktreeNameGenerationFailedError"),
|
||||||
|
Schema.Literal("WorktreeCreateFailedError"),
|
||||||
|
Schema.Literal("WorktreeStartCommandFailedError"),
|
||||||
|
Schema.Literal("WorktreeRemoveFailedError"),
|
||||||
|
Schema.Literal("WorktreeResetFailedError"),
|
||||||
|
Schema.Literal("WorktreeListFailedError"),
|
||||||
|
])
|
||||||
|
export class WorktreeApiError extends Schema.ErrorClass<WorktreeApiError>("WorktreeError")(
|
||||||
|
{
|
||||||
|
name: WorktreeErrorName,
|
||||||
|
data: Schema.Struct({ message: Schema.String }),
|
||||||
|
},
|
||||||
|
{ httpApiStatus: 400 },
|
||||||
|
) {}
|
||||||
export const SessionListQuery = Schema.Struct({
|
export const SessionListQuery = Schema.Struct({
|
||||||
...WorkspaceRoutingQueryFields,
|
...WorkspaceRoutingQueryFields,
|
||||||
roots: Schema.optional(QueryBoolean),
|
roots: Schema.optional(QueryBoolean),
|
||||||
@@ -141,6 +157,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, {
|
HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, {
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(WorktreeList, "List of worktree directories"),
|
success: described(WorktreeList, "List of worktree directories"),
|
||||||
|
error: WorktreeApiError,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "worktree.list",
|
identifier: "worktree.list",
|
||||||
@@ -152,7 +169,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Schema.optional(Worktree.CreateInput),
|
payload: Schema.optional(Worktree.CreateInput),
|
||||||
success: described(Worktree.Info, "Worktree created"),
|
success: described(Worktree.Info, "Worktree created"),
|
||||||
error: HttpApiError.BadRequest,
|
error: WorktreeApiError,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "worktree.create",
|
identifier: "worktree.create",
|
||||||
@@ -164,7 +181,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Worktree.RemoveInput,
|
payload: Worktree.RemoveInput,
|
||||||
success: described(Schema.Boolean, "Worktree removed"),
|
success: described(Schema.Boolean, "Worktree removed"),
|
||||||
error: HttpApiError.BadRequest,
|
error: WorktreeApiError,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "worktree.remove",
|
identifier: "worktree.remove",
|
||||||
@@ -176,7 +193,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
|||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
payload: Worktree.ResetInput,
|
payload: Worktree.ResetInput,
|
||||||
success: described(Schema.Boolean, "Worktree reset"),
|
success: described(Schema.Boolean, "Worktree reset"),
|
||||||
error: HttpApiError.BadRequest,
|
error: WorktreeApiError,
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "worktree.reset",
|
identifier: "worktree.reset",
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ import { Effect, Option } from "effect"
|
|||||||
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
|
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
|
||||||
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
||||||
import { InstanceHttpApi } from "../api"
|
import { InstanceHttpApi } from "../api"
|
||||||
import { ConsoleSwitchPayload, SessionListQuery, ToolListQuery } from "../groups/experimental"
|
import { ConsoleSwitchPayload, SessionListQuery, ToolListQuery, WorktreeApiError } from "../groups/experimental"
|
||||||
|
|
||||||
|
function mapWorktreeError<A, R>(self: Effect.Effect<A, Worktree.Error, R>) {
|
||||||
|
return self.pipe(
|
||||||
|
Effect.mapError(
|
||||||
|
(error) => new WorktreeApiError({ name: error._tag, data: { message: error.message } }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "experimental", (handlers) =>
|
export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "experimental", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -100,14 +108,14 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
|
|||||||
const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: {
|
const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: {
|
||||||
payload: Worktree.CreateInput | undefined
|
payload: Worktree.CreateInput | undefined
|
||||||
}) {
|
}) {
|
||||||
return yield* worktreeSvc.create(ctx.payload)
|
return yield* mapWorktreeError(worktreeSvc.create(ctx.payload))
|
||||||
})
|
})
|
||||||
|
|
||||||
const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: {
|
const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: {
|
||||||
payload: Worktree.RemoveInput
|
payload: Worktree.RemoveInput
|
||||||
}) {
|
}) {
|
||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
yield* worktreeSvc.remove(input.payload)
|
yield* mapWorktreeError(worktreeSvc.remove(input.payload))
|
||||||
yield* project.removeSandbox(ctx.project.id, input.payload.directory)
|
yield* project.removeSandbox(ctx.project.id, input.payload.directory)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -115,7 +123,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
|
|||||||
const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: {
|
const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: {
|
||||||
payload: Worktree.ResetInput
|
payload: Worktree.ResetInput
|
||||||
}) {
|
}) {
|
||||||
yield* worktreeSvc.reset(ctx.payload)
|
yield* mapWorktreeError(worktreeSvc.reset(ctx.payload))
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
|
|||||||
status: iife(() => {
|
status: iife(() => {
|
||||||
if (error instanceof Provider.ModelNotFoundError) return 400
|
if (error instanceof Provider.ModelNotFoundError) return 400
|
||||||
if (error.name === "ProviderAuthValidationFailed") return 400
|
if (error.name === "ProviderAuthValidationFailed") return 400
|
||||||
if (error.name.startsWith("Worktree")) return 400
|
|
||||||
return 500
|
return 500
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { NamedError } from "@opencode-ai/core/util/error"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { InstanceLayer } from "@/project/instance-layer"
|
import { InstanceLayer } from "@/project/instance-layer"
|
||||||
import { InstanceStore } from "@/project/instance-store"
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
@@ -64,33 +63,48 @@ export const ResetInput = Schema.Struct({
|
|||||||
}).annotate({ identifier: "WorktreeResetInput" })
|
}).annotate({ identifier: "WorktreeResetInput" })
|
||||||
export type ResetInput = Schema.Schema.Type<typeof ResetInput>
|
export type ResetInput = Schema.Schema.Type<typeof ResetInput>
|
||||||
|
|
||||||
export const NotGitError = NamedError.create("WorktreeNotGitError", {
|
export class NotGitError extends Schema.TaggedErrorClass<NotGitError>()("WorktreeNotGitError", {
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
})
|
}) {}
|
||||||
|
|
||||||
export const NameGenerationFailedError = NamedError.create("WorktreeNameGenerationFailedError", {
|
export class NameGenerationFailedError extends Schema.TaggedErrorClass<NameGenerationFailedError>()(
|
||||||
message: Schema.String,
|
"WorktreeNameGenerationFailedError",
|
||||||
})
|
{
|
||||||
|
message: Schema.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
export const CreateFailedError = NamedError.create("WorktreeCreateFailedError", {
|
export class CreateFailedError extends Schema.TaggedErrorClass<CreateFailedError>()("WorktreeCreateFailedError", {
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
})
|
}) {}
|
||||||
|
|
||||||
export const StartCommandFailedError = NamedError.create("WorktreeStartCommandFailedError", {
|
export class StartCommandFailedError extends Schema.TaggedErrorClass<StartCommandFailedError>()(
|
||||||
message: Schema.String,
|
"WorktreeStartCommandFailedError",
|
||||||
})
|
{
|
||||||
|
message: Schema.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
export const RemoveFailedError = NamedError.create("WorktreeRemoveFailedError", {
|
export class RemoveFailedError extends Schema.TaggedErrorClass<RemoveFailedError>()("WorktreeRemoveFailedError", {
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
})
|
}) {}
|
||||||
|
|
||||||
export const ResetFailedError = NamedError.create("WorktreeResetFailedError", {
|
export class ResetFailedError extends Schema.TaggedErrorClass<ResetFailedError>()("WorktreeResetFailedError", {
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
})
|
}) {}
|
||||||
|
|
||||||
export const ListFailedError = NamedError.create("WorktreeListFailedError", {
|
export class ListFailedError extends Schema.TaggedErrorClass<ListFailedError>()("WorktreeListFailedError", {
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
})
|
}) {}
|
||||||
|
|
||||||
|
export type Error =
|
||||||
|
| NotGitError
|
||||||
|
| NameGenerationFailedError
|
||||||
|
| CreateFailedError
|
||||||
|
| StartCommandFailedError
|
||||||
|
| RemoveFailedError
|
||||||
|
| ResetFailedError
|
||||||
|
| ListFailedError
|
||||||
|
|
||||||
function slugify(input: string) {
|
function slugify(input: string) {
|
||||||
return input
|
return input
|
||||||
@@ -121,12 +135,12 @@ function failedRemoves(...chunks: string[]) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly makeWorktreeInfo: (options?: { name?: string; detached?: boolean }) => Effect.Effect<Info>
|
readonly makeWorktreeInfo: (options?: { name?: string; detached?: boolean }) => Effect.Effect<Info, Error>
|
||||||
readonly createFromInfo: (info: Info, startCommand?: string) => Effect.Effect<void>
|
readonly createFromInfo: (info: Info, startCommand?: string) => Effect.Effect<void, Error>
|
||||||
readonly create: (input?: CreateInput) => Effect.Effect<Info>
|
readonly create: (input?: CreateInput) => Effect.Effect<Info, Error>
|
||||||
readonly list: () => Effect.Effect<(Omit<Info, "branch"> & { branch?: string })[]>
|
readonly list: () => Effect.Effect<(Omit<Info, "branch"> & { branch?: string })[], Error>
|
||||||
readonly remove: (input: RemoveInput) => Effect.Effect<boolean>
|
readonly remove: (input: RemoveInput) => Effect.Effect<boolean, Error>
|
||||||
readonly reset: (input: ResetInput) => Effect.Effect<boolean>
|
readonly reset: (input: ResetInput) => Effect.Effect<boolean, Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Worktree") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Worktree") {}
|
||||||
@@ -193,7 +207,7 @@ export const layer: Layer.Layer<
|
|||||||
|
|
||||||
return { name, directory, ...(branch ? { branch } : {}) }
|
return { name, directory, ...(branch ? { branch } : {}) }
|
||||||
}
|
}
|
||||||
throw new NameGenerationFailedError({ message: "Failed to generate a unique worktree name" })
|
return yield* new NameGenerationFailedError({ message: "Failed to generate a unique worktree name" })
|
||||||
})
|
})
|
||||||
|
|
||||||
const makeWorktreeInfo = Effect.fn("Worktree.makeWorktreeInfo")(function* (input?: {
|
const makeWorktreeInfo = Effect.fn("Worktree.makeWorktreeInfo")(function* (input?: {
|
||||||
@@ -202,7 +216,7 @@ export const layer: Layer.Layer<
|
|||||||
}) {
|
}) {
|
||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
if (ctx.project.vcs !== "git") {
|
if (ctx.project.vcs !== "git") {
|
||||||
throw new NotGitError({ message: "Worktrees are only supported for git projects" })
|
return yield* new NotGitError({ message: "Worktrees are only supported for git projects" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = pathSvc.join(Global.Path.data, "worktree", ctx.project.id)
|
const root = pathSvc.join(Global.Path.data, "worktree", ctx.project.id)
|
||||||
@@ -220,7 +234,7 @@ export const layer: Layer.Layer<
|
|||||||
{ cwd: ctx.worktree },
|
{ cwd: ctx.worktree },
|
||||||
)
|
)
|
||||||
if (created.code !== 0) {
|
if (created.code !== 0) {
|
||||||
throw new CreateFailedError({ message: created.stderr || created.text || "Failed to create git worktree" })
|
return yield* new CreateFailedError({ message: created.stderr || created.text || "Failed to create git worktree" })
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* project.addSandbox(ctx.project.id, info.directory).pipe(Effect.catch(() => Effect.void))
|
yield* project.addSandbox(ctx.project.id, info.directory).pipe(Effect.catch(() => Effect.void))
|
||||||
@@ -336,7 +350,7 @@ export const layer: Layer.Layer<
|
|||||||
|
|
||||||
const result = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
|
const result = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
|
||||||
if (result.code !== 0) {
|
if (result.code !== 0) {
|
||||||
throw new ListFailedError({ message: result.stderr || result.text || "Failed to read git worktrees" })
|
return yield* new ListFailedError({ message: result.stderr || result.text || "Failed to read git worktrees" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const primary = yield* canonical(ctx.worktree)
|
const primary = yield* canonical(ctx.worktree)
|
||||||
@@ -364,27 +378,27 @@ export const layer: Layer.Layer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanDirectory(target: string) {
|
function cleanDirectory(target: string) {
|
||||||
return Effect.promise(() =>
|
return Effect.tryPromise({
|
||||||
import("fs/promises")
|
try: () =>
|
||||||
.then((fsp) => fsp.rm(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }))
|
import("fs/promises").then((fsp) =>
|
||||||
.catch((error) => {
|
fsp.rm(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }),
|
||||||
const message = errorMessage(error)
|
),
|
||||||
throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" })
|
catch: (error) =>
|
||||||
}),
|
new RemoveFailedError({ message: errorMessage(error) || "Failed to remove git worktree directory" }),
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) {
|
const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) {
|
||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
if (ctx.project.vcs !== "git") {
|
if (ctx.project.vcs !== "git") {
|
||||||
throw new NotGitError({ message: "Worktrees are only supported for git projects" })
|
return yield* new NotGitError({ message: "Worktrees are only supported for git projects" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const directory = yield* canonical(input.directory)
|
const directory = yield* canonical(input.directory)
|
||||||
|
|
||||||
const list = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
|
const list = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
|
||||||
if (list.code !== 0) {
|
if (list.code !== 0) {
|
||||||
throw new RemoveFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" })
|
return yield* new RemoveFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = parseWorktreeList(list.text)
|
const entries = parseWorktreeList(list.text)
|
||||||
@@ -404,14 +418,14 @@ export const layer: Layer.Layer<
|
|||||||
if (removed.code !== 0) {
|
if (removed.code !== 0) {
|
||||||
const next = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
|
const next = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
|
||||||
if (next.code !== 0) {
|
if (next.code !== 0) {
|
||||||
throw new RemoveFailedError({
|
return yield* new RemoveFailedError({
|
||||||
message: removed.stderr || removed.text || next.stderr || next.text || "Failed to remove git worktree",
|
message: removed.stderr || removed.text || next.stderr || next.text || "Failed to remove git worktree",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const stale = yield* locateWorktree(parseWorktreeList(next.text), directory)
|
const stale = yield* locateWorktree(parseWorktreeList(next.text), directory)
|
||||||
if (stale?.path) {
|
if (stale?.path) {
|
||||||
throw new RemoveFailedError({ message: removed.stderr || removed.text || "Failed to remove git worktree" })
|
return yield* new RemoveFailedError({ message: removed.stderr || removed.text || "Failed to remove git worktree" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +435,7 @@ export const layer: Layer.Layer<
|
|||||||
if (branch) {
|
if (branch) {
|
||||||
const deleted = yield* git(["branch", "-D", branch], { cwd: ctx.worktree })
|
const deleted = yield* git(["branch", "-D", branch], { cwd: ctx.worktree })
|
||||||
if (deleted.code !== 0) {
|
if (deleted.code !== 0) {
|
||||||
throw new RemoveFailedError({
|
return yield* new RemoveFailedError({
|
||||||
message: deleted.stderr || deleted.text || "Failed to delete worktree branch",
|
message: deleted.stderr || deleted.text || "Failed to delete worktree branch",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -436,7 +450,7 @@ export const layer: Layer.Layer<
|
|||||||
error: (r: GitResult) => Error,
|
error: (r: GitResult) => Error,
|
||||||
) {
|
) {
|
||||||
const result = yield* git(args, opts)
|
const result = yield* git(args, opts)
|
||||||
if (result.code !== 0) throw error(result)
|
if (result.code !== 0) return yield* error(result)
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -511,30 +525,30 @@ export const layer: Layer.Layer<
|
|||||||
const reset = Effect.fn("Worktree.reset")(function* (input: ResetInput) {
|
const reset = Effect.fn("Worktree.reset")(function* (input: ResetInput) {
|
||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
if (ctx.project.vcs !== "git") {
|
if (ctx.project.vcs !== "git") {
|
||||||
throw new NotGitError({ message: "Worktrees are only supported for git projects" })
|
return yield* new NotGitError({ message: "Worktrees are only supported for git projects" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const directory = yield* canonical(input.directory)
|
const directory = yield* canonical(input.directory)
|
||||||
const primary = yield* canonical(ctx.worktree)
|
const primary = yield* canonical(ctx.worktree)
|
||||||
if (directory === primary) {
|
if (directory === primary) {
|
||||||
throw new ResetFailedError({ message: "Cannot reset the primary workspace" })
|
return yield* new ResetFailedError({ message: "Cannot reset the primary workspace" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
|
const list = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
|
||||||
if (list.code !== 0) {
|
if (list.code !== 0) {
|
||||||
throw new ResetFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" })
|
return yield* new ResetFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = yield* locateWorktree(parseWorktreeList(list.text), directory)
|
const entry = yield* locateWorktree(parseWorktreeList(list.text), directory)
|
||||||
if (!entry?.path) {
|
if (!entry?.path) {
|
||||||
throw new ResetFailedError({ message: "Worktree not found" })
|
return yield* new ResetFailedError({ message: "Worktree not found" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const worktreePath = entry.path
|
const worktreePath = entry.path
|
||||||
|
|
||||||
const base = yield* gitSvc.defaultBranch(ctx.worktree)
|
const base = yield* gitSvc.defaultBranch(ctx.worktree)
|
||||||
if (!base) {
|
if (!base) {
|
||||||
throw new ResetFailedError({ message: "Default branch not found" })
|
return yield* new ResetFailedError({ message: "Default branch not found" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const sep = base.ref.indexOf("/")
|
const sep = base.ref.indexOf("/")
|
||||||
@@ -556,7 +570,7 @@ export const layer: Layer.Layer<
|
|||||||
|
|
||||||
const cleanResult = yield* sweep(worktreePath)
|
const cleanResult = yield* sweep(worktreePath)
|
||||||
if (cleanResult.code !== 0) {
|
if (cleanResult.code !== 0) {
|
||||||
throw new ResetFailedError({ message: cleanResult.stderr || cleanResult.text || "Failed to clean worktree" })
|
return yield* new ResetFailedError({ message: cleanResult.stderr || cleanResult.text || "Failed to clean worktree" })
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* gitExpect(
|
yield* gitExpect(
|
||||||
@@ -579,11 +593,11 @@ export const layer: Layer.Layer<
|
|||||||
|
|
||||||
const status = yield* git(["-c", "core.fsmonitor=false", "status", "--porcelain=v1"], { cwd: worktreePath })
|
const status = yield* git(["-c", "core.fsmonitor=false", "status", "--porcelain=v1"], { cwd: worktreePath })
|
||||||
if (status.code !== 0) {
|
if (status.code !== 0) {
|
||||||
throw new ResetFailedError({ message: status.stderr || status.text || "Failed to read git status" })
|
return yield* new ResetFailedError({ message: status.stderr || status.text || "Failed to read git status" })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.text.trim()) {
|
if (status.text.trim()) {
|
||||||
throw new ResetFailedError({ message: `Worktree reset left local changes:\n${status.text.trim()}` })
|
return yield* new ResetFailedError({ message: `Worktree reset left local changes:\n${status.text.trim()}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* runStartScripts(worktreePath, { projectID: ctx.project.id }).pipe(
|
yield* runStartScripts(worktreePath, { projectID: ctx.project.id }).pipe(
|
||||||
|
|||||||
@@ -135,13 +135,17 @@ describe("Worktree", () => {
|
|||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("throws NotGitError for non-git directories", () =>
|
it.instance("fails with NotGitError for non-git directories", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const svc = yield* Worktree.Service
|
const svc = yield* Worktree.Service
|
||||||
const exit = yield* Effect.exit(svc.makeWorktreeInfo())
|
const exit = yield* Effect.exit(svc.makeWorktreeInfo())
|
||||||
|
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Worktree.NotGitError)
|
if (Exit.isFailure(exit)) {
|
||||||
|
const error = Cause.squash(exit.cause)
|
||||||
|
expect(error).toBeInstanceOf(Worktree.NotGitError)
|
||||||
|
if (error instanceof Worktree.NotGitError) expect(error._tag).toBe("WorktreeNotGitError")
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -286,14 +290,18 @@ describe("Worktree", () => {
|
|||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
it.instance("throws NotGitError for non-git directories", () =>
|
it.instance("fails with NotGitError for non-git directories", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const test = yield* TestInstance
|
const test = yield* TestInstance
|
||||||
const svc = yield* Worktree.Service
|
const svc = yield* Worktree.Service
|
||||||
const exit = yield* Effect.exit(svc.remove({ directory: path.join(test.directory, "fake") }))
|
const exit = yield* Effect.exit(svc.remove({ directory: path.join(test.directory, "fake") }))
|
||||||
|
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Worktree.NotGitError)
|
if (Exit.isFailure(exit)) {
|
||||||
|
const error = Cause.squash(exit.cause)
|
||||||
|
expect(error).toBeInstanceOf(Worktree.NotGitError)
|
||||||
|
if (error instanceof Worktree.NotGitError) expect(error._tag).toBe("WorktreeNotGitError")
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ function withContext<A, E>(
|
|||||||
messages: (sessionID) =>
|
messages: (sessionID) =>
|
||||||
run(modules.Session.Service.use((svc) => svc.messages({ sessionID }).pipe(Effect.orDie))),
|
run(modules.Session.Service.use((svc) => svc.messages({ sessionID }).pipe(Effect.orDie))),
|
||||||
todos: (sessionID, todos) => run(modules.Todo.Service.use((svc) => svc.update({ sessionID, todos }))),
|
todos: (sessionID, todos) => run(modules.Todo.Service.use((svc) => svc.update({ sessionID, todos }))),
|
||||||
worktree: (input) => run(modules.Worktree.Service.use((svc) => svc.create(input))),
|
worktree: (input) => run(modules.Worktree.Service.use((svc) => svc.create(input).pipe(Effect.orDie))),
|
||||||
worktreeRemove: (directory) =>
|
worktreeRemove: (directory) =>
|
||||||
run(modules.Worktree.Service.use((svc) => svc.remove({ directory })).pipe(Effect.ignore)),
|
run(modules.Worktree.Service.use((svc) => svc.remove({ directory })).pipe(Effect.ignore)),
|
||||||
llmText: (value) => Effect.suspend(() => llm().text(value)),
|
llmText: (value) => Effect.suspend(() => llm().text(value)),
|
||||||
|
|||||||
@@ -192,6 +192,23 @@ describe("experimental HttpApi", () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.instance("returns declared worktree errors", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* TestInstance
|
||||||
|
const response = yield* request(ExperimentalPaths.worktree, tmp.directory, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(400)
|
||||||
|
expect(yield* json(response)).toEqual({
|
||||||
|
name: "WorktreeNotGitError",
|
||||||
|
data: { message: "Worktrees are only supported for git projects" },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"serves Console org switch through the default server app",
|
"serves Console org switch through the default server app",
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user