@sven1103/opencode-worktree-workflow 0.6.0 → 0.6.2

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
@@ -42,6 +42,11 @@ In practice:
42
42
  - if the native tools are unavailable, use the local CLI fallback from the same installed package
43
43
  - if the package is not installed, no CLI fallback is available
44
44
 
45
+ Important distinction:
46
+
47
+ - `worktree_prepare` and `worktree_cleanup` are native OpenCode tools, not shell commands
48
+ - from a terminal, use `npx opencode-worktree-workflow ...` or `./node_modules/.bin/opencode-worktree-workflow ...`
49
+
45
50
  ## Install in an OpenCode project
46
51
 
47
52
  Add the package as a project dependency, following the official docs style:
@@ -145,9 +150,11 @@ If your setup uses installed skill files, copy the released `SKILL.md` into a `w
145
150
 
146
151
  This package now ships the plugin capability, a CLI fallback surface, thin slash commands, and a co-shipped policy skill.
147
152
 
153
+ These native tools are exposed inside OpenCode after the plugin is loaded. They are not terminal commands.
154
+
148
155
  ## Structured contract
149
156
 
150
- The native tool results and CLI `--json` output now use a versioned structured contract with a `schema_version` field.
157
+ The package now exposes a versioned structured contract with a `schema_version` field. Native tools return human-readable text and publish the structured result in tool metadata, while CLI `--json` prints the same structured object directly.
151
158
 
152
159
  - current `schema_version`: `1.0.0`
153
160
  - contract overview: `docs/contract.md`
@@ -163,21 +170,33 @@ Human-readable output remains available through the result `message`, but caller
163
170
 
164
171
  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
172
 
173
+ Use the CLI from a terminal when you want to run the workflow manually. Run it inside a real git repository. By default, the workflow expects a normal remote and base-branch setup such as `origin` plus the repository default branch, unless you override that in `.opencode/worktree-workflow.json`.
174
+
166
175
  Examples:
167
176
 
168
177
  ```sh
178
+ npx opencode-worktree-workflow --help
179
+ npx opencode-worktree-workflow wt-clean --help
169
180
  npx opencode-worktree-workflow wt-new "Improve checkout retry logic"
170
181
  npx opencode-worktree-workflow wt-new "Improve checkout retry logic" --json
171
182
  npx opencode-worktree-workflow wt-clean preview
172
183
  npx opencode-worktree-workflow wt-clean apply feature/foo --json
