@goodtek/vibeops 0.2.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 (93) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +444 -0
  4. package/dist/agent/loader.js +71 -0
  5. package/dist/agent/prompt.js +66 -0
  6. package/dist/bootstrap/installer.js +149 -0
  7. package/dist/bootstrap/manifest.js +15 -0
  8. package/dist/bootstrap/substitute.js +35 -0
  9. package/dist/cli.js +241 -0
  10. package/dist/commands/agent-list.js +32 -0
  11. package/dist/commands/agent-prompt.js +59 -0
  12. package/dist/commands/agent-show.js +26 -0
  13. package/dist/commands/github-init.js +554 -0
  14. package/dist/commands/github-status.js +164 -0
  15. package/dist/commands/init.js +179 -0
  16. package/dist/commands/notion-init.js +764 -0
  17. package/dist/commands/notion-sync.js +405 -0
  18. package/dist/commands/notion-test.js +595 -0
  19. package/dist/commands/plan.js +114 -0
  20. package/dist/commands/status.js +17 -0
  21. package/dist/commands/task-check.js +155 -0
  22. package/dist/commands/task-done.js +98 -0
  23. package/dist/commands/task-generate.js +206 -0
  24. package/dist/commands/task-pull.js +277 -0
  25. package/dist/commands/task-rollback.js +174 -0
  26. package/dist/commands/task-start.js +90 -0
  27. package/dist/lib/brief.js +349 -0
  28. package/dist/lib/config.js +158 -0
  29. package/dist/lib/filesystem.js +67 -0
  30. package/dist/lib/git.js +237 -0
  31. package/dist/lib/github-cli.js +247 -0
  32. package/dist/lib/inquirer-helpers.js +111 -0
  33. package/dist/lib/logger.js +42 -0
  34. package/dist/lib/notion-client.js +459 -0
  35. package/dist/lib/notion-discovery.js +671 -0
  36. package/dist/lib/notion-env.js +140 -0
  37. package/dist/lib/notion-mappers.js +148 -0
  38. package/dist/lib/notion-schema.js +272 -0
  39. package/dist/lib/notion-sync.js +337 -0
  40. package/dist/lib/notion-target.js +247 -0
  41. package/dist/lib/package-json.js +133 -0
  42. package/dist/lib/paths.js +26 -0
  43. package/dist/lib/project-docs.js +95 -0
  44. package/dist/lib/prompt-builder.js +125 -0
  45. package/dist/lib/task-generator.js +183 -0
  46. package/dist/lib/task-prompt.js +23 -0
  47. package/dist/lib/task-pull.js +354 -0
  48. package/dist/lib/task-scaffold.js +128 -0
  49. package/dist/lib/task-summary.js +276 -0
  50. package/dist/lib/task.js +364 -0
  51. package/dist/status/collector.js +103 -0
  52. package/dist/status/format.js +177 -0
  53. package/dist/types/brief.js +126 -0
  54. package/dist/types/config.js +17 -0
  55. package/dist/types/task.js +1 -0
  56. package/dist/version.js +8 -0
  57. package/package.json +61 -0
  58. package/templates/.cursor/rules/00-project-governance.mdc +28 -0
  59. package/templates/.cursor/rules/01-agent-orchestration.mdc +48 -0
  60. package/templates/.cursor/rules/02-task-workflow.mdc +38 -0
  61. package/templates/.cursor/rules/03-git-safety.mdc +30 -0
  62. package/templates/.cursor/rules/04-docs-update.mdc +22 -0
  63. package/templates/.vibeops/agents/architect.md +47 -0
  64. package/templates/.vibeops/agents/builder.md +38 -0
  65. package/templates/.vibeops/agents/docs.md +54 -0
  66. package/templates/.vibeops/agents/orchestrator.md +40 -0
  67. package/templates/.vibeops/agents/planner.md +60 -0
  68. package/templates/.vibeops/agents/recovery.md +49 -0
  69. package/templates/.vibeops/agents/reviewer.md +47 -0
  70. package/templates/.vibeops/agents/tester.md +43 -0
  71. package/templates/.vibeops/prompts/create-plan.md +33 -0
  72. package/templates/.vibeops/prompts/generate-tasks.md +41 -0
  73. package/templates/.vibeops/prompts/implement-task.md +39 -0
  74. package/templates/.vibeops/prompts/review-task.md +34 -0
  75. package/templates/.vibeops/prompts/rollback.md +32 -0
  76. package/templates/.vibeops/prompts/start-project.md +39 -0
  77. package/templates/.vibeops/workflows/notion-sync.md +53 -0
  78. package/templates/.vibeops/workflows/project-start.md +73 -0
  79. package/templates/.vibeops/workflows/rollback.md +45 -0
  80. package/templates/.vibeops/workflows/task-lifecycle.md +71 -0
  81. package/templates/AGENTS.md +98 -0
  82. package/templates/docs/logs/README.md +38 -0
  83. package/templates/docs/project/00-overview.md +27 -0
  84. package/templates/docs/project/01-requirements.md +30 -0
  85. package/templates/docs/project/02-mvp-scope.md +36 -0
  86. package/templates/docs/project/03-architecture.md +34 -0
  87. package/templates/docs/project/04-tech-stack.md +29 -0
  88. package/templates/docs/project/05-current-state.md +35 -0
  89. package/templates/docs/project/06-decisions.md +20 -0
  90. package/templates/docs/project/07-backlog.md +23 -0
  91. package/templates/docs/project/08-env.md +29 -0
  92. package/templates/docs/project/09-deployment.md +28 -0
  93. package/templates/docs/tasks/TASK-000-template.md +72 -0
