@sven1103/opencode-worktree-workflow 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,46 @@
1
1
  # OpenCode Worktree Workflow
2
2
 
3
- `@sven1103/opencode-worktree-workflow` is an OpenCode plugin that adds git worktree helpers for creating synced feature worktrees and cleaning up merged ones.
3
+ `@sven1103/opencode-worktree-workflow` is an npm package that provides OpenCode git worktree helpers for creating synced feature worktrees and cleaning up merged ones.
4
+
5
+ ## Quick start
6
+
7
+ To get the workflow running in a project:
8
+
9
+ 1. Install the package once by following [Recommended setup](#recommended-setup).
10
+ 2. Enable the plugin in your OpenCode config as shown in [Recommended setup](#recommended-setup).
11
+ 3. If you want manual `/wt-new` and `/wt-clean` triggers, install the markdown files from [Install slash commands](#install-slash-commands).
12
+ 4. If you want policy guidance for when to isolate work, install the skill from [Co-shipped skill](#co-shipped-skill).
13
+ 5. If you need to understand how the local fallback works, see [CLI fallback](#cli-fallback).
14
+
15
+ ## Recommended setup
16
+
17
+ Install the package once:
18
+
19
+ ```sh
20
+ npm install -D @sven1103/opencode-worktree-workflow
21
+ ```
22
+
23
+ Enable the native OpenCode plugin in `opencode.json`:
24
+
25
+ ```json
26
+ {
27
+ "$schema": "https://opencode.ai/config.json",
28
+ "plugin": ["@sven1103/opencode-worktree-workflow"]
29
+ }
30
+ ```
31
+
32
+ This single package provides two access modes:
33
+
34
+ - native plugin tools inside OpenCode: `worktree_prepare`, `worktree_cleanup`
35
+ - local CLI fallback from the same installed package:
36
+ - `npx opencode-worktree-workflow wt-new "<title>" --json`
37
+ - `npx opencode-worktree-workflow wt-clean <args> --json`
38
+
39
+ In practice:
40
+
41
+ - if the plugin is loaded, use the native tools first
42
+ - if the native tools are unavailable, use the local CLI fallback from the same installed package
43
+ - if the package is not installed, no CLI fallback is available
4
44
 
5
45
  ## Install in an OpenCode project
6
46
 
@@ -28,7 +68,7 @@ Keeping the npm dependency in `package.json` makes the installation more durable
28
68
  If you do not already install dependencies in your project, you can add the package directly with npm:
29
69
 
30
70
  ```sh
31
- npm install @sven1103/opencode-worktree-workflow
71
+ npm install -D @sven1103/opencode-worktree-workflow
32
72
  ```
33
73
 
34
74
  ## Install slash commands
@@ -75,12 +115,76 @@ curl -fsSL "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/
75
115
  curl -fsSL "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/download/${VERSION}/wt-clean.md" -o ".opencode/commands/wt-clean.md"
76
116
  ```
77
117
 
118
+ ## Co-shipped skill
119
+
120
+ This repo also co-ships a `worktree-workflow` skill as a policy layer over the package capability.
121
+
122
+ - checked-in skill: `skills/worktree-workflow/SKILL.md`
123
+ - release asset: `SKILL.md`
124
+
125
+ The skill teaches when to use task-scoped worktrees, when repo root is still acceptable, and how to prefer the native tool path before falling back to the packaged CLI.
126
+
127
+ Project-local install (latest release):
128
+
129
+ ```sh
130
+ mkdir -p .opencode/skills/worktree-workflow
131
+ curl -fsSL "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/latest/download/SKILL.md" -o ".opencode/skills/worktree-workflow/SKILL.md"
132
+ ```
133
+
134
+ ```sh
135
+ mkdir -p .opencode/skills/worktree-workflow
136
+ wget -qO ".opencode/skills/worktree-workflow/SKILL.md" "https://github.com/sven1103-agent/opencode-worktree-plugin/releases/latest/download/SKILL.md"
137
+ ```
138
+
139
+ If your setup uses installed skill files, copy the released `SKILL.md` into a `worktree-workflow/` skill folder in the appropriate location for that environment, or consume the checked-in file from this repo directly.
140
+
78
141
  ## What the plugin provides
79
142
 
80
143
  - `worktree_prepare`: create a worktree and matching branch from the latest configured base-branch commit, or the default branch when no base branch is configured
81
144
  - `worktree_cleanup`: preview all connected worktrees against the configured base branch, auto-clean safe ones, and optionally remove selected review items
82
145
 
83
- This package currently focuses on plugin distribution. Slash command packaging can be layered on later.
146
+ This package now ships the plugin capability, a CLI fallback surface, thin slash commands, and a co-shipped policy skill.
147
+
148
+ ## Structured contract
149
+
150
+ The native tool results and CLI `--json` output now use a versioned structured contract with a `schema_version` field.
151
+
152
+ - current `schema_version`: `1.0.0`
153
+ - contract overview: `docs/contract.md`
154
+ - compatibility model: `docs/compatibility.md`
155
+ - checked-in schemas for transparency:
156
+ - `schemas/worktree-prepare.result.schema.json`
157
+ - `schemas/worktree-cleanup-preview.result.schema.json`
158
+ - `schemas/worktree-cleanup-apply.result.schema.json`
159
+
160
+ Human-readable output remains available through the result `message`, but callers should depend on the structured fields rather than parsing prose.
161
+
162
+ ## CLI fallback
163
+
164
+ The npm package also exposes a local CLI so agents can fall back to the same installed package when the native plugin tools are unavailable.
165
+
166
+ Examples:
167
+
168
+ ```sh
169
+ npx opencode-worktree-workflow wt-new "Improve checkout retry logic"
170
+ npx opencode-worktree-workflow wt-new "Improve checkout retry logic" --json
171
+ npx opencode-worktree-workflow wt-clean preview
172
+ npx opencode-worktree-workflow wt-clean apply feature/foo --json
173
+ ```
174
+
175
+ Defaults:
176
+
177
+ - human-readable output by default
178
+ - structured output with `--json`
179
+ - the CLI shares the same underlying implementation and result contract as the native tools
180
+ - the CLI fallback depends on the package already being installed in the project
181
+
182
+ ## Compatibility model
183
+
184
+ The repo keeps config loading, argument normalization, and execution semantics centralized in the package implementation so existing installations continue to work across native tools, CLI fallback, and slash commands.
185
+
186
+ - compatibility overview: `docs/compatibility.md`
187
+ - existing `.opencode/worktree-workflow.json` setups remain the supported configuration path
84
188
 
85
189
  ## Optional project configuration
86
190
 
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@sven1103/opencode-worktree-workflow",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "OpenCode plugin for creating and cleaning up git worktrees.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
+ "bin": {
8
+ "opencode-worktree-workflow": "./src/cli.js"
9
+ },
7
10
  "exports": {
8
11
  ".": "./src/index.js"
9
12
  },
10
13
  "files": [
11
- "src"
14
+ "src",
15
+ "schemas",
16
+ "skills"
12
17
  ],
13
18
  "keywords": [
14
19
  "opencode",
@@ -0,0 +1,185 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/sven1103-agent/opencode-worktree-plugin/schemas/worktree-cleanup-apply.result.schema.json",
4
+ "title": "worktree_cleanup apply result",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "ok",
10
+ "mode",
11
+ "default_branch",
12
+ "base_branch",
13
+ "base_ref",
14
+ "requested_selectors",
15
+ "removed",
16
+ "failed"
17
+ ],
18
+ "properties": {
19
+ "schema_version": {
20
+ "type": "string",
21
+ "const": "1.0.0"
22
+ },
23
+ "ok": {
24
+ "type": "boolean",
25
+ "const": true
26
+ },
27
+ "mode": {
28
+ "type": "string",
29
+ "const": "apply"
30
+ },
31
+ "default_branch": {
32
+ "type": "string",
33
+ "minLength": 1
34
+ },
35
+ "base_branch": {
36
+ "type": "string",
37
+ "minLength": 1
38
+ },
39
+ "base_ref": {
40
+ "type": "string",
41
+ "minLength": 1
42
+ },
43
+ "requested_selectors": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "string"
47
+ }
48
+ },
49
+ "removed": {
50
+ "type": "array",
51
+ "items": {
52
+ "$ref": "#/$defs/removedItem"
53
+ }
54
+ },
55
+ "failed": {
56
+ "type": "array",
57
+ "items": {
58
+ "$ref": "#/$defs/failedItem"
59
+ }
60
+ },
61
+ "message": {
62
+ "type": "string"
63
+ }
64
+ },
65
+ "$defs": {
66
+ "baseCleanupItem": {
67
+ "type": "object",
68
+ "required": [
69
+ "branch",
70
+ "worktree_path",
71
+ "head",
72
+ "status",
73
+ "reason",
74
+ "detached",
75
+ "selectable"
76
+ ],
77
+ "properties": {
78
+ "branch": {
79
+ "type": ["string", "null"]
80
+ },
81
+ "worktree_path": {
82
+ "type": ["string", "null"]
83
+ },
84
+ "head": {
85
+ "type": ["string", "null"]
86
+ },
87
+ "status": {
88
+ "type": ["string", "null"],
89
+ "enum": ["safe", "review", "blocked", null]
90
+ },
91
+ "reason": {
92
+ "type": ["string", "null"]
93
+ },
94
+ "detached": {
95
+ "type": "boolean"
96
+ },
97
+ "selectable": {
98
+ "type": ["boolean", "null"]
99
+ }
100
+ }
101
+ },
102
+ "removedItem": {
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "required": [
106
+ "branch",
107
+ "worktree_path",
108
+ "head",
109
+ "status",
110
+ "reason",
111
+ "detached",
112
+ "selectable",
113
+ "selected"
114
+ ],
115
+ "properties": {
116
+ "branch": {
117
+ "type": ["string", "null"]
118
+ },
119
+ "worktree_path": {
120
+ "type": ["string", "null"]
121
+ },
122
+ "head": {
123
+ "type": ["string", "null"]
124
+ },
125
+ "status": {
126
+ "type": ["string", "null"],
127
+ "enum": ["safe", "review", "blocked", null]
128
+ },
129
+ "reason": {
130
+ "type": ["string", "null"]
131
+ },
132
+ "detached": {
133
+ "type": "boolean"
134
+ },
135
+ "selectable": {
136
+ "type": ["boolean", "null"]
137
+ },
138
+ "selected": {
139
+ "type": "boolean"
140
+ }
141
+ }
142
+ },
143
+ "failedItem": {
144
+ "type": "object",
145
+ "additionalProperties": false,
146
+ "required": [
147
+ "selector",
148
+ "branch",
149
+ "worktree_path",
150
+ "head",
151
+ "status",
152
+ "reason",
153
+ "detached",
154
+ "selectable"
155
+ ],
156
+ "properties": {
157
+ "selector": {
158
+ "type": ["string", "null"]
159
+ },
160
+ "branch": {
161
+ "type": ["string", "null"]
162
+ },
163
+ "worktree_path": {
164
+ "type": ["string", "null"]
165
+ },
166
+ "head": {
167
+ "type": ["string", "null"]
168
+ },
169
+ "status": {
170
+ "type": ["string", "null"],
171
+ "enum": ["safe", "review", "blocked", null]
172
+ },
173
+ "reason": {
174
+ "type": ["string", "null"]
175
+ },
176
+ "detached": {
177
+ "type": "boolean"
178
+ },
179
+ "selectable": {
180
+ "type": ["boolean", "null"]
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,109 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/sven1103-agent/opencode-worktree-plugin/schemas/worktree-cleanup-preview.result.schema.json",
4
+ "title": "worktree_cleanup preview result",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "ok",
10
+ "mode",
11
+ "default_branch",
12
+ "base_branch",
13
+ "base_ref",
14
+ "groups"
15
+ ],
16
+ "properties": {
17
+ "schema_version": {
18
+ "type": "string",
19
+ "const": "1.0.0"
20
+ },
21
+ "ok": {
22
+ "type": "boolean",
23
+ "const": true
24
+ },
25
+ "mode": {
26
+ "type": "string",
27
+ "const": "preview"
28
+ },
29
+ "default_branch": {
30
+ "type": "string",
31
+ "minLength": 1
32
+ },
33
+ "base_branch": {
34
+ "type": "string",
35
+ "minLength": 1
36
+ },
37
+ "base_ref": {
38
+ "type": "string",
39
+ "minLength": 1
40
+ },
41
+ "groups": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["safe", "review", "blocked"],
45
+ "properties": {
46
+ "safe": {
47
+ "type": "array",
48
+ "items": {
49
+ "$ref": "#/$defs/cleanupItem"
50
+ }
51
+ },
52
+ "review": {
53
+ "type": "array",
54
+ "items": {
55
+ "$ref": "#/$defs/cleanupItem"
56
+ }
57
+ },
58
+ "blocked": {
59
+ "type": "array",
60
+ "items": {
61
+ "$ref": "#/$defs/cleanupItem"
62
+ }
63
+ }
64
+ }
65
+ },
66
+ "message": {
67
+ "type": "string"
68
+ }
69
+ },
70
+ "$defs": {
71
+ "cleanupItem": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "required": [
75
+ "branch",
76
+ "worktree_path",
77
+ "head",
78
+ "status",
79
+ "reason",
80
+ "detached",
81
+ "selectable"
82
+ ],
83
+ "properties": {
84
+ "branch": {
85
+ "type": ["string", "null"]
86
+ },
87
+ "worktree_path": {
88
+ "type": ["string", "null"]
89
+ },
90
+ "head": {
91
+ "type": ["string", "null"]
92
+ },
93
+ "status": {
94
+ "type": ["string", "null"],
95
+ "enum": ["safe", "review", "blocked", null]
96
+ },
97
+ "reason": {
98
+ "type": ["string", "null"]
99
+ },
100
+ "detached": {
101
+ "type": "boolean"
102
+ },
103
+ "selectable": {
104
+ "type": ["boolean", "null"]
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/sven1103-agent/opencode-worktree-plugin/schemas/worktree-prepare.result.schema.json",
4
+ "title": "worktree_prepare result",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "ok",
10
+ "title",
11
+ "branch",
12
+ "worktree_path",
13
+ "default_branch",
14
+ "base_branch",
15
+ "base_ref",
16
+ "base_commit",
17
+ "created"
18
+ ],
19
+ "properties": {
20
+ "schema_version": {
21
+ "type": "string",
22
+ "const": "1.0.0"
23
+ },
24
+ "ok": {
25
+ "type": "boolean",
26
+ "const": true
27
+ },
28
+ "title": {
29
+ "type": "string",
30
+ "minLength": 1
31
+ },
32
+ "branch": {
33
+ "type": "string",
34
+ "minLength": 1
35
+ },
36
+ "worktree_path": {
37
+ "type": "string",
38
+ "minLength": 1
39
+ },
40
+ "default_branch": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ },
44
+ "base_branch": {
45
+ "type": "string",
46
+ "minLength": 1
47
+ },
48
+ "base_ref": {
49
+ "type": "string",
50
+ "minLength": 1
51
+ },
52
+ "base_commit": {
53
+ "type": "string",
54
+ "minLength": 1
55
+ },
56
+ "created": {
57
+ "type": "boolean",
58
+ "const": true
59
+ },
60
+ "message": {
61
+ "type": "string"
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: worktree-workflow
3
+ description: Use this skill when you need to decide whether a task should move into a git worktree, when repo root is still safe, or when you need to choose between native worktree tools and the standard CLI fallback.
4
+ ---
5
+
6
+ ## When to use me
7
+
8
+ - Use this skill when work should be isolated from the current checkout.
9
+ - Use this skill when the task is substantial, risky, or likely to involve multiple collaborating agents.
10
+ - Use this skill when you need to decide whether repo root is still safe for a tiny edit.
11
+ - Use this skill when you need to choose the native worktree tools first and the packaged CLI fallback second.
12
+
13
+ ## Goals
14
+
15
+ - Keep user work isolated from repo root when appropriate.
16
+ - Prefer task-scoped worktrees for non-trivial editable work.
17
+ - Support both native worktree tools and CLI fallback environments.
18
+ - Keep policy in the skill and execution semantics in the package.
19
+
20
+ ## Root policy
21
+
22
+ - Treat repo root as shared space.
23
+ - Use repo root only for tiny, root-safe tasks.
24
+ - Treat a task as root-safe only when it is one focused change, touches at most one or two closely related files, does not need parallel delegation, does not imply a likely edit-test-fix loop, does not involve risky refactoring or migration work, and does not risk interfering with unrelated dirty root state.
25
+
26
+ ## Task worktree policy
27
+
28
+ - Prefer one task-scoped worktree for non-trivial editable work.
29
+ - Treat a task worktree as belonging to a task or workstream, not to a single agent.
30
+ - Keep planning, implementation, and review for one linear task in the same task worktree unless the work splits.
31
+ - Create a separate divergent worktree only when concurrent branches of work may conflict or need independent experimentation.
32
+
33
+ ## Capability ladder
34
+
35
+ - Use the native worktree tools as the primary path when the native worktree tools are available.
36
+ - Use the packaged CLI fallback path when the native tools are unavailable.
37
+ - Continue in repo root only for tiny, root-safe tasks when no worktree capability is available; otherwise stop and explain that isolation capability is unavailable.
38
+
39
+ ## Creation behavior
40
+
41
+ - Use a short descriptive task title when creating a worktree.
42
+ - Treat the returned `worktree_path` as the active execution target for follow-up work.
43
+ - Use that worktree path as the working directory for later shell commands.
44
+ - Use paths inside that worktree for later file reads or edits.
45
+
46
+ ## Cleanup behavior
47
+
48
+ - Keep cleanup preview-first by default because cleanup is preview-first unless deletion is clearly intended.
49
+ - Use cleanup apply only when deletion is clearly intended and controlled by the orchestrating runtime.
50
+ - Treat slash commands as manual human entry points, not as the canonical agent interface.
51
+
52
+ ## Boundaries
53
+
54
+ - Do not encode runtime storage, session artifact, or orchestration file-layout details here.
55
+ - Do not duplicate package argument normalization or config parsing here.
56
+ - Rely on the shared package implementation for config loading, base-branch resolution, cleanup normalization, and structured result semantics.
57
+
58
+ ## Examples
59
+
60
+ - Move a risky refactor into a task-scoped worktree before editing multiple files and running several test-fix loops.
61
+ - Stay in repo root for a tiny, root-safe doc fix that touches one related file and does not need delegation.
62
+ - Prefer the native worktree tools first, then switch to the packaged CLI fallback path if the native tools are unavailable.
package/src/cli.js ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+
8
+ import { WorktreeWorkflowPlugin } from "./index.js";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ function shellEscape(value) {
13
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
14
+ }
15
+
16
+ function createShell(cwdBase) {
17
+ const shell = (strings, ...values) => {
18
+ const [firstValue] = values;
19
+ const raw = firstValue?.raw || strings.raw?.join("") || strings.join("");
20
+ let cwd = cwdBase;
21
+
22
+ return {
23
+ cwd(nextCwd) {
24
+ cwd = nextCwd;
25
+ return this;
26
+ },
27
+ quiet() {
28
+ return this;
29
+ },
30
+ async nothrow() {
31
+ try {
32
+ const result = await execFileAsync("sh", ["-lc", raw], { cwd });
33
+ return {
34
+ text() {
35
+ return result.stdout;
36
+ },
37
+ stderr: Buffer.from(result.stderr),
38
+ exitCode: 0,
39
+ };
40
+ } catch (error) {
41
+ return {
42
+ text() {
43
+ return error.stdout || "";
44
+ },
45
+ stderr: Buffer.from(error.stderr || ""),
46
+ exitCode: typeof error.code === "number" ? error.code : 1,
47
+ };
48
+ }
49
+ },
50
+ };
51
+ };
52
+
53
+ shell.escape = shellEscape;
54
+ return shell;
55
+ }
56
+
57
+ function printUsage() {
58
+ process.stdout.write(
59
+ [
60
+ "Usage:",
61
+ " opencode-worktree-workflow wt-new <title> [--json]",
62
+ " opencode-worktree-workflow wt-clean [preview|apply] [selectors...] [--json]",
63
+ "",
64
+ "Examples:",
65
+ " opencode-worktree-workflow wt-new \"Improve checkout retry logic\"",
66
+ " opencode-worktree-workflow wt-clean preview",
67
+ " opencode-worktree-workflow wt-clean apply feature/foo",
68
+ ].join("\n") + "\n",
69
+ );
70
+ }
71
+
72
+ function printSubcommandUsage(command) {
73
+ if (command === "wt-new") {
74
+ process.stdout.write(
75
+ [
76
+ "Usage:",
77
+ " opencode-worktree-workflow wt-new <title> [--json]",
78
+ "",
79
+ "Create a synced worktree and branch from the configured base branch.",
80
+ ].join("\n") + "\n",
81
+ );
82
+ return;
83
+ }
84
+
85
+ if (command === "wt-clean") {
86
+ process.stdout.write(
87
+ [
88
+ "Usage:",
89
+ " opencode-worktree-workflow wt-clean [preview|apply] [selectors...] [--json]",
90
+ "",
91
+ "Preview connected worktrees or remove safe and explicitly selected review worktrees.",
92
+ ].join("\n") + "\n",
93
+ );
94
+ }
95
+ }
96
+
97
+ export function parseCliArgs(argv) {
98
+ const outputJson = argv.includes("--json");
99
+ const args = argv.filter((arg) => arg !== "--json");
100
+ return { outputJson, args };
101
+ }
102
+
103
+ export async function run(argv = process.argv.slice(2)) {
104
+ const { outputJson, args } = parseCliArgs(argv);
105
+ const [command, ...rest] = args;
106
+
107
+ if (!command || command === "--help" || command === "-h" || command === "help") {
108
+ printUsage();
109
+ process.exitCode = command ? 0 : 1;
110
+ return;
111
+ }
112
+
113
+ if ((command === "wt-new" || command === "wt-clean") && rest.some((arg) => arg === "--help" || arg === "-h" || arg === "help")) {
114
+ printSubcommandUsage(command);
115
+ return;
116
+ }
117
+
118
+ const plugin = await WorktreeWorkflowPlugin({
119
+ $: createShell(process.cwd()),
120
+ directory: process.cwd(),
121
+ });
122
+
123
+ let result;
124
+
125
+ if (command === "wt-new") {
126
+ const title = rest.join(" ").trim();
127
+
128
+ if (!title) {
129
+ throw new Error("wt-new requires a descriptive title.");
130
+ }
131
+
132
+ result = await plugin.tool.worktree_prepare.execute(
133
+ { title },
134
+ { metadata() {}, worktree: process.cwd() },
135
+ );
136
+ } else if (command === "wt-clean") {
137
+ const raw = rest.join(" ").trim();
138
+ result = await plugin.tool.worktree_cleanup.execute(
139
+ { raw, selectors: [] },
140
+ { metadata() {}, worktree: process.cwd() },
141
+ );
142
+ } else {
143
+ throw new Error(`Unknown command: ${command}`);
144
+ }
145
+
146
+ if (outputJson) {
147
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
148
+ return;
149
+ }
150
+
151
+ process.stdout.write(`${result.message || JSON.stringify(result, null, 2)}\n`);
152
+ }
153
+
154
+ export function isInvokedAsScript(argvPath = process.argv[1]) {
155
+ if (!argvPath) {
156
+ return false;
157
+ }
158
+
159
+ try {
160
+ return fs.realpathSync(argvPath) === fileURLToPath(import.meta.url);
161
+ } catch {
162
+ return fileURLToPath(import.meta.url) === argvPath;
163
+ }
164
+ }
165
+
166
+ const invokedAsScript = isInvokedAsScript();
167
+
168
+ if (invokedAsScript) {
169
+ run().catch((error) => {
170
+ process.stderr.write(`${error.message || String(error)}\n`);
171
+ process.exitCode = 1;
172
+ });
173
+ }
package/src/index.js CHANGED
@@ -13,6 +13,8 @@ const DEFAULTS = {
13
13
  protectedBranches: [],
14
14
  };
15
15
 
16
+ const RESULT_SCHEMA_VERSION = "1.0.0";
17
+
16
18
  async function pathExists(targetPath) {
17
19
  try {
18
20
  await fs.access(targetPath);
@@ -22,6 +24,14 @@ async function pathExists(targetPath) {
22
24
  }
23
25
  }
24
26
 
27
+ function isMissingGitRepositoryError(message) {
28
+ return /not a git repository/i.test(message);
29
+ }
30
+
31
+ function isMissingRemoteError(message, remote) {
32
+ return new RegExp(`No such remote:?\s+${remote}|does not appear to be a git repository|Could not read from remote repository`, "i").test(message);
33
+ }
34
+
25
35
  async function readJsonFile(filePath) {
26
36
  if (!(await pathExists(filePath))) {
27
37
  return null;
@@ -163,6 +173,18 @@ function formatPreview(grouped, defaultBranch) {
163
173
  ].join("\n");
164
174
  }
165
175
 
176
+ function formatPrepareSummary(result) {
177
+ return [
178
+ `Created worktree for "${result.title}".`,
179
+ `- branch: ${result.branch}`,
180
+ `- worktree: ${result.worktree_path}`,
181
+ `- default branch: ${result.default_branch}`,
182
+ `- base branch: ${result.base_branch}`,
183
+ `- base ref: ${result.base_ref}`,
184
+ `- base commit: ${result.base_commit}`,
185
+ ].join("\n");
186
+ }
187
+
166
188
  function formatCleanupSummary(defaultBranch, removed, failed, requestedSelectors) {
167
189
  const lines = [`Cleaned worktrees relative to ${defaultBranch}:`];
168
190
 
@@ -194,6 +216,88 @@ function formatCleanupSummary(defaultBranch, removed, failed, requestedSelectors
194
216
  return lines.join("\n");
195
217
  }
196
218
 
219
+ function toStructuredCleanupItem(item) {
220
+ return {
221
+ branch: item.branch ?? null,
222
+ worktree_path: item.path ?? item.worktree_path ?? null,
223
+ head: item.head ?? null,
224
+ status: item.status ?? null,
225
+ reason: item.reason ?? null,
226
+ detached: Boolean(item.detached),
227
+ selectable: typeof item.selectable === "boolean" ? item.selectable : null,
228
+ };
229
+ }
230
+
231
+ function toStructuredCleanupFailure(item) {
232
+ return {
233
+ selector: item.selector ?? null,
234
+ branch: item.branch ?? null,
235
+ worktree_path: item.path ?? item.worktree_path ?? null,
236
+ head: item.head ?? null,
237
+ status: item.status ?? null,
238
+ reason: item.reason ?? null,
239
+ detached: Boolean(item.detached),
240
+ selectable: typeof item.selectable === "boolean" ? item.selectable : null,
241
+ };
242
+ }
243
+
244
+ function buildPrepareResult({ title, branch, worktreePath, defaultBranch, baseBranch, baseRef, baseCommit }) {
245
+ const result = {
246
+ schema_version: RESULT_SCHEMA_VERSION,
247
+ ok: true,
248
+ title,
249
+ branch,
250
+ worktree_path: worktreePath,
251
+ default_branch: defaultBranch,
252
+ base_branch: baseBranch,
253
+ base_ref: baseRef,
254
+ base_commit: baseCommit,
255
+ created: true,
256
+ };
257
+
258
+ return {
259
+ ...result,
260
+ message: formatPrepareSummary(result),
261
+ };
262
+ }
263
+
264
+ function buildCleanupPreviewResult({ defaultBranch, baseBranch, baseRef, grouped }) {
265
+ const structuredGroups = {
266
+ safe: grouped.safe.map(toStructuredCleanupItem),
267
+ review: grouped.review.map(toStructuredCleanupItem),
268
+ blocked: grouped.blocked.map(toStructuredCleanupItem),
269
+ };
270
+
271
+ return {
272
+ schema_version: RESULT_SCHEMA_VERSION,
273
+ ok: true,
274
+ mode: "preview",
275
+ default_branch: defaultBranch,
276
+ base_branch: baseBranch,
277
+ base_ref: baseRef,
278
+ groups: structuredGroups,
279
+ message: formatPreview(grouped, baseBranch),
280
+ };
281
+ }
282
+
283
+ function buildCleanupApplyResult({ defaultBranch, baseBranch, baseRef, removed, failed, requestedSelectors }) {
284
+ return {
285
+ schema_version: RESULT_SCHEMA_VERSION,
286
+ ok: true,
287
+ mode: "apply",
288
+ default_branch: defaultBranch,
289
+ base_branch: baseBranch,
290
+ base_ref: baseRef,
291
+ requested_selectors: requestedSelectors,
292
+ removed: removed.map((item) => ({
293
+ ...toStructuredCleanupItem(item),
294
+ selected: Boolean(item.selected),
295
+ })),
296
+ failed: failed.map(toStructuredCleanupFailure),
297
+ message: formatCleanupSummary(baseBranch, removed, failed, requestedSelectors),
298
+ };
299
+ }
300
+
197
301
  function splitCleanupToken(value) {
198
302
  if (typeof value !== "string") {
199
303
  return [];
@@ -275,8 +379,17 @@ function normalizeCleanupArgs(args, config) {
275
379
  }
276
380
 
277
381
  export const __internal = {
382
+ RESULT_SCHEMA_VERSION,
383
+ buildCleanupApplyResult,
384
+ buildCleanupPreviewResult,
385
+ buildPrepareResult,
386
+ classifyEntry,
387
+ isMissingGitRepositoryError,
388
+ isMissingRemoteError,
278
389
  parseCleanupRawArguments,
279
390
  normalizeCleanupArgs,
391
+ toStructuredCleanupFailure,
392
+ toStructuredCleanupItem,
280
393
  };
281
394
 
282
395
  function selectorMatches(item, selector) {
@@ -367,8 +480,18 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
367
480
  }
368
481
 
369
482
  async function getRepoRoot() {
370
- const result = await git(["rev-parse", "--show-toplevel"]);
371
- return result.stdout;
483
+ try {
484
+ const result = await git(["rev-parse", "--show-toplevel"]);
485
+ return result.stdout;
486
+ } catch (error) {
487
+ if (isMissingGitRepositoryError(error.message || "")) {
488
+ throw new Error(
489
+ "This command must run inside a git repository. Initialize a repository first or run it from an existing repo root.",
490
+ );
491
+ }
492
+
493
+ throw error;
494
+ }
372
495
  }
373
496
 
374
497
  async function loadWorkflowConfig(repoRoot) {
@@ -454,7 +577,17 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
454
577
  }
455
578
 
456
579
  async function getBaseRef(repoRoot, remote, baseBranch) {
457
- await git(["fetch", "--prune", remote, baseBranch], { cwd: repoRoot });
580
+ try {
581
+ await git(["fetch", "--prune", remote, baseBranch], { cwd: repoRoot });
582
+ } catch (error) {
583
+ if (isMissingRemoteError(error.message || "", remote)) {
584
+ throw new Error(
585
+ `Could not fetch base branch information from remote \"${remote}\". Configure the expected remote in .opencode/worktree-workflow.json or add that remote to this repository.`,
586
+ );
587
+ }
588
+
589
+ throw error;
590
+ }
458
591
 
459
592
  const remoteRef = `refs/remotes/${remote}/${baseBranch}`;
460
593
  const remoteExists = await git(["show-ref", "--verify", "--quiet", remoteRef], {
@@ -529,15 +662,15 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
529
662
  );
530
663
  }
531
664
 
532
- return [
533
- `Created worktree for \"${args.title}\".`,
534
- `- branch: ${branchName}`,
535
- `- worktree: ${worktreePath}`,
536
- `- default branch: ${defaultBranch}`,
537
- `- base branch: ${baseBranch}`,
538
- `- base ref: ${baseRef}`,
539
- `- base commit: ${baseCommit}`,
540
- ].join("\n");
665
+ return buildPrepareResult({
666
+ title: args.title,
667
+ branch: branchName,
668
+ worktreePath,
669
+ defaultBranch,
670
+ baseBranch,
671
+ baseRef,
672
+ baseCommit,
673
+ });
541
674
  },
542
675
  }),
