opencode-sdlc-plugin 1.0.0-alpha.0 → 1.1.1

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/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { tmpdir, homedir, platform } from 'os';
2
2
  import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, statSync, truncateSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { Octokit } from '@octokit/rest';
4
6
  import { minimatch } from 'minimatch';
5
7
  import { z } from 'zod';
6
8
  import { readFile, mkdir, writeFile, readdir } from 'fs/promises';
@@ -173,14 +175,692 @@ function handleSessionError(event, tracker) {
173
175
  });
174
176
  }
175
177
  }
178
+ function resolveGitHubToken(explicitToken) {
179
+ if (explicitToken) {
180
+ return explicitToken;
181
+ }
182
+ const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
183
+ if (envToken) {
184
+ return envToken;
185
+ }
186
+ try {
187
+ const ghToken = execSync("gh auth token", {
188
+ encoding: "utf-8",
189
+ timeout: 5e3,
190
+ stdio: ["pipe", "pipe", "pipe"]
191
+ }).trim();
192
+ if (ghToken) {
193
+ return ghToken;
194
+ }
195
+ } catch {
196
+ }
197
+ return null;
198
+ }
199
+ function isGitHubAuthenticated(token) {
200
+ return resolveGitHubToken(token) !== null;
201
+ }
202
+ var GitHubClient = class {
203
+ octokit;
204
+ owner;
205
+ repo;
206
+ constructor(options = {}) {
207
+ const token = resolveGitHubToken(options.token);
208
+ if (!token) {
209
+ throw new Error(
210
+ "GitHub authentication not found. Set GITHUB_TOKEN environment variable or run 'gh auth login'."
211
+ );
212
+ }
213
+ this.octokit = new Octokit({ auth: token });
214
+ this.owner = options.owner;
215
+ this.repo = options.repo;
216
+ }
217
+ /**
218
+ * Set the default owner/repo for operations
219
+ */
220
+ setRepo(owner, repo) {
221
+ this.owner = owner;
222
+ this.repo = repo;
223
+ }
224
+ getRepoParams(owner, repo) {
225
+ const o = owner || this.owner;
226
+ const r = repo || this.repo;
227
+ if (!o || !r) {
228
+ throw new Error("Repository owner and name are required");
229
+ }
230
+ return { owner: o, repo: r };
231
+ }
232
+ // ==========================================================================
233
+ // Issues
234
+ // ==========================================================================
235
+ /**
236
+ * Get a single issue by number
237
+ */
238
+ async getIssue(issueNumber, owner, repo) {
239
+ const params = this.getRepoParams(owner, repo);
240
+ const { data } = await this.octokit.issues.get({
241
+ ...params,
242
+ issue_number: issueNumber
243
+ });
244
+ return {
245
+ number: data.number,
246
+ title: data.title,
247
+ body: data.body ?? null,
248
+ url: data.html_url,
249
+ state: data.state,
250
+ labels: (data.labels || []).map(
251
+ (label) => typeof label === "string" ? { name: label } : { name: label.name || "" }
252
+ )
253
+ };
254
+ }
255
+ /**
256
+ * List issues in a repository
257
+ */
258
+ async listIssues(options = {}, owner, repo) {
259
+ const params = this.getRepoParams(owner, repo);
260
+ const { data } = await this.octokit.issues.listForRepo({
261
+ ...params,
262
+ state: options.state || "open",
263
+ labels: options.labels?.join(","),
264
+ per_page: options.limit || 30
265
+ });
266
+ return data.filter((issue) => !issue.pull_request).map((issue) => ({
267
+ number: issue.number,
268
+ title: issue.title,
269
+ url: issue.html_url,
270
+ state: issue.state
271
+ }));
272
+ }
273
+ /**
274
+ * Update an issue's body
275
+ */
276
+ async updateIssueBody(issueNumber, body, owner, repo) {
277
+ const params = this.getRepoParams(owner, repo);
278
+ await this.octokit.issues.update({
279
+ ...params,
280
+ issue_number: issueNumber,
281
+ body
282
+ });
283
+ }
284
+ /**
285
+ * Add labels to an issue
286
+ */
287
+ async addLabels(issueNumber, labels, owner, repo) {
288
+ const params = this.getRepoParams(owner, repo);
289
+ await this.octokit.issues.addLabels({
290
+ ...params,
291
+ issue_number: issueNumber,
292
+ labels
293
+ });
294
+ }
295
+ // ==========================================================================
296
+ // Pull Requests
297
+ // ==========================================================================
298
+ /**
299
+ * Create a pull request
300
+ */
301
+ async createPullRequest(options, owner, repo) {
302
+ const params = this.getRepoParams(owner, repo);
303
+ const { data } = await this.octokit.pulls.create({
304
+ ...params,
305
+ title: options.title,
306
+ body: options.body,
307
+ head: options.head,
308
+ base: options.base || "main",
309
+ draft: options.draft
310
+ });
311
+ return {
312
+ number: data.number,
313
+ url: data.html_url
314
+ };
315
+ }
316
+ /**
317
+ * Get pull request details
318
+ */
319
+ async getPullRequest(prNumber, owner, repo) {
320
+ const params = this.getRepoParams(owner, repo);
321
+ const { data } = await this.octokit.pulls.get({
322
+ ...params,
323
+ pull_number: prNumber
324
+ });
325
+ return {
326
+ number: data.number,
327
+ title: data.title,
328
+ body: data.body,
329
+ url: data.html_url,
330
+ state: data.merged ? "merged" : data.state,
331
+ mergeable: data.mergeable,
332
+ mergeableState: data.mergeable_state,
333
+ draft: data.draft || false
334
+ };
335
+ }
336
+ /**
337
+ * Get pull request status including checks and reviews
338
+ */
339
+ async getPullRequestStatus(prNumber, owner, repo) {
340
+ const params = this.getRepoParams(owner, repo);
341
+ const pr = await this.getPullRequest(prNumber, owner, repo);
342
+ const { data: prData } = await this.octokit.pulls.get({
343
+ ...params,
344
+ pull_number: prNumber
345
+ });
346
+ let checks = [];
347
+ try {
348
+ const { data: checksData } = await this.octokit.checks.listForRef({
349
+ ...params,
350
+ ref: prData.head.sha
351
+ });
352
+ checks = checksData.check_runs.map((check) => ({
353
+ name: check.name,
354
+ status: check.status === "completed" ? check.conclusion === "success" ? "pass" : check.conclusion === "skipped" ? "skipped" : "fail" : "pending",
355
+ conclusion: check.conclusion
356
+ }));
357
+ } catch {
358
+ }
359
+ const { data: reviewsData } = await this.octokit.pulls.listReviews({
360
+ ...params,
361
+ pull_number: prNumber
362
+ });
363
+ const reviews = reviewsData.map((review) => ({
364
+ reviewer: review.user?.login || "unknown",
365
+ state: review.state
366
+ }));
367
+ return {
368
+ number: pr.number,
369
+ url: pr.url,
370
+ state: pr.state,
371
+ mergeable: pr.mergeable,
372
+ checks,
373
+ reviews
374
+ };
375
+ }
376
+ /**
377
+ * Get pull request diff
378
+ */
379
+ async getPullRequestDiff(prNumber, owner, repo) {
380
+ const params = this.getRepoParams(owner, repo);
381
+ const { data } = await this.octokit.pulls.get({
382
+ ...params,
383
+ pull_number: prNumber,
384
+ mediaType: { format: "diff" }
385
+ });
386
+ return data;
387
+ }
388
+ /**
389
+ * Merge a pull request
390
+ */
391
+ async mergePullRequest(options, owner, repo) {
392
+ const params = this.getRepoParams(owner, repo);
393
+ const { data } = await this.octokit.pulls.merge({
394
+ ...params,
395
+ pull_number: options.prNumber,
396
+ merge_method: options.mergeMethod || "squash",
397
+ commit_title: options.commitTitle,
398
+ commit_message: options.commitMessage
399
+ });
400
+ return {
401
+ sha: data.sha,
402
+ merged: data.merged
403
+ };
404
+ }
405
+ // ==========================================================================
406
+ // Repositories
407
+ // ==========================================================================
408
+ /**
409
+ * Create a new repository (personal or in an organization)
410
+ */
411
+ async createRepository(options) {
412
+ let data;
413
+ if (options.org) {
414
+ const response = await this.octokit.repos.createInOrg({
415
+ org: options.org,
416
+ name: options.name,
417
+ description: options.description,
418
+ private: options.private ?? false,
419
+ auto_init: options.autoInit ?? false
420
+ });
421
+ data = response.data;
422
+ } else {
423
+ const response = await this.octokit.repos.createForAuthenticatedUser({
424
+ name: options.name,
425
+ description: options.description,
426
+ private: options.private ?? false,
427
+ auto_init: options.autoInit ?? false
428
+ });
429
+ data = response.data;
430
+ }
431
+ return {
432
+ id: data.id,
433
+ name: data.name,
434
+ fullName: data.full_name,
435
+ url: data.html_url,
436
+ private: data.private,
437
+ defaultBranch: data.default_branch || "main"
438
+ };
439
+ }
440
+ /**
441
+ * List organizations the authenticated user belongs to
442
+ */
443
+ async listOrganizations() {
444
+ const query = `
445
+ query {
446
+ viewer {
447
+ organizations(first: 100) {
448
+ nodes {
449
+ id
450
+ databaseId
451
+ login
452
+ name
453
+ }
454
+ }
455
+ }
456
+ }
457
+ `;
458
+ const result = await this.octokit.graphql(query);
459
+ return result.viewer.organizations.nodes.map((org) => ({
460
+ id: String(org.databaseId),
461
+ nodeId: org.id,
462
+ login: org.login,
463
+ name: org.name
464
+ }));
465
+ }
466
+ /**
467
+ * Get repository details
468
+ */
469
+ async getRepository(owner, repo) {
470
+ const params = this.getRepoParams(owner, repo);
471
+ const { data } = await this.octokit.repos.get(params);
472
+ return {
473
+ id: data.id,
474
+ name: data.name,
475
+ fullName: data.full_name,
476
+ url: data.html_url,
477
+ private: data.private,
478
+ defaultBranch: data.default_branch || "main"
479
+ };
480
+ }
481
+ /**
482
+ * List repositories for the authenticated user
483
+ */
484
+ async listUserRepos(options = {}) {
485
+ const { data } = await this.octokit.repos.listForAuthenticatedUser({
486
+ type: options.type || "owner",
487
+ per_page: options.limit || 30,
488
+ sort: "updated"
489
+ });
490
+ return data.map((repo) => ({
491
+ id: repo.id,
492
+ name: repo.name,
493
+ fullName: repo.full_name,
494
+ url: repo.html_url,
495
+ private: repo.private,
496
+ defaultBranch: repo.default_branch || "main"
497
+ }));
498
+ }
499
+ // ==========================================================================
500
+ // Projects (GraphQL required for Projects V2)
501
+ // ==========================================================================
502
+ /**
503
+ * List projects for the authenticated user
504
+ * Note: Projects V2 requires GraphQL API
505
+ */
506
+ async listUserProjects(limit = 20) {
507
+ const query = `
508
+ query($first: Int!) {
509
+ viewer {
510
+ projectsV2(first: $first) {
511
+ nodes {
512
+ id
513
+ number
514
+ title
515
+ url
516
+ }
517
+ }
518
+ }
519
+ }
520
+ `;
521
+ const result = await this.octokit.graphql(query, { first: limit });
522
+ return result.viewer.projectsV2.nodes;
523
+ }
524
+ /**
525
+ * Get project by number for a user
526
+ */
527
+ async getUserProject(userLogin, projectNumber) {
528
+ const query = `
529
+ query($login: String!, $number: Int!) {
530
+ user(login: $login) {
531
+ projectV2(number: $number) {
532
+ id
533
+ number
534
+ title
535
+ url
536
+ }
537
+ }
538
+ }
539
+ `;
540
+ try {
541
+ const result = await this.octokit.graphql(query, { login: userLogin, number: projectNumber });
542
+ return result.user.projectV2;
543
+ } catch {
544
+ return null;
545
+ }
546
+ }
547
+ /**
548
+ * Get project by number for an organization
549
+ */
550
+ async getOrgProject(orgLogin, projectNumber) {
551
+ const query = `
552
+ query($login: String!, $number: Int!) {
553
+ organization(login: $login) {
554
+ projectV2(number: $number) {
555
+ id
556
+ number
557
+ title
558
+ url
559
+ }
560
+ }
561
+ }
562
+ `;
563
+ try {
564
+ const result = await this.octokit.graphql(query, { login: orgLogin, number: projectNumber });
565
+ return result.organization.projectV2;
566
+ } catch {
567
+ return null;
568
+ }
569
+ }
570
+ /**
571
+ * List projects for an organization
572
+ */
573
+ async listOrgProjects(orgLogin, limit = 20) {
574
+ const query = `
575
+ query($login: String!, $first: Int!) {
576
+ organization(login: $login) {
577
+ projectsV2(first: $first) {
578
+ nodes {
579
+ id
580
+ number
581
+ title
582
+ url
583
+ }
584
+ }
585
+ }
586
+ }
587
+ `;
588
+ try {
589
+ const result = await this.octokit.graphql(query, { login: orgLogin, first: limit });
590
+ return result.organization.projectsV2.nodes;
591
+ } catch {
592
+ return [];
593
+ }
594
+ }
595
+ /**
596
+ * Create a new project
597
+ */
598
+ async createProject(options) {
599
+ let ownerId;
600
+ if (options.isOrg) {
601
+ const query = `
602
+ query($login: String!) {
603
+ organization(login: $login) {
604
+ id
605
+ }
606
+ }
607
+ `;
608
+ const result2 = await this.octokit.graphql(query, { login: options.ownerLogin });
609
+ ownerId = result2.organization.id;
610
+ } else {
611
+ const query = `
612
+ query($login: String!) {
613
+ user(login: $login) {
614
+ id
615
+ }
616
+ }
617
+ `;
618
+ const result2 = await this.octokit.graphql(query, { login: options.ownerLogin });
619
+ ownerId = result2.user.id;
620
+ }
621
+ const mutation = `
622
+ mutation($ownerId: ID!, $title: String!) {
623
+ createProjectV2(input: { ownerId: $ownerId, title: $title }) {
624
+ projectV2 {
625
+ id
626
+ number
627
+ title
628
+ url
629
+ }
630
+ }
631
+ }
632
+ `;
633
+ const result = await this.octokit.graphql(mutation, { ownerId, title: options.title });
634
+ return result.createProjectV2.projectV2;
635
+ }
636
+ /**
637
+ * Copy an existing project
638
+ */
639
+ async copyProject(options) {
640
+ let sourceProject;
641
+ if (options.sourceIsOrg) {
642
+ sourceProject = await this.getOrgProject(
643
+ options.sourceOwnerLogin,
644
+ options.sourceProjectNumber
645
+ );
646
+ } else {
647
+ sourceProject = await this.getUserProject(
648
+ options.sourceOwnerLogin,
649
+ options.sourceProjectNumber
650
+ );
651
+ }
652
+ if (!sourceProject) {
653
+ throw new Error(
654
+ `Source project #${options.sourceProjectNumber} not found for ${options.sourceOwnerLogin}`
655
+ );
656
+ }
657
+ let targetOwnerId;
658
+ if (options.targetIsOrg) {
659
+ const query = `
660
+ query($login: String!) {
661
+ organization(login: $login) {
662
+ id
663
+ }
664
+ }
665
+ `;
666
+ const result2 = await this.octokit.graphql(query, { login: options.targetOwnerLogin });
667
+ targetOwnerId = result2.organization.id;
668
+ } else {
669
+ const query = `
670
+ query($login: String!) {
671
+ user(login: $login) {
672
+ id
673
+ }
674
+ }
675
+ `;
676
+ const result2 = await this.octokit.graphql(query, { login: options.targetOwnerLogin });
677
+ targetOwnerId = result2.user.id;
678
+ }
679
+ const mutation = `
680
+ mutation($projectId: ID!, $ownerId: ID!, $title: String!, $includeDraftIssues: Boolean) {
681
+ copyProjectV2(input: {
682
+ projectId: $projectId,
683
+ ownerId: $ownerId,
684
+ title: $title,
685
+ includeDraftIssues: $includeDraftIssues
686
+ }) {
687
+ projectV2 {
688
+ id
689
+ number
690
+ title
691
+ url
692
+ }
693
+ }
694
+ }
695
+ `;
696
+ const result = await this.octokit.graphql(mutation, {
697
+ projectId: sourceProject.id,
698
+ ownerId: targetOwnerId,
699
+ title: options.title,
700
+ includeDraftIssues: options.includeDraftIssues ?? false
701
+ });
702
+ return result.copyProjectV2.projectV2;
703
+ }
704
+ /**
705
+ * Link a repository to a project
706
+ * Note: This creates a linked repository in the project
707
+ */
708
+ async linkRepoToProject(projectId, repoOwner, repoName) {
709
+ const { data: repoData } = await this.octokit.repos.get({
710
+ owner: repoOwner,
711
+ repo: repoName
712
+ });
713
+ const mutation = `
714
+ mutation($projectId: ID!, $repositoryId: ID!) {
715
+ linkProjectV2ToRepository(input: { projectId: $projectId, repositoryId: $repositoryId }) {
716
+ repository {
717
+ id
718
+ }
719
+ }
720
+ }
721
+ `;
722
+ await this.octokit.graphql(mutation, {
723
+ projectId,
724
+ repositoryId: repoData.node_id
725
+ });
726
+ }
727
+ /**
728
+ * Add an issue to a project
729
+ */
730
+ async addIssueToProject(projectId, issueNodeId) {
731
+ const mutation = `
732
+ mutation($projectId: ID!, $contentId: ID!) {
733
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
734
+ item {
735
+ id
736
+ }
737
+ }
738
+ }
739
+ `;
740
+ const result = await this.octokit.graphql(mutation, {
741
+ projectId,
742
+ contentId: issueNodeId
743
+ });
744
+ return result.addProjectV2ItemById.item.id;
745
+ }
746
+ // ==========================================================================
747
+ // Branch Protection / Rulesets
748
+ // ==========================================================================
749
+ /**
750
+ * Create a branch ruleset with common protection rules
751
+ */
752
+ async createBranchRuleset(options, owner, repo) {
753
+ const params = this.getRepoParams(owner, repo);
754
+ const rules = [];
755
+ if (options.requirePullRequest) {
756
+ rules.push({
757
+ type: "pull_request",
758
+ parameters: {
759
+ dismiss_stale_reviews_on_push: options.dismissStaleReviews ?? false,
760
+ require_code_owner_review: options.requireCodeOwnerReview ?? false,
761
+ require_last_push_approval: false,
762
+ required_approving_review_count: options.requiredApprovals ?? 1,
763
+ required_review_thread_resolution: false
764
+ }
765
+ });
766
+ }
767
+ if (options.requireSignedCommits) {
768
+ rules.push({ type: "required_signatures" });
769
+ }
770
+ if (options.requireLinearHistory) {
771
+ rules.push({ type: "required_linear_history" });
772
+ }
773
+ if (options.preventDeletion) {
774
+ rules.push({ type: "deletion" });
775
+ }
776
+ if (options.preventForcePush) {
777
+ rules.push({ type: "non_fast_forward" });
778
+ }
779
+ const { data } = await this.octokit.repos.createRepoRuleset({
780
+ ...params,
781
+ name: options.name,
782
+ enforcement: options.enforcement,
783
+ conditions: {
784
+ ref_name: {
785
+ include: options.targetBranches,
786
+ exclude: []
787
+ }
788
+ },
789
+ rules
790
+ });
791
+ return { id: data.id };
792
+ }
793
+ // ==========================================================================
794
+ // Utility Methods
795
+ // ==========================================================================
796
+ /**
797
+ * Get the authenticated user's login
798
+ */
799
+ async getAuthenticatedUser() {
800
+ const { data } = await this.octokit.users.getAuthenticated();
801
+ return { login: data.login, id: data.id };
802
+ }
803
+ /**
804
+ * Check if a repository exists
805
+ */
806
+ async repoExists(owner, repo) {
807
+ try {
808
+ await this.octokit.repos.get({ owner, repo });
809
+ return true;
810
+ } catch {
811
+ return false;
812
+ }
813
+ }
814
+ };
815
+ function createGitHubClientIfAuthenticated(options = {}) {
816
+ if (!isGitHubAuthenticated(options.token)) {
817
+ return null;
818
+ }
819
+ try {
820
+ return new GitHubClient(options);
821
+ } catch {
822
+ return null;
823
+ }
824
+ }
176
825
 
