@ycadence/sdk 0.0.0-stage → 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/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ export {
2
+ type ClientOptions,
3
+ DevPulseClient,
4
+ DevPulseError,
5
+ parseRetryAfter,
6
+ type RequestOptions,
7
+ type ResponseInfo,
8
+ type RetryOptions,
9
+ YCadenceClient,
10
+ } from './client.js'
11
+ export {
12
+ type ApiFailure,
13
+ type ApiIssue,
14
+ createError,
15
+ type ErrorExtras,
16
+ errorClassFor,
17
+ RequestIntercepted,
18
+ YCadenceConflictError,
19
+ YCadenceError,
20
+ YCadenceNetworkError,
21
+ YCadenceNotFoundError,
22
+ YCadencePermissionError,
23
+ YCadencePreconditionError,
24
+ YCadenceRateLimitError,
25
+ YCadenceValidationError,
26
+ } from './errors.js'
27
+ export {
28
+ buildPath,
29
+ type HttpMethod,
30
+ isIdempotentMethod,
31
+ OPERATIONS,
32
+ type Operation,
33
+ type OperationId,
34
+ } from './operations.js'
35
+ export { collect, paginate } from './pagination.js'
36
+ export * from './types.js'
37
+ export { SDK_VERSION } from './version.js'
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Every API operation the client calls, in one table.
3
+ *
4
+ * The client builds each request from this table rather than from a path
5
+ * written inline in a method, so there is one place to compare with the
6
+ * server: `apps/web/lib/api/sdk-contract.test.ts` fails when an entry has no
7
+ * matching route contract (unless it is marked `pending`), and the CLI's
8
+ * reference and `--dry-run` name operations by these ids.
9
+ */
10
+
11
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
12
+
13
+ export interface Operation {
14
+ method: HttpMethod
15
+ /** The route contract's path, with `{param}` placeholders. */
16
+ path: string
17
+ /**
18
+ * Planned by the work management contracts and not served yet. The client
19
+ * still calls it; a 404 from an older server is reported as such.
20
+ */
21
+ pending?: string
22
+ }
23
+
24
+ export const OPERATIONS = {
25
+ health: { method: 'GET', path: '/api/health' },
26
+ me: { method: 'GET', path: '/api/v1/me' },
27
+ listProjects: { method: 'GET', path: '/api/v1/projects' },
28
+
29
+ listWorkItems: { method: 'GET', path: '/api/v1/projects/{projectId}/work-items' },
30
+ createWorkItem: { method: 'POST', path: '/api/v1/projects/{projectId}/work-items' },
31
+ getWorkItem: { method: 'GET', path: '/api/v1/work-items/{workItemId}' },
32
+ updateWorkItem: { method: 'PATCH', path: '/api/v1/work-items/{workItemId}' },
33
+ deleteWorkItem: { method: 'DELETE', path: '/api/v1/work-items/{workItemId}' },
34
+
35
+ listStates: { method: 'GET', path: '/api/v1/projects/{projectId}/states' },
36
+ listLabels: { method: 'GET', path: '/api/v1/projects/{projectId}/labels' },
37
+ createLabel: { method: 'POST', path: '/api/v1/projects/{projectId}/labels' },
38
+ listTypes: { method: 'GET', path: '/api/v1/projects/{projectId}/types' },
39
+
40
+ listCycles: { method: 'GET', path: '/api/v1/projects/{projectId}/cycles' },
41
+ createCycle: { method: 'POST', path: '/api/v1/projects/{projectId}/cycles' },
42
+ getCycleProgress: { method: 'GET', path: '/api/v1/cycles/{cycleId}/progress' },
43
+ addWorkItemsToCycle: { method: 'POST', path: '/api/v1/cycles/{cycleId}/work-items' },
44
+
45
+ listViews: { method: 'GET', path: '/api/v1/projects/{projectId}/views' },
46
+ createView: { method: 'POST', path: '/api/v1/projects/{projectId}/views' },
47
+
48
+ listIntake: { method: 'GET', path: '/api/v1/projects/{projectId}/intake' },
49
+ submitIntake: { method: 'POST', path: '/api/v1/projects/{projectId}/intake' },
50
+ triageIntake: { method: 'POST', path: '/api/v1/intake-items/{intakeItemId}/triage' },
51
+
52
+ previewImport: { method: 'POST', path: '/api/v1/projects/{projectId}/imports/preview' },
53
+ startImport: { method: 'POST', path: '/api/v1/projects/{projectId}/imports' },
54
+ listImports: { method: 'GET', path: '/api/v1/projects/{projectId}/imports' },
55
+ getImport: { method: 'GET', path: '/api/v1/imports/{runId}' },
56
+ } as const satisfies Record<string, Operation>
57
+
58
+ export type OperationId = keyof typeof OPERATIONS
59
+
60
+ /** GET and HEAD are safe to repeat; anything else needs an idempotency key first. */
61
+ export function isIdempotentMethod(method: string): boolean {
62
+ return method === 'GET' || method === 'HEAD'
63
+ }
64
+
65
+ /** Fill `{param}` placeholders, encoding each value. */
66
+ export function buildPath(template: string, params: Record<string, string> = {}): string {
67
+ return template.replace(/\{(\w+)\}/g, (_match, name: string) => {
68
+ const value = params[name]
69
+ if (value === undefined || value === '') {
70
+ throw new TypeError(`Missing path parameter "${name}" for ${template}.`)
71
+ }
72
+ return encodeURIComponent(value)
73
+ })
74
+ }
@@ -0,0 +1,36 @@
1
+ import type { Page } from './types'
2
+
3
+ /**
4
+ * Follow `nextCursor` to the end, one page at a time.
5
+ *
6
+ * Lazy: a caller that breaks out early pays for the pages it read and no more.
7
+ */
8
+ export async function* paginate<T>(
9
+ fetchPage: (cursor: string | undefined) => Promise<Page<T>>,
10
+ options: { cursor?: string; maxItems?: number } = {}
11
+ ): AsyncGenerator<T> {
12
+ let cursor = options.cursor
13
+ let yielded = 0
14
+ do {
15
+ const page = await fetchPage(cursor)
16
+ for (const item of page.items) {
17
+ if (options.maxItems !== undefined && yielded >= options.maxItems) return
18
+ yielded += 1
19
+ yield item
20
+ }
21
+ cursor = page.nextCursor ?? undefined
22
+ } while (cursor !== undefined)
23
+ }
24
+
25
+ /** Every item from {@link paginate}, as an array, up to `maxItems`. */
26
+ export async function collect<T>(
27
+ iterable: AsyncIterable<T>,
28
+ maxItems = Number.POSITIVE_INFINITY
29
+ ): Promise<T[]> {
30
+ const items: T[] = []
31
+ for await (const item of iterable) {
32
+ if (items.length >= maxItems) break
33
+ items.push(item)
34
+ }
35
+ return items
36
+ }
package/src/types.ts ADDED
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Request and response shapes of the v1 API, as they arrive over JSON.
3
+ *
4
+ * The routes define these with zod in `apps/web` (`lib/api/contracts.ts` and
5
+ * the route files). The SDK is published without dependencies, so it cannot
6
+ * import those schemas; instead each response type has a field list below that
7
+ * the compiler checks against the type, and `apps/web/lib/api/sdk-contract.test.ts`
8
+ * checks against the route's zod schema. A field added on one side and not the
9
+ * other fails that test rather than reaching a user.
10
+ *
11
+ * Dates are ISO strings: that is what JSON carries.
12
+ */
13
+
14
+ export type Priority = 'urgent' | 'high' | 'medium' | 'low' | 'none'
15
+ export const PRIORITIES: readonly Priority[] = ['urgent', 'high', 'medium', 'low', 'none']
16
+
17
+ export type StateGroup = 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled' | 'triage'
18
+
19
+ export interface WorkItem {
20
+ id: string
21
+ key: string
22
+ sequenceId: number
23
+ name: string
24
+ priority: Priority
25
+ stateId: string | null
26
+ assigneeIds: string[]
27
+ labelIds: string[]
28
+ parentId: string | null
29
+ sortOrder: string
30
+ createdAt: string
31
+ /**
32
+ * Returned by `GET /work-items/{id}` (always, from Slice 2A on); not yet by
33
+ * lists or writes.
34
+ */
35
+ projectId?: string
36
+ /** Not returned yet. Sent back as `If-Match` once the server versions items. */
37
+ version?: number
38
+ }
39
+
40
+ export interface WorkItemDetail extends WorkItem {
41
+ /** Optional only so the SDK still reads a server older than Slice 2A. */
42
+ projectId?: string
43
+ description: string | null
44
+ startDate: string | null
45
+ targetDate: string | null
46
+ archivedAt: string | null
47
+ updatedAt: string
48
+ }
49
+
50
+ export interface Page<T> {
51
+ items: T[]
52
+ nextCursor: string | null
53
+ }
54
+
55
+ export interface CreateWorkItemInput {
56
+ name: string
57
+ description?: string
58
+ stateId?: string
59
+ priority?: Priority
60
+ assigneeIds?: string[]
61
+ labelIds?: string[]
62
+ parentId?: string
63
+ /** ISO date. */
64
+ startDate?: string
65
+ targetDate?: string
66
+ }
67
+
68
+ /**
69
+ * The fields PATCH accepts today. The server strips anything else, so sending
70
+ * an unlisted field would look like success and change nothing.
71
+ */
72
+ export interface UpdateWorkItemInput {
73
+ name?: string
74
+ description?: string
75
+ stateId?: string
76
+ priority?: Priority
77
+ parentId?: string | null
78
+ startDate?: string | null
79
+ targetDate?: string | null
80
+ }
81
+
82
+ export interface ListWorkItemsOptions {
83
+ /** A @devpulse/query expression; sent JSON-encoded as `filter`. */
84
+ filter?: unknown
85
+ limit?: number
86
+ cursor?: string
87
+ /** A state id. */
88
+ state?: string
89
+ priority?: Priority
90
+ }
91
+
92
+ export interface State {
93
+ id: string
94
+ name: string
95
+ group: StateGroup | string
96
+ color: string
97
+ description: string | null
98
+ /** Fractional-index sort key; the list arrives already ordered by it. */
99
+ sequence: string
100
+ isDefault: boolean
101
+ }
102
+
103
+ export interface Label {
104
+ id: string
105
+ name: string
106
+ color: string
107
+ description: string | null
108
+ sortOrder: string
109
+ }
110
+
111
+ export interface Cycle {
112
+ id: string
113
+ name: string
114
+ description: string | null
115
+ startDate: string | null
116
+ endDate: string | null
117
+ status: 'draft' | 'upcoming' | 'active' | 'completed'
118
+ sortOrder: string
119
+ completedAt: string | null
120
+ archivedAt: string | null
121
+ }
122
+
123
+ export interface CycleProgress {
124
+ progress: unknown
125
+ burndown: unknown
126
+ frozen: boolean
127
+ }
128
+
129
+ export interface View {
130
+ id: string
131
+ name: string
132
+ description: string | null
133
+ filter: unknown
134
+ display: unknown
135
+ access: 'private' | 'public'
136
+ ownerId: string | null
137
+ sortOrder: string
138
+ }
139
+
140
+ export interface CreateViewInput {
141
+ name: string
142
+ description?: string
143
+ where?: unknown
144
+ display?: Record<string, unknown>
145
+ access?: 'private' | 'public'
146
+ }
147
+
148
+ export type IntakeStatus = 'pending' | 'accepted' | 'declined' | 'snoozed' | 'duplicate'
149
+
150
+ export interface IntakeItem {
151
+ id: string
152
+ workItemId: string
153
+ status: IntakeStatus
154
+ source: string
155
+ sourceEmail: string | null
156
+ snoozedUntil: string | null
157
+ submittedAt: string
158
+ }
159
+
160
+ export interface SubmitIntakeInput {
161
+ title: string
162
+ description?: string
163
+ source?: 'in_app' | 'form' | 'email' | 'api' | 'integration'
164
+ sourceEmail?: string
165
+ extra?: Record<string, unknown>
166
+ }
167
+
168
+ export interface SubmitIntakeResult {
169
+ intakeItemId: string
170
+ workItemId: string
171
+ sequenceId: number
172
+ }
173
+
174
+ export interface TriageInput {
175
+ status: 'accepted' | 'declined' | 'snoozed' | 'duplicate'
176
+ /** ISO date; required when snoozing. */
177
+ snoozedUntil?: string
178
+ /** Required when marking a duplicate. */
179
+ duplicateOfId?: string
180
+ }
181
+
182
+ export interface TriageResult {
183
+ status: string
184
+ movedToStateId: string | null
185
+ }
186
+
187
+ export interface ImportPreview {
188
+ source: string
189
+ total: number
190
+ states: { name: string; suggestedStateId: string | null }[]
191
+ labels: { name: string; exists: boolean }[]
192
+ assignees: { value: string; matched: boolean }[]
193
+ skipped: { row: number; reason: string }[]
194
+ unmappedPriorities: string[]
195
+ /**
196
+ * False when re-importing would create duplicates rather than update — a CSV
197
+ * with no id column. Worth showing before the import, not after.
198
+ */
199
+ idempotent: boolean
200
+ }
201
+
202
+ export interface ImportRun {
203
+ id: string
204
+ source: string
205
+ status: string
206
+ totalItems: number
207
+ createdCount: number
208
+ updatedCount: number
209
+ cursor: number
210
+ skipped: { row: number; reason: string }[]
211
+ unmatched: { kind: string; value: string; item: string }[]
212
+ error: string | null
213
+ startedAt?: string | null
214
+ finishedAt?: string | null
215
+ createdAt?: string
216
+ }
217
+
218
+ export type ImportSource = 'csv' | 'github' | 'jira' | 'linear'
219
+
220
+ export interface ImportInput {
221
+ source: ImportSource
222
+ /** CSV text, or the parsed JSON export for the API sources. */
223
+ data: unknown
224
+ repository?: string
225
+ baseUrl?: string
226
+ /** Source state name to state id. Only for `startImport`. */
227
+ stateMap?: Record<string, string>
228
+ }
229
+
230
+ export interface Health {
231
+ status: string
232
+ checks: Record<string, string>
233
+ }
234
+
235
+ /** `GET /api/v1/me`: who the credential is. Never includes the key. */
236
+ export interface Me {
237
+ principal: {
238
+ kind: 'personal_key' | 'service_key' | string
239
+ scopeType?: 'project' | 'organization' | 'personal' | string
240
+ apiKeyId: string
241
+ keyPrefix: string
242
+ userId: string | null
243
+ email: string | null
244
+ organizationRole?: string | null
245
+ }
246
+ organization: { id: string; slug: string; name: string }
247
+ project: { id: string; identifier: string; name: string } | null
248
+ scopes: string[]
249
+ expiresAt: string | null
250
+ }
251
+
252
+ /** `GET /api/v1/projects`. */
253
+ export interface Project {
254
+ id: string
255
+ identifier: string
256
+ name: string
257
+ archivedAt: string | null
258
+ }
259
+
260
+ /**
261
+ * Field lists, checked by the compiler to name every key of the type (and no
262
+ * others), and by the contract test to match the route's response schema.
263
+ */
264
+ type FieldsOf<T> = { [K in keyof Required<T>]: true }
265
+
266
+ export const WORK_ITEM_FIELDS: FieldsOf<WorkItem> = {
267
+ id: true,
268
+ key: true,
269
+ sequenceId: true,
270
+ name: true,
271
+ priority: true,
272
+ stateId: true,
273
+ assigneeIds: true,
274
+ labelIds: true,
275
+ parentId: true,
276
+ sortOrder: true,
277
+ createdAt: true,
278
+ projectId: true,
279
+ version: true,
280
+ }
281
+
282
+ export const WORK_ITEM_DETAIL_FIELDS: FieldsOf<WorkItemDetail> = {
283
+ ...WORK_ITEM_FIELDS,
284
+ description: true,
285
+ startDate: true,
286
+ targetDate: true,
287
+ archivedAt: true,
288
+ updatedAt: true,
289
+ }
290
+
291
+ export const STATE_FIELDS: FieldsOf<State> = {
292
+ id: true,
293
+ name: true,
294
+ group: true,
295
+ color: true,
296
+ description: true,
297
+ sequence: true,
298
+ isDefault: true,
299
+ }
300
+
301
+ export const LABEL_FIELDS: FieldsOf<Label> = {
302
+ id: true,
303
+ name: true,
304
+ color: true,
305
+ description: true,
306
+ sortOrder: true,
307
+ }
308
+
309
+ export const CYCLE_FIELDS: FieldsOf<Cycle> = {
310
+ id: true,
311
+ name: true,
312
+ description: true,
313
+ startDate: true,
314
+ endDate: true,
315
+ status: true,
316
+ sortOrder: true,
317
+ completedAt: true,
318
+ archivedAt: true,
319
+ }
320
+
321
+ export const VIEW_FIELDS: FieldsOf<View> = {
322
+ id: true,
323
+ name: true,
324
+ description: true,
325
+ filter: true,
326
+ display: true,
327
+ access: true,
328
+ ownerId: true,
329
+ sortOrder: true,
330
+ }
331
+
332
+ export const INTAKE_ITEM_FIELDS: FieldsOf<IntakeItem> = {
333
+ id: true,
334
+ workItemId: true,
335
+ status: true,
336
+ source: true,
337
+ sourceEmail: true,
338
+ snoozedUntil: true,
339
+ submittedAt: true,
340
+ }
341
+
342
+ export const IMPORT_RUN_FIELDS: FieldsOf<ImportRun> = {
343
+ id: true,
344
+ source: true,
345
+ status: true,
346
+ totalItems: true,
347
+ createdCount: true,
348
+ updatedCount: true,
349
+ cursor: true,
350
+ skipped: true,
351
+ unmatched: true,
352
+ error: true,
353
+ startedAt: true,
354
+ finishedAt: true,
355
+ createdAt: true,
356
+ }
357
+
358
+ /**
359
+ * Fields the SDK declares ahead of the server (the work management contracts
360
+ * add them). The contract test allows them to be missing from today's schemas.
361
+ */
362
+ export const PENDING_FIELDS: Record<string, readonly string[]> = {
363
+ workItem: ['projectId', 'version'],
364
+ }
package/src/version.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The published version, for `User-Agent`.
3
+ *
4
+ * Replaced at build time (`bun build --define`) from package.json, so the
5
+ * Version PR that bumps package.json needs no second edit. Unbuilt source —
6
+ * tests, the workspace — reports `0.0.0-dev`.
7
+ */
8
+ declare const __YCADENCE_SDK_VERSION__: string | undefined
9
+
10
+ export const SDK_VERSION: string =
11
+ typeof __YCADENCE_SDK_VERSION__ === 'string' ? __YCADENCE_SDK_VERSION__ : '0.0.0-dev'