@goodtek/vibeops 0.4.0 → 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 (106) hide show
  1. package/CHANGELOG.md +198 -1
  2. package/README.md +62 -402
  3. package/dist/bootstrap/installer.js +4 -16
  4. package/dist/cli.js +52 -244
  5. package/dist/commands/init.js +3 -3
  6. package/dist/commands/status.js +145 -8
  7. package/dist/commands/task-add.js +100 -0
  8. package/dist/commands/task-done.js +117 -74
  9. package/dist/lib/config.js +2 -118
  10. package/dist/lib/git.js +190 -8
  11. package/dist/lib/llm-complete.js +62 -0
  12. package/dist/lib/paths.js +7 -3
  13. package/dist/lib/plan-llm-openai.js +0 -63
  14. package/dist/lib/resolve-task.js +34 -0
  15. package/dist/lib/slug.js +11 -0
  16. package/dist/lib/task-add-llm.js +81 -0
  17. package/dist/lib/task-context.js +68 -0
  18. package/dist/lib/task-done-llm.js +148 -0
  19. package/dist/lib/task-merge.js +115 -0
  20. package/dist/lib/task-scaffold.js +70 -92
  21. package/dist/lib/task-start.js +72 -0
  22. package/dist/lib/task-summary.js +12 -239
  23. package/dist/lib/task.js +80 -37
  24. package/dist/types/config.js +0 -16
  25. package/package.json +3 -4
  26. package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  27. package/templates/.cursor/rules/02-git-safety.mdc +11 -0
  28. package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
  29. package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
  30. package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
  31. package/templates/AGENTS.md +26 -87
  32. package/templates/docs/logs/README.md +4 -35
  33. package/templates/docs/project/03-architecture.md +10 -29
  34. package/templates/docs/project/05-current-state.md +7 -27
  35. package/templates/docs/project/06-decisions.md +6 -17
  36. package/templates/docs/tasks/TASK-000-template.md +9 -44
  37. package/dist/agent/loader.js +0 -71
  38. package/dist/agent/prompt.js +0 -66
  39. package/dist/commands/agent-list.js +0 -32
  40. package/dist/commands/agent-prompt.js +0 -59
  41. package/dist/commands/agent-show.js +0 -26
  42. package/dist/commands/github-init.js +0 -554
  43. package/dist/commands/github-status.js +0 -164
  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 -343
  48. package/dist/commands/task-check.js +0 -155
  49. package/dist/commands/task-generate.js +0 -206
  50. package/dist/commands/task-pull.js +0 -277
  51. package/dist/commands/task-rollback.js +0 -174
  52. package/dist/commands/task-start.js +0 -102
  53. package/dist/lib/brief.js +0 -356
  54. package/dist/lib/github-cli.js +0 -247
  55. package/dist/lib/notion-client.js +0 -459
  56. package/dist/lib/notion-discovery.js +0 -671
  57. package/dist/lib/notion-env.js +0 -140
  58. package/dist/lib/notion-mappers.js +0 -148
  59. package/dist/lib/notion-schema.js +0 -272
  60. package/dist/lib/notion-sync.js +0 -337
  61. package/dist/lib/notion-target.js +0 -247
  62. package/dist/lib/package-json.js +0 -133
  63. package/dist/lib/plan-apply-planner.js +0 -131
  64. package/dist/lib/plan-codex-pkce.js +0 -234
  65. package/dist/lib/plan-llm-models.js +0 -237
  66. package/dist/lib/plan-llm-normalize.js +0 -146
  67. package/dist/lib/plan-llm-session.js +0 -682
  68. package/dist/lib/plan-post-apply-setup.js +0 -116
  69. package/dist/lib/project-docs.js +0 -95
  70. package/dist/lib/prompt-builder.js +0 -135
  71. package/dist/lib/task-generator.js +0 -183
  72. package/dist/lib/task-prompt.js +0 -23
  73. package/dist/lib/task-pull.js +0 -354
  74. package/dist/status/collector.js +0 -103
  75. package/dist/status/format.js +0 -177
  76. package/dist/types/brief.js +0 -126
  77. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  78. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  79. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  80. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  81. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  82. package/templates/.vibeops/agents/architect.md +0 -47
  83. package/templates/.vibeops/agents/builder.md +0 -38
  84. package/templates/.vibeops/agents/docs.md +0 -54
  85. package/templates/.vibeops/agents/orchestrator.md +0 -40
  86. package/templates/.vibeops/agents/planner.md +0 -60
  87. package/templates/.vibeops/agents/recovery.md +0 -49
  88. package/templates/.vibeops/agents/reviewer.md +0 -47
  89. package/templates/.vibeops/agents/tester.md +0 -43
  90. package/templates/.vibeops/prompts/create-plan.md +0 -33
  91. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  92. package/templates/.vibeops/prompts/implement-task.md +0 -39
  93. package/templates/.vibeops/prompts/review-task.md +0 -34
  94. package/templates/.vibeops/prompts/rollback.md +0 -32
  95. package/templates/.vibeops/prompts/start-project.md +0 -39
  96. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  97. package/templates/.vibeops/workflows/project-start.md +0 -73
  98. package/templates/.vibeops/workflows/rollback.md +0 -45
  99. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  100. package/templates/docs/project/00-overview.md +0 -27
  101. package/templates/docs/project/01-requirements.md +0 -30
  102. package/templates/docs/project/02-mvp-scope.md +0 -36
  103. package/templates/docs/project/04-tech-stack.md +0 -29
  104. package/templates/docs/project/07-backlog.md +0 -23
  105. package/templates/docs/project/08-env.md +0 -29
  106. package/templates/docs/project/09-deployment.md +0 -28
