Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/opencode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 7 additions & 5 deletions plugins/opencode/src/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ interface Runtime {
rpc?: typeof rpc
}

const hints: Partial<Record<string, string>> = {
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<string, unknown>): Options {
for (const key of Object.keys(value)) {
if (key !== "executable" && key !== "copyAll" && key !== "hooks" && key !== "database")
Expand All @@ -44,11 +50,7 @@ export function makeStrategy(value: Record<string, unknown> = {}, 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) =>
Expand Down
3 changes: 2 additions & 1 deletion plugins/opencode/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
24 changes: 24 additions & 0 deletions plugins/opencode/test/strategy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading