@happyvertical/projects 0.78.2 → 0.78.3

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/README.md CHANGED
@@ -130,6 +130,9 @@ Get project details including fields and statuses.
130
130
 
131
131
  Add an issue or pull request to the project.
132
132
 
133
+ The GitHub adapter returns the created item's identity and content type. Use
134
+ `listItems()` when content metadata, status, or provider fields are required.
135
+
133
136
  **Parameters:**
134
137
  - `contentId: string` - Node ID of the issue or PR (from @happyvertical/repos)
135
138
 
@@ -146,6 +149,9 @@ Remove an item from the project.
146
149
 
147
150
  Get a specific project item.
148
151
 
152
+ The GitHub adapter returns item identity and content type only. Use
153
+ `listItems()` for the enriched content metadata, status, and provider fields.
154
+
149
155
  **Parameters:**
150
156
  - `itemId: string` - Project item ID
151
157
 
@@ -153,10 +159,18 @@ Get a specific project item.
153
159
 
154
160
  ##### `listItems(filters?: ItemFilters): Promise<ProjectItem[]>`
155
161
 
156
- List all items in the project.
162
+ List items in the project. The GitHub adapter follows Projects V2 pagination,
163
+ skips deleted or inaccessible content, and returns each item's content type,
164
+ status, provider field values, and top-level `title`, `url`, and `assignees`
165
+ metadata for provider-neutral rendering. Provider fields remain separately
166
+ keyed by their original names, even when a field has the same name as top-level
167
+ metadata. Results default to at most 100 items and are fetched in pages sized to
168
+ stay below GitHub's GraphQL node limit. A `limit` of 0 returns no items.
169
+ The GitHub adapter currently applies only `limit` and `cursor`; callers should
170
+ apply `status`, `assignees`, or `labels` filters to the returned items.
157
171
 
158
172
  **Parameters:**
159
- - `filters?: ItemFilters` - Optional filters (limit, cursor, status)
173
+ - `filters?: ItemFilters` - Optional filters (`limit` defaults to 100)
160
174
 
161
175
  **Returns:** Array of project items
162
176
 
package/dist/index.d.ts CHANGED
@@ -62,10 +62,45 @@ export declare class GitHubProject implements IProject {
62
62
  private statusOptions?;
63
63
  constructor(config: ProjectConfig);
64
64
  getProject(): Promise<Project>;
65
+ /**
66
+ * Add an issue or pull request to the configured GitHub project.
67
+ *
68
+ * The mutation result contains item identity and content type only. Use
69
+ * {@link listItems} when content metadata, status, or fields are required.
70
+ *
71
+ * @param contentId - GitHub node ID for the issue or pull request
72
+ * @returns The created provider-neutral project item
73
+ */
65
74
  addItem(contentId: string): Promise<ProjectItem>;
66
75
  removeItem(itemId: string): Promise<void>;
76
+ /**
77
+ * Get one GitHub Projects V2 item by its project-item ID.
78
+ *
79
+ * This identity lookup does not hydrate content metadata, status, or fields.
80
+ * Use {@link listItems} for the enriched board-item representation.
81
+ *
82
+ * @param itemId - GitHub project-item node ID
83
+ * @returns The item, or `null` when it or its content is inaccessible
84
+ */
67
85
  getItem(itemId: string): Promise<ProjectItem | null>;
86
+ /**
87
+ * List GitHub Projects V2 items after an optional cursor.
88
+ *
89
+ * Deleted or inaccessible content is skipped, and the result defaults to at
90
+ * most 100 accessible items to preserve the adapter's historical limit.
91
+ *
92
+ * @param filters - Optional cursor and maximum result count
93
+ * @returns Provider-neutral project items with content metadata and fields
94
+ */
68
95
  listItems(filters?: ItemFilters): Promise<ProjectItem[]>;
96
+ /**
97
+ * Convert one GitHub Projects V2 item into the provider-neutral item shape.
98
+ */
99
+ private mapProjectItem;
100
+ /**
101
+ * Convert GitHub's field-value union into stable scalar or string-array values.
102
+ */
103
+ private mapProjectFieldValue;
69
104
  updateItemStatus(itemId: string, status: string): Promise<void>;
70
105
  listStatuses(): Promise<Status[]>;
71
106
  updateItemField(itemId: string, fieldId: string, value: unknown): Promise<void>;
@@ -91,11 +126,19 @@ export declare interface IProject {
91
126
  listFields(): Promise<Field[]>;
92
127
  }
93
128
 
129
+ /**
130
+ * Options for reading project items.
131
+ */
94
132
  export declare interface ItemFilters {
133
+ /** Return items in this status when supported by the provider. */
95
134
  status?: string;
135
+ /** Return items assigned to these provider usernames when supported. */
96
136
  assignees?: string[];
137
+ /** Return items with these labels when supported. */
97
138
  labels?: string[];
139
+ /** Maximum number of items to return. Defaults to 100; zero returns none. */
98
140
  limit?: number;
141
+ /** Provider cursor after which item pagination begins. */
99
142
  cursor?: string;
100
143
  }
101
144
 
@@ -159,11 +202,25 @@ export declare enum ProjectErrorCode {
159
202
  UNKNOWN = "UNKNOWN"
160
203
  }
161
204
 
205
+ /**
206
+ * A provider-neutral item on a project board.
207
+ */
162
208
  export declare interface ProjectItem {
209
+ /** Provider project-item ID. */
163
210
  id: string;
211
+ /** Provider content-node ID for the issue, pull request, or draft issue. */
164
212
  contentId: string;
213
+ /** Content title when returned by the provider. */
214
+ title?: string;
215
+ /** Content URL, or `null` when the content has no provider URL. */
216
+ url?: string | null;
217
+ /** Provider usernames assigned to the content. */
218
+ assignees?: string[];
219
+ /** Selected status name from the configured or canonical status field. */
165
220
  status?: string;
221
+ /** Provider field values keyed by field name. */
166
222
  fields: Record<string, unknown>;
223
+ /** Kind of content represented by the project item. */
167
224
  type: 'Issue' | 'PullRequest' | 'DraftIssue';
168
225
  }
169
226
 
package/dist/index.js CHANGED
@@ -107,6 +107,8 @@ async function getProject(options) {
107
107
  );
108
108
  }
109
109
  }
110
+ const GITHUB_DEFAULT_ITEMS_LIMIT = 100;
111
+ const GITHUB_ITEMS_PAGE_SIZE = 20;
110
112
  class GitHubProject {
111
113
  graphql;
112
114
  projectId;
@@ -203,6 +205,15 @@ class GitHubProject {
203
205
  };
204
206
  }
205
207
  // Items
