@goodtek/vibeops 1.0.0 → 1.1.2
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 +39 -0
- package/README.md +51 -60
- package/dist/bootstrap/installer.js +38 -4
- package/dist/bootstrap/manifest.js +17 -7
- package/dist/cli.js +64 -15
- package/dist/commands/init.js +279 -114
- package/dist/commands/llm.js +177 -0
- package/dist/commands/status.js +37 -1
- package/dist/commands/task-add.js +33 -6
- package/dist/commands/task-done.js +100 -31
- package/dist/lib/config.js +84 -4
- package/dist/lib/env.js +32 -0
- package/dist/lib/git-config.js +21 -0
- package/dist/lib/git-host.js +12 -0
- package/dist/lib/git-policy.js +67 -0
- package/dist/lib/git-remote.js +108 -0
- package/dist/lib/git.js +43 -0
- package/dist/lib/init-clients.js +55 -0
- package/dist/lib/llm-complete.js +18 -19
- package/dist/lib/llm-connect.js +150 -0
- package/dist/lib/llm-preference.js +102 -0
- package/dist/lib/llm-status.js +19 -0
- package/dist/lib/pr-create.js +66 -0
- package/dist/lib/task-add-llm.js +8 -1
- package/dist/lib/task-done-llm.js +8 -1
- package/dist/lib/task-pr-llm.js +50 -0
- package/dist/lib/task-push-pr.js +131 -0
- package/dist/lib/task-start.js +46 -18
- package/dist/lib/task.js +15 -0
- package/package.json +1 -1
- package/templates/clients/claude/CLAUDE.md +28 -0
- package/templates/{AGENTS.md → core/AGENTS.md} +16 -9
- package/templates/{.cursor/skills → skills-shared}/implement-task/SKILL.md +1 -1
- package/templates/{.cursor/skills → skills-shared}/plan-task/SKILL.md +3 -3
- package/dist/lib/task-merge.js +0 -115
- /package/templates/{.cursor → clients/cursor/.cursor}/rules/01-task-source-of-truth.mdc +0 -0
- /package/templates/{.cursor → clients/cursor/.cursor}/rules/02-git-safety.mdc +0 -0
- /package/templates/{.cursor → clients/cursor/.cursor}/rules/03-docs-on-done.mdc +0 -0
- /package/templates/{docs → core/docs}/logs/README.md +0 -0
- /package/templates/{docs → core/docs}/project/03-architecture.md +0 -0
- /package/templates/{docs → core/docs}/project/05-current-state.md +0 -0
- /package/templates/{docs → core/docs}/project/06-decisions.md +0 -0
- /package/templates/{docs → core/docs}/tasks/TASK-000-template.md +0 -0
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { readConfig } from "../lib/config.js";
|
|
2
|
+
import { readConfig, getClientsFromConfig } from "../lib/config.js";
|
|
3
|
+
import { formatGitPolicySummary } from "../lib/git-policy.js";
|
|
3
4
|
import { readText } from "../lib/filesystem.js";
|
|
4
5
|
import { readGitInfo } from "../lib/git.js";
|
|
6
|
+
import { buildLlmStatusReport } from "../lib/llm-status.js";
|
|
5
7
|
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
6
8
|
import { projectPaths } from "../lib/paths.js";
|
|
7
9
|
import { VERSION } from "../version.js";
|
|
@@ -29,6 +31,7 @@ async function buildReport(cwd) {
|
|
|
29
31
|
let taskBranch = null;
|
|
30
32
|
let branchExists = null;
|
|
31
33
|
let goalExcerpt = "";
|
|
34
|
+
let mergeRequestUrl = null;
|
|
32
35
|
if (focus !== null) {
|
|
33
36
|
const body = await readText(focus.filePath);
|
|
34
37
|
goalExcerpt = summarizeGoal(body);
|
|
@@ -36,6 +39,7 @@ async function buildReport(cwd) {
|
|
|
36
39
|
testFilled = hasNonEmptySection(body, "Test Result");
|
|
37
40
|
const ctx = await readGitContext(focus.filePath);
|
|
38
41
|
taskBranch = ctx?.taskBranch ?? null;
|
|
42
|
+
mergeRequestUrl = ctx?.mergeRequestUrl ?? null;
|
|
39
43
|
onTaskBranch = isOnTaskBranch(git.branch, ctx);
|
|
40
44
|
branchExists = await taskBranchExistsFor(cwd, focus);
|
|
41
45
|
focusBlock = {
|
|
@@ -49,6 +53,7 @@ async function buildReport(cwd) {
|
|
|
49
53
|
onTaskBranch,
|
|
50
54
|
taskBranch,
|
|
51
55
|
taskBranchExists: branchExists,
|
|
56
|
+
mergeRequestUrl,
|
|
52
57
|
};
|
|
53
58
|
}
|
|
54
59
|
const hint = computeNextHint({
|
|
@@ -58,14 +63,17 @@ async function buildReport(cwd) {
|
|
|
58
63
|
testFilled,
|
|
59
64
|
onTaskBranch,
|
|
60
65
|
});
|
|
66
|
+
const llm = isVibeopsProject ? await buildLlmStatusReport(cwd) : null;
|
|
61
67
|
return {
|
|
62
68
|
vibeopsVersion: VERSION,
|
|
63
69
|
isVibeopsProject,
|
|
64
70
|
projectName: config?.name ?? null,
|
|
71
|
+
clients: getClientsFromConfig(config),
|
|
65
72
|
git: {
|
|
66
73
|
isRepo: git.isRepo,
|
|
67
74
|
branch: git.branch,
|
|
68
75
|
dirty: git.dirty,
|
|
76
|
+
policy: config?.git ? formatGitPolicySummary(config.git) : null,
|
|
69
77
|
},
|
|
70
78
|
counts,
|
|
71
79
|
inProgress: inProgress.map((t) => ({
|
|
@@ -75,6 +83,7 @@ async function buildReport(cwd) {
|
|
|
75
83
|
})),
|
|
76
84
|
focus: focusBlock,
|
|
77
85
|
nextHint: hintToText(hint, cwd, focus),
|
|
86
|
+
llm,
|
|
78
87
|
};
|
|
79
88
|
}
|
|
80
89
|
function printHuman(report) {
|
|
@@ -90,6 +99,9 @@ function printHuman(report) {
|
|
|
90
99
|
return;
|
|
91
100
|
}
|
|
92
101
|
log.info(` ${dim("Project")} ${report.projectName ?? "(unnamed)"}`);
|
|
102
|
+
if (report.clients.length > 0) {
|
|
103
|
+
log.info(` ${dim("Clients")} ${report.clients.join(", ")}`);
|
|
104
|
+
}
|
|
93
105
|
if (report.counts) {
|
|
94
106
|
const c = report.counts;
|
|
95
107
|
log.info(` ${dim("Tasks")} ${c.total} total · ${cyan(String(c.in_progress))} in progress · ${c.done} done`);
|
|
@@ -125,6 +137,9 @@ function printHuman(report) {
|
|
|
125
137
|
log.info(` ${dim("Tree")} ${yellow("dirty")}`);
|
|
126
138
|
else if (report.git.dirty === false)
|
|
127
139
|
log.info(` ${dim("Tree")} ${green("clean")}`);
|
|
140
|
+
if (report.git.policy) {
|
|
141
|
+
log.info(` ${dim("Policy")} ${report.git.policy}`);
|
|
142
|
+
}
|
|
128
143
|
if (report.focus?.taskBranch) {
|
|
129
144
|
const on = report.focus.onTaskBranch === true
|
|
130
145
|
? green("yes")
|
|
@@ -133,6 +148,27 @@ function printHuman(report) {
|
|
|
133
148
|
: dim("—");
|
|
134
149
|
log.info(` ${dim("Task br.")} ${report.focus.taskBranch} · on branch: ${on}`);
|
|
135
150
|
}
|
|
151
|
+
if (report.focus?.mergeRequestUrl) {
|
|
152
|
+
log.info(` ${dim("MR/PR")} ${report.focus.mergeRequestUrl}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (report.llm) {
|
|
156
|
+
log.blank();
|
|
157
|
+
log.info(bold(" LLM"));
|
|
158
|
+
log.info(` ${dim("Preferred")} ${report.llm.preference}`);
|
|
159
|
+
const active = report.llm.active !== null
|
|
160
|
+
? green(`${report.llm.active} (ok)`)
|
|
161
|
+
: report.llm.anyAvailable
|
|
162
|
+
? yellow("(preference not available)")
|
|
163
|
+
: yellow("none");
|
|
164
|
+
log.info(` ${dim("Active")} ${active}`);
|
|
165
|
+
for (const p of report.llm.providers) {
|
|
166
|
+
const summary = p.summary.length > 56 ? `${p.summary.slice(0, 53)}…` : p.summary;
|
|
167
|
+
log.info(` ${dim(p.id.padEnd(14))} ${p.ok ? green("ok") : dim("—")} ${dim(summary)}`);
|
|
168
|
+
}
|
|
169
|
+
if (!report.llm.anyAvailable) {
|
|
170
|
+
log.info(` ${dim("Setup")} ${cyan("vibeops llm connect")}`);
|
|
171
|
+
}
|
|
136
172
|
}
|
|
137
173
|
log.blank();
|
|
138
174
|
log.info(`${dim("Next")} ${report.nextHint}`);
|
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { pathExists, writeText } from "../lib/filesystem.js";
|
|
3
|
+
import { GitConfigError, requireGitConfig } from "../lib/git-config.js";
|
|
3
4
|
import { askInput } from "../lib/inquirer-helpers.js";
|
|
4
5
|
import { bold, cyan, dim, log, yellow } from "../lib/logger.js";
|
|
5
6
|
import { projectPaths } from "../lib/paths.js";
|
|
6
|
-
import { readConfig } from "../lib/config.js";
|
|
7
7
|
import { slugify } from "../lib/slug.js";
|
|
8
8
|
import { fallbackTaskDraft, llmScaffoldTask } from "../lib/task-add-llm.js";
|
|
9
9
|
import { allocateNextTaskNumber, formatTaskId, uniqueTaskPath, } from "../lib/task-scaffold.js";
|
|
10
10
|
import { findBlockingTask, relPath } from "../lib/task-context.js";
|
|
11
11
|
import { startTaskBranch } from "../lib/task-start.js";
|
|
12
12
|
import { loadActionableTasks, readGitContext } from "../lib/task.js";
|
|
13
|
+
async function loadGitConfigOrNull(root, dryRun) {
|
|
14
|
+
try {
|
|
15
|
+
return await requireGitConfig(root);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
if (e instanceof GitConfigError) {
|
|
19
|
+
if (dryRun)
|
|
20
|
+
return null;
|
|
21
|
+
log.error(e.message);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
13
28
|
export async function taskAddCommand(opts = {}) {
|
|
14
29
|
const root = resolve(opts.cwd ?? process.cwd());
|
|
15
30
|
const dryRun = opts.dryRun === true;
|
|
16
31
|
const nonInteractive = opts.nonInteractive === true || dryRun || process.stdin.isTTY !== true;
|
|
17
32
|
log.info(bold("vibeops task add"));
|
|
18
33
|
log.blank();
|
|
34
|
+
const gitCfg = await loadGitConfigOrNull(root, dryRun);
|
|
35
|
+
if (!dryRun && gitCfg === null)
|
|
36
|
+
return;
|
|
19
37
|
const paths = projectPaths(root);
|
|
20
38
|
const blocking = await findBlockingTask(paths, root);
|
|
21
39
|
if (blocking !== null) {
|
|
@@ -59,6 +77,7 @@ export async function taskAddCommand(opts = {}) {
|
|
|
59
77
|
}
|
|
60
78
|
else {
|
|
61
79
|
log.warn("LLM unavailable — using minimal TASK template.");
|
|
80
|
+
log.info(dim(` Run ${cyan("vibeops llm connect")} to set up Codex, Cursor Agent CLI, or OpenAI.`));
|
|
62
81
|
const fb = fallbackTaskDraft(taskId, idea);
|
|
63
82
|
title = fb.title;
|
|
64
83
|
slug = fb.slug;
|
|
@@ -67,9 +86,19 @@ export async function taskAddCommand(opts = {}) {
|
|
|
67
86
|
}
|
|
68
87
|
const { filePath } = uniqueTaskPath(paths.docsTasks, taskId, slugify(slug || title), tasks.map((t) => t.filePath));
|
|
69
88
|
const relFile = relPath(root, filePath);
|
|
89
|
+
const integration = gitCfg?.integrationBranch ?? "integration";
|
|
70
90
|
if (dryRun) {
|
|
71
91
|
log.info(`[dry-run] Would create ${bold(taskId)} → ${cyan(relFile)}`);
|
|
72
|
-
log.info(dim(` branch task/${slugify(slug || title).replace(/^(\d+)-/, "$1-")}`));
|
|
92
|
+
log.info(dim(` branch task/${slugify(slug || title).replace(/^(\d+)-/, "$1-")} from ${integration}`));
|
|
93
|
+
if (gitCfg) {
|
|
94
|
+
await startTaskBranch({
|
|
95
|
+
cwd: root,
|
|
96
|
+
taskFile: filePath,
|
|
97
|
+
integrationBranch: gitCfg.integrationBranch,
|
|
98
|
+
remote: gitCfg.remote,
|
|
99
|
+
dryRun: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
73
102
|
return;
|
|
74
103
|
}
|
|
75
104
|
if (await pathExists(filePath)) {
|
|
@@ -77,15 +106,13 @@ export async function taskAddCommand(opts = {}) {
|
|
|
77
106
|
}
|
|
78
107
|
await writeText(filePath, markdown);
|
|
79
108
|
log.ok(`Created ${bold(taskId)} → ${cyan(relFile)}`);
|
|
80
|
-
const config = await readConfig(root);
|
|
81
|
-
if (config === null) {
|
|
82
|
-
log.warn("No .vibeops.json — run vibeops init for full template.");
|
|
83
|
-
}
|
|
84
109
|
log.blank();
|
|
85
110
|
log.step("Starting task branch…");
|
|
86
111
|
const started = await startTaskBranch({
|
|
87
112
|
cwd: root,
|
|
88
113
|
taskFile: filePath,
|
|
114
|
+
integrationBranch: gitCfg.integrationBranch,
|
|
115
|
+
remote: gitCfg.remote,
|
|
89
116
|
allowDirty: true,
|
|
90
117
|
});
|
|
91
118
|
if (!started) {
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { askYesNo } from "../lib/inquirer-helpers.js";
|
|
3
2
|
import { readText } from "../lib/filesystem.js";
|
|
4
3
|
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
5
4
|
import { projectPaths } from "../lib/paths.js";
|
|
6
5
|
import { resolveTask, taskNotFoundMessage } from "../lib/resolve-task.js";
|
|
7
|
-
import { mergeTaskBranch } from "../lib/task-merge.js";
|
|
8
6
|
import { applyTaskDoneMemory, fallbackResultSections, llmCompleteTaskDone, writeTaskResultSections, } from "../lib/task-done-llm.js";
|
|
9
7
|
import { relPath } from "../lib/task-context.js";
|
|
10
|
-
import {
|
|
8
|
+
import { requireGitConfig } from "../lib/git-config.js";
|
|
9
|
+
import { gitAddPaths, gitPush, listWorkingTreeRelPaths, partitionPathsForAutoCommit, readGitInfo, runGit, } from "../lib/git.js";
|
|
11
10
|
import { hasNonEmptySection, markGitContextDone, parseTaskFilename, readGitContext, readTaskFile, updateInlineStatus, } from "../lib/task.js";
|
|
11
|
+
import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
|
|
12
12
|
function commitMessageFor(taskId, title) {
|
|
13
13
|
const slug = title.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim() || taskId;
|
|
14
14
|
return `feat(${taskId.toLowerCase()}): ${slug}`;
|
|
15
15
|
}
|
|
16
|
+
function closureCommitMessageFor(taskId) {
|
|
17
|
+
return `docs(${taskId.toLowerCase()}): close task metadata`;
|
|
18
|
+
}
|
|
16
19
|
async function tryCommitDirty(cwd, message, dryRun) {
|
|
17
20
|
const git = await readGitInfo(cwd);
|
|
18
21
|
if (!git.isRepo || git.dirty !== true)
|
|
@@ -32,11 +35,45 @@ async function tryCommitDirty(cwd, message, dryRun) {
|
|
|
32
35
|
log.ok(`Committed: ${message}`);
|
|
33
36
|
return true;
|
|
34
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* After push/MR: write Status Done + Git Context closure, commit, push so the remote
|
|
40
|
+
* branch matches local TASK metadata (canonical two-phase close).
|
|
41
|
+
*/
|
|
42
|
+
async function commitClosureMetadataAndPush(opts) {
|
|
43
|
+
if (opts.dryRun) {
|
|
44
|
+
log.info(dim(" would set Status → Done and update Git Context (doneAt)"));
|
|
45
|
+
log.info(dim(" would commit closure metadata"));
|
|
46
|
+
if (opts.push !== null) {
|
|
47
|
+
log.info(dim(` would git push ${opts.push.remote} ${opts.push.branch} (closure commit)`));
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
await updateInlineStatus(opts.taskFile, "done");
|
|
52
|
+
await markGitContextDone(opts.taskFile);
|
|
53
|
+
log.ok("Status → Done");
|
|
54
|
+
const committed = await tryCommitDirty(opts.cwd, closureCommitMessageFor(opts.taskId), false);
|
|
55
|
+
if (!committed) {
|
|
56
|
+
log.warn("No file changes for closure commit — verify TASK Status and Git Context on the remote branch.");
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (opts.push === null)
|
|
60
|
+
return true;
|
|
61
|
+
try {
|
|
62
|
+
await gitPush(opts.cwd, opts.push.remote, opts.push.branch, false);
|
|
63
|
+
log.ok(`Pushed closure commit → ${opts.push.remote}/${opts.push.branch}`);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
68
|
+
log.error(`Closure commit push failed: ${msg}`);
|
|
69
|
+
log.info(dim(`Fix auth/remote, then: git push ${opts.push.remote} ${opts.push.branch}`));
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
35
73
|
export async function taskDoneCommand(taskRef, options = {}) {
|
|
36
74
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
37
75
|
const paths = projectPaths(cwd);
|
|
38
76
|
const dryRun = options.dryRun === true;
|
|
39
|
-
const interactive = !dryRun && process.stdin.isTTY === true && options.yes !== true;
|
|
40
77
|
const resolved = await resolveTask(paths, cwd, taskRef);
|
|
41
78
|
if (resolved === null) {
|
|
42
79
|
log.error(taskNotFoundMessage(taskRef));
|
|
@@ -98,43 +135,75 @@ export async function taskDoneCommand(taskRef, options = {}) {
|
|
|
98
135
|
if (!resultOk || !testOk) {
|
|
99
136
|
log.info(" · LLM fill Result / Test Result + patch docs/project/*");
|
|
100
137
|
}
|
|
101
|
-
log.info(" ·
|
|
102
|
-
log.info(" ·
|
|
138
|
+
log.info(" · commit implementation, push task branch, open MR/PR (unless --no-pr)");
|
|
139
|
+
log.info(" · Status → Done; commit closure metadata; push again");
|
|
140
|
+
await finishTaskWithPullRequest({
|
|
141
|
+
cwd,
|
|
142
|
+
taskFile,
|
|
143
|
+
dryRun: true,
|
|
144
|
+
skipPr: options.noPr === true,
|
|
145
|
+
});
|
|
146
|
+
const gitCtxDry = await readGitContext(taskFile);
|
|
147
|
+
const gitCfgDry = gitCtxDry !== null
|
|
148
|
+
? await requireGitConfig(cwd).catch(() => null)
|
|
149
|
+
: null;
|
|
150
|
+
await commitClosureMetadataAndPush({
|
|
151
|
+
cwd,
|
|
152
|
+
taskFile,
|
|
153
|
+
taskId,
|
|
154
|
+
dryRun: true,
|
|
155
|
+
push: gitCtxDry !== null && gitCfgDry !== null
|
|
156
|
+
? { remote: gitCfgDry.remote, branch: gitCtxDry.taskBranch }
|
|
157
|
+
: null,
|
|
158
|
+
});
|
|
103
159
|
return;
|
|
104
160
|
}
|
|
105
|
-
await updateInlineStatus(taskFile, "done");
|
|
106
|
-
await markGitContextDone(taskFile);
|
|
107
|
-
log.ok("Status → Done");
|
|
108
161
|
const parts = parseTaskFilename(taskFile);
|
|
109
162
|
const commitMsg = commitMessageFor(parts.id, meta.title);
|
|
110
163
|
await tryCommitDirty(cwd, commitMsg, false);
|
|
111
164
|
const gitCtx = await readGitContext(taskFile);
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
165
|
+
if (gitCtx !== null) {
|
|
166
|
+
const prResult = await finishTaskWithPullRequest({
|
|
167
|
+
cwd,
|
|
168
|
+
taskFile,
|
|
169
|
+
skipPr: options.noPr === true,
|
|
170
|
+
});
|
|
171
|
+
if (!prResult.ok) {
|
|
172
|
+
log.error("TASK left In Progress — fix push/MR, then rerun task done.");
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
return;
|
|
120
175
|
}
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
taskId: meta.id,
|
|
125
|
-
taskTitle: meta.title,
|
|
126
|
-
gitCtx,
|
|
127
|
-
allowDirty: options.allowDirty,
|
|
128
|
-
});
|
|
129
|
-
if (!mergeResult.ok)
|
|
130
|
-
process.exitCode = 1;
|
|
176
|
+
if (prResult.mergeRequestUrl) {
|
|
177
|
+
log.info(` ${dim("MR/PR")} ${prResult.mergeRequestUrl}`);
|
|
178
|
+
log.info(dim("Merge on GitHub/GitLab — CI deploys after merge."));
|
|
131
179
|
}
|
|
132
|
-
|
|
133
|
-
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
log.info(dim("No Git Context — push/MR skipped."));
|
|
183
|
+
}
|
|
184
|
+
const gitCtxAfter = await readGitContext(taskFile);
|
|
185
|
+
let pushTarget = null;
|
|
186
|
+
if (gitCtxAfter !== null) {
|
|
187
|
+
try {
|
|
188
|
+
const gitCfg = await requireGitConfig(cwd);
|
|
189
|
+
pushTarget = { remote: gitCfg.remote, branch: gitCtxAfter.taskBranch };
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
if (e instanceof Error)
|
|
193
|
+
log.warn(e.message);
|
|
134
194
|
}
|
|
135
195
|
}
|
|
136
|
-
|
|
137
|
-
|
|
196
|
+
const closed = await commitClosureMetadataAndPush({
|
|
197
|
+
cwd,
|
|
198
|
+
taskFile,
|
|
199
|
+
taskId: parts.id,
|
|
200
|
+
dryRun: false,
|
|
201
|
+
push: pushTarget,
|
|
202
|
+
});
|
|
203
|
+
if (!closed) {
|
|
204
|
+
log.error("TASK metadata is Done locally but closure push failed — push manually, then continue.");
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
return;
|
|
138
207
|
}
|
|
139
208
|
log.blank();
|
|
140
209
|
log.info(`Next: ${cyan("vibeops task add")} or ${cyan("vibeops status")}`);
|
package/dist/lib/config.js
CHANGED
|
@@ -1,8 +1,57 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { readTextOrNull, writeText } from "./filesystem.js";
|
|
2
|
+
import { pathExists, readTextOrNull, writeText } from "./filesystem.js";
|
|
3
3
|
import { VIBEOPS_CONFIG_FILE } from "./paths.js";
|
|
4
4
|
import { VERSION } from "../version.js";
|
|
5
|
-
import { VIBEOPS_CONFIG_SCHEMA_VERSION } from "../types/config.js";
|
|
5
|
+
import { VIBEOPS_CONFIG_SCHEMA_VERSION, } from "../types/config.js";
|
|
6
|
+
import { isVibeopsClientId } from "./init-clients.js";
|
|
7
|
+
function parseClientsBlock(raw) {
|
|
8
|
+
if (!Array.isArray(raw))
|
|
9
|
+
return undefined;
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const item of raw) {
|
|
12
|
+
if (typeof item === "string" && isVibeopsClientId(item) && !out.includes(item)) {
|
|
13
|
+
out.push(item);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out.length > 0 ? out : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function getClientsFromConfig(config) {
|
|
19
|
+
if (config?.clients && config.clients.length > 0)
|
|
20
|
+
return [...config.clients];
|
|
21
|
+
return ["cursor"];
|
|
22
|
+
}
|
|
23
|
+
export async function isVibeopsProject(root) {
|
|
24
|
+
if ((await readConfig(root)) !== null)
|
|
25
|
+
return true;
|
|
26
|
+
return pathExists(join(root, VIBEOPS_CONFIG_FILE));
|
|
27
|
+
}
|
|
28
|
+
function parseGitBlock(raw) {
|
|
29
|
+
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const o = raw;
|
|
33
|
+
const remote = typeof o.remote === "string" && o.remote.length > 0 ? o.remote : "origin";
|
|
34
|
+
const host = o.host === "github" || o.host === "gitlab" ? o.host : undefined;
|
|
35
|
+
const integrationBranch = typeof o.integrationBranch === "string" && o.integrationBranch.length > 0
|
|
36
|
+
? o.integrationBranch
|
|
37
|
+
: undefined;
|
|
38
|
+
const productionBranch = typeof o.productionBranch === "string" && o.productionBranch.length > 0
|
|
39
|
+
? o.productionBranch
|
|
40
|
+
: undefined;
|
|
41
|
+
if (!host || !integrationBranch || !productionBranch)
|
|
42
|
+
return undefined;
|
|
43
|
+
return { remote, host, integrationBranch, productionBranch };
|
|
44
|
+
}
|
|
45
|
+
function parseLlmBlock(raw) {
|
|
46
|
+
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const p = raw.provider;
|
|
50
|
+
if (p === "auto" || p === "codex-oauth" || p === "cursor-agent" || p === "openai") {
|
|
51
|
+
return { provider: p };
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
6
55
|
export async function readConfig(root) {
|
|
7
56
|
const text = await readTextOrNull(join(root, VIBEOPS_CONFIG_FILE));
|
|
8
57
|
if (text === null)
|
|
@@ -13,11 +62,17 @@ export async function readConfig(root) {
|
|
|
13
62
|
typeof parsed.vibeopsVersion === "string" &&
|
|
14
63
|
typeof parsed.createdAt === "string" &&
|
|
15
64
|
parsed.schemaVersion === VIBEOPS_CONFIG_SCHEMA_VERSION) {
|
|
65
|
+
const llm = parseLlmBlock(parsed.llm);
|
|
66
|
+
const git = parseGitBlock(parsed.git);
|
|
67
|
+
const clients = parseClientsBlock(parsed.clients) ?? ["cursor"];
|
|
16
68
|
return {
|
|
17
69
|
name: parsed.name,
|
|
18
70
|
vibeopsVersion: parsed.vibeopsVersion,
|
|
19
71
|
schemaVersion: VIBEOPS_CONFIG_SCHEMA_VERSION,
|
|
20
72
|
createdAt: parsed.createdAt,
|
|
73
|
+
clients,
|
|
74
|
+
...(git ? { git } : {}),
|
|
75
|
+
...(llm ? { llm } : {}),
|
|
21
76
|
};
|
|
22
77
|
}
|
|
23
78
|
return null;
|
|
@@ -26,12 +81,15 @@ export async function readConfig(root) {
|
|
|
26
81
|
return null;
|
|
27
82
|
}
|
|
28
83
|
}
|
|
29
|
-
export function buildConfig(name, nowIso) {
|
|
84
|
+
export function buildConfig(name, clients, git, existing, nowIso) {
|
|
30
85
|
return {
|
|
31
86
|
name,
|
|
32
87
|
vibeopsVersion: VERSION,
|
|
33
88
|
schemaVersion: VIBEOPS_CONFIG_SCHEMA_VERSION,
|
|
34
|
-
createdAt: nowIso ?? new Date().toISOString(),
|
|
89
|
+
createdAt: existing?.createdAt ?? nowIso ?? new Date().toISOString(),
|
|
90
|
+
clients,
|
|
91
|
+
git,
|
|
92
|
+
llm: existing?.llm ?? { provider: "auto" },
|
|
35
93
|
};
|
|
36
94
|
}
|
|
37
95
|
export function configToJson(config) {
|
|
@@ -40,3 +98,25 @@ export function configToJson(config) {
|
|
|
40
98
|
export async function writeConfig(root, config) {
|
|
41
99
|
await writeText(join(root, VIBEOPS_CONFIG_FILE), configToJson(config));
|
|
42
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Merge `llm.provider` into existing `.vibeops.json` (preserves other keys).
|
|
103
|
+
*/
|
|
104
|
+
export async function setLlmProviderPreference(root, provider) {
|
|
105
|
+
const path = join(root, VIBEOPS_CONFIG_FILE);
|
|
106
|
+
const text = await readTextOrNull(path);
|
|
107
|
+
if (text === null) {
|
|
108
|
+
throw new Error(`${VIBEOPS_CONFIG_FILE} not found — run vibeops init first.`);
|
|
109
|
+
}
|
|
110
|
+
let data;
|
|
111
|
+
try {
|
|
112
|
+
data = JSON.parse(text);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw new Error(`${VIBEOPS_CONFIG_FILE} is not valid JSON.`);
|
|
116
|
+
}
|
|
117
|
+
const prevLlm = typeof data.llm === "object" && data.llm !== null && !Array.isArray(data.llm)
|
|
118
|
+
? data.llm
|
|
119
|
+
: {};
|
|
120
|
+
data.llm = { ...prevLlm, provider };
|
|
121
|
+
await writeText(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
122
|
+
}
|
package/dist/lib/env.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { VIBEOPS_ENV_FILE } from "./paths.js";
|
|
4
|
+
/**
|
|
5
|
+
* Loads `KEY=value` pairs from `.vibeops.env` without overwriting existing `process.env`.
|
|
6
|
+
*/
|
|
7
|
+
export async function loadVibeopsEnv(cwd) {
|
|
8
|
+
const path = join(resolve(cwd), VIBEOPS_ENV_FILE);
|
|
9
|
+
try {
|
|
10
|
+
const text = await readFile(path, "utf-8");
|
|
11
|
+
for (const line of text.split("\n")) {
|
|
12
|
+
const t = line.trim();
|
|
13
|
+
if (t.length === 0 || t.startsWith("#"))
|
|
14
|
+
continue;
|
|
15
|
+
const eq = t.indexOf("=");
|
|
16
|
+
if (eq <= 0)
|
|
17
|
+
continue;
|
|
18
|
+
const key = t.slice(0, eq).trim();
|
|
19
|
+
let val = t.slice(eq + 1).trim();
|
|
20
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
21
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
22
|
+
val = val.slice(1, -1);
|
|
23
|
+
}
|
|
24
|
+
if (key.length > 0 && process.env[key] === undefined) {
|
|
25
|
+
process.env[key] = val;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* missing or unreadable — ok */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readConfig } from "./config.js";
|
|
2
|
+
export class GitConfigError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "GitConfigError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Load `.vibeops.json` git block or throw with re-init guidance. */
|
|
9
|
+
export async function requireGitConfig(cwd) {
|
|
10
|
+
const config = await readConfig(cwd);
|
|
11
|
+
if (!config) {
|
|
12
|
+
throw new GitConfigError("No .vibeops.json found. Run vibeops init in this project first.");
|
|
13
|
+
}
|
|
14
|
+
if (!config.git?.integrationBranch || !config.git?.productionBranch) {
|
|
15
|
+
throw new GitConfigError("Missing git policy in .vibeops.json. Re-run vibeops init to set integration and production branches.");
|
|
16
|
+
}
|
|
17
|
+
if (!config.git.host || !config.git.remote) {
|
|
18
|
+
throw new GitConfigError("Incomplete git config in .vibeops.json. Re-run vibeops init.");
|
|
19
|
+
}
|
|
20
|
+
return config.git;
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Infer Git host from a remote URL. */
|
|
2
|
+
export function detectGitHost(remoteUrl) {
|
|
3
|
+
const u = remoteUrl.trim().toLowerCase();
|
|
4
|
+
if (u.includes("github.com"))
|
|
5
|
+
return "github";
|
|
6
|
+
if (u.includes("gitlab"))
|
|
7
|
+
return "gitlab";
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
export function mergeRequestLabel(host) {
|
|
11
|
+
return host === "gitlab" ? "Merge request" : "Pull request";
|
|
12
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { select } from "@inquirer/prompts";
|
|
2
|
+
import { askInput } from "./inquirer-helpers.js";
|
|
3
|
+
export const GIT_POLICY_PRESETS = [
|
|
4
|
+
{
|
|
5
|
+
id: "gitflow",
|
|
6
|
+
label: "GitFlow lite — develop (integration) + main (production)",
|
|
7
|
+
integrationBranch: "develop",
|
|
8
|
+
productionBranch: "main",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "trunk",
|
|
12
|
+
label: "Trunk — main only (integration and production = main)",
|
|
13
|
+
integrationBranch: "main",
|
|
14
|
+
productionBranch: "main",
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
export function parseGitPolicyArg(raw) {
|
|
18
|
+
if (raw === undefined || raw.trim().length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
const t = raw.trim().toLowerCase();
|
|
21
|
+
if (t === "gitflow" || t === "develop-main")
|
|
22
|
+
return "gitflow";
|
|
23
|
+
if (t === "trunk" || t === "main")
|
|
24
|
+
return "trunk";
|
|
25
|
+
if (t === "custom")
|
|
26
|
+
return "custom";
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function resolvePreset(id) {
|
|
30
|
+
return GIT_POLICY_PRESETS.find((p) => p.id === id) ?? null;
|
|
31
|
+
}
|
|
32
|
+
export async function askGitPolicy() {
|
|
33
|
+
const preset = await select({
|
|
34
|
+
message: "Branch policy",
|
|
35
|
+
choices: [
|
|
36
|
+
...GIT_POLICY_PRESETS.map((p) => ({ name: p.label, value: p.id })),
|
|
37
|
+
{ name: "Custom branch names", value: "custom" },
|
|
38
|
+
],
|
|
39
|
+
loop: false,
|
|
40
|
+
});
|
|
41
|
+
if (preset !== "custom") {
|
|
42
|
+
const p = resolvePreset(preset);
|
|
43
|
+
return {
|
|
44
|
+
integrationBranch: p.integrationBranch,
|
|
45
|
+
productionBranch: p.productionBranch,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const integrationBranch = await askInput({
|
|
49
|
+
message: "Integration branch (task branches start here; MR/PR target)",
|
|
50
|
+
nonInteractive: false,
|
|
51
|
+
default: "develop",
|
|
52
|
+
required: true,
|
|
53
|
+
});
|
|
54
|
+
const productionBranch = await askInput({
|
|
55
|
+
message: "Production branch (releases / deploy; merge manually)",
|
|
56
|
+
nonInteractive: false,
|
|
57
|
+
default: "main",
|
|
58
|
+
required: true,
|
|
59
|
+
});
|
|
60
|
+
return { integrationBranch, productionBranch };
|
|
61
|
+
}
|
|
62
|
+
export function formatGitPolicySummary(git) {
|
|
63
|
+
if (git.integrationBranch === git.productionBranch) {
|
|
64
|
+
return `${git.integrationBranch} (integration + production)`;
|
|
65
|
+
}
|
|
66
|
+
return `integration=${git.integrationBranch}, production=${git.productionBranch}`;
|
|
67
|
+
}
|