@goodtek/vibeops 1.0.0 → 1.1.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 +31 -0
- package/README.md +51 -60
- package/dist/bootstrap/installer.js +38 -4
- package/dist/bootstrap/manifest.js +23 -3
- 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 +27 -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 +13 -11
- package/templates/.cursor/rules/02-git-safety.mdc +5 -4
- package/templates/AGENTS.md +2 -1
- package/templates/clients/claude/CLAUDE.md +28 -0
- package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
- package/templates/core/AGENTS.md +44 -0
- 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/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/init.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { basename, resolve } from "node:path";
|
|
2
2
|
import { install, printReport } from "../bootstrap/installer.js";
|
|
3
|
-
import { buildConfig } from "../lib/config.js";
|
|
4
|
-
import {
|
|
3
|
+
import { buildConfig, isVibeopsProject, readConfig, writeConfig } from "../lib/config.js";
|
|
4
|
+
import { gitAddAll, gitBranchExists, gitCheckout, gitCommit, gitCreateBranch, gitInit, gitPullFastForwardOnly, gitPush, gitRemoteBranchExists, gitRemoteUrl, gitSetDefaultBranch, gitStatusPorcelain, hasAnyCommit, isGitRepository, } from "../lib/git.js";
|
|
5
|
+
import { formatGitPolicySummary, askGitPolicy, parseGitPolicyArg, resolvePreset } from "../lib/git-policy.js";
|
|
6
|
+
import { ensureOriginRemote } from "../lib/git-remote.js";
|
|
7
|
+
import { askInitClients, formatClientsList, parseClientsArg, } from "../lib/init-clients.js";
|
|
5
8
|
import { askInput, askYesNo } from "../lib/inquirer-helpers.js";
|
|
6
9
|
import { cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
7
|
-
const DEFAULT_BRANCH = "main";
|
|
8
10
|
const DEFAULT_COMMIT_MESSAGE = "chore: initialize vibeops project";
|
|
9
11
|
function deriveProjectName(root, explicit) {
|
|
10
12
|
if (explicit && explicit.trim().length > 0)
|
|
@@ -14,124 +16,282 @@ function deriveProjectName(root, explicit) {
|
|
|
14
16
|
return base;
|
|
15
17
|
return "untitled-project";
|
|
16
18
|
}
|
|
19
|
+
function parseGitHostArg(raw) {
|
|
20
|
+
if (raw === undefined || raw.trim().length === 0)
|
|
21
|
+
return null;
|
|
22
|
+
const t = raw.trim().toLowerCase();
|
|
23
|
+
if (t === "github" || t === "gitlab")
|
|
24
|
+
return t;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
async function confirmReinitIfNeeded(projectRoot, options) {
|
|
28
|
+
const existing = await isVibeopsProject(projectRoot);
|
|
29
|
+
if (!existing)
|
|
30
|
+
return true;
|
|
31
|
+
if (options.dryRun) {
|
|
32
|
+
log.info(dim("(dry-run) would re-initialize existing VibeOps project templates"));
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (options.yes === true || options.force === true) {
|
|
36
|
+
log.warn("Re-initializing existing VibeOps project (templates will be overwritten).");
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (process.stdin.isTTY !== true) {
|
|
40
|
+
log.error("This directory is already a VibeOps project. Use --yes to re-run init, or --force.");
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
log.warn(yellow("This directory is already a VibeOps project."));
|
|
45
|
+
log.info(dim("Re-running init overwrites AGENTS.md, client rules/skills, and project doc stubs. docs/tasks/TASK-*.md are kept."));
|
|
46
|
+
log.blank();
|
|
47
|
+
const proceed = await askYesNo({
|
|
48
|
+
message: "Continue and overwrite template files?",
|
|
49
|
+
nonInteractive: false,
|
|
50
|
+
defaultValue: false,
|
|
51
|
+
});
|
|
52
|
+
if (!proceed) {
|
|
53
|
+
log.info(dim("Stopped — no files changed."));
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
async function resolveClients(options, interactive) {
|
|
60
|
+
const fromFlag = parseClientsArg(options.clients);
|
|
61
|
+
if (options.clients !== undefined && fromFlag === null) {
|
|
62
|
+
log.error('Invalid --clients. Use comma-separated: cursor, claude, codex (e.g. --clients cursor,claude)');
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (fromFlag !== null)
|
|
67
|
+
return fromFlag;
|
|
68
|
+
if (interactive) {
|
|
69
|
+
log.blank();
|
|
70
|
+
log.info("Agent clients");
|
|
71
|
+
return askInitClients();
|
|
72
|
+
}
|
|
73
|
+
if (options.dryRun) {
|
|
74
|
+
return ["cursor"];
|
|
75
|
+
}
|
|
76
|
+
log.error("Select at least one agent: --clients cursor,claude,codex (non-interactive mode).");
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
async function resolveBranchPolicy(options, interactive) {
|
|
81
|
+
const presetId = parseGitPolicyArg(options.gitPolicy);
|
|
82
|
+
if (options.gitPolicy !== undefined && presetId === null) {
|
|
83
|
+
log.error("Invalid --git-policy. Use: gitflow, trunk, or custom");
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (typeof options.integrationBranch === "string" &&
|
|
88
|
+
typeof options.productionBranch === "string") {
|
|
89
|
+
return {
|
|
90
|
+
integrationBranch: options.integrationBranch.trim(),
|
|
91
|
+
productionBranch: options.productionBranch.trim(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (presetId !== null && presetId !== "custom") {
|
|
95
|
+
const p = resolvePreset(presetId);
|
|
96
|
+
return {
|
|
97
|
+
integrationBranch: p.integrationBranch,
|
|
98
|
+
productionBranch: p.productionBranch,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (interactive) {
|
|
102
|
+
log.blank();
|
|
103
|
+
log.info("Branch policy");
|
|
104
|
+
return askGitPolicy();
|
|
105
|
+
}
|
|
106
|
+
if (options.dryRun) {
|
|
107
|
+
return { integrationBranch: "develop", productionBranch: "main" };
|
|
108
|
+
}
|
|
109
|
+
return { integrationBranch: "develop", productionBranch: "main" };
|
|
110
|
+
}
|
|
111
|
+
function printNextSteps(clients, git) {
|
|
112
|
+
log.blank();
|
|
113
|
+
log.info("Next steps:");
|
|
114
|
+
log.info(" 1. Read AGENTS.md");
|
|
115
|
+
if (clients.includes("cursor")) {
|
|
116
|
+
log.info(" 2. Cursor: @docs/tasks/TASK-001-….md in Ask, then Agent (+ /plan-task, /implement-task)");
|
|
117
|
+
}
|
|
118
|
+
if (clients.includes("claude")) {
|
|
119
|
+
log.info(" 2. Claude Code: open TASK file; use /plan-task then /implement-task");
|
|
120
|
+
}
|
|
121
|
+
if (clients.includes("codex")) {
|
|
122
|
+
log.info(" 2. Codex: open TASK file; use $plan-task / $implement-task when needed");
|
|
123
|
+
}
|
|
124
|
+
if (!clients.includes("cursor") && !clients.includes("claude") && !clients.includes("codex")) {
|
|
125
|
+
log.info(" 2. Open the current TASK file in your agent before coding");
|
|
126
|
+
}
|
|
127
|
+
log.info(` 3. ${cyan("vibeops llm connect")} — LLM for task add / task done`);
|
|
128
|
+
log.info(" 4. Push branches to origin (first time only):");
|
|
129
|
+
log.info(` ${dim(`git push -u ${git.remote} ${git.productionBranch}`)}`);
|
|
130
|
+
if (git.integrationBranch !== git.productionBranch) {
|
|
131
|
+
log.info(` ${dim(`git push -u ${git.remote} ${git.integrationBranch}`)}`);
|
|
132
|
+
}
|
|
133
|
+
log.info(` 5. ${cyan("vibeops task add")} — branches from ${git.integrationBranch} (pulls latest first)`);
|
|
134
|
+
log.info(` 6. ${cyan("vibeops task done")} — push + open MR/PR; merge on ${git.host} (CI deploys)`);
|
|
135
|
+
}
|
|
17
136
|
export async function initCommand(options = {}) {
|
|
18
137
|
const dryRun = options.dryRun === true;
|
|
19
|
-
const force = options.force === true;
|
|
20
138
|
const projectRoot = resolve(options.cwd ?? process.cwd());
|
|
21
|
-
const interactive = !dryRun &&
|
|
22
|
-
|
|
23
|
-
|
|
139
|
+
const interactive = !dryRun &&
|
|
140
|
+
process.stdin.isTTY === true &&
|
|
141
|
+
options.clients === undefined &&
|
|
142
|
+
options.gitPolicy === undefined;
|
|
143
|
+
const proceed = await confirmReinitIfNeeded(projectRoot, options);
|
|
144
|
+
if (!proceed)
|
|
145
|
+
return;
|
|
146
|
+
const existingConfig = await readConfig(projectRoot);
|
|
147
|
+
const reinit = existingConfig !== null || (await isVibeopsProject(projectRoot));
|
|
148
|
+
const force = options.force === true || reinit;
|
|
149
|
+
const clients = await resolveClients(options, interactive);
|
|
150
|
+
if (clients === null)
|
|
151
|
+
return;
|
|
152
|
+
const branchPolicy = await resolveBranchPolicy(options, interactive);
|
|
153
|
+
if (branchPolicy === null)
|
|
154
|
+
return;
|
|
155
|
+
const hostFromFlag = parseGitHostArg(options.gitHost);
|
|
156
|
+
if (options.gitHost !== undefined && hostFromFlag === null) {
|
|
157
|
+
log.error("Invalid --git-host. Use: github or gitlab");
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let gitConfig = {
|
|
162
|
+
remote: "origin",
|
|
163
|
+
host: hostFromFlag ?? existingConfig?.git?.host ?? "github",
|
|
164
|
+
integrationBranch: branchPolicy.integrationBranch,
|
|
165
|
+
productionBranch: branchPolicy.productionBranch,
|
|
166
|
+
};
|
|
167
|
+
const name = options.name?.trim() ||
|
|
168
|
+
existingConfig?.name ||
|
|
169
|
+
deriveProjectName(projectRoot, options.name);
|
|
170
|
+
const config = buildConfig(name, clients, gitConfig, existingConfig);
|
|
24
171
|
log.step(`vibeops init ${dryRun ? "(dry-run) " : ""}→ ${projectRoot}`);
|
|
25
172
|
log.info(` project: ${name}`);
|
|
26
173
|
log.info(` vibeops: ${config.vibeopsVersion}`);
|
|
27
|
-
|
|
28
|
-
|
|
174
|
+
log.info(` clients: ${formatClientsList(clients)}`);
|
|
175
|
+
log.info(` git: ${formatGitPolicySummary(gitConfig)}`);
|
|
176
|
+
if (force && reinit)
|
|
177
|
+
log.warn("Template files will be overwritten where they already exist.");
|
|
29
178
|
log.blank();
|
|
30
|
-
const report = await install({
|
|
179
|
+
const report = await install({
|
|
180
|
+
projectRoot,
|
|
181
|
+
config,
|
|
182
|
+
clients,
|
|
183
|
+
dryRun,
|
|
184
|
+
force,
|
|
185
|
+
});
|
|
31
186
|
printReport(report, dryRun);
|
|
32
|
-
const gitPlan = await resolveGitPlan(options, interactive);
|
|
33
|
-
await runGitSetup(projectRoot, gitPlan, dryRun);
|
|
34
|
-
if (!dryRun) {
|
|
35
|
-
log.blank();
|
|
36
|
-
log.info("Next steps:");
|
|
37
|
-
log.info(" 1. Read AGENTS.md and `.cursor/rules/`.");
|
|
38
|
-
log.info(" 2. Run `vibeops task add` to create the first TASK.");
|
|
39
|
-
log.info(" 3. Plan in Cursor Ask (@docs/tasks/TASK-001-….md), then implement.");
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
async function resolveGitPlan(options, interactive) {
|
|
43
|
-
const defaultBranch = typeof options.defaultBranch === "string" && options.defaultBranch.trim().length > 0
|
|
44
|
-
? options.defaultBranch.trim()
|
|
45
|
-
: DEFAULT_BRANCH;
|
|
46
187
|
const commitMessage = typeof options.commitMessage === "string" && options.commitMessage.trim().length > 0
|
|
47
188
|
? options.commitMessage.trim()
|
|
48
189
|
: DEFAULT_COMMIT_MESSAGE;
|
|
49
|
-
|
|
190
|
+
let shouldInitialCommit = options.initialCommit !== false &&
|
|
191
|
+
(options.initialCommit === true || options.git === true);
|
|
192
|
+
let finalCommitMessage = commitMessage;
|
|
193
|
+
if (interactive && !dryRun) {
|
|
50
194
|
log.blank();
|
|
51
|
-
log.info("Git
|
|
52
|
-
|
|
53
|
-
message: "Initialize Git repository?",
|
|
54
|
-
nonInteractive: false,
|
|
55
|
-
defaultValue: true,
|
|
56
|
-
});
|
|
57
|
-
if (!shouldInitGit) {
|
|
58
|
-
return {
|
|
59
|
-
shouldInitGit: false,
|
|
60
|
-
shouldSetDefaultBranch: false,
|
|
61
|
-
defaultBranch,
|
|
62
|
-
shouldInitialCommit: false,
|
|
63
|
-
commitMessage,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const useMain = await askYesNo({
|
|
67
|
-
message: "Use `main` as default branch?",
|
|
68
|
-
nonInteractive: false,
|
|
69
|
-
defaultValue: true,
|
|
70
|
-
});
|
|
71
|
-
const shouldInitialCommit = await askYesNo({
|
|
195
|
+
log.info("Git repository");
|
|
196
|
+
shouldInitialCommit = await askYesNo({
|
|
72
197
|
message: "Create initial commit?",
|
|
73
198
|
nonInteractive: false,
|
|
74
199
|
defaultValue: true,
|
|
75
200
|
});
|
|
76
|
-
|
|
77
|
-
|
|
201
|
+
if (shouldInitialCommit) {
|
|
202
|
+
finalCommitMessage = await askInput({
|
|
78
203
|
message: "Initial commit message",
|
|
79
204
|
nonInteractive: false,
|
|
80
205
|
default: commitMessage,
|
|
81
206
|
required: true,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
shouldInitGit: true,
|
|
86
|
-
shouldSetDefaultBranch: useMain,
|
|
87
|
-
defaultBranch,
|
|
88
|
-
shouldInitialCommit,
|
|
89
|
-
commitMessage: finalMessage.length > 0 ? finalMessage : commitMessage,
|
|
90
|
-
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
91
209
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
210
|
+
else if (!dryRun && !shouldInitialCommit && options.initialCommit === undefined) {
|
|
211
|
+
shouldInitialCommit = true;
|
|
212
|
+
}
|
|
213
|
+
await runGitSetup(projectRoot, {
|
|
214
|
+
productionBranch: gitConfig.productionBranch,
|
|
215
|
+
integrationBranch: gitConfig.integrationBranch,
|
|
216
|
+
shouldInitialCommit,
|
|
217
|
+
commitMessage: finalCommitMessage,
|
|
218
|
+
}, dryRun);
|
|
219
|
+
const hadRemoteBefore = (await gitRemoteUrl(projectRoot, gitConfig.remote)) !== null;
|
|
220
|
+
const remote = await ensureOriginRemote({
|
|
221
|
+
cwd: projectRoot,
|
|
222
|
+
remoteName: gitConfig.remote,
|
|
223
|
+
dryRun,
|
|
224
|
+
nonInteractive: !interactive,
|
|
225
|
+
allowMissing: options.allowNoRemote === true,
|
|
226
|
+
defaultBranch: gitConfig.productionBranch,
|
|
227
|
+
});
|
|
228
|
+
if (remote !== null) {
|
|
229
|
+
gitConfig = {
|
|
230
|
+
...gitConfig,
|
|
231
|
+
remote: remote.remote,
|
|
232
|
+
host: remote.host,
|
|
100
233
|
};
|
|
101
234
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
235
|
+
// If we created/configured a new remote during init, push baseline branches so
|
|
236
|
+
// `task add` can always pull latest integration before branching.
|
|
237
|
+
if (!dryRun && options.allowNoRemote !== true && !hadRemoteBefore) {
|
|
238
|
+
const remoteName = gitConfig.remote;
|
|
239
|
+
log.blank();
|
|
240
|
+
log.info("Remote bootstrap:");
|
|
241
|
+
try {
|
|
242
|
+
// Ensure local branches are up-to-date with origin if they already exist remotely.
|
|
243
|
+
const prodRemoteExists = await gitRemoteBranchExists(projectRoot, remoteName, gitConfig.productionBranch);
|
|
244
|
+
if (prodRemoteExists) {
|
|
245
|
+
await gitCheckout(projectRoot, gitConfig.productionBranch);
|
|
246
|
+
await gitPullFastForwardOnly(projectRoot, remoteName, gitConfig.productionBranch);
|
|
247
|
+
}
|
|
248
|
+
await gitCheckout(projectRoot, gitConfig.productionBranch);
|
|
249
|
+
await gitPush(projectRoot, remoteName, gitConfig.productionBranch, true);
|
|
250
|
+
log.ok(`pushed ${gitConfig.productionBranch} → ${remoteName}`);
|
|
251
|
+
if (gitConfig.integrationBranch !== gitConfig.productionBranch) {
|
|
252
|
+
const intRemoteExists = await gitRemoteBranchExists(projectRoot, remoteName, gitConfig.integrationBranch);
|
|
253
|
+
if (intRemoteExists) {
|
|
254
|
+
await gitCheckout(projectRoot, gitConfig.integrationBranch);
|
|
255
|
+
await gitPullFastForwardOnly(projectRoot, remoteName, gitConfig.integrationBranch);
|
|
256
|
+
}
|
|
257
|
+
await gitCheckout(projectRoot, gitConfig.integrationBranch);
|
|
258
|
+
await gitPush(projectRoot, remoteName, gitConfig.integrationBranch, true);
|
|
259
|
+
log.ok(`pushed ${gitConfig.integrationBranch} → ${remoteName}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
264
|
+
log.warn(`Automatic push failed: ${msg}`);
|
|
265
|
+
log.info(dim("Run these manually:"));
|
|
266
|
+
log.info(dim(` git push -u ${remoteName} ${gitConfig.productionBranch}`));
|
|
267
|
+
if (gitConfig.integrationBranch !== gitConfig.productionBranch) {
|
|
268
|
+
log.info(dim(` git push -u ${remoteName} ${gitConfig.integrationBranch}`));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!dryRun) {
|
|
273
|
+
await writeConfig(projectRoot, buildConfig(name, clients, gitConfig, existingConfig));
|
|
274
|
+
if (options.allowNoRemote === true && remote === null) {
|
|
275
|
+
log.warn("No git remote configured (--allow-no-remote). task done push/MR will fail until origin exists.");
|
|
276
|
+
}
|
|
277
|
+
printNextSteps(clients, gitConfig);
|
|
278
|
+
}
|
|
109
279
|
}
|
|
110
280
|
async function runGitSetup(projectRoot, plan, dryRun) {
|
|
111
281
|
log.blank();
|
|
112
282
|
log.info("Git setup:");
|
|
113
|
-
if (!plan.shouldInitGit) {
|
|
114
|
-
log.info(` ${dim("skipped")} Git initialization`);
|
|
115
|
-
if (dryRun) {
|
|
116
|
-
log.info(` ${dim("hint")} use ${cyan("vibeops init --git --initial-commit")} to include Git setup in non-interactive mode`);
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
283
|
const alreadyRepo = await isGitRepository(projectRoot);
|
|
121
284
|
const hasCommitsBefore = alreadyRepo ? await hasAnyCommit(projectRoot) : false;
|
|
122
285
|
if (dryRun) {
|
|
123
286
|
log.info(` ${alreadyRepo ? dim("would skip") : green("would run")} git init`);
|
|
124
|
-
|
|
125
|
-
|
|
287
|
+
log.info(` ${green("would set")} production branch ${cyan(plan.productionBranch)}`);
|
|
288
|
+
if (plan.integrationBranch !== plan.productionBranch) {
|
|
289
|
+
log.info(` ${green("would create")} integration branch ${cyan(plan.integrationBranch)}`);
|
|
126
290
|
}
|
|
127
291
|
if (plan.shouldInitialCommit) {
|
|
128
|
-
log.info(` ${green("would run")} git add
|
|
129
|
-
log.info(` ${green("would run")} git commit -m ${JSON.stringify(plan.commitMessage)}`);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
log.info(` ${dim("would skip")} initial commit`);
|
|
292
|
+
log.info(` ${green("would run")} git add . && git commit`);
|
|
133
293
|
}
|
|
134
|
-
log.info(` ${dim("
|
|
294
|
+
log.info(` ${dim("would require")} remote origin (unless --allow-no-remote)`);
|
|
135
295
|
return;
|
|
136
296
|
}
|
|
137
297
|
if (alreadyRepo) {
|
|
@@ -141,39 +301,44 @@ async function runGitSetup(projectRoot, plan, dryRun) {
|
|
|
141
301
|
await gitInit(projectRoot);
|
|
142
302
|
log.ok("git init");
|
|
143
303
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
304
|
+
const hasCommitsNow = await hasAnyCommit(projectRoot);
|
|
305
|
+
if (!hasCommitsNow) {
|
|
306
|
+
await gitSetDefaultBranch(projectRoot, plan.productionBranch);
|
|
307
|
+
log.ok(`production branch ${plan.productionBranch}`);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
log.info(` ${yellow("skipped")} default branch change (repository already has commits)`);
|
|
311
|
+
}
|
|
312
|
+
if (plan.shouldInitialCommit && !hasCommitsBefore && !hasCommitsNow) {
|
|
313
|
+
const changedFiles = await gitStatusPorcelain(projectRoot);
|
|
314
|
+
if (changedFiles.length > 0) {
|
|
315
|
+
await gitAddAll(projectRoot);
|
|
316
|
+
await gitCommit(projectRoot, plan.commitMessage);
|
|
317
|
+
log.ok(`initial commit on ${plan.productionBranch}`);
|
|
148
318
|
}
|
|
149
319
|
else {
|
|
150
|
-
|
|
151
|
-
log.ok(`default branch ${plan.defaultBranch}`);
|
|
320
|
+
log.info(` ${dim("skipped")} initial commit (nothing to commit)`);
|
|
152
321
|
}
|
|
153
322
|
}
|
|
154
|
-
if (
|
|
155
|
-
log.info(` ${dim("skipped")} initial commit`);
|
|
156
|
-
return;
|
|
323
|
+
else if (hasCommitsNow || hasCommitsBefore) {
|
|
324
|
+
log.info(` ${dim("skipped")} initial commit (repository already has commits)`);
|
|
157
325
|
}
|
|
158
|
-
|
|
159
|
-
|
|
326
|
+
await ensureIntegrationBranch(projectRoot, plan);
|
|
327
|
+
}
|
|
328
|
+
/** Create integration branch when missing (including re-init on existing repos). */
|
|
329
|
+
async function ensureIntegrationBranch(projectRoot, plan) {
|
|
330
|
+
if (plan.integrationBranch === plan.productionBranch)
|
|
160
331
|
return;
|
|
161
|
-
|
|
162
|
-
const changedFiles = await gitStatusPorcelain(projectRoot);
|
|
163
|
-
log.info(` ${dim("initial commit files")} ${changedFiles.length} file${changedFiles.length === 1 ? "" : "s"} will be included`);
|
|
164
|
-
if (changedFiles.length === 0) {
|
|
165
|
-
log.info(` ${dim("skipped")} initial commit (nothing to commit)`);
|
|
332
|
+
if (await gitBranchExists(projectRoot, plan.integrationBranch))
|
|
166
333
|
return;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
log.info(` ${dim("hint")} check Git user.name/user.email, then run ${cyan(`git commit -m ${JSON.stringify(plan.commitMessage)}`)} manually.`);
|
|
177
|
-
process.exitCode = 1;
|
|
334
|
+
if (!(await hasAnyCommit(projectRoot)))
|
|
335
|
+
return;
|
|
336
|
+
const start = (await gitBranchExists(projectRoot, plan.productionBranch))
|
|
337
|
+
? plan.productionBranch
|
|
338
|
+
: "HEAD";
|
|
339
|
+
await gitCreateBranch(projectRoot, plan.integrationBranch, start);
|
|
340
|
+
log.ok(`integration branch ${plan.integrationBranch}`);
|
|
341
|
+
if (await gitBranchExists(projectRoot, plan.productionBranch)) {
|
|
342
|
+
await gitCheckout(projectRoot, plan.productionBranch);
|
|
178
343
|
}
|
|
179
344
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { password, select } from "@inquirer/prompts";
|
|
3
|
+
import { readConfig, setLlmProviderPreference } from "../lib/config.js";
|
|
4
|
+
import { configureOpenAiKey, guideCodexOAuth, printConnectSetupHint, runCursorAgentLogin, } from "../lib/llm-connect.js";
|
|
5
|
+
import { getLlmPreferenceFromConfig, labelLlmProvider, parseLlmProviderPreference, probeAllLlmProviders, resolveLlmProviderForUse, } from "../lib/llm-preference.js";
|
|
6
|
+
import { buildLlmStatusReport, formatLlmStatusLine } from "../lib/llm-status.js";
|
|
7
|
+
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
8
|
+
async function requireProject(cwd) {
|
|
9
|
+
const c = await readConfig(cwd);
|
|
10
|
+
if (c === null) {
|
|
11
|
+
log.error("Not a VibeOps project — run vibeops init first.");
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
async function printLlmStatusHuman(cwd) {
|
|
18
|
+
const report = await buildLlmStatusReport(cwd);
|
|
19
|
+
const { preferenceLabel, activeLabel } = formatLlmStatusLine(report);
|
|
20
|
+
log.blank();
|
|
21
|
+
log.info(bold(" LLM"));
|
|
22
|
+
log.info(` ${dim("Preferred")} ${preferenceLabel}`);
|
|
23
|
+
log.info(` ${dim("Active")} ${report.active !== null ? green(activeLabel) : yellow(activeLabel)}`);
|
|
24
|
+
for (const p of report.providers) {
|
|
25
|
+
const mark = p.ok ? green("ok") : dim("—");
|
|
26
|
+
log.info(` ${dim(p.id.padEnd(14))} ${mark} ${dim(p.summary)}`);
|
|
27
|
+
}
|
|
28
|
+
if (!report.anyAvailable) {
|
|
29
|
+
log.info(` ${dim("Hint")} Run ${cyan("vibeops llm connect")}`);
|
|
30
|
+
}
|
|
31
|
+
log.blank();
|
|
32
|
+
}
|
|
33
|
+
export async function llmStatusCommand(opts = {}) {
|
|
34
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
35
|
+
if (!(await requireProject(cwd)))
|
|
36
|
+
return;
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
const report = await buildLlmStatusReport(cwd);
|
|
39
|
+
log.raw(JSON.stringify(report, null, 2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await printLlmStatusHuman(cwd);
|
|
43
|
+
}
|
|
44
|
+
export async function llmUseCommand(providerArg, opts = {}) {
|
|
45
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
46
|
+
if (!(await requireProject(cwd)))
|
|
47
|
+
return;
|
|
48
|
+
const nonInteractive = opts.nonInteractive === true || process.stdin.isTTY !== true;
|
|
49
|
+
let preference = null;
|
|
50
|
+
if (providerArg !== undefined) {
|
|
51
|
+
preference = parseLlmProviderPreference(providerArg);
|
|
52
|
+
if (preference === null) {
|
|
53
|
+
log.error(`Unknown provider "${providerArg}". Use auto, codex-oauth, cursor-agent, or openai.`);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (nonInteractive) {
|
|
59
|
+
log.error("Provider required in non-interactive mode: vibeops llm use <provider>");
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const probes = await probeAllLlmProviders(cwd);
|
|
65
|
+
const choices = [
|
|
66
|
+
{ name: labelLlmProvider("auto"), value: "auto" },
|
|
67
|
+
...probes.map((p) => ({
|
|
68
|
+
name: `${labelLlmProvider(p.id)}${p.ok ? "" : " (not connected)"}`,
|
|
69
|
+
value: p.id,
|
|
70
|
+
disabled: p.ok ? undefined : "Run vibeops llm connect first",
|
|
71
|
+
})),
|
|
72
|
+
];
|
|
73
|
+
preference = await select({
|
|
74
|
+
message: "Default LLM for task add / task done",
|
|
75
|
+
choices,
|
|
76
|
+
loop: false,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
await setLlmProviderPreference(cwd, preference);
|
|
80
|
+
log.ok(`LLM preference → ${labelLlmProvider(preference)}`);
|
|
81
|
+
const active = await resolveLlmProviderForUse(cwd, preference);
|
|
82
|
+
if (active === null && preference !== "auto") {
|
|
83
|
+
log.warn(`"${preference}" is not connected yet. Run ${cyan("vibeops llm connect")}.`);
|
|
84
|
+
printConnectSetupHint(preference);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
}
|
|
87
|
+
else if (active !== null) {
|
|
88
|
+
log.info(` Active now: ${green(labelLlmProvider(active))}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function llmConnectCommand(opts = {}) {
|
|
92
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
93
|
+
if (!(await requireProject(cwd)))
|
|
94
|
+
return;
|
|
95
|
+
const nonInteractive = opts.nonInteractive === true || process.stdin.isTTY !== true;
|
|
96
|
+
if (nonInteractive) {
|
|
97
|
+
log.error("llm connect requires an interactive terminal.");
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
log.info(bold("vibeops llm connect"));
|
|
102
|
+
log.info(dim("Set up providers, then pick which one task add / task done use."));
|
|
103
|
+
log.blank();
|
|
104
|
+
let done = false;
|
|
105
|
+
while (!done) {
|
|
106
|
+
const probes = await probeAllLlmProviders(cwd);
|
|
107
|
+
const config = await readConfig(cwd);
|
|
108
|
+
const pref = getLlmPreferenceFromConfig(config);
|
|
109
|
+
const active = await resolveLlmProviderForUse(cwd, pref);
|
|
110
|
+
log.info(bold(" Connected providers"));
|
|
111
|
+
for (const p of probes) {
|
|
112
|
+
log.info(` ${p.ok ? green("✓") : dim("·")} ${p.id} — ${dim(p.summary)}`);
|
|
113
|
+
}
|
|
114
|
+
log.info(` ${dim("Preferred")} ${labelLlmProvider(pref)} · ${dim("Active")} ${active ? labelLlmProvider(active) : yellow("none")}`);
|
|
115
|
+
log.blank();
|
|
116
|
+
const action = await select({
|
|
117
|
+
message: "LLM setup",
|
|
118
|
+
choices: [
|
|
119
|
+
{ name: "Choose default provider (task add / task done)", value: "pick" },
|
|
120
|
+
{ name: "Set up Cursor Agent CLI (`agent login`)", value: "setup-cursor" },
|
|
121
|
+
{ name: "Show Codex OAuth setup (`codex login`)", value: "setup-codex" },
|
|
122
|
+
{ name: "Set up OpenAI API key (.vibeops.env)", value: "setup-openai" },
|
|
123
|
+
{ name: "Done", value: "done" },
|
|
124
|
+
],
|
|
125
|
+
loop: false,
|
|
126
|
+
});
|
|
127
|
+
switch (action) {
|
|
128
|
+
case "pick": {
|
|
129
|
+
await llmUseCommand(undefined, { cwd });
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "setup-cursor": {
|
|
133
|
+
const result = await runCursorAgentLogin(cwd);
|
|
134
|
+
if (result.ok)
|
|
135
|
+
log.ok(result.message);
|
|
136
|
+
else {
|
|
137
|
+
log.warn(result.message);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
}
|
|
140
|
+
log.blank();
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "setup-codex": {
|
|
144
|
+
const result = await guideCodexOAuth();
|
|
145
|
+
if (result.ok)
|
|
146
|
+
log.ok(result.message);
|
|
147
|
+
else {
|
|
148
|
+
for (const line of result.message.split("\n"))
|
|
149
|
+
log.info(line);
|
|
150
|
+
}
|
|
151
|
+
log.blank();
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "setup-openai": {
|
|
155
|
+
const key = await password({
|
|
156
|
+
message: "OpenAI API key (stored in .vibeops.env, gitignored)",
|
|
157
|
+
mask: "*",
|
|
158
|
+
validate: (v) => (v.trim().length > 0 ? true : "Key is required"),
|
|
159
|
+
});
|
|
160
|
+
const result = await configureOpenAiKey(cwd, key);
|
|
161
|
+
if (result.ok)
|
|
162
|
+
log.ok(result.message);
|
|
163
|
+
else
|
|
164
|
+
log.warn(result.message);
|
|
165
|
+
log.blank();
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case "done":
|
|
169
|
+
done = true;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const report = await buildLlmStatusReport(cwd);
|
|
174
|
+
const { activeLabel } = formatLlmStatusLine(report);
|
|
175
|
+
log.info(`Saved preference: ${labelLlmProvider(report.preference)} · Active: ${activeLabel}`);
|
|
176
|
+
log.info(`Check anytime: ${cyan("vibeops llm status")} or ${cyan("vibeops status")}`);
|
|
177
|
+
}
|