@@ -0,0 +1,595 @@
1
+ import { resolve } from "node:path";
2
+ import { notionProjectsTargetId, notionTasksTargetId, readConfig, } from "../lib/config.js";
3
+ import { bold, cyan, dim, green, log, red, yellow } from "../lib/logger.js";
4
+ import { loadNotionEnv, maskToken } from "../lib/notion-env.js";
5
+ import { createNotionClient, notionApiError, } from "../lib/notion-client.js";
6
+ import { PROJECTS_DB_PROPERTIES, TASKS_DB_PROPERTIES, validateDatabaseSchema, } from "../lib/notion-schema.js";
7
+ import { resolveNotionDataSourceTarget, } from "../lib/notion-target.js";
8
+ import { projectPaths } from "../lib/paths.js";
9
+ export async function notionTestCommand(options = {}) {
10
+ const cwd = resolve(options.cwd ?? process.cwd());
11
+ const paths = projectPaths(cwd);
12
+ const wantJson = options.json === true;
13
+ const wantShape = options.debugShape === true;
14
+ const report = {
15
+ cwd,
16
+ configPresent: false,
17
+ notion: null,
18
+ envSource: "none",
19
+ hasToken: false,
20
+ tokenMasked: null,
21
+ checks: [],
22
+ ok: false,
23
+ };
24
+ const config = await readConfig(paths.root);
25
+ report.configPresent = config !== null;
26
+ const cfgCheck = config
27
+ ? {
28
+ id: "config.present",
29
+ label: "Load .vibeops.json",
30
+ ok: true,
31
+ status: "ok",
32
+ }
33
+ : {
34
+ id: "config.present",
35
+ label: "Load .vibeops.json",
36
+ ok: false,
37
+ status: "fail",
38
+ detail: `${paths.config} is missing. Run \`vibeops init\` first.`,
39
+ };
40
+ report.checks.push(cfgCheck);
41
+ if (config === null) {
42
+ return finalize(report, wantJson);
43
+ }
44
+ const notion = config.notion ?? null;
45
+ report.notion = notion;
46
+ const enabledCheck = notion?.enabled
47
+ ? {
48
+ id: "config.notion.enabled",
49
+ label: "notion.enabled = true",
50
+ ok: true,
51
+ status: "ok",
52
+ }
53
+ : {
54
+ id: "config.notion.enabled",
55
+ label: "notion.enabled = true",
56
+ ok: false,
57
+ status: "fail",
58
+ detail: `Currently false (or missing). Enable it with \`vibeops notion init --enable\`.`,
59
+ };
60
+ report.checks.push(enabledCheck);
61
+ const projectsInputId = notion === null ? "" : notionProjectsTargetId(notion);
62
+ const tasksInputId = notion === null ? "" : notionTasksTargetId(notion);
63
+ const projDbCheck = notion && projectsInputId.length > 0
64
+ ? {
65
+ id: "config.notion.projectsTarget",
66
+ label: "notion.projectsTargetId/projectsDatabaseId configured",
67
+ ok: true,
68
+ status: "ok",
69
+ detail: notion.projectsTargetId.length > 0
70
+ ? `projectsTargetId=${notion.projectsTargetId}`
71
+ : `projectsDatabaseId=${notion.projectsDatabaseId}`,
72
+ }
73
+ : {
74
+ id: "config.notion.projectsTarget",
75
+ label: "notion.projectsTargetId/projectsDatabaseId configured",
76
+ ok: false,
77
+ status: "fail",
78
+ detail: "Empty. Run `vibeops notion init` to discover a data_source target, or set the fallback via `--projects-db <id>`.",
79
+ };
80
+ report.checks.push(projDbCheck);
81
+ const tasksDbCheck = notion && tasksInputId.length > 0
82
+ ? {
83
+ id: "config.notion.tasksTarget",
84
+ label: "notion.tasksTargetId/tasksDatabaseId configured",
85
+ ok: true,
86
+ status: "ok",
87
+ detail: notion.tasksTargetId.length > 0
88
+ ? `tasksTargetId=${notion.tasksTargetId}`
89
+ : `tasksDatabaseId=${notion.tasksDatabaseId}`,
90
+ }
91
+ : {
92
+ id: "config.notion.tasksTarget",
93
+ label: "notion.tasksTargetId/tasksDatabaseId configured",
94
+ ok: false,
95
+ status: "fail",
96
+ detail: "Empty. Run `vibeops notion init` to discover a data_source target, or set the fallback via `--tasks-db <id>`.",
97
+ };
98
+ report.checks.push(tasksDbCheck);
99
+ const envInputs = await loadNotionEnv(cwd);
100
+ report.envSource = envInputs.source;
101
+ report.hasToken = envInputs.token !== null;
102
+ report.tokenMasked = envInputs.token === null ? null : maskToken(envInputs.token);
103
+ const tokenCheck = envInputs.token
104
+ ? {
105
+ id: "env.notion.token",
106
+ label: `Load NOTION_TOKEN (source: ${envInputs.source})`,
107
+ ok: true,
108
+ status: "ok",
109
+ detail: report.tokenMasked,
110
+ }
111
+ : {
112
+ id: "env.notion.token",
113
+ label: "Load NOTION_TOKEN",
114
+ ok: false,
115
+ status: "fail",
116
+ detail: "Not found. Add `NOTION_TOKEN=secret_…` to .vibeops.env or export it via process.env.",
117
+ };
118
+ report.checks.push(tokenCheck);
119
+ const canCallApi = config !== null &&
120
+ notion !== null &&
121
+ notion.enabled &&
122
+ projectsInputId.length > 0 &&
123
+ tasksInputId.length > 0 &&
124
+ envInputs.token !== null;
125
+ if (!canCallApi) {
126
+ skipApi(report, "config / env not ready — skipping Notion API calls");
127
+ return finalize(report, wantJson);
128
+ }
129
+ let client;
130
+ try {
131
+ client = await createNotionClient(envInputs.token);
132
+ }
133
+ catch (err) {
134
+ const apiErr = notionApiError(err);
135
+ report.checks.push({
136
+ id: "notion.sdk.load",
137
+ label: "Load @notionhq/client",
138
+ ok: false,
139
+ status: "fail",
140
+ detail: apiErr.message,
141
+ apiError: apiErr,
142
+ });
143
+ skipApi(report, "Failed to load @notionhq/client");
144
+ return finalize(report, wantJson);
145
+ }
146
+ report.checks.push({
147
+ id: "notion.sdk.load",
148
+ label: "Load @notionhq/client",
149
+ ok: true,
150
+ status: "ok",
151
+ });
152
+ const usersMeOk = await runCheck({
153
+ id: "notion.users.me",
154
+ label: "Notion API auth (users.me)",
155
+ run: async () => {
156
+ const me = await client.usersMe();
157
+ return { detail: `${me.type ?? "bot"} · ${me.id}` };
158
+ },
159
+ });
160
+ report.checks.push(usersMeOk);
161
+ if (!usersMeOk.ok) {
162
+ skipApi(report, "users.me failed — skipping downstream checks", [
163
+ "notion.projects.retrieve",
164
+ "notion.projects.schema",
165
+ "notion.tasks.retrieve",
166
+ "notion.tasks.schema",
167
+ ]);
168
+ return finalize(report, wantJson);
169
+ }
170
+ if (wantShape) {
171
+ report.debugShape = [];
172
+ for (const kind of ["projects", "tasks"]) {
173
+ const inputId = kind === "projects" ? projectsInputId : tasksInputId;
174
+ try {
175
+ const resolved = await resolveNotionDataSourceTarget(client, inputId, kind);
176
+ if (resolved.ok) {
177
+ const configuredContainer = kind === "projects" ? notion.projectsDatabaseId : notion.tasksDatabaseId;
178
+ const configuredTarget = kind === "projects" ? notion.projectsTargetId : notion.tasksTargetId;
179
+ const projectViolations = validateDatabaseSchema({
180
+ db: "projects",
181
+ required: PROJECTS_DB_PROPERTIES,
182
+ retrieveResponse: resolved.properties,
183
+ });
184
+ const taskViolations = validateDatabaseSchema({
185
+ db: "tasks",
186
+ required: TASKS_DB_PROPERTIES,
187
+ retrieveResponse: resolved.properties,
188
+ });
189
+ const schemaHint = projectViolations.length === 0
190
+ ? "project"
191
+ : taskViolations.length === 0
192
+ ? "task"
193
+ : "unknown";
194
+ report.debugShape.push({
195
+ kind,
196
+ inputId,
197
+ resolvedDataSourceId: resolved.resolvedId,
198
+ source: debugSourceLabel({
199
+ rawSource: resolved.source,
200
+ configuredTarget,
201
+ configuredContainer,
202
+ inputId,
203
+ }),
204
+ hasProperties: true,
205
+ propertiesKeysCount: Object.keys(resolved.properties).length,
206
+ schemaHint,
207
+ });
208
+ continue;
209
+ }
210
+ }
211
+ catch {
212
+ // Keep going to the raw database-shape probe below.
213
+ }
214
+ try {
215
+ const shape = await client.probeDatabaseShape(inputId);
216
+ report.debugShape.push({ kind, inputId, shape });
217
+ }
218
+ catch (err) {
219
+ const apiErr = notionApiError(err);
220
+ report.debugShape.push({
221
+ kind,
222
+ inputId,
223
+ error: {
224
+ code: apiErr.code,
225
+ ...(apiErr.status !== undefined ? { status: apiErr.status } : {}),
226
+ message: apiErr.message,
227
+ },
228
+ });
229
+ }
230
+ }
231
+ }
232
+ await runResolveAndSchema(client, projectsInputId, "projects", report);
233
+ await runResolveAndSchema(client, tasksInputId, "tasks", report);
234
+ return finalize(report, wantJson);
235
+ }
236
+ function debugSourceLabel(inputs) {
237
+ if (inputs.configuredTarget.length > 0 &&
238
+ inputs.configuredContainer.length > 0 &&
239
+ inputs.inputId === inputs.configuredTarget) {
240
+ return "page-child-database";
241
+ }
242
+ if (inputs.rawSource === "input-data-source")
243
+ return "direct-data-source";
244
+ if (inputs.rawSource === "database-default-data-source")
245
+ return "database-data-source";
246
+ return inputs.rawSource;
247
+ }
248
+ /**
249
+ * Shared resolve + schema-check pair — keeps `notion test` in lock-step with
250
+ * `notion sync`. Both go through `resolveNotionDataSourceTarget` so the
251
+ * `database → data_source` fallback path applies uniformly.
252
+ *
253
+ * Emits three checks per kind:
254
+ * - `notion.{kind}.retrieve` — the `database` retrieve call (transport).
255
+ * - `notion.{kind}.resolve` — the resolver result (input id → resolved id).
256
+ * - `notion.{kind}.schema` — the property-schema validation.
257
+ */
258
+ async function runResolveAndSchema(client, inputId, kind, report) {
259
+ const required = kind === "projects" ? PROJECTS_DB_PROPERTIES : TASKS_DB_PROPERTIES;
260
+ const retrieveId = `notion.${kind}.retrieve`;
261
+ const resolveId = `notion.${kind}.resolve`;
262
+ const schemaId = `notion.${kind}.schema`;
263
+ const labelDb = kind === "projects" ? "Projects DB" : "Tasks DB";
264
+ let resolved;
265
+ try {
266
+ resolved = await resolveNotionDataSourceTarget(client, inputId, kind);
267
+ }
268
+ catch (err) {
269
+ const apiErr = notionApiError(err);
270
+ report.checks.push({
271
+ id: retrieveId,
272
+ label: `${labelDb} retrieve`,
273
+ ok: false,
274
+ status: "fail",
275
+ detail: explainNotionError(apiErr),
276
+ apiError: apiErr,
277
+ });
278
+ report.checks.push({
279
+ id: resolveId,
280
+ label: `${labelDb} resolve target (database → data_source)`,
281
+ ok: false,
282
+ status: "skip",
283
+ detail: "Skipped because retrieve failed",
284
+ });
285
+ report.checks.push({
286
+ id: schemaId,
287
+ label: `${labelDb} required-property validation`,
288
+ ok: false,
289
+ status: "skip",
290
+ detail: "Skipped because retrieve failed",
291
+ });
292
+ return;
293
+ }
294
+ // `notion.{kind}.retrieve` — pass when resolver got past at least one Notion call.
295
+ if (resolved.ok) {
296
+ report.checks.push({
297
+ id: retrieveId,
298
+ label: `${labelDb} retrieve`,
299
+ ok: true,
300
+ status: "ok",
301
+ detail: `input id=${inputId} input object=${resolved.inputObject}`,
302
+ });
303
+ }
304
+ else if (resolved.reason === "transport") {
305
+ report.checks.push({
306
+ id: retrieveId,
307
+ label: `${labelDb} retrieve`,
308
+ ok: false,
309
+ status: "fail",
310
+ detail: resolved.message,
311
+ ...(resolved.apiError !== undefined ? { apiError: resolved.apiError } : {}),
312
+ });
313
+ report.checks.push({
314
+ id: resolveId,
315
+ label: `${labelDb} resolve target (database → data_source)`,
316
+ ok: false,
317
+ status: "skip",
318
+ detail: "Skipped because retrieve failed",
319
+ });
320
+ report.checks.push({
321
+ id: schemaId,
322
+ label: `${labelDb} required-property validation`,
323
+ ok: false,
324
+ status: "skip",
325
+ detail: "Skipped because retrieve failed",
326
+ });
327
+ return;
328
+ }
329
+ else {
330
+ // resolver did the database retrieve OK but found no usable data_source.
331
+ report.checks.push({
332
+ id: retrieveId,
333
+ label: `${labelDb} retrieve`,
334
+ ok: true,
335
+ status: "ok",
336
+ detail: `input id=${inputId} input object=${resolved.partial?.inputObject ?? "(unknown)"}`,
337
+ });
338
+ }
339
+ // `notion.{kind}.resolve`
340
+ if (resolved.ok) {
341
+ const detail = `input id=${resolved.inputId} input object=${resolved.inputObject} ` +
342
+ `resolved id=${resolved.resolvedId} resolved object=${resolved.resolvedObject} ` +
343
+ `source=${resolved.source}`;
344
+ report.checks.push({
345
+ id: resolveId,
346
+ label: `${labelDb} resolve target (database → data_source)`,
347
+ ok: true,
348
+ status: "ok",
349
+ detail,
350
+ });
351
+ }
352
+ else {
353
+ report.checks.push({
354
+ id: resolveId,
355
+ label: `${labelDb} resolve target (database → data_source)`,
356
+ ok: false,
357
+ status: "fail",
358
+ detail: resolved.message,
359
+ });
360
+ report.checks.push({
361
+ id: schemaId,
362
+ label: `${labelDb} required-property validation`,
363
+ ok: false,
364
+ status: "skip",
365
+ detail: "Skipped because target resolution failed",
366
+ });
367
+ return;
368
+ }
369
+ // `notion.{kind}.schema`
370
+ const violations = validateDatabaseSchema({
371
+ db: kind,
372
+ required,
373
+ retrieveResponse: resolved.properties,
374
+ });
375
+ report.checks.push({
376
+ id: schemaId,
377
+ label: `${labelDb} required-property validation`,
378
+ ok: violations.length === 0,
379
+ status: violations.length === 0 ? "ok" : "fail",
380
+ ...(violations.length === 0
381
+ ? { detail: `All ${required.length} properties exist with matching types` }
382
+ : { detail: `${violations.length} violation(s)`, violations }),
383
+ });
384
+ }
385
+ async function runCheck(inputs) {
386
+ try {
387
+ const r = await inputs.run();
388
+ return {
389
+ id: inputs.id,
390
+ label: inputs.label,
391
+ ok: true,
392
+ status: "ok",
393
+ ...(r.detail ? { detail: r.detail } : {}),
394
+ };
395
+ }
396
+ catch (err) {
397
+ const apiErr = notionApiError(err);
398
+ return {
399
+ id: inputs.id,
400
+ label: inputs.label,
401
+ ok: false,
402
+ status: "fail",
403
+ detail: explainNotionError(apiErr),
404
+ apiError: apiErr,
405
+ };
406
+ }
407
+ }
408
+ function explainNotionError(err) {
409
+ const tail = err.status ? ` (HTTP ${err.status})` : "";
410
+ switch (err.code) {
411
+ case "unauthorized":
412
+ return `NOTION_TOKEN was rejected. The integration may be expired or the token incorrect.${tail}`;
413
+ case "restricted_resource":
414
+ return `The Notion DB is not shared with the integration. Open the DB page → ⋯ → Connections and add the integration.${tail}`;
415
+ case "object_not_found":
416
+ return `Database id not found. Verify projectsDatabaseId / tasksDatabaseId in .vibeops.json.${tail}`;
417
+ case "validation_error":
418
+ return `Request rejected (validation_error): ${err.message}${tail}`;
419
+ case "rate_limited":
420
+ return `Notion API rate limit. Retry shortly.${tail}`;
421
+ case "request_timeout":
422
+ case "ETIMEDOUT":
423
+ return `Notion API 5s timeout. Check your network.${tail}`;
424
+ default:
425
+ return `${err.code}: ${err.message}${tail}`;
426
+ }
427
+ }
428
+ function skipApi(report, reason, ids = [
429
+ "notion.sdk.load",
430
+ "notion.users.me",
431
+ "notion.projects.retrieve",
432
+ "notion.projects.schema",
433
+ "notion.tasks.retrieve",
434
+ "notion.tasks.schema",
435
+ ]) {
436
+ for (const id of ids) {
437
+ if (report.checks.some((c) => c.id === id))
438
+ continue;
439
+ report.checks.push({
440
+ id,
441
+ label: humanLabel(id),
442
+ ok: false,
443
+ status: "skip",
444
+ detail: reason,
445
+ });
446
+ }
447
+ }
448
+ function humanLabel(id) {
449
+ switch (id) {
450
+ case "notion.sdk.load": return "Load @notionhq/client";
451
+ case "notion.users.me": return "Notion API auth (users.me)";
452
+ case "notion.projects.retrieve": return "databases.retrieve(projectsDatabaseId)";
453
+ case "notion.projects.schema": return "Projects DB required-property validation";
454
+ case "notion.tasks.retrieve": return "databases.retrieve(tasksDatabaseId)";
455
+ case "notion.tasks.schema": return "Tasks DB required-property validation";
456
+ default: return id;
457
+ }
458
+ }
459
+ function finalize(report, wantJson) {
460
+ report.ok = report.checks.every((c) => c.status === "ok");
461
+ if (wantJson) {
462
+ const sanitized = sanitizeForJson(report);
463
+ process.stdout.write(`${JSON.stringify(sanitized, null, 2)}\n`);
464
+ process.exitCode = report.ok ? 0 : 1;
465
+ return;
466
+ }
467
+ log.info(bold("vibeops notion test"));
468
+ log.info(` ${dim("cwd")} ${report.cwd}`);
469
+ log.blank();
470
+ if (report.debugShape !== undefined && report.debugShape.length > 0) {
471
+ for (const probe of report.debugShape) {
472
+ const kindLabel = probe.kind === "projects" ? "Projects DB shape" : "Tasks DB shape";
473
+ log.info(` ${bold(kindLabel)} ${dim(`input id=${probe.inputId}`)}`);
474
+ if (probe.error !== undefined) {
475
+ log.info(` ${dim("retrieve failed:")} ${red(probe.error.code)}${probe.error.status !== undefined ? ` (HTTP ${probe.error.status})` : ""} — ${probe.error.message}`);
476
+ continue;
477
+ }
478
+ if (probe.resolvedDataSourceId !== undefined) {
479
+ log.info(` ${dim("selected input id ")} ${cyan(probe.inputId)}`);
480
+ log.info(` ${dim("resolved source ")} ${probe.source ?? "(unknown)"}`);
481
+ log.info(` ${dim("resolved data src ")} ${cyan(probe.resolvedDataSourceId)}`);
482
+ log.info(` ${dim("has properties ")} ${probe.hasProperties ? green("yes") : red("no")}`);
483
+ log.info(` ${dim("property keys ")} ${String(probe.propertiesKeysCount ?? 0)}`);
484
+ log.info(` ${dim("schema hint ")} ${probe.schemaHint ?? "unknown"}`);
485
+ continue;
486
+ }
487
+ const s = probe.shape;
488
+ log.info(` ${dim("object ")} ${s.object}`);
489
+ log.info(` ${dim("id ")} ${cyan(s.id)}`);
490
+ if (s.title !== undefined) {
491
+ log.info(` ${dim("title ")} ${s.title}`);
492
+ }
493
+ log.info(` ${dim("has properties ")} ${s.hasProperties ? green("yes") : red("no")}${s.hasProperties ? ` ${dim(`(len=${s.propertiesKeysLength})`)}` : ""}`);
494
+ log.info(` ${dim("data_sources ")} ${s.hasDataSources ? "" : red("not present")}${s.hasDataSources ? `${cyan(`${s.dataSourcesLength}`)}${s.dataSourcesField !== undefined && s.dataSourcesField !== "data_sources" ? ` ${yellow(`(via '${s.dataSourcesField}')`)}` : ""}` : ""}`);
495
+ for (const ref of s.dataSources) {
496
+ log.info(` ${dim("-")} id=${cyan(ref.id)}${ref.name !== undefined ? ` name=${ref.name}` : ""}`);
497
+ }
498
+ log.info(` ${dim("top-level keys ")} ${s.topLevelKeys.join(", ")}`);
499
+ }
500
+ log.blank();
501
+ }
502
+ for (const c of report.checks) {
503
+ const tag = c.status === "ok"
504
+ ? green("✓")
505
+ : c.status === "skip"
506
+ ? yellow("·")
507
+ : red("✗");
508
+ log.info(` ${tag} ${c.label}`);
509
+ if (c.detail)
510
+ log.info(` ${dim(c.detail)}`);
511
+ if (c.violations && c.violations.length > 0) {
512
+ for (const v of c.violations) {
513
+ if (v.kind === "status-options-missing" ||
514
+ v.kind === "status-options-unreadable") {
515
+ const tag2 = red(v.kind === "status-options-missing"
516
+ ? "status-options-missing"
517
+ : "status-options-unreadable");
518
+ log.info(` · ${tag2} ${cyan(v.property)}`);
519
+ if (v.kind === "status-options-missing" &&
520
+ v.missingOptions !== undefined &&
521
+ v.missingOptions.length > 0) {
522
+ log.info(` ${dim("missing")} ${v.missingOptions.join(", ")}`);
523
+ }
524
+ if (v.requiredOptions !== undefined && v.requiredOptions.length > 0) {
525
+ log.info(` ${dim("Add these options in Notion")}: Status property → Edit options → ${v.requiredOptions.join(", ")}`);
526
+ }
527
+ if (v.kind === "status-options-missing" &&
528
+ v.foundOptions !== undefined &&
529
+ v.foundOptions.length > 0) {
530
+ log.info(` ${dim("found in Notion")}: ${v.foundOptions.join(", ")}`);
531
+ }
532
+ continue;
533
+ }
534
+ const tag2 = v.kind === "missing"
535
+ ? red("missing")
536
+ : v.kind === "type-mismatch"
537
+ ? red("type-mismatch")
538
+ : v.kind === "missing-properties"
539
+ ? red("missing-properties")
540
+ : red(v.kind);
541
+ const detail = v.kind === "missing"
542
+ ? `expected types: ${v.allowedTypes.join(" | ")}`
543
+ : v.kind === "type-mismatch"
544
+ ? `expected ${v.allowedTypes.join(" | ")} but got ${v.actualType ?? "?"}`
545
+ : v.description;
546
+ log.info(` · ${tag2} ${cyan(v.property)} — ${dim(detail)}`);
547
+ }
548
+ }
549
+ }
550
+ log.blank();
551
+ if (report.ok) {
552
+ log.ok("Notion connection OK — ready for sync.");
553
+ process.exitCode = 0;
554
+ }
555
+ else {
556
+ const failed = report.checks.filter((c) => c.status === "fail").length;
557
+ const skipped = report.checks.filter((c) => c.status === "skip").length;
558
+ log.error(`Notion connection failed — ${failed} fail${skipped > 0 ? `, ${skipped} skipped` : ""}.`);
559
+ log.info(dim(" Security: NOTION_TOKEN values shown above are masked. The real value lives in .vibeops.env."));
560
+ process.exitCode = 1;
561
+ }
562
+ }
563
+ function sanitizeForJson(report) {
564
+ return {
565
+ cwd: report.cwd,
566
+ configPresent: report.configPresent,
567
+ notion: report.notion
568
+ ? {
569
+ enabled: report.notion.enabled,
570
+ projectsTargetId: report.notion.projectsTargetId,
571
+ tasksTargetId: report.notion.tasksTargetId,
572
+ projectsDatabaseId: report.notion.projectsDatabaseId,
573
+ tasksDatabaseId: report.notion.tasksDatabaseId,
574
+ }
575
+ : null,
576
+ env: {
577
+ source: report.envSource,
578
+ hasToken: report.hasToken,
579
+ tokenMasked: report.tokenMasked,
580
+ },
581
+ ok: report.ok,
582
+ checks: report.checks.map((c) => ({
583
+ id: c.id,
584
+ label: c.label,
585
+ status: c.status,
586
+ ok: c.ok,
587
+ ...(c.detail ? { detail: c.detail } : {}),
588
+ ...(c.violations ? { violations: c.violations } : {}),
589
+ ...(c.apiError ? { apiError: c.apiError } : {}),
590
+ })),
591
+ ...(report.debugShape !== undefined
592
+ ? { debugShape: report.debugShape }
593
+ : {}),
594
+ };
595
+ }