@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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/dist/agent/loader.js +71 -0
- package/dist/agent/prompt.js +66 -0
- package/dist/bootstrap/installer.js +149 -0
- package/dist/bootstrap/manifest.js +15 -0
- package/dist/bootstrap/substitute.js +35 -0
- package/dist/cli.js +241 -0
- package/dist/commands/agent-list.js +32 -0
- package/dist/commands/agent-prompt.js +59 -0
- package/dist/commands/agent-show.js +26 -0
- package/dist/commands/github-init.js +554 -0
- package/dist/commands/github-status.js +164 -0
- package/dist/commands/init.js +179 -0
- package/dist/commands/notion-init.js +764 -0
- package/dist/commands/notion-sync.js +405 -0
- package/dist/commands/notion-test.js +595 -0
- package/dist/commands/plan.js +114 -0
- package/dist/commands/status.js +17 -0
- package/dist/commands/task-check.js +155 -0
- package/dist/commands/task-done.js +98 -0
- package/dist/commands/task-generate.js +206 -0
- package/dist/commands/task-pull.js +277 -0
- package/dist/commands/task-rollback.js +174 -0
- package/dist/commands/task-start.js +90 -0
- package/dist/lib/brief.js +349 -0
- package/dist/lib/config.js +158 -0
- package/dist/lib/filesystem.js +67 -0
- package/dist/lib/git.js +237 -0
- package/dist/lib/github-cli.js +247 -0
- package/dist/lib/inquirer-helpers.js +111 -0
- package/dist/lib/logger.js +42 -0
- package/dist/lib/notion-client.js +459 -0
- package/dist/lib/notion-discovery.js +671 -0
- package/dist/lib/notion-env.js +140 -0
- package/dist/lib/notion-mappers.js +148 -0
- package/dist/lib/notion-schema.js +272 -0
- package/dist/lib/notion-sync.js +337 -0
- package/dist/lib/notion-target.js +247 -0
- package/dist/lib/package-json.js +133 -0
- package/dist/lib/paths.js +26 -0
- package/dist/lib/project-docs.js +95 -0
- package/dist/lib/prompt-builder.js +125 -0
- package/dist/lib/task-generator.js +183 -0
- package/dist/lib/task-prompt.js +23 -0
- package/dist/lib/task-pull.js +354 -0
- package/dist/lib/task-scaffold.js +128 -0
- package/dist/lib/task-summary.js +276 -0
- package/dist/lib/task.js +364 -0
- package/dist/status/collector.js +103 -0
- package/dist/status/format.js +177 -0
- package/dist/types/brief.js +126 -0
- package/dist/types/config.js +17 -0
- package/dist/types/task.js +1 -0
- package/dist/version.js +8 -0
- package/package.json +61 -0
- package/templates/.cursor/rules/00-project-governance.mdc +28 -0
- package/templates/.cursor/rules/01-agent-orchestration.mdc +48 -0
- package/templates/.cursor/rules/02-task-workflow.mdc +38 -0
- package/templates/.cursor/rules/03-git-safety.mdc +30 -0
- package/templates/.cursor/rules/04-docs-update.mdc +22 -0
- package/templates/.vibeops/agents/architect.md +47 -0
- package/templates/.vibeops/agents/builder.md +38 -0
- package/templates/.vibeops/agents/docs.md +54 -0
- package/templates/.vibeops/agents/orchestrator.md +40 -0
- package/templates/.vibeops/agents/planner.md +60 -0
- package/templates/.vibeops/agents/recovery.md +49 -0
- package/templates/.vibeops/agents/reviewer.md +47 -0
- package/templates/.vibeops/agents/tester.md +43 -0
- package/templates/.vibeops/prompts/create-plan.md +33 -0
- package/templates/.vibeops/prompts/generate-tasks.md +41 -0
- package/templates/.vibeops/prompts/implement-task.md +39 -0
- package/templates/.vibeops/prompts/review-task.md +34 -0
- package/templates/.vibeops/prompts/rollback.md +32 -0
- package/templates/.vibeops/prompts/start-project.md +39 -0
- package/templates/.vibeops/workflows/notion-sync.md +53 -0
- package/templates/.vibeops/workflows/project-start.md +73 -0
- package/templates/.vibeops/workflows/rollback.md +45 -0
- package/templates/.vibeops/workflows/task-lifecycle.md +71 -0
- package/templates/AGENTS.md +98 -0
- package/templates/docs/logs/README.md +38 -0
- package/templates/docs/project/00-overview.md +27 -0
- package/templates/docs/project/01-requirements.md +30 -0
- package/templates/docs/project/02-mvp-scope.md +36 -0
- package/templates/docs/project/03-architecture.md +34 -0
- package/templates/docs/project/04-tech-stack.md +29 -0
- package/templates/docs/project/05-current-state.md +35 -0
- package/templates/docs/project/06-decisions.md +20 -0
- package/templates/docs/project/07-backlog.md +23 -0
- package/templates/docs/project/08-env.md +29 -0
- package/templates/docs/project/09-deployment.md +28 -0
- package/templates/docs/tasks/TASK-000-template.md +72 -0
|
@@ -0,0 +1,405 @@
|
|
|
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
|
+
}
|