@happyvertical/projects 0.74.8

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/AGENT.md ADDED
@@ -0,0 +1,33 @@
1
+ # @happyvertical/projects
2
+
3
+ <!-- BEGIN AGENT:GENERATED -->
4
+ ## Purpose
5
+ Standardized project management interface for GitHub Projects, Jira, ZenHub, and Linear
6
+
7
+ ## Package Map
8
+ - Package: `@happyvertical/projects`
9
+ - Hierarchy path: `@happyvertical/sdk > packages > projects`
10
+ - Workspace position: `21 of 30` local packages
11
+ - Internal dependencies: `@happyvertical/graphql`, `@happyvertical/repos`
12
+ - Internal dependents: `@happyvertical/github-actions`
13
+ - Knowledge graph files: `AGENT.md`, `metadata.json`, `ecosystem-manifest.json`
14
+
15
+ ## Build & Test
16
+ ```bash
17
+ pnpm --filter @happyvertical/projects build
18
+ pnpm --filter @happyvertical/projects test
19
+ pnpm --filter @happyvertical/projects clean
20
+ ```
21
+
22
+ ## Agent Correction Loops
23
+ - If module resolution or export errors mention a workspace dependency, build the dependency first (`pnpm --filter @happyvertical/graphql build`, `pnpm --filter @happyvertical/repos build`) and then rerun `pnpm --filter @happyvertical/projects build`.
24
+ - If tests or exports fail after API, type, or bundle changes, run `pnpm --filter @happyvertical/projects clean` followed by `pnpm --filter @happyvertical/projects build` and `pnpm --filter @happyvertical/projects test`.
25
+ - If failures span multiple packages or Turborepo ordering looks wrong, run `pnpm build` and `pnpm typecheck` from the repo root before retrying package-scoped commands.
26
+
27
+ ## Ecosystem Relationships
28
+ - Provides: Standardized project management interface for GitHub Projects, Jira, ZenHub, and Linear
29
+ - Implements: none
30
+ - Requires: @happyvertical/graphql, @happyvertical/repos
31
+ - Stability: stable (Primary package surface is described as implemented and production-oriented.)
32
+ <!-- END AGENT:GENERATED -->
33
+
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,352 @@
1
+ # @happyvertical/projects
2
+
3
+ Standardized project management interface for GitHub Projects, Jira, ZenHub, and Linear.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @happyvertical/projects @happyvertical/repos
9
+ ```
10
+
11
+ ## Claude Code Context
12
+
13
+ Install Claude Code context files for AI-assisted development:
14
+
15
+ ```bash
16
+ npx have-projects-context
17
+ ```
18
+
19
+ This copies the package's `AGENT.md` documentation and `metadata.json` metadata to your project's `.claude/` directory, enabling Claude to provide better assistance when working with this package.
20
+
21
+ ## Usage
22
+
23
+ ### Basic Example
24
+
25
+ ```typescript
26
+ import { getProject } from '@happyvertical/projects';
27
+ import { getRepository } from '@happyvertical/repos';
28
+
29
+ // Get repository to retrieve issue node IDs
30
+ const repo = await getRepository({
31
+ type: 'github',
32
+ owner: 'happyvertical',
33
+ repo: 'sdk',
34
+ token: process.env.GITHUB_TOKEN
35
+ });
36
+
37
+ // Get project with status field configuration
38
+ const project = await getProject({
39
+ type: 'github',
40
+ projectId: 'PVT_kwDOB9Y8ns4A8-TY',
41
+ token: process.env.GITHUB_TOKEN,
42
+ statusFieldId: 'PVTSSF_lADOB9Y8ns4A8-TYzgw0GaY',
43
+ statusOptions: {
44
+ 'New': 'option-id-1',
45
+ 'Backlog': 'option-id-2',
46
+ 'In Progress': 'option-id-3',
47
+ 'Done': 'option-id-4'
48
+ }
49
+ });
50
+
51
+ // Add issue to project
52
+ const issue = await repo.getIssue(352);
53
+ const item = await project.addItem(issue.id);
54
+
55
+ // Update status
56
+ await project.updateItemStatus(item.id, 'In Progress');
57
+ ```
58
+
59
+ ### Kanban Workflow
60
+
61
+ ```typescript
62
+ // List all items
63
+ const items = await project.listItems();
64
+
65
+ // Get available statuses
66
+ const statuses = await project.listStatuses();
67
+
68
+ // Move item through workflow
69
+ await project.updateItemStatus(itemId, 'Ready');
70
+ await project.updateItemStatus(itemId, 'In Progress');
71
+ await project.updateItemStatus(itemId, 'Review');
72
+ await project.updateItemStatus(itemId, 'Done');
73
+ ```
74
+
75
+ ### Custom Field Management
76
+
77
+ ```typescript
78
+ // List all project fields
79
+ const fields = await project.listFields();
80
+
81
+ // Update custom field
82
+ const priorityField = fields.find(f => f.name === 'Priority');
83
+ if (priorityField) {
84
+ await project.updateItemField(itemId, priorityField.id, 'high-priority-option-id');
85
+ }
86
+ ```
87
+
88
+ ## API Reference
89
+
90
+ ### Factory Function
91
+
92
+ #### `getProject(options)`
93
+
94
+ Creates a project management client instance.
95
+
96
+ **Parameters:**
97
+ - `options: ProjectConfig | IProject` - Project configuration or existing instance
98
+
99
+ **Returns:** `Promise<IProject>`
100
+
101
+ **Example:**
102
+ ```typescript
103
+ const project = await getProject({
104
+ type: 'github',
105
+ projectId: 'PVT_kwDOB9Y8ns4A8-TY',
106
+ token: process.env.GITHUB_TOKEN,
107
+ statusFieldId: 'PVTSSF_lADOB9Y8ns4A8-TYzgw0GaY',
108
+ statusOptions: {
109
+ 'New': 'option-id-1',
110
+ 'Backlog': 'option-id-2',
111
+ 'In Progress': 'option-id-3',
112
+ 'Done': 'option-id-4'
113
+ }
114
+ });
115
+ ```
116
+
117
+ ### IProject Interface
118
+
119
+ #### Project Information
120
+
121
+ ##### `getProject(): Promise<Project>`
122
+
123
+ Get project details including fields and statuses.
124
+
125
+ **Returns:** Project metadata with all fields and status options
126
+
127
+ #### Item Management
128
+
129
+ ##### `addItem(contentId: string): Promise<ProjectItem>`
130
+
131
+ Add an issue or pull request to the project.
132
+
133
+ **Parameters:**
134
+ - `contentId: string` - Node ID of the issue or PR (from @happyvertical/repos)
135
+
136
+ **Returns:** Created project item
137
+
138
+ ##### `removeItem(itemId: string): Promise<void>`
139
+
140
+ Remove an item from the project.
141
+
142
+ **Parameters:**
143
+ - `itemId: string` - Project item ID
144
+
145
+ ##### `getItem(itemId: string): Promise<ProjectItem | null>`
146
+
147
+ Get a specific project item.
148
+
149
+ **Parameters:**
150
+ - `itemId: string` - Project item ID
151
+
152
+ **Returns:** Project item or null if not found
153
+
154
+ ##### `listItems(filters?: ItemFilters): Promise<ProjectItem[]>`
155
+
156
+ List all items in the project.
157
+
158
+ **Parameters:**
159
+ - `filters?: ItemFilters` - Optional filters (limit, cursor, status)
160
+
161
+ **Returns:** Array of project items
162
+
163
+ #### Status Management
164
+
165
+ ##### `updateItemStatus(itemId: string, status: string): Promise<void>`
166
+
167
+ Update the status of a project item.
168
+
169
+ **Parameters:**
170
+ - `itemId: string` - Project item ID
171
+ - `status: string` - Status name (must match statusOptions in config)
172
+
173
+ **Example:**
174
+ ```typescript
175
+ await project.updateItemStatus(itemId, 'In Progress');
176
+ ```
177
+
178
+ ##### `listStatuses(): Promise<Status[]>`
179
+
180
+ Get all available status options.
181
+
182
+ **Returns:** Array of status definitions with IDs, names, colors
183
+
184
+ #### Field Management
185
+
186
+ ##### `updateItemField(itemId: string, fieldId: string, value: unknown): Promise<void>`
187
+
188
+ Update a custom field value.
189
+
190
+ **Parameters:**
191
+ - `itemId: string` - Project item ID
192
+ - `fieldId: string` - Field ID
193
+ - `value: unknown` - Field value (type depends on field type)
194
+
195
+ ##### `listFields(): Promise<Field[]>`
196
+
197
+ Get all project fields.
198
+
199
+ **Returns:** Array of field definitions
200
+
201
+ ## GitHub Projects V2 Setup
202
+
203
+ ### Finding Your Project ID
204
+
205
+ 1. Navigate to your GitHub Project
206
+ 2. In the URL, find the project number: `https://github.com/orgs/ORG/projects/123`
207
+ 3. Use GitHub CLI to get the node ID:
208
+ ```bash
209
+ gh api graphql -f query='
210
+ query {
211
+ organization(login: "ORG") {
212
+ projectV2(number: 123) {
213
+ id
214
+ }
215
+ }
216
+ }
217
+ '
218
+ ```
219
+
220
+ ### Finding Status Field ID and Options
221
+
222
+ ```bash
223
+ gh api graphql -f query='
224
+ query {
225
+ node(id: "PVT_kwDOB9Y8ns4A8-TY") {
226
+ ... on ProjectV2 {
227
+ fields(first: 20) {
228
+ nodes {
229
+ ... on ProjectV2SingleSelectField {
230
+ id
231
+ name
232
+ options {
233
+ id
234
+ name
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ '
243
+ ```
244
+
245
+ Look for the "Status" field and note:
246
+ - Field ID (`PVTSSF_...`)
247
+ - Option IDs for each status
248
+
249
+ ### Configuration Example
250
+
251
+ ```typescript
252
+ const project = await getProject({
253
+ type: 'github',
254
+ projectId: 'PVT_kwDOB9Y8ns4A8-TY', // From step 1
255
+ token: process.env.GITHUB_TOKEN,
256
+ statusFieldId: 'PVTSSF_lADOB9Y8ns4A8-TYzgw0GaY', // From step 2
257
+ statusOptions: {
258
+ 'New': '47fc9ee4', // Option IDs from step 2
259
+ 'Backlog': '4c6e9e0c',
260
+ 'Planning': 'f75ad846',
261
+ 'Ready': '98236657',
262
+ 'In Progress': 'b5f9e3e1',
263
+ 'Review': '5e1b3e8f',
264
+ 'Done': '3f4e5d6c'
265
+ }
266
+ });
267
+ ```
268
+
269
+ ## Standard Kanban Statuses
270
+
271
+ The package exports standard kanban status names:
272
+
273
+ ```typescript
274
+ import { KANBAN_STATUSES } from '@happyvertical/projects';
275
+
276
+ // ['New', 'Backlog', 'Planning', 'Ready', 'In Progress', 'Review', 'Done']
277
+ ```
278
+
279
+ ## Error Handling
280
+
281
+ ```typescript
282
+ import { ProjectError, ProjectErrorCode } from '@happyvertical/projects';
283
+
284
+ try {
285
+ await project.updateItemStatus(itemId, 'Invalid Status');
286
+ } catch (error) {
287
+ if (error instanceof ProjectError) {
288
+ switch (error.code) {
289
+ case ProjectErrorCode.INVALID_STATUS:
290
+ console.error('Status not found:', error.message);
291
+ break;
292
+ case ProjectErrorCode.UNAUTHORIZED:
293
+ console.error('Token lacks permissions');
294
+ break;
295
+ case ProjectErrorCode.RATE_LIMITED:
296
+ console.error('Rate limited, retry after delay');
297
+ break;
298
+ }
299
+ }
300
+ }
301
+ ```
302
+
303
+ ## Integration with @happyvertical/repos
304
+
305
+ Projects work seamlessly with repositories:
306
+
307
+ ```typescript
308
+ import { getRepository } from '@happyvertical/repos';
309
+ import { getProject } from '@happyvertical/projects';
310
+
311
+ const repo = await getRepository({
312
+ type: 'github',
313
+ owner: 'happyvertical',
314
+ repo: 'sdk',
315
+ token: process.env.GITHUB_TOKEN
316
+ });
317
+
318
+ const project = await getProject({
319
+ type: 'github',
320
+ projectId: 'PVT_kwDOB9Y8ns4A8-TY',
321
+ token: process.env.GITHUB_TOKEN,
322
+ statusFieldId: 'PVTSSF_lADOB9Y8ns4A8-TYzgw0GaY',
323
+ statusOptions: {
324
+ 'New': 'option-id-1',
325
+ 'In Progress': 'option-id-2',
326
+ 'Done': 'option-id-3'
327
+ }
328
+ });
329
+
330
+ // Create issue and add to project
331
+ const issue = await repo.createIssue({
332
+ title: 'New feature',
333
+ body: 'Description',
334
+ labels: ['type: feature']
335
+ });
336
+
337
+ const item = await project.addItem(issue.id);
338
+ await project.updateItemStatus(item.id, 'In Progress');
339
+ ```
340
+
341
+ ## Supported Platforms
342
+
343
+ | Platform | Status | Notes |
344
+ |----------|--------|-------|
345
+ | GitHub Projects V2 | ✅ Complete | Full GraphQL implementation |
346
+ | Jira | 🚧 Planned | Coming soon |
347
+ | ZenHub | 🚧 Planned | Coming soon |
348
+ | Linear | 🚧 Planned | Coming soon |
349
+
350
+ ## License
351
+
352
+ MIT
@@ -0,0 +1 @@
1
+ export { }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=claude-context.d.ts.map
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, copyFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const Dirname = dirname(fileURLToPath(import.meta.url));
6
+ const pkgRoot = join(Dirname, "../..");
7
+ const targetDir = join(process.cwd(), ".claude");
8
+ if (!existsSync(targetDir)) {
9
+ mkdirSync(targetDir, { recursive: true });
10
+ }
11
+ const pkgName = "projects";
12
+ const agentMdSrc = existsSync(join(pkgRoot, "AGENT.md")) ? join(pkgRoot, "AGENT.md") : join(pkgRoot, "CLAUDE.md");
13
+ const metaSrc = existsSync(join(pkgRoot, "metadata.json")) ? join(pkgRoot, "metadata.json") : join(pkgRoot, ".claude-meta.json");
14
+ if (existsSync(agentMdSrc)) {
15
+ copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));
16
+ }
17
+ if (existsSync(metaSrc)) {
18
+ copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));
19
+ }
20
+ console.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);
21
+ //# sourceMappingURL=claude-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-context.js","sources":["../../src/cli/claude-context.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * CLI script to install agent context for @happyvertical/projects\n * Run the published context installer binary for this package.\n */\nimport { copyFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst Dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgRoot = join(Dirname, '../..');\nconst targetDir = join(process.cwd(), '.claude');\n\nif (!existsSync(targetDir)) {\n mkdirSync(targetDir, { recursive: true });\n}\n\nconst pkgName = 'projects';\nconst agentMdSrc = existsSync(join(pkgRoot, 'AGENT.md'))\n ? join(pkgRoot, 'AGENT.md')\n : join(pkgRoot, 'CLAUDE.md');\nconst metaSrc = existsSync(join(pkgRoot, 'metadata.json'))\n ? join(pkgRoot, 'metadata.json')\n : join(pkgRoot, '.claude-meta.json');\n\nif (existsSync(agentMdSrc)) {\n copyFileSync(agentMdSrc, join(targetDir, `have-${pkgName}.md`));\n}\n\nif (existsSync(metaSrc)) {\n copyFileSync(metaSrc, join(targetDir, `have-${pkgName}.meta.json`));\n}\n\nconsole.log(`✓ Installed @happyvertical/${pkgName} context to .claude/`);\n"],"names":[],"mappings":";;;;AASA,MAAM,UAAU,QAAQ,cAAc,YAAY,GAAG,CAAC;AACtD,MAAM,UAAU,KAAK,SAAS,OAAO;AACrC,MAAM,YAAY,KAAK,QAAQ,IAAA,GAAO,SAAS;AAE/C,IAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,YAAU,WAAW,EAAE,WAAW,KAAA,CAAM;AAC1C;AAEA,MAAM,UAAU;AAChB,MAAM,aAAa,WAAW,KAAK,SAAS,UAAU,CAAC,IACnD,KAAK,SAAS,UAAU,IACxB,KAAK,SAAS,WAAW;AAC7B,MAAM,UAAU,WAAW,KAAK,SAAS,eAAe,CAAC,IACrD,KAAK,SAAS,eAAe,IAC7B,KAAK,SAAS,mBAAmB;AAErC,IAAI,WAAW,UAAU,GAAG;AAC1B,eAAa,YAAY,KAAK,WAAW,QAAQ,OAAO,KAAK,CAAC;AAChE;AAEA,IAAI,WAAW,OAAO,GAAG;AACvB,eAAa,SAAS,KAAK,WAAW,QAAQ,OAAO,YAAY,CAAC;AACpE;AAEA,QAAQ,IAAI,8BAA8B,OAAO,sBAAsB;"}
@@ -0,0 +1,178 @@
1
+ export declare interface Field {
2
+ id: string;
3
+ name: string;
4
+ type: 'text' | 'number' | 'date' | 'single_select' | 'iteration';
5
+ options?: FieldOption[];
6
+ }
7
+
8
+ export declare interface FieldOption {
9
+ id: string;
10
+ name: string;
11
+ description?: string;
12
+ color?: string;
13
+ }
14
+
15
+ /**
16
+ * Get a project management client instance
17
+ *
18
+ * This is the main entry point following the pattern from @happyvertical/files and @happyvertical/sql.
19
+ * It can accept either a configuration object or an existing project instance.
20
+ *
21
+ * @param options - Project configuration or existing project instance
22
+ * @returns Promise resolving to project interface
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * import { getProject } from '@happyvertical/projects';
27
+ *
28
+ * // Create a GitHub Projects client
29
+ * const project = await getProject({
30
+ * type: 'github',
31
+ * owner: 'happyvertical',
32
+ * projectId: 'PVT_kwDOB9Y8ns4A8-TY',
33
+ * token: process.env.GITHUB_TOKEN,
34
+ * statusFieldId: 'PVTSSF_lADOB9Y8ns4A8-TYzgw0GaY',
35
+ * statusOptions: {
36
+ * 'New': 'option-id-1',
37
+ * 'Backlog': 'option-id-2',
38
+ * 'In Progress': 'option-id-3',
39
+ * 'Done': 'option-id-4'
40
+ * }
41
+ * });
42
+ *
43
+ * // Use the client
44
+ * await project.addItem(issueNodeId);
45
+ * await project.updateItemStatus(itemId, 'In Progress');
46
+ *
47
+ * // Pass existing instance (returns it unchanged)
48
+ * const sameProject = await getProject(project);
49
+ * ```
50
+ */
51
+ export declare function getProject(options: ProjectConfig | IProject): Promise<IProject>;
52
+
53
+ /**
54
+ * GitHub Projects V2 implementation
55
+ */
56
+ export declare class GitHubProject implements IProject {
57
+ private graphql;
58
+ private projectId;
59
+ private owner?;
60
+ private repo?;
61
+ private statusFieldId?;
62
+ private statusOptions?;
63
+ constructor(config: ProjectConfig);
64
+ getProject(): Promise<Project>;
65
+ addItem(contentId: string): Promise<ProjectItem>;
66
+ removeItem(itemId: string): Promise<void>;
67
+ getItem(itemId: string): Promise<ProjectItem | null>;
68
+ listItems(filters?: ItemFilters): Promise<ProjectItem[]>;
69
+ updateItemStatus(itemId: string, status: string): Promise<void>;
70
+ listStatuses(): Promise<Status[]>;
71
+ updateItemField(itemId: string, fieldId: string, value: unknown): Promise<void>;
72
+ listFields(): Promise<Field[]>;
73
+ /**
74
+ * Map GitHub field type to our standard types
75
+ */
76
+ private mapFieldType;
77
+ }
78
+
79
+ /**
80
+ * Project interface - all project implementations must implement this
81
+ */
82
+ export declare interface IProject {
83
+ getProject(): Promise<Project>;
84
+ addItem(contentId: string): Promise<ProjectItem>;
85
+ removeItem(itemId: string): Promise<void>;
86
+ getItem(itemId: string): Promise<ProjectItem | null>;
87
+ listItems(filters?: ItemFilters): Promise<ProjectItem[]>;
88
+ updateItemStatus(itemId: string, status: string): Promise<void>;
89
+ listStatuses(): Promise<Status[]>;
90
+ updateItemField(itemId: string, fieldId: string, value: unknown): Promise<void>;
91
+ listFields(): Promise<Field[]>;
92
+ }
93
+
94
+ export declare interface ItemFilters {
95
+ status?: string;
96
+ assignees?: string[];
97
+ labels?: string[];
98
+ limit?: number;
99
+ cursor?: string;
100
+ }
101
+
102
+ /**
103
+ * Kanban status names for 6-lane structure
104
+ */
105
+ export declare const KANBAN_STATUSES: readonly ["New", "Backlog", "Planning", "Ready", "In Progress", "Review", "Done"];
106
+
107
+ export declare type KanbanStatus = (typeof KANBAN_STATUSES)[number];
108
+
109
+ /**
110
+ * Core types for project management operations
111
+ */
112
+ export declare interface Project {
113
+ id: string;
114
+ title: string;
115
+ description?: string;
116
+ owner: string;
117
+ url: string;
118
+ statuses: Status[];
119
+ fields: Field[];
120
+ }
121
+
122
+ /**
123
+ * Project configuration
124
+ */
125
+ export declare interface ProjectConfig {
126
+ type: 'github' | 'jira' | 'zenhub' | 'linear';
127
+ projectId: string;
128
+ token: string;
129
+ owner?: string;
130
+ repo?: string;
131
+ statusFieldId?: string;
132
+ statusOptions?: Record<string, string>;
133
+ boardId?: string;
134
+ teamId?: string;
135
+ }
136
+
137
+ export declare class ProjectError extends Error {
138
+ code: ProjectErrorCode;
139
+ statusCode?: number | undefined;
140
+ response?: unknown | undefined;
141
+ constructor(message: string, code: ProjectErrorCode, statusCode?: number | undefined, response?: unknown | undefined);
142
+ static fromHTTPStatus(statusCode: number, message: string, response?: unknown): ProjectError;
143
+ static networkError(message: string, cause?: Error): ProjectError;
144
+ isRetryable(): boolean;
145
+ }
146
+
147
+ /**
148
+ * Project error handling
149
+ */
150
+ export declare enum ProjectErrorCode {
151
+ NOT_FOUND = "NOT_FOUND",
152
+ UNAUTHORIZED = "UNAUTHORIZED",
153
+ INVALID_STATUS = "INVALID_STATUS",
154
+ INVALID_FIELD = "INVALID_FIELD",
155
+ ITEM_NOT_IN_PROJECT = "ITEM_NOT_IN_PROJECT",
156
+ RATE_LIMITED = "RATE_LIMITED",
157
+ NETWORK_ERROR = "NETWORK_ERROR",
158
+ VALIDATION_ERROR = "VALIDATION_ERROR",
159
+ UNKNOWN = "UNKNOWN"
160
+ }
161
+
162
+ export declare interface ProjectItem {
163
+ id: string;
164
+ contentId: string;
165
+ status?: string;
166
+ fields: Record<string, unknown>;
167
+ type: 'Issue' | 'PullRequest' | 'DraftIssue';
168
+ }
169
+
170
+ export declare interface Status {
171
+ id: string;
172
+ name: string;
173
+ description?: string;
174
+ color?: string;
175
+ order: number;
176
+ }
177
+
178
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,409 @@
1
+ import { GraphQLClient } from "@happyvertical/graphql";
2
+ var ProjectErrorCode = /* @__PURE__ */ ((ProjectErrorCode2) => {
3
+ ProjectErrorCode2["NOT_FOUND"] = "NOT_FOUND";
4
+ ProjectErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
5
+ ProjectErrorCode2["INVALID_STATUS"] = "INVALID_STATUS";
6
+ ProjectErrorCode2["INVALID_FIELD"] = "INVALID_FIELD";
7
+ ProjectErrorCode2["ITEM_NOT_IN_PROJECT"] = "ITEM_NOT_IN_PROJECT";
8
+ ProjectErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
9
+ ProjectErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
10
+ ProjectErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
11
+ ProjectErrorCode2["UNKNOWN"] = "UNKNOWN";
12
+ return ProjectErrorCode2;
13
+ })(ProjectErrorCode || {});
14
+ class ProjectError extends Error {
15
+ constructor(message, code, statusCode, response) {
16
+ super(message);
17
+ this.code = code;
18
+ this.statusCode = statusCode;
19
+ this.response = response;
20
+ this.name = "ProjectError";
21
+ Error.captureStackTrace(this, this.constructor);
22
+ }
23
+ static fromHTTPStatus(statusCode, message, response) {
24
+ let code;
25
+ switch (statusCode) {
26
+ case 404:
27
+ code = "NOT_FOUND";
28
+ break;
29
+ case 401:
30
+ code = "UNAUTHORIZED";
31
+ break;
32
+ case 403:
33
+ code = "UNAUTHORIZED";
34
+ break;
35
+ case 429:
36
+ code = "RATE_LIMITED";
37
+ break;
38
+ case 422:
39
+ code = "VALIDATION_ERROR";
40
+ break;
41
+ default:
42
+ code = "UNKNOWN";
43
+ }
44
+ return new ProjectError(message, code, statusCode, response);
45
+ }
46
+ static networkError(message, cause) {
47
+ return new ProjectError(
48
+ message,
49
+ "NETWORK_ERROR",
50
+ void 0,
51
+ cause
52
+ );
53
+ }
54
+ isRetryable() {
55
+ return this.code === "RATE_LIMITED" || this.code === "NETWORK_ERROR";
56
+ }
57
+ }
58
+ function isProjectInstance(value) {
59
+ return value !== null && typeof value === "object" && "getProject" in value && "addItem" in value && "updateItemStatus" in value && typeof value.getProject === "function";
60
+ }
61
+ async function getProject(options) {
62
+ if (isProjectInstance(options)) {
63
+ return options;
64
+ }
65
+ if (!options.type) {
66
+ throw new ProjectError(
67
+ "Project type is required",
68
+ ProjectErrorCode.VALIDATION_ERROR
69
+ );
70
+ }
71
+ if (!options.projectId) {
72
+ throw new ProjectError(
73
+ "Project ID is required",
74
+ ProjectErrorCode.VALIDATION_ERROR
75
+ );
76
+ }
77
+ if (!options.token) {
78
+ throw new ProjectError(
79
+ "Authentication token is required",
80
+ ProjectErrorCode.UNAUTHORIZED
81
+ );
82
+ }
83
+ switch (options.type) {
84
+ case "github": {
85
+ const { GitHubProject: GitHubProject2 } = await Promise.resolve().then(() => index);
86
+ return new GitHubProject2(options);
87
+ }
88
+ case "jira":
89
+ throw new ProjectError(
90
+ "Jira support not yet implemented",
91
+ ProjectErrorCode.UNKNOWN
92
+ );
93
+ case "zenhub":
94
+ throw new ProjectError(
95
+ "ZenHub support not yet implemented",
96
+ ProjectErrorCode.UNKNOWN
97
+ );
98
+ case "linear":
99
+ throw new ProjectError(
100
+ "Linear support not yet implemented",
101
+ ProjectErrorCode.UNKNOWN
102
+ );
103
+ default:
104
+ throw new ProjectError(
105
+ `Unsupported project type: ${options.type}`,
106
+ ProjectErrorCode.VALIDATION_ERROR
107
+ );
108
+ }
109
+ }
110
+ class GitHubProject {
111
+ graphql;
112
+ projectId;
113
+ owner;
114
+ repo;
115
+ statusFieldId;
116
+ statusOptions;
117
+ constructor(config) {
118
+ if (config.type !== "github") {
119
+ throw new Error("Invalid config type for GitHubProject");
120
+ }
121
+ this.graphql = new GraphQLClient({
122
+ endpoint: "https://api.github.com/graphql",
123
+ token: config.token
124
+ });
125
+ this.projectId = config.projectId;
126
+ this.owner = config.owner;
127
+ this.repo = config.repo;
128
+ this.statusFieldId = config.statusFieldId;
129
+ this.statusOptions = config.statusOptions;
130
+ }
131
+ // Project Info
132
+ async getProject() {
133
+ const query = `
134
+ query($projectId: ID!) {
135
+ node(id: $projectId) {
136
+ ... on ProjectV2 {
137
+ id
138
+ title
139
+ shortDescription
140
+ url
141
+ owner {
142
+ ... on Organization {
143
+ login
144
+ }
145
+ ... on User {
146
+ login
147
+ }
148
+ }
149
+ fields(first: 20) {
150
+ nodes {
151
+ ... on ProjectV2Field {
152
+ id
153
+ name
154
+ dataType
155
+ }
156
+ ... on ProjectV2SingleSelectField {
157
+ id
158
+ name
159
+ dataType
160
+ options {
161
+ id
162
+ name
163
+ description
164
+ color
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ `;
173
+ const data = await this.graphql.query(query, {
174
+ projectId: this.projectId
175
+ });
176
+ const statusField = data.node.fields.nodes.find(
177
+ (f) => f.name === "Status" && f.options
178
+ );
179
+ return {
180
+ id: data.node.id,
181
+ title: data.node.title,
182
+ description: data.node.shortDescription,
183
+ owner: data.node.owner.login,
184
+ url: data.node.url,
185
+ statuses: statusField?.options ? statusField.options.map((opt, index2) => ({
186
+ id: opt.id,
187
+ name: opt.name,
188
+ description: opt.description,
189
+ color: opt.color,
190
+ order: index2
191
+ })) : [],
192
+ fields: data.node.fields.nodes.map((f) => ({
193
+ id: f.id,
194
+ name: f.name,
195
+ type: this.mapFieldType(f.dataType),
196
+ options: f.options?.map((opt) => ({
197
+ id: opt.id,
198
+ name: opt.name,
199
+ description: opt.description,
200
+ color: opt.color
201
+ }))
202
+ }))
203
+ };
204
+ }
205
+ // Items
206
+ async addItem(contentId) {
207
+ const mutation = `
208
+ mutation($projectId: ID!, $contentId: ID!) {
209
+ addProjectV2ItemById(input: {
210
+ projectId: $projectId
211
+ contentId: $contentId
212
+ }) {
213
+ item {
214
+ id
215
+ content {
216
+ ... on Issue {
217
+ id
218
+ }
219
+ ... on PullRequest {
220
+ id
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ `;
227
+ const data = await this.graphql.mutate(mutation, {
228
+ projectId: this.projectId,
229
+ contentId
230
+ });
231
+ return {
232
+ id: data.addProjectV2ItemById.item.id,
233
+ contentId: data.addProjectV2ItemById.item.content.id,
234
+ fields: {},
235
+ type: "Issue"
236
+ };
237
+ }
238
+ async removeItem(itemId) {
239
+ const mutation = `
240
+ mutation($projectId: ID!, $itemId: ID!) {
241
+ deleteProjectV2Item(input: {
242
+ projectId: $projectId
243
+ itemId: $itemId
244
+ }) {
245
+ deletedItemId
246
+ }
247
+ }
248
+ `;
249
+ await this.graphql.mutate(mutation, {
250
+ projectId: this.projectId,
251
+ itemId
252
+ });
253
+ }
254
+ async getItem(itemId) {
255
+ const query = `
256
+ query($itemId: ID!) {
257
+ node(id: $itemId) {
258
+ ... on ProjectV2Item {
259
+ id
260
+ content {
261
+ ... on Issue {
262
+ id
263
+ }
264
+ ... on PullRequest {
265
+ id
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ `;
272
+ const data = await this.graphql.query(query, { itemId });
273
+ if (!data.node) {
274
+ return null;
275
+ }
276
+ return {
277
+ id: data.node.id,
278
+ contentId: data.node.content.id,
279
+ fields: {},
280
+ type: "Issue"
281
+ };
282
+ }
283
+ async listItems(filters) {
284
+ const query = `
285
+ query($projectId: ID!, $first: Int!, $after: String) {
286
+ node(id: $projectId) {
287
+ ... on ProjectV2 {
288
+ items(first: $first, after: $after) {
289
+ nodes {
290
+ id
291
+ content {
292
+ ... on Issue {
293
+ id
294
+ }
295
+ ... on PullRequest {
296
+ id
297
+ }
298
+ }
299
+ }
300
+ pageInfo {
301
+ hasNextPage
302
+ endCursor
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ `;
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) => ({
315
+ id: item.id,
316
+ contentId: item.content.id,
317
+ fields: {},
318
+ type: "Issue"
319
+ }));
320
+ }
321
+ // Status Management
322
+ async updateItemStatus(itemId, status) {
323
+ if (!this.statusFieldId || !this.statusOptions) {
324
+ throw new ProjectError(
325
+ "Status field configuration is required. Provide statusFieldId and statusOptions in config.",
326
+ ProjectErrorCode.INVALID_STATUS
327
+ );
328
+ }
329
+ const statusOptionId = this.statusOptions[status];
330
+ if (!statusOptionId) {
331
+ throw new ProjectError(
332
+ `Status "${status}" not found in project configuration. Available: ${Object.keys(this.statusOptions).join(", ")}`,
333
+ ProjectErrorCode.INVALID_STATUS
334
+ );
335
+ }
336
+ await this.updateItemField(itemId, this.statusFieldId, statusOptionId);
337
+ }
338
+ async listStatuses() {
339
+ const project = await this.getProject();
340
+ return project.statuses;
341
+ }
342
+ // Field Management
343
+ async updateItemField(itemId, fieldId, value) {
344
+ const mutation = `
345
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
346
+ updateProjectV2ItemFieldValue(input: {
347
+ projectId: $projectId
348
+ itemId: $itemId
349
+ fieldId: $fieldId
350
+ value: $value
351
+ }) {
352
+ projectV2Item {
353
+ id
354
+ }
355
+ }
356
+ }
357
+ `;
358
+ await this.graphql.mutate(mutation, {
359
+ projectId: this.projectId,
360
+ itemId,
361
+ fieldId,
362
+ value: { singleSelectOptionId: value }
363
+ });
364
+ }
365
+ async listFields() {
366
+ const project = await this.getProject();
367
+ return project.fields;
368
+ }
369
+ /**
370
+ * Map GitHub field type to our standard types
371
+ */
372
+ mapFieldType(dataType) {
373
+ switch (dataType.toLowerCase()) {
374
+ case "text":
375
+ return "text";
376
+ case "number":
377
+ return "number";
378
+ case "date":
379
+ return "date";
380
+ case "single_select":
381
+ return "single_select";
382
+ case "iteration":
383
+ return "iteration";
384
+ default:
385
+ return "text";
386
+ }
387
+ }
388
+ }
389
+ const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
390
+ __proto__: null,
391
+ GitHubProject
392
+ }, Symbol.toStringTag, { value: "Module" }));
393
+ const KANBAN_STATUSES = [
394
+ "New",
395
+ "Backlog",
396
+ "Planning",
397
+ "Ready",
398
+ "In Progress",
399
+ "Review",
400
+ "Done"
401
+ ];
402
+ export {
403
+ GitHubProject,
404
+ KANBAN_STATUSES,
405
+ ProjectError,
406
+ ProjectErrorCode,
407
+ getProject
408
+ };
409
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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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"}
package/metadata.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@happyvertical/projects",
3
+ "path": "packages/projects",
4
+ "position": {
5
+ "index": 21,
6
+ "count": 30
7
+ },
8
+ "description": "Standardized project management interface for GitHub Projects, Jira, ZenHub, and Linear",
9
+ "provides": [
10
+ "Standardized project management interface for GitHub Projects, Jira, ZenHub, and Linear"
11
+ ],
12
+ "implements": [],
13
+ "requires": {
14
+ "workspace": [
15
+ "@happyvertical/graphql",
16
+ "@happyvertical/repos"
17
+ ],
18
+ "externalHappyVertical": [],
19
+ "external": []
20
+ },
21
+ "dependents": [
22
+ "@happyvertical/github-actions"
23
+ ],
24
+ "stability": {
25
+ "level": "stable",
26
+ "reason": "Primary package surface is described as implemented and production-oriented."
27
+ },
28
+ "keywords": [
29
+ "projects"
30
+ ]
31
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@happyvertical/projects",
3
+ "version": "0.74.8",
4
+ "description": "Standardized project management interface for GitHub Projects, Jira, ZenHub, and Linear",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "bin": {
15
+ "have-projects-context": "./dist/cli/claude-context.js"
16
+ },
17
+ "dependencies": {
18
+ "@happyvertical/graphql": "0.74.8",
19
+ "@happyvertical/repos": "0.74.8"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "25.0.10",
23
+ "typescript": "^5.9.3",
24
+ "vite": "7.3.2",
25
+ "vite-plugin-dts": "4.5.4",
26
+ "vitest": "4.1.5"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE",
32
+ "AGENT.md",
33
+ "metadata.json"
34
+ ],
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org",
37
+ "access": "public"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/happyvertical/sdk.git",
42
+ "directory": "packages/projects"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/happyvertical/sdk/issues"
46
+ },
47
+ "homepage": "https://github.com/happyvertical/sdk/tree/main/packages/projects#readme",
48
+ "license": "MIT",
49
+ "scripts": {
50
+ "test": "npx vitest run --passWithNoTests",
51
+ "test:watch": "npx vitest",
52
+ "build": "vite build",
53
+ "build:watch": "vite build --watch",
54
+ "clean": "rm -rf dist",
55
+ "dev": "npm run build:watch & npm run test:watch"
56
+ }
57
+ }