@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,405 +0,0 @@
1
- import { resolve } from "node:path";
2
- import { bold, cyan, dim, gray, green, log, red, yellow } from "../lib/logger.js";
3
- import { maskToken } from "../lib/notion-env.js";
4
- import { notionApiError } from "../lib/notion-client.js";
5
- import { MISSING_PROPERTIES_HINT, PROJECTS_DB_PROPERTIES, STATUS_OPTIONS_HINT, TASKS_DB_PROPERTIES, } from "../lib/notion-schema.js";
6
- import { createNotionClient, executeProjectUpsert, executeTaskUpsert, fetchSchemas, loadSyncContext, planSync, } from "../lib/notion-sync.js";
7
- function emptyReport(cwd, dryRun) {
8
- return {
9
- cwd,
10
- ok: false,
11
- dryRun,
12
- tokenMasked: null,
13
- notion: null,
14
- project: null,
15
- counts: {
16
- project: { create: 0, update: 0 },
17
- tasks: { create: 0, update: 0 },
18
- },
19
- entries: [],
20
- errors: [],
21
- };
22
- }
23
- export async function notionSyncCommand(options = {}) {
24
- const cwd = resolve(options.cwd ?? process.cwd());
25
- const dryRun = options.dryRun === true;
26
- const wantJson = options.json === true;
27
- const report = emptyReport(cwd, dryRun);
28
- const ctxRes = await loadSyncContext(cwd);
29
- if (!ctxRes.ok) {
30
- report.errors.push({ reason: ctxRes.reason, message: ctxRes.message });
31
- return finalize(report, wantJson, "preflight");
32
- }
33
- const ctx = ctxRes;
34
- report.tokenMasked = maskToken(ctx.token);
35
- report.notion = {
36
- enabled: ctx.notion.enabled,
37
- projectsTargetId: ctx.notion.projectsTargetId,
38
- tasksTargetId: ctx.notion.tasksTargetId,
39
- projectsDatabaseId: ctx.notion.projectsDatabaseId,
40
- tasksDatabaseId: ctx.notion.tasksDatabaseId,
41
- };
42
- report.project = {
43
- projectId: ctx.project.projectId,
44
- name: ctx.project.projectName,
45
- currentPhase: ctx.project.currentPhase,
46
- gitRepoUrl: ctx.project.gitRepoUrl,
47
- docsPath: ctx.project.docsProjectPath,
48
- };
49
- let client;
50
- try {
51
- client = await createNotionClient(ctx.token);
52
- }
53
- catch (err) {
54
- const apiErr = notionApiError(err);
55
- report.errors.push({
56
- reason: "sdk-load",
57
- message: `Failed to load @notionhq/client — ${apiErr.message}`,
58
- details: apiErr,
59
- });
60
- return finalize(report, wantJson, "preflight");
61
- }
62
- const schemaRes = await fetchSchemas(client, ctx.notion);
63
- if (!schemaRes.ok) {
64
- report.errors.push({
65
- reason: schemaRes.reason,
66
- message: explainNotionError(schemaRes.error),
67
- details: schemaRes.error,
68
- });
69
- return finalize(report, wantJson, "preflight");
70
- }
71
- report.schemas = [
72
- {
73
- kind: "projects",
74
- inputId: schemaRes.projects.inputId,
75
- inputObject: schemaRes.projects.inputObject,
76
- resolvedId: schemaRes.projects.resolvedId,
77
- resolvedObject: schemaRes.projects.resolvedObject,
78
- source: schemaRes.projects.source,
79
- parentKind: schemaRes.projects.resolvedObject === "data_source"
80
- ? "data_source_id"
81
- : "database_id",
82
- ...(schemaRes.projects.parentDatabaseId !== undefined
83
- ? { parentDatabaseId: schemaRes.projects.parentDatabaseId }
84
- : {}),
85
- propertiesMissing: schemaRes.projects.propertiesMissing,
86
- violationsCount: schemaRes.projects.violations.length,
87
- warnings: schemaRes.projects.warnings,
88
- },
89
- {
90
- kind: "tasks",
91
- inputId: schemaRes.tasks.inputId,
92
- inputObject: schemaRes.tasks.inputObject,
93
- resolvedId: schemaRes.tasks.resolvedId,
94
- resolvedObject: schemaRes.tasks.resolvedObject,
95
- source: schemaRes.tasks.source,
96
- parentKind: schemaRes.tasks.resolvedObject === "data_source"
97
- ? "data_source_id"
98
- : "database_id",
99
- ...(schemaRes.tasks.parentDatabaseId !== undefined
100
- ? { parentDatabaseId: schemaRes.tasks.parentDatabaseId }
101
- : {}),
102
- propertiesMissing: schemaRes.tasks.propertiesMissing,
103
- violationsCount: schemaRes.tasks.violations.length,
104
- warnings: schemaRes.tasks.warnings,
105
- },
106
- ];
107
- const violations = [
108
- ...schemaRes.projects.violations,
109
- ...schemaRes.tasks.violations,
110
- ];
111
- if (violations.length > 0) {
112
- const propertiesMissing = schemaRes.projects.propertiesMissing ||
113
- schemaRes.tasks.propertiesMissing;
114
- const statusOptionsMissing = violations.some((v) => v.kind === "status-options-missing" ||
115
- v.kind === "status-options-unreadable");
116
- let message;
117
- let reason;
118
- if (propertiesMissing) {
119
- message = `Notion DB response is missing a properties object. ${MISSING_PROPERTIES_HINT}`;
120
- reason = "schema-missing-properties";
121
- }
122
- else if (statusOptionsMissing) {
123
- message = `Notion Status property is missing required options (${violations.length} violation${violations.length === 1 ? "" : "s"}). ${STATUS_OPTIONS_HINT}`;
124
- reason = "schema-status-options";
125
- }
126
- else {
127
- message = `Notion DB schema does not match VibeOps requirements (${violations.length} violation${violations.length === 1 ? "" : "s"}).`;
128
- reason = "schema";
129
- }
130
- report.errors.push({
131
- reason,
132
- message,
133
- details: violations,
134
- });
135
- // Sync fast-fails BEFORE any mutation when schema is incomplete — even in
136
- // actual run we never partially upsert when status options are missing.
137
- return finalize(report, wantJson, "schema");
138
- }
139
- let plan;
140
- try {
141
- plan = await planSync({
142
- ctx,
143
- schemas: schemaRes,
144
- detectExisting: true,
145
- onlyTasks: options.onlyTasks === true,
146
- onlyProject: options.onlyProject === true,
147
- client,
148
- });
149
- }
150
- catch (err) {
151
- const apiErr = notionApiError(err);
152
- report.errors.push({
153
- reason: "query",
154
- message: explainNotionError(apiErr),
155
- details: apiErr,
156
- });
157
- return finalize(report, wantJson, "query");
158
- }
159
- report.counts = plan.counts;
160
- if (plan.project) {
161
- report.entries.push(planEntryToReport(plan.project, ctx));
162
- }
163
- for (const t of plan.tasks) {
164
- report.entries.push(taskEntryToReport(t));
165
- }
166
- if (dryRun) {
167
- report.ok = true;
168
- return finalize(report, wantJson, "dry-run");
169
- }
170
- let mutateFailed = false;
171
- const projectsTargetId = schemaRes.projects.resolvedId;
172
- const projectsParentKind = schemaRes.projects.resolvedObject === "data_source" ? "data_source_id" : "database_id";
173
- const tasksTargetId = schemaRes.tasks.resolvedId;
174
- const tasksParentKind = schemaRes.tasks.resolvedObject === "data_source" ? "data_source_id" : "database_id";
175
- if (plan.project) {
176
- try {
177
- const r = await executeProjectUpsert(client, projectsTargetId, plan.project);
178
- const idx = report.entries.findIndex((e) => e.kind === "project");
179
- if (idx >= 0)
180
- report.entries[idx].pageId = r.pageId;
181
- }
182
- catch (err) {
183
- mutateFailed = true;
184
- const apiErr = notionApiError(err);
185
- report.errors.push({
186
- reason: "project-upsert",
187
- message: formatMutateError({
188
- err: apiErr,
189
- action: plan.project.verb === "update" ? "update-page" : "create-page",
190
- parentKind: projectsParentKind,
191
- targetId: projectsTargetId,
192
- }),
193
- details: apiErr,
194
- });
195
- }
196
- }
197
- for (const entry of plan.tasks) {
198
- try {
199
- const r = await executeTaskUpsert(client, tasksTargetId, entry);
200
- const idx = report.entries.findIndex((e) => e.kind === "task" && e.taskId === entry.taskId);
201
- if (idx >= 0)
202
- report.entries[idx].pageId = r.pageId;
203
- }
204
- catch (err) {
205
- mutateFailed = true;
206
- const apiErr = notionApiError(err);
207
- report.errors.push({
208
- reason: "task-upsert",
209
- message: `${entry.taskId}: ${formatMutateError({
210
- err: apiErr,
211
- action: entry.verb === "update" ? "update-page" : "create-page",
212
- parentKind: tasksParentKind,
213
- targetId: tasksTargetId,
214
- })}`,
215
- details: apiErr,
216
- });
217
- }
218
- }
219
- report.ok = !mutateFailed;
220
- finalize(report, wantJson, mutateFailed ? "mutate-error" : "ok");
221
- }
222
- /**
223
- * Mutation-time error message. Always carries:
224
- * - action (create/update/query)
225
- * - target id (the data_source id we used)
226
- * - parent kind (data_source_id vs database_id) for create
227
- * NEVER prints the bearer token.
228
- */
229
- function formatMutateError(inputs) {
230
- const head = explainNotionError(inputs.err);
231
- const parts = [`action=${inputs.action}`, `target=${inputs.targetId}`];
232
- if (inputs.action === "create-page" && inputs.parentKind !== undefined) {
233
- parts.push(`parent=${inputs.parentKind}`);
234
- }
235
- const hint = mutateHint(inputs.err);
236
- return `${head} [${parts.join(", ")}]${hint}`;
237
- }
238
- function mutateHint(err) {
239
- if (err.status === 404) {
240
- return " — Notion could not find the target id. Verify the resolved data_source id, and confirm the integration is connected to that data_source directly. Diagnose with `vibeops notion test --debug-shape`.";
241
- }
242
- // "Invalid status option" / "Invalid select option" — the options VibeOps
243
- // uses are not registered in Notion. The status-options validator should
244
- // catch this earlier, but environments where Notion does not return
245
- // status options can still trip on the actual run; surface guidance here.
246
- if (err.code === "validation_error" &&
247
- /Invalid (status|select) option/i.test(err.message)) {
248
- return ` — ${STATUS_OPTIONS_HINT}`;
249
- }
250
- return "";
251
- }
252
- function planEntryToReport(entry, ctx) {
253
- return {
254
- kind: "project",
255
- verb: entry.verb,
256
- title: ctx.project.projectName,
257
- taskId: ctx.project.projectId,
258
- pageId: entry.existingPageId,
259
- docsRelativePath: ctx.project.docsProjectPath,
260
- };
261
- }
262
- function taskEntryToReport(entry) {
263
- return {
264
- kind: "task",
265
- verb: entry.verb,
266
- taskId: entry.taskId,
267
- title: entry.title,
268
- pageId: entry.existingPageId,
269
- docsRelativePath: entry.docsRelativePath,
270
- };
271
- }
272
- function explainNotionError(err) {
273
- const tail = err.status ? ` (HTTP ${err.status})` : "";
274
- switch (err.code) {
275
- case "unauthorized":
276
- return `NOTION_TOKEN was rejected. Verify the integration is not expired and the value is correct.${tail}`;
277
- case "restricted_resource":
278
- return `The Notion DB is not shared with the integration. Add it via Notion DB → Connections.${tail}`;
279
- case "object_not_found":
280
- return `Notion resource not found. Verify the database id / page id.${tail}`;
281
- case "validation_error":
282
- return `Request rejected (validation_error): ${err.message}${tail}`;
283
- case "rate_limited":
284
- return `Notion API rate limit — retry shortly.${tail}`;
285
- case "request_timeout":
286
- case "ETIMEDOUT":
287
- return `Notion API 5s timeout. Check your network.${tail}`;
288
- default:
289
- return `${err.code}: ${err.message}${tail}`;
290
- }
291
- }
292
- function finalize(report, wantJson, phase) {
293
- if (wantJson) {
294
- process.stdout.write(`${JSON.stringify({ phase, ...report }, null, 2)}\n`);
295
- process.exitCode = report.ok ? 0 : 1;
296
- return;
297
- }
298
- log.info(bold("vibeops notion sync"));
299
- log.info(` ${dim("cwd")} ${report.cwd}${report.dryRun ? ` ${yellow("[dry-run]")}` : ""}`);
300
- if (report.tokenMasked) {
301
- log.info(` ${dim("token")} ${report.tokenMasked}`);
302
- }
303
- if (report.project) {
304
- log.info(` ${dim("project")} ${report.project.name} ${gray(`(id=${report.project.projectId})`)}`);
305
- log.info(` ${dim("phase")} ${report.project.currentPhase} ${dim("git remote")} ${report.project.gitRepoUrl.length > 0 ? report.project.gitRepoUrl : gray("(none)")}`);
306
- }
307
- log.blank();
308
- if (report.schemas !== undefined && report.schemas.length > 0) {
309
- for (const s of report.schemas) {
310
- const status = s.propertiesMissing
311
- ? red("missing-properties")
312
- : s.violationsCount > 0
313
- ? red(`${s.violationsCount} violation${s.violationsCount === 1 ? "" : "s"}`)
314
- : green("schema valid");
315
- log.info(` ${bold(`${s.kind} DB target`)}`);
316
- log.info(` ${dim("input id ")} ${cyan(s.inputId)}`);
317
- log.info(` ${dim("input object ")} ${s.inputObject}`);
318
- log.info(` ${dim("resolved id ")} ${cyan(s.resolvedId)}`);
319
- log.info(` ${dim("resolved object")} ${s.resolvedObject}`);
320
- log.info(` ${dim("source ")} ${s.source}`);
321
- if (s.parentDatabaseId !== undefined) {
322
- log.info(` ${dim("parent database")} ${gray(s.parentDatabaseId)}`);
323
- }
324
- log.info(` ${dim("create parent ")} ${s.parentKind} ${cyan(s.resolvedId)}`);
325
- log.info(` ${dim("query target ")} ${s.parentKind === "data_source_id" ? "data_source" : "database"} ${cyan(s.resolvedId)}`);
326
- log.info(` ${dim("schema ")} ${status}`);
327
- for (const w of s.warnings) {
328
- log.info(` ${dim("warning ")} ${yellow(w)}`);
329
- }
330
- }
331
- log.blank();
332
- }
333
- if (report.errors.length > 0) {
334
- for (const e of report.errors) {
335
- log.error(`${cyan(e.reason)} — ${e.message}`);
336
- if ((e.reason === "schema" ||
337
- e.reason === "schema-missing-properties" ||
338
- e.reason === "schema-status-options") &&
339
- Array.isArray(e.details)) {
340
- for (const v of e.details) {
341
- if (v.kind === "status-options-missing" ||
342
- v.kind === "status-options-unreadable") {
343
- log.info(` · ${red(v.kind)} ${cyan(`${v.db}.${v.property}`)}`);
344
- if (v.kind === "status-options-missing" &&
345
- v.missingOptions !== undefined &&
346
- v.missingOptions.length > 0) {
347
- log.info(` ${dim("missing")} ${v.missingOptions.join(", ")}`);
348
- }
349
- if (v.requiredOptions !== undefined &&
350
- v.requiredOptions.length > 0) {
351
- log.info(` ${dim("Add these options in Notion")}: Status property → Edit options → ${v.requiredOptions.join(", ")}`);
352
- }
353
- if (v.kind === "status-options-missing" &&
354
- v.foundOptions !== undefined &&
355
- v.foundOptions.length > 0) {
356
- log.info(` ${dim("found in Notion")}: ${v.foundOptions.join(", ")}`);
357
- }
358
- continue;
359
- }
360
- const detail = v.kind === "missing-properties"
361
- ? // The resolver puts the actionable hint into description —
362
- // surface that instead of the static "no properties object" line.
363
- v.description
364
- : v.kind === "missing"
365
- ? `expected types: ${v.allowedTypes.join(" | ")}`
366
- : `expected ${v.allowedTypes.join(" | ")} but got ${v.actualType ?? "?"}`;
367
- log.info(` · ${red(v.kind)} ${cyan(`${v.db}.${v.property}`)} — ${dim(detail)}`);
368
- }
369
- log.info(dim(` All 8 Projects DB properties, 10 Tasks DB properties, and the required Status options must be present. Inspect details with \`vibeops notion test\`.`));
370
- }
371
- }
372
- log.blank();
373
- }
374
- const total = report.counts.project.create + report.counts.project.update;
375
- const tasksTotal = report.counts.tasks.create + report.counts.tasks.update;
376
- log.info(` ${bold("Project")} ${green(`create ${report.counts.project.create}`)} ${cyan(`update ${report.counts.project.update}`)} ${dim(`total ${total}`)}`);
377
- log.info(` ${bold("Tasks")} ${green(`create ${report.counts.tasks.create}`)} ${cyan(`update ${report.counts.tasks.update}`)} ${dim(`total ${tasksTotal}`)}`);
378
- log.blank();
379
- if (report.entries.length > 0) {
380
- log.info(bold("preview"));
381
- for (const e of report.entries) {
382
- const verbTag = e.verb === "create" ? green("create") : cyan("update");
383
- const kindTag = e.kind === "project" ? gray("project") : gray("task ");
384
- const id = e.taskId ?? "";
385
- const path = e.docsRelativePath ? dim(` ${e.docsRelativePath}`) : "";
386
- const title = e.title ? ` ${e.title}` : "";
387
- log.info(` ${verbTag} ${kindTag} ${cyan(id)}${title}${path}`);
388
- }
389
- log.blank();
390
- }
391
- log.info(dim(` schemas covered: Projects(${PROJECTS_DB_PROPERTIES.length}) · Tasks(${TASKS_DB_PROPERTIES.length}).`));
392
- if (report.dryRun) {
393
- log.info(yellow(" dry-run — no Notion mutation performed."));
394
- }
395
- if (report.ok) {
396
- log.ok(report.dryRun
397
- ? "Sync plan OK — re-run without --dry-run to apply."
398
- : "Notion sync complete.");
399
- process.exitCode = 0;
400
- }
401
- else {
402
- log.error("Notion sync failed — see errors above.");
403
- process.exitCode = 1;
404
- }
405
- }