@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,308 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { notionSyncCommand } from "../commands/notion-sync.js";
|
|
3
|
-
import { askYesNo } from "./inquirer-helpers.js";
|
|
4
|
-
import { readConfig } from "./config.js";
|
|
5
|
-
import { pathExists, readText, readTextOrNull, writeText } from "./filesystem.js";
|
|
6
|
-
import { detectDefaultBranch, gitAddPaths, gitCheckoutNewBranch, gitHeadCommit, gitMergeInProgress, gitRemoteUrl, listUnmergedRelPaths, listWorkingTreeRelPaths, partitionPathsForAutoCommit, readGitInfo, runGit, tryResolveGovernanceUnmerged, } from "./git.js";
|
|
7
|
-
import { bold, cyan, dim, log, yellow } from "./logger.js";
|
|
8
|
-
import { llmCompleteJson, resolveAvailableLlmProvider } from "./llm-complete.js";
|
|
9
|
-
import { NEXT_TASK_SUGGESTION_REL } from "./mvp-constants.js";
|
|
10
|
-
import { LAST_DONE_SUMMARY_REL, readLastDoneSummary, } from "./mvp-done-summary.js";
|
|
11
|
-
import { mergeTaskBranch } from "./task-merge.js";
|
|
12
|
-
import { parseTaskFilename, readSection } from "./task.js";
|
|
13
|
-
const BRIEF_REL = ".vibeops/brief/project-brief.md";
|
|
14
|
-
async function gitDiffStatSummary(cwd) {
|
|
15
|
-
try {
|
|
16
|
-
const stat = await runGit(cwd, ["diff", "--stat"]);
|
|
17
|
-
const body = stat.stdout.trim();
|
|
18
|
-
return body.length > 0 ? body : "(no staged/unstaged diff — check untracked files with git status)";
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return "(could not read git diff)";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export async function syncNotionAfterDone(opts) {
|
|
25
|
-
if (opts.noNotionSync === true) {
|
|
26
|
-
log.info(dim("Notion sync skipped (--no-notion-sync)."));
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const config = await readConfig(opts.cwd);
|
|
30
|
-
if (config?.notion?.enabled !== true) {
|
|
31
|
-
log.info(dim("Notion sync skipped (not enabled in .vibeops.json)."));
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
log.blank();
|
|
35
|
-
log.step(bold("Notion sync"));
|
|
36
|
-
if (opts.dryRun === true) {
|
|
37
|
-
log.info(dim(" dry-run: would run vibeops notion sync"));
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const prevCode = process.exitCode;
|
|
41
|
-
await notionSyncCommand({ cwd: opts.cwd, dryRun: false });
|
|
42
|
-
if (process.exitCode === 1) {
|
|
43
|
-
log.warn("Notion sync failed — fix integration and run `vibeops notion sync`.");
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
log.ok("Notion sync complete.");
|
|
47
|
-
}
|
|
48
|
-
if (prevCode === 1)
|
|
49
|
-
process.exitCode = 1;
|
|
50
|
-
}
|
|
51
|
-
async function handleMergeInProgressDirtyTree(opts) {
|
|
52
|
-
const unmerged = await listUnmergedRelPaths(opts.cwd);
|
|
53
|
-
if (unmerged.length > 0) {
|
|
54
|
-
log.warn("Unmerged files from the task merge:");
|
|
55
|
-
for (const p of unmerged.slice(0, 12))
|
|
56
|
-
log.info(` ${dim("·")} ${p}`);
|
|
57
|
-
if (unmerged.length > 12)
|
|
58
|
-
log.info(` ${dim("·")} … and ${unmerged.length - 12} more`);
|
|
59
|
-
const resolved = await tryResolveGovernanceUnmerged(opts.cwd);
|
|
60
|
-
if (resolved.length > 0) {
|
|
61
|
-
log.ok(`Auto-resolved ${resolved.length} governance conflict(s) (kept incoming branch version).`);
|
|
62
|
-
}
|
|
63
|
-
const still = await listUnmergedRelPaths(opts.cwd);
|
|
64
|
-
if (still.length > 0) {
|
|
65
|
-
log.error("Remaining conflicts — resolve manually, then:");
|
|
66
|
-
for (const p of still)
|
|
67
|
-
log.info(` ${dim("·")} ${p}`);
|
|
68
|
-
log.info(dim(" git add <paths> && git commit (completes merge)"));
|
|
69
|
-
log.info(dim(" Or abort: git merge --quit"));
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const allPaths = await listWorkingTreeRelPaths(opts.cwd);
|
|
74
|
-
const { committable } = partitionPathsForAutoCommit(allPaths, {
|
|
75
|
-
unmerged: await listUnmergedRelPaths(opts.cwd),
|
|
76
|
-
});
|
|
77
|
-
if (committable.length > 0) {
|
|
78
|
-
await gitAddPaths(opts.cwd, committable);
|
|
79
|
-
log.info(dim("Staged remaining changes."));
|
|
80
|
-
}
|
|
81
|
-
log.warn("Complete the merge on this branch:");
|
|
82
|
-
log.info(dim(" git commit -m \"merge: complete TASK merge and post-done files\""));
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
async function cleanupDirtyTree(opts) {
|
|
86
|
-
const git = await readGitInfo(opts.cwd);
|
|
87
|
-
if (!git.isRepo || git.dirty !== true)
|
|
88
|
-
return true;
|
|
89
|
-
if (await gitMergeInProgress(opts.cwd)) {
|
|
90
|
-
log.blank();
|
|
91
|
-
log.warn(`${yellow("!")} Git merge still in progress — cannot create a cleanup branch yet.`);
|
|
92
|
-
return handleMergeInProgressDirtyTree(opts);
|
|
93
|
-
}
|
|
94
|
-
const unmerged = await listUnmergedRelPaths(opts.cwd);
|
|
95
|
-
const allPaths = await listWorkingTreeRelPaths(opts.cwd);
|
|
96
|
-
const { committable, excluded, unmerged: unmergedListed } = partitionPathsForAutoCommit(allPaths, { unmerged });
|
|
97
|
-
log.blank();
|
|
98
|
-
log.warn(`${yellow("!")} Working tree is still dirty after done.`);
|
|
99
|
-
const summary = await gitDiffStatSummary(opts.cwd);
|
|
100
|
-
log.info(dim("Changes:"));
|
|
101
|
-
log.info(dim(summary.split("\n").map((l) => ` ${l}`).join("\n")));
|
|
102
|
-
if (unmergedListed.length > 0) {
|
|
103
|
-
log.warn("Skipping unmerged paths until conflicts are resolved:");
|
|
104
|
-
for (const p of unmergedListed.slice(0, 8))
|
|
105
|
-
log.info(` ${dim("·")} ${p}`);
|
|
106
|
-
}
|
|
107
|
-
if (excluded.length > 0) {
|
|
108
|
-
log.blank();
|
|
109
|
-
log.warn("These paths are build/install artifacts — VibeOps will not commit them:");
|
|
110
|
-
for (const p of excluded.slice(0, 8))
|
|
111
|
-
log.info(` ${dim("·")} ${p}`);
|
|
112
|
-
if (excluded.length > 8)
|
|
113
|
-
log.info(` ${dim("·")} … and ${excluded.length - 8} more`);
|
|
114
|
-
log.info(dim(" Add node_modules/, .next/, etc. to .gitignore if they are not already ignored."));
|
|
115
|
-
}
|
|
116
|
-
if (committable.length === 0) {
|
|
117
|
-
log.warn("Nothing safe to auto-commit — fix .gitignore and remove tracked artifacts before the next TASK.");
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
log.blank();
|
|
121
|
-
log.info(dim("Would commit:"));
|
|
122
|
-
for (const p of committable.slice(0, 12))
|
|
123
|
-
log.info(` ${dim("·")} ${p}`);
|
|
124
|
-
if (committable.length > 12)
|
|
125
|
-
log.info(` ${dim("·")} … and ${committable.length - 12} more`);
|
|
126
|
-
let proceed = false;
|
|
127
|
-
if (opts.interactive === true) {
|
|
128
|
-
proceed = await askYesNo({
|
|
129
|
-
message: "Create a cleanup branch, commit the paths above (not node_modules/.next), merge, and delete the branch?",
|
|
130
|
-
nonInteractive: false,
|
|
131
|
-
defaultValue: true,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
if (!proceed) {
|
|
135
|
-
log.info(dim("Cleanup skipped — commit manually before starting the next TASK."));
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
if (opts.dryRun === true) {
|
|
139
|
-
log.info(dim(" dry-run: would create chore/vibeops-post-done-* branch, commit, merge"));
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
const taskId = parseTaskFilename(opts.taskFile).id;
|
|
143
|
-
const baseBranch = (await detectDefaultBranch(opts.cwd)) ?? "main";
|
|
144
|
-
const baseCommit = (await gitHeadCommit(opts.cwd, false)) ?? "";
|
|
145
|
-
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
146
|
-
const cleanupBranch = `chore/vibeops-post-done-${stamp}`;
|
|
147
|
-
await gitCheckoutNewBranch(opts.cwd, cleanupBranch);
|
|
148
|
-
await gitAddPaths(opts.cwd, committable);
|
|
149
|
-
await runGit(opts.cwd, [
|
|
150
|
-
"commit",
|
|
151
|
-
"-q",
|
|
152
|
-
"-m",
|
|
153
|
-
`chore(post-done): cleanup uncommitted work after ${taskId}`,
|
|
154
|
-
]);
|
|
155
|
-
log.ok(`Committed on ${cleanupBranch}`);
|
|
156
|
-
const remote = (await gitRemoteUrl(opts.cwd, "origin")) ?? "";
|
|
157
|
-
if (remote.length === 0) {
|
|
158
|
-
log.warn("No origin remote — merge locally only.");
|
|
159
|
-
await runGit(opts.cwd, ["switch", baseBranch]);
|
|
160
|
-
await runGit(opts.cwd, ["merge", "--no-ff", cleanupBranch, "-m", `merge: post-mvp cleanup`]);
|
|
161
|
-
await runGit(opts.cwd, ["branch", "-d", cleanupBranch]);
|
|
162
|
-
return (await readGitInfo(opts.cwd)).dirty !== true;
|
|
163
|
-
}
|
|
164
|
-
const ctx = {
|
|
165
|
-
baseBranch,
|
|
166
|
-
baseCommit,
|
|
167
|
-
taskBranch: cleanupBranch,
|
|
168
|
-
startedAt: new Date().toISOString(),
|
|
169
|
-
};
|
|
170
|
-
const merged = await mergeTaskBranch({
|
|
171
|
-
cwd: opts.cwd,
|
|
172
|
-
taskId,
|
|
173
|
-
taskTitle: opts.taskTitle,
|
|
174
|
-
gitCtx: ctx,
|
|
175
|
-
mode: "direct",
|
|
176
|
-
});
|
|
177
|
-
if (!merged.ok) {
|
|
178
|
-
log.warn("Cleanup merge did not finish — resolve Git state manually.");
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
const after = await readGitInfo(opts.cwd);
|
|
182
|
-
if (after.dirty === true) {
|
|
183
|
-
log.warn("Tree is still dirty after cleanup merge.");
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
log.ok("Working tree clean after post-MVP cleanup.");
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
function renderSuggestionMarkdown(s, priorDoneSummary) {
|
|
190
|
-
const priorBlock = priorDoneSummary !== null
|
|
191
|
-
? `## Previous iteration (from \`${LAST_DONE_SUMMARY_REL}\`)\n\n${priorDoneSummary.trim()}\n\n---\n\n`
|
|
192
|
-
: "";
|
|
193
|
-
return `# Next iteration (suggested)
|
|
194
|
-
|
|
195
|
-
${priorBlock}## Title
|
|
196
|
-
|
|
197
|
-
${s.title}
|
|
198
|
-
|
|
199
|
-
## One-line idea
|
|
200
|
-
|
|
201
|
-
${s.oneLineIdea}
|
|
202
|
-
|
|
203
|
-
## Goal
|
|
204
|
-
|
|
205
|
-
${s.goal}
|
|
206
|
-
|
|
207
|
-
## Suggested scope
|
|
208
|
-
|
|
209
|
-
${s.scope.map((x) => `- ${x}`).join("\n")}
|
|
210
|
-
|
|
211
|
-
## Rationale
|
|
212
|
-
|
|
213
|
-
${s.rationale}
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
Run planning again when ready:
|
|
218
|
-
|
|
219
|
-
\`\`\`bash
|
|
220
|
-
vibeops plan --idea "${s.oneLineIdea.replace(/"/g, '\\"')}"
|
|
221
|
-
\`\`\`
|
|
222
|
-
`;
|
|
223
|
-
}
|
|
224
|
-
async function suggestNextTask(opts) {
|
|
225
|
-
const git = await readGitInfo(opts.cwd);
|
|
226
|
-
if (git.dirty === true) {
|
|
227
|
-
log.info(dim("Next-task suggestion skipped until the working tree is clean."));
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
const briefPath = join(opts.cwd, BRIEF_REL);
|
|
231
|
-
const brief = (await readTextOrNull(briefPath)) ??
|
|
232
|
-
"(no project brief — run vibeops plan or add .vibeops/brief/project-brief.md)";
|
|
233
|
-
const priorDone = await readLastDoneSummary(opts.cwd);
|
|
234
|
-
const result = readSection(await readText(opts.taskFile), "Result");
|
|
235
|
-
const provider = await resolveAvailableLlmProvider(opts.cwd);
|
|
236
|
-
if (provider === null) {
|
|
237
|
-
log.info(dim(`Next-task suggestion skipped (no LLM). Connect Codex OAuth, Cursor Agent, or OPENAI_API_KEY.`));
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
log.blank();
|
|
241
|
-
log.step(bold("Next iteration (LLM suggestion)"));
|
|
242
|
-
const providerLabel = provider === "codex-oauth"
|
|
243
|
-
? "Codex OAuth"
|
|
244
|
-
: provider === "cursor-agent"
|
|
245
|
-
? "Cursor Agent CLI"
|
|
246
|
-
: "OpenAI API";
|
|
247
|
-
log.info(dim(` using ${providerLabel}`));
|
|
248
|
-
if (opts.dryRun === true) {
|
|
249
|
-
log.info(dim(` dry-run: would write ${NEXT_TASK_SUGGESTION_REL}`));
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
const { text: raw } = await llmCompleteJson([
|
|
254
|
-
{
|
|
255
|
-
role: "system",
|
|
256
|
-
content: "You suggest the next small product iteration after an MVP shipped. Reply with JSON only: title, oneLineIdea, goal, scope (string array, 3-6 items), rationale.",
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
role: "user",
|
|
260
|
-
content: [
|
|
261
|
-
`Project brief:\n${brief.slice(0, 8000)}`,
|
|
262
|
-
priorDone
|
|
263
|
-
? `\nLast done summary (${LAST_DONE_SUMMARY_REL}):\n${priorDone.slice(0, 12000)}`
|
|
264
|
-
: "",
|
|
265
|
-
`\nMVP Result (TASK file):\n${result.slice(0, 6000)}`,
|
|
266
|
-
"\nSuggest the next TASK (post-MVP), not a full replan.",
|
|
267
|
-
].join("\n"),
|
|
268
|
-
},
|
|
269
|
-
], { cwd: opts.cwd, provider });
|
|
270
|
-
const parsed = JSON.parse(raw);
|
|
271
|
-
const suggestion = {
|
|
272
|
-
title: String(parsed.title ?? "Next iteration"),
|
|
273
|
-
oneLineIdea: String(parsed.oneLineIdea ?? parsed.title ?? "Next feature slice"),
|
|
274
|
-
goal: String(parsed.goal ?? ""),
|
|
275
|
-
scope: Array.isArray(parsed.scope)
|
|
276
|
-
? parsed.scope.map((x) => String(x))
|
|
277
|
-
: ["(define scope in vibeops plan)"],
|
|
278
|
-
rationale: String(parsed.rationale ?? ""),
|
|
279
|
-
};
|
|
280
|
-
const outPath = join(opts.cwd, NEXT_TASK_SUGGESTION_REL);
|
|
281
|
-
await writeText(outPath, renderSuggestionMarkdown(suggestion, priorDone));
|
|
282
|
-
log.ok(`Wrote ${cyan(NEXT_TASK_SUGGESTION_REL)}`);
|
|
283
|
-
log.info(` ${dim("title")} ${suggestion.title}`);
|
|
284
|
-
log.info(` ${dim("idea")} ${suggestion.oneLineIdea}`);
|
|
285
|
-
}
|
|
286
|
-
catch (e) {
|
|
287
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
288
|
-
log.warn(`Next-task suggestion failed (${msg}).`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/** Notion sync, dirty-tree cleanup, and optional next-task LLM suggestion after `vibeops done`. */
|
|
292
|
-
export async function runDoneFollowUp(opts) {
|
|
293
|
-
await syncNotionAfterDone(opts);
|
|
294
|
-
const clean = await cleanupDirtyTree(opts);
|
|
295
|
-
if (clean) {
|
|
296
|
-
await suggestNextTask(opts);
|
|
297
|
-
}
|
|
298
|
-
log.blank();
|
|
299
|
-
log.info(bold("Next"));
|
|
300
|
-
log.info(` · ${cyan("vibeops status")}`);
|
|
301
|
-
if (await pathExists(join(opts.cwd, LAST_DONE_SUMMARY_REL))) {
|
|
302
|
-
log.info(` · ${cyan(LAST_DONE_SUMMARY_REL)} — what shipped last time`);
|
|
303
|
-
}
|
|
304
|
-
if (await pathExists(join(opts.cwd, NEXT_TASK_SUGGESTION_REL))) {
|
|
305
|
-
log.info(` · ${cyan(NEXT_TASK_SUGGESTION_REL)} — suggested follow-up`);
|
|
306
|
-
}
|
|
307
|
-
log.info(` · ${cyan("vibeops plan --idea \"…\"")} — start the next iteration when ready`);
|
|
308
|
-
}
|
package/dist/lib/github-cli.js
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { runGit } from "./git.js";
|
|
4
|
-
const exec = promisify(execFile);
|
|
5
|
-
export async function runGh(args) {
|
|
6
|
-
try {
|
|
7
|
-
const { stdout, stderr } = await exec("gh", [...args], { env: process.env });
|
|
8
|
-
return { ok: true, stdout, stderr, exitCode: 0 };
|
|
9
|
-
}
|
|
10
|
-
catch (err) {
|
|
11
|
-
const e = err;
|
|
12
|
-
return {
|
|
13
|
-
ok: false,
|
|
14
|
-
stdout: e.stdout ?? "",
|
|
15
|
-
stderr: e.stderr ?? (typeof e.message === "string" ? e.message : ""),
|
|
16
|
-
exitCode: typeof e.code === "number" ? e.code : null,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/** Returns true if `gh --version` exits 0. */
|
|
21
|
-
export async function isGhInstalled() {
|
|
22
|
-
const res = await runGh(["--version"]);
|
|
23
|
-
return res.ok;
|
|
24
|
-
}
|
|
25
|
-
const USERNAME_RE = /Logged in to (?:github\.com|[\w.-]+) (?:as|account) ([^\s)]+)/i;
|
|
26
|
-
export async function ghAuthStatus() {
|
|
27
|
-
const installed = await isGhInstalled();
|
|
28
|
-
if (!installed) {
|
|
29
|
-
return {
|
|
30
|
-
installed: false,
|
|
31
|
-
authenticated: false,
|
|
32
|
-
username: null,
|
|
33
|
-
hosts: [],
|
|
34
|
-
detail: "gh CLI not installed",
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
const res = await runGh(["auth", "status"]);
|
|
38
|
-
const text = `${res.stdout}\n${res.stderr}`;
|
|
39
|
-
const authenticated = res.ok || /Logged in to/i.test(text);
|
|
40
|
-
const userMatch = USERNAME_RE.exec(text);
|
|
41
|
-
const hosts = [];
|
|
42
|
-
for (const m of text.matchAll(/^([\w.-]+\.com)\s*$/gm)) {
|
|
43
|
-
const host = m[1]?.trim();
|
|
44
|
-
if (typeof host === "string" && host.length > 0 && !hosts.includes(host)) {
|
|
45
|
-
hosts.push(host);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (hosts.length === 0 && /github\.com/i.test(text))
|
|
49
|
-
hosts.push("github.com");
|
|
50
|
-
return {
|
|
51
|
-
installed: true,
|
|
52
|
-
authenticated,
|
|
53
|
-
username: userMatch !== null ? userMatch[1].trim() : null,
|
|
54
|
-
hosts,
|
|
55
|
-
// Strip any potential token-looking strings defensively.
|
|
56
|
-
detail: maskAuthDetail(text),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function maskAuthDetail(text) {
|
|
60
|
-
return text
|
|
61
|
-
.replace(/gh[op]_[A-Za-z0-9]{20,}/g, "gh***")
|
|
62
|
-
.replace(/ghp_[A-Za-z0-9]{20,}/g, "gh***")
|
|
63
|
-
.replace(/ghu_[A-Za-z0-9]{20,}/g, "gh***")
|
|
64
|
-
.replace(/(Authorization:\s*Bearer\s+)\S+/gi, "$1***")
|
|
65
|
-
.trim();
|
|
66
|
-
}
|
|
67
|
-
/** Best-effort `gh api user --jq .login`. Returns `null` when gh fails. */
|
|
68
|
-
export async function ghCurrentUser() {
|
|
69
|
-
const res = await runGh(["api", "user", "--jq", ".login"]);
|
|
70
|
-
if (!res.ok)
|
|
71
|
-
return null;
|
|
72
|
-
const login = res.stdout.trim();
|
|
73
|
-
return login.length > 0 ? login : null;
|
|
74
|
-
}
|
|
75
|
-
export async function ghRepoExists(owner, repo) {
|
|
76
|
-
if (owner.length === 0 || repo.length === 0)
|
|
77
|
-
return false;
|
|
78
|
-
const res = await runGh(["repo", "view", `${owner}/${repo}`, "--json", "name"]);
|
|
79
|
-
return res.ok;
|
|
80
|
-
}
|
|
81
|
-
export function buildGhCreateRepoArgs(input) {
|
|
82
|
-
const slug = `${input.owner}/${input.repo}`;
|
|
83
|
-
const args = ["repo", "create", slug];
|
|
84
|
-
args.push(input.visibility === "public" ? "--public" : "--private");
|
|
85
|
-
if (typeof input.source === "string" && input.source.length > 0) {
|
|
86
|
-
args.push(`--source=${input.source}`);
|
|
87
|
-
}
|
|
88
|
-
args.push(`--remote=${input.remote ?? "origin"}`);
|
|
89
|
-
if (typeof input.description === "string" && input.description.length > 0) {
|
|
90
|
-
args.push("--description", input.description);
|
|
91
|
-
}
|
|
92
|
-
return args;
|
|
93
|
-
}
|
|
94
|
-
export async function ghCreateRepo(input) {
|
|
95
|
-
const argv = buildGhCreateRepoArgs(input);
|
|
96
|
-
const command = `gh ${argv.map(shellSafe).join(" ")}`;
|
|
97
|
-
if (input.dryRun === true) {
|
|
98
|
-
return { ok: true, command, argv, dryRun: true };
|
|
99
|
-
}
|
|
100
|
-
const res = await runGh(argv);
|
|
101
|
-
return {
|
|
102
|
-
ok: res.ok,
|
|
103
|
-
command,
|
|
104
|
-
argv,
|
|
105
|
-
stdout: res.stdout,
|
|
106
|
-
stderr: res.stderr,
|
|
107
|
-
exitCode: res.exitCode,
|
|
108
|
-
dryRun: false,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Run `gh auth login` with the user's TTY (stdio inherit). Returns a promise
|
|
113
|
-
* resolving with the child's exit code. Caller is responsible for verifying
|
|
114
|
-
* `--dry-run` / `--yes` / non-TTY guards before invoking — this helper does
|
|
115
|
-
* NOT decide policy.
|
|
116
|
-
*/
|
|
117
|
-
export function ghAuthLoginInteractive() {
|
|
118
|
-
return new Promise((resolve) => {
|
|
119
|
-
const child = spawn("gh", ["auth", "login"], {
|
|
120
|
-
stdio: "inherit",
|
|
121
|
-
env: process.env,
|
|
122
|
-
});
|
|
123
|
-
child.on("exit", (code) => {
|
|
124
|
-
resolve(typeof code === "number" ? code : 1);
|
|
125
|
-
});
|
|
126
|
-
child.on("error", () => {
|
|
127
|
-
resolve(127);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
const RE_SSH = /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i;
|
|
132
|
-
const RE_HTTPS = /^https?:\/\/github\.com\/([^/\s?#]+)\/([^/\s?#]+?)(?:\.git)?(?:[?#].*)?$/i;
|
|
133
|
-
const RE_GIT_HTTPS = /^git\+https?:\/\/github\.com\/([^/\s?#]+)\/([^/\s?#]+?)(?:\.git)?(?:[?#].*)?$/i;
|
|
134
|
-
const RE_SLUG = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
|
|
135
|
-
/** Strip a trailing `.git` and any `?...#...` suffix from a repo name. */
|
|
136
|
-
function cleanRepoName(name) {
|
|
137
|
-
return name.replace(/\.git$/i, "").trim();
|
|
138
|
-
}
|
|
139
|
-
export function parseGitHubRemote(rawUrl) {
|
|
140
|
-
const url = rawUrl.trim();
|
|
141
|
-
const empty = {
|
|
142
|
-
isGithub: false,
|
|
143
|
-
owner: null,
|
|
144
|
-
repo: null,
|
|
145
|
-
url,
|
|
146
|
-
protocol: null,
|
|
147
|
-
httpsUrl: null,
|
|
148
|
-
gitHttpsUrl: null,
|
|
149
|
-
};
|
|
150
|
-
if (url.length === 0)
|
|
151
|
-
return empty;
|
|
152
|
-
let owner = null;
|
|
153
|
-
let repo = null;
|
|
154
|
-
let protocol = null;
|
|
155
|
-
const ssh = RE_SSH.exec(url);
|
|
156
|
-
if (ssh !== null) {
|
|
157
|
-
owner = ssh[1];
|
|
158
|
-
repo = cleanRepoName(ssh[2]);
|
|
159
|
-
protocol = "ssh";
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
const gitHttps = RE_GIT_HTTPS.exec(url);
|
|
163
|
-
if (gitHttps !== null) {
|
|
164
|
-
owner = gitHttps[1];
|
|
165
|
-
repo = cleanRepoName(gitHttps[2]);
|
|
166
|
-
protocol = "git";
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
const https = RE_HTTPS.exec(url);
|
|
170
|
-
if (https !== null) {
|
|
171
|
-
owner = https[1];
|
|
172
|
-
repo = cleanRepoName(https[2]);
|
|
173
|
-
protocol = "https";
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
const slug = RE_SLUG.exec(url);
|
|
177
|
-
if (slug !== null) {
|
|
178
|
-
owner = slug[1];
|
|
179
|
-
repo = cleanRepoName(slug[2]);
|
|
180
|
-
protocol = "slug";
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (owner === null || repo === null || repo.length === 0)
|
|
186
|
-
return empty;
|
|
187
|
-
const httpsUrl = `https://github.com/${owner}/${repo}`;
|
|
188
|
-
const gitHttpsUrl = `git+https://github.com/${owner}/${repo}.git`;
|
|
189
|
-
return {
|
|
190
|
-
isGithub: true,
|
|
191
|
-
owner,
|
|
192
|
-
repo,
|
|
193
|
-
url,
|
|
194
|
-
protocol,
|
|
195
|
-
httpsUrl,
|
|
196
|
-
gitHttpsUrl,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
export async function gitRemoteList(cwd) {
|
|
200
|
-
try {
|
|
201
|
-
const { stdout } = await exec("git", ["remote", "-v"], { cwd });
|
|
202
|
-
const out = [];
|
|
203
|
-
const seen = new Set();
|
|
204
|
-
for (const line of stdout.split(/\r?\n/)) {
|
|
205
|
-
const trimmed = line.trim();
|
|
206
|
-
if (trimmed.length === 0)
|
|
207
|
-
continue;
|
|
208
|
-
const m = /^([^\s]+)\s+([^\s]+)\s+\((fetch|push)\)$/.exec(trimmed);
|
|
209
|
-
if (m === null)
|
|
210
|
-
continue;
|
|
211
|
-
const name = m[1];
|
|
212
|
-
const url = m[2];
|
|
213
|
-
const key = `${name}\t${url}`;
|
|
214
|
-
if (seen.has(key))
|
|
215
|
-
continue;
|
|
216
|
-
seen.add(key);
|
|
217
|
-
out.push({ name, url });
|
|
218
|
-
}
|
|
219
|
-
// Deduplicate to one entry per remote name (prefer fetch).
|
|
220
|
-
const byName = new Map();
|
|
221
|
-
for (const e of out) {
|
|
222
|
-
if (!byName.has(e.name))
|
|
223
|
-
byName.set(e.name, e);
|
|
224
|
-
}
|
|
225
|
-
return Array.from(byName.values());
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
/** Add a new git remote. Throws if remote already exists or git fails. */
|
|
232
|
-
export async function gitRemoteAdd(cwd, name, url) {
|
|
233
|
-
await runGit(cwd, ["remote", "add", name, url]);
|
|
234
|
-
}
|
|
235
|
-
/** Update an existing git remote URL. Throws if remote does not exist. */
|
|
236
|
-
export async function gitRemoteSetUrl(cwd, name, url) {
|
|
237
|
-
await runGit(cwd, ["remote", "set-url", name, url]);
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Quote a single argv element for human-readable command preview. Not used
|
|
241
|
-
* to actually execute commands — we always go through execFile arg arrays.
|
|
242
|
-
*/
|
|
243
|
-
function shellSafe(s) {
|
|
244
|
-
if (/^[\w@/:.+=-]+$/.test(s))
|
|
245
|
-
return s;
|
|
246
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
247
|
-
}
|
package/dist/lib/guide-state.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { pathExists, readTextOrNull, writeText } from "./filesystem.js";
|
|
4
|
-
export const GUIDE_STATE_SCHEMA_VERSION = 1;
|
|
5
|
-
export function guideStatePath(vibeopsDir) {
|
|
6
|
-
return join(vibeopsDir, "state", "guide.json");
|
|
7
|
-
}
|
|
8
|
-
export async function readGuideState(vibeopsDir) {
|
|
9
|
-
const path = guideStatePath(vibeopsDir);
|
|
10
|
-
const raw = await readTextOrNull(path);
|
|
11
|
-
if (raw === null || raw.trim().length === 0)
|
|
12
|
-
return null;
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(raw);
|
|
15
|
-
if (parsed.schemaVersion !== GUIDE_STATE_SCHEMA_VERSION)
|
|
16
|
-
return null;
|
|
17
|
-
if (typeof parsed.stepId !== "string")
|
|
18
|
-
return null;
|
|
19
|
-
return {
|
|
20
|
-
schemaVersion: GUIDE_STATE_SCHEMA_VERSION,
|
|
21
|
-
taskId: typeof parsed.taskId === "string" ? parsed.taskId : null,
|
|
22
|
-
stepId: parsed.stepId,
|
|
23
|
-
history: Array.isArray(parsed.history)
|
|
24
|
-
? parsed.history.filter((x) => typeof x === "string")
|
|
25
|
-
: [],
|
|
26
|
-
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
export async function writeGuideState(vibeopsDir, state) {
|
|
34
|
-
const dir = join(vibeopsDir, "state");
|
|
35
|
-
if (!(await pathExists(dir))) {
|
|
36
|
-
await mkdir(dir, { recursive: true });
|
|
37
|
-
}
|
|
38
|
-
await writeText(guideStatePath(vibeopsDir), `${JSON.stringify(state, null, 2)}\n`);
|
|
39
|
-
}
|
|
40
|
-
export function pushGuideHistory(prev, taskId, stepId) {
|
|
41
|
-
const history = [...(prev?.history ?? [])];
|
|
42
|
-
if (prev?.stepId && prev.stepId !== stepId) {
|
|
43
|
-
history.push(prev.stepId);
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
schemaVersion: GUIDE_STATE_SCHEMA_VERSION,
|
|
47
|
-
taskId,
|
|
48
|
-
stepId,
|
|
49
|
-
history,
|
|
50
|
-
updatedAt: new Date().toISOString(),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
export function popGuideHistory(state) {
|
|
54
|
-
if (state.history.length === 0) {
|
|
55
|
-
return { next: null, stepId: null };
|
|
56
|
-
}
|
|
57
|
-
const history = [...state.history];
|
|
58
|
-
const stepId = history.pop();
|
|
59
|
-
return {
|
|
60
|
-
stepId,
|
|
61
|
-
next: {
|
|
62
|
-
schemaVersion: GUIDE_STATE_SCHEMA_VERSION,
|
|
63
|
-
taskId: state.taskId,
|
|
64
|
-
stepId,
|
|
65
|
-
history,
|
|
66
|
-
updatedAt: new Date().toISOString(),
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|