@goodtek/vibeops 1.1.2 → 2.0.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/CHANGELOG.md +24 -0
- package/README.md +28 -12
- package/dist/bootstrap/installer.js +1 -1
- package/dist/cli.js +58 -8
- package/dist/commands/init.js +3 -3
- package/dist/commands/llm.js +3 -3
- package/dist/commands/status.js +1 -0
- package/dist/commands/task-add.js +2 -2
- package/dist/commands/task-merge.js +134 -0
- package/dist/commands/task-release.js +103 -0
- package/dist/commands/{task-done.js → task-ship.js} +28 -63
- package/dist/commands/task-sync.js +183 -0
- package/dist/lib/git.js +7 -0
- package/dist/lib/paths.js +1 -1
- package/dist/lib/pr-create.js +63 -0
- package/dist/lib/resolve-task.js +1 -1
- package/dist/lib/task-context.js +22 -7
- package/dist/lib/task-git-commit.js +32 -0
- package/dist/lib/task-lifecycle-target.js +41 -0
- package/dist/lib/task-scaffold.js +1 -1
- package/dist/lib/{task-done-llm.js → task-ship-llm.js} +9 -6
- package/dist/lib/task.js +7 -0
- package/package.json +1 -1
- package/templates/clients/claude/CLAUDE.md +5 -27
- package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +5 -5
- package/templates/clients/cursor/.cursor/rules/03-docs-before-ship.mdc +13 -0
- package/templates/core/AGENTS.md +16 -34
- package/templates/core/docs/logs/README.md +1 -1
- package/templates/core/docs/project/03-architecture.md +1 -1
- package/templates/core/docs/project/05-current-state.md +1 -1
- package/templates/core/docs/project/06-decisions.md +1 -1
- package/templates/skills-shared/implement-task/SKILL.md +7 -16
- package/templates/clients/cursor/.cursor/rules/03-docs-on-done.mdc +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to VibeOps are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.0.0 - 2026-06-03
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- **TASK lifecycle commands renamed and split:** `task done` removed. Use **`task ship`** (submit: commit, push, PR, Status → Review), **`task merge`** (merge PR into integration), **`task sync`** (integration pull, Status → Done, branch cleanup), **`task release`** (integration → production PR).
|
|
12
|
+
- Daily commands: `init`, `task add`, `task ship`, `task merge`, `task sync`, `status`, `llm`. Optional: `task release`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`vibeops task merge`** — default `gh pr merge` / `glab mr merge` (squash unless `--merge` / `--rebase`); `--dry-run`.
|
|
17
|
+
- **`vibeops task release`** — develop → main (or configured branches) release PR + merge; noop when trunk policy.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **`task ship`** replaces `task done`: Done status and `doneAt` are set on **`task sync`** on the integration branch, not at ship time.
|
|
22
|
+
- **`status`** next hints: ship → merge → sync.
|
|
23
|
+
- Templates and Cursor rules reference ship / merge / sync (rule file `03-docs-before-ship.mdc`).
|
|
24
|
+
|
|
25
|
+
## 1.1.3 - 2026-06-03
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`vibeops task sync [TASK-NNN]`** — after the MR is merged on the host: `git fetch --prune`, fast-forward the integration branch (e.g. `develop`), delete the local task branch (`-d`, or `-D` with `--force`), and optionally delete the remote task branch (`--no-remote-delete` to skip). Resolves the task branch from the current `task/*` checkout, the latest Done TASK, or an explicit ref.
|
|
30
|
+
|
|
7
31
|
## 1.1.2 - 2026-06-03
|
|
8
32
|
|
|
9
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@goodtek/vibeops)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on Git branches, and
|
|
8
|
+
VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on Git branches, and runs a clear GitFlow lifecycle. You plan and implement in your chosen agent; the CLI handles files, Git, and short LLM assists.
|
|
9
9
|
|
|
10
10
|
## Commands
|
|
11
11
|
|
|
@@ -13,7 +13,10 @@ VibeOps bootstraps an **agent-friendly repo**, starts numbered **TASK** files on
|
|
|
13
13
|
|---------|---------|
|
|
14
14
|
| `vibeops init` | Core docs + agent packs (cursor / claude / codex) |
|
|
15
15
|
| `vibeops task add` | New `TASK-NNN` file + task branch |
|
|
16
|
-
| `vibeops task
|
|
16
|
+
| `vibeops task ship` | LLM summary, commit, push, open MR/PR (Status → Review) |
|
|
17
|
+
| `vibeops task merge` | Merge TASK MR/PR into integration branch (default: squash) |
|
|
18
|
+
| `vibeops task sync` | After merge: update integration branch, mark Done, delete task branch |
|
|
19
|
+
| `vibeops task release` | Release PR: integration → production (GitFlow) |
|
|
17
20
|
| `vibeops status` | Briefing: TASK, Git, LLM, clients |
|
|
18
21
|
| `vibeops llm` | Connect LLM providers (`connect` · `status` · `use`) |
|
|
19
22
|
|
|
@@ -45,17 +48,25 @@ Re-init **overwrites** templates (rules, skills, doc stubs). **`docs/tasks/TASK-
|
|
|
45
48
|
vibeops init --clients cursor --git --initial-commit
|
|
46
49
|
|
|
47
50
|
vibeops task add
|
|
48
|
-
# Plan / build
|
|
51
|
+
# Plan / build in Cursor (@docs/tasks/TASK-NNN-*.md)
|
|
49
52
|
|
|
50
|
-
vibeops task
|
|
51
|
-
vibeops
|
|
53
|
+
vibeops task ship
|
|
54
|
+
vibeops task merge
|
|
55
|
+
vibeops task sync
|
|
56
|
+
vibeops task add
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Occasionally (GitFlow release to production):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
vibeops task release
|
|
52
63
|
```
|
|
53
64
|
|
|
54
|
-
If a TASK is **In Progress**, `task add` exits with a guide
|
|
65
|
+
If a TASK is **In Progress** or **Review**, `task add` exits with a guide.
|
|
55
66
|
|
|
56
67
|
## LLM (optional)
|
|
57
68
|
|
|
58
|
-
For **`task add`** / **`task
|
|
69
|
+
For **`task add`** / **`task ship`** only (not for coding in the IDE):
|
|
59
70
|
|
|
60
71
|
```bash
|
|
61
72
|
vibeops llm connect
|
|
@@ -80,16 +91,21 @@ pnpm install && pnpm build && pnpm smoke
|
|
|
80
91
|
|
|
81
92
|
- **`init`**: `--clients`, `--yes`, `--dry-run`, `--force`, `--git`, `--initial-commit`, `--git-policy gitflow|trunk`, `--integration-branch`, `--production-branch`, `--allow-no-remote`, `--cwd`
|
|
82
93
|
- **`task add`**: `--dry-run`, `--non-interactive --idea "…"`
|
|
83
|
-
- **`task
|
|
94
|
+
- **`task ship`**: `--dry-run`, `--no-pr`
|
|
95
|
+
- **`task merge`**: `--dry-run`, `--merge`, `--rebase`
|
|
96
|
+
- **`task sync`**: `--dry-run`, `--no-remote-delete`, `--force`
|
|
97
|
+
- **`task release`**: `--dry-run`, `--no-merge`, `--merge`, `--rebase`
|
|
84
98
|
- **`status`**: `--json`
|
|
85
99
|
|
|
86
100
|
## Git
|
|
87
101
|
|
|
88
102
|
- **Init** records branch policy in `.vibeops.json` (`integrationBranch`, `productionBranch`, `host`).
|
|
89
|
-
- **`task add`**:
|
|
90
|
-
- **`task
|
|
91
|
-
-
|
|
92
|
-
-
|
|
103
|
+
- **`task add`**: `task/<slug>` from **integration** (e.g. `develop`).
|
|
104
|
+
- **`task ship`**: commit → push → MR/PR → Status **Review** on task branch.
|
|
105
|
+
- **`task merge`**: merge MR/PR into integration (CLI default: squash).
|
|
106
|
+
- **`task sync`**: integration ff-only pull → Status **Done** → delete `task/*` branches.
|
|
107
|
+
- **`task release`**: integration → production PR + merge (skipped on trunk policy).
|
|
108
|
+
- No force-push to shared branches.
|
|
93
109
|
|
|
94
110
|
## License
|
|
95
111
|
|
|
@@ -12,7 +12,7 @@ function envExampleContents() {
|
|
|
12
12
|
"# VibeOps · optional environment",
|
|
13
13
|
"# Copy to .vibeops.env (gitignored). Never commit secrets.",
|
|
14
14
|
"#",
|
|
15
|
-
"# LLM for task add / task
|
|
15
|
+
"# LLM for task add / task ship (optional — Codex OAuth and Cursor Agent CLI also work):",
|
|
16
16
|
"# OPENAI_API_KEY=",
|
|
17
17
|
"",
|
|
18
18
|
].join("\n");
|
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,16 @@ import { initCommand } from "./commands/init.js";
|
|
|
4
4
|
import { llmConnectCommand, llmStatusCommand, llmUseCommand } from "./commands/llm.js";
|
|
5
5
|
import { statusCommand } from "./commands/status.js";
|
|
6
6
|
import { taskAddCommand } from "./commands/task-add.js";
|
|
7
|
-
import {
|
|
7
|
+
import { taskMergeCommand } from "./commands/task-merge.js";
|
|
8
|
+
import { taskReleaseCommand } from "./commands/task-release.js";
|
|
9
|
+
import { taskShipCommand } from "./commands/task-ship.js";
|
|
10
|
+
import { taskSyncCommand } from "./commands/task-sync.js";
|
|
8
11
|
import { loadVibeopsEnv } from "./lib/env.js";
|
|
9
12
|
import { VERSION } from "./version.js";
|
|
10
13
|
const program = new Command();
|
|
11
14
|
program
|
|
12
15
|
.name("vibeops")
|
|
13
|
-
.description("VibeOps
|
|
16
|
+
.description("VibeOps — TASK workflow (init · task add/ship/merge/sync · status · llm)")
|
|
14
17
|
.version(VERSION, "-v, --version", "Print the VibeOps version");
|
|
15
18
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
16
19
|
const opts = actionCommand.opts();
|
|
@@ -53,10 +56,10 @@ program
|
|
|
53
56
|
gitHost: opts.gitHost,
|
|
54
57
|
});
|
|
55
58
|
});
|
|
56
|
-
const task = program.command("task").description("TASK lifecycle");
|
|
59
|
+
const task = program.command("task").description("TASK lifecycle (add · ship · merge · sync · release)");
|
|
57
60
|
task
|
|
58
61
|
.command("add")
|
|
59
|
-
.description("Create next TASK-NNN, task branch, In Progress
|
|
62
|
+
.description("Create next TASK-NNN, task branch, In Progress")
|
|
60
63
|
.option("--dry-run", "Plan only")
|
|
61
64
|
.option("--non-interactive", "CI: minimal scaffold (optional --idea)")
|
|
62
65
|
.option("--idea <text>", "Short description (CI)")
|
|
@@ -70,18 +73,65 @@ task
|
|
|
70
73
|
});
|
|
71
74
|
});
|
|
72
75
|
task
|
|
73
|
-
.command("
|
|
74
|
-
.description("
|
|
76
|
+
.command("ship [taskRef]")
|
|
77
|
+
.description("Submit TASK: LLM summary, commit, push, open MR/PR (Status → Review)")
|
|
75
78
|
.option("--dry-run", "Plan only")
|
|
76
79
|
.option("--no-pr", "Push only; skip creating MR/PR")
|
|
77
80
|
.option("--cwd <path>", "Target directory")
|
|
78
81
|
.action(async (taskRef, opts) => {
|
|
79
|
-
await
|
|
82
|
+
await taskShipCommand(taskRef, {
|
|
80
83
|
dryRun: Boolean(opts.dryRun),
|
|
81
84
|
noPr: Boolean(opts.noPr),
|
|
82
85
|
cwd: opts.cwd,
|
|
83
86
|
});
|
|
84
87
|
});
|
|
88
|
+
task
|
|
89
|
+
.command("merge [taskRef]")
|
|
90
|
+
.description("Merge TASK MR/PR into integration branch (default: squash)")
|
|
91
|
+
.option("--dry-run", "Plan only")
|
|
92
|
+
.option("--merge", "Merge commit (instead of squash)")
|
|
93
|
+
.option("--rebase", "Rebase merge")
|
|
94
|
+
.option("--cwd <path>", "Target directory")
|
|
95
|
+
.action(async (taskRef, opts) => {
|
|
96
|
+
await taskMergeCommand(taskRef, {
|
|
97
|
+
dryRun: Boolean(opts.dryRun),
|
|
98
|
+
merge: Boolean(opts.merge),
|
|
99
|
+
rebase: Boolean(opts.rebase),
|
|
100
|
+
cwd: opts.cwd,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
task
|
|
104
|
+
.command("sync [taskRef]")
|
|
105
|
+
.description("After MR merge: update integration branch, mark Done, delete task branch")
|
|
106
|
+
.option("--dry-run", "Plan only")
|
|
107
|
+
.option("--no-remote-delete", "Keep the task branch on the remote")
|
|
108
|
+
.option("--force", "Delete local task branch with -D if not fully merged")
|
|
109
|
+
.option("--cwd <path>", "Target directory")
|
|
110
|
+
.action(async (taskRef, opts) => {
|
|
111
|
+
await taskSyncCommand(taskRef, {
|
|
112
|
+
dryRun: Boolean(opts.dryRun),
|
|
113
|
+
noRemoteDelete: Boolean(opts.noRemoteDelete),
|
|
114
|
+
force: Boolean(opts.force),
|
|
115
|
+
cwd: opts.cwd,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
task
|
|
119
|
+
.command("release")
|
|
120
|
+
.description("Open (and merge) release MR/PR: integration branch → production")
|
|
121
|
+
.option("--dry-run", "Plan only")
|
|
122
|
+
.option("--no-merge", "Create release PR only")
|
|
123
|
+
.option("--merge", "Merge commit (instead of squash)")
|
|
124
|
+
.option("--rebase", "Rebase merge")
|
|
125
|
+
.option("--cwd <path>", "Target directory")
|
|
126
|
+
.action(async (opts) => {
|
|
127
|
+
await taskReleaseCommand({
|
|
128
|
+
dryRun: Boolean(opts.dryRun),
|
|
129
|
+
noMerge: Boolean(opts.noMerge),
|
|
130
|
+
merge: Boolean(opts.merge),
|
|
131
|
+
rebase: Boolean(opts.rebase),
|
|
132
|
+
cwd: opts.cwd,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
85
135
|
program
|
|
86
136
|
.command("status")
|
|
87
137
|
.description("Briefing: active TASK, Git, LLM, next hint")
|
|
@@ -93,7 +143,7 @@ program
|
|
|
93
143
|
cwd: opts.cwd,
|
|
94
144
|
});
|
|
95
145
|
});
|
|
96
|
-
const llm = program.command("llm").description("LLM providers for task add / task
|
|
146
|
+
const llm = program.command("llm").description("LLM providers for task add / task ship");
|
|
97
147
|
llm
|
|
98
148
|
.command("connect")
|
|
99
149
|
.description("Interactive menu: connect providers and choose default")
|
package/dist/commands/init.js
CHANGED
|
@@ -124,14 +124,14 @@ function printNextSteps(clients, git) {
|
|
|
124
124
|
if (!clients.includes("cursor") && !clients.includes("claude") && !clients.includes("codex")) {
|
|
125
125
|
log.info(" 2. Open the current TASK file in your agent before coding");
|
|
126
126
|
}
|
|
127
|
-
log.info(` 3. ${cyan("vibeops llm connect")} — LLM for task add / task
|
|
127
|
+
log.info(` 3. ${cyan("vibeops llm connect")} — LLM for task add / task ship`);
|
|
128
128
|
log.info(" 4. Push branches to origin (first time only):");
|
|
129
129
|
log.info(` ${dim(`git push -u ${git.remote} ${git.productionBranch}`)}`);
|
|
130
130
|
if (git.integrationBranch !== git.productionBranch) {
|
|
131
131
|
log.info(` ${dim(`git push -u ${git.remote} ${git.integrationBranch}`)}`);
|
|
132
132
|
}
|
|
133
133
|
log.info(` 5. ${cyan("vibeops task add")} — branches from ${git.integrationBranch} (pulls latest first)`);
|
|
134
|
-
log.info(` 6. ${cyan("vibeops task
|
|
134
|
+
log.info(` 6. ${cyan("vibeops task ship")} → merge → sync — TASK lifecycle on ${git.host}`);
|
|
135
135
|
}
|
|
136
136
|
export async function initCommand(options = {}) {
|
|
137
137
|
const dryRun = options.dryRun === true;
|
|
@@ -272,7 +272,7 @@ export async function initCommand(options = {}) {
|
|
|
272
272
|
if (!dryRun) {
|
|
273
273
|
await writeConfig(projectRoot, buildConfig(name, clients, gitConfig, existingConfig));
|
|
274
274
|
if (options.allowNoRemote === true && remote === null) {
|
|
275
|
-
log.warn("No git remote configured (--allow-no-remote). task
|
|
275
|
+
log.warn("No git remote configured (--allow-no-remote). task ship push/MR will fail until origin exists.");
|
|
276
276
|
}
|
|
277
277
|
printNextSteps(clients, gitConfig);
|
|
278
278
|
}
|
package/dist/commands/llm.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function llmUseCommand(providerArg, opts = {}) {
|
|
|
71
71
|
})),
|
|
72
72
|
];
|
|
73
73
|
preference = await select({
|
|
74
|
-
message: "Default LLM for task add / task
|
|
74
|
+
message: "Default LLM for task add / task ship",
|
|
75
75
|
choices,
|
|
76
76
|
loop: false,
|
|
77
77
|
});
|
|
@@ -99,7 +99,7 @@ export async function llmConnectCommand(opts = {}) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
log.info(bold("vibeops llm connect"));
|
|
102
|
-
log.info(dim("Set up providers, then pick which one task add / task
|
|
102
|
+
log.info(dim("Set up providers, then pick which one task add / task ship use."));
|
|
103
103
|
log.blank();
|
|
104
104
|
let done = false;
|
|
105
105
|
while (!done) {
|
|
@@ -116,7 +116,7 @@ export async function llmConnectCommand(opts = {}) {
|
|
|
116
116
|
const action = await select({
|
|
117
117
|
message: "LLM setup",
|
|
118
118
|
choices: [
|
|
119
|
-
{ name: "Choose default provider (task add / task
|
|
119
|
+
{ name: "Choose default provider (task add / task ship)", value: "pick" },
|
|
120
120
|
{ name: "Set up Cursor Agent CLI (`agent login`)", value: "setup-cursor" },
|
|
121
121
|
{ name: "Show Codex OAuth setup (`codex login`)", value: "setup-codex" },
|
|
122
122
|
{ name: "Set up OpenAI API key (.vibeops.env)", value: "setup-openai" },
|
package/dist/commands/status.js
CHANGED
|
@@ -62,6 +62,7 @@ async function buildReport(cwd) {
|
|
|
62
62
|
resultFilled,
|
|
63
63
|
testFilled,
|
|
64
64
|
onTaskBranch,
|
|
65
|
+
hasMergeRequest: typeof mergeRequestUrl === "string" && mergeRequestUrl.length > 0,
|
|
65
66
|
});
|
|
66
67
|
const llm = isVibeopsProject ? await buildLlmStatusReport(cwd) : null;
|
|
67
68
|
return {
|
|
@@ -44,7 +44,7 @@ export async function taskAddCommand(opts = {}) {
|
|
|
44
44
|
}
|
|
45
45
|
log.info(` ${dim("file")} ${relPath(root, blocking.filePath)}`);
|
|
46
46
|
log.blank();
|
|
47
|
-
log.info(`Finish it first: ${cyan(`vibeops task
|
|
47
|
+
log.info(`Finish it first: ${cyan(`vibeops task sync ${blocking.id}`)} (after ship + merge)`);
|
|
48
48
|
log.info(`Then run ${cyan("vibeops task add")} again.`);
|
|
49
49
|
process.exitCode = 1;
|
|
50
50
|
return;
|
|
@@ -123,5 +123,5 @@ export async function taskAddCommand(opts = {}) {
|
|
|
123
123
|
log.info(bold("Next in Cursor"));
|
|
124
124
|
log.info(` Ask: @${relFile} — plan Scope / Acceptance Criteria`);
|
|
125
125
|
log.info(` Agent: same file — implement`);
|
|
126
|
-
log.info(`
|
|
126
|
+
log.info(` Ship: ${cyan(`vibeops task ship ${taskId}`)}`);
|
|
127
127
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
|
|
3
|
+
import { detectGitHost, mergeRequestLabel } from "../lib/git-host.js";
|
|
4
|
+
import { gitRemoteUrl } from "../lib/git.js";
|
|
5
|
+
import { bold, cyan, dim, log } from "../lib/logger.js";
|
|
6
|
+
import { projectPaths } from "../lib/paths.js";
|
|
7
|
+
import { taskNotFoundMessage } from "../lib/resolve-task.js";
|
|
8
|
+
import { getMergeRequestState, mergeMergeRequest, probeMergeRequestCli, } from "../lib/pr-create.js";
|
|
9
|
+
import { relPath } from "../lib/task-context.js";
|
|
10
|
+
import { resolveLifecycleTarget } from "../lib/task-lifecycle-target.js";
|
|
11
|
+
import { readGitContext } from "../lib/task.js";
|
|
12
|
+
function resolveMergeMethod(opts) {
|
|
13
|
+
if (opts.rebase === true)
|
|
14
|
+
return "rebase";
|
|
15
|
+
if (opts.merge === true)
|
|
16
|
+
return "merge";
|
|
17
|
+
return "squash";
|
|
18
|
+
}
|
|
19
|
+
export async function taskMergeCommand(taskRef, options = {}) {
|
|
20
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
21
|
+
const dryRun = options.dryRun === true;
|
|
22
|
+
const paths = projectPaths(cwd);
|
|
23
|
+
let gitCfg;
|
|
24
|
+
try {
|
|
25
|
+
gitCfg = await requireGitConfig(cwd);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if (e instanceof GitConfigError) {
|
|
29
|
+
log.error(e.message);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
const target = await resolveLifecycleTarget(paths, cwd, taskRef);
|
|
36
|
+
if (target === null) {
|
|
37
|
+
if (taskRef?.trim()) {
|
|
38
|
+
log.error(taskNotFoundMessage(taskRef));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
log.error("No TASK to merge. Pass TASK-NNN, checkout a task/* branch, or run task ship first.");
|
|
42
|
+
}
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const ctx = await readGitContext(target.taskFile);
|
|
47
|
+
if (ctx === null) {
|
|
48
|
+
log.error("No Git Context on TASK — run task add first.");
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!ctx.mergeRequestUrl) {
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
log.info(dim(`would merge ${ctx.taskBranch} → ${ctx.baseBranch} after MR exists (run task ship)`));
|
|
55
|
+
log.blank();
|
|
56
|
+
log.info(`Next: ${cyan("vibeops task sync")}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
log.error("No Merge Request URL on TASK — run task ship first.");
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const remoteUrl = await gitRemoteUrl(cwd, gitCfg.remote);
|
|
64
|
+
const host = remoteUrl !== null && detectGitHost(remoteUrl) !== null
|
|
65
|
+
? detectGitHost(remoteUrl)
|
|
66
|
+
: gitCfg.host;
|
|
67
|
+
const method = resolveMergeMethod(options);
|
|
68
|
+
const label = mergeRequestLabel(host);
|
|
69
|
+
log.info(bold(`vibeops task merge ${target.taskId}`));
|
|
70
|
+
log.info(` ${dim("file")} ${relPath(cwd, target.taskFile)}`);
|
|
71
|
+
log.info(` ${dim("MR/PR")} ${ctx.mergeRequestUrl}`);
|
|
72
|
+
log.info(` ${dim("target")} ${ctx.baseBranch} (${gitCfg.integrationBranch})`);
|
|
73
|
+
log.info(` ${dim("method")} ${method}`);
|
|
74
|
+
log.blank();
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
await mergeMergeRequest({
|
|
77
|
+
cwd,
|
|
78
|
+
host,
|
|
79
|
+
url: ctx.mergeRequestUrl,
|
|
80
|
+
method,
|
|
81
|
+
dryRun: true,
|
|
82
|
+
});
|
|
83
|
+
log.blank();
|
|
84
|
+
log.info(`Next: ${cyan("vibeops task sync")}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const state = await getMergeRequestState(cwd, host, ctx.mergeRequestUrl);
|
|
88
|
+
if (state === "merged") {
|
|
89
|
+
log.ok(`${label} already merged.`);
|
|
90
|
+
log.blank();
|
|
91
|
+
log.info(`Next: ${cyan("vibeops task sync")}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (state === "open") {
|
|
95
|
+
const cliOk = await probeMergeRequestCli(host);
|
|
96
|
+
if (!cliOk) {
|
|
97
|
+
const tool = host === "gitlab" ? "glab" : "gh";
|
|
98
|
+
log.error(`${tool} CLI not found — merge in the host UI, then task sync.`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await mergeMergeRequest({ cwd, host, url: ctx.mergeRequestUrl, method });
|
|
104
|
+
log.ok(`${label} merged into ${ctx.baseBranch}.`);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
108
|
+
log.error(`Merge failed: ${msg}`);
|
|
109
|
+
log.info(dim("Merge in the host UI if branch protection blocks CLI merge."));
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (state === "closed") {
|
|
115
|
+
log.warn(`${label} is closed but not merged — resolve manually.`);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
log.warn(`Could not read ${label} state — attempting merge anyway.`);
|
|
121
|
+
try {
|
|
122
|
+
await mergeMergeRequest({ cwd, host, url: ctx.mergeRequestUrl, method });
|
|
123
|
+
log.ok(`${label} merge command completed.`);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
127
|
+
log.error(`Merge failed: ${msg}`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
log.blank();
|
|
133
|
+
log.info(`Next: ${cyan("vibeops task sync")}`);
|
|
134
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
|
|
3
|
+
import { detectGitHost, mergeRequestLabel } from "../lib/git-host.js";
|
|
4
|
+
import { gitRemoteUrl } from "../lib/git.js";
|
|
5
|
+
import { bold, dim, log } from "../lib/logger.js";
|
|
6
|
+
import { createMergeRequest, getMergeRequestState, mergeMergeRequest, probeMergeRequestCli, } from "../lib/pr-create.js";
|
|
7
|
+
function resolveMergeMethod(opts) {
|
|
8
|
+
if (opts.rebase === true)
|
|
9
|
+
return "rebase";
|
|
10
|
+
if (opts.merge === true)
|
|
11
|
+
return "merge";
|
|
12
|
+
return "squash";
|
|
13
|
+
}
|
|
14
|
+
export async function taskReleaseCommand(options = {}) {
|
|
15
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
16
|
+
const dryRun = options.dryRun === true;
|
|
17
|
+
let gitCfg;
|
|
18
|
+
try {
|
|
19
|
+
gitCfg = await requireGitConfig(cwd);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (e instanceof GitConfigError) {
|
|
23
|
+
log.error(e.message);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
const { integrationBranch, productionBranch, remote } = gitCfg;
|
|
30
|
+
if (integrationBranch === productionBranch) {
|
|
31
|
+
log.info("Trunk policy: integration and production are the same branch — no release PR step.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const remoteUrl = await gitRemoteUrl(cwd, remote);
|
|
35
|
+
const host = remoteUrl !== null && detectGitHost(remoteUrl) !== null
|
|
36
|
+
? detectGitHost(remoteUrl)
|
|
37
|
+
: gitCfg.host;
|
|
38
|
+
const label = mergeRequestLabel(host);
|
|
39
|
+
const title = `Release: ${integrationBranch} → ${productionBranch}`;
|
|
40
|
+
const body = [
|
|
41
|
+
`Integrate \`${integrationBranch}\` into \`${productionBranch}\`.`,
|
|
42
|
+
"",
|
|
43
|
+
"Opened by `vibeops task release`.",
|
|
44
|
+
].join("\n");
|
|
45
|
+
log.info(bold("vibeops task release"));
|
|
46
|
+
log.info(` ${dim("head")} ${integrationBranch}`);
|
|
47
|
+
log.info(` ${dim("base")} ${productionBranch}`);
|
|
48
|
+
log.blank();
|
|
49
|
+
if (dryRun) {
|
|
50
|
+
log.info(bold("dry-run — would:"));
|
|
51
|
+
log.info(` · ${label}: ${integrationBranch} → ${productionBranch}`);
|
|
52
|
+
if (options.noMerge !== true) {
|
|
53
|
+
log.info(` · merge ${label} (${resolveMergeMethod(options)})`);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const cliOk = await probeMergeRequestCli(host);
|
|
58
|
+
if (!cliOk) {
|
|
59
|
+
const tool = host === "gitlab" ? "glab" : "gh";
|
|
60
|
+
log.error(`${tool} CLI not found — create the release ${label} manually.`);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let releaseUrl;
|
|
65
|
+
try {
|
|
66
|
+
const { url } = await createMergeRequest({
|
|
67
|
+
cwd,
|
|
68
|
+
host,
|
|
69
|
+
baseBranch: productionBranch,
|
|
70
|
+
headBranch: integrationBranch,
|
|
71
|
+
title,
|
|
72
|
+
body,
|
|
73
|
+
});
|
|
74
|
+
releaseUrl = url;
|
|
75
|
+
log.ok(`${label}: ${url}`);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
79
|
+
log.error(`Could not create release ${label}: ${msg}`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (options.noMerge === true) {
|
|
84
|
+
log.info(dim("Merge skipped (--no-merge)."));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const state = await getMergeRequestState(cwd, host, releaseUrl);
|
|
88
|
+
if (state === "merged") {
|
|
89
|
+
log.ok(`Release ${label} already merged.`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const method = resolveMergeMethod(options);
|
|
93
|
+
try {
|
|
94
|
+
await mergeMergeRequest({ cwd, host, url: releaseUrl, method });
|
|
95
|
+
log.ok(`Release ${label} merged into ${productionBranch}.`);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
99
|
+
log.error(`Release merge failed: ${msg}`);
|
|
100
|
+
log.info(dim(`Complete merge in the host UI: ${releaseUrl}`));
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
}
|