@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 +20 -1
- package/package.json +1 -1
- package/src/cli.js +57 -11
- package/src/index.js +75 -26
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
|
|
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
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
|
|
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 =
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
},
|