@goodtek/vibeops 0.7.22 → 1.0.0

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +62 -382
  3. package/dist/bootstrap/installer.js +4 -16
  4. package/dist/cli.js +26 -160
  5. package/dist/commands/init.js +3 -3
  6. package/dist/commands/status.js +145 -8
  7. package/dist/commands/task-add.js +64 -164
  8. package/dist/commands/task-done.js +117 -74
  9. package/dist/lib/config.js +2 -118
  10. package/dist/lib/git.js +1 -1
  11. package/dist/lib/paths.js +7 -3
  12. package/dist/lib/plan-llm-openai.js +0 -63
  13. package/dist/lib/resolve-task.js +34 -0
  14. package/dist/lib/task-add-llm.js +46 -233
  15. package/dist/lib/task-context.js +68 -0
  16. package/dist/lib/task-done-llm.js +148 -0
  17. package/dist/lib/task-merge.js +54 -159
  18. package/dist/lib/task-scaffold.js +39 -43
  19. package/dist/lib/task-start.js +72 -0
  20. package/dist/lib/task-summary.js +12 -239
  21. package/dist/lib/task.js +15 -24
  22. package/dist/types/config.js +0 -16
  23. package/package.json +3 -5
  24. package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  25. package/templates/.cursor/rules/02-git-safety.mdc +11 -0
  26. package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
  27. package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
  28. package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
  29. package/templates/AGENTS.md +26 -87
  30. package/templates/docs/logs/README.md +4 -35
  31. package/templates/docs/project/03-architecture.md +10 -29
  32. package/templates/docs/project/05-current-state.md +7 -27
  33. package/templates/docs/project/06-decisions.md +6 -17
  34. package/templates/docs/tasks/TASK-000-template.md +9 -44
  35. package/dist/agent/loader.js +0 -71
  36. package/dist/agent/prompt.js +0 -66
  37. package/dist/commands/agent-list.js +0 -32
  38. package/dist/commands/agent-prompt.js +0 -59
  39. package/dist/commands/agent-show.js +0 -26
  40. package/dist/commands/done.js +0 -160
  41. package/dist/commands/github-init.js +0 -554
  42. package/dist/commands/github-status.js +0 -164
  43. package/dist/commands/next.js +0 -283
  44. package/dist/commands/notion-init.js +0 -764
  45. package/dist/commands/notion-sync.js +0 -405
  46. package/dist/commands/notion-test.js +0 -595
  47. package/dist/commands/plan.js +0 -146
  48. package/dist/commands/rollback.js +0 -126
  49. package/dist/commands/start.js +0 -129
  50. package/dist/commands/task-check.js +0 -155
  51. package/dist/commands/task-generate.js +0 -206
  52. package/dist/commands/task-pull.js +0 -277
  53. package/dist/commands/task-rollback.js +0 -174
  54. package/dist/commands/task-start.js +0 -102
  55. package/dist/commands/task-status.js +0 -122
  56. package/dist/lib/brief.js +0 -356
  57. package/dist/lib/cursor-task-prompt.js +0 -60
  58. package/dist/lib/done-follow-up.js +0 -308
  59. package/dist/lib/github-cli.js +0 -247
  60. package/dist/lib/guide-state.js +0 -69
  61. package/dist/lib/mvp-artifacts.js +0 -204
  62. package/dist/lib/mvp-constants.js +0 -15
  63. package/dist/lib/mvp-diff-summary.js +0 -43
  64. package/dist/lib/mvp-done-summary.js +0 -335
  65. package/dist/lib/notion-client.js +0 -459
  66. package/dist/lib/notion-discovery.js +0 -671
  67. package/dist/lib/notion-env.js +0 -140
  68. package/dist/lib/notion-mappers.js +0 -148
  69. package/dist/lib/notion-schema.js +0 -272
  70. package/dist/lib/notion-sync.js +0 -337
  71. package/dist/lib/notion-target.js +0 -247
  72. package/dist/lib/package-json.js +0 -133
  73. package/dist/lib/plan-apply-planner.js +0 -131
  74. package/dist/lib/plan-codex-pkce.js +0 -234
  75. package/dist/lib/plan-llm-models.js +0 -237
  76. package/dist/lib/plan-llm-normalize.js +0 -146
  77. package/dist/lib/plan-llm-session.js +0 -682
  78. package/dist/lib/plan-post-apply-setup.js +0 -116
  79. package/dist/lib/project-docs.js +0 -95
  80. package/dist/lib/prompt-builder.js +0 -135
  81. package/dist/lib/resolve-command-task.js +0 -40
  82. package/dist/lib/task-add-build-prompt.js +0 -39
  83. package/dist/lib/task-add-cursor.js +0 -81
  84. package/dist/lib/task-generator.js +0 -183
  85. package/dist/lib/task-prompt.js +0 -23
  86. package/dist/lib/task-pull.js +0 -354
  87. package/dist/lib/task-status-brief.js +0 -132
  88. package/dist/lib/workflow-guide.js +0 -485
  89. package/dist/status/collector.js +0 -103
  90. package/dist/status/format.js +0 -181
  91. package/dist/types/brief.js +0 -126
  92. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  93. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  94. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  95. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  96. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  97. package/templates/.vibeops/agents/architect.md +0 -47
  98. package/templates/.vibeops/agents/builder.md +0 -38
  99. package/templates/.vibeops/agents/docs.md +0 -54
  100. package/templates/.vibeops/agents/orchestrator.md +0 -40
  101. package/templates/.vibeops/agents/planner.md +0 -60
  102. package/templates/.vibeops/agents/recovery.md +0 -49
  103. package/templates/.vibeops/agents/reviewer.md +0 -47
  104. package/templates/.vibeops/agents/tester.md +0 -43
  105. package/templates/.vibeops/prompts/create-plan.md +0 -33
  106. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  107. package/templates/.vibeops/prompts/implement-task.md +0 -39
  108. package/templates/.vibeops/prompts/review-task.md +0 -34
  109. package/templates/.vibeops/prompts/rollback.md +0 -32
  110. package/templates/.vibeops/prompts/start-project.md +0 -39
  111. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  112. package/templates/.vibeops/workflows/project-start.md +0 -73
  113. package/templates/.vibeops/workflows/rollback.md +0 -45
  114. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  115. package/templates/docs/project/00-overview.md +0 -27
  116. package/templates/docs/project/01-requirements.md +0 -30
  117. package/templates/docs/project/02-mvp-scope.md +0 -36
  118. package/templates/docs/project/04-tech-stack.md +0 -29
  119. package/templates/docs/project/07-backlog.md +0 -23
  120. package/templates/docs/project/08-env.md +0 -29
  121. package/templates/docs/project/09-deployment.md +0 -28
@@ -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
- }
@@ -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
- }
@@ -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
- }