@goodtek/vibeops 2.4.4 → 2.5.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 +19 -0
- package/README.md +53 -21
- package/dist/cli.js +4 -26
- package/dist/commands/init.js +43 -21
- package/dist/commands/pull.js +8 -1
- package/dist/commands/status.js +91 -72
- package/dist/commands/task-add.js +86 -19
- package/dist/commands/task-merge.js +7 -3
- package/dist/commands/task-release.js +5 -2
- package/dist/lib/git-host-cli.js +115 -0
- package/dist/lib/git-integration-sync.js +214 -0
- package/dist/lib/git-remote.js +177 -59
- package/dist/lib/git.js +25 -0
- package/dist/lib/task-context.js +28 -16
- package/dist/lib/task-push-pr.js +6 -2
- package/dist/lib/task-reship.js +1 -1
- package/dist/lib/task-start.js +24 -15
- package/package.json +16 -12
- package/templates/core/AGENTS.md +1 -1
- package/templates/core/docs/project/06-decisions.md +1 -1
- package/dist/commands/task-reship.js +0 -21
package/dist/lib/git-remote.js
CHANGED
|
@@ -1,32 +1,14 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { execFile } from "node:child_process";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
|
-
import {
|
|
4
|
+
import { brewInstall, formatHostCliHint, formatHostCliMissingMessage, hostCliTool, parseOwnerRepo, probeBrew, probeHostCliAuth, remoteUrlForHost, repoExistsOnHost, runInteractiveCli, } from "./git-host-cli.js";
|
|
4
5
|
import { detectGitHost } from "./git-host.js";
|
|
5
6
|
import { gitRemoteUrl, runGit } from "./git.js";
|
|
6
|
-
import {
|
|
7
|
+
import { askInput, askSelect, askYesNo } from "./inquirer-helpers.js";
|
|
8
|
+
import { dim, log, yellow } from "./logger.js";
|
|
7
9
|
const execFileAsync = promisify(execFile);
|
|
8
|
-
async function probeGh() {
|
|
9
|
-
try {
|
|
10
|
-
await execFileAsync("gh", ["auth", "status"], { maxBuffer: 1024 * 1024 });
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
async function probeGlab() {
|
|
18
|
-
try {
|
|
19
|
-
await execFileAsync("glab", ["auth", "status"], { maxBuffer: 1024 * 1024 });
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
10
|
async function createGithubRepo(cwd, name) {
|
|
27
|
-
const { stdout } = await execFileAsync("gh",
|
|
28
|
-
// gh requires an explicit visibility flag when non-interactive.
|
|
29
|
-
["repo", "create", name, "--source=.", "--remote=origin", "--push", "--private"], { cwd, maxBuffer: 8 * 1024 * 1024 });
|
|
11
|
+
const { stdout } = await execFileAsync("gh", ["repo", "create", name, "--source=.", "--remote=origin", "--push", "--private"], { cwd, maxBuffer: 8 * 1024 * 1024 });
|
|
30
12
|
const match = stdout.match(/https:\/\/github\.com\/[^\s]+/);
|
|
31
13
|
if (match)
|
|
32
14
|
return match[0].replace(/\.git$/, "") + ".git";
|
|
@@ -42,8 +24,135 @@ async function createGitlabRepo(cwd, name, defaultBranch) {
|
|
|
42
24
|
return url;
|
|
43
25
|
throw new Error("glab repo create finished but origin URL could not be read.");
|
|
44
26
|
}
|
|
27
|
+
async function addRemoteUrl(cwd, remote, url) {
|
|
28
|
+
const host = detectGitHost(url);
|
|
29
|
+
if (!host) {
|
|
30
|
+
throw new Error("Only GitHub and GitLab HTTPS/SSH URLs are supported.");
|
|
31
|
+
}
|
|
32
|
+
await runGit(cwd, ["remote", "add", remote, url.trim()]);
|
|
33
|
+
return { remote, url: url.trim(), host };
|
|
34
|
+
}
|
|
35
|
+
async function resolveConnectUrl(host, input) {
|
|
36
|
+
const trimmed = input.trim();
|
|
37
|
+
const parsed = parseOwnerRepo(trimmed);
|
|
38
|
+
if (parsed) {
|
|
39
|
+
if (trimmed.startsWith("http") || trimmed.startsWith("git@") || trimmed.startsWith("ssh://")) {
|
|
40
|
+
return trimmed.endsWith(".git") ? trimmed : `${trimmed.replace(/\/$/, "")}.git`;
|
|
41
|
+
}
|
|
42
|
+
return remoteUrlForHost(host, parsed.owner, parsed.repo);
|
|
43
|
+
}
|
|
44
|
+
if (detectGitHost(trimmed)) {
|
|
45
|
+
return trimmed.endsWith(".git") ? trimmed : `${trimmed.replace(/\/$/, "")}.git`;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Could not parse remote: ${trimmed}`);
|
|
48
|
+
}
|
|
49
|
+
async function repairMissingCli(host) {
|
|
50
|
+
const tool = hostCliTool(host);
|
|
51
|
+
const status = await probeHostCliAuth(host);
|
|
52
|
+
log.warn(status === "missing"
|
|
53
|
+
? formatHostCliMissingMessage(host, "create a remote repository")
|
|
54
|
+
: `${tool} is installed but not logged in.`);
|
|
55
|
+
for (const line of formatHostCliHint(host)) {
|
|
56
|
+
log.info(dim(` ${line}`));
|
|
57
|
+
}
|
|
58
|
+
const brewOk = status === "missing" ? await probeBrew() : false;
|
|
59
|
+
const choices = [
|
|
60
|
+
"I installed / logged in — retry",
|
|
61
|
+
...(brewOk ? [`Run brew install ${tool}`] : []),
|
|
62
|
+
"Enter remote URL manually",
|
|
63
|
+
"Skip remote for now",
|
|
64
|
+
];
|
|
65
|
+
// Loop so brew install / retry can re-prompt.
|
|
66
|
+
for (;;) {
|
|
67
|
+
const pick = await askSelect({
|
|
68
|
+
message: "How do you want to continue?",
|
|
69
|
+
nonInteractive: false,
|
|
70
|
+
choices,
|
|
71
|
+
});
|
|
72
|
+
if (pick === "I installed / logged in — retry") {
|
|
73
|
+
const again = await probeHostCliAuth(host);
|
|
74
|
+
if (again === "ok")
|
|
75
|
+
return "retry";
|
|
76
|
+
log.warn(again === "missing"
|
|
77
|
+
? `${tool} still not found on PATH.`
|
|
78
|
+
: `${tool} still not authenticated. Run \`${tool} auth login\`.`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (pick.startsWith("Run brew install")) {
|
|
82
|
+
log.info(dim(`Running: brew install ${tool}`));
|
|
83
|
+
try {
|
|
84
|
+
await brewInstall(tool);
|
|
85
|
+
log.ok(`Installed ${tool}`);
|
|
86
|
+
log.info(dim(`Next: ${tool} auth login`));
|
|
87
|
+
const loginNow = await askYesNo({
|
|
88
|
+
message: `Run \`${tool} auth login\` now?`,
|
|
89
|
+
nonInteractive: false,
|
|
90
|
+
defaultValue: true,
|
|
91
|
+
});
|
|
92
|
+
if (loginNow) {
|
|
93
|
+
try {
|
|
94
|
+
await runInteractiveCli(tool, ["auth", "login"]);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* user may cancel */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
103
|
+
log.warn(`brew install failed: ${msg}`);
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (pick === "Enter remote URL manually")
|
|
108
|
+
return "manual";
|
|
109
|
+
return "skip";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function connectExisting(cwd, remote, host) {
|
|
113
|
+
const input = await askInput({
|
|
114
|
+
message: "Repository URL or owner/repo",
|
|
115
|
+
nonInteractive: false,
|
|
116
|
+
required: true,
|
|
117
|
+
});
|
|
118
|
+
const url = await resolveConnectUrl(host, input);
|
|
119
|
+
const parsed = parseOwnerRepo(url) ?? parseOwnerRepo(input);
|
|
120
|
+
if (parsed && (await probeHostCliAuth(host)) === "ok") {
|
|
121
|
+
const exists = await repoExistsOnHost(host, parsed.owner, parsed.repo);
|
|
122
|
+
if (!exists) {
|
|
123
|
+
log.warn(yellow(`Could not verify ${parsed.owner}/${parsed.repo} on ${host} (missing or private). Continuing with remote add.`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return addRemoteUrl(cwd, remote, url);
|
|
127
|
+
}
|
|
128
|
+
async function createOnHost(cwd, remote, host, defaultBranch) {
|
|
129
|
+
const folder = basename(cwd);
|
|
130
|
+
const label = host === "gitlab" ? "GitLab project path (group/project)" : "GitHub repository (owner/repo or name)";
|
|
131
|
+
const name = await askInput({
|
|
132
|
+
message: label,
|
|
133
|
+
nonInteractive: false,
|
|
134
|
+
required: true,
|
|
135
|
+
default: folder,
|
|
136
|
+
});
|
|
137
|
+
log.info(dim(`Creating ${host} repository via ${hostCliTool(host)}…`));
|
|
138
|
+
if (host === "gitlab") {
|
|
139
|
+
const url = await createGitlabRepo(cwd, name, defaultBranch);
|
|
140
|
+
// glab may name remote differently; ensure our remote name.
|
|
141
|
+
const current = await gitRemoteUrl(cwd, remote);
|
|
142
|
+
if (!current) {
|
|
143
|
+
const origin = await gitRemoteUrl(cwd, "origin");
|
|
144
|
+
if (origin && remote !== "origin") {
|
|
145
|
+
await runGit(cwd, ["remote", "rename", "origin", remote]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { remote, url, host };
|
|
149
|
+
}
|
|
150
|
+
const url = await createGithubRepo(cwd, name);
|
|
151
|
+
return { remote, url, host };
|
|
152
|
+
}
|
|
45
153
|
/**
|
|
46
|
-
* Ensures `origin` (or named remote) exists.
|
|
154
|
+
* Ensures `origin` (or named remote) exists.
|
|
155
|
+
* Interactive: ask host → create/connect; soft-gate when CLI missing.
|
|
47
156
|
*/
|
|
48
157
|
export async function ensureOriginRemote(opts) {
|
|
49
158
|
const remote = opts.remoteName ?? "origin";
|
|
@@ -59,8 +168,9 @@ export async function ensureOriginRemote(opts) {
|
|
|
59
168
|
return { remote, url: existing, host };
|
|
60
169
|
}
|
|
61
170
|
if (opts.dryRun) {
|
|
62
|
-
|
|
63
|
-
|
|
171
|
+
const host = opts.preferredHost ?? "github";
|
|
172
|
+
log.info(dim(`would configure git remote ${remote} (${host})`));
|
|
173
|
+
return { remote, url: "(dry-run)", host };
|
|
64
174
|
}
|
|
65
175
|
if (opts.allowMissing) {
|
|
66
176
|
return null;
|
|
@@ -69,40 +179,48 @@ export async function ensureOriginRemote(opts) {
|
|
|
69
179
|
throw new Error(`Git remote "${remote}" is not configured. Add it with: git remote add ${remote} <url>`);
|
|
70
180
|
}
|
|
71
181
|
log.blank();
|
|
72
|
-
log.info("Remote
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
message: "GitLab project path (group/project)",
|
|
88
|
-
nonInteractive: false,
|
|
89
|
-
required: true,
|
|
90
|
-
});
|
|
91
|
-
log.info(dim("Creating GitLab project via glab…"));
|
|
92
|
-
const defaultBranch = typeof opts.defaultBranch === "string" && opts.defaultBranch.length > 0
|
|
93
|
-
? opts.defaultBranch
|
|
94
|
-
: "main";
|
|
95
|
-
const url = await createGitlabRepo(opts.cwd, name, defaultBranch);
|
|
96
|
-
return { remote, url, host: "gitlab" };
|
|
182
|
+
log.info("Remote setup — used later for pull requests / merge requests.");
|
|
183
|
+
const hostPick = await askSelect({
|
|
184
|
+
message: "Where will this project's remote live?",
|
|
185
|
+
nonInteractive: false,
|
|
186
|
+
choices: ["GitHub", "GitLab", "Skip for now (local only)"],
|
|
187
|
+
default: opts.preferredHost === "gitlab"
|
|
188
|
+
? "GitLab"
|
|
189
|
+
: opts.preferredHost === "github"
|
|
190
|
+
? "GitHub"
|
|
191
|
+
: undefined,
|
|
192
|
+
});
|
|
193
|
+
if (hostPick.startsWith("Skip")) {
|
|
194
|
+
log.warn("No git remote configured. task ship push/MR will fail until origin exists.");
|
|
195
|
+
log.info(dim(` Later: git remote add ${remote} <url>`));
|
|
196
|
+
return null;
|
|
97
197
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
198
|
+
const host = hostPick === "GitLab" ? "gitlab" : "github";
|
|
199
|
+
const mode = await askSelect({
|
|
200
|
+
message: "Set up the remote",
|
|
100
201
|
nonInteractive: false,
|
|
101
|
-
|
|
202
|
+
choices: ["Create a new repository", "Connect an existing repository"],
|
|
102
203
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
204
|
+
if (mode.startsWith("Connect")) {
|
|
205
|
+
return connectExisting(opts.cwd, remote, host);
|
|
206
|
+
}
|
|
207
|
+
// Create path — requires authenticated CLI (soft gate).
|
|
208
|
+
for (;;) {
|
|
209
|
+
const auth = await probeHostCliAuth(host);
|
|
210
|
+
if (auth === "ok") {
|
|
211
|
+
const defaultBranch = typeof opts.defaultBranch === "string" && opts.defaultBranch.length > 0
|
|
212
|
+
? opts.defaultBranch
|
|
213
|
+
: "main";
|
|
214
|
+
return createOnHost(opts.cwd, remote, host, defaultBranch);
|
|
215
|
+
}
|
|
216
|
+
const action = await repairMissingCli(host);
|
|
217
|
+
if (action === "retry")
|
|
218
|
+
continue;
|
|
219
|
+
if (action === "manual") {
|
|
220
|
+
return connectExisting(opts.cwd, remote, host);
|
|
221
|
+
}
|
|
222
|
+
log.warn("No git remote configured. task ship push/MR will fail until origin exists.");
|
|
223
|
+
log.info(dim(` Later: git remote add ${remote} <url>`));
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
108
226
|
}
|
package/dist/lib/git.js
CHANGED
|
@@ -519,6 +519,31 @@ export async function gitDeleteRemoteBranch(cwd, remote, branch) {
|
|
|
519
519
|
export async function gitPullFastForwardOnly(cwd, remote, branch) {
|
|
520
520
|
await runGit(cwd, ["pull", "--ff-only", remote, branch]);
|
|
521
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Commits reachable only from `left` vs only from `right` (git left-right).
|
|
524
|
+
* Example: `develop...origin/develop` → { left: local-only, right: remote-only }.
|
|
525
|
+
*/
|
|
526
|
+
export async function gitLeftRightCount(cwd, leftRef, rightRef) {
|
|
527
|
+
try {
|
|
528
|
+
const { stdout } = await runGit(cwd, [
|
|
529
|
+
"rev-list",
|
|
530
|
+
"--left-right",
|
|
531
|
+
"--count",
|
|
532
|
+
`${leftRef}...${rightRef}`,
|
|
533
|
+
]);
|
|
534
|
+
const parts = stdout.trim().split(/\s+/);
|
|
535
|
+
if (parts.length < 2)
|
|
536
|
+
return null;
|
|
537
|
+
const left = Number.parseInt(parts[0], 10);
|
|
538
|
+
const right = Number.parseInt(parts[1], 10);
|
|
539
|
+
if (!Number.isFinite(left) || !Number.isFinite(right))
|
|
540
|
+
return null;
|
|
541
|
+
return { left, right };
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
522
547
|
/** Merge `remote/branch` into the current HEAD (follow-up integration). */
|
|
523
548
|
export async function gitMergeRemoteBranch(cwd, remote, branch) {
|
|
524
549
|
await runGit(cwd, ["merge", `${remote}/${branch}`, "--no-edit"]);
|
package/dist/lib/task-context.js
CHANGED
|
@@ -49,7 +49,7 @@ export function computeNextHint(input) {
|
|
|
49
49
|
if (input.hasMergeRequest && !input.mergeRequestMerged)
|
|
50
50
|
return "task-merge";
|
|
51
51
|
if (input.hasLocalChanges)
|
|
52
|
-
return "task-
|
|
52
|
+
return "task-ship-followup";
|
|
53
53
|
return "task-add";
|
|
54
54
|
}
|
|
55
55
|
if (!input.resultFilled || !input.testFilled) {
|
|
@@ -57,36 +57,48 @@ export function computeNextHint(input) {
|
|
|
57
57
|
}
|
|
58
58
|
return input.hasMergeRequest ? "task-merge" : "task-ship";
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
/** Multi-line NEXT block lines (without the leading arrow prefix). */
|
|
61
|
+
export function hintToLines(hint, cwd, task) {
|
|
61
62
|
switch (hint) {
|
|
62
63
|
case "init":
|
|
63
|
-
return "
|
|
64
|
+
return ["vibeops init"];
|
|
64
65
|
case "task-add":
|
|
65
|
-
return "
|
|
66
|
+
return ["vibeops task add"];
|
|
66
67
|
case "task-ship":
|
|
67
|
-
return task ? `
|
|
68
|
-
case "task-
|
|
68
|
+
return [task ? `vibeops task ship ${task.id}` : "vibeops task ship"];
|
|
69
|
+
case "task-ship-followup":
|
|
69
70
|
return task
|
|
70
|
-
?
|
|
71
|
-
|
|
71
|
+
? [
|
|
72
|
+
`Follow up on ${task.id} (after merge: confirm or --new-cycle)`,
|
|
73
|
+
`vibeops task ship ${task.id}`,
|
|
74
|
+
]
|
|
75
|
+
: ["vibeops task ship --new-cycle"];
|
|
72
76
|
case "task-merge":
|
|
73
77
|
return task
|
|
74
|
-
? `
|
|
75
|
-
: "
|
|
78
|
+
? [`vibeops task merge ${task.id}`, "(or merge in the host UI)"]
|
|
79
|
+
: ["vibeops task merge"];
|
|
76
80
|
case "task-sync":
|
|
77
81
|
return task
|
|
78
|
-
? `
|
|
79
|
-
: "
|
|
82
|
+
? [`vibeops task sync ${task.id}`, "then: vibeops task add"]
|
|
83
|
+
: ["vibeops task sync"];
|
|
80
84
|
case "cursor-plan":
|
|
81
85
|
return task
|
|
82
|
-
?
|
|
83
|
-
|
|
86
|
+
? [
|
|
87
|
+
`Cursor Ask: @${relPath(cwd, task.filePath)} — refine Scope / AC`,
|
|
88
|
+
]
|
|
89
|
+
: ["Open the TASK file in Cursor Ask"];
|
|
84
90
|
case "cursor-implement":
|
|
85
91
|
return task
|
|
86
|
-
?
|
|
87
|
-
|
|
92
|
+
? [
|
|
93
|
+
`Cursor Agent: @${relPath(cwd, task.filePath)} — implement`,
|
|
94
|
+
`then: vibeops task ship ${task.id}`,
|
|
95
|
+
]
|
|
96
|
+
: ["Implement per the TASK file, then vibeops task ship"];
|
|
88
97
|
}
|
|
89
98
|
}
|
|
99
|
+
export function hintToText(hint, cwd, task) {
|
|
100
|
+
return hintToLines(hint, cwd, task).join(" · ");
|
|
101
|
+
}
|
|
90
102
|
export async function listInProgressTasks(paths) {
|
|
91
103
|
return (await loadActionableTasks(paths.docsTasks)).filter((t) => t.status === "in_progress");
|
|
92
104
|
}
|
package/dist/lib/task-push-pr.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatHostCliHint, formatHostCliMissingMessage } from "./git-host-cli.js";
|
|
1
2
|
import { readText } from "./filesystem.js";
|
|
2
3
|
import { readConfig } from "./config.js";
|
|
3
4
|
import { detectGitHost, mergeRequestLabel } from "./git-host.js";
|
|
@@ -94,8 +95,11 @@ export async function finishTaskWithPullRequest(opts) {
|
|
|
94
95
|
}
|
|
95
96
|
const cliOk = await probeMergeRequestCli(gitCfg.host);
|
|
96
97
|
if (!cliOk) {
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
log.warn(formatHostCliMissingMessage(gitCfg.host, `create the ${label}`));
|
|
99
|
+
for (const line of formatHostCliHint(gitCfg.host)) {
|
|
100
|
+
log.info(dim(` ${line}`));
|
|
101
|
+
}
|
|
102
|
+
log.info(dim(`Create the ${label} manually: ${gitCtx.taskBranch} → ${gitCtx.baseBranch}`));
|
|
99
103
|
return { ok: true, pushed: true };
|
|
100
104
|
}
|
|
101
105
|
const config = await readConfig(opts.cwd);
|
package/dist/lib/task-reship.js
CHANGED
|
@@ -43,7 +43,7 @@ export function reshipNotFoundMessage(taskRef) {
|
|
|
43
43
|
if (taskRef?.trim()) {
|
|
44
44
|
return `Shipped TASK not found: ${normalizeTaskRef(taskRef)}. Check docs/tasks/.`;
|
|
45
45
|
}
|
|
46
|
-
return "No Shipped TASK found. Pass `vibeops task
|
|
46
|
+
return "No Shipped TASK found. Pass `vibeops task ship TASK-NNN` (use --new-cycle after merge).";
|
|
47
47
|
}
|
|
48
48
|
async function switchToTaskBranch(cwd, taskBranch) {
|
|
49
49
|
try {
|
package/dist/lib/task-start.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
2
|
import { branchNameForTaskFile, readGitContext, readTaskFile, updateInlineStatus, upsertGitContext, } from "./task.js";
|
|
3
|
-
import { gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitGovernanceOnlyDirty, gitHeadCommit,
|
|
3
|
+
import { gitBranchExists, gitCheckout, gitCheckoutNewBranch, gitGovernanceOnlyDirty, gitHeadCommit, gitSwitchToBranch, readGitInfo, } from "./git.js";
|
|
4
|
+
import { ensureIntegrationSynced, printIntegrationSyncDiagnosis, } from "./git-integration-sync.js";
|
|
4
5
|
import { cyan, dim, log } from "./logger.js";
|
|
5
6
|
function relOrAbs(root, p) {
|
|
6
7
|
const r = relative(root, p);
|
|
7
8
|
return r === "" ? "." : r.startsWith("..") ? p : r;
|
|
8
9
|
}
|
|
10
|
+
/** True when In Progress TASK has no task branch yet (failed mid-add). */
|
|
11
|
+
export async function isIncompleteTaskStart(cwd, taskFile) {
|
|
12
|
+
const ctx = await readGitContext(taskFile);
|
|
13
|
+
if (ctx === null)
|
|
14
|
+
return true;
|
|
15
|
+
return !(await gitBranchExists(cwd, ctx.taskBranch));
|
|
16
|
+
}
|
|
9
17
|
/** Create or resume task branch from integration branch and mark TASK In Progress. */
|
|
10
18
|
export async function startTaskBranch(opts) {
|
|
11
19
|
const cwd = resolve(opts.cwd);
|
|
@@ -49,20 +57,21 @@ export async function startTaskBranch(opts) {
|
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
// Ensure integration branch is up-to-date before creating a new task branch.
|
|
52
|
-
if (!branchExists) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
if (!branchExists && opts.skipIntegrationPull !== true) {
|
|
61
|
+
const synced = await ensureIntegrationSynced({
|
|
62
|
+
cwd,
|
|
63
|
+
remote,
|
|
64
|
+
integrationBranch,
|
|
65
|
+
fetch: true,
|
|
66
|
+
});
|
|
67
|
+
if (!synced.ok) {
|
|
68
|
+
printIntegrationSyncDiagnosis(synced.diagnosis);
|
|
69
|
+
log.blank();
|
|
70
|
+
log.info(dim(`TASK file is already created — after fixing sync, rerun ${cyan("vibeops task add")} to resume the branch (does not create a second TASK).`));
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (synced.pulled) {
|
|
74
|
+
log.info(dim(synced.diagnosis.summary));
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
const baseBranch = integrationBranch;
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodtek/vibeops",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "Minimal CLI rails for Cursor-based TASK-driven vibe coding.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@9.15.9",
|
|
6
7
|
"author": "VibeOps contributors",
|
|
7
8
|
"license": "MIT",
|
|
8
9
|
"repository": {
|
|
@@ -29,6 +30,19 @@
|
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=20"
|
|
31
32
|
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "tsx src/cli.ts",
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"start": "node dist/cli.js",
|
|
38
|
+
"test": "pnpm build && node --test \"tests/*.test.mjs\"",
|
|
39
|
+
"smoke": "pnpm typecheck && pnpm build && node --test \"tests/*.test.mjs\" && node scripts/smoke.mjs",
|
|
40
|
+
"clean": "rm -rf dist",
|
|
41
|
+
"prepack": "pnpm clean && pnpm build",
|
|
42
|
+
"publish:dry": "pnpm publish --dry-run --no-git-checks",
|
|
43
|
+
"publish:npm": "scripts/npm-publish.sh",
|
|
44
|
+
"publish:npm:dry": "scripts/npm-publish.sh --dry-run"
|
|
45
|
+
},
|
|
32
46
|
"keywords": [
|
|
33
47
|
"cursor",
|
|
34
48
|
"ai",
|
|
@@ -47,15 +61,5 @@
|
|
|
47
61
|
"@types/node": "^20.14.0",
|
|
48
62
|
"tsx": "^4.19.0",
|
|
49
63
|
"typescript": "^5.5.4"
|
|
50
|
-
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"dev": "tsx src/cli.ts",
|
|
53
|
-
"build": "tsc -p tsconfig.json",
|
|
54
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
-
"start": "node dist/cli.js",
|
|
56
|
-
"test": "pnpm build && node --test \"tests/*.test.mjs\"",
|
|
57
|
-
"smoke": "pnpm typecheck && pnpm build && node --test \"tests/*.test.mjs\" && node scripts/smoke.mjs",
|
|
58
|
-
"clean": "rm -rf dist",
|
|
59
|
-
"publish:dry": "pnpm publish --dry-run --no-git-checks"
|
|
60
64
|
}
|
|
61
|
-
}
|
|
65
|
+
}
|
package/templates/core/AGENTS.md
CHANGED
|
@@ -40,5 +40,5 @@ vibeops llm connect # optional — task add / ship
|
|
|
40
40
|
|
|
41
41
|
- One **In Progress** TASK at a time; **Shipped** does not block `task add`.
|
|
42
42
|
- Fill **Result** and **Test Result** before `vibeops task ship` (warned, not blocked, when updating an open PR).
|
|
43
|
-
- `ship` is state-aware: re-run it to update an open PR; after merge it needs `--new-cycle` (or a confirm) to start a new PR cycle.
|
|
43
|
+
- `ship` is state-aware: re-run it to update an open PR; after merge it needs `--new-cycle` (or a confirm) to start a new PR cycle.
|
|
44
44
|
- Agents do not run `task merge` or `task sync` unless the human asks.
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
## D-001 · VibeOps workflow
|
|
6
6
|
|
|
7
7
|
- **Date:** {{CREATED_AT}}
|
|
8
|
-
- **Decision:** TASK-driven development with `vibeops task add` / `task ship` / `
|
|
8
|
+
- **Decision:** TASK-driven development with `vibeops task add` / `task ship` / `merge` / `sync` and Cursor for plan/build. TASK status: **In Progress** → **Shipped** only.
|
|
9
9
|
- **Why:** Git `docs/tasks` beats chat as source of truth.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { dim, log, yellow } from "../lib/logger.js";
|
|
2
|
-
import { taskShipCommand } from "./task-ship.js";
|
|
3
|
-
/**
|
|
4
|
-
* @deprecated `reship` is now `vibeops task ship --new-cycle`.
|
|
5
|
-
* Kept as a thin alias for backward compatibility.
|
|
6
|
-
*/
|
|
7
|
-
export async function taskReshipCommand(taskRef, options = {}) {
|
|
8
|
-
log.warn(`${yellow("reship is now `ship` (deprecated alias)")} — delegating to \`vibeops task ship --new-cycle\`.`);
|
|
9
|
-
log.info(dim("Run `vibeops task ship` directly; it detects first submit / open-PR update / new PR cycle."));
|
|
10
|
-
log.blank();
|
|
11
|
-
await taskShipCommand(taskRef, {
|
|
12
|
-
dryRun: options.dryRun,
|
|
13
|
-
cwd: options.cwd,
|
|
14
|
-
noPr: options.noPr,
|
|
15
|
-
newCycle: true,
|
|
16
|
-
noIntegrate: options.noIntegrate,
|
|
17
|
-
recreateBranch: options.recreateBranch,
|
|
18
|
-
skipLlm: options.skipLlm,
|
|
19
|
-
allowOpenMr: options.allowOpenMr,
|
|
20
|
-
});
|
|
21
|
-
}
|