208
+ /**
209
+ * Add an issue or pull request to the configured GitHub project.
210
+ *
211
+ * The mutation result contains item identity and content type only. Use
212
+ * {@link listItems} when content metadata, status, or fields are required.
213
+ *
214
+ * @param contentId - GitHub node ID for the issue or pull request
215
+ * @returns The created provider-neutral project item
216
+ */
206
217
  async addItem(contentId) {
207
218
  const mutation = `
208
219
  mutation($projectId: ID!, $contentId: ID!) {
@@ -215,9 +226,11 @@ class GitHubProject {
215
226
  content {
216
227
  ... on Issue {
217
228
  id
229
+ typename: __typename
218
230
  }
219
231
  ... on PullRequest {
220
232
  id
233
+ typename: __typename
221
234
  }
222
235
  }
223
236
  }
@@ -232,7 +245,7 @@ class GitHubProject {
232
245
  id: data.addProjectV2ItemById.item.id,
233
246
  contentId: data.addProjectV2ItemById.item.content.id,
234
247
  fields: {},
235
- type: "Issue"
248
+ type: data.addProjectV2ItemById.item.content.typename
236
249
  };
237
250
  }
238
251
  async removeItem(itemId) {
@@ -251,6 +264,15 @@ class GitHubProject {
251
264
  itemId
252
265
  });
253
266
  }
267
+ /**
268
+ * Get one GitHub Projects V2 item by its project-item ID.
269
+ *
270
+ * This identity lookup does not hydrate content metadata, status, or fields.
271
+ * Use {@link listItems} for the enriched board-item representation.
272
+ *
273
+ * @param itemId - GitHub project-item node ID
274
+ * @returns The item, or `null` when it or its content is inaccessible
275
+ */
254
276
  async getItem(itemId) {
255
277
  const query = `
256
278
  query($itemId: ID!) {
@@ -260,9 +282,15 @@ class GitHubProject {
260
282
  content {
261
283
  ... on Issue {
262
284
  id
285
+ typename: __typename
263
286
  }
264
287
  ... on PullRequest {
265
288
  id
289
+ typename: __typename
290
+ }
291
+ ... on DraftIssue {
292
+ id
293
+ typename: __typename
266
294
  }
267
295
  }
268
296
  }
@@ -270,16 +298,25 @@ class GitHubProject {
270
298
  }
271
299
  `;
272
300
  const data = await this.graphql.query(query, { itemId });
273
- if (!data.node) {
301
+ if (!data.node?.content?.id || !data.node.content.typename) {
274
302
  return null;
275
303
  }
276
304
  return {
277
305
  id: data.node.id,
278
306
  contentId: data.node.content.id,
279
307
  fields: {},
280
- type: "Issue"
308
+ type: data.node.content.typename
281
309
  };
282
310
  }
311
+ /**
312
+ * List GitHub Projects V2 items after an optional cursor.
313
+ *
314
+ * Deleted or inaccessible content is skipped, and the result defaults to at
315
+ * most 100 accessible items to preserve the adapter's historical limit.
316
+ *
317
+ * @param filters - Optional cursor and maximum result count
318
+ * @returns Provider-neutral project items with content metadata and fields
319
+ */
283
320
  async listItems(filters) {
284
321
  const query = `
285
322
  query($projectId: ID!, $first: Int!, $after: String) {
@@ -291,9 +328,169 @@ class GitHubProject {
291
328
  content {
292
329
  ... on Issue {
293
330
  id
331
+ typename: __typename
332
+ title
333
+ url
334
+ assignees(first: 100) {
335
+ nodes {
336
+ login
337
+ }
338
+ }
294
339
  }
295
340
  ... on PullRequest {
296
341
  id
342
+ typename: __typename
343
+ title
344
+ url
345
+ assignees(first: 100) {
346
+ nodes {
347
+ login
348
+ }
349
+ }
350
+ }
351
+ ... on DraftIssue {
352
+ id
353
+ typename: __typename
354
+ title
355
+ assignees(first: 100) {
356
+ nodes {
357
+ login
358
+ }
359
+ }
360
+ }
361
+ }
362
+ fieldValues(first: 50) {
363
+ nodes {
364
+ typename: __typename
365
+ # GitHub Projects have at most 50 fields. A 20-item page and
366
+ # four 100-node nested connections stay below GitHub's
367
+ # 500,000-node query limit while avoiding partial values.
368
+ ... on ProjectV2ItemFieldDateValue {
369
+ field {
370
+ ...ProjectField
371
+ }
372
+ date
373
+ }
374
+ ... on ProjectV2ItemFieldIterationValue {
375
+ field {
376
+ ...ProjectField
377
+ }
378
+ title
379
+ }
380
+ ... on ProjectV2ItemFieldLabelValue {
381
+ field {
382
+ ...ProjectField
383
+ }
384
+ labels(first: 100) {
385
+ nodes {
386
+ name
387
+ }
388
+ }
389
+ }
390
+ ... on ProjectV2ItemFieldMilestoneValue {
391
+ field {
392
+ ...ProjectField
393
+ }
394
+ milestone {
395
+ title
396
+ }
397
+ }
398
+ ... on ProjectV2ItemFieldNumberValue {
399
+ field {
400
+ ...ProjectField
401
+ }
402
+ number
403
+ }
404
+ ... on ProjectV2ItemFieldPullRequestValue {
405
+ field {
406
+ ...ProjectField
407
+ }
408
+ pullRequests(first: 100) {
409
+ nodes {
410
+ url
411
+ }
412
+ }
413
+ }
414
+ ... on ProjectV2ItemFieldRepositoryValue {
415
+ field {
416
+ ...ProjectField
417
+ }
418
+ repository {
419
+ nameWithOwner
420
+ }
421
+ }
422
+ ... on ProjectV2ItemFieldReviewerValue {
423
+ field {
424
+ ...ProjectField
425
+ }
426
+ reviewers(first: 100) {
427
+ nodes {
428
+ ... on Bot {
429
+ login
430
+ }
431
+ ... on EnterpriseTeam {
432
+ slug
433
+ }
434
+ ... on Mannequin {
435
+ login
436
+ }
437
+ ... on Team {
438
+ slug
439
+ }
440
+ ... on User {
441
+ login
442
+ }
443
+ }
444
+ }
445
+ }
446
+ ... on ProjectV2ItemFieldSingleSelectValue {
447
+ field {
448
+ ...ProjectField
449
+ }
450
+ name
451
+ }
452
+ ... on ProjectV2ItemFieldTextValue {
453
+ field {
454
+ ...ProjectField
455
+ }
456
+ text
457
+ }
458
+ ... on ProjectV2ItemFieldUserValue {
459
+ field {
460
+ ...ProjectField
461
+ }
462
+ users(first: 100) {
463
+ nodes {
464
+ login
465
+ }
466
+ }
467
+ }
468
+ ... on ProjectV2ItemIssueFieldValue {
469
+ field {
470
+ ...ProjectField
471
+ }
472
+ issueFieldValue {
473
+ typename: __typename
474
+ ... on IssueFieldDateValue {
475
+ dateValue: value
476
+ }
477
+ ... on IssueFieldMultiSelectValue {
478
+ multiSelectOptions: options {
479
+ name
480
+ }
481
+ }
482
+ ... on IssueFieldNumberValue {
483
+ numberValue: value
484
+ }
485
+ ... on IssueFieldSingleSelectValue {
486
+ name
487
+ singleSelectValue: value
488
+ }
489
+ ... on IssueFieldTextValue {
490
+ textValue: value
491
+ }
492
+ }
493
+ }
297
494
  }
298
495
  }
299
496
  }
@@ -305,18 +502,152 @@ class GitHubProject {
305
502
  }
306
503
  }
307
504
  }
505
+
506
+ fragment ProjectField on ProjectV2FieldCommon {
507
+ id
508
+ name
509
+ }
308
510
  `;
309
- const data = await this.graphql.query(query, {
310
- projectId: this.projectId,
311
- first: filters?.limit || 100,
312
- after: filters?.cursor
313
- });
314
- return data.node.items.nodes.map((item) => ({
511
+ const requestedLimit = filters?.limit ?? GITHUB_DEFAULT_ITEMS_LIMIT;
512
+ const limit = Number.isFinite(requestedLimit) ? Math.max(0, Math.floor(requestedLimit)) : GITHUB_DEFAULT_ITEMS_LIMIT;
513
+ if (limit === 0) {
514
+ return [];
515
+ }
516
+ const items = [];
517
+ let after = filters?.cursor;
518
+ let fillPage = false;
519
+ const seenCursors = /* @__PURE__ */ new Set();
520
+ while (items.length < limit) {
521
+ const remaining = limit - items.length;
522
+ const first = fillPage ? GITHUB_ITEMS_PAGE_SIZE : Math.min(GITHUB_ITEMS_PAGE_SIZE, remaining);
523
+ const data = await this.graphql.query(query, {
524
+ projectId: this.projectId,
525
+ first,
526
+ after
527
+ });
528
+ if (!data.node) {
529
+ throw new ProjectError(
530
+ `GitHub project ${this.projectId} was not found or is inaccessible`,
531
+ ProjectErrorCode.NOT_FOUND
532
+ );
533
+ }
534
+ const connection = data.node.items;
535
+ let skippedContent = false;
536
+ for (const node of connection.nodes) {
537
+ if (!node) {
538
+ skippedContent = true;
539
+ continue;
540
+ }
541
+ const item = this.mapProjectItem(node);
542
+ if (item) {
543
+ items.push(item);
544
+ } else {
545
+ skippedContent = true;
546
+ }
547
+ if (items.length >= limit) {
548
+ break;
549
+ }
550
+ }
551
+ fillPage = skippedContent;
552
+ const { endCursor, hasNextPage } = connection.pageInfo;
553
+ if (!hasNextPage || !endCursor || endCursor === after || seenCursors.has(endCursor)) {
554
+ break;
555
+ }
556
+ seenCursors.add(endCursor);
557
+ after = endCursor;
558
+ }
559
+ return items;
560
+ }
561
+ /**
562
+ * Convert one GitHub Projects V2 item into the provider-neutral item shape.
563
+ */
564
+ mapProjectItem(item) {
565
+ const { content } = item;
566
+ if (!content) {
567
+ return null;
568
+ }
569
+ const fieldEntries = [];
570
+ let status;
571
+ for (const fieldValue of item.fieldValues.nodes) {
572
+ if (!fieldValue?.field) {
573
+ continue;
574
+ }
575
+ const value = this.mapProjectFieldValue(fieldValue);
576
+ if (value !== void 0) {
577
+ fieldEntries.push([fieldValue.field.name, value]);
578
+ }
579
+ const isStatusField = this.statusFieldId ? fieldValue.field.id === this.statusFieldId : fieldValue.field.name.toLowerCase() === "status";
580
+ if (isStatusField && typeof value === "string") {
581
+ status = value;
582
+ }
583
+ }
584
+ return {
315
585
  id: item.id,
316
- contentId: item.content.id,
317
- fields: {},
318
- type: "Issue"
319
- }));
586
+ contentId: content.id,
587
+ title: content.title,
588
+ url: content.url ?? null,
589
+ assignees: content.assignees.nodes.flatMap(
590
+ (assignee) => assignee ? [assignee.login] : []
591
+ ),
592
+ status,
593
+ fields: Object.fromEntries(fieldEntries),
594
+ type: content.typename
595
+ };
596
+ }
597
+ /**
598
+ * Convert GitHub's field-value union into stable scalar or string-array values.
599
+ */
600
+ mapProjectFieldValue(value) {
601
+ switch (value.typename) {
602
+ case "ProjectV2ItemFieldDateValue":
603
+ return value.date;
604
+ case "ProjectV2ItemFieldIterationValue":
605
+ return value.title;
606
+ case "ProjectV2ItemFieldLabelValue":
607
+ return value.labels?.nodes.flatMap(
608
+ (label) => label ? [label.name] : []
609
+ );
610
+ case "ProjectV2ItemFieldMilestoneValue":
611
+ return value.milestone?.title;
612
+ case "ProjectV2ItemFieldNumberValue":
613
+ return value.number;
614
+ case "ProjectV2ItemFieldPullRequestValue":
615
+ return value.pullRequests?.nodes.flatMap(
616
+ (pullRequest) => pullRequest ? [pullRequest.url] : []
617
+ );
618
+ case "ProjectV2ItemFieldRepositoryValue":
619
+ return value.repository?.nameWithOwner;
620
+ case "ProjectV2ItemFieldReviewerValue":
621
+ return value.reviewers?.nodes.flatMap((reviewer) => {
622
+ const name = reviewer?.login ?? reviewer?.slug;
623
+ return name ? [name] : [];
624
+ });
625
+ case "ProjectV2ItemFieldSingleSelectValue":
626
+ return value.name;
627
+ case "ProjectV2ItemFieldTextValue":
628
+ return value.text;
629
+ case "ProjectV2ItemFieldUserValue":
630
+ return value.users?.nodes.flatMap((user) => user ? [user.login] : []);
631
+ case "ProjectV2ItemIssueFieldValue": {
632
+ const issueValue = value.issueFieldValue;
633
+ switch (issueValue?.typename) {
634
+ case "IssueFieldDateValue":
635
+ return issueValue.dateValue;
636
+ case "IssueFieldMultiSelectValue":
637
+ return issueValue.multiSelectOptions?.map((option) => option.name);
638
+ case "IssueFieldNumberValue":
639
+ return issueValue.numberValue;
640
+ case "IssueFieldSingleSelectValue":
641
+ return issueValue.name ?? issueValue.singleSelectValue;
642
+ case "IssueFieldTextValue":
643
+ return issueValue.textValue;
644
+ default:
645
+ return void 0;
646
+ }
647
+ }
648
+ default:
649
+ return void 0;
650
+ }
320
651
  }
321
652
  // Status Management
322
653
  async updateItemStatus(itemId, status) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/errors.ts","../src/factory.ts","../src/github/index.ts","../src/types.ts"],"sourcesContent":["/**\n * Project error handling\n */\n\nexport enum ProjectErrorCode {\n NOT_FOUND = 'NOT_FOUND',\n UNAUTHORIZED = 'UNAUTHORIZED',\n INVALID_STATUS = 'INVALID_STATUS',\n INVALID_FIELD = 'INVALID_FIELD',\n ITEM_NOT_IN_PROJECT = 'ITEM_NOT_IN_PROJECT',\n RATE_LIMITED = 'RATE_LIMITED',\n NETWORK_ERROR = 'NETWORK_ERROR',\n VALIDATION_ERROR = 'VALIDATION_ERROR',\n UNKNOWN = 'UNKNOWN',\n}\n\nexport class ProjectError extends Error {\n constructor(\n message: string,\n public code: ProjectErrorCode,\n public statusCode?: number,\n public response?: unknown,\n ) {\n super(message);\n this.name = 'ProjectError';\n Error.captureStackTrace(this, this.constructor);\n }\n\n static fromHTTPStatus(\n statusCode: number,\n message: string,\n response?: unknown,\n ): ProjectError {\n let code: ProjectErrorCode;\n\n switch (statusCode) {\n case 404:\n code = ProjectErrorCode.NOT_FOUND;\n break;\n case 401:\n code = ProjectErrorCode.UNAUTHORIZED;\n break;\n case 403:\n code = ProjectErrorCode.UNAUTHORIZED;\n break;\n case 429:\n code = ProjectErrorCode.RATE_LIMITED;\n break;\n case 422:\n code = ProjectErrorCode.VALIDATION_ERROR;\n break;\n default:\n code = ProjectErrorCode.UNKNOWN;\n }\n\n return new ProjectError(message, code, statusCode, response);\n }\n\n static networkError(message: string, cause?: Error): ProjectError {\n return new ProjectError(\n message,\n ProjectErrorCode.NETWORK_ERROR,\n undefined,\n cause,\n );\n }\n\n isRetryable(): boolean {\n return (\n this.code === ProjectErrorCode.RATE_LIMITED ||\n this.code === ProjectErrorCode.NETWORK_ERROR\n );\n }\n}\n","/**\n * Project factory function\n *\n * Creates project instances following the pattern from @happyvertical/files and @happyvertical/sql\n */\n\nimport { ProjectError, ProjectErrorCode } from './errors.js';\nimport type { IProject, ProjectConfig } from './types.js';\n\n/**\n * Check if value is already a project instance\n */\nfunction isProjectInstance(value: unknown): value is IProject {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'getProject' in value &&\n 'addItem' in value &&\n 'updateItemStatus' in value &&\n typeof (value as IProject).getProject === 'function'\n );\n}\n\n/**\n * Get a project management client instance\n *\n * This is the main entry point following the pattern from @happyvertical/files and @happyvertical/sql.\n * It can accept either a configuration object or an existing project instance.\n *\n * @param options - Project configuration or existing project instance\n * @returns Promise resolving to project interface\n *\n * @example\n * ```typescript\n * import { getProject } from '@happyvertical/projects';\n *\n * // Create a GitHub Projects client\n * const project = await getProject({\n * type: 'github',\n * owner: 'happyvertical',\n * projectId: 'PVT_kwDOB9Y8ns4A8-TY',\n * token: process.env.GITHUB_TOKEN,\n * statusFieldId: 'PVTSSF_lADOB9Y8ns4A8-TYzgw0GaY',\n * statusOptions: {\n * 'New': 'option-id-1',\n * 'Backlog': 'option-id-2',\n * 'In Progress': 'option-id-3',\n * 'Done': 'option-id-4'\n * }\n * });\n *\n * // Use the client\n * await project.addItem(issueNodeId);\n * await project.updateItemStatus(itemId, 'In Progress');\n *\n * // Pass existing instance (returns it unchanged)\n * const sameProject = await getProject(project);\n * ```\n */\nexport async function getProject(\n options: ProjectConfig | IProject,\n): Promise<IProject> {\n // If already a project instance, return it\n if (isProjectInstance(options)) {\n return options;\n }\n\n // Validate configuration\n if (!options.type) {\n throw new ProjectError(\n 'Project type is required',\n ProjectErrorCode.VALIDATION_ERROR,\n );\n }\n\n if (!options.projectId) {\n throw new ProjectError(\n 'Project ID is required',\n ProjectErrorCode.VALIDATION_ERROR,\n );\n }\n\n if (!options.token) {\n throw new ProjectError(\n 'Authentication token is required',\n ProjectErrorCode.UNAUTHORIZED,\n );\n }\n\n // Create project instance based on type\n switch (options.type) {\n case 'github': {\n const { GitHubProject } = await import('./github/index.js');\n return new GitHubProject(options);\n }\n\n case 'jira':\n throw new ProjectError(\n 'Jira support not yet implemented',\n ProjectErrorCode.UNKNOWN,\n );\n\n case 'zenhub':\n throw new ProjectError(\n 'ZenHub support not yet implemented',\n ProjectErrorCode.UNKNOWN,\n );\n\n case 'linear':\n throw new ProjectError(\n 'Linear support not yet implemented',\n ProjectErrorCode.UNKNOWN,\n );\n\n default:\n throw new ProjectError(\n `Unsupported project type: ${options.type}`,\n ProjectErrorCode.VALIDATION_ERROR,\n );\n }\n}\n","/**\n * GitHub Projects V2 implementation\n */\n\nimport { GraphQLClient, type IGraphQLClient } from '@happyvertical/graphql';\nimport { ProjectError, ProjectErrorCode } from '../errors.js';\nimport type {\n Field,\n IProject,\n ItemFilters,\n Project,\n ProjectConfig,\n ProjectItem,\n Status,\n} from '../types.js';\n\n/**\n * GitHub Projects V2 implementation\n */\nexport class GitHubProject implements IProject {\n private graphql: IGraphQLClient;\n private projectId: string;\n private owner?: string;\n private repo?: string;\n private statusFieldId?: string;\n private statusOptions?: Record<string, string>;\n\n constructor(config: ProjectConfig) {\n if (config.type !== 'github') {\n throw new Error('Invalid config type for GitHubProject');\n }\n\n this.graphql = new GraphQLClient({\n endpoint: 'https://api.github.com/graphql',\n token: config.token,\n });\n this.projectId = config.projectId;\n this.owner = config.owner;\n this.repo = config.repo;\n this.statusFieldId = config.statusFieldId;\n this.statusOptions = config.statusOptions;\n }\n\n // Project Info\n async getProject(): Promise<Project> {\n const query = `\n query($projectId: ID!) {\n node(id: $projectId) {\n ... on ProjectV2 {\n id\n title\n shortDescription\n url\n owner {\n ... on Organization {\n login\n }\n ... on User {\n login\n }\n }\n fields(first: 20) {\n nodes {\n ... on ProjectV2Field {\n id\n name\n dataType\n }\n ... on ProjectV2SingleSelectField {\n id\n name\n dataType\n options {\n id\n name\n description\n color\n }\n }\n }\n }\n }\n }\n }\n `;\n\n const data = (await this.graphql.query(query, {\n projectId: this.projectId,\n })) as {\n node: {\n id: string;\n title: string;\n shortDescription: string;\n url: string;\n owner: { login: string };\n fields: {\n nodes: Array<{\n id: string;\n name: string;\n dataType: string;\n options?: Array<{\n id: string;\n name: string;\n description?: string;\n color?: string;\n }>;\n }>;\n };\n };\n };\n\n const statusField = data.node.fields.nodes.find(\n (f) => f.name === 'Status' && f.options,\n );\n\n return {\n id: data.node.id,\n title: data.node.title,\n description: data.node.shortDescription,\n owner: data.node.owner.login,\n url: data.node.url,\n statuses: statusField?.options\n ? statusField.options.map((opt, index) => ({\n id: opt.id,\n name: opt.name,\n description: opt.description,\n color: opt.color,\n order: index,\n }))\n : [],\n fields: data.node.fields.nodes.map((f) => ({\n id: f.id,\n name: f.name,\n type: this.mapFieldType(f.dataType),\n options: f.options?.map((opt) => ({\n id: opt.id,\n name: opt.name,\n description: opt.description,\n color: opt.color,\n })),\n })),\n };\n }\n\n // Items\n async addItem(contentId: string): Promise<ProjectItem> {\n const mutation = `\n mutation($projectId: ID!, $contentId: ID!) {\n addProjectV2ItemById(input: {\n projectId: $projectId\n contentId: $contentId\n }) {\n item {\n id\n content {\n ... on Issue {\n id\n }\n ... on PullRequest {\n id\n }\n }\n }\n }\n }\n `;\n\n const data = (await this.graphql.mutate(mutation, {\n projectId: this.projectId,\n contentId,\n })) as {\n addProjectV2ItemById: {\n item: {\n id: string;\n content: {\n id: string;\n };\n };\n };\n };\n\n return {\n id: data.addProjectV2ItemById.item.id,\n contentId: data.addProjectV2ItemById.item.content.id,\n fields: {},\n type: 'Issue',\n };\n }\n\n async removeItem(itemId: string): Promise<void> {\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!) {\n deleteProjectV2Item(input: {\n projectId: $projectId\n itemId: $itemId\n }) {\n deletedItemId\n }\n }\n `;\n\n await this.graphql.mutate(mutation, {\n projectId: this.projectId,\n itemId,\n });\n }\n\n async getItem(itemId: string): Promise<ProjectItem | null> {\n const query = `\n query($itemId: ID!) {\n node(id: $itemId) {\n ... on ProjectV2Item {\n id\n content {\n ... on Issue {\n id\n }\n ... on PullRequest {\n id\n }\n }\n }\n }\n }\n `;\n\n const data = (await this.graphql.query(query, { itemId })) as {\n node: {\n id: string;\n content: {\n id: string;\n };\n } | null;\n };\n\n if (!data.node) {\n return null;\n }\n\n return {\n id: data.node.id,\n contentId: data.node.content.id,\n fields: {},\n type: 'Issue',\n };\n }\n\n async listItems(filters?: ItemFilters): Promise<ProjectItem[]> {\n const query = `\n query($projectId: ID!, $first: Int!, $after: String) {\n node(id: $projectId) {\n ... on ProjectV2 {\n items(first: $first, after: $after) {\n nodes {\n id\n content {\n ... on Issue {\n id\n }\n ... on PullRequest {\n id\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n }\n }\n `;\n\n const data = (await this.graphql.query(query, {\n projectId: this.projectId,\n first: filters?.limit || 100,\n after: filters?.cursor,\n })) as {\n node: {\n items: {\n nodes: Array<{\n id: string;\n content: {\n id: string;\n };\n }>;\n };\n };\n };\n\n return data.node.items.nodes.map((item) => ({\n id: item.id,\n contentId: item.content.id,\n fields: {},\n type: 'Issue' as const,\n }));\n }\n\n // Status Management\n async updateItemStatus(itemId: string, status: string): Promise<void> {\n if (!this.statusFieldId || !this.statusOptions) {\n throw new ProjectError(\n 'Status field configuration is required. Provide statusFieldId and statusOptions in config.',\n ProjectErrorCode.INVALID_STATUS,\n );\n }\n\n const statusOptionId = this.statusOptions[status];\n if (!statusOptionId) {\n throw new ProjectError(\n `Status \"${status}\" not found in project configuration. Available: ${Object.keys(this.statusOptions).join(', ')}`,\n ProjectErrorCode.INVALID_STATUS,\n );\n }\n\n await this.updateItemField(itemId, this.statusFieldId, statusOptionId);\n }\n\n async listStatuses(): Promise<Status[]> {\n const project = await this.getProject();\n return project.statuses;\n }\n\n // Field Management\n async updateItemField(\n itemId: string,\n fieldId: string,\n value: unknown,\n ): Promise<void> {\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {\n updateProjectV2ItemFieldValue(input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: $value\n }) {\n projectV2Item {\n id\n }\n }\n }\n `;\n\n await this.graphql.mutate(mutation, {\n projectId: this.projectId,\n itemId,\n fieldId,\n value: { singleSelectOptionId: value },\n });\n }\n\n async listFields(): Promise<Field[]> {\n const project = await this.getProject();\n return project.fields;\n }\n\n /**\n * Map GitHub field type to our standard types\n */\n private mapFieldType(\n dataType: string,\n ): 'text' | 'number' | 'date' | 'single_select' | 'iteration' {\n switch (dataType.toLowerCase()) {\n case 'text':\n return 'text';\n case 'number':\n return 'number';\n case 'date':\n return 'date';\n case 'single_select':\n return 'single_select';\n case 'iteration':\n return 'iteration';\n default:\n return 'text';\n }\n }\n}\n","/**\n * Core types for project management operations\n */\n\nexport interface Project {\n id: string;\n title: string;\n description?: string;\n owner: string; // Organization or user\n url: string;\n statuses: Status[];\n fields: Field[];\n}\n\nexport interface ProjectItem {\n id: string; // Project item ID\n contentId: string; // Issue/PR node ID\n status?: string;\n fields: Record<string, unknown>;\n type: 'Issue' | 'PullRequest' | 'DraftIssue';\n}\n\nexport interface Status {\n id: string;\n name: string;\n description?: string;\n color?: string;\n order: number;\n}\n\nexport interface Field {\n id: string;\n name: string;\n type: 'text' | 'number' | 'date' | 'single_select' | 'iteration';\n options?: FieldOption[];\n}\n\nexport interface FieldOption {\n id: string;\n name: string;\n description?: string;\n color?: string;\n}\n\nexport interface ItemFilters {\n status?: string;\n assignees?: string[];\n labels?: string[];\n limit?: number;\n cursor?: string; // For pagination\n}\n\n/**\n * Project configuration\n */\nexport interface ProjectConfig {\n type: 'github' | 'jira' | 'zenhub' | 'linear';\n projectId: string;\n token: string;\n\n // GitHub Projects V2\n owner?: string; // Organization or user\n repo?: string; // Repository name (for repo-level projects)\n statusFieldId?: string;\n statusOptions?: Record<string, string>; // Map status name to option ID\n\n // Jira\n boardId?: string;\n\n // Linear\n teamId?: string;\n}\n\n/**\n * Kanban status names for 6-lane structure\n */\nexport const KANBAN_STATUSES = [\n 'New',\n 'Backlog',\n 'Planning',\n 'Ready',\n 'In Progress',\n 'Review',\n 'Done',\n] as const;\n\nexport type KanbanStatus = (typeof KANBAN_STATUSES)[number];\n\n/**\n * Project interface - all project implementations must implement this\n */\nexport interface IProject {\n // Project Info\n getProject(): Promise<Project>;\n\n // Items (Issues/Cards)\n addItem(contentId: string): Promise<ProjectItem>;\n removeItem(itemId: string): Promise<void>;\n getItem(itemId: string): Promise<ProjectItem | null>;\n listItems(filters?: ItemFilters): Promise<ProjectItem[]>;\n\n // Status/Column Management\n updateItemStatus(itemId: string, status: string): Promise<void>;\n listStatuses(): Promise<Status[]>;\n\n // Field Management\n updateItemField(\n itemId: string,\n fieldId: string,\n value: unknown,\n ): Promise<void>;\n listFields(): Promise<Field[]>;\n}\n"],"names":["ProjectErrorCode","GitHubProject","index"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/errors.ts","../src/factory.ts","../src/github/index.ts","../src/types.ts"],"sourcesContent":["/**\n * Project error handling\n */\n\nexport enum ProjectErrorCode {\n NOT_FOUND = 'NOT_FOUND',\n UNAUTHORIZED = 'UNAUTHORIZED',\n INVALID_STATUS = 'INVALID_STATUS',\n INVALID_FIELD = 'INVALID_FIELD',\n ITEM_NOT_IN_PROJECT = 'ITEM_NOT_IN_PROJECT',\n RATE_LIMITED = 'RATE_LIMITED',\n NETWORK_ERROR = 'NETWORK_ERROR',\n VALIDATION_ERROR = 'VALIDATION_ERROR',\n UNKNOWN = 'UNKNOWN',\n}\n\nexport class ProjectError extends Error {\n constructor(\n message: string,\n public code: ProjectErrorCode,\n public statusCode?: number,\n public response?: unknown,\n ) {\n super(message);\n this.name = 'ProjectError';\n Error.captureStackTrace(this, this.constructor);\n }\n\n static fromHTTPStatus(\n statusCode: number,\n message: string,\n response?: unknown,\n ): ProjectError {\n let code: ProjectErrorCode;\n\n switch (statusCode) {\n case 404:\n code = ProjectErrorCode.NOT_FOUND;\n break;\n case 401:\n code = ProjectErrorCode.UNAUTHORIZED;\n break;\n case 403:\n code = ProjectErrorCode.UNAUTHORIZED;\n break;\n case 429:\n code = ProjectErrorCode.RATE_LIMITED;\n break;\n case 422:\n code = ProjectErrorCode.VALIDATION_ERROR;\n break;\n default:\n code = ProjectErrorCode.UNKNOWN;\n }\n\n return new ProjectError(message, code, statusCode, response);\n }\n\n static networkError(message: string, cause?: Error): ProjectError {\n return new ProjectError(\n message,\n ProjectErrorCode.NETWORK_ERROR,\n undefined,\n cause,\n );\n }\n\n isRetryable(): boolean {\n return (\n this.code === ProjectErrorCode.RATE_LIMITED ||\n this.code === ProjectErrorCode.NETWORK_ERROR\n );\n }\n}\n","/**\n * Project factory function\n *\n * Creates project instances following the pattern from @happyvertical/files and @happyvertical/sql\n */\n\nimport { ProjectError, ProjectErrorCode } from './errors.js';\nimport type { IProject, ProjectConfig } from './types.js';\n\n/**\n * Check if value is already a project instance\n */\nfunction isProjectInstance(value: unknown): value is IProject {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'getProject' in value &&\n 'addItem' in value &&\n 'updateItemStatus' in value &&\n typeof (value as IProject).getProject === 'function'\n );\n}\n\n/**\n * Get a project management client instance\n *\n * This is the main entry point following the pattern from @happyvertical/files and @happyvertical/sql.\n * It can accept either a configuration object or an existing project instance.\n *\n * @param options - Project configuration or existing project instance\n * @returns Promise resolving to project interface\n *\n * @example\n * ```typescript\n * import { getProject } from '@happyvertical/projects';\n *\n * // Create a GitHub Projects client\n * const project = await getProject({\n * type: 'github',\n * owner: 'happyvertical',\n * projectId: 'PVT_kwDOB9Y8ns4A8-TY',\n * token: process.env.GITHUB_TOKEN,\n * statusFieldId: 'PVTSSF_lADOB9Y8ns4A8-TYzgw0GaY',\n * statusOptions: {\n * 'New': 'option-id-1',\n * 'Backlog': 'option-id-2',\n * 'In Progress': 'option-id-3',\n * 'Done': 'option-id-4'\n * }\n * });\n *\n * // Use the client\n * await project.addItem(issueNodeId);\n * await project.updateItemStatus(itemId, 'In Progress');\n *\n * // Pass existing instance (returns it unchanged)\n * const sameProject = await getProject(project);\n * ```\n */\nexport async function getProject(\n options: ProjectConfig | IProject,\n): Promise<IProject> {\n // If already a project instance, return it\n if (isProjectInstance(options)) {\n return options;\n }\n\n // Validate configuration\n if (!options.type) {\n throw new ProjectError(\n 'Project type is required',\n ProjectErrorCode.VALIDATION_ERROR,\n );\n }\n\n if (!options.projectId) {\n throw new ProjectError(\n 'Project ID is required',\n ProjectErrorCode.VALIDATION_ERROR,\n );\n }\n\n if (!options.token) {\n throw new ProjectError(\n 'Authentication token is required',\n ProjectErrorCode.UNAUTHORIZED,\n );\n }\n\n // Create project instance based on type\n switch (options.type) {\n case 'github': {\n const { GitHubProject } = await import('./github/index.js');\n return new GitHubProject(options);\n }\n\n case 'jira':\n throw new ProjectError(\n 'Jira support not yet implemented',\n ProjectErrorCode.UNKNOWN,\n );\n\n case 'zenhub':\n throw new ProjectError(\n 'ZenHub support not yet implemented',\n ProjectErrorCode.UNKNOWN,\n );\n\n case 'linear':\n throw new ProjectError(\n 'Linear support not yet implemented',\n ProjectErrorCode.UNKNOWN,\n );\n\n default:\n throw new ProjectError(\n `Unsupported project type: ${options.type}`,\n ProjectErrorCode.VALIDATION_ERROR,\n );\n }\n}\n","/**\n * GitHub Projects V2 implementation\n */\n\nimport { GraphQLClient, type IGraphQLClient } from '@happyvertical/graphql';\nimport { ProjectError, ProjectErrorCode } from '../errors.js';\nimport type {\n Field,\n IProject,\n ItemFilters,\n Project,\n ProjectConfig,\n ProjectItem,\n Status,\n} from '../types.js';\n\nconst GITHUB_DEFAULT_ITEMS_LIMIT = 100;\nconst GITHUB_ITEMS_PAGE_SIZE = 20;\n\ninterface GitHubProjectItemReference {\n typename: ProjectItem['type'];\n id: string;\n}\n\ninterface GitHubProjectItemContent extends GitHubProjectItemReference {\n title: string;\n url?: string;\n assignees: {\n nodes: Array<{ login: string } | null>;\n };\n}\n\ninterface GitHubProjectFieldValue {\n typename: string;\n field?: {\n id: string;\n name: string;\n };\n date?: string | null;\n title?: string;\n labels?: {\n nodes: Array<{ name: string } | null>;\n } | null;\n milestone?: {\n title: string;\n } | null;\n number?: number | null;\n pullRequests?: {\n nodes: Array<{ url: string } | null>;\n } | null;\n repository?: {\n nameWithOwner: string;\n } | null;\n reviewers?: {\n nodes: Array<{\n login?: string;\n slug?: string;\n } | null>;\n } | null;\n name?: string | null;\n text?: string | null;\n users?: {\n nodes: Array<{ login: string } | null>;\n } | null;\n issueFieldValue?: {\n typename: string;\n name?: string;\n dateValue?: string;\n multiSelectOptions?: Array<{ name: string }>;\n numberValue?: number;\n singleSelectValue?: string;\n textValue?: string;\n } | null;\n}\n\ninterface GitHubProjectItemNode {\n id: string;\n content: GitHubProjectItemContent | null;\n fieldValues: {\n nodes: Array<GitHubProjectFieldValue | null>;\n };\n}\n\ninterface GitHubProjectItemsPage {\n node: {\n items: {\n nodes: Array<GitHubProjectItemNode | null>;\n pageInfo: {\n hasNextPage: boolean;\n endCursor: string | null;\n };\n };\n } | null;\n}\n\n/**\n * GitHub Projects V2 implementation\n */\nexport class GitHubProject implements IProject {\n private graphql: IGraphQLClient;\n private projectId: string;\n private owner?: string;\n private repo?: string;\n private statusFieldId?: string;\n private statusOptions?: Record<string, string>;\n\n constructor(config: ProjectConfig) {\n if (config.type !== 'github') {\n throw new Error('Invalid config type for GitHubProject');\n }\n\n this.graphql = new GraphQLClient({\n endpoint: 'https://api.github.com/graphql',\n token: config.token,\n });\n this.projectId = config.projectId;\n this.owner = config.owner;\n this.repo = config.repo;\n this.statusFieldId = config.statusFieldId;\n this.statusOptions = config.statusOptions;\n }\n\n // Project Info\n async getProject(): Promise<Project> {\n const query = `\n query($projectId: ID!) {\n node(id: $projectId) {\n ... on ProjectV2 {\n id\n title\n shortDescription\n url\n owner {\n ... on Organization {\n login\n }\n ... on User {\n login\n }\n }\n fields(first: 20) {\n nodes {\n ... on ProjectV2Field {\n id\n name\n dataType\n }\n ... on ProjectV2SingleSelectField {\n id\n name\n dataType\n options {\n id\n name\n description\n color\n }\n }\n }\n }\n }\n }\n }\n `;\n\n const data = (await this.graphql.query(query, {\n projectId: this.projectId,\n })) as {\n node: {\n id: string;\n title: string;\n shortDescription: string;\n url: string;\n owner: { login: string };\n fields: {\n nodes: Array<{\n id: string;\n name: string;\n dataType: string;\n options?: Array<{\n id: string;\n name: string;\n description?: string;\n color?: string;\n }>;\n }>;\n };\n };\n };\n\n const statusField = data.node.fields.nodes.find(\n (f) => f.name === 'Status' && f.options,\n );\n\n return {\n id: data.node.id,\n title: data.node.title,\n description: data.node.shortDescription,\n owner: data.node.owner.login,\n url: data.node.url,\n statuses: statusField?.options\n ? statusField.options.map((opt, index) => ({\n id: opt.id,\n name: opt.name,\n description: opt.description,\n color: opt.color,\n order: index,\n }))\n : [],\n fields: data.node.fields.nodes.map((f) => ({\n id: f.id,\n name: f.name,\n type: this.mapFieldType(f.dataType),\n options: f.options?.map((opt) => ({\n id: opt.id,\n name: opt.name,\n description: opt.description,\n color: opt.color,\n })),\n })),\n };\n }\n\n // Items\n /**\n * Add an issue or pull request to the configured GitHub project.\n *\n * The mutation result contains item identity and content type only. Use\n * {@link listItems} when content metadata, status, or fields are required.\n *\n * @param contentId - GitHub node ID for the issue or pull request\n * @returns The created provider-neutral project item\n */\n async addItem(contentId: string): Promise<ProjectItem> {\n const mutation = `\n mutation($projectId: ID!, $contentId: ID!) {\n addProjectV2ItemById(input: {\n projectId: $projectId\n contentId: $contentId\n }) {\n item {\n id\n content {\n ... on Issue {\n id\n typename: __typename\n }\n ... on PullRequest {\n id\n typename: __typename\n }\n }\n }\n }\n }\n `;\n\n const data = (await this.graphql.mutate(mutation, {\n projectId: this.projectId,\n contentId,\n })) as {\n addProjectV2ItemById: {\n item: {\n id: string;\n content: GitHubProjectItemReference;\n };\n };\n };\n\n return {\n id: data.addProjectV2ItemById.item.id,\n contentId: data.addProjectV2ItemById.item.content.id,\n fields: {},\n type: data.addProjectV2ItemById.item.content.typename,\n };\n }\n\n async removeItem(itemId: string): Promise<void> {\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!) {\n deleteProjectV2Item(input: {\n projectId: $projectId\n itemId: $itemId\n }) {\n deletedItemId\n }\n }\n `;\n\n await this.graphql.mutate(mutation, {\n projectId: this.projectId,\n itemId,\n });\n }\n\n /**\n * Get one GitHub Projects V2 item by its project-item ID.\n *\n * This identity lookup does not hydrate content metadata, status, or fields.\n * Use {@link listItems} for the enriched board-item representation.\n *\n * @param itemId - GitHub project-item node ID\n * @returns The item, or `null` when it or its content is inaccessible\n */\n async getItem(itemId: string): Promise<ProjectItem | null> {\n const query = `\n query($itemId: ID!) {\n node(id: $itemId) {\n ... on ProjectV2Item {\n id\n content {\n ... on Issue {\n id\n typename: __typename\n }\n ... on PullRequest {\n id\n typename: __typename\n }\n ... on DraftIssue {\n id\n typename: __typename\n }\n }\n }\n }\n }\n `;\n\n const data = (await this.graphql.query(query, { itemId })) as {\n node: {\n id: string;\n content: Partial<GitHubProjectItemReference> | null;\n } | null;\n };\n\n if (!data.node?.content?.id || !data.node.content.typename) {\n return null;\n }\n\n return {\n id: data.node.id,\n contentId: data.node.content.id,\n fields: {},\n type: data.node.content.typename,\n };\n }\n\n /**\n * List GitHub Projects V2 items after an optional cursor.\n *\n * Deleted or inaccessible content is skipped, and the result defaults to at\n * most 100 accessible items to preserve the adapter's historical limit.\n *\n * @param filters - Optional cursor and maximum result count\n * @returns Provider-neutral project items with content metadata and fields\n */\n async listItems(filters?: ItemFilters): Promise<ProjectItem[]> {\n const query = `\n query($projectId: ID!, $first: Int!, $after: String) {\n node(id: $projectId) {\n ... on ProjectV2 {\n items(first: $first, after: $after) {\n nodes {\n id\n content {\n ... on Issue {\n id\n typename: __typename\n title\n url\n assignees(first: 100) {\n nodes {\n login\n }\n }\n }\n ... on PullRequest {\n id\n typename: __typename\n title\n url\n assignees(first: 100) {\n nodes {\n login\n }\n }\n }\n ... on DraftIssue {\n id\n typename: __typename\n title\n assignees(first: 100) {\n nodes {\n login\n }\n }\n }\n }\n fieldValues(first: 50) {\n nodes {\n typename: __typename\n # GitHub Projects have at most 50 fields. A 20-item page and\n # four 100-node nested connections stay below GitHub's\n # 500,000-node query limit while avoiding partial values.\n ... on ProjectV2ItemFieldDateValue {\n field {\n ...ProjectField\n }\n date\n }\n ... on ProjectV2ItemFieldIterationValue {\n field {\n ...ProjectField\n }\n title\n }\n ... on ProjectV2ItemFieldLabelValue {\n field {\n ...ProjectField\n }\n labels(first: 100) {\n nodes {\n name\n }\n }\n }\n ... on ProjectV2ItemFieldMilestoneValue {\n field {\n ...ProjectField\n }\n milestone {\n title\n }\n }\n ... on ProjectV2ItemFieldNumberValue {\n field {\n ...ProjectField\n }\n number\n }\n ... on ProjectV2ItemFieldPullRequestValue {\n field {\n ...ProjectField\n }\n pullRequests(first: 100) {\n nodes {\n url\n }\n }\n }\n ... on ProjectV2ItemFieldRepositoryValue {\n field {\n ...ProjectField\n }\n repository {\n nameWithOwner\n }\n }\n ... on ProjectV2ItemFieldReviewerValue {\n field {\n ...ProjectField\n }\n reviewers(first: 100) {\n nodes {\n ... on Bot {\n login\n }\n ... on EnterpriseTeam {\n slug\n }\n ... on Mannequin {\n login\n }\n ... on Team {\n slug\n }\n ... on User {\n login\n }\n }\n }\n }\n ... on ProjectV2ItemFieldSingleSelectValue {\n field {\n ...ProjectField\n }\n name\n }\n ... on ProjectV2ItemFieldTextValue {\n field {\n ...ProjectField\n }\n text\n }\n ... on ProjectV2ItemFieldUserValue {\n field {\n ...ProjectField\n }\n users(first: 100) {\n nodes {\n login\n }\n }\n }\n ... on ProjectV2ItemIssueFieldValue {\n field {\n ...ProjectField\n }\n issueFieldValue {\n typename: __typename\n ... on IssueFieldDateValue {\n dateValue: value\n }\n ... on IssueFieldMultiSelectValue {\n multiSelectOptions: options {\n name\n }\n }\n ... on IssueFieldNumberValue {\n numberValue: value\n }\n ... on IssueFieldSingleSelectValue {\n name\n singleSelectValue: value\n }\n ... on IssueFieldTextValue {\n textValue: value\n }\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n }\n }\n\n fragment ProjectField on ProjectV2FieldCommon {\n id\n name\n }\n `;\n\n const requestedLimit = filters?.limit ?? GITHUB_DEFAULT_ITEMS_LIMIT;\n const limit = Number.isFinite(requestedLimit)\n ? Math.max(0, Math.floor(requestedLimit))\n : GITHUB_DEFAULT_ITEMS_LIMIT;\n if (limit === 0) {\n return [];\n }\n\n const items: ProjectItem[] = [];\n let after = filters?.cursor;\n let fillPage = false;\n const seenCursors = new Set<string>();\n\n while (items.length < limit) {\n const remaining = limit - items.length;\n const first = fillPage\n ? GITHUB_ITEMS_PAGE_SIZE\n : Math.min(GITHUB_ITEMS_PAGE_SIZE, remaining);\n const data = await this.graphql.query<GitHubProjectItemsPage>(query, {\n projectId: this.projectId,\n first,\n after,\n });\n if (!data.node) {\n throw new ProjectError(\n `GitHub project ${this.projectId} was not found or is inaccessible`,\n ProjectErrorCode.NOT_FOUND,\n );\n }\n const connection = data.node.items;\n let skippedContent = false;\n\n for (const node of connection.nodes) {\n if (!node) {\n skippedContent = true;\n continue;\n }\n const item = this.mapProjectItem(node);\n if (item) {\n items.push(item);\n } else {\n skippedContent = true;\n }\n if (items.length >= limit) {\n break;\n }\n }\n fillPage = skippedContent;\n\n const { endCursor, hasNextPage } = connection.pageInfo;\n if (\n !hasNextPage ||\n !endCursor ||\n endCursor === after ||\n seenCursors.has(endCursor)\n ) {\n break;\n }\n seenCursors.add(endCursor);\n after = endCursor;\n }\n\n return items;\n }\n\n /**\n * Convert one GitHub Projects V2 item into the provider-neutral item shape.\n */\n private mapProjectItem(item: GitHubProjectItemNode): ProjectItem | null {\n const { content } = item;\n if (!content) {\n return null;\n }\n\n const fieldEntries: Array<[string, unknown]> = [];\n let status: string | undefined;\n\n for (const fieldValue of item.fieldValues.nodes) {\n if (!fieldValue?.field) {\n continue;\n }\n const value = this.mapProjectFieldValue(fieldValue);\n if (value !== undefined) {\n fieldEntries.push([fieldValue.field.name, value]);\n }\n\n const isStatusField = this.statusFieldId\n ? fieldValue.field.id === this.statusFieldId\n : fieldValue.field.name.toLowerCase() === 'status';\n if (isStatusField && typeof value === 'string') {\n status = value;\n }\n }\n\n return {\n id: item.id,\n contentId: content.id,\n title: content.title,\n url: content.url ?? null,\n assignees: content.assignees.nodes.flatMap((assignee) =>\n assignee ? [assignee.login] : [],\n ),\n status,\n fields: Object.fromEntries(fieldEntries),\n type: content.typename,\n };\n }\n\n /**\n * Convert GitHub's field-value union into stable scalar or string-array values.\n */\n private mapProjectFieldValue(value: GitHubProjectFieldValue): unknown {\n switch (value.typename) {\n case 'ProjectV2ItemFieldDateValue':\n return value.date;\n case 'ProjectV2ItemFieldIterationValue':\n return value.title;\n case 'ProjectV2ItemFieldLabelValue':\n return value.labels?.nodes.flatMap((label) =>\n label ? [label.name] : [],\n );\n case 'ProjectV2ItemFieldMilestoneValue':\n return value.milestone?.title;\n case 'ProjectV2ItemFieldNumberValue':\n return value.number;\n case 'ProjectV2ItemFieldPullRequestValue':\n return value.pullRequests?.nodes.flatMap((pullRequest) =>\n pullRequest ? [pullRequest.url] : [],\n );\n case 'ProjectV2ItemFieldRepositoryValue':\n return value.repository?.nameWithOwner;\n case 'ProjectV2ItemFieldReviewerValue':\n return value.reviewers?.nodes.flatMap((reviewer) => {\n const name = reviewer?.login ?? reviewer?.slug;\n return name ? [name] : [];\n });\n case 'ProjectV2ItemFieldSingleSelectValue':\n return value.name;\n case 'ProjectV2ItemFieldTextValue':\n return value.text;\n case 'ProjectV2ItemFieldUserValue':\n return value.users?.nodes.flatMap((user) => (user ? [user.login] : []));\n case 'ProjectV2ItemIssueFieldValue': {\n const issueValue = value.issueFieldValue;\n switch (issueValue?.typename) {\n case 'IssueFieldDateValue':\n return issueValue.dateValue;\n case 'IssueFieldMultiSelectValue':\n return issueValue.multiSelectOptions?.map((option) => option.name);\n case 'IssueFieldNumberValue':\n return issueValue.numberValue;\n case 'IssueFieldSingleSelectValue':\n return issueValue.name ?? issueValue.singleSelectValue;\n case 'IssueFieldTextValue':\n return issueValue.textValue;\n default:\n return undefined;\n }\n }\n default:\n return undefined;\n }\n }\n\n // Status Management\n async updateItemStatus(itemId: string, status: string): Promise<void> {\n if (!this.statusFieldId || !this.statusOptions) {\n throw new ProjectError(\n 'Status field configuration is required. Provide statusFieldId and statusOptions in config.',\n ProjectErrorCode.INVALID_STATUS,\n );\n }\n\n const statusOptionId = this.statusOptions[status];\n if (!statusOptionId) {\n throw new ProjectError(\n `Status \"${status}\" not found in project configuration. Available: ${Object.keys(this.statusOptions).join(', ')}`,\n ProjectErrorCode.INVALID_STATUS,\n );\n }\n\n await this.updateItemField(itemId, this.statusFieldId, statusOptionId);\n }\n\n async listStatuses(): Promise<Status[]> {\n const project = await this.getProject();\n return project.statuses;\n }\n\n // Field Management\n async updateItemField(\n itemId: string,\n fieldId: string,\n value: unknown,\n ): Promise<void> {\n const mutation = `\n mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {\n updateProjectV2ItemFieldValue(input: {\n projectId: $projectId\n itemId: $itemId\n fieldId: $fieldId\n value: $value\n }) {\n projectV2Item {\n id\n }\n }\n }\n `;\n\n await this.graphql.mutate(mutation, {\n projectId: this.projectId,\n itemId,\n fieldId,\n value: { singleSelectOptionId: value },\n });\n }\n\n async listFields(): Promise<Field[]> {\n const project = await this.getProject();\n return project.fields;\n }\n\n /**\n * Map GitHub field type to our standard types\n */\n private mapFieldType(\n dataType: string,\n ): 'text' | 'number' | 'date' | 'single_select' | 'iteration' {\n switch (dataType.toLowerCase()) {\n case 'text':\n return 'text';\n case 'number':\n return 'number';\n case 'date':\n return 'date';\n case 'single_select':\n return 'single_select';\n case 'iteration':\n return 'iteration';\n default:\n return 'text';\n }\n }\n}\n","/**\n * Core types for project management operations\n */\n\nexport interface Project {\n id: string;\n title: string;\n description?: string;\n owner: string; // Organization or user\n url: string;\n statuses: Status[];\n fields: Field[];\n}\n\n/**\n * A provider-neutral item on a project board.\n */\nexport interface ProjectItem {\n /** Provider project-item ID. */\n id: string;\n /** Provider content-node ID for the issue, pull request, or draft issue. */\n contentId: string;\n /** Content title when returned by the provider. */\n title?: string;\n /** Content URL, or `null` when the content has no provider URL. */\n url?: string | null;\n /** Provider usernames assigned to the content. */\n assignees?: string[];\n /** Selected status name from the configured or canonical status field. */\n status?: string;\n /** Provider field values keyed by field name. */\n fields: Record<string, unknown>;\n /** Kind of content represented by the project item. */\n type: 'Issue' | 'PullRequest' | 'DraftIssue';\n}\n\nexport interface Status {\n id: string;\n name: string;\n description?: string;\n color?: string;\n order: number;\n}\n\nexport interface Field {\n id: string;\n name: string;\n type: 'text' | 'number' | 'date' | 'single_select' | 'iteration';\n options?: FieldOption[];\n}\n\nexport interface FieldOption {\n id: string;\n name: string;\n description?: string;\n color?: string;\n}\n\n/**\n * Options for reading project items.\n */\nexport interface ItemFilters {\n /** Return items in this status when supported by the provider. */\n status?: string;\n /** Return items assigned to these provider usernames when supported. */\n assignees?: string[];\n /** Return items with these labels when supported. */\n labels?: string[];\n /** Maximum number of items to return. Defaults to 100; zero returns none. */\n limit?: number;\n /** Provider cursor after which item pagination begins. */\n cursor?: string;\n}\n\n/**\n * Project configuration\n */\nexport interface ProjectConfig {\n type: 'github' | 'jira' | 'zenhub' | 'linear';\n projectId: string;\n token: string;\n\n // GitHub Projects V2\n owner?: string; // Organization or user\n repo?: string; // Repository name (for repo-level projects)\n statusFieldId?: string;\n statusOptions?: Record<string, string>; // Map status name to option ID\n\n // Jira\n boardId?: string;\n\n // Linear\n teamId?: string;\n}\n\n/**\n * Kanban status names for 6-lane structure\n */\nexport const KANBAN_STATUSES = [\n 'New',\n 'Backlog',\n 'Planning',\n 'Ready',\n 'In Progress',\n 'Review',\n 'Done',\n] as const;\n\nexport type KanbanStatus = (typeof KANBAN_STATUSES)[number];\n\n/**\n * Project interface - all project implementations must implement this\n */\nexport interface IProject {\n // Project Info\n getProject(): Promise<Project>;\n\n // Items (Issues/Cards)\n addItem(contentId: string): Promise<ProjectItem>;\n removeItem(itemId: string): Promise<void>;\n getItem(itemId: string): Promise<ProjectItem | null>;\n listItems(filters?: ItemFilters): Promise<ProjectItem[]>;\n\n // Status/Column Management\n updateItemStatus(itemId: string, status: string): Promise<void>;\n listStatuses(): Promise<Status[]>;\n\n // Field Management\n updateItemField(\n itemId: string,\n fieldId: string,\n value: unknown,\n ): Promise<void>;\n listFields(): Promise<Field[]>;\n}\n"],"names":["ProjectErrorCode","GitHubProject","index"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/projects",
3
- "version": "0.78.2",
3
+ "version": "0.78.3",
4
4
  "description": "Standardized project management interface for GitHub Projects, Jira, ZenHub, and Linear",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,8 +15,8 @@
15
15
  "have-projects-context": "./dist/cli/claude-context.js"
16
16
  },
17
17
  "dependencies": {
18
- "@happyvertical/graphql": "0.78.2",
19
- "@happyvertical/repos": "0.78.2"
18
+ "@happyvertical/graphql": "0.78.3",
19
+ "@happyvertical/repos": "0.78.3"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "25.0.10",