177
826
  // src/plugin/utils/github-issues.ts
178
827
  var log3 = createPluginLogger("github-issues");
828
+ var cachedClient = null;
829
+ var cachedClientRepo = null;
830
+ function getClient(config) {
831
+ if (!config.github?.owner || !config.github?.repo) return null;
832
+ const repoKey = `${config.github.owner}/${config.github.repo}`;
833
+ if (cachedClient && cachedClientRepo === repoKey) {
834
+ return cachedClient;
835
+ }
836
+ cachedClient = createGitHubClientIfAuthenticated({
837
+ owner: config.github.owner,
838
+ repo: config.github.repo
839
+ });
840
+ cachedClientRepo = cachedClient ? repoKey : null;
841
+ return cachedClient;
842
+ }
179
843
  function getRepo(config) {
180
844
  if (!config.github?.owner || !config.github?.repo) return null;
181
845
  return { owner: config.github.owner, repo: config.github.repo };
182
846
  }
183
847
  async function fetchIssue(ctx, config, issueNumber) {
848
+ const client = getClient(config);
849
+ if (client) {
850
+ try {
851
+ const issue = await client.getIssue(issueNumber);
852
+ return {
853
+ number: issue.number,
854
+ title: issue.title,
855
+ body: issue.body,
856
+ url: issue.url,
857
+ state: issue.state,
858
+ labels: issue.labels
859
+ };
860
+ } catch (error) {
861
+ log3.warn("Failed to fetch issue via Octokit", { issueNumber, error: String(error) });
862
+ }
863
+ }
184
864
  const repo = getRepo(config);
185
865
  if (!repo) return null;
186
866
  try {
@@ -194,6 +874,20 @@ async function fetchIssue(ctx, config, issueNumber) {
194
874
  }
195
875
  }
196
876
  async function listIssues(ctx, config, status, limit = 20) {
877
+ const client = getClient(config);
878
+ if (client && !status) {
879
+ try {
880
+ const issues = await client.listIssues({ limit });
881
+ return issues.map((issue) => ({
882
+ number: issue.number,
883
+ title: issue.title,
884
+ url: issue.url,
885
+ state: issue.state
886
+ }));
887
+ } catch (error) {
888
+ log3.warn("Failed to list issues via Octokit", { error: String(error) });
889
+ }
890
+ }
197
891
  const repo = getRepo(config);
198
892
  if (!repo) return [];
199
893
  try {
@@ -219,6 +913,15 @@ async function moveIssueToStatus(ctx, config, issueNumber, status) {
219
913
  }
220
914
  }
221
915
  async function updateIssueBody(ctx, config, issueNumber, body) {
916
+ const client = getClient(config);
917
+ if (client) {
918
+ try {
919
+ await client.updateIssueBody(issueNumber, body);
920
+ return true;
921
+ } catch (error) {
922
+ log3.warn("Failed to update issue body via Octokit", { issueNumber, error: String(error) });
923
+ }
924
+ }
222
925
  const repo = getRepo(config);
223
926
  if (!repo) return false;
224
927
  try {