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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -81,12 +81,12 @@ interface SubagentModelsConfig {
81
81
  * Options passed to the install command
82
82
  */
83
83
  interface InstallOptions {
84
- preset: string;
85
84
  yes: boolean;
86
85
  advanced: boolean;
87
86
  global: boolean;
88
87
  local: boolean;
89
88
  reconfigure?: boolean;
89
+ listProfiles?: boolean;
90
90
  }
91
91
  /**
92
92
  * Authentication method for a provider
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,496 @@ 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
410
+ */
411
+ async createRepository(options) {
412
+ const { data } = await this.octokit.repos.createForAuthenticatedUser({
413
+ name: options.name,
414
+ description: options.description,
415
+ private: options.private ?? false,
416
+ auto_init: options.autoInit ?? false
417
+ });
418
+ return {
419
+ id: data.id,
420
+ name: data.name,
421
+ fullName: data.full_name,
422
+ url: data.html_url,
423
+ private: data.private,
424
+ defaultBranch: data.default_branch || "main"
425
+ };
426
+ }
427
+ /**
428
+ * Get repository details
429
+ */
430
+ async getRepository(owner, repo) {
431
+ const params = this.getRepoParams(owner, repo);
432
+ const { data } = await this.octokit.repos.get(params);
433
+ return {
434
+ id: data.id,
435
+ name: data.name,
436
+ fullName: data.full_name,
437
+ url: data.html_url,
438
+ private: data.private,
439
+ defaultBranch: data.default_branch || "main"
440
+ };
441
+ }
442
+ /**
443
+ * List repositories for the authenticated user
444
+ */
445
+ async listUserRepos(options = {}) {
446
+ const { data } = await this.octokit.repos.listForAuthenticatedUser({
447
+ type: options.type || "owner",
448
+ per_page: options.limit || 30,
449
+ sort: "updated"
450
+ });
451
+ return data.map((repo) => ({
452
+ id: repo.id,
453
+ name: repo.name,
454
+ fullName: repo.full_name,
455
+ url: repo.html_url,
456
+ private: repo.private,
457
+ defaultBranch: repo.default_branch || "main"
458
+ }));
459
+ }
460
+ // ==========================================================================
461
+ // Projects (GraphQL required for Projects V2)
462
+ // ==========================================================================
463
+ /**
464
+ * List projects for the authenticated user
465
+ * Note: Projects V2 requires GraphQL API
466
+ */
467
+ async listUserProjects(limit = 20) {
468
+ const query = `
469
+ query($first: Int!) {
470
+ viewer {
471
+ projectsV2(first: $first) {
472
+ nodes {
473
+ id
474
+ number
475
+ title
476
+ url
477
+ }
478
+ }
479
+ }
480
+ }
481
+ `;
482
+ const result = await this.octokit.graphql(query, { first: limit });
483
+ return result.viewer.projectsV2.nodes;
484
+ }
485
+ /**
486
+ * Get project by number for a user
487
+ */
488
+ async getUserProject(userLogin, projectNumber) {
489
+ const query = `
490
+ query($login: String!, $number: Int!) {
491
+ user(login: $login) {
492
+ projectV2(number: $number) {
493
+ id
494
+ number
495
+ title
496
+ url
497
+ }
498
+ }
499
+ }
500
+ `;
501
+ try {
502
+ const result = await this.octokit.graphql(query, { login: userLogin, number: projectNumber });
503
+ return result.user.projectV2;
504
+ } catch {
505
+ return null;
506
+ }
507
+ }
508
+ /**
509
+ * Link a repository to a project
510
+ * Note: This creates a linked repository in the project
511
+ */
512
+ async linkRepoToProject(projectId, repoOwner, repoName) {
513
+ const { data: repoData } = await this.octokit.repos.get({
514
+ owner: repoOwner,
515
+ repo: repoName
516
+ });
517
+ const mutation = `
518
+ mutation($projectId: ID!, $repositoryId: ID!) {
519
+ linkProjectV2ToRepository(input: { projectId: $projectId, repositoryId: $repositoryId }) {
520
+ repository {
521
+ id
522
+ }
523
+ }
524
+ }
525
+ `;
526
+ await this.octokit.graphql(mutation, {
527
+ projectId,
528
+ repositoryId: repoData.node_id
529
+ });
530
+ }
531
+ /**
532
+ * Add an issue to a project
533
+ */
534
+ async addIssueToProject(projectId, issueNodeId) {
535
+ const mutation = `
536
+ mutation($projectId: ID!, $contentId: ID!) {
537
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
538
+ item {
539
+ id
540
+ }
541
+ }
542
+ }
543
+ `;
544
+ const result = await this.octokit.graphql(mutation, {
545
+ projectId,
546
+ contentId: issueNodeId
547
+ });
548
+ return result.addProjectV2ItemById.item.id;
549
+ }
550
+ // ==========================================================================
551
+ // Branch Protection / Rulesets
552
+ // ==========================================================================
553
+ /**
554
+ * Create a branch ruleset with common protection rules
555
+ */
556
+ async createBranchRuleset(options, owner, repo) {
557
+ const params = this.getRepoParams(owner, repo);
558
+ const rules = [];
559
+ if (options.requirePullRequest) {
560
+ rules.push({
561
+ type: "pull_request",
562
+ parameters: {
563
+ dismiss_stale_reviews_on_push: options.dismissStaleReviews ?? false,
564
+ require_code_owner_review: options.requireCodeOwnerReview ?? false,
565
+ require_last_push_approval: false,
566
+ required_approving_review_count: options.requiredApprovals ?? 1,
567
+ required_review_thread_resolution: false
568
+ }
569
+ });
570
+ }
571
+ if (options.requireSignedCommits) {
572
+ rules.push({ type: "required_signatures" });
573
+ }
574
+ if (options.requireLinearHistory) {
575
+ rules.push({ type: "required_linear_history" });
576
+ }
577
+ if (options.preventDeletion) {
578
+ rules.push({ type: "deletion" });
579
+ }
580
+ if (options.preventForcePush) {
581
+ rules.push({ type: "non_fast_forward" });
582
+ }
583
+ const { data } = await this.octokit.repos.createRepoRuleset({
584
+ ...params,
585
+ name: options.name,
586
+ enforcement: options.enforcement,
587
+ conditions: {
588
+ ref_name: {
589
+ include: options.targetBranches,
590
+ exclude: []
591
+ }
592
+ },
593
+ rules
594
+ });
595
+ return { id: data.id };
596
+ }
597
+ // ==========================================================================
598
+ // Utility Methods
599
+ // ==========================================================================
600
+ /**
601
+ * Get the authenticated user's login
602
+ */
603
+ async getAuthenticatedUser() {
604
+ const { data } = await this.octokit.users.getAuthenticated();
605
+ return { login: data.login, id: data.id };
606
+ }
607
+ /**
608
+ * Check if a repository exists
609
+ */
610
+ async repoExists(owner, repo) {
611
+ try {
612
+ await this.octokit.repos.get({ owner, repo });
613
+ return true;
614
+ } catch {
615
+ return false;
616
+ }
617
+ }
618
+ };
619
+ function createGitHubClientIfAuthenticated(options = {}) {
620
+ if (!isGitHubAuthenticated(options.token)) {
621
+ return null;
622
+ }
623
+ try {
624
+ return new GitHubClient(options);
625
+ } catch {
626
+ return null;
627
+ }
628
+ }
176
629
 
