workon 3.7.1 → 3.8.0
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 +13 -2
- package/dist/cli.js +136 -0
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -219,6 +219,9 @@ workon worktree status
|
|
|
219
219
|
# Merge this worktree's branch into main/master
|
|
220
220
|
workon worktree merge
|
|
221
221
|
|
|
222
|
+
# Recycle worktree for the next task (ff to latest main)
|
|
223
|
+
workon worktree recycle
|
|
224
|
+
|
|
222
225
|
# Remove this worktree (shows instructions to exit first)
|
|
223
226
|
workon worktree remove
|
|
224
227
|
```
|
|
@@ -359,6 +362,8 @@ workon myproject::feat-x # all events in worktree
|
|
|
359
362
|
| `worktree merge` | `-i, --into <branch>` | Sets target branch explicitly |
|
|
360
363
|
| `worktree merge` | `-s, --squash` | Uses squash merge (default: regular merge) |
|
|
361
364
|
| `worktree merge` | `--delete-branch` | Deletes merged branch (avoids prompt) |
|
|
365
|
+
| `worktree recycle` | `[branch]` | Remote branch to fast-forward to (auto-detects main/master/develop/dev) |
|
|
366
|
+
| `worktree recycle` | `-y, --yes` | Skips confirmation prompt |
|
|
362
367
|
| `worktree remove` | `-y, --yes` | Skips confirmation prompt |
|
|
363
368
|
| `worktree remove` | `-f, --force` | Force-removes with uncommitted changes |
|
|
364
369
|
|
|
@@ -386,8 +391,11 @@ workon worktrees add my-feature
|
|
|
386
391
|
|
|
387
392
|
# 4. Push, create a PR, get it reviewed and merged
|
|
388
393
|
|
|
389
|
-
#
|
|
394
|
+
# 5a. Tear down the worktree
|
|
390
395
|
workon worktrees remove my-feature
|
|
396
|
+
|
|
397
|
+
# 5b. Or recycle for the next task (ff to latest main, keep worktree)
|
|
398
|
+
workon worktree recycle
|
|
391
399
|
```
|
|
392
400
|
|
|
393
401
|
**Automated workflow (non-interactive, agent-friendly):**
|
|
@@ -407,8 +415,11 @@ workon worktrees add my-feature -b main -y -o
|
|
|
407
415
|
workon worktrees branch my-feature pr-branch -y -p
|
|
408
416
|
# ... PR merged via gh/API ...
|
|
409
417
|
|
|
410
|
-
#
|
|
418
|
+
# 5a. Tear down: remove worktree and delete the branch
|
|
411
419
|
workon worktrees remove my-feature -y
|
|
420
|
+
|
|
421
|
+
# 5b. Or recycle: ff to latest main and reuse for the next task
|
|
422
|
+
workon worktree recycle -y
|
|
412
423
|
```
|
|
413
424
|
|
|
414
425
|
The key difference is that every step in the automated version completes without waiting for user input, making it safe to chain together in scripts or have an AI agent drive the entire flow.
|
package/dist/cli.js
CHANGED
|
@@ -4899,6 +4899,20 @@ function createWorktreeCommand(ctx) {
|
|
|
4899
4899
|
}
|
|
4900
4900
|
await removeCurrentWorktree(worktreeInfo, options, ctx);
|
|
4901
4901
|
});
|
|
4902
|
+
command.command("recycle").description(
|
|
4903
|
+
"Recycle this worktree for the next task: switch to its original branch and fast-forward to a remote branch"
|
|
4904
|
+
).argument("[branch]", "Remote branch to fast-forward to (auto-detects default)").option("-y, --yes", "Skip confirmation prompt").action(async (branch, options) => {
|
|
4905
|
+
const worktreeInfo = await detectWorktreeContext();
|
|
4906
|
+
if (!worktreeInfo) {
|
|
4907
|
+
log.error("Not in a git repository.");
|
|
4908
|
+
process.exit(1);
|
|
4909
|
+
}
|
|
4910
|
+
if (!worktreeInfo.isWorktree) {
|
|
4911
|
+
log.error("You're in the main repository, not a worktree.");
|
|
4912
|
+
process.exit(1);
|
|
4913
|
+
}
|
|
4914
|
+
await recycleCurrentWorktree(worktreeInfo, branch, options, ctx);
|
|
4915
|
+
});
|
|
4902
4916
|
return command;
|
|
4903
4917
|
}
|
|
4904
4918
|
async function showWorktreeStatus(worktreeInfo, log) {
|
|
@@ -4939,6 +4953,7 @@ ${chalk8.bold("Changes:")}`);
|
|
|
4939
4953
|
console.log(`
|
|
4940
4954
|
${chalk8.bold("Commands:")}`);
|
|
4941
4955
|
console.log(` workon worktree merge - Merge this branch and remove worktree`);
|
|
4956
|
+
console.log(` workon worktree recycle - Reset worktree for the next task`);
|
|
4942
4957
|
console.log(` workon worktree remove - Remove this worktree`);
|
|
4943
4958
|
console.log();
|
|
4944
4959
|
}
|
|
@@ -5121,6 +5136,127 @@ To remove this worktree:`);
|
|
|
5121
5136
|
)
|
|
5122
5137
|
);
|
|
5123
5138
|
}
|
|
5139
|
+
async function getWorktreeOriginalBranch(mainRepoPath, worktreeName) {
|
|
5140
|
+
try {
|
|
5141
|
+
const manager = new WorktreeManager(mainRepoPath);
|
|
5142
|
+
const worktree = await manager.get(worktreeName);
|
|
5143
|
+
if (worktree && worktree.branch && worktree.branch !== "(detached)") {
|
|
5144
|
+
return worktree.branch;
|
|
5145
|
+
}
|
|
5146
|
+
const git = simpleGit5(mainRepoPath);
|
|
5147
|
+
const branches = await git.branchLocal();
|
|
5148
|
+
for (const branchName of branches.all) {
|
|
5149
|
+
if (branchName.replace(/\//g, "-") === worktreeName) {
|
|
5150
|
+
return branchName;
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
return null;
|
|
5154
|
+
} catch {
|
|
5155
|
+
return null;
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
async function detectDefaultRemoteBranch(git) {
|
|
5159
|
+
const candidates = ["main", "master", "develop", "dev"];
|
|
5160
|
+
try {
|
|
5161
|
+
const remoteRefs = await git.raw(["ls-remote", "--heads", "origin"]);
|
|
5162
|
+
const refNames = new Set(
|
|
5163
|
+
remoteRefs.trim().split("\n").filter((line) => line.includes("refs/heads/")).map((line) => line.replace(/.*refs\/heads\//, ""))
|
|
5164
|
+
);
|
|
5165
|
+
for (const candidate of candidates) {
|
|
5166
|
+
if (refNames.has(candidate)) {
|
|
5167
|
+
return candidate;
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
return null;
|
|
5171
|
+
} catch {
|
|
5172
|
+
return null;
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
async function recycleCurrentWorktree(worktreeInfo, targetBranch, options, ctx) {
|
|
5176
|
+
const { log } = ctx;
|
|
5177
|
+
if (!worktreeInfo.worktreePath || !worktreeInfo.worktreeName) {
|
|
5178
|
+
log.error("Unable to determine worktree info.");
|
|
5179
|
+
process.exit(1);
|
|
5180
|
+
}
|
|
5181
|
+
if (worktreeInfo.branch === "(detached)") {
|
|
5182
|
+
log.error("Worktree is in detached HEAD state.");
|
|
5183
|
+
log.info("Use 'git checkout <branch>' to switch to a branch first.");
|
|
5184
|
+
process.exit(1);
|
|
5185
|
+
}
|
|
5186
|
+
const git = simpleGit5(worktreeInfo.worktreePath);
|
|
5187
|
+
const originalBranch = await getWorktreeOriginalBranch(
|
|
5188
|
+
worktreeInfo.mainRepoPath,
|
|
5189
|
+
worktreeInfo.worktreeName
|
|
5190
|
+
);
|
|
5191
|
+
if (!originalBranch) {
|
|
5192
|
+
log.error("Unable to determine the branch associated with this worktree.");
|
|
5193
|
+
process.exit(1);
|
|
5194
|
+
}
|
|
5195
|
+
if (!targetBranch) {
|
|
5196
|
+
const detected = await detectDefaultRemoteBranch(git);
|
|
5197
|
+
if (!detected) {
|
|
5198
|
+
log.error("Could not detect default remote branch (tried main, master, develop, dev).");
|
|
5199
|
+
log.info("Specify the branch explicitly: workon worktree recycle <branch>");
|
|
5200
|
+
process.exit(1);
|
|
5201
|
+
}
|
|
5202
|
+
targetBranch = detected;
|
|
5203
|
+
}
|
|
5204
|
+
const status = await git.status();
|
|
5205
|
+
const hasChanges = status.modified.length > 0 || status.not_added.length > 0 || status.deleted.length > 0 || status.staged.length > 0 || status.created.length > 0 || status.renamed.length > 0 || status.conflicted.length > 0;
|
|
5206
|
+
if (hasChanges) {
|
|
5207
|
+
log.error("This worktree has uncommitted changes.");
|
|
5208
|
+
log.info("Please commit, stash, or discard your changes before recycling.");
|
|
5209
|
+
process.exit(1);
|
|
5210
|
+
}
|
|
5211
|
+
const alreadyOnBranch = worktreeInfo.branch === originalBranch;
|
|
5212
|
+
if (!options.yes) {
|
|
5213
|
+
console.log(`
|
|
5214
|
+
${chalk8.bold("Recycle worktree:")}`);
|
|
5215
|
+
console.log(` Worktree: ${chalk8.cyan(worktreeInfo.worktreeName)}`);
|
|
5216
|
+
if (alreadyOnBranch) {
|
|
5217
|
+
console.log(` Branch: ${chalk8.green(originalBranch)} (already on it)`);
|
|
5218
|
+
} else {
|
|
5219
|
+
console.log(` Current: ${chalk8.yellow(worktreeInfo.branch)}`);
|
|
5220
|
+
console.log(` Switch to: ${chalk8.green(originalBranch)}`);
|
|
5221
|
+
}
|
|
5222
|
+
console.log(` FF from: ${chalk8.green(`origin/${targetBranch}`)}`);
|
|
5223
|
+
console.log();
|
|
5224
|
+
const shouldProceed = await confirm11({
|
|
5225
|
+
message: "Proceed?",
|
|
5226
|
+
default: true
|
|
5227
|
+
});
|
|
5228
|
+
if (!shouldProceed) {
|
|
5229
|
+
log.info("Recycle cancelled.");
|
|
5230
|
+
return;
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
const spinner = ora5("Fetching latest from remote...").start();
|
|
5234
|
+
try {
|
|
5235
|
+
await git.fetch("origin", targetBranch);
|
|
5236
|
+
if (!alreadyOnBranch) {
|
|
5237
|
+
spinner.text = `Switching to branch '${originalBranch}'...`;
|
|
5238
|
+
await git.checkout(originalBranch);
|
|
5239
|
+
}
|
|
5240
|
+
spinner.text = `Fast-forwarding '${originalBranch}' to origin/${targetBranch}...`;
|
|
5241
|
+
await git.merge([`origin/${targetBranch}`, "--ff-only"]);
|
|
5242
|
+
spinner.succeed(`Recycled! '${originalBranch}' is now at the tip of origin/${targetBranch}`);
|
|
5243
|
+
console.log(chalk8.green("\nWorktree is ready for the next task."));
|
|
5244
|
+
} catch (error) {
|
|
5245
|
+
const message = error.message;
|
|
5246
|
+
spinner.fail("Recycle failed.");
|
|
5247
|
+
if (message.includes("ff-only")) {
|
|
5248
|
+
log.error(
|
|
5249
|
+
`Cannot fast-forward '${originalBranch}' to origin/${targetBranch}. The branch has diverged.`
|
|
5250
|
+
);
|
|
5251
|
+
log.info("You may need to reset or rebase manually.");
|
|
5252
|
+
} else if (message.includes("did not match any")) {
|
|
5253
|
+
log.error(`Remote branch 'origin/${targetBranch}' not found.`);
|
|
5254
|
+
} else {
|
|
5255
|
+
log.error(message);
|
|
5256
|
+
}
|
|
5257
|
+
process.exit(1);
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5124
5260
|
|
|
5125
5261
|
// src/commands/index.ts
|
|
5126
5262
|
var __filename = fileURLToPath(import.meta.url);
|