@@ -1,354 +0,0 @@
1
- /**
2
- * Planning + execution helpers for `vibeops task pull`.
3
- *
4
- * Notion is queried for TASK rows whose Status matches `--status` (default
5
- * "Planned"). For each row that does NOT yet have a local TASK file we plan
6
- * a new `docs/tasks/TASK-NNN-slug.md` skeleton.
7
- *
8
- * Rules:
9
- * - Never overwrite an existing local file.
10
- * - Never write any TASK body section beyond the Notion-sourced metadata.
11
- * - Always include a `## Notion Page` section so `notion sync` can later
12
- * update the same row in place.
13
- *
14
- * Mutation surface:
15
- * - `executePullEntry` writes the local file and (if Docs Path on Notion
16
- * was empty) optionally updates the Notion page's Docs Path. Callers in
17
- * dry-run mode MUST skip this.
18
- */
19
- import { readdir } from "node:fs/promises";
20
- import { basename, join, posix } from "node:path";
21
- import { pathExists, writeText } from "./filesystem.js";
22
- import { projectPaths } from "./paths.js";
23
- import { formatTaskId, highestTaskNumber, } from "./task.js";
24
- import { slugify } from "./task-generator.js";
25
- import { readRichText, readSelect, readStatus, readTitle, richTextEqualsFilter, richTextProperty, statusEqualsFilter, andFilter, } from "./notion-mappers.js";
26
- import { renderPulledTaskMarkdown, } from "./task-summary.js";
27
- export function rowFromNotionPage(page) {
28
- const p = page.properties;
29
- return {
30
- pageId: page.id,
31
- taskId: readRichText(p["Task ID"]),
32
- name: readTitle(p.Name),
33
- status: readStatus(p.Status),
34
- mvpPhase: readSelect(p["MVP Phase"]),
35
- priority: readSelect(p.Priority),
36
- summary: readRichText(p.Summary),
37
- docsPath: readRichText(p["Docs Path"]),
38
- };
39
- }
40
- const DEFAULT_STATUS_NAMES = ["Planned"];
41
- const DEFAULT_LIMIT = 20;
42
- const MAX_LIMIT = 100;
43
- function buildPullFilter(projectId, statusNames) {
44
- const statusFilters = statusNames.map((n) => statusEqualsFilter("Status", n));
45
- const statusFilter = statusFilters.length === 1
46
- ? statusFilters[0]
47
- : { or: statusFilters };
48
- return andFilter([
49
- richTextEqualsFilter("Project ID", projectId),
50
- statusFilter,
51
- ]);
52
- }
53
- /**
54
- * Notion `Docs Path` is **trusted only when its basename matches the TASK
55
- * ID**. A row whose Task ID is `TASK-099` but whose Docs Path points at
56
- * `docs/tasks/TASK-012-package-polish-readme.md` is a mismatch — surfacing
57
- * it as `local-file-exists` silently shadows the real TASK-099 candidate.
58
- *
59
- * Acceptable basenames (case sensitive — Notion is case sensitive here):
60
- * - `TASK-099.md`
61
- * - `TASK-099-anything.md` (the standard `TASK-NNN-slug.md` form)
62
- *
63
- * Anything else is a mismatch.
64
- */
65
- export function docsPathMatchesTaskId(docsRelativePath, taskId) {
66
- if (docsRelativePath.length === 0 || taskId.length === 0)
67
- return false;
68
- const base = basename(docsRelativePath);
69
- return base === `${taskId}.md` || base.startsWith(`${taskId}-`);
70
- }
71
- /** Scan `docs/tasks` for any file whose basename starts with `TASK-NNN`. */
72
- async function findLocalTaskFileForId(docsTasksDir, taskId) {
73
- let entries;
74
- try {
75
- entries = await readdir(docsTasksDir);
76
- }
77
- catch {
78
- return null;
79
- }
80
- for (const name of entries) {
81
- if (name === `${taskId}.md` || name.startsWith(`${taskId}-`)) {
82
- return join(docsTasksDir, name);
83
- }
84
- }
85
- return null;
86
- }
87
- export async function planPull(inputs) {
88
- const statusNames = inputs.statusNames ?? DEFAULT_STATUS_NAMES;
89
- const limit = Math.max(1, Math.min(inputs.limit ?? DEFAULT_LIMIT, MAX_LIMIT));
90
- const res = await inputs.client.queryDataSource(inputs.tasksDataSourceId, {
91
- filter: buildPullFilter(inputs.projectId, statusNames),
92
- pageSize: limit,
93
- });
94
- const paths = projectPaths(inputs.cwd);
95
- const startCounter = await highestTaskNumber(paths.docsTasks);
96
- let nextNumber = startCounter + 1;
97
- const entries = [];
98
- const skipped = [];
99
- const trace = [];
100
- // First pass: detect duplicate Task IDs across the Notion query result so
101
- // a second row with the same id cannot quietly create a second entry. We
102
- // keep the first row and skip subsequent ones with `duplicate-task-id`.
103
- const seenTaskIds = new Set();
104
- const duplicatePageIds = new Set();
105
- for (const page of res.results) {
106
- const id = rowFromNotionPage(page).taskId.trim();
107
- if (id.length === 0)
108
- continue;
109
- if (seenTaskIds.has(id)) {
110
- duplicatePageIds.add(page.id);
111
- }
112
- else {
113
- seenTaskIds.add(id);
114
- }
115
- }
116
- for (const page of res.results) {
117
- const row = rowFromNotionPage(page);
118
- const notionDocsPath = row.docsPath.trim();
119
- // (A) Task ID required — guard against blank ids before we touch local fs.
120
- const taskIdRaw = row.taskId.trim();
121
- if (taskIdRaw.length === 0 && notionDocsPath.length === 0) {
122
- // No Task ID and no Docs Path → allocate a fresh one, treated as a
123
- // legitimate "new" candidate (existing behaviour). Trace it so
124
- // `--verbose` makes the allocation visible.
125
- const taskId = formatTaskId(nextNumber);
126
- nextNumber++;
127
- const slug = slugify(row.name.length > 0 ? row.name : taskId, taskId.toLowerCase());
128
- const docsRelativePath = posix.join("docs/tasks", `${taskId}-${slug}.md`);
129
- const absPath = join(paths.docsTasks, `${taskId}-${slug}.md`);
130
- // Even an allocated id may already be present on disk (race / manual
131
- // creation). Re-check.
132
- if (await pathExists(absPath)) {
133
- skipped.push({
134
- pageId: page.id,
135
- taskId,
136
- reason: "local-file-exists",
137
- docsRelativePath,
138
- detail: `local resolved path: ${docsRelativePath}`,
139
- });
140
- trace.push({
141
- taskId,
142
- pageId: page.id,
143
- notionDocsPath,
144
- localResolvedPath: docsRelativePath,
145
- decision: "skip-local-file-exists",
146
- reason: "Task ID was empty, allocated next id already on disk",
147
- });
148
- continue;
149
- }
150
- entries.push({
151
- pageId: page.id,
152
- taskId,
153
- title: row.name.length > 0 ? row.name : taskId,
154
- status: row.status,
155
- mvpPhase: row.mvpPhase,
156
- summary: row.summary,
157
- docsRelativePath,
158
- absPath,
159
- notionNeedsDocsPath: true,
160
- detail: "allocated Task ID (Notion row had none)",
161
- });
162
- trace.push({
163
- taskId,
164
- pageId: page.id,
165
- notionDocsPath: "",
166
- localResolvedPath: docsRelativePath,
167
- decision: "new-file",
168
- reason: "Task ID empty → allocated; no local file with that id",
169
- });
170
- continue;
171
- }
172
- if (taskIdRaw.length === 0) {
173
- // Has a Docs Path but no Task ID — refuse to act. Renaming pages from
174
- // empty ids is risky and not in TASK-011 scope.
175
- skipped.push({
176
- pageId: page.id,
177
- taskId: "(none)",
178
- reason: "no-task-id",
179
- docsRelativePath: notionDocsPath,
180
- detail: `notion docs path: ${notionDocsPath}`,
181
- });
182
- trace.push({
183
- taskId: "(none)",
184
- pageId: page.id,
185
- notionDocsPath,
186
- localResolvedPath: notionDocsPath,
187
- decision: "skip-no-task-id",
188
- reason: "Notion row has Docs Path but no Task ID",
189
- });
190
- continue;
191
- }
192
- const taskId = taskIdRaw;
193
- // (A.2) duplicate Task ID across the considered rows.
194
- if (duplicatePageIds.has(page.id)) {
195
- skipped.push({
196
- pageId: page.id,
197
- taskId,
198
- reason: "duplicate-task-id",
199
- docsRelativePath: notionDocsPath,
200
- detail: `another Notion row already used this Task ID in the same query`,
201
- });
202
- trace.push({
203
- taskId,
204
- pageId: page.id,
205
- notionDocsPath,
206
- localResolvedPath: "",
207
- decision: "skip-duplicate-task-id",
208
- reason: "duplicate Task ID across considered rows — kept first row only",
209
- });
210
- continue;
211
- }
212
- // (B) Notion Docs Path exists but does NOT match this Task ID. Refuse
213
- // to create / overwrite — surface a `docs-path-mismatch` so the user
214
- // can fix Notion. We deliberately do NOT auto-rename — auto-fixing
215
- // Notion's Docs Path on a mismatch is reserved for a future
216
- // `--fix-docs-path` opt-in.
217
- if (notionDocsPath.length > 0 &&
218
- !docsPathMatchesTaskId(notionDocsPath, taskId)) {
219
- skipped.push({
220
- pageId: page.id,
221
- taskId,
222
- reason: "docs-path-mismatch",
223
- docsRelativePath: notionDocsPath,
224
- detail: `notion docs path: ${notionDocsPath}\n` +
225
- `expected basename prefix: ${taskId}- or ${taskId}.md\n` +
226
- `action: fix Notion 'Docs Path' for this row (auto-fix not enabled).`,
227
- });
228
- trace.push({
229
- taskId,
230
- pageId: page.id,
231
- notionDocsPath,
232
- localResolvedPath: notionDocsPath,
233
- decision: "skip-docs-path-mismatch",
234
- reason: `Notion Docs Path basename does not match ${taskId}- prefix`,
235
- });
236
- continue;
237
- }
238
- // (C) / (D) / (F) — resolve where the local file would live and check
239
- // for existing files. If Notion gave us a matching Docs Path, honour
240
- // it. Otherwise scan `docs/tasks` for any `TASK-NNN-*.md` already on
241
- // disk; if found, treat the row as `local-file-exists`. If not, plan a
242
- // new `TASK-NNN-slug.md`.
243
- const slug = slugify(row.name.length > 0 ? row.name : taskId, taskId.toLowerCase());
244
- let docsRelativePath;
245
- let absPath;
246
- let notionNeedsDocsPath;
247
- let decisionDetail;
248
- if (notionDocsPath.length > 0) {
249
- docsRelativePath = notionDocsPath;
250
- absPath = join(inputs.cwd, notionDocsPath);
251
- notionNeedsDocsPath = false;
252
- decisionDetail = `notion docs path: ${notionDocsPath}`;
253
- }
254
- else {
255
- const existing = await findLocalTaskFileForId(paths.docsTasks, taskId);
256
- if (existing !== null) {
257
- const rel = posix.join("docs/tasks", basename(existing));
258
- skipped.push({
259
- pageId: page.id,
260
- taskId,
261
- reason: "local-file-exists",
262
- docsRelativePath: rel,
263
- detail: `local resolved path: ${rel}\n` +
264
- `notion docs path: (empty)`,
265
- });
266
- trace.push({
267
- taskId,
268
- pageId: page.id,
269
- notionDocsPath: "",
270
- localResolvedPath: rel,
271
- decision: "skip-local-file-exists",
272
- reason: "Notion Docs Path empty, but a local file matching the Task ID was found on disk",
273
- });
274
- continue;
275
- }
276
- docsRelativePath = posix.join("docs/tasks", `${taskId}-${slug}.md`);
277
- absPath = join(paths.docsTasks, `${taskId}-${slug}.md`);
278
- notionNeedsDocsPath = true;
279
- decisionDetail = `local resolved path: ${docsRelativePath}`;
280
- }
281
- if (await pathExists(absPath)) {
282
- skipped.push({
283
- pageId: page.id,
284
- taskId,
285
- reason: "local-file-exists",
286
- docsRelativePath,
287
- detail: decisionDetail,
288
- });
289
- trace.push({
290
- taskId,
291
- pageId: page.id,
292
- notionDocsPath,
293
- localResolvedPath: docsRelativePath,
294
- decision: "skip-local-file-exists",
295
- reason: notionDocsPath.length > 0
296
- ? "Notion Docs Path matched Task ID and file already exists on disk"
297
- : "local search found a file matching the Task ID",
298
- });
299
- continue;
300
- }
301
- entries.push({
302
- pageId: page.id,
303
- taskId,
304
- title: row.name.length > 0 ? row.name : taskId,
305
- status: row.status,
306
- mvpPhase: row.mvpPhase,
307
- summary: row.summary,
308
- docsRelativePath,
309
- absPath,
310
- notionNeedsDocsPath,
311
- detail: decisionDetail,
312
- });
313
- trace.push({
314
- taskId,
315
- pageId: page.id,
316
- notionDocsPath,
317
- localResolvedPath: docsRelativePath,
318
- decision: "new-file",
319
- reason: notionDocsPath.length > 0
320
- ? "Notion Docs Path matched Task ID — local file does not exist yet"
321
- : "Notion Docs Path empty — planning fresh local file under docs/tasks",
322
- });
323
- }
324
- return { entries, skipped, considered: res.results.length, trace };
325
- }
326
- export async function executePullEntry(client, entry) {
327
- const md = renderPulledTaskMarkdown({
328
- taskId: entry.taskId,
329
- title: entry.title,
330
- status: entry.status,
331
- mvpPhase: entry.mvpPhase,
332
- summary: entry.summary,
333
- pageId: entry.pageId,
334
- docsRelativePath: entry.docsRelativePath,
335
- });
336
- await writeText(entry.absPath, md);
337
- let notionUpdated = false;
338
- if (entry.notionNeedsDocsPath) {
339
- await client.updatePage({
340
- pageId: entry.pageId,
341
- properties: {
342
- "Docs Path": richTextProperty(entry.docsRelativePath),
343
- },
344
- });
345
- notionUpdated = true;
346
- }
347
- return {
348
- taskId: entry.taskId,
349
- pageId: entry.pageId,
350
- absPath: entry.absPath,
351
- docsRelativePath: entry.docsRelativePath,
352
- notionUpdated,
353
- };
354
- }
@@ -1,103 +0,0 @@
1
- import { basename } from "node:path";
2
- import { isDirectory, isFile, pathExists } from "../lib/filesystem.js";
3
- import { readConfig } from "../lib/config.js";
4
- import { readGitInfo } from "../lib/git.js";
5
- import { getNotionTokenSource } from "../lib/notion-env.js";
6
- import { readPackageJson, } from "../lib/package-json.js";
7
- import { countTasks, pickNextTask, scanTasks } from "../lib/task.js";
8
- import { projectPaths } from "../lib/paths.js";
9
- import { DEFAULT_GITHUB_CONFIG, } from "../types/config.js";
10
- async function presenceCheck(label, path, required, asDir) {
11
- const present = asDir ? await isDirectory(path) : await isFile(path);
12
- return { label, path, present, required };
13
- }
14
- function snapshotNotion(notion, token) {
15
- const projects = (notion?.projectsTargetId ?? "").length > 0 ||
16
- (notion?.projectsDatabaseId ?? "").length > 0;
17
- const tasks = (notion?.tasksTargetId ?? "").length > 0 ||
18
- (notion?.tasksDatabaseId ?? "").length > 0;
19
- return {
20
- enabled: notion?.enabled ?? false,
21
- hasToken: token.hasToken,
22
- tokenSource: token.source,
23
- hasProjectsTarget: projects,
24
- hasTasksTarget: tasks,
25
- };
26
- }
27
- function snapshotGithub(github) {
28
- const g = github ?? DEFAULT_GITHUB_CONFIG;
29
- return {
30
- enabled: g.enabled,
31
- mode: g.enabled ? g.mode : "",
32
- owner: g.owner,
33
- repo: g.repo,
34
- remote: g.remote,
35
- url: g.url,
36
- };
37
- }
38
- function pickBinName(pkg) {
39
- const bin = pkg.bin;
40
- if (typeof bin === "string") {
41
- return basename(bin).replace(/\.[A-Za-z0-9]+$/, "");
42
- }
43
- if (bin !== null && typeof bin === "object") {
44
- const keys = Object.keys(bin);
45
- if (keys.length > 0)
46
- return keys[0];
47
- }
48
- return "";
49
- }
50
- function snapshotPackage(pkg) {
51
- if (pkg === null) {
52
- return { exists: false, name: "", version: "", bin: "" };
53
- }
54
- const data = pkg.data;
55
- const name = typeof data.name === "string" ? data.name : "";
56
- const version = typeof data.version === "string"
57
- ? (data.version ?? "")
58
- : "";
59
- return { exists: true, name, version, bin: pickBinName(data) };
60
- }
61
- export async function collectStatus(cwd) {
62
- const paths = projectPaths(cwd);
63
- const config = await readConfig(paths.root);
64
- const checks = [
65
- await presenceCheck(".vibeops.json", paths.config, true, false),
66
- await presenceCheck("AGENTS.md", paths.agentsMd, true, false),
67
- await presenceCheck(".cursor/rules/", paths.cursorRules, true, true),
68
- await presenceCheck("docs/project/", paths.docsProject, true, true),
69
- await presenceCheck("docs/tasks/", paths.docsTasks, true, true),
70
- await presenceCheck("docs/logs/", paths.docsLogs, false, true),
71
- await presenceCheck(".vibeops/agents/", paths.vibeopsAgents, true, true),
72
- await presenceCheck(".vibeops/prompts/", paths.vibeopsPrompts, false, true),
73
- await presenceCheck(".vibeops/workflows/", paths.vibeopsWorkflows, false, true),
74
- await presenceCheck(".vibeops.env.example", paths.envExample, false, false),
75
- ];
76
- const missingRequired = checks.filter((c) => c.required && !c.present);
77
- const tasks = (await pathExists(paths.docsTasks))
78
- ? await scanTasks(paths.docsTasks)
79
- : [];
80
- const taskCounts = countTasks(tasks);
81
- const nextTask = pickNextTask(tasks);
82
- const git = await readGitInfo(paths.root);
83
- const tokenProbe = await getNotionTokenSource(paths.root);
84
- const notion = snapshotNotion(config?.notion, tokenProbe);
85
- const github = snapshotGithub(config?.github);
86
- const pkg = await readPackageJson(paths.root);
87
- const packageSnapshot = snapshotPackage(pkg);
88
- const isVibeopsProject = config !== null && missingRequired.length === 0;
89
- return {
90
- cwd: paths.root,
91
- isVibeopsProject,
92
- config,
93
- checks,
94
- missingRequired,
95
- tasks,
96
- taskCounts,
97
- nextTask,
98
- git,
99
- notion,
100
- github,
101
- package: packageSnapshot,
102
- };
103
- }
@@ -1,177 +0,0 @@
1
- import { relative } from "node:path";
2
- import { bold, cyan, dim, green, log, red, yellow } from "../lib/logger.js";
3
- function relOrAbs(root, p) {
4
- const r = relative(root, p);
5
- if (r === "")
6
- return ".";
7
- if (r.startsWith(".."))
8
- return p;
9
- return r;
10
- }
11
- function statusBadge(value, label) {
12
- if (value === 0)
13
- return dim(`${label}:0`);
14
- return `${label}:${value}`;
15
- }
16
- function pad(label, width) {
17
- if (label.length >= width)
18
- return label;
19
- return label + " ".repeat(width - label.length);
20
- }
21
- function tokenLine(report) {
22
- if (!report.notion.hasToken)
23
- return red("missing");
24
- return `${green("configured")} ${dim(`(${report.notion.tokenSource})`)}`;
25
- }
26
- function targetLine(present) {
27
- return present ? green("configured") : red("missing");
28
- }
29
- function notionHint(report) {
30
- if (!report.notion.enabled)
31
- return "run `vibeops notion init`";
32
- if (!report.notion.hasToken)
33
- return "set NOTION_TOKEN in `.vibeops.env`";
34
- if (!report.notion.hasProjectsTarget || !report.notion.hasTasksTarget) {
35
- return "run `vibeops notion init` to pick a Projects / Tasks DB";
36
- }
37
- return "run `vibeops notion test`";
38
- }
39
- export function printHuman(report) {
40
- log.info(bold("Project"));
41
- if (report.config) {
42
- log.info(` name ${report.config.name}`);
43
- log.info(` vibeopsVersion ${report.config.vibeopsVersion}`);
44
- log.info(` schemaVersion ${String(report.config.schemaVersion)}`);
45
- log.info(` createdAt ${report.config.createdAt}`);
46
- }
47
- else {
48
- log.info(` ${red("(no .vibeops.json — not a VibeOps project)")}`);
49
- }
50
- log.blank();
51
- log.info(bold("Installation"));
52
- for (const c of report.checks) {
53
- const tag = c.present
54
- ? green("✓")
55
- : c.required
56
- ? red("✗")
57
- : yellow("·");
58
- const reqHint = c.required ? "" : dim(" (optional)");
59
- log.info(` ${tag} ${c.label}${reqHint} ${dim(relOrAbs(report.cwd, c.path))}`);
60
- }
61
- if (report.missingRequired.length > 0) {
62
- log.blank();
63
- log.warn(`${report.missingRequired.length} required path(s) missing.`);
64
- }
65
- log.blank();
66
- log.info(bold("Tasks"));
67
- const { total, planned, in_progress, review, blocked, done } = report.taskCounts;
68
- log.info(` ${cyan(`total:${total}`)} ${statusBadge(planned, "planned")} ${statusBadge(in_progress, "in_progress")} ${statusBadge(review, "review")} ${statusBadge(blocked, "blocked")} ${statusBadge(done, "done")}`);
69
- if (report.nextTask) {
70
- log.info(` next ${report.nextTask.id} — ${report.nextTask.title || dim("(no title)")} ${dim(`[${report.nextTask.status}]`)}`);
71
- }
72
- else if (total === 0) {
73
- log.info(` ${dim("no TASK files yet — run `vibeops task generate`")}`);
74
- }
75
- else {
76
- log.info(` ${dim("all tasks done")}`);
77
- }
78
- log.blank();
79
- log.info(bold("Git"));
80
- if (!report.git.isRepo) {
81
- log.info(` ${dim("not a git repository")}`);
82
- }
83
- else {
84
- if (report.git.state === "unborn") {
85
- log.info(` branch ${report.git.branch ?? "(unknown)"} ${dim("(unborn, no commits yet)")}`);
86
- }
87
- else if (report.git.state === "detached") {
88
- log.info(` branch ${dim("(detached)")}${report.git.branch ? ` ${report.git.branch}` : ""}`);
89
- }
90
- else {
91
- log.info(` branch ${report.git.branch ?? dim("(unknown)")}`);
92
- }
93
- log.info(` status ${report.git.dirty ? yellow("dirty") : green("clean")}`);
94
- if (report.git.state === "unborn") {
95
- log.info(` hint create the first commit or run ${cyan("`vibeops init --git --initial-commit`")}`);
96
- }
97
- }
98
- log.blank();
99
- // Notion: enabled / token / projects target / tasks target / hint.
100
- // We intentionally do NOT print legacy env names (NOTION_API_KEY,
101
- // NOTION_PROJECT_DB, NOTION_TASK_DB) — modern VibeOps only uses
102
- // NOTION_TOKEN + notion.{projectsTargetId,tasksTargetId}.
103
- log.info(bold("Notion"));
104
- const notionLabelWidth = 16; // "projects target"
105
- log.info(` ${pad("enabled", notionLabelWidth)} ${report.notion.enabled ? green("yes") : dim("no")}`);
106
- log.info(` ${pad("token", notionLabelWidth)} ${tokenLine(report)}`);
107
- log.info(` ${pad("projects target", notionLabelWidth)} ${targetLine(report.notion.hasProjectsTarget)}`);
108
- log.info(` ${pad("tasks target", notionLabelWidth)} ${targetLine(report.notion.hasTasksTarget)}`);
109
- log.info(` ${pad("hint", notionLabelWidth)} ${dim(notionHint(report))}`);
110
- log.blank();
111
- log.info(bold("GitHub"));
112
- const githubLabelWidth = 11; // "owner/repo"
113
- if (!report.github.enabled) {
114
- log.info(` ${pad("enabled", githubLabelWidth)} ${dim("no")}`);
115
- log.info(` ${pad("hint", githubLabelWidth)} ${dim("run `vibeops github init`")}`);
116
- }
117
- else {
118
- log.info(` ${pad("enabled", githubLabelWidth)} ${green("yes")}`);
119
- if (report.github.mode.length > 0) {
120
- log.info(` ${pad("mode", githubLabelWidth)} ${report.github.mode}`);
121
- }
122
- const slug = report.github.owner.length > 0 && report.github.repo.length > 0
123
- ? `${report.github.owner}/${report.github.repo}`
124
- : dim("(unknown)");
125
- log.info(` ${pad("owner/repo", githubLabelWidth)} ${slug}`);
126
- log.info(` ${pad("remote", githubLabelWidth)} ${report.github.remote.length > 0 ? report.github.remote : dim("(none)")}`);
127
- log.info(` ${pad("url", githubLabelWidth)} ${report.github.url.length > 0 ? report.github.url : dim("(none)")}`);
128
- }
129
- log.blank();
130
- log.info(bold("Package"));
131
- const pkgLabelWidth = 8; // "version"
132
- if (!report.package.exists) {
133
- log.info(` ${dim("package.json missing")}`);
134
- }
135
- else {
136
- log.info(` ${pad("name", pkgLabelWidth)} ${report.package.name.length > 0 ? report.package.name : dim("(unset)")}`);
137
- log.info(` ${pad("version", pkgLabelWidth)} ${report.package.version.length > 0 ? report.package.version : dim("(unset)")}`);
138
- log.info(` ${pad("bin", pkgLabelWidth)} ${report.package.bin.length > 0 ? report.package.bin : dim("(none)")}`);
139
- }
140
- log.blank();
141
- if (!report.isVibeopsProject) {
142
- log.error("Not a VibeOps project — run `vibeops init` first.");
143
- }
144
- else {
145
- log.ok("VibeOps project ready.");
146
- }
147
- }
148
- export function toJson(report) {
149
- return `${JSON.stringify({
150
- cwd: report.cwd,
151
- isVibeopsProject: report.isVibeopsProject,
152
- config: report.config,
153
- installation: {
154
- checks: report.checks.map((c) => ({
155
- label: c.label,
156
- path: c.path,
157
- present: c.present,
158
- required: c.required,
159
- })),
160
- missingRequired: report.missingRequired.map((c) => c.label),
161
- },
162
- tasks: {
163
- counts: report.taskCounts,
164
- next: report.nextTask
165
- ? {
166
- id: report.nextTask.id,
167
- title: report.nextTask.title,
168
- status: report.nextTask.status,
169
- }
170
- : null,
171
- },
172
- git: report.git,
173
- notion: report.notion,
174
- github: report.github,
175
- package: report.package,
176
- }, null, 2)}\n`;
177
- }