173
184
  ```
174
185
 
186
+ Direct local bin examples:
187
+
188
+ ```sh
189
+ ./node_modules/.bin/opencode-worktree-workflow --help
190
+ ./node_modules/.bin/opencode-worktree-workflow wt-clean preview
191
+ ```
192
+
175
193
  Defaults:
176
194
 
177
195
  - human-readable output by default
178
196
  - structured output with `--json`
179
197
  - the CLI shares the same underlying implementation and result contract as the native tools
180
198
  - the CLI fallback depends on the package already being installed in the project
199
+ - if you run it outside a git repo or without the expected remote context, the CLI returns an actionable error
181
200
 
182
201
  ## Compatibility model
183
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sven1103/opencode-worktree-workflow",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "OpenCode plugin for creating and cleaning up git worktrees.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execFile } from "node:child_process";
4
+ import fs from "node:fs";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { promisify } from "node:util";
6
7
 
@@ -68,6 +69,31 @@ function printUsage() {
68
69
  );
69
70
  }
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
+
71
97
  export function parseCliArgs(argv) {
72
98
  const outputJson = argv.includes("--json");
73
99
  const args = argv.filter((arg) => arg !== "--json");
@@ -84,12 +110,26 @@ export async function run(argv = process.argv.slice(2)) {
84
110
  return;
85
111
  }
86
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
+
87
118
  const plugin = await WorktreeWorkflowPlugin({
88
119
  $: createShell(process.cwd()),
89
120
  directory: process.cwd(),
90
121
  });
91
122
 
92
123
  let result;
124
+ let structuredResult = null;
125
+ const toolContext = {
126
+ metadata(input) {
127
+ if (input?.metadata?.result) {
128
+ structuredResult = input.metadata.result;
129
+ }
130
+ },
131
+ worktree: process.cwd(),
132
+ };
93
133
 
94
134
  if (command === "wt-new") {
95
135
  const title = rest.join(" ").trim();
@@ -98,29 +138,35 @@ export async function run(argv = process.argv.slice(2)) {
98
138
  throw new Error("wt-new requires a descriptive title.");
99
139
  }
100
140
 
101
- result = await plugin.tool.worktree_prepare.execute(
102
- { title },
103
- { metadata() {}, worktree: process.cwd() },
104
- );
141
+ result = await plugin.tool.worktree_prepare.execute({ title }, toolContext);
105
142
  } else if (command === "wt-clean") {
106
143
  const raw = rest.join(" ").trim();
107
- result = await plugin.tool.worktree_cleanup.execute(
108
- { raw, selectors: [] },
109
- { metadata() {}, worktree: process.cwd() },
110
- );
144
+ result = await plugin.tool.worktree_cleanup.execute({ raw, selectors: [] }, toolContext);
111
145
  } else {
112
146
  throw new Error(`Unknown command: ${command}`);
113
147
  }
114
148
 
115
149
  if (outputJson) {
116
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
150
+ process.stdout.write(`${JSON.stringify(structuredResult ?? { ok: true, message: result }, null, 2)}\n`);
117
151
  return;
118
152
  }
119
153
 
120
- process.stdout.write(`${result.message || JSON.stringify(result, null, 2)}\n`);
154
+ process.stdout.write(`${result}\n`);
155
+ }
156
+
157
+ export function isInvokedAsScript(argvPath = process.argv[1]) {
158
+ if (!argvPath) {
159
+ return false;
160
+ }
161
+
162
+ try {
163
+ return fs.realpathSync(argvPath) === fileURLToPath(import.meta.url);
164
+ } catch {
165
+ return fileURLToPath(import.meta.url) === argvPath;
166
+ }
121
167
  }
122
168
 
123
- const invokedAsScript = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
169
+ const invokedAsScript = isInvokedAsScript();
124
170
 
125
171
  if (invokedAsScript) {
126
172
  run().catch((error) => {
package/src/index.js CHANGED
@@ -24,6 +24,14 @@ async function pathExists(targetPath) {
24
24
  }
25
25
  }
26
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
+
27
35
  async function readJsonFile(filePath) {
28
36
  if (!(await pathExists(filePath))) {
29
37
  return null;
@@ -253,6 +261,16 @@ function buildPrepareResult({ title, branch, worktreePath, defaultBranch, baseBr
253
261
  };
254
262
  }
255
263
 
264
+ function publishStructuredResult(context, result) {
265
+ context.metadata({
266
+ metadata: {
267
+ result,
268
+ },
269
+ });
270
+
271
+ return result.message || JSON.stringify(result, null, 2);
272
+ }
273
+
256
274
  function buildCleanupPreviewResult({ defaultBranch, baseBranch, baseRef, grouped }) {
257
275
  const structuredGroups = {
258
276
  safe: grouped.safe.map(toStructuredCleanupItem),
@@ -376,6 +394,8 @@ export const __internal = {
376
394
  buildCleanupPreviewResult,
377
395
  buildPrepareResult,
378
396
  classifyEntry,
397
+ isMissingGitRepositoryError,
398
+ isMissingRemoteError,
379
399
  parseCleanupRawArguments,
380
400
  normalizeCleanupArgs,
381
401
  toStructuredCleanupFailure,
@@ -470,8 +490,18 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
470
490
  }
471
491
 
472
492
  async function getRepoRoot() {
473
- const result = await git(["rev-parse", "--show-toplevel"]);
474
- return result.stdout;
493
+ try {
494
+ const result = await git(["rev-parse", "--show-toplevel"]);
495
+ return result.stdout;
496
+ } catch (error) {
497
+ if (isMissingGitRepositoryError(error.message || "")) {
498
+ throw new Error(
499
+ "This command must run inside a git repository. Initialize a repository first or run it from an existing repo root.",
500
+ );
501
+ }
502
+
503
+ throw error;
504
+ }
475
505
  }
476
506
 
477
507
  async function loadWorkflowConfig(repoRoot) {
@@ -557,7 +587,17 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
557
587
  }
558
588
 
559
589
  async function getBaseRef(repoRoot, remote, baseBranch) {
560
- await git(["fetch", "--prune", remote, baseBranch], { cwd: repoRoot });
590
+ try {
591
+ await git(["fetch", "--prune", remote, baseBranch], { cwd: repoRoot });
592
+ } catch (error) {
593
+ if (isMissingRemoteError(error.message || "", remote)) {
594
+ throw new Error(
595
+ `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.`,
596
+ );
597
+ }
598
+
599
+ throw error;
600
+ }
561
601
 
562
602
  const remoteRef = `refs/remotes/${remote}/${baseBranch}`;
563
603
  const remoteExists = await git(["show-ref", "--verify", "--quiet", remoteRef], {
@@ -632,15 +672,18 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
632
672
  );
633
673
  }
634
674
 
635
- return buildPrepareResult({
636
- title: args.title,
637
- branch: branchName,
638
- worktreePath,
639
- defaultBranch,
640
- baseBranch,
641
- baseRef,
642
- baseCommit,
643
- });
675
+ return publishStructuredResult(
676
+ context,
677
+ buildPrepareResult({
678
+ title: args.title,
679
+ branch: branchName,
680
+ worktreePath,
681
+ defaultBranch,
682
+ baseBranch,
683
+ baseRef,
684
+ baseCommit,
685
+ }),
686
+ );
644
687
  },
645
688
  }),
646
689
  worktree_cleanup: tool({
@@ -696,12 +739,15 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
696
739
  }
697
740
 
698
741
  if (normalizedArgs.mode !== "apply") {
699
- return buildCleanupPreviewResult({
700
- defaultBranch,
701
- baseBranch,
702
- baseRef,
703
- grouped,
704
- });
742
+ return publishStructuredResult(
743
+ context,
744
+ buildCleanupPreviewResult({
745
+ defaultBranch,
746
+ baseBranch,
747
+ baseRef,
748
+ grouped,
749
+ }),
750
+ );
705
751
  }
706
752
 
707
753
  const requestedSelectors = [...new Set(normalizedArgs.selectors || [])];
@@ -788,14 +834,17 @@ export const WorktreeWorkflowPlugin = async ({ $, directory }) => {
788
834
  allowFailure: true,
789
835
  });
790
836
 
791
- return buildCleanupApplyResult({
792
- defaultBranch,
793
- baseBranch,
794
- baseRef,
795
- removed,
796
- failed,
797
- requestedSelectors,
798
- });
837
+ return publishStructuredResult(
838
+ context,
839
+ buildCleanupApplyResult({
840
+ defaultBranch,
841
+ baseBranch,
842
+ baseRef,
843
+ removed,
844
+ failed,
845
+ requestedSelectors,
846
+ }),
847
+ );
799
848
  },
800
849
  }),
801
850
  },