@goodtek/vibeops 2.1.3 → 2.2.1
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/CHANGELOG.md +17 -0
- package/README.md +7 -3
- package/dist/cli.js +16 -6
- package/dist/commands/pull.js +79 -0
- package/dist/commands/task-reship.js +23 -13
- package/dist/lib/task-context.js +2 -2
- package/dist/lib/task-push-pr.js +34 -13
- package/dist/lib/task-reship.js +93 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to VibeOps are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.2.1 - 2026-06-09
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`vibeops task reship --allow-open-mr`**: push then reuse the open MR/PR instead of failing on duplicate `gh pr create`. Skips reship metadata archive when updating the same PR.
|
|
12
|
+
- **`finishTaskWithPullRequest`**: push before checking for an open MR so follow-up commits reach the remote.
|
|
13
|
+
|
|
14
|
+
## 2.2.0 - 2026-06-09
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`vibeops pull`** — one command: `git fetch`, switch to integration branch (e.g. `develop`), fast-forward pull.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **`vibeops task reship`**: no longer blocks on a dirty working tree — uncommitted edits (including on `develop`) are carried onto the task branch. When the task branch is missing locally and on the remote (e.g. after `task sync`), it is **created automatically** from the integration branch. Removed `--allow-dirty` (behavior is now default).
|
|
23
|
+
|
|
7
24
|
## 2.1.3 - 2026-06-08
|
|
8
25
|
|
|
9
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -14,10 +14,11 @@ VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on
|
|
|
14
14
|
| `vibeops init` | Core docs + agent packs (cursor / claude / codex) |
|
|
15
15
|
| `vibeops task add` | New `TASK-NNN` file + task branch |
|
|
16
16
|
| `vibeops task ship` | LLM summary, commit, push, open MR/PR (Status → **Shipped**) |
|
|
17
|
-
| `vibeops task reship` | Shipped
|
|
17
|
+
| `vibeops task reship` | Shipped follow-up: move uncommitted work to task branch, commit, new MR/PR (Status stays Shipped) |
|
|
18
18
|
| `vibeops task merge` | Merge TASK MR/PR into integration branch (default: squash) |
|
|
19
19
|
| `vibeops task sync` | After merge: ff-only integration pull, delete task branches (no TASK md edits) |
|
|
20
20
|
| `vibeops task release` | Release PR: integration → production (GitFlow) |
|
|
21
|
+
| `vibeops pull` | Fetch remote + update integration branch (e.g. develop) |
|
|
21
22
|
| `vibeops status` | Briefing: TASK, Git, LLM, clients |
|
|
22
23
|
| `vibeops llm` | Connect LLM providers (`connect` · `status` · `use`) |
|
|
23
24
|
|
|
@@ -53,6 +54,7 @@ vibeops task add
|
|
|
53
54
|
|
|
54
55
|
vibeops task ship
|
|
55
56
|
vibeops task merge
|
|
57
|
+
vibeops pull
|
|
56
58
|
# Follow-up on same TASK (after merge):
|
|
57
59
|
vibeops task reship TASK-NNN
|
|
58
60
|
vibeops task merge
|
|
@@ -96,10 +98,11 @@ pnpm install && pnpm build && pnpm smoke
|
|
|
96
98
|
- **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--allow-no-remote`, `--cwd`
|
|
97
99
|
- **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
|
|
98
100
|
- **`task ship`**: `--dry-run`, `--no-pr`
|
|
99
|
-
- **`task reship`**: `--dry-run`, `--no-pr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`, `--allow-open-mr
|
|
101
|
+
- **`task reship`**: `--dry-run`, `--no-pr`, `--no-integrate`, `--recreate-branch`, `--skip-llm`, `--allow-open-mr`
|
|
100
102
|
- **`task merge`**: `--dry-run`, `--merge`, `--rebase`
|
|
101
103
|
- **`task sync`**: `--dry-run`, `--no-remote-delete`, `--force`
|
|
102
104
|
- **`task release`**: `--dry-run`, `--no-merge`, `--merge`, `--rebase`
|
|
105
|
+
- **`pull`**: `--dry-run`
|
|
103
106
|
- **`status`**: `--json`
|
|
104
107
|
|
|
105
108
|
## Git
|
|
@@ -107,9 +110,10 @@ pnpm install && pnpm build && pnpm smoke
|
|
|
107
110
|
- **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
|
|
108
111
|
- **`task add`**: `task/<slug>` from **integration** (e.g. `develop`).
|
|
109
112
|
- **`task ship`**: commit implementation + ship metadata (Status **Shipped**) → push once → MR/PR (URL on host; `task merge` resolves by branch).
|
|
110
|
-
- **`task reship`**: Shipped TASK
|
|
113
|
+
- **`task reship`**: Shipped TASK follow-up — carries uncommitted work onto the task branch (creates it from integration when missing after sync), integrates `develop`, commit → push → **new** PR.
|
|
111
114
|
- **`task merge`**: merge MR/PR into integration (CLI or host UI; TASK md unchanged).
|
|
112
115
|
- **`task sync`**: integration ff-only pull → delete `task/*` branches (TASK md unchanged).
|
|
116
|
+
- **`pull`**: fetch + switch to integration branch + `git pull --ff-only` (one command).
|
|
113
117
|
- **`task release`**: integration → production PR + merge (skipped on trunk policy).
|
|
114
118
|
- No force-push to shared branches.
|
|
115
119
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { pullCommand } from "./commands/pull.js";
|
|
3
4
|
import { initCommand } from "./commands/init.js";
|
|
4
5
|
import { llmConnectCommand, llmStatusCommand, llmUseCommand } from "./commands/llm.js";
|
|
5
6
|
import { statusCommand } from "./commands/status.js";
|
|
@@ -14,7 +15,7 @@ import { VERSION } from "./version.js";
|
|
|
14
15
|
const program = new Command();
|
|
15
16
|
program
|
|
16
17
|
.name("vibeops")
|
|
17
|
-
.description("VibeOps — TASK workflow (init · task
|
|
18
|
+
.description("VibeOps — TASK workflow (init · task · pull · status · llm)")
|
|
18
19
|
.version(VERSION, "-v, --version", "Print the VibeOps version");
|
|
19
20
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
20
21
|
const opts = actionCommand.opts();
|
|
@@ -88,14 +89,13 @@ task
|
|
|
88
89
|
});
|
|
89
90
|
task
|
|
90
91
|
.command("reship [taskRef]")
|
|
91
|
-
.description("Follow-up on Shipped TASK:
|
|
92
|
+
.description("Follow-up on Shipped TASK: move uncommitted work to task branch, commit, new MR/PR")
|
|
92
93
|
.option("--dry-run", "Plan only")
|
|
93
94
|
.option("--no-pr", "Push only; skip creating MR/PR")
|
|
94
95
|
.option("--no-integrate", "Skip merging integration branch into task branch")
|
|
95
|
-
.option("--recreate-branch", "
|
|
96
|
+
.option("--recreate-branch", "Create task branch from integration instead of reusing local/remote ref")
|
|
96
97
|
.option("--skip-llm", "Do not run LLM for Result / Test Result")
|
|
97
|
-
.option("--allow-open-mr", "
|
|
98
|
-
.option("--allow-dirty", "Proceed when only governance paths are dirty")
|
|
98
|
+
.option("--allow-open-mr", "Update the open MR/PR with new commits (merge before reship for a new PR)")
|
|
99
99
|
.option("--cwd <path>", "Target directory")
|
|
100
100
|
.action(async (taskRef, opts) => {
|
|
101
101
|
await taskReshipCommand(taskRef, {
|
|
@@ -105,7 +105,6 @@ task
|
|
|
105
105
|
recreateBranch: Boolean(opts.recreateBranch),
|
|
106
106
|
skipLlm: Boolean(opts.skipLlm),
|
|
107
107
|
allowOpenMr: Boolean(opts.allowOpenMr),
|
|
108
|
-
allowDirty: Boolean(opts.allowDirty),
|
|
109
108
|
cwd: opts.cwd,
|
|
110
109
|
});
|
|
111
110
|
});
|
|
@@ -156,6 +155,17 @@ task
|
|
|
156
155
|
cwd: opts.cwd,
|
|
157
156
|
});
|
|
158
157
|
});
|
|
158
|
+
program
|
|
159
|
+
.command("pull")
|
|
160
|
+
.description("Fetch remote and update integration branch (e.g. develop)")
|
|
161
|
+
.option("--dry-run", "Plan only")
|
|
162
|
+
.option("--cwd <path>", "Target directory")
|
|
163
|
+
.action(async (opts) => {
|
|
164
|
+
await pullCommand({
|
|
165
|
+
dryRun: Boolean(opts.dryRun),
|
|
166
|
+
cwd: opts.cwd,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
159
169
|
program
|
|
160
170
|
.command("status")
|
|
161
171
|
.description("Briefing: active TASK, Git, LLM, next hint")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
|
|
3
|
+
import { gitFetchRemote, gitPullFastForwardOnly, gitRemoteBranchExists, gitRemoteUrl, gitSwitchToBranch, readGitInfo, } from "../lib/git.js";
|
|
4
|
+
import { bold, cyan, dim, log } from "../lib/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* One-step remote sync: fetch, switch to integration branch, fast-forward pull.
|
|
7
|
+
*/
|
|
8
|
+
export async function pullCommand(options = {}) {
|
|
9
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
10
|
+
const dryRun = options.dryRun === true;
|
|
11
|
+
let gitCfg;
|
|
12
|
+
try {
|
|
13
|
+
gitCfg = await requireGitConfig(cwd);
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
if (e instanceof GitConfigError) {
|
|
17
|
+
log.error(e.message);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw e;
|
|
22
|
+
}
|
|
23
|
+
const { remote, integrationBranch } = gitCfg;
|
|
24
|
+
log.info(bold("vibeops pull"));
|
|
25
|
+
log.info(` ${dim("remote")} ${remote}`);
|
|
26
|
+
log.info(` ${dim("integration")} ${cyan(integrationBranch)}`);
|
|
27
|
+
log.blank();
|
|
28
|
+
const git = await readGitInfo(cwd);
|
|
29
|
+
if (!git.isRepo) {
|
|
30
|
+
log.error("Not a git repository.");
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const needsSwitch = git.branch !== integrationBranch;
|
|
35
|
+
if (dryRun) {
|
|
36
|
+
log.info(bold("dry-run — would:"));
|
|
37
|
+
log.info(` · git fetch ${remote} --prune`);
|
|
38
|
+
if (needsSwitch) {
|
|
39
|
+
log.info(` · git switch ${integrationBranch}`);
|
|
40
|
+
}
|
|
41
|
+
log.info(` · git pull --ff-only ${remote} ${integrationBranch}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if ((await gitRemoteUrl(cwd, remote)) === null) {
|
|
45
|
+
log.error(`No remote "${remote}". Add a remote or re-run vibeops init.`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await gitFetchRemote(cwd, remote);
|
|
51
|
+
log.ok(`Fetched ${remote}`);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
55
|
+
log.warn(`git fetch failed (${msg}). Continuing with local refs.`);
|
|
56
|
+
}
|
|
57
|
+
if (needsSwitch) {
|
|
58
|
+
const switched = await gitSwitchToBranch(cwd, integrationBranch, remote);
|
|
59
|
+
if (!switched) {
|
|
60
|
+
log.error(`Branch "${integrationBranch}" not found locally or on ${remote}. Push integration first or check the remote URL.`);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
log.ok(`On ${integrationBranch}`);
|
|
65
|
+
}
|
|
66
|
+
if (!(await gitRemoteBranchExists(cwd, remote, integrationBranch))) {
|
|
67
|
+
log.warn(`No ${remote}/${integrationBranch} on the remote yet — fetch only.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await gitPullFastForwardOnly(cwd, remote, integrationBranch);
|
|
72
|
+
log.ok(`Up to date with ${remote}/${integrationBranch}`);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
76
|
+
log.error(`Could not fast-forward ${integrationBranch}: ${msg}. Commit or stash local changes, then rerun \`vibeops pull\`.`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -44,7 +44,8 @@ export async function taskReshipCommand(taskRef, options = {}) {
|
|
|
44
44
|
}
|
|
45
45
|
const integrationBranch = gitCfg?.integrationBranch ?? "develop";
|
|
46
46
|
const remote = gitCfg?.remote ?? "origin";
|
|
47
|
-
|
|
47
|
+
const updateOpenMr = options.allowOpenMr === true;
|
|
48
|
+
if (!dryRun && !(await assertMergeRequestNotOpen(cwd, taskFile, updateOpenMr))) {
|
|
48
49
|
process.exitCode = 1;
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
@@ -56,7 +57,6 @@ export async function taskReshipCommand(taskRef, options = {}) {
|
|
|
56
57
|
remote,
|
|
57
58
|
recreateBranch: options.recreateBranch === true,
|
|
58
59
|
dryRun,
|
|
59
|
-
allowDirty: options.allowDirty === true,
|
|
60
60
|
});
|
|
61
61
|
if (!branchOk) {
|
|
62
62
|
process.exitCode = 1;
|
|
@@ -114,7 +114,12 @@ export async function taskReshipCommand(taskRef, options = {}) {
|
|
|
114
114
|
log.blank();
|
|
115
115
|
if (dryRun) {
|
|
116
116
|
log.info(bold("dry-run — would also:"));
|
|
117
|
-
|
|
117
|
+
if (updateOpenMr) {
|
|
118
|
+
log.info(" · feat commit, push, update open MR/PR (unless --no-pr)");
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
log.info(" · feat commit + reship metadata, then push once and open MR/PR (unless --no-pr)");
|
|
122
|
+
}
|
|
118
123
|
log.blank();
|
|
119
124
|
log.info(`Next: ${cyan("vibeops task merge")}, then optional ${cyan("task sync")}`);
|
|
120
125
|
return;
|
|
@@ -126,7 +131,7 @@ export async function taskReshipCommand(taskRef, options = {}) {
|
|
|
126
131
|
}
|
|
127
132
|
const dirtyBefore = await listWorkingTreeRelPaths(cwd);
|
|
128
133
|
if (dirtyBefore.length === 0) {
|
|
129
|
-
log.error("No changes to reship. Edit files on
|
|
134
|
+
log.error("No changes to reship. Edit files (e.g. on develop), then rerun.");
|
|
130
135
|
process.exitCode = 1;
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
@@ -138,19 +143,24 @@ export async function taskReshipCommand(taskRef, options = {}) {
|
|
|
138
143
|
process.exitCode = 1;
|
|
139
144
|
return;
|
|
140
145
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
taskFile,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
if (updateOpenMr) {
|
|
147
|
+
log.info(dim("Updating open MR/PR (--allow-open-mr)."));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
gitCtx = await refreshGitContextBaseCommit(taskFile, gitCtx, cwd);
|
|
151
|
+
gitCtx = await prepareGitContextForReship(cwd, taskFile, gitCtx);
|
|
152
|
+
await commitReshipMetadata({
|
|
153
|
+
cwd,
|
|
154
|
+
taskFile,
|
|
155
|
+
taskId: parts.id,
|
|
156
|
+
dryRun: false,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
149
159
|
const prResult = await finishTaskWithPullRequest({
|
|
150
160
|
cwd,
|
|
151
161
|
taskFile,
|
|
152
162
|
skipPr: options.noPr === true,
|
|
153
|
-
forceNewMergeRequest:
|
|
163
|
+
forceNewMergeRequest: !updateOpenMr,
|
|
154
164
|
});
|
|
155
165
|
if (!prResult.ok) {
|
|
156
166
|
log.error("Reship failed — fix push/MR, then rerun task reship.");
|
package/dist/lib/task-context.js
CHANGED
|
@@ -48,7 +48,7 @@ export function computeNextHint(input) {
|
|
|
48
48
|
return "task-sync";
|
|
49
49
|
if (input.hasMergeRequest && !input.mergeRequestMerged)
|
|
50
50
|
return "task-merge";
|
|
51
|
-
if (input.hasLocalChanges
|
|
51
|
+
if (input.hasLocalChanges)
|
|
52
52
|
return "task-reship";
|
|
53
53
|
return "task-add";
|
|
54
54
|
}
|
|
@@ -67,7 +67,7 @@ export function hintToText(hint, cwd, task) {
|
|
|
67
67
|
return task ? `Run \`vibeops task ship ${task.id}\`.` : "Run `vibeops task ship`.";
|
|
68
68
|
case "task-reship":
|
|
69
69
|
return task
|
|
70
|
-
? `Follow-up on ${task.id}? Edit
|
|
70
|
+
? `Follow-up on ${task.id}? Edit files (develop is fine), then \`vibeops task reship ${task.id}\`.`
|
|
71
71
|
: "Run `vibeops task reship TASK-NNN` for a Shipped follow-up.";
|
|
72
72
|
case "task-merge":
|
|
73
73
|
return task
|
package/dist/lib/task-push-pr.js
CHANGED
|
@@ -7,6 +7,12 @@ import { dim, log } from "./logger.js";
|
|
|
7
7
|
import { createMergeRequest, findMergeRequestByBranches, probeMergeRequestCli, } from "./pr-create.js";
|
|
8
8
|
import { fallbackTaskPr, generateTaskPrWithLlm } from "./task-pr-llm.js";
|
|
9
9
|
import { readGitContext, readTaskFile } from "./task.js";
|
|
10
|
+
function mergeRequestAlreadyExistsMessage(msg) {
|
|
11
|
+
const lower = msg.toLowerCase();
|
|
12
|
+
return (lower.includes("already exists") ||
|
|
13
|
+
lower.includes("merge request already exists") ||
|
|
14
|
+
lower.includes("pull request already exists"));
|
|
15
|
+
}
|
|
10
16
|
async function buildDiffSummary(cwd, gitCtx) {
|
|
11
17
|
const files = await gitDiffNameOnly(cwd, `${gitCtx.baseCommit}..HEAD`);
|
|
12
18
|
const commits = await gitLogOneline(cwd, `${gitCtx.baseCommit}..HEAD`);
|
|
@@ -55,19 +61,6 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
55
61
|
if (git.branch !== gitCtx.taskBranch) {
|
|
56
62
|
log.warn(`Current branch is ${git.branch ?? "(detached)"}, not ${gitCtx.taskBranch}. Push may fail.`);
|
|
57
63
|
}
|
|
58
|
-
if (opts.skipPr !== true && opts.forceNewMergeRequest !== true) {
|
|
59
|
-
const existing = await findMergeRequestByBranches({
|
|
60
|
-
cwd: opts.cwd,
|
|
61
|
-
host: gitCfg.host,
|
|
62
|
-
headBranch: gitCtx.taskBranch,
|
|
63
|
-
baseBranch: gitCtx.baseBranch,
|
|
64
|
-
state: "open",
|
|
65
|
-
});
|
|
66
|
-
if (existing !== null) {
|
|
67
|
-
log.info(dim(`Open ${label} already exists: ${existing.url}`));
|
|
68
|
-
return { ok: true, mergeRequestUrl: existing.url, pushed: true };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
64
|
const ahead = await gitCommitsAhead(opts.cwd, gitCtx.baseCommit, "HEAD");
|
|
72
65
|
if (ahead === 0) {
|
|
73
66
|
log.warn("No commits ahead of base — pushing branch anyway.");
|
|
@@ -85,6 +78,20 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
85
78
|
log.info(dim("Merge request creation skipped (--no-pr)."));
|
|
86
79
|
return { ok: true, pushed: true };
|
|
87
80
|
}
|
|
81
|
+
if (opts.forceNewMergeRequest !== true) {
|
|
82
|
+
const existing = await findMergeRequestByBranches({
|
|
83
|
+
cwd: opts.cwd,
|
|
84
|
+
host: gitCfg.host,
|
|
85
|
+
headBranch: gitCtx.taskBranch,
|
|
86
|
+
baseBranch: gitCtx.baseBranch,
|
|
87
|
+
state: "open",
|
|
88
|
+
});
|
|
89
|
+
if (existing !== null) {
|
|
90
|
+
log.ok(`Open ${label} updated: ${existing.url}`);
|
|
91
|
+
log.info(dim("MR/PR URL is on the host — not written to TASK md (use task merge)."));
|
|
92
|
+
return { ok: true, mergeRequestUrl: existing.url, pushed: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
88
95
|
const cliOk = await probeMergeRequestCli(gitCfg.host);
|
|
89
96
|
if (!cliOk) {
|
|
90
97
|
const tool = gitCfg.host === "gitlab" ? "glab" : "gh";
|
|
@@ -121,6 +128,20 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
121
128
|
}
|
|
122
129
|
catch (e) {
|
|
123
130
|
const msg = e instanceof Error ? e.message : String(e);
|
|
131
|
+
if (mergeRequestAlreadyExistsMessage(msg)) {
|
|
132
|
+
const existing = await findMergeRequestByBranches({
|
|
133
|
+
cwd: opts.cwd,
|
|
134
|
+
host: gitCfg.host,
|
|
135
|
+
headBranch: gitCtx.taskBranch,
|
|
136
|
+
baseBranch: gitCtx.baseBranch,
|
|
137
|
+
state: "open",
|
|
138
|
+
});
|
|
139
|
+
if (existing !== null) {
|
|
140
|
+
log.ok(`Open ${label} updated: ${existing.url}`);
|
|
141
|
+
log.info(dim("MR/PR URL is on the host — not written to TASK md (use task merge)."));
|
|
142
|
+
return { ok: true, mergeRequestUrl: existing.url, pushed: true };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
124
145
|
log.error(`Could not create merge request: ${msg}`);
|
|
125
146
|
log.info(dim(`Branch was pushed. Create MR manually: ${gitCtx.taskBranch} → ${gitCtx.baseBranch}`));
|
|
126
147
|
return { ok: false, pushed: true };
|
package/dist/lib/task-reship.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { GitConfigError, requireGitConfig } from "./git-config.js";
|
|
3
|
-
import { gitBranchExists, gitCheckout, gitCheckoutNewBranch,
|
|
3
|
+
import { gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitDeleteBranch, gitFetchRemote, gitHeadCommit, gitMergeRemoteBranch, gitMergeInProgress, gitPullFastForwardOnly, gitRemoteBranchExists, gitSwitchToBranch, listUnmergedRelPaths, readGitInfo, } from "./git.js";
|
|
4
4
|
import { dim, log } from "./logger.js";
|
|
5
5
|
import { findMergeRequestByBranches } from "./pr-create.js";
|
|
6
6
|
import { normalizeTaskRef } from "./resolve-task.js";
|
|
@@ -45,52 +45,44 @@ export function reshipNotFoundMessage(taskRef) {
|
|
|
45
45
|
}
|
|
46
46
|
return "No Shipped TASK found. Pass `vibeops task reship TASK-NNN`.";
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const git = await readGitInfo(cwd);
|
|
53
|
-
if (!git.isRepo) {
|
|
54
|
-
log.error("Not a git repository.");
|
|
55
|
-
return false;
|
|
48
|
+
async function switchToTaskBranch(cwd, taskBranch) {
|
|
49
|
+
try {
|
|
50
|
+
await gitCheckout(cwd, taskBranch);
|
|
51
|
+
return true;
|
|
56
52
|
}
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
log.warn("Only governance paths are dirty — proceeding.");
|
|
53
|
+
catch (e) {
|
|
54
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
55
|
+
log.error(`Could not switch to ${taskBranch} with uncommitted changes. Commit, stash, or resolve conflicts, then rerun.`);
|
|
56
|
+
if (msg.length > 0)
|
|
57
|
+
log.info(dim(msg));
|
|
58
|
+
return false;
|
|
64
59
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else {
|
|
75
|
-
log.error(`Local branch ${taskBranch} missing. Rerun with --recreate-branch.`);
|
|
60
|
+
}
|
|
61
|
+
async function removeLocalTaskBranch(cwd, taskBranch, integrationBranch, remote) {
|
|
62
|
+
if (!(await gitBranchExists(cwd, taskBranch)))
|
|
63
|
+
return true;
|
|
64
|
+
const git = await readGitInfo(cwd);
|
|
65
|
+
if (git.branch === taskBranch) {
|
|
66
|
+
const ok = await gitSwitchToBranch(cwd, integrationBranch, remote);
|
|
67
|
+
if (!ok) {
|
|
68
|
+
log.error(`Integration branch "${integrationBranch}" not found.`);
|
|
76
69
|
return false;
|
|
77
70
|
}
|
|
78
|
-
return true;
|
|
79
71
|
}
|
|
80
|
-
|
|
81
|
-
await
|
|
82
|
-
log.
|
|
83
|
-
return true;
|
|
72
|
+
try {
|
|
73
|
+
await gitDeleteBranch(cwd, taskBranch, { force: true });
|
|
74
|
+
log.info(dim(`Removed local ${taskBranch}.`));
|
|
84
75
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
log.error(`Branch ${taskBranch} not found locally or on ${opts.remote}. Use --recreate-branch.`);
|
|
91
|
-
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
78
|
+
log.error(`Could not remove local ${taskBranch}: ${msg}`);
|
|
92
79
|
return false;
|
|
93
80
|
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
async function recreateTaskBranchFromIntegration(opts) {
|
|
84
|
+
const cwd = resolve(opts.cwd);
|
|
85
|
+
const { taskBranch } = opts.gitCtx;
|
|
94
86
|
const ok = await gitSwitchToBranch(cwd, opts.integrationBranch, opts.remote);
|
|
95
87
|
if (!ok) {
|
|
96
88
|
log.error(`Integration branch "${opts.integrationBranch}" not found.`);
|
|
@@ -111,15 +103,75 @@ export async function ensureTaskBranchForReship(opts) {
|
|
|
111
103
|
log.error("Integration branch has no commits.");
|
|
112
104
|
return false;
|
|
113
105
|
}
|
|
114
|
-
|
|
106
|
+
try {
|
|
107
|
+
await gitCheckoutNewBranch(cwd, taskBranch, opts.integrationBranch);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
111
|
+
log.error(`Could not create ${taskBranch} from ${opts.integrationBranch} with uncommitted changes.`);
|
|
112
|
+
if (msg.length > 0)
|
|
113
|
+
log.info(dim(msg));
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
115
116
|
await upsertGitContext(opts.taskFile, {
|
|
116
117
|
...opts.gitCtx,
|
|
117
118
|
baseBranch: opts.integrationBranch,
|
|
118
119
|
baseCommit: head,
|
|
119
120
|
});
|
|
120
|
-
log.ok(`
|
|
121
|
+
log.ok(`Created ${taskBranch} from ${opts.integrationBranch}`);
|
|
121
122
|
return true;
|
|
122
123
|
}
|
|
124
|
+
/** Checkout or recreate the task branch without changing TASK Status. */
|
|
125
|
+
export async function ensureTaskBranchForReship(opts) {
|
|
126
|
+
const cwd = resolve(opts.cwd);
|
|
127
|
+
const { taskBranch } = opts.gitCtx;
|
|
128
|
+
const git = await readGitInfo(cwd);
|
|
129
|
+
if (!git.isRepo) {
|
|
130
|
+
log.error("Not a git repository.");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (git.dirty === true) {
|
|
134
|
+
log.info(dim("Uncommitted changes will be carried onto the task branch."));
|
|
135
|
+
}
|
|
136
|
+
const localExists = await gitBranchExists(cwd, taskBranch);
|
|
137
|
+
const remoteExists = await gitRemoteBranchExists(cwd, opts.remote, taskBranch);
|
|
138
|
+
const recreateFromIntegration = opts.recreateBranch === true || (!localExists && !remoteExists);
|
|
139
|
+
if (opts.dryRun) {
|
|
140
|
+
if (localExists && !opts.recreateBranch) {
|
|
141
|
+
log.info(dim(` would git switch ${taskBranch}`));
|
|
142
|
+
}
|
|
143
|
+
else if (!recreateFromIntegration && remoteExists) {
|
|
144
|
+
log.info(dim(` would checkout ${taskBranch} from ${opts.remote}`));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if (localExists && opts.recreateBranch) {
|
|
148
|
+
log.info(dim(` would remove local ${taskBranch}`));
|
|
149
|
+
}
|
|
150
|
+
log.info(dim(` would create ${taskBranch} from ${opts.integrationBranch}`));
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (localExists && !opts.recreateBranch) {
|
|
155
|
+
if (!(await switchToTaskBranch(cwd, taskBranch)))
|
|
156
|
+
return false;
|
|
157
|
+
log.ok(`On ${taskBranch}`);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (!recreateFromIntegration && remoteExists) {
|
|
161
|
+
const checkedOut = await gitSwitchToBranch(cwd, taskBranch, opts.remote);
|
|
162
|
+
if (checkedOut) {
|
|
163
|
+
log.ok(`Checked out ${taskBranch} from ${opts.remote}`);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
log.warn(`Could not checkout ${opts.remote}/${taskBranch}; creating from integration.`);
|
|
167
|
+
}
|
|
168
|
+
if (localExists) {
|
|
169
|
+
const removed = await removeLocalTaskBranch(cwd, taskBranch, opts.integrationBranch, opts.remote);
|
|
170
|
+
if (!removed)
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return recreateTaskBranchFromIntegration(opts);
|
|
174
|
+
}
|
|
123
175
|
/** Merge latest integration into the current task branch. */
|
|
124
176
|
export async function integrateIntegrationIntoTaskBranch(opts) {
|
|
125
177
|
const cwd = resolve(opts.cwd);
|