diff --git a/plugins/opencode/README.md b/plugins/opencode/README.md index 4b69894..b74936a 100644 --- a/plugins/opencode/README.md +++ b/plugins/opencode/README.md @@ -34,3 +34,4 @@ Move sessions with OpenCode's `opencode.session_move` tool. - Starting refs are not supported; Rift snapshots a working directory, not a commit. - Remove a workspace's child Rifts before removing it. - `rift.remove` refuses to remove the workspace the current session is in. +- Windows can't create workspaces, because Rift needs copy-on-write cloning. diff --git a/plugins/opencode/src/strategy.ts b/plugins/opencode/src/strategy.ts index df58552..480bf73 100644 --- a/plugins/opencode/src/strategy.ts +++ b/plugins/opencode/src/strategy.ts @@ -19,6 +19,12 @@ interface Runtime { rpc?: typeof rpc } +const hints: Partial> = { + workspace_not_initialized: "Rift source is not initialized; run `rift init` from the project root first", + initialization_required: "This Rift workspace must be initialized first; run `rift init` from its root folder", + missing_marker: "This Rift workspace is missing its `.rift` marker; run `rift init` to restore it", +} + function parseOptions(value: Record): Options { for (const key of Object.keys(value)) { if (key !== "executable" && key !== "copyAll" && key !== "hooks" && key !== "database") @@ -44,11 +50,7 @@ export function makeStrategy(value: Record = {}, runtime: Runti call(options.executable, { database: options.database, ...command }, signal) const failure = (error: unknown) => { const message = - error instanceof RpcError && error.code === "workspace_not_initialized" - ? "Rift source is not initialized; run `rift init` from the project root first" - : error instanceof Error - ? error.message - : String(error) + (error instanceof RpcError && hints[error.code]) || (error instanceof Error ? error.message : String(error)) return new Worktree.OperationError({ message }) } const committed = (error: unknown, hook: string) => diff --git a/plugins/opencode/src/tools.ts b/plugins/opencode/src/tools.ts index a88ca22..1bd8d2e 100644 --- a/plugins/opencode/src/tools.ts +++ b/plugins/opencode/src/tools.ts @@ -114,7 +114,8 @@ export async function registerTools(ctx: Context, listeners: Listeners) { }) editor.add({ name: "remove", - description: "Remove a Rift workspace from the current project.", + description: + "Remove a Rift workspace from the current project. Refuses the workspace this session is in and workspaces that still have child workspaces.", input: { type: "object", properties: { diff --git a/plugins/opencode/test/strategy.test.ts b/plugins/opencode/test/strategy.test.ts index ddd0757..7a74046 100644 --- a/plugins/opencode/test/strategy.test.ts +++ b/plugins/opencode/test/strategy.test.ts @@ -71,6 +71,30 @@ describe("Rift workspace strategy", () => { }) }) + test("explains initialization failures", async () => { + const failing = (code: string) => + makeStrategy( + {}, + { + rpc: async () => { + throw new RpcError({ code, message: `${code} at /project`, path: "/project" }) + }, + }, + ) + const create = (code: string) => + failing(code).create({ sourceDirectory: "/project", directory: "/worktrees/task" }, context) + await expect(create("workspace_not_initialized")).rejects.toThrow( + "Rift source is not initialized; run `rift init` from the project root first", + ) + await expect(create("initialization_required")).rejects.toThrow( + "This Rift workspace must be initialized first; run `rift init` from its root folder", + ) + await expect(create("missing_marker")).rejects.toThrow( + "This Rift workspace is missing its `.rift` marker; run `rift init` to restore it", + ) + await expect(create("hook_failed")).rejects.toThrow("hook_failed at /project") + }) + test("keeps OpenCode inventory consistent after post-hook failures", async () => { const warnings: Array<{ directory: string; message: string }> = [] const strategy = makeStrategy(