543
676
  worktree_cleanup: tool({
@@ -593,7 +726,12 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
593
726
  }
594
727
 
595
728
  if (normalizedArgs.mode !== "apply") {
596
- return formatPreview(grouped, baseBranch);
729
+ return buildCleanupPreviewResult({
730
+ defaultBranch,
731
+ baseBranch,
732
+ baseRef,
733
+ grouped,
734
+ });
597
735
  }
598
736
 
599
737
  const requestedSelectors = [...new Set(normalizedArgs.selectors || [])];
@@ -622,7 +760,10 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
622
760
  continue;
623
761
  }
624
762
 
625
- selected.push(match);
763
+ selected.push({
764
+ ...match,
765
+ selector,
766
+ });
626
767
  }
627
768
 
628
769
  const targets = [...grouped.safe];
@@ -631,6 +772,7 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
631
772
  if (!targets.some((target) => target.path === item.path)) {
632
773
  targets.push({
633
774
  ...item,
775
+ selector: item.selector ?? null,
634
776
  selected: true,
635
777
  });
636
778
  }
@@ -676,7 +818,14 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
676
818
  allowFailure: true,
677
819
  });
678
820
 
679
- return formatCleanupSummary(baseBranch, removed, failed, requestedSelectors);
821
+ return buildCleanupApplyResult({
822
+ defaultBranch,
823
+ baseBranch,
824
+ baseRef,
825
+ removed,
826
+ failed,
827
+ requestedSelectors,
828
+ });
680
829
  },
681
830
  }),
682
831
  },