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 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
- # 5. Tear down the worktree
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
- # 5. Tear down: remove worktree and delete the branch
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);