prjct-cli 0.20.1 → 0.21.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/core/integrations/notion/client.ts +323 -0
- package/core/integrations/notion/index.ts +43 -0
- package/core/integrations/notion/setup.ts +230 -0
- package/core/integrations/notion/sync.ts +311 -0
- package/core/integrations/notion/templates.ts +234 -0
- package/core/plugin/builtin/notion.ts +178 -0
- package/core/types/config.ts +4 -0
- package/core/types/index.ts +9 -0
- package/core/types/integrations.ts +57 -0
- package/package.json +1 -1
- package/templates/commands/init.md +43 -0
- package/templates/commands/notion-setup.md +191 -0
- package/templates/mcp-config.json +28 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion Sync Logic
|
|
3
|
+
* Sync prjct data to Notion databases.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ShippedFeature, Idea } from '../../types/storage'
|
|
7
|
+
import type { NotionIntegrationConfig } from '../../types/integrations'
|
|
8
|
+
import { notionClient } from './client'
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export interface SyncResult {
|
|
15
|
+
success: boolean
|
|
16
|
+
action: 'created' | 'updated' | 'skipped'
|
|
17
|
+
pageId?: string
|
|
18
|
+
url?: string
|
|
19
|
+
error?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Property Builders
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build Notion properties for a shipped feature
|
|
28
|
+
*/
|
|
29
|
+
function buildShippedProperties(
|
|
30
|
+
feature: ShippedFeature,
|
|
31
|
+
projectId: string
|
|
32
|
+
): Record<string, unknown> {
|
|
33
|
+
const props: Record<string, unknown> = {
|
|
34
|
+
Name: {
|
|
35
|
+
title: [{ text: { content: feature.name } }],
|
|
36
|
+
},
|
|
37
|
+
Project: {
|
|
38
|
+
rich_text: [{ text: { content: projectId } }],
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (feature.version) {
|
|
43
|
+
props.Version = {
|
|
44
|
+
rich_text: [{ text: { content: feature.version } }],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (feature.type) {
|
|
49
|
+
props.Type = {
|
|
50
|
+
select: { name: feature.type },
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (feature.shippedAt) {
|
|
55
|
+
props['Shipped Date'] = {
|
|
56
|
+
date: { start: feature.shippedAt.split('T')[0] },
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (feature.codeMetrics) {
|
|
61
|
+
const metrics = feature.codeMetrics
|
|
62
|
+
props['Lines Changed'] = {
|
|
63
|
+
number: (metrics.linesAdded || 0) + (metrics.linesRemoved || 0),
|
|
64
|
+
}
|
|
65
|
+
props['Files Changed'] = {
|
|
66
|
+
number: metrics.filesChanged || 0,
|
|
67
|
+
}
|
|
68
|
+
props.Commits = {
|
|
69
|
+
number: metrics.commits || 0,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (feature.duration) {
|
|
74
|
+
props.Duration = {
|
|
75
|
+
rich_text: [{ text: { content: feature.duration } }],
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (feature.description) {
|
|
80
|
+
props.Description = {
|
|
81
|
+
rich_text: [{ text: { content: feature.description.slice(0, 2000) } }],
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return props
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build Notion properties for an idea
|
|
90
|
+
*/
|
|
91
|
+
function buildIdeaProperties(
|
|
92
|
+
idea: Idea,
|
|
93
|
+
projectId: string
|
|
94
|
+
): Record<string, unknown> {
|
|
95
|
+
const props: Record<string, unknown> = {
|
|
96
|
+
Idea: {
|
|
97
|
+
title: [{ text: { content: idea.text } }],
|
|
98
|
+
},
|
|
99
|
+
Project: {
|
|
100
|
+
rich_text: [{ text: { content: projectId } }],
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (idea.status) {
|
|
105
|
+
const statusMap: Record<string, string> = {
|
|
106
|
+
pending: 'Pending',
|
|
107
|
+
converted: 'Converted',
|
|
108
|
+
completed: 'Converted',
|
|
109
|
+
archived: 'Archived',
|
|
110
|
+
}
|
|
111
|
+
props.Status = {
|
|
112
|
+
status: { name: statusMap[idea.status] || 'Pending' },
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (idea.priority) {
|
|
117
|
+
props.Priority = {
|
|
118
|
+
select: { name: idea.priority },
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (idea.tags && idea.tags.length > 0) {
|
|
123
|
+
props.Tags = {
|
|
124
|
+
multi_select: idea.tags.map((tag) => ({ name: tag })),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const createdDate = idea.createdAt || idea.addedAt
|
|
129
|
+
if (createdDate) {
|
|
130
|
+
props.Created = {
|
|
131
|
+
date: { start: createdDate.split('T')[0] },
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (idea.convertedTo) {
|
|
136
|
+
props['Converted To'] = {
|
|
137
|
+
rich_text: [{ text: { content: idea.convertedTo } }],
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return props
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Sync Functions
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sync a shipped feature to Notion
|
|
150
|
+
*/
|
|
151
|
+
export async function syncShippedFeature(
|
|
152
|
+
projectId: string,
|
|
153
|
+
feature: ShippedFeature,
|
|
154
|
+
config: NotionIntegrationConfig
|
|
155
|
+
): Promise<SyncResult> {
|
|
156
|
+
if (!config.enabled || !config.databases.shipped) {
|
|
157
|
+
return { success: false, action: 'skipped', error: 'Notion not configured' }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!notionClient.isReady()) {
|
|
161
|
+
return { success: false, action: 'skipped', error: 'Notion client not ready' }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const databaseId = config.databases.shipped
|
|
166
|
+
const properties = buildShippedProperties(feature, projectId)
|
|
167
|
+
|
|
168
|
+
// Check if already exists (upsert)
|
|
169
|
+
const existingPageId = await notionClient.findPageByProjectAndName(
|
|
170
|
+
databaseId,
|
|
171
|
+
projectId,
|
|
172
|
+
feature.name
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if (existingPageId) {
|
|
176
|
+
const page = await notionClient.updatePage(existingPageId, properties)
|
|
177
|
+
if (page) {
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
action: 'updated',
|
|
181
|
+
pageId: page.id,
|
|
182
|
+
url: page.url,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
const page = await notionClient.createPage(databaseId, properties)
|
|
187
|
+
if (page) {
|
|
188
|
+
return {
|
|
189
|
+
success: true,
|
|
190
|
+
action: 'created',
|
|
191
|
+
pageId: page.id,
|
|
192
|
+
url: page.url,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { success: false, action: 'skipped', error: 'Failed to sync' }
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
action: 'skipped',
|
|
202
|
+
error: (error as Error).message,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Sync an idea to Notion
|
|
209
|
+
*/
|
|
210
|
+
export async function syncIdea(
|
|
211
|
+
projectId: string,
|
|
212
|
+
idea: Idea,
|
|
213
|
+
config: NotionIntegrationConfig
|
|
214
|
+
): Promise<SyncResult> {
|
|
215
|
+
if (!config.enabled || !config.databases.ideas) {
|
|
216
|
+
return { success: false, action: 'skipped', error: 'Notion not configured' }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!notionClient.isReady()) {
|
|
220
|
+
return { success: false, action: 'skipped', error: 'Notion client not ready' }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const databaseId = config.databases.ideas
|
|
225
|
+
const properties = buildIdeaProperties(idea, projectId)
|
|
226
|
+
|
|
227
|
+
// Check if already exists (by ID in text)
|
|
228
|
+
const existingPageId = await notionClient.findPageByProjectAndName(
|
|
229
|
+
databaseId,
|
|
230
|
+
projectId,
|
|
231
|
+
idea.text
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if (existingPageId) {
|
|
235
|
+
const page = await notionClient.updatePage(existingPageId, properties)
|
|
236
|
+
if (page) {
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
action: 'updated',
|
|
240
|
+
pageId: page.id,
|
|
241
|
+
url: page.url,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
const page = await notionClient.createPage(databaseId, properties)
|
|
246
|
+
if (page) {
|
|
247
|
+
return {
|
|
248
|
+
success: true,
|
|
249
|
+
action: 'created',
|
|
250
|
+
pageId: page.id,
|
|
251
|
+
url: page.url,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { success: false, action: 'skipped', error: 'Failed to sync' }
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
action: 'skipped',
|
|
261
|
+
error: (error as Error).message,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Full sync - sync all data to Notion
|
|
268
|
+
* Used for initial setup or manual resync
|
|
269
|
+
*/
|
|
270
|
+
export async function fullSync(
|
|
271
|
+
projectId: string,
|
|
272
|
+
config: NotionIntegrationConfig,
|
|
273
|
+
data: {
|
|
274
|
+
shipped?: ShippedFeature[]
|
|
275
|
+
ideas?: Idea[]
|
|
276
|
+
}
|
|
277
|
+
): Promise<{
|
|
278
|
+
shipped: { synced: number; failed: number }
|
|
279
|
+
ideas: { synced: number; failed: number }
|
|
280
|
+
}> {
|
|
281
|
+
const results = {
|
|
282
|
+
shipped: { synced: 0, failed: 0 },
|
|
283
|
+
ideas: { synced: 0, failed: 0 },
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Sync shipped features
|
|
287
|
+
if (data.shipped && config.databases.shipped) {
|
|
288
|
+
for (const feature of data.shipped) {
|
|
289
|
+
const result = await syncShippedFeature(projectId, feature, config)
|
|
290
|
+
if (result.success) {
|
|
291
|
+
results.shipped.synced++
|
|
292
|
+
} else {
|
|
293
|
+
results.shipped.failed++
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Sync ideas
|
|
299
|
+
if (data.ideas && config.databases.ideas) {
|
|
300
|
+
for (const idea of data.ideas) {
|
|
301
|
+
const result = await syncIdea(projectId, idea, config)
|
|
302
|
+
if (result.success) {
|
|
303
|
+
results.ideas.synced++
|
|
304
|
+
} else {
|
|
305
|
+
results.ideas.failed++
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return results
|
|
311
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion Database Templates
|
|
3
|
+
* Schemas for auto-creating prjct databases in Notion.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { NotionDatabaseSchema } from './client'
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Database Schemas
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shipped Features Database Schema
|
|
14
|
+
* Tracks shipped features with metrics
|
|
15
|
+
*/
|
|
16
|
+
export const SHIPPED_DATABASE_SCHEMA: NotionDatabaseSchema = {
|
|
17
|
+
title: 'prjct: Shipped Features',
|
|
18
|
+
properties: {
|
|
19
|
+
Name: { type: 'title', title: {} },
|
|
20
|
+
Version: { type: 'rich_text', rich_text: {} },
|
|
21
|
+
Type: {
|
|
22
|
+
type: 'select',
|
|
23
|
+
select: {
|
|
24
|
+
options: [
|
|
25
|
+
{ name: 'feature', color: 'blue' },
|
|
26
|
+
{ name: 'fix', color: 'red' },
|
|
27
|
+
{ name: 'improvement', color: 'green' },
|
|
28
|
+
{ name: 'refactor', color: 'purple' },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
'Shipped Date': { type: 'date', date: {} },
|
|
33
|
+
'Lines Changed': { type: 'number', number: { format: 'number' } },
|
|
34
|
+
'Files Changed': { type: 'number', number: { format: 'number' } },
|
|
35
|
+
Commits: { type: 'number', number: { format: 'number' } },
|
|
36
|
+
Duration: { type: 'rich_text', rich_text: {} },
|
|
37
|
+
Description: { type: 'rich_text', rich_text: {} },
|
|
38
|
+
Project: { type: 'rich_text', rich_text: {} },
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Roadmap Database Schema
|
|
44
|
+
* Tracks feature roadmap and progress
|
|
45
|
+
*/
|
|
46
|
+
export const ROADMAP_DATABASE_SCHEMA: NotionDatabaseSchema = {
|
|
47
|
+
title: 'prjct: Roadmap',
|
|
48
|
+
properties: {
|
|
49
|
+
Feature: { type: 'title', title: {} },
|
|
50
|
+
Status: {
|
|
51
|
+
type: 'status',
|
|
52
|
+
status: {
|
|
53
|
+
options: [
|
|
54
|
+
{ name: 'Planned', color: 'gray' },
|
|
55
|
+
{ name: 'Active', color: 'blue' },
|
|
56
|
+
{ name: 'Completed', color: 'green' },
|
|
57
|
+
{ name: 'Shipped', color: 'purple' },
|
|
58
|
+
],
|
|
59
|
+
groups: [
|
|
60
|
+
{ name: 'To Do', option_ids: [], color: 'gray' },
|
|
61
|
+
{ name: 'In Progress', option_ids: [], color: 'blue' },
|
|
62
|
+
{ name: 'Done', option_ids: [], color: 'green' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
Priority: {
|
|
67
|
+
type: 'select',
|
|
68
|
+
select: {
|
|
69
|
+
options: [
|
|
70
|
+
{ name: 'High', color: 'red' },
|
|
71
|
+
{ name: 'Medium', color: 'yellow' },
|
|
72
|
+
{ name: 'Low', color: 'gray' },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
Progress: { type: 'number', number: { format: 'percent' } },
|
|
77
|
+
Phase: { type: 'rich_text', rich_text: {} },
|
|
78
|
+
'Target Date': { type: 'date', date: {} },
|
|
79
|
+
Description: { type: 'rich_text', rich_text: {} },
|
|
80
|
+
Tasks: { type: 'number', number: { format: 'number' } },
|
|
81
|
+
Project: { type: 'rich_text', rich_text: {} },
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Ideas Database Schema
|
|
87
|
+
* Captures and tracks ideas
|
|
88
|
+
*/
|
|
89
|
+
export const IDEAS_DATABASE_SCHEMA: NotionDatabaseSchema = {
|
|
90
|
+
title: 'prjct: Ideas',
|
|
91
|
+
properties: {
|
|
92
|
+
Idea: { type: 'title', title: {} },
|
|
93
|
+
Status: {
|
|
94
|
+
type: 'status',
|
|
95
|
+
status: {
|
|
96
|
+
options: [
|
|
97
|
+
{ name: 'Pending', color: 'gray' },
|
|
98
|
+
{ name: 'Converted', color: 'green' },
|
|
99
|
+
{ name: 'Archived', color: 'default' },
|
|
100
|
+
],
|
|
101
|
+
groups: [
|
|
102
|
+
{ name: 'Not Started', option_ids: [], color: 'gray' },
|
|
103
|
+
{ name: 'Done', option_ids: [], color: 'green' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
Priority: {
|
|
108
|
+
type: 'select',
|
|
109
|
+
select: {
|
|
110
|
+
options: [
|
|
111
|
+
{ name: 'high', color: 'red' },
|
|
112
|
+
{ name: 'medium', color: 'yellow' },
|
|
113
|
+
{ name: 'low', color: 'gray' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
Tags: { type: 'multi_select', multi_select: { options: [] } },
|
|
118
|
+
Created: { type: 'date', date: {} },
|
|
119
|
+
'Converted To': { type: 'rich_text', rich_text: {} },
|
|
120
|
+
Project: { type: 'rich_text', rich_text: {} },
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Active Tasks Database Schema
|
|
126
|
+
* Tracks current task queue
|
|
127
|
+
*/
|
|
128
|
+
export const TASKS_DATABASE_SCHEMA: NotionDatabaseSchema = {
|
|
129
|
+
title: 'prjct: Active Tasks',
|
|
130
|
+
properties: {
|
|
131
|
+
Task: { type: 'title', title: {} },
|
|
132
|
+
Priority: {
|
|
133
|
+
type: 'select',
|
|
134
|
+
select: {
|
|
135
|
+
options: [
|
|
136
|
+
{ name: 'critical', color: 'red' },
|
|
137
|
+
{ name: 'high', color: 'orange' },
|
|
138
|
+
{ name: 'medium', color: 'yellow' },
|
|
139
|
+
{ name: 'low', color: 'gray' },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
Type: {
|
|
144
|
+
type: 'select',
|
|
145
|
+
select: {
|
|
146
|
+
options: [
|
|
147
|
+
{ name: 'feature', color: 'blue' },
|
|
148
|
+
{ name: 'bug', color: 'red' },
|
|
149
|
+
{ name: 'improvement', color: 'green' },
|
|
150
|
+
{ name: 'chore', color: 'gray' },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
Section: {
|
|
155
|
+
type: 'select',
|
|
156
|
+
select: {
|
|
157
|
+
options: [
|
|
158
|
+
{ name: 'active', color: 'blue' },
|
|
159
|
+
{ name: 'backlog', color: 'gray' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
Completed: { type: 'checkbox', checkbox: {} },
|
|
164
|
+
Agent: { type: 'rich_text', rich_text: {} },
|
|
165
|
+
Feature: { type: 'rich_text', rich_text: {} },
|
|
166
|
+
Created: { type: 'date', date: {} },
|
|
167
|
+
'Completed At': { type: 'date', date: {} },
|
|
168
|
+
Project: { type: 'rich_text', rich_text: {} },
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// =============================================================================
|
|
173
|
+
// Dashboard Template
|
|
174
|
+
// =============================================================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Dashboard page content template
|
|
178
|
+
*/
|
|
179
|
+
export function getDashboardContent(
|
|
180
|
+
projectName: string,
|
|
181
|
+
databases: {
|
|
182
|
+
shipped?: string
|
|
183
|
+
roadmap?: string
|
|
184
|
+
ideas?: string
|
|
185
|
+
tasks?: string
|
|
186
|
+
}
|
|
187
|
+
): string {
|
|
188
|
+
const sections = []
|
|
189
|
+
|
|
190
|
+
sections.push(`# ${projectName} Dashboard`)
|
|
191
|
+
sections.push('')
|
|
192
|
+
sections.push('This dashboard is automatically synced by prjct-cli.')
|
|
193
|
+
sections.push('')
|
|
194
|
+
|
|
195
|
+
if (databases.shipped) {
|
|
196
|
+
sections.push('## Shipped Features')
|
|
197
|
+
sections.push(`View all shipped features and metrics.`)
|
|
198
|
+
sections.push('')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (databases.roadmap) {
|
|
202
|
+
sections.push('## Roadmap')
|
|
203
|
+
sections.push(`Track feature progress and planning.`)
|
|
204
|
+
sections.push('')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (databases.ideas) {
|
|
208
|
+
sections.push('## Ideas')
|
|
209
|
+
sections.push(`Captured ideas and their status.`)
|
|
210
|
+
sections.push('')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (databases.tasks) {
|
|
214
|
+
sections.push('## Active Tasks')
|
|
215
|
+
sections.push(`Current task queue and completion status.`)
|
|
216
|
+
sections.push('')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
sections.push('---')
|
|
220
|
+
sections.push('Powered by [prjct-cli](https://prjct.app)')
|
|
221
|
+
|
|
222
|
+
return sections.join('\n')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// =============================================================================
|
|
226
|
+
// All Schemas Export
|
|
227
|
+
// =============================================================================
|
|
228
|
+
|
|
229
|
+
export const ALL_DATABASE_SCHEMAS = {
|
|
230
|
+
shipped: SHIPPED_DATABASE_SCHEMA,
|
|
231
|
+
roadmap: ROADMAP_DATABASE_SCHEMA,
|
|
232
|
+
ideas: IDEAS_DATABASE_SCHEMA,
|
|
233
|
+
tasks: TASKS_DATABASE_SCHEMA,
|
|
234
|
+
} as const
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion Plugin for prjct-cli
|
|
3
|
+
*
|
|
4
|
+
* Syncs prjct data to Notion databases on events.
|
|
5
|
+
* Activated when project has integrations.notion.enabled = true
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*
|
|
9
|
+
* Configuration in GlobalConfig.integrations.notion:
|
|
10
|
+
* {
|
|
11
|
+
* "enabled": true,
|
|
12
|
+
* "databases": {
|
|
13
|
+
* "shipped": "notion-db-id",
|
|
14
|
+
* "ideas": "notion-db-id"
|
|
15
|
+
* },
|
|
16
|
+
* "syncOn": { "ship": true, "idea": true }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { HookPoints } from '../hooks'
|
|
21
|
+
import type { NotionIntegrationConfig } from '../../types/integrations'
|
|
22
|
+
import type { ShippedFeature, Idea } from '../../types/storage'
|
|
23
|
+
import { notionClient, syncShippedFeature, syncIdea } from '../../integrations/notion'
|
|
24
|
+
|
|
25
|
+
interface NotionPluginContext {
|
|
26
|
+
projectId: string
|
|
27
|
+
config: NotionIntegrationConfig
|
|
28
|
+
apiToken?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const plugin = {
|
|
32
|
+
name: 'notion',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
description: 'Sync prjct data to Notion',
|
|
35
|
+
|
|
36
|
+
// Plugin state
|
|
37
|
+
enabled: false,
|
|
38
|
+
projectId: null as string | null,
|
|
39
|
+
config: null as NotionIntegrationConfig | null,
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Activate plugin
|
|
43
|
+
*/
|
|
44
|
+
async activate(context: NotionPluginContext): Promise<void> {
|
|
45
|
+
const { projectId, config, apiToken } = context
|
|
46
|
+
|
|
47
|
+
if (!config?.enabled) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initialize Notion client
|
|
52
|
+
notionClient.initialize(config, apiToken || process.env.NOTION_TOKEN)
|
|
53
|
+
|
|
54
|
+
if (!notionClient.isReady()) {
|
|
55
|
+
console.warn('[notion] API token not configured, plugin disabled')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
plugin.enabled = true
|
|
60
|
+
plugin.projectId = projectId
|
|
61
|
+
plugin.config = config
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Deactivate plugin
|
|
66
|
+
*/
|
|
67
|
+
async deactivate(): Promise<void> {
|
|
68
|
+
plugin.enabled = false
|
|
69
|
+
plugin.projectId = null
|
|
70
|
+
plugin.config = null
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Hook handlers
|
|
75
|
+
*/
|
|
76
|
+
hooks: {
|
|
77
|
+
/**
|
|
78
|
+
* Sync shipped feature to Notion after ship
|
|
79
|
+
*/
|
|
80
|
+
[HookPoints.AFTER_FEATURE_SHIP]: async function (data: {
|
|
81
|
+
feature: ShippedFeature
|
|
82
|
+
projectId?: string
|
|
83
|
+
}): Promise<void> {
|
|
84
|
+
if (!plugin.enabled || !plugin.config || !plugin.projectId) return
|
|
85
|
+
if (!plugin.config.syncOn?.ship) return
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const result = await syncShippedFeature(
|
|
89
|
+
data.projectId || plugin.projectId,
|
|
90
|
+
data.feature,
|
|
91
|
+
plugin.config
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if (result.success) {
|
|
95
|
+
console.log(`[notion] Synced: ${data.feature.name} (${result.action})`)
|
|
96
|
+
} else if (result.error) {
|
|
97
|
+
console.warn(`[notion] Sync failed: ${result.error}`)
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Graceful degradation - don't fail the ship
|
|
101
|
+
console.warn('[notion] Sync error:', (error as Error).message)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sync idea to Notion after capture
|
|
107
|
+
*/
|
|
108
|
+
[HookPoints.AFTER_IDEA_CAPTURE]: async function (data: {
|
|
109
|
+
idea: Idea
|
|
110
|
+
projectId?: string
|
|
111
|
+
}): Promise<void> {
|
|
112
|
+
if (!plugin.enabled || !plugin.config || !plugin.projectId) return
|
|
113
|
+
if (!plugin.config.syncOn?.idea) return
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const result = await syncIdea(
|
|
117
|
+
data.projectId || plugin.projectId,
|
|
118
|
+
data.idea,
|
|
119
|
+
plugin.config
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if (result.success) {
|
|
123
|
+
console.log(`[notion] Synced idea: ${data.idea.text.slice(0, 30)}... (${result.action})`)
|
|
124
|
+
} else if (result.error) {
|
|
125
|
+
console.warn(`[notion] Sync failed: ${result.error}`)
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// Graceful degradation - don't fail the idea capture
|
|
129
|
+
console.warn('[notion] Sync error:', (error as Error).message)
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Sync task completion (optional)
|
|
135
|
+
*/
|
|
136
|
+
[HookPoints.AFTER_TASK_COMPLETE]: async function (data: {
|
|
137
|
+
taskName?: string
|
|
138
|
+
projectId?: string
|
|
139
|
+
}): Promise<void> {
|
|
140
|
+
if (!plugin.enabled || !plugin.config || !plugin.projectId) return
|
|
141
|
+
if (!plugin.config.syncOn?.done) return
|
|
142
|
+
|
|
143
|
+
// Task sync is optional and less critical
|
|
144
|
+
// Could update task status in Notion if database exists
|
|
145
|
+
if (data.taskName) {
|
|
146
|
+
console.log(`[notion] Task completed: ${data.taskName}`)
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if plugin is active
|
|
153
|
+
*/
|
|
154
|
+
isActive(): boolean {
|
|
155
|
+
return plugin.enabled && plugin.config?.enabled === true
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get current sync status
|
|
160
|
+
*/
|
|
161
|
+
getStatus(): {
|
|
162
|
+
enabled: boolean
|
|
163
|
+
databases: number
|
|
164
|
+
lastSync?: string
|
|
165
|
+
} {
|
|
166
|
+
const dbCount = plugin.config?.databases
|
|
167
|
+
? Object.values(plugin.config.databases).filter(Boolean).length
|
|
168
|
+
: 0
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
enabled: plugin.enabled,
|
|
172
|
+
databases: dbCount,
|
|
173
|
+
lastSync: plugin.config?.lastSyncAt,
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export default plugin
|