177
630
  // src/plugin/utils/github-issues.ts
178
631
  var log3 = createPluginLogger("github-issues");
632
+ var cachedClient = null;
633
+ var cachedClientRepo = null;
634
+ function getClient(config) {
635
+ if (!config.github?.owner || !config.github?.repo) return null;
636
+ const repoKey = `${config.github.owner}/${config.github.repo}`;
637
+ if (cachedClient && cachedClientRepo === repoKey) {
638
+ return cachedClient;
639
+ }
640
+ cachedClient = createGitHubClientIfAuthenticated({
641
+ owner: config.github.owner,
642
+ repo: config.github.repo
643
+ });
644
+ cachedClientRepo = cachedClient ? repoKey : null;
645
+ return cachedClient;
646
+ }
179
647
  function getRepo(config) {
180
648
  if (!config.github?.owner || !config.github?.repo) return null;
181
649
  return { owner: config.github.owner, repo: config.github.repo };
182
650
  }
183
651
  async function fetchIssue(ctx, config, issueNumber) {
652
+ const client = getClient(config);
653
+ if (client) {
654
+ try {
655
+ const issue = await client.getIssue(issueNumber);
656
+ return {
657
+ number: issue.number,
658
+ title: issue.title,
659
+ body: issue.body,
660
+ url: issue.url,
661
+ state: issue.state,
662
+ labels: issue.labels
663
+ };
664
+ } catch (error) {
665
+ log3.warn("Failed to fetch issue via Octokit", { issueNumber, error: String(error) });
666
+ }
667
+ }
184
668
  const repo = getRepo(config);
185
669
  if (!repo) return null;
186
670
  try {
@@ -194,6 +678,20 @@ async function fetchIssue(ctx, config, issueNumber) {
194
678
  }
195
679
  }
196
680
  async function listIssues(ctx, config, status, limit = 20) {
681
+ const client = getClient(config);
682
+ if (client && !status) {
683
+ try {
684
+ const issues = await client.listIssues({ limit });
685
+ return issues.map((issue) => ({
686
+ number: issue.number,
687
+ title: issue.title,
688
+ url: issue.url,
689
+ state: issue.state
690
+ }));
691
+ } catch (error) {
692
+ log3.warn("Failed to list issues via Octokit", { error: String(error) });
693
+ }
694
+ }
197
695
  const repo = getRepo(config);
198
696
  if (!repo) return [];
199
697
  try {
@@ -219,6 +717,15 @@ async function moveIssueToStatus(ctx, config, issueNumber, status) {
219
717
  }
220
718
  }
221
719
  async function updateIssueBody(ctx, config, issueNumber, body) {
720
+ const client = getClient(config);
721
+ if (client) {
722
+ try {
723
+ await client.updateIssueBody(issueNumber, body);
724
+ return true;
725
+ } catch (error) {
726
+ log3.warn("Failed to update issue body via Octokit", { issueNumber, error: String(error) });
727
+ }
728
+ }
222
729
  const repo = getRepo(config);
223
730
  if (!repo) return false;
224
731
  try {