@goodtek/vibeops 0.7.22 → 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 +51 -0
- package/README.md +51 -380
- package/dist/bootstrap/installer.js +42 -20
- package/dist/bootstrap/manifest.js +23 -3
- package/dist/cli.js +69 -154
- package/dist/commands/init.js +279 -114
- package/dist/commands/llm.js +177 -0
- package/dist/commands/status.js +181 -8
- package/dist/commands/task-add.js +89 -162
- package/dist/commands/task-done.js +112 -73
- package/dist/lib/config.js +77 -113
- 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 +44 -1
- 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/paths.js +7 -3
- package/dist/lib/plan-llm-openai.js +0 -63
- package/dist/lib/pr-create.js +66 -0
- package/dist/lib/resolve-task.js +34 -0
- package/dist/lib/task-add-llm.js +53 -233
- package/dist/lib/task-context.js +68 -0
- package/dist/lib/task-done-llm.js +155 -0
- package/dist/lib/task-pr-llm.js +50 -0
- package/dist/lib/task-push-pr.js +131 -0
- package/dist/lib/task-scaffold.js +39 -43
- package/dist/lib/task-start.js +100 -0
- package/dist/lib/task-summary.js +12 -239
- package/dist/lib/task.js +30 -24
- package/dist/types/config.js +0 -16
- package/package.json +14 -14
- package/templates/.cursor/rules/02-git-safety.mdc +12 -0
- package/templates/AGENTS.md +27 -87
- package/templates/clients/claude/CLAUDE.md +28 -0
- package/templates/clients/cursor/.cursor/rules/01-task-source-of-truth.mdc +12 -0
- package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
- package/templates/clients/cursor/.cursor/rules/03-docs-on-done.mdc +13 -0
- package/templates/core/AGENTS.md +44 -0
- package/templates/core/docs/logs/README.md +7 -0
- package/templates/core/docs/project/03-architecture.md +15 -0
- package/templates/core/docs/project/05-current-state.md +15 -0
- package/templates/core/docs/project/06-decisions.md +9 -0
- package/templates/core/docs/tasks/TASK-000-template.md +37 -0
- package/templates/skills-shared/implement-task/SKILL.md +22 -0
- package/templates/skills-shared/plan-task/SKILL.md +19 -0
- package/dist/agent/loader.js +0 -71
- package/dist/agent/prompt.js +0 -66
- package/dist/commands/agent-list.js +0 -32
- package/dist/commands/agent-prompt.js +0 -59
- package/dist/commands/agent-show.js +0 -26
- package/dist/commands/done.js +0 -160
- package/dist/commands/github-init.js +0 -554
- package/dist/commands/github-status.js +0 -164
- package/dist/commands/next.js +0 -283
- package/dist/commands/notion-init.js +0 -764
- package/dist/commands/notion-sync.js +0 -405
- package/dist/commands/notion-test.js +0 -595
- package/dist/commands/plan.js +0 -146
- package/dist/commands/rollback.js +0 -126
- package/dist/commands/start.js +0 -129
- package/dist/commands/task-check.js +0 -155
- package/dist/commands/task-generate.js +0 -206
- package/dist/commands/task-pull.js +0 -277
- package/dist/commands/task-rollback.js +0 -174
- package/dist/commands/task-start.js +0 -102
- package/dist/commands/task-status.js +0 -122
- package/dist/lib/brief.js +0 -356
- package/dist/lib/cursor-task-prompt.js +0 -60
- package/dist/lib/done-follow-up.js +0 -308
- package/dist/lib/github-cli.js +0 -247
- package/dist/lib/guide-state.js +0 -69
- package/dist/lib/mvp-artifacts.js +0 -204
- package/dist/lib/mvp-constants.js +0 -15
- package/dist/lib/mvp-diff-summary.js +0 -43
- package/dist/lib/mvp-done-summary.js +0 -335
- package/dist/lib/notion-client.js +0 -459
- package/dist/lib/notion-discovery.js +0 -671
- package/dist/lib/notion-env.js +0 -140
- package/dist/lib/notion-mappers.js +0 -148
- package/dist/lib/notion-schema.js +0 -272
- package/dist/lib/notion-sync.js +0 -337
- package/dist/lib/notion-target.js +0 -247
- package/dist/lib/package-json.js +0 -133
- package/dist/lib/plan-apply-planner.js +0 -131
- package/dist/lib/plan-codex-pkce.js +0 -234
- package/dist/lib/plan-llm-models.js +0 -237
- package/dist/lib/plan-llm-normalize.js +0 -146
- package/dist/lib/plan-llm-session.js +0 -682
- package/dist/lib/plan-post-apply-setup.js +0 -116
- package/dist/lib/project-docs.js +0 -95
- package/dist/lib/prompt-builder.js +0 -135
- package/dist/lib/resolve-command-task.js +0 -40
- package/dist/lib/task-add-build-prompt.js +0 -39
- package/dist/lib/task-add-cursor.js +0 -81
- package/dist/lib/task-generator.js +0 -183
- package/dist/lib/task-merge.js +0 -220
- package/dist/lib/task-prompt.js +0 -23
- package/dist/lib/task-pull.js +0 -354
- package/dist/lib/task-status-brief.js +0 -132
- package/dist/lib/workflow-guide.js +0 -485
- package/dist/status/collector.js +0 -103
- package/dist/status/format.js +0 -181
- package/dist/types/brief.js +0 -126
- package/templates/.cursor/rules/00-project-governance.mdc +0 -28
- package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
- package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
- package/templates/.cursor/rules/03-git-safety.mdc +0 -30
- package/templates/.cursor/rules/04-docs-update.mdc +0 -22
- package/templates/.vibeops/agents/architect.md +0 -47
- package/templates/.vibeops/agents/builder.md +0 -38
- package/templates/.vibeops/agents/docs.md +0 -54
- package/templates/.vibeops/agents/orchestrator.md +0 -40
- package/templates/.vibeops/agents/planner.md +0 -60
- package/templates/.vibeops/agents/recovery.md +0 -49
- package/templates/.vibeops/agents/reviewer.md +0 -47
- package/templates/.vibeops/agents/tester.md +0 -43
- package/templates/.vibeops/prompts/create-plan.md +0 -33
- package/templates/.vibeops/prompts/generate-tasks.md +0 -41
- package/templates/.vibeops/prompts/implement-task.md +0 -39
- package/templates/.vibeops/prompts/review-task.md +0 -34
- package/templates/.vibeops/prompts/rollback.md +0 -32
- package/templates/.vibeops/prompts/start-project.md +0 -39
- package/templates/.vibeops/workflows/notion-sync.md +0 -53
- package/templates/.vibeops/workflows/project-start.md +0 -73
- package/templates/.vibeops/workflows/rollback.md +0 -45
- package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
- package/templates/docs/logs/README.md +0 -38
- package/templates/docs/project/00-overview.md +0 -27
- package/templates/docs/project/01-requirements.md +0 -30
- package/templates/docs/project/02-mvp-scope.md +0 -36
- package/templates/docs/project/03-architecture.md +0 -34
- package/templates/docs/project/04-tech-stack.md +0 -29
- package/templates/docs/project/05-current-state.md +0 -35
- package/templates/docs/project/06-decisions.md +0 -20
- package/templates/docs/project/07-backlog.md +0 -23
- package/templates/docs/project/08-env.md +0 -29
- package/templates/docs/project/09-deployment.md +0 -28
- package/templates/docs/tasks/TASK-000-template.md +0 -72
|
@@ -1,98 +1,137 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { readText } from "../lib/filesystem.js";
|
|
3
3
|
import { bold, cyan, dim, green, log, yellow } from "../lib/logger.js";
|
|
4
4
|
import { projectPaths } from "../lib/paths.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { resolveTask, taskNotFoundMessage } from "../lib/resolve-task.js";
|
|
6
|
+
import { applyTaskDoneMemory, fallbackResultSections, llmCompleteTaskDone, writeTaskResultSections, } from "../lib/task-done-llm.js";
|
|
7
|
+
import { relPath } from "../lib/task-context.js";
|
|
8
|
+
import { gitAddPaths, listWorkingTreeRelPaths, partitionPathsForAutoCommit, readGitInfo, runGit, } from "../lib/git.js";
|
|
9
|
+
import { hasNonEmptySection, markGitContextDone, parseTaskFilename, readGitContext, readTaskFile, updateInlineStatus, } from "../lib/task.js";
|
|
10
|
+
import { finishTaskWithPullRequest } from "../lib/task-push-pr.js";
|
|
11
|
+
function commitMessageFor(taskId, title) {
|
|
12
|
+
const slug = title.replace(/^TASK-\d+\s*[:\-]\s*/i, "").trim() || taskId;
|
|
13
|
+
return `feat(${taskId.toLowerCase()}): ${slug}`;
|
|
9
14
|
}
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
15
|
+
async function tryCommitDirty(cwd, message, dryRun) {
|
|
16
|
+
const git = await readGitInfo(cwd);
|
|
17
|
+
if (!git.isRepo || git.dirty !== true)
|
|
18
|
+
return false;
|
|
19
|
+
const { committable, excluded } = partitionPathsForAutoCommit(await listWorkingTreeRelPaths(cwd), { unmerged: [] });
|
|
20
|
+
if (committable.length === 0)
|
|
21
|
+
return false;
|
|
22
|
+
if (excluded.length > 0) {
|
|
23
|
+
log.warn(`Skipping ${excluded.length} build artifact path(s) from auto-commit.`);
|
|
24
|
+
}
|
|
25
|
+
if (dryRun) {
|
|
26
|
+
log.info(dim(` would commit ${committable.length} path(s)`));
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
await gitAddPaths(cwd, committable);
|
|
30
|
+
await runGit(cwd, ["commit", "-q", "-m", message]);
|
|
31
|
+
log.ok(`Committed: ${message}`);
|
|
32
|
+
return true;
|
|
16
33
|
}
|
|
17
|
-
export async function taskDoneCommand(
|
|
34
|
+
export async function taskDoneCommand(taskRef, options = {}) {
|
|
18
35
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
19
36
|
const paths = projectPaths(cwd);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
const dryRun = options.dryRun === true;
|
|
38
|
+
const resolved = await resolveTask(paths, cwd, taskRef);
|
|
39
|
+
if (resolved === null) {
|
|
40
|
+
log.error(taskNotFoundMessage(taskRef));
|
|
23
41
|
process.exitCode = 1;
|
|
24
42
|
return;
|
|
25
43
|
}
|
|
44
|
+
const { taskId, taskFile } = resolved;
|
|
26
45
|
const meta = await readTaskFile(taskFile);
|
|
27
46
|
const body = await readText(taskFile);
|
|
28
|
-
|
|
29
|
-
log.info(`
|
|
30
|
-
log.info(` ${dim("
|
|
31
|
-
log.info(` ${dim("status")} ${statusDisplay(meta.status)}`);
|
|
47
|
+
const relFile = relPath(cwd, taskFile);
|
|
48
|
+
log.info(bold(`vibeops task done ${taskId}`));
|
|
49
|
+
log.info(` ${dim("file")} ${relFile}`);
|
|
32
50
|
log.blank();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
let resultOk = hasNonEmptySection(body, "Result");
|
|
52
|
+
let testOk = hasNonEmptySection(body, "Test Result");
|
|
53
|
+
if (!resultOk || !testOk) {
|
|
54
|
+
log.step("LLM — Result / Test Result + project memory");
|
|
55
|
+
if (dryRun) {
|
|
56
|
+
log.info(dim(" would call LLM and patch docs/project/*"));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const patch = await llmCompleteTaskDone({
|
|
60
|
+
cwd,
|
|
61
|
+
taskId,
|
|
62
|
+
taskTitle: meta.title,
|
|
63
|
+
taskBody: body,
|
|
64
|
+
taskFileRel: relFile,
|
|
65
|
+
});
|
|
66
|
+
if (patch !== null) {
|
|
67
|
+
await writeTaskResultSections(taskFile, patch.result, patch.testResult);
|
|
68
|
+
log.ok(`Result / Test Result updated (${patch.provider})`);
|
|
69
|
+
const memory = await applyTaskDoneMemory(cwd, patch);
|
|
70
|
+
for (const p of memory.updated)
|
|
71
|
+
log.ok(`Updated ${p}`);
|
|
72
|
+
for (const p of memory.skipped)
|
|
73
|
+
log.skip(`Skipped ${p}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
log.warn("LLM unavailable — using git diff fallback for Result sections.");
|
|
77
|
+
const fb = await fallbackResultSections(cwd, taskId, taskFile);
|
|
78
|
+
await writeTaskResultSections(taskFile, fb.result, fb.testResult);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const body2 = dryRun ? body : await readText(taskFile);
|
|
82
|
+
resultOk = hasNonEmptySection(body2, "Result");
|
|
83
|
+
testOk = hasNonEmptySection(body2, "Test Result");
|
|
45
84
|
}
|
|
85
|
+
log.info(bold("Sections"));
|
|
86
|
+
log.info(` ${resultOk ? green("✓") : yellow("·")} Result`);
|
|
87
|
+
log.info(` ${testOk ? green("✓") : yellow("·")} Test Result`);
|
|
46
88
|
log.blank();
|
|
47
|
-
if (
|
|
48
|
-
log.error(
|
|
89
|
+
if (!dryRun && (!resultOk || !testOk)) {
|
|
90
|
+
log.error("Fill Result and Test Result in the TASK file, then rerun.");
|
|
49
91
|
process.exitCode = 1;
|
|
50
92
|
return;
|
|
51
93
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
log.info(
|
|
58
|
-
log.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
if (dryRun) {
|
|
95
|
+
log.info(bold("dry-run — would:"));
|
|
96
|
+
if (!resultOk || !testOk) {
|
|
97
|
+
log.info(" · LLM fill Result / Test Result + patch docs/project/*");
|
|
98
|
+
}
|
|
99
|
+
log.info(" · commit if dirty, push task branch, open MR/PR (unless --no-pr)");
|
|
100
|
+
log.info(" · Status → Done after push/MR succeed");
|
|
101
|
+
await finishTaskWithPullRequest({
|
|
102
|
+
cwd,
|
|
103
|
+
taskFile,
|
|
104
|
+
dryRun: true,
|
|
105
|
+
skipPr: options.noPr === true,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
64
108
|
}
|
|
65
|
-
const ctx = await readGitContext(taskFile);
|
|
66
109
|
const parts = parseTaskFilename(taskFile);
|
|
67
|
-
const commitMsg = commitMessageFor(parts.id, meta.title
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
110
|
+
const commitMsg = commitMessageFor(parts.id, meta.title);
|
|
111
|
+
await tryCommitDirty(cwd, commitMsg, false);
|
|
112
|
+
const gitCtx = await readGitContext(taskFile);
|
|
113
|
+
if (gitCtx !== null) {
|
|
114
|
+
const prResult = await finishTaskWithPullRequest({
|
|
115
|
+
cwd,
|
|
116
|
+
taskFile,
|
|
117
|
+
skipPr: options.noPr === true,
|
|
118
|
+
});
|
|
119
|
+
if (!prResult.ok) {
|
|
120
|
+
log.error("TASK left In Progress — fix push/MR, then rerun task done.");
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (prResult.mergeRequestUrl) {
|
|
125
|
+
log.info(` ${dim("MR/PR")} ${prResult.mergeRequestUrl}`);
|
|
126
|
+
log.info(dim("Merge on GitHub/GitLab — CI deploys after merge."));
|
|
127
|
+
}
|
|
72
128
|
}
|
|
73
129
|
else {
|
|
74
|
-
log.info(
|
|
130
|
+
log.info(dim("No Git Context — push/MR skipped."));
|
|
75
131
|
}
|
|
132
|
+
await updateInlineStatus(taskFile, "done");
|
|
133
|
+
await markGitContextDone(taskFile);
|
|
134
|
+
log.ok("Status → Done");
|
|
76
135
|
log.blank();
|
|
77
|
-
log.info(
|
|
78
|
-
log.info(` ${dim("TODO: run `vibeops notion sync` to push this TASK metadata.")}`);
|
|
79
|
-
log.blank();
|
|
80
|
-
const all = await scanTasks(paths.docsTasks);
|
|
81
|
-
const next = pickNextTask(all.filter((t) => t.id.toUpperCase() !== meta.id.toUpperCase()));
|
|
82
|
-
log.info(bold("Next TASK candidate"));
|
|
83
|
-
if (next) {
|
|
84
|
-
log.info(` → ${cyan(next.id)} — ${next.title || dim("(no title)")} ${dim(`[${statusDisplay(next.status)}]`)}`);
|
|
85
|
-
log.info(` file: ${dim(relOrAbs(cwd, next.filePath))}`);
|
|
86
|
-
log.info(` start: ${dim(`vibeops task start ${next.id}`)}`);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
log.info(` ${dim("(no remaining planned / in-progress / review TASK)")}`);
|
|
90
|
-
}
|
|
91
|
-
log.blank();
|
|
92
|
-
if (target === "review") {
|
|
93
|
-
log.info(`Status moved to ${cyan("Review")}. After human or Reviewer Agent review, finalize with ${cyan(`vibeops task done ${meta.id} --finalize`)}.`);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
log.ok(`${meta.id} finalized → Done.`);
|
|
97
|
-
}
|
|
136
|
+
log.info(`Next: ${cyan("vibeops task add")} or ${cyan("vibeops status")}`);
|
|
98
137
|
}
|
package/dist/lib/config.js
CHANGED
|
@@ -1,39 +1,56 @@
|
|
|
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 {
|
|
6
|
-
|
|
7
|
-
|
|
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))
|
|
8
9
|
return undefined;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
17
|
-
return out;
|
|
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;
|
|
18
17
|
}
|
|
19
|
-
function
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
22
|
-
return "";
|
|
18
|
+
export function getClientsFromConfig(config) {
|
|
19
|
+
if (config?.clients && config.clients.length > 0)
|
|
20
|
+
return [...config.clients];
|
|
21
|
+
return ["cursor"];
|
|
23
22
|
}
|
|
24
|
-
function
|
|
25
|
-
if (
|
|
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)) {
|
|
26
30
|
return undefined;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
|
37
54
|
}
|
|
38
55
|
export async function readConfig(root) {
|
|
39
56
|
const text = await readTextOrNull(join(root, VIBEOPS_CONFIG_FILE));
|
|
@@ -45,19 +62,18 @@ export async function readConfig(root) {
|
|
|
45
62
|
typeof parsed.vibeopsVersion === "string" &&
|
|
46
63
|
typeof parsed.createdAt === "string" &&
|
|
47
64
|
parsed.schemaVersion === VIBEOPS_CONFIG_SCHEMA_VERSION) {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
65
|
+
const llm = parseLlmBlock(parsed.llm);
|
|
66
|
+
const git = parseGitBlock(parsed.git);
|
|
67
|
+
const clients = parseClientsBlock(parsed.clients) ?? ["cursor"];
|
|
68
|
+
return {
|
|
51
69
|
name: parsed.name,
|
|
52
70
|
vibeopsVersion: parsed.vibeopsVersion,
|
|
53
71
|
schemaVersion: VIBEOPS_CONFIG_SCHEMA_VERSION,
|
|
54
72
|
createdAt: parsed.createdAt,
|
|
73
|
+
clients,
|
|
74
|
+
...(git ? { git } : {}),
|
|
75
|
+
...(llm ? { llm } : {}),
|
|
55
76
|
};
|
|
56
|
-
if (notion)
|
|
57
|
-
config.notion = notion;
|
|
58
|
-
if (github)
|
|
59
|
-
config.github = github;
|
|
60
|
-
return config;
|
|
61
77
|
}
|
|
62
78
|
return null;
|
|
63
79
|
}
|
|
@@ -65,12 +81,15 @@ export async function readConfig(root) {
|
|
|
65
81
|
return null;
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
|
-
export function buildConfig(name, nowIso) {
|
|
84
|
+
export function buildConfig(name, clients, git, existing, nowIso) {
|
|
69
85
|
return {
|
|
70
86
|
name,
|
|
71
87
|
vibeopsVersion: VERSION,
|
|
72
88
|
schemaVersion: VIBEOPS_CONFIG_SCHEMA_VERSION,
|
|
73
|
-
createdAt: nowIso ?? new Date().toISOString(),
|
|
89
|
+
createdAt: existing?.createdAt ?? nowIso ?? new Date().toISOString(),
|
|
90
|
+
clients,
|
|
91
|
+
git,
|
|
92
|
+
llm: existing?.llm ?? { provider: "auto" },
|
|
74
93
|
};
|
|
75
94
|
}
|
|
76
95
|
export function configToJson(config) {
|
|
@@ -79,80 +98,25 @@ export function configToJson(config) {
|
|
|
79
98
|
export async function writeConfig(root, config) {
|
|
80
99
|
await writeText(join(root, VIBEOPS_CONFIG_FILE), configToJson(config));
|
|
81
100
|
}
|
|
82
|
-
/** Merge a partial notion section into an existing config without touching other fields. */
|
|
83
|
-
export function mergeNotionConfig(base, patch) {
|
|
84
|
-
const current = base.notion ?? {
|
|
85
|
-
enabled: false,
|
|
86
|
-
projectsTargetId: "",
|
|
87
|
-
tasksTargetId: "",
|
|
88
|
-
projectsDatabaseId: "",
|
|
89
|
-
tasksDatabaseId: "",
|
|
90
|
-
};
|
|
91
|
-
const next = {
|
|
92
|
-
enabled: patch.enabled ?? current.enabled,
|
|
93
|
-
projectsTargetId: patch.projectsTargetId !== undefined && patch.projectsTargetId.length > 0
|
|
94
|
-
? patch.projectsTargetId
|
|
95
|
-
: current.projectsTargetId,
|
|
96
|
-
tasksTargetId: patch.tasksTargetId !== undefined && patch.tasksTargetId.length > 0
|
|
97
|
-
? patch.tasksTargetId
|
|
98
|
-
: current.tasksTargetId,
|
|
99
|
-
projectsDatabaseId: patch.projectsDatabaseId !== undefined && patch.projectsDatabaseId.length > 0
|
|
100
|
-
? patch.projectsDatabaseId
|
|
101
|
-
: current.projectsDatabaseId,
|
|
102
|
-
tasksDatabaseId: patch.tasksDatabaseId !== undefined && patch.tasksDatabaseId.length > 0
|
|
103
|
-
? patch.tasksDatabaseId
|
|
104
|
-
: current.tasksDatabaseId,
|
|
105
|
-
};
|
|
106
|
-
const changed = base.notion === undefined ||
|
|
107
|
-
next.enabled !== current.enabled ||
|
|
108
|
-
next.projectsTargetId !== current.projectsTargetId ||
|
|
109
|
-
next.tasksTargetId !== current.tasksTargetId ||
|
|
110
|
-
next.projectsDatabaseId !== current.projectsDatabaseId ||
|
|
111
|
-
next.tasksDatabaseId !== current.tasksDatabaseId;
|
|
112
|
-
return { merged: { ...base, notion: next }, changed };
|
|
113
|
-
}
|
|
114
|
-
/** Preferred Projects target id for API calls: data_source target first, legacy DB fallback. */
|
|
115
|
-
export function notionProjectsTargetId(notion) {
|
|
116
|
-
return notion.projectsTargetId.length > 0
|
|
117
|
-
? notion.projectsTargetId
|
|
118
|
-
: notion.projectsDatabaseId;
|
|
119
|
-
}
|
|
120
|
-
/** Preferred Tasks target id for API calls: data_source target first, legacy DB fallback. */
|
|
121
|
-
export function notionTasksTargetId(notion) {
|
|
122
|
-
return notion.tasksTargetId.length > 0 ? notion.tasksTargetId : notion.tasksDatabaseId;
|
|
123
|
-
}
|
|
124
101
|
/**
|
|
125
|
-
* Merge
|
|
126
|
-
* other fields (notion, name, etc.). Empty strings in the patch are treated
|
|
127
|
-
* as "no value provided" — the existing value is kept. Pass through
|
|
128
|
-
* `enabled` / `mode` always (booleans / enums always overwrite).
|
|
102
|
+
* Merge `llm.provider` into existing `.vibeops.json` (preserves other keys).
|
|
129
103
|
*/
|
|
130
|
-
export function
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
149
|
-
const changed = base.github === undefined ||
|
|
150
|
-
next.enabled !== current.enabled ||
|
|
151
|
-
next.mode !== current.mode ||
|
|
152
|
-
next.owner !== current.owner ||
|
|
153
|
-
next.repo !== current.repo ||
|
|
154
|
-
next.remote !== current.remote ||
|
|
155
|
-
next.visibility !== current.visibility ||
|
|
156
|
-
next.url !== current.url;
|
|
157
|
-
return { merged: { ...base, github: next }, changed };
|
|
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`);
|
|
158
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
|
+
}
|