workon 3.7.0 → 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 +221 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.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
|
@@ -1275,7 +1275,7 @@ var init_tmux = __esm({
|
|
|
1275
1275
|
getWorktreeSessionName(projectName, worktreeName) {
|
|
1276
1276
|
const sanitizedProject = sanitizeForShell(projectName);
|
|
1277
1277
|
const sanitizedWorktree = sanitizeForShell(worktreeName);
|
|
1278
|
-
return `${this.sessionPrefix}${sanitizedProject}
|
|
1278
|
+
return `${this.sessionPrefix}${sanitizedProject}_wt_${sanitizedWorktree}`;
|
|
1279
1279
|
}
|
|
1280
1280
|
async killSession(sessionName) {
|
|
1281
1281
|
try {
|
|
@@ -3942,6 +3942,13 @@ async function processProject(projectParam, options, ctx) {
|
|
|
3942
3942
|
const projects = config.getProjects();
|
|
3943
3943
|
const environment = await EnvironmentRecognizer.recognize(File5.cwd());
|
|
3944
3944
|
if (environment.$isProjectEnvironment && (projectName === "this" || projectName === ".")) {
|
|
3945
|
+
if (worktreeName) {
|
|
3946
|
+
log.error(
|
|
3947
|
+
`Worktree syntax is not supported with '${projectName}'. Use the full project name instead:`
|
|
3948
|
+
);
|
|
3949
|
+
log.info(` workon ${environment.project.name}::${worktreeName}`);
|
|
3950
|
+
process.exit(1);
|
|
3951
|
+
}
|
|
3945
3952
|
log.info(`Opening current project: ${environment.project.name}`);
|
|
3946
3953
|
await switchTo(environment, requestedCommands, options, ctx);
|
|
3947
3954
|
return;
|
|
@@ -3969,7 +3976,7 @@ async function processProject(projectParam, options, ctx) {
|
|
|
3969
3976
|
log.debug(`Using worktree path: ${worktree.path}`);
|
|
3970
3977
|
projectEnv.project.overridePath(worktree.path);
|
|
3971
3978
|
}
|
|
3972
|
-
await switchTo(projectEnv, requestedCommands, options, ctx);
|
|
3979
|
+
await switchTo(projectEnv, requestedCommands, options, ctx, worktreeName);
|
|
3973
3980
|
} else {
|
|
3974
3981
|
log.error(`Project '${projectName}' not found.`);
|
|
3975
3982
|
log.info(`Run 'workon' without arguments to see available projects or create a new one.`);
|
|
@@ -3987,7 +3994,7 @@ Available commands: ${availableCommands}`
|
|
|
3987
3994
|
);
|
|
3988
3995
|
}
|
|
3989
3996
|
}
|
|
3990
|
-
async function switchTo(environment, requestedCommands, options, ctx) {
|
|
3997
|
+
async function switchTo(environment, requestedCommands, options, ctx, worktreeName) {
|
|
3991
3998
|
const { log } = ctx;
|
|
3992
3999
|
const project = environment.project;
|
|
3993
4000
|
let events;
|
|
@@ -4011,11 +4018,35 @@ async function switchTo(environment, requestedCommands, options, ctx) {
|
|
|
4011
4018
|
const hasNpmEvent = events.includes("npm");
|
|
4012
4019
|
const dryRun = options.dryRun || false;
|
|
4013
4020
|
if (hasCwd && hasClaudeEvent && hasNpmEvent) {
|
|
4014
|
-
await handleThreePaneLayout(
|
|
4021
|
+
await handleThreePaneLayout(
|
|
4022
|
+
project,
|
|
4023
|
+
isShellMode,
|
|
4024
|
+
dryRun,
|
|
4025
|
+
shellCommands,
|
|
4026
|
+
events,
|
|
4027
|
+
ctx,
|
|
4028
|
+
worktreeName
|
|
4029
|
+
);
|
|
4015
4030
|
} else if (hasCwd && hasNpmEvent) {
|
|
4016
|
-
await handleTwoPaneNpmLayout(
|
|
4031
|
+
await handleTwoPaneNpmLayout(
|
|
4032
|
+
project,
|
|
4033
|
+
isShellMode,
|
|
4034
|
+
dryRun,
|
|
4035
|
+
shellCommands,
|
|
4036
|
+
events,
|
|
4037
|
+
ctx,
|
|
4038
|
+
worktreeName
|
|
4039
|
+
);
|
|
4017
4040
|
} else if (hasCwd && hasClaudeEvent) {
|
|
4018
|
-
await handleSplitTerminal(
|
|
4041
|
+
await handleSplitTerminal(
|
|
4042
|
+
project,
|
|
4043
|
+
isShellMode,
|
|
4044
|
+
dryRun,
|
|
4045
|
+
shellCommands,
|
|
4046
|
+
events,
|
|
4047
|
+
ctx,
|
|
4048
|
+
worktreeName
|
|
4049
|
+
);
|
|
4019
4050
|
} else {
|
|
4020
4051
|
for (const event of events) {
|
|
4021
4052
|
if (!dryRun) {
|
|
@@ -4049,10 +4080,11 @@ async function getNpmCommand2(project) {
|
|
|
4049
4080
|
const { NpmEvent: NpmEvent2 } = await Promise.resolve().then(() => (init_npm(), npm_exports));
|
|
4050
4081
|
return NpmEvent2.getNpmCommand(npmConfig);
|
|
4051
4082
|
}
|
|
4052
|
-
async function handleTmuxLayout(project, layout, options, shellCommands, events, ctx) {
|
|
4083
|
+
async function handleTmuxLayout(project, layout, options, shellCommands, events, ctx, worktreeName) {
|
|
4053
4084
|
const { log } = ctx;
|
|
4054
4085
|
const tmux = new TmuxManager();
|
|
4055
4086
|
const { isShellMode, dryRun } = options;
|
|
4087
|
+
const tmuxSessionId = worktreeName ? `${project.name}_wt_${worktreeName}` : project.name;
|
|
4056
4088
|
let tmuxHandled = false;
|
|
4057
4089
|
if (isShellMode) {
|
|
4058
4090
|
if (await tmux.isTmuxAvailable()) {
|
|
@@ -4068,7 +4100,7 @@ async function handleTmuxLayout(project, layout, options, shellCommands, events,
|
|
|
4068
4100
|
shellCommands.push(...cmds);
|
|
4069
4101
|
}
|
|
4070
4102
|
}
|
|
4071
|
-
const commands = buildLayoutShellCommands(tmux, project, layout);
|
|
4103
|
+
const commands = buildLayoutShellCommands(tmux, tmuxSessionId, project, layout);
|
|
4072
4104
|
shellCommands.push(...commands);
|
|
4073
4105
|
tmuxHandled = true;
|
|
4074
4106
|
} else {
|
|
@@ -4080,7 +4112,7 @@ async function handleTmuxLayout(project, layout, options, shellCommands, events,
|
|
|
4080
4112
|
} else if (!dryRun) {
|
|
4081
4113
|
if (await tmux.isTmuxAvailable()) {
|
|
4082
4114
|
try {
|
|
4083
|
-
const sessionName = await createTmuxSession(tmux, project, layout);
|
|
4115
|
+
const sessionName = await createTmuxSession(tmux, tmuxSessionId, project, layout);
|
|
4084
4116
|
await tmux.attachToSession(sessionName);
|
|
4085
4117
|
tmuxHandled = true;
|
|
4086
4118
|
} catch (error) {
|
|
@@ -4104,19 +4136,19 @@ async function handleTmuxLayout(project, layout, options, shellCommands, events,
|
|
|
4104
4136
|
}
|
|
4105
4137
|
}
|
|
4106
4138
|
}
|
|
4107
|
-
function buildLayoutShellCommands(tmux, project, layout) {
|
|
4139
|
+
function buildLayoutShellCommands(tmux, sessionId, project, layout) {
|
|
4108
4140
|
switch (layout.type) {
|
|
4109
4141
|
case "split-claude":
|
|
4110
|
-
return tmux.buildShellCommands(
|
|
4142
|
+
return tmux.buildShellCommands(sessionId, project.path.path, layout.claudeArgs);
|
|
4111
4143
|
case "three-pane":
|
|
4112
4144
|
return tmux.buildThreePaneShellCommands(
|
|
4113
|
-
|
|
4145
|
+
sessionId,
|
|
4114
4146
|
project.path.path,
|
|
4115
4147
|
layout.claudeArgs,
|
|
4116
4148
|
layout.npmCommand
|
|
4117
4149
|
);
|
|
4118
4150
|
case "two-pane-npm":
|
|
4119
|
-
return tmux.buildTwoPaneNpmShellCommands(
|
|
4151
|
+
return tmux.buildTwoPaneNpmShellCommands(sessionId, project.path.path, layout.npmCommand);
|
|
4120
4152
|
}
|
|
4121
4153
|
}
|
|
4122
4154
|
function buildFallbackCommandsWithEvents(shellCommands, project, layout, remainingEvents, _ctx) {
|
|
@@ -4134,50 +4166,77 @@ function buildFallbackCommandsWithEvents(shellCommands, project, layout, remaini
|
|
|
4134
4166
|
}
|
|
4135
4167
|
}
|
|
4136
4168
|
}
|
|
4137
|
-
async function createTmuxSession(tmux, project, layout) {
|
|
4169
|
+
async function createTmuxSession(tmux, sessionId, project, layout) {
|
|
4138
4170
|
switch (layout.type) {
|
|
4139
4171
|
case "split-claude":
|
|
4140
|
-
return tmux.createSplitSession(
|
|
4172
|
+
return tmux.createSplitSession(sessionId, project.path.path, layout.claudeArgs);
|
|
4141
4173
|
case "three-pane":
|
|
4142
4174
|
return tmux.createThreePaneSession(
|
|
4143
|
-
|
|
4175
|
+
sessionId,
|
|
4144
4176
|
project.path.path,
|
|
4145
4177
|
layout.claudeArgs,
|
|
4146
4178
|
layout.npmCommand
|
|
4147
4179
|
);
|
|
4148
4180
|
case "two-pane-npm":
|
|
4149
|
-
return tmux.createTwoPaneNpmSession(
|
|
4181
|
+
return tmux.createTwoPaneNpmSession(sessionId, project.path.path, layout.npmCommand);
|
|
4150
4182
|
}
|
|
4151
4183
|
}
|
|
4152
|
-
async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands, events, ctx) {
|
|
4184
|
+
async function handleSplitTerminal(project, isShellMode, dryRun, shellCommands, events, ctx, worktreeName) {
|
|
4185
|
+
const sessionLabel = worktreeName ? `${project.name}::${worktreeName}` : project.name;
|
|
4153
4186
|
const layout = {
|
|
4154
4187
|
type: "split-claude",
|
|
4155
4188
|
handledEvents: ["cwd", "claude"],
|
|
4156
|
-
dryRunMessage: `Would create split tmux session '${
|
|
4189
|
+
dryRunMessage: `Would create split tmux session '${sessionLabel}' with Claude`,
|
|
4157
4190
|
claudeArgs: getClaudeArgs2(project),
|
|
4158
4191
|
npmCommand: null
|
|
4159
4192
|
};
|
|
4160
|
-
await handleTmuxLayout(
|
|
4193
|
+
await handleTmuxLayout(
|
|
4194
|
+
project,
|
|
4195
|
+
layout,
|
|
4196
|
+
{ isShellMode, dryRun },
|
|
4197
|
+
shellCommands,
|
|
4198
|
+
events,
|
|
4199
|
+
ctx,
|
|
4200
|
+
worktreeName
|
|
4201
|
+
);
|
|
4161
4202
|
}
|
|
4162
|
-
async function handleThreePaneLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
|
|
4203
|
+
async function handleThreePaneLayout(project, isShellMode, dryRun, shellCommands, events, ctx, worktreeName) {
|
|
4204
|
+
const sessionLabel = worktreeName ? `${project.name}::${worktreeName}` : project.name;
|
|
4163
4205
|
const layout = {
|
|
4164
4206
|
type: "three-pane",
|
|
4165
4207
|
handledEvents: ["cwd", "claude", "npm"],
|
|
4166
|
-
dryRunMessage: `Would create three-pane tmux session '${
|
|
4208
|
+
dryRunMessage: `Would create three-pane tmux session '${sessionLabel}' with Claude and NPM`,
|
|
4167
4209
|
claudeArgs: getClaudeArgs2(project),
|
|
4168
4210
|
npmCommand: await getNpmCommand2(project)
|
|
4169
4211
|
};
|
|
4170
|
-
await handleTmuxLayout(
|
|
4212
|
+
await handleTmuxLayout(
|
|
4213
|
+
project,
|
|
4214
|
+
layout,
|
|
4215
|
+
{ isShellMode, dryRun },
|
|
4216
|
+
shellCommands,
|
|
4217
|
+
events,
|
|
4218
|
+
ctx,
|
|
4219
|
+
worktreeName
|
|
4220
|
+
);
|
|
4171
4221
|
}
|
|
4172
|
-
async function handleTwoPaneNpmLayout(project, isShellMode, dryRun, shellCommands, events, ctx) {
|
|
4222
|
+
async function handleTwoPaneNpmLayout(project, isShellMode, dryRun, shellCommands, events, ctx, worktreeName) {
|
|
4223
|
+
const sessionLabel = worktreeName ? `${project.name}::${worktreeName}` : project.name;
|
|
4173
4224
|
const layout = {
|
|
4174
4225
|
type: "two-pane-npm",
|
|
4175
4226
|
handledEvents: ["cwd", "npm"],
|
|
4176
|
-
dryRunMessage: `Would create two-pane tmux session '${
|
|
4227
|
+
dryRunMessage: `Would create two-pane tmux session '${sessionLabel}' with NPM`,
|
|
4177
4228
|
claudeArgs: [],
|
|
4178
4229
|
npmCommand: await getNpmCommand2(project)
|
|
4179
4230
|
};
|
|
4180
|
-
await handleTmuxLayout(
|
|
4231
|
+
await handleTmuxLayout(
|
|
4232
|
+
project,
|
|
4233
|
+
layout,
|
|
4234
|
+
{ isShellMode, dryRun },
|
|
4235
|
+
shellCommands,
|
|
4236
|
+
events,
|
|
4237
|
+
ctx,
|
|
4238
|
+
worktreeName
|
|
4239
|
+
);
|
|
4181
4240
|
}
|
|
4182
4241
|
async function processEvent(event, context, ctx) {
|
|
4183
4242
|
const { log } = ctx;
|
|
@@ -4840,6 +4899,20 @@ function createWorktreeCommand(ctx) {
|
|
|
4840
4899
|
}
|
|
4841
4900
|
await removeCurrentWorktree(worktreeInfo, options, ctx);
|
|
4842
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
|
+
});
|
|
4843
4916
|
return command;
|
|
4844
4917
|
}
|
|
4845
4918
|
async function showWorktreeStatus(worktreeInfo, log) {
|
|
@@ -4880,6 +4953,7 @@ ${chalk8.bold("Changes:")}`);
|
|
|
4880
4953
|
console.log(`
|
|
4881
4954
|
${chalk8.bold("Commands:")}`);
|
|
4882
4955
|
console.log(` workon worktree merge - Merge this branch and remove worktree`);
|
|
4956
|
+
console.log(` workon worktree recycle - Reset worktree for the next task`);
|
|
4883
4957
|
console.log(` workon worktree remove - Remove this worktree`);
|
|
4884
4958
|
console.log();
|
|
4885
4959
|
}
|
|
@@ -5062,6 +5136,127 @@ To remove this worktree:`);
|
|
|
5062
5136
|
)
|
|
5063
5137
|
);
|
|
5064
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
|
+
}
|
|
5065
5260
|
|
|
5066
5261
|
// src/commands/index.ts
|
|
5067
5262
|
var __filename = fileURLToPath(import.meta.url);
|