@bretwardjames/ghp-core 0.1.1 → 0.1.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/dist/index.cjs CHANGED
@@ -30,7 +30,6 @@ __export(index_exports, {
30
30
  buildRepoUrl: () => buildRepoUrl,
31
31
  checkoutBranch: () => checkoutBranch,
32
32
  createBranch: () => createBranch,
33
- createInMemoryAdapter: () => createInMemoryAdapter,
34
33
  detectRepository: () => detectRepository,
35
34
  fetchOrigin: () => fetchOrigin,
36
35
  generateBranchName: () => generateBranchName,
@@ -41,11 +40,14 @@ __export(index_exports, {
41
40
  getRepositoryRoot: () => getRepositoryRoot,
42
41
  hasUncommittedChanges: () => hasUncommittedChanges,
43
42
  isGitRepository: () => isGitRepository,
43
+ parseBranchLink: () => parseBranchLink,
44
44
  parseGitHubUrl: () => parseGitHubUrl,
45
45
  parseIssueUrl: () => parseIssueUrl,
46
46
  pullLatest: () => pullLatest,
47
47
  queries: () => queries_exports,
48
- sanitizeForBranchName: () => sanitizeForBranchName
48
+ removeBranchLinkFromBody: () => removeBranchLinkFromBody,
49
+ sanitizeForBranchName: () => sanitizeForBranchName,
50
+ setBranchLinkInBody: () => setBranchLinkInBody
49
51
  });
50
52
  module.exports = __toCommonJS(index_exports);
51
53
 
@@ -75,6 +77,7 @@ __export(queries_exports, {
75
77
  REPOSITORY_ID_QUERY: () => REPOSITORY_ID_QUERY,
76
78
  REPOSITORY_PROJECTS_QUERY: () => REPOSITORY_PROJECTS_QUERY,
77
79
  UPDATE_ISSUE_BODY_MUTATION: () => UPDATE_ISSUE_BODY_MUTATION,
80
+ UPDATE_ISSUE_MUTATION: () => UPDATE_ISSUE_MUTATION,
78
81
  UPDATE_ISSUE_TYPE_MUTATION: () => UPDATE_ISSUE_TYPE_MUTATION,
79
82
  UPDATE_ITEM_FIELD_MUTATION: () => UPDATE_ITEM_FIELD_MUTATION,
80
83
  UPDATE_ITEM_STATUS_MUTATION: () => UPDATE_ITEM_STATUS_MUTATION,
@@ -430,6 +433,15 @@ var UPDATE_ISSUE_BODY_MUTATION = `
430
433
  }
431
434
  }
432
435
  `;
436
+ var UPDATE_ISSUE_MUTATION = `
437
+ mutation($issueId: ID!, $title: String, $body: String) {
438
+ updateIssue(input: { id: $issueId, title: $title, body: $body }) {
439
+ issue {
440
+ id
441
+ }
442
+ }
443
+ }
444
+ `;
433
445
 
434
446
  // src/github-api.ts
435
447
  function createAuthError(message, type, details) {
@@ -968,141 +980,94 @@ var GitHubAPI = class {
968
980
  return false;
969
981
  }
970
982
  }
983
+ /**
984
+ * Update an issue's title and/or body
985
+ */
986
+ async updateIssue(repo, issueNumber, updates) {
987
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
988
+ try {
989
+ const issueResponse = await this.graphqlWithAuth(ISSUE_FOR_UPDATE_QUERY, {
990
+ owner: repo.owner,
991
+ name: repo.name,
992
+ number: issueNumber
993
+ });
994
+ if (!issueResponse.repository.issue) {
995
+ return false;
996
+ }
997
+ await this.graphqlWithAuth(UPDATE_ISSUE_MUTATION, {
998
+ issueId: issueResponse.repository.issue.id,
999
+ title: updates.title,
1000
+ body: updates.body
1001
+ });
1002
+ return true;
1003
+ } catch {
1004
+ return false;
1005
+ }
1006
+ }
971
1007
  };
972
1008
 
973
1009
  // src/branch-linker.ts
974
- var BranchLinker = class {
975
- storage;
976
- constructor(storage) {
977
- this.storage = storage;
978
- }
979
- /**
980
- * Load links from storage (handles both sync and async adapters)
981
- */
982
- async loadLinks() {
983
- const result = this.storage.load();
984
- return result instanceof Promise ? await result : result;
1010
+ var BRANCH_LINK_PATTERN = /<!--\s*ghp-branch:\s*(.+?)\s*-->/;
1011
+ function parseBranchLink(body) {
1012
+ if (!body) return null;
1013
+ const match = body.match(BRANCH_LINK_PATTERN);
1014
+ return match ? match[1].trim() : null;
1015
+ }
1016
+ function setBranchLinkInBody(body, branch) {
1017
+ const currentBody = body || "";
1018
+ const tag = `<!-- ghp-branch: ${branch} -->`;
1019
+ if (BRANCH_LINK_PATTERN.test(currentBody)) {
1020
+ return currentBody.replace(BRANCH_LINK_PATTERN, tag);
1021
+ } else {
1022
+ return currentBody.trim() + "\n\n" + tag;
985
1023
  }
986
- /**
987
- * Save links to storage (handles both sync and async adapters)
988
- */
989
- async saveLinks(links) {
990
- const result = this.storage.save(links);
991
- if (result instanceof Promise) {
992
- await result;
993
- }
1024
+ }
1025
+ function removeBranchLinkFromBody(body) {
1026
+ if (!body) return "";
1027
+ return body.replace(BRANCH_LINK_PATTERN, "").trim();
1028
+ }
1029
+ var BranchLinker = class {
1030
+ api;
1031
+ constructor(api) {
1032
+ this.api = api;
994
1033
  }
995
1034
  /**
996
1035
  * Create a link between a branch and an issue.
997
- * If a link already exists for this branch or issue in this repo, it will be replaced.
1036
+ * Stores the link as a hidden comment in the issue body.
998
1037
  */
999
- async link(branch, issueNumber, issueTitle, itemId, repo) {
1000
- const links = await this.loadLinks();
1001
- const filtered = links.filter(
1002
- (l) => !(l.repo === repo && (l.branch === branch || l.issueNumber === issueNumber))
1003
- );
1004
- filtered.push({
1005
- branch,
1006
- issueNumber,
1007
- issueTitle,
1008
- itemId,
1009
- repo,
1010
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1011
- });
1012
- await this.saveLinks(filtered);
1038
+ async link(repo, issueNumber, branch) {
1039
+ const details = await this.api.getIssueDetails(repo, issueNumber);
1040
+ if (!details) return false;
1041
+ const newBody = setBranchLinkInBody(details.body, branch);
1042
+ return this.api.updateIssueBody(repo, issueNumber, newBody);
1013
1043
  }
1014
1044
  /**
1015
- * Remove the link for an issue.
1016
- * @returns true if a link was removed, false if no link existed
1045
+ * Remove the branch link from an issue.
1017
1046
  */
1018
1047
  async unlink(repo, issueNumber) {
1019
- const links = await this.loadLinks();
1020
- const filtered = links.filter(
1021
- (l) => !(l.repo === repo && l.issueNumber === issueNumber)
1022
- );
1023
- if (filtered.length === links.length) {
1024
- return false;
1025
- }
1026
- await this.saveLinks(filtered);
1027
- return true;
1028
- }
1029
- /**
1030
- * Remove the link for a branch.
1031
- * @returns true if a link was removed, false if no link existed
1032
- */
1033
- async unlinkBranch(repo, branch) {
1034
- const links = await this.loadLinks();
1035
- const filtered = links.filter(
1036
- (l) => !(l.repo === repo && l.branch === branch)
1037
- );
1038
- if (filtered.length === links.length) {
1039
- return false;
1040
- }
1041
- await this.saveLinks(filtered);
1042
- return true;
1048
+ const details = await this.api.getIssueDetails(repo, issueNumber);
1049
+ if (!details) return false;
1050
+ const currentBranch = parseBranchLink(details.body);
1051
+ if (!currentBranch) return false;
1052
+ const newBody = removeBranchLinkFromBody(details.body);
1053
+ return this.api.updateIssueBody(repo, issueNumber, newBody);
1043
1054
  }
1044
1055
  /**
1045
1056
  * Get the branch linked to an issue.
1046
1057
  */
1047
1058
  async getBranchForIssue(repo, issueNumber) {
1048
- const links = await this.loadLinks();
1049
- const link = links.find((l) => l.repo === repo && l.issueNumber === issueNumber);
1050
- return link?.branch || null;
1059
+ const details = await this.api.getIssueDetails(repo, issueNumber);
1060
+ if (!details) return null;
1061
+ return parseBranchLink(details.body);
1051
1062
  }
1052
1063
  /**
1053
- * Get the full link info for a branch.
1064
+ * Check if an issue has a branch link.
1054
1065
  */
1055
- async getLinkForBranch(repo, branch) {
1056
- const links = await this.loadLinks();
1057
- return links.find((l) => l.repo === repo && l.branch === branch) || null;
1058
- }
1059
- /**
1060
- * Get the full link info for an issue.
1061
- */
1062
- async getLinkForIssue(repo, issueNumber) {
1063
- const links = await this.loadLinks();
1064
- return links.find((l) => l.repo === repo && l.issueNumber === issueNumber) || null;
1065
- }
1066
- /**
1067
- * Get all links for a repository.
1068
- */
1069
- async getLinksForRepo(repo) {
1070
- const links = await this.loadLinks();
1071
- return links.filter((l) => l.repo === repo);
1072
- }
1073
- /**
1074
- * Get all links.
1075
- */
1076
- async getAllLinks() {
1077
- return this.loadLinks();
1078
- }
1079
- /**
1080
- * Check if a branch has a link.
1081
- */
1082
- async hasLinkForBranch(repo, branch) {
1083
- const link = await this.getLinkForBranch(repo, branch);
1084
- return link !== null;
1085
- }
1086
- /**
1087
- * Check if an issue has a link.
1088
- */
1089
- async hasLinkForIssue(repo, issueNumber) {
1090
- const link = await this.getLinkForIssue(repo, issueNumber);
1091
- return link !== null;
1066
+ async hasLink(repo, issueNumber) {
1067
+ const branch = await this.getBranchForIssue(repo, issueNumber);
1068
+ return branch !== null;
1092
1069
  }
1093
1070
  };
1094
- function createInMemoryAdapter() {
1095
- const adapter = {
1096
- links: [],
1097
- load() {
1098
- return [...this.links];
1099
- },
1100
- save(links) {
1101
- this.links = [...links];
1102
- }
1103
- };
1104
- return adapter;
1105
- }
1106
1071
 
1107
1072
  // src/git-utils.ts
1108
1073
  var import_child_process = require("child_process");
@@ -1290,7 +1255,6 @@ async function getDefaultBranch(options = {}) {
1290
1255
  buildRepoUrl,
1291
1256
  checkoutBranch,
1292
1257
  createBranch,
1293
- createInMemoryAdapter,
1294
1258
  detectRepository,
1295
1259
  fetchOrigin,
1296
1260
  generateBranchName,
@@ -1301,9 +1265,12 @@ async function getDefaultBranch(options = {}) {
1301
1265
  getRepositoryRoot,
1302
1266
  hasUncommittedChanges,
1303
1267
  isGitRepository,
1268
+ parseBranchLink,
1304
1269
  parseGitHubUrl,
1305
1270
  parseIssueUrl,
1306
1271
  pullLatest,
1307
1272
  queries,
1308
- sanitizeForBranchName
1273
+ removeBranchLinkFromBody,
1274
+ sanitizeForBranchName,
1275
+ setBranchLinkInBody
1309
1276
  });
package/dist/index.d.cts CHANGED
@@ -29,25 +29,6 @@ interface AuthError extends Error {
29
29
  requiredScopes?: string[];
30
30
  ssoUrl?: string;
31
31
  }
32
- /**
33
- * Interface for persisting branch-issue links.
34
- * Implement this for different storage backends (file system, VSCode state, etc.)
35
- */
36
- interface StorageAdapter {
37
- load(): BranchLink[] | Promise<BranchLink[]>;
38
- save(links: BranchLink[]): void | Promise<void>;
39
- }
40
- /**
41
- * A link between a git branch and a GitHub issue/item
42
- */
43
- interface BranchLink {
44
- branch: string;
45
- issueNumber: number;
46
- issueTitle: string;
47
- itemId: string;
48
- repo: string;
49
- linkedAt: string;
50
- }
51
32
  /**
52
33
  * Options for git operations
53
34
  */
@@ -517,123 +498,63 @@ declare class GitHubAPI {
517
498
  * Update an issue's body/description
518
499
  */
519
500
  updateIssueBody(repo: RepoInfo, issueNumber: number, body: string): Promise<boolean>;
501
+ /**
502
+ * Update an issue's title and/or body
503
+ */
504
+ updateIssue(repo: RepoInfo, issueNumber: number, updates: {
505
+ title?: string;
506
+ body?: string;
507
+ }): Promise<boolean>;
520
508
  }
521
509
 
522
510
  /**
523
- * Branch-issue linking with pluggable storage.
524
- *
525
- * The BranchLinker class manages associations between git branches and GitHub issues.
526
- * Storage is abstracted via the StorageAdapter interface, allowing different backends:
527
- * - File system (for CLI)
528
- * - VSCode workspaceState (for extensions)
529
- * - In-memory (for testing)
530
- *
531
- * @example CLI usage with file storage:
532
- * ```typescript
533
- * import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
534
- * import { homedir } from 'os';
535
- * import { join } from 'path';
511
+ * Branch-issue linking stored directly in GitHub issue bodies.
536
512
  *
537
- * const DATA_DIR = join(homedir(), '.config', 'ghp-cli');
538
- * const LINKS_FILE = join(DATA_DIR, 'branch-links.json');
513
+ * Links are stored as hidden HTML comments in the issue body:
514
+ * <!-- ghp-branch: feature/my-branch -->
539
515
  *
540
- * const fileAdapter: StorageAdapter = {
541
- * load() {
542
- * if (existsSync(LINKS_FILE)) {
543
- * return JSON.parse(readFileSync(LINKS_FILE, 'utf-8'));
544
- * }
545
- * return [];
546
- * },
547
- * save(links) {
548
- * if (!existsSync(DATA_DIR)) {
549
- * mkdirSync(DATA_DIR, { recursive: true });
550
- * }
551
- * writeFileSync(LINKS_FILE, JSON.stringify(links, null, 2));
552
- * }
553
- * };
554
- *
555
- * const linker = new BranchLinker(fileAdapter);
556
- * ```
557
- *
558
- * @example VSCode usage with workspaceState:
559
- * ```typescript
560
- * const vscodeAdapter: StorageAdapter = {
561
- * load() {
562
- * return context.workspaceState.get<BranchLink[]>('branchLinks', []);
563
- * },
564
- * save(links) {
565
- * context.workspaceState.update('branchLinks', links);
566
- * }
567
- * };
568
- *
569
- * const linker = new BranchLinker(vscodeAdapter);
570
- * ```
516
+ * This allows branch links to be shared across all consumers (CLI, VSCode, etc.)
517
+ * since they're stored on GitHub itself.
571
518
  */
572
519
 
573
520
  /**
574
- * Manages branch-issue links using a pluggable storage adapter.
521
+ * Parse the linked branch from an issue body.
522
+ */
523
+ declare function parseBranchLink(body: string | null | undefined): string | null;
524
+ /**
525
+ * Set or update the branch link in an issue body.
526
+ * Returns the updated body string.
527
+ */
528
+ declare function setBranchLinkInBody(body: string | null | undefined, branch: string): string;
529
+ /**
530
+ * Remove the branch link from an issue body.
531
+ * Returns the updated body string.
532
+ */
533
+ declare function removeBranchLinkFromBody(body: string | null | undefined): string;
534
+ /**
535
+ * Manages branch-issue links stored in GitHub issue bodies.
575
536
  */
576
537
  declare class BranchLinker {
577
- private storage;
578
- constructor(storage: StorageAdapter);
579
- /**
580
- * Load links from storage (handles both sync and async adapters)
581
- */
582
- private loadLinks;
583
- /**
584
- * Save links to storage (handles both sync and async adapters)
585
- */
586
- private saveLinks;
538
+ private api;
539
+ constructor(api: GitHubAPI);
587
540
  /**
588
541
  * Create a link between a branch and an issue.
589
- * If a link already exists for this branch or issue in this repo, it will be replaced.
590
- */
591
- link(branch: string, issueNumber: number, issueTitle: string, itemId: string, repo: string): Promise<void>;
592
- /**
593
- * Remove the link for an issue.
594
- * @returns true if a link was removed, false if no link existed
542
+ * Stores the link as a hidden comment in the issue body.
595
543
  */
596
- unlink(repo: string, issueNumber: number): Promise<boolean>;
544
+ link(repo: RepoInfo, issueNumber: number, branch: string): Promise<boolean>;
597
545
  /**
598
- * Remove the link for a branch.
599
- * @returns true if a link was removed, false if no link existed
546
+ * Remove the branch link from an issue.
600
547
  */
601
- unlinkBranch(repo: string, branch: string): Promise<boolean>;
548
+ unlink(repo: RepoInfo, issueNumber: number): Promise<boolean>;
602
549
  /**
603
550
  * Get the branch linked to an issue.
604
551
  */
605
- getBranchForIssue(repo: string, issueNumber: number): Promise<string | null>;
606
- /**
607
- * Get the full link info for a branch.
608
- */
609
- getLinkForBranch(repo: string, branch: string): Promise<BranchLink | null>;
552
+ getBranchForIssue(repo: RepoInfo, issueNumber: number): Promise<string | null>;
610
553
  /**
611
- * Get the full link info for an issue.
554
+ * Check if an issue has a branch link.
612
555
  */
613
- getLinkForIssue(repo: string, issueNumber: number): Promise<BranchLink | null>;
614
- /**
615
- * Get all links for a repository.
616
- */
617
- getLinksForRepo(repo: string): Promise<BranchLink[]>;
618
- /**
619
- * Get all links.
620
- */
621
- getAllLinks(): Promise<BranchLink[]>;
622
- /**
623
- * Check if a branch has a link.
624
- */
625
- hasLinkForBranch(repo: string, branch: string): Promise<boolean>;
626
- /**
627
- * Check if an issue has a link.
628
- */
629
- hasLinkForIssue(repo: string, issueNumber: number): Promise<boolean>;
556
+ hasLink(repo: RepoInfo, issueNumber: number): Promise<boolean>;
630
557
  }
631
- /**
632
- * Create an in-memory storage adapter for testing.
633
- */
634
- declare function createInMemoryAdapter(): StorageAdapter & {
635
- links: BranchLink[];
636
- };
637
558
 
638
559
  /**
639
560
  * Git utility functions for working with local repositories.
@@ -862,6 +783,10 @@ declare const UPDATE_ISSUE_TYPE_MUTATION = "\n mutation($issueId: ID!, $issue
862
783
  * Mutation to update issue body/description
863
784
  */
864
785
  declare const UPDATE_ISSUE_BODY_MUTATION = "\n mutation($issueId: ID!, $body: String!) {\n updateIssue(input: { id: $issueId, body: $body }) {\n issue {\n id\n }\n }\n }\n";
786
+ /**
787
+ * Mutation to update issue title and/or body
788
+ */
789
+ declare const UPDATE_ISSUE_MUTATION = "\n mutation($issueId: ID!, $title: String, $body: String) {\n updateIssue(input: { id: $issueId, title: $title, body: $body }) {\n issue {\n id\n }\n }\n }\n";
865
790
 
866
791
  declare const queries_ADD_COMMENT_MUTATION: typeof ADD_COMMENT_MUTATION;
867
792
  declare const queries_ADD_LABELS_MUTATION: typeof ADD_LABELS_MUTATION;
@@ -883,12 +808,13 @@ declare const queries_REMOVE_LABELS_MUTATION: typeof REMOVE_LABELS_MUTATION;
883
808
  declare const queries_REPOSITORY_ID_QUERY: typeof REPOSITORY_ID_QUERY;
884
809
  declare const queries_REPOSITORY_PROJECTS_QUERY: typeof REPOSITORY_PROJECTS_QUERY;
885
810
  declare const queries_UPDATE_ISSUE_BODY_MUTATION: typeof UPDATE_ISSUE_BODY_MUTATION;
811
+ declare const queries_UPDATE_ISSUE_MUTATION: typeof UPDATE_ISSUE_MUTATION;
886
812
  declare const queries_UPDATE_ISSUE_TYPE_MUTATION: typeof UPDATE_ISSUE_TYPE_MUTATION;
887
813
  declare const queries_UPDATE_ITEM_FIELD_MUTATION: typeof UPDATE_ITEM_FIELD_MUTATION;
888
814
  declare const queries_UPDATE_ITEM_STATUS_MUTATION: typeof UPDATE_ITEM_STATUS_MUTATION;
889
815
  declare const queries_VIEWER_QUERY: typeof VIEWER_QUERY;
890
816
  declare namespace queries {
891
- export { queries_ADD_COMMENT_MUTATION as ADD_COMMENT_MUTATION, queries_ADD_LABELS_MUTATION as ADD_LABELS_MUTATION, queries_ADD_TO_PROJECT_MUTATION as ADD_TO_PROJECT_MUTATION, queries_COLLABORATORS_QUERY as COLLABORATORS_QUERY, queries_CREATE_ISSUE_MUTATION as CREATE_ISSUE_MUTATION, queries_ISSUES_WITH_LABEL_QUERY as ISSUES_WITH_LABEL_QUERY, queries_ISSUE_AND_LABEL_QUERY as ISSUE_AND_LABEL_QUERY, queries_ISSUE_DETAILS_QUERY as ISSUE_DETAILS_QUERY, queries_ISSUE_FOR_UPDATE_QUERY as ISSUE_FOR_UPDATE_QUERY, queries_ISSUE_NODE_ID_QUERY as ISSUE_NODE_ID_QUERY, queries_ISSUE_TYPES_QUERY as ISSUE_TYPES_QUERY, queries_LABEL_EXISTS_QUERY as LABEL_EXISTS_QUERY, queries_PROJECT_FIELDS_QUERY as PROJECT_FIELDS_QUERY, queries_PROJECT_ITEMS_QUERY as PROJECT_ITEMS_QUERY, queries_PROJECT_VIEWS_QUERY as PROJECT_VIEWS_QUERY, queries_RECENT_ISSUES_QUERY as RECENT_ISSUES_QUERY, queries_REMOVE_LABELS_MUTATION as REMOVE_LABELS_MUTATION, queries_REPOSITORY_ID_QUERY as REPOSITORY_ID_QUERY, queries_REPOSITORY_PROJECTS_QUERY as REPOSITORY_PROJECTS_QUERY, queries_UPDATE_ISSUE_BODY_MUTATION as UPDATE_ISSUE_BODY_MUTATION, queries_UPDATE_ISSUE_TYPE_MUTATION as UPDATE_ISSUE_TYPE_MUTATION, queries_UPDATE_ITEM_FIELD_MUTATION as UPDATE_ITEM_FIELD_MUTATION, queries_UPDATE_ITEM_STATUS_MUTATION as UPDATE_ITEM_STATUS_MUTATION, queries_VIEWER_QUERY as VIEWER_QUERY };
817
+ export { queries_ADD_COMMENT_MUTATION as ADD_COMMENT_MUTATION, queries_ADD_LABELS_MUTATION as ADD_LABELS_MUTATION, queries_ADD_TO_PROJECT_MUTATION as ADD_TO_PROJECT_MUTATION, queries_COLLABORATORS_QUERY as COLLABORATORS_QUERY, queries_CREATE_ISSUE_MUTATION as CREATE_ISSUE_MUTATION, queries_ISSUES_WITH_LABEL_QUERY as ISSUES_WITH_LABEL_QUERY, queries_ISSUE_AND_LABEL_QUERY as ISSUE_AND_LABEL_QUERY, queries_ISSUE_DETAILS_QUERY as ISSUE_DETAILS_QUERY, queries_ISSUE_FOR_UPDATE_QUERY as ISSUE_FOR_UPDATE_QUERY, queries_ISSUE_NODE_ID_QUERY as ISSUE_NODE_ID_QUERY, queries_ISSUE_TYPES_QUERY as ISSUE_TYPES_QUERY, queries_LABEL_EXISTS_QUERY as LABEL_EXISTS_QUERY, queries_PROJECT_FIELDS_QUERY as PROJECT_FIELDS_QUERY, queries_PROJECT_ITEMS_QUERY as PROJECT_ITEMS_QUERY, queries_PROJECT_VIEWS_QUERY as PROJECT_VIEWS_QUERY, queries_RECENT_ISSUES_QUERY as RECENT_ISSUES_QUERY, queries_REMOVE_LABELS_MUTATION as REMOVE_LABELS_MUTATION, queries_REPOSITORY_ID_QUERY as REPOSITORY_ID_QUERY, queries_REPOSITORY_PROJECTS_QUERY as REPOSITORY_PROJECTS_QUERY, queries_UPDATE_ISSUE_BODY_MUTATION as UPDATE_ISSUE_BODY_MUTATION, queries_UPDATE_ISSUE_MUTATION as UPDATE_ISSUE_MUTATION, queries_UPDATE_ISSUE_TYPE_MUTATION as UPDATE_ISSUE_TYPE_MUTATION, queries_UPDATE_ITEM_FIELD_MUTATION as UPDATE_ITEM_FIELD_MUTATION, queries_UPDATE_ITEM_STATUS_MUTATION as UPDATE_ITEM_STATUS_MUTATION, queries_VIEWER_QUERY as VIEWER_QUERY };
892
818
  }
893
819
 
894
- export { type AssigneeInfo, type AuthError, type BranchLink, BranchLinker, type Collaborator, type DateFieldValue, type FieldInfo, type FieldValue, type FieldValueConnection, GitHubAPI, type GitHubAPIOptions, type GitOptions, type IssueDetails, type IssueReference, type IterationFieldValue, type LabelInfo, type NumberFieldValue, type Project, type ProjectConfig, type ProjectItem, type ProjectItemContent, type ProjectItemsQueryResponse, type ProjectV2, type ProjectV2Field, type ProjectV2Item, type ProjectV2View, type ProjectWithViews, type ProjectsQueryResponse, type RepoInfo, type SingleSelectFieldValue, type StatusField, type StorageAdapter, type TextFieldValue, type TokenProvider, branchExists, buildIssueUrl, buildOrgProjectUrl, buildProjectUrl, buildPullRequestUrl, buildRepoUrl, checkoutBranch, createBranch, createInMemoryAdapter, detectRepository, fetchOrigin, generateBranchName, getCommitsAhead, getCommitsBehind, getCurrentBranch, getDefaultBranch, getRepositoryRoot, hasUncommittedChanges, isGitRepository, parseGitHubUrl, parseIssueUrl, pullLatest, queries, sanitizeForBranchName };
820
+ export { type AssigneeInfo, type AuthError, BranchLinker, type Collaborator, type DateFieldValue, type FieldInfo, type FieldValue, type FieldValueConnection, GitHubAPI, type GitHubAPIOptions, type GitOptions, type IssueDetails, type IssueReference, type IterationFieldValue, type LabelInfo, type NumberFieldValue, type Project, type ProjectConfig, type ProjectItem, type ProjectItemContent, type ProjectItemsQueryResponse, type ProjectV2, type ProjectV2Field, type ProjectV2Item, type ProjectV2View, type ProjectWithViews, type ProjectsQueryResponse, type RepoInfo, type SingleSelectFieldValue, type StatusField, type TextFieldValue, type TokenProvider, branchExists, buildIssueUrl, buildOrgProjectUrl, buildProjectUrl, buildPullRequestUrl, buildRepoUrl, checkoutBranch, createBranch, detectRepository, fetchOrigin, generateBranchName, getCommitsAhead, getCommitsBehind, getCurrentBranch, getDefaultBranch, getRepositoryRoot, hasUncommittedChanges, isGitRepository, parseBranchLink, parseGitHubUrl, parseIssueUrl, pullLatest, queries, removeBranchLinkFromBody, sanitizeForBranchName, setBranchLinkInBody };
package/dist/index.d.ts CHANGED
@@ -29,25 +29,6 @@ interface AuthError extends Error {
29
29
  requiredScopes?: string[];
30
30
  ssoUrl?: string;
31
31
  }
32
- /**
33
- * Interface for persisting branch-issue links.
34
- * Implement this for different storage backends (file system, VSCode state, etc.)
35
- */
36
- interface StorageAdapter {
37
- load(): BranchLink[] | Promise<BranchLink[]>;
38
- save(links: BranchLink[]): void | Promise<void>;
39
- }
40
- /**
41
- * A link between a git branch and a GitHub issue/item
42
- */
43
- interface BranchLink {
44
- branch: string;
45
- issueNumber: number;
46
- issueTitle: string;
47
- itemId: string;
48
- repo: string;
49
- linkedAt: string;
50
- }
51
32
  /**
52
33
  * Options for git operations
53
34
  */
@@ -517,123 +498,63 @@ declare class GitHubAPI {
517
498
  * Update an issue's body/description
518
499
  */
519
500
  updateIssueBody(repo: RepoInfo, issueNumber: number, body: string): Promise<boolean>;
501
+ /**
502
+ * Update an issue's title and/or body
503
+ */
504
+ updateIssue(repo: RepoInfo, issueNumber: number, updates: {
505
+ title?: string;
506
+ body?: string;
507
+ }): Promise<boolean>;
520
508
  }
521
509
 
522
510
  /**
523
- * Branch-issue linking with pluggable storage.
524
- *
525
- * The BranchLinker class manages associations between git branches and GitHub issues.
526
- * Storage is abstracted via the StorageAdapter interface, allowing different backends:
527
- * - File system (for CLI)
528
- * - VSCode workspaceState (for extensions)
529
- * - In-memory (for testing)
530
- *
531
- * @example CLI usage with file storage:
532
- * ```typescript
533
- * import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
534
- * import { homedir } from 'os';
535
- * import { join } from 'path';
511
+ * Branch-issue linking stored directly in GitHub issue bodies.
536
512
  *
537
- * const DATA_DIR = join(homedir(), '.config', 'ghp-cli');
538
- * const LINKS_FILE = join(DATA_DIR, 'branch-links.json');
513
+ * Links are stored as hidden HTML comments in the issue body:
514
+ * <!-- ghp-branch: feature/my-branch -->
539
515
  *
540
- * const fileAdapter: StorageAdapter = {
541
- * load() {
542
- * if (existsSync(LINKS_FILE)) {
543
- * return JSON.parse(readFileSync(LINKS_FILE, 'utf-8'));
544
- * }
545
- * return [];
546
- * },
547
- * save(links) {
548
- * if (!existsSync(DATA_DIR)) {
549
- * mkdirSync(DATA_DIR, { recursive: true });
550
- * }
551
- * writeFileSync(LINKS_FILE, JSON.stringify(links, null, 2));
552
- * }
553
- * };
554
- *
555
- * const linker = new BranchLinker(fileAdapter);
556
- * ```
557
- *
558
- * @example VSCode usage with workspaceState:
559
- * ```typescript
560
- * const vscodeAdapter: StorageAdapter = {
561
- * load() {
562
- * return context.workspaceState.get<BranchLink[]>('branchLinks', []);
563
- * },
564
- * save(links) {
565
- * context.workspaceState.update('branchLinks', links);
566
- * }
567
- * };
568
- *
569
- * const linker = new BranchLinker(vscodeAdapter);
570
- * ```
516
+ * This allows branch links to be shared across all consumers (CLI, VSCode, etc.)
517
+ * since they're stored on GitHub itself.
571
518
  */
572
519
 
573
520
  /**
574
- * Manages branch-issue links using a pluggable storage adapter.
521
+ * Parse the linked branch from an issue body.
522
+ */
523
+ declare function parseBranchLink(body: string | null | undefined): string | null;
524
+ /**
525
+ * Set or update the branch link in an issue body.
526
+ * Returns the updated body string.
527
+ */
528
+ declare function setBranchLinkInBody(body: string | null | undefined, branch: string): string;
529
+ /**
530
+ * Remove the branch link from an issue body.
531
+ * Returns the updated body string.
532
+ */
533
+ declare function removeBranchLinkFromBody(body: string | null | undefined): string;
534
+ /**
535
+ * Manages branch-issue links stored in GitHub issue bodies.
575
536
  */
576
537
  declare class BranchLinker {
577
- private storage;
578
- constructor(storage: StorageAdapter);
579
- /**
580
- * Load links from storage (handles both sync and async adapters)
581
- */
582
- private loadLinks;
583
- /**
584
- * Save links to storage (handles both sync and async adapters)
585
- */
586
- private saveLinks;
538
+ private api;
539
+ constructor(api: GitHubAPI);
587
540
  /**
588
541
  * Create a link between a branch and an issue.
589
- * If a link already exists for this branch or issue in this repo, it will be replaced.
590
- */
591
- link(branch: string, issueNumber: number, issueTitle: string, itemId: string, repo: string): Promise<void>;
592
- /**
593
- * Remove the link for an issue.
594
- * @returns true if a link was removed, false if no link existed
542
+ * Stores the link as a hidden comment in the issue body.
595
543
  */
596
- unlink(repo: string, issueNumber: number): Promise<boolean>;
544
+ link(repo: RepoInfo, issueNumber: number, branch: string): Promise<boolean>;
597
545
  /**
598
- * Remove the link for a branch.
599
- * @returns true if a link was removed, false if no link existed
546
+ * Remove the branch link from an issue.
600
547
  */
601
- unlinkBranch(repo: string, branch: string): Promise<boolean>;
548
+ unlink(repo: RepoInfo, issueNumber: number): Promise<boolean>;
602
549
  /**
603
550
  * Get the branch linked to an issue.
604
551
  */
605
- getBranchForIssue(repo: string, issueNumber: number): Promise<string | null>;
606
- /**
607
- * Get the full link info for a branch.
608
- */
609
- getLinkForBranch(repo: string, branch: string): Promise<BranchLink | null>;
552
+ getBranchForIssue(repo: RepoInfo, issueNumber: number): Promise<string | null>;
610
553
  /**
611
- * Get the full link info for an issue.
554
+ * Check if an issue has a branch link.
612
555
  */
613
- getLinkForIssue(repo: string, issueNumber: number): Promise<BranchLink | null>;
614
- /**
615
- * Get all links for a repository.
616
- */
617
- getLinksForRepo(repo: string): Promise<BranchLink[]>;
618
- /**
619
- * Get all links.
620
- */
621
- getAllLinks(): Promise<BranchLink[]>;
622
- /**
623
- * Check if a branch has a link.
624
- */
625
- hasLinkForBranch(repo: string, branch: string): Promise<boolean>;
626
- /**
627
- * Check if an issue has a link.
628
- */
629
- hasLinkForIssue(repo: string, issueNumber: number): Promise<boolean>;
556
+ hasLink(repo: RepoInfo, issueNumber: number): Promise<boolean>;
630
557
  }
631
- /**
632
- * Create an in-memory storage adapter for testing.
633
- */
634
- declare function createInMemoryAdapter(): StorageAdapter & {
635
- links: BranchLink[];
636
- };
637
558
 
638
559
  /**
639
560
  * Git utility functions for working with local repositories.
@@ -862,6 +783,10 @@ declare const UPDATE_ISSUE_TYPE_MUTATION = "\n mutation($issueId: ID!, $issue
862
783
  * Mutation to update issue body/description
863
784
  */
864
785
  declare const UPDATE_ISSUE_BODY_MUTATION = "\n mutation($issueId: ID!, $body: String!) {\n updateIssue(input: { id: $issueId, body: $body }) {\n issue {\n id\n }\n }\n }\n";
786
+ /**
787
+ * Mutation to update issue title and/or body
788
+ */
789
+ declare const UPDATE_ISSUE_MUTATION = "\n mutation($issueId: ID!, $title: String, $body: String) {\n updateIssue(input: { id: $issueId, title: $title, body: $body }) {\n issue {\n id\n }\n }\n }\n";
865
790
 
866
791
  declare const queries_ADD_COMMENT_MUTATION: typeof ADD_COMMENT_MUTATION;
867
792
  declare const queries_ADD_LABELS_MUTATION: typeof ADD_LABELS_MUTATION;
@@ -883,12 +808,13 @@ declare const queries_REMOVE_LABELS_MUTATION: typeof REMOVE_LABELS_MUTATION;
883
808
  declare const queries_REPOSITORY_ID_QUERY: typeof REPOSITORY_ID_QUERY;
884
809
  declare const queries_REPOSITORY_PROJECTS_QUERY: typeof REPOSITORY_PROJECTS_QUERY;
885
810
  declare const queries_UPDATE_ISSUE_BODY_MUTATION: typeof UPDATE_ISSUE_BODY_MUTATION;
811
+ declare const queries_UPDATE_ISSUE_MUTATION: typeof UPDATE_ISSUE_MUTATION;
886
812
  declare const queries_UPDATE_ISSUE_TYPE_MUTATION: typeof UPDATE_ISSUE_TYPE_MUTATION;
887
813
  declare const queries_UPDATE_ITEM_FIELD_MUTATION: typeof UPDATE_ITEM_FIELD_MUTATION;
888
814
  declare const queries_UPDATE_ITEM_STATUS_MUTATION: typeof UPDATE_ITEM_STATUS_MUTATION;
889
815
  declare const queries_VIEWER_QUERY: typeof VIEWER_QUERY;
890
816
  declare namespace queries {
891
- export { queries_ADD_COMMENT_MUTATION as ADD_COMMENT_MUTATION, queries_ADD_LABELS_MUTATION as ADD_LABELS_MUTATION, queries_ADD_TO_PROJECT_MUTATION as ADD_TO_PROJECT_MUTATION, queries_COLLABORATORS_QUERY as COLLABORATORS_QUERY, queries_CREATE_ISSUE_MUTATION as CREATE_ISSUE_MUTATION, queries_ISSUES_WITH_LABEL_QUERY as ISSUES_WITH_LABEL_QUERY, queries_ISSUE_AND_LABEL_QUERY as ISSUE_AND_LABEL_QUERY, queries_ISSUE_DETAILS_QUERY as ISSUE_DETAILS_QUERY, queries_ISSUE_FOR_UPDATE_QUERY as ISSUE_FOR_UPDATE_QUERY, queries_ISSUE_NODE_ID_QUERY as ISSUE_NODE_ID_QUERY, queries_ISSUE_TYPES_QUERY as ISSUE_TYPES_QUERY, queries_LABEL_EXISTS_QUERY as LABEL_EXISTS_QUERY, queries_PROJECT_FIELDS_QUERY as PROJECT_FIELDS_QUERY, queries_PROJECT_ITEMS_QUERY as PROJECT_ITEMS_QUERY, queries_PROJECT_VIEWS_QUERY as PROJECT_VIEWS_QUERY, queries_RECENT_ISSUES_QUERY as RECENT_ISSUES_QUERY, queries_REMOVE_LABELS_MUTATION as REMOVE_LABELS_MUTATION, queries_REPOSITORY_ID_QUERY as REPOSITORY_ID_QUERY, queries_REPOSITORY_PROJECTS_QUERY as REPOSITORY_PROJECTS_QUERY, queries_UPDATE_ISSUE_BODY_MUTATION as UPDATE_ISSUE_BODY_MUTATION, queries_UPDATE_ISSUE_TYPE_MUTATION as UPDATE_ISSUE_TYPE_MUTATION, queries_UPDATE_ITEM_FIELD_MUTATION as UPDATE_ITEM_FIELD_MUTATION, queries_UPDATE_ITEM_STATUS_MUTATION as UPDATE_ITEM_STATUS_MUTATION, queries_VIEWER_QUERY as VIEWER_QUERY };
817
+ export { queries_ADD_COMMENT_MUTATION as ADD_COMMENT_MUTATION, queries_ADD_LABELS_MUTATION as ADD_LABELS_MUTATION, queries_ADD_TO_PROJECT_MUTATION as ADD_TO_PROJECT_MUTATION, queries_COLLABORATORS_QUERY as COLLABORATORS_QUERY, queries_CREATE_ISSUE_MUTATION as CREATE_ISSUE_MUTATION, queries_ISSUES_WITH_LABEL_QUERY as ISSUES_WITH_LABEL_QUERY, queries_ISSUE_AND_LABEL_QUERY as ISSUE_AND_LABEL_QUERY, queries_ISSUE_DETAILS_QUERY as ISSUE_DETAILS_QUERY, queries_ISSUE_FOR_UPDATE_QUERY as ISSUE_FOR_UPDATE_QUERY, queries_ISSUE_NODE_ID_QUERY as ISSUE_NODE_ID_QUERY, queries_ISSUE_TYPES_QUERY as ISSUE_TYPES_QUERY, queries_LABEL_EXISTS_QUERY as LABEL_EXISTS_QUERY, queries_PROJECT_FIELDS_QUERY as PROJECT_FIELDS_QUERY, queries_PROJECT_ITEMS_QUERY as PROJECT_ITEMS_QUERY, queries_PROJECT_VIEWS_QUERY as PROJECT_VIEWS_QUERY, queries_RECENT_ISSUES_QUERY as RECENT_ISSUES_QUERY, queries_REMOVE_LABELS_MUTATION as REMOVE_LABELS_MUTATION, queries_REPOSITORY_ID_QUERY as REPOSITORY_ID_QUERY, queries_REPOSITORY_PROJECTS_QUERY as REPOSITORY_PROJECTS_QUERY, queries_UPDATE_ISSUE_BODY_MUTATION as UPDATE_ISSUE_BODY_MUTATION, queries_UPDATE_ISSUE_MUTATION as UPDATE_ISSUE_MUTATION, queries_UPDATE_ISSUE_TYPE_MUTATION as UPDATE_ISSUE_TYPE_MUTATION, queries_UPDATE_ITEM_FIELD_MUTATION as UPDATE_ITEM_FIELD_MUTATION, queries_UPDATE_ITEM_STATUS_MUTATION as UPDATE_ITEM_STATUS_MUTATION, queries_VIEWER_QUERY as VIEWER_QUERY };
892
818
  }
893
819
 
894
- export { type AssigneeInfo, type AuthError, type BranchLink, BranchLinker, type Collaborator, type DateFieldValue, type FieldInfo, type FieldValue, type FieldValueConnection, GitHubAPI, type GitHubAPIOptions, type GitOptions, type IssueDetails, type IssueReference, type IterationFieldValue, type LabelInfo, type NumberFieldValue, type Project, type ProjectConfig, type ProjectItem, type ProjectItemContent, type ProjectItemsQueryResponse, type ProjectV2, type ProjectV2Field, type ProjectV2Item, type ProjectV2View, type ProjectWithViews, type ProjectsQueryResponse, type RepoInfo, type SingleSelectFieldValue, type StatusField, type StorageAdapter, type TextFieldValue, type TokenProvider, branchExists, buildIssueUrl, buildOrgProjectUrl, buildProjectUrl, buildPullRequestUrl, buildRepoUrl, checkoutBranch, createBranch, createInMemoryAdapter, detectRepository, fetchOrigin, generateBranchName, getCommitsAhead, getCommitsBehind, getCurrentBranch, getDefaultBranch, getRepositoryRoot, hasUncommittedChanges, isGitRepository, parseGitHubUrl, parseIssueUrl, pullLatest, queries, sanitizeForBranchName };
820
+ export { type AssigneeInfo, type AuthError, BranchLinker, type Collaborator, type DateFieldValue, type FieldInfo, type FieldValue, type FieldValueConnection, GitHubAPI, type GitHubAPIOptions, type GitOptions, type IssueDetails, type IssueReference, type IterationFieldValue, type LabelInfo, type NumberFieldValue, type Project, type ProjectConfig, type ProjectItem, type ProjectItemContent, type ProjectItemsQueryResponse, type ProjectV2, type ProjectV2Field, type ProjectV2Item, type ProjectV2View, type ProjectWithViews, type ProjectsQueryResponse, type RepoInfo, type SingleSelectFieldValue, type StatusField, type TextFieldValue, type TokenProvider, branchExists, buildIssueUrl, buildOrgProjectUrl, buildProjectUrl, buildPullRequestUrl, buildRepoUrl, checkoutBranch, createBranch, detectRepository, fetchOrigin, generateBranchName, getCommitsAhead, getCommitsBehind, getCurrentBranch, getDefaultBranch, getRepositoryRoot, hasUncommittedChanges, isGitRepository, parseBranchLink, parseGitHubUrl, parseIssueUrl, pullLatest, queries, removeBranchLinkFromBody, sanitizeForBranchName, setBranchLinkInBody };
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ __export(queries_exports, {
30
30
  REPOSITORY_ID_QUERY: () => REPOSITORY_ID_QUERY,
31
31
  REPOSITORY_PROJECTS_QUERY: () => REPOSITORY_PROJECTS_QUERY,
32
32
  UPDATE_ISSUE_BODY_MUTATION: () => UPDATE_ISSUE_BODY_MUTATION,
33
+ UPDATE_ISSUE_MUTATION: () => UPDATE_ISSUE_MUTATION,
33
34
  UPDATE_ISSUE_TYPE_MUTATION: () => UPDATE_ISSUE_TYPE_MUTATION,
34
35
  UPDATE_ITEM_FIELD_MUTATION: () => UPDATE_ITEM_FIELD_MUTATION,
35
36
  UPDATE_ITEM_STATUS_MUTATION: () => UPDATE_ITEM_STATUS_MUTATION,
@@ -385,6 +386,15 @@ var UPDATE_ISSUE_BODY_MUTATION = `
385
386
  }
386
387
  }
387
388
  `;
389
+ var UPDATE_ISSUE_MUTATION = `
390
+ mutation($issueId: ID!, $title: String, $body: String) {
391
+ updateIssue(input: { id: $issueId, title: $title, body: $body }) {
392
+ issue {
393
+ id
394
+ }
395
+ }
396
+ }
397
+ `;
388
398
 
389
399
  // src/github-api.ts
390
400
  function createAuthError(message, type, details) {
@@ -923,141 +933,94 @@ var GitHubAPI = class {
923
933
  return false;
924
934
  }
925
935
  }
936
+ /**
937
+ * Update an issue's title and/or body
938
+ */
939
+ async updateIssue(repo, issueNumber, updates) {
940
+ if (!this.graphqlWithAuth) throw new Error("Not authenticated");
941
+ try {
942
+ const issueResponse = await this.graphqlWithAuth(ISSUE_FOR_UPDATE_QUERY, {
943
+ owner: repo.owner,
944
+ name: repo.name,
945
+ number: issueNumber
946
+ });
947
+ if (!issueResponse.repository.issue) {
948
+ return false;
949
+ }
950
+ await this.graphqlWithAuth(UPDATE_ISSUE_MUTATION, {
951
+ issueId: issueResponse.repository.issue.id,
952
+ title: updates.title,
953
+ body: updates.body
954
+ });
955
+ return true;
956
+ } catch {
957
+ return false;
958
+ }
959
+ }
926
960
  };
927
961
 
928
962
  // src/branch-linker.ts
929
- var BranchLinker = class {
930
- storage;
931
- constructor(storage) {
932
- this.storage = storage;
933
- }
934
- /**
935
- * Load links from storage (handles both sync and async adapters)
936
- */
937
- async loadLinks() {
938
- const result = this.storage.load();
939
- return result instanceof Promise ? await result : result;
963
+ var BRANCH_LINK_PATTERN = /<!--\s*ghp-branch:\s*(.+?)\s*-->/;
964
+ function parseBranchLink(body) {
965
+ if (!body) return null;
966
+ const match = body.match(BRANCH_LINK_PATTERN);
967
+ return match ? match[1].trim() : null;
968
+ }
969
+ function setBranchLinkInBody(body, branch) {
970
+ const currentBody = body || "";
971
+ const tag = `<!-- ghp-branch: ${branch} -->`;
972
+ if (BRANCH_LINK_PATTERN.test(currentBody)) {
973
+ return currentBody.replace(BRANCH_LINK_PATTERN, tag);
974
+ } else {
975
+ return currentBody.trim() + "\n\n" + tag;
940
976
  }
941
- /**
942
- * Save links to storage (handles both sync and async adapters)
943
- */
944
- async saveLinks(links) {
945
- const result = this.storage.save(links);
946
- if (result instanceof Promise) {
947
- await result;
948
- }
977
+ }
978
+ function removeBranchLinkFromBody(body) {
979
+ if (!body) return "";
980
+ return body.replace(BRANCH_LINK_PATTERN, "").trim();
981
+ }
982
+ var BranchLinker = class {
983
+ api;
984
+ constructor(api) {
985
+ this.api = api;
949
986
  }
950
987
  /**
951
988
  * Create a link between a branch and an issue.
952
- * If a link already exists for this branch or issue in this repo, it will be replaced.
989
+ * Stores the link as a hidden comment in the issue body.
953
990
  */
954
- async link(branch, issueNumber, issueTitle, itemId, repo) {
955
- const links = await this.loadLinks();
956
- const filtered = links.filter(
957
- (l) => !(l.repo === repo && (l.branch === branch || l.issueNumber === issueNumber))
958
- );
959
- filtered.push({
960
- branch,
961
- issueNumber,
962
- issueTitle,
963
- itemId,
964
- repo,
965
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
966
- });
967
- await this.saveLinks(filtered);
991
+ async link(repo, issueNumber, branch) {
992
+ const details = await this.api.getIssueDetails(repo, issueNumber);
993
+ if (!details) return false;
994
+ const newBody = setBranchLinkInBody(details.body, branch);
995
+ return this.api.updateIssueBody(repo, issueNumber, newBody);
968
996
  }
969
997
  /**
970
- * Remove the link for an issue.
971
- * @returns true if a link was removed, false if no link existed
998
+ * Remove the branch link from an issue.
972
999
  */
973
1000
  async unlink(repo, issueNumber) {
974
- const links = await this.loadLinks();
975
- const filtered = links.filter(
976
- (l) => !(l.repo === repo && l.issueNumber === issueNumber)
977
- );
978
- if (filtered.length === links.length) {
979
- return false;
980
- }
981
- await this.saveLinks(filtered);
982
- return true;
983
- }
984
- /**
985
- * Remove the link for a branch.
986
- * @returns true if a link was removed, false if no link existed
987
- */
988
- async unlinkBranch(repo, branch) {
989
- const links = await this.loadLinks();
990
- const filtered = links.filter(
991
- (l) => !(l.repo === repo && l.branch === branch)
992
- );
993
- if (filtered.length === links.length) {
994
- return false;
995
- }
996
- await this.saveLinks(filtered);
997
- return true;
1001
+ const details = await this.api.getIssueDetails(repo, issueNumber);
1002
+ if (!details) return false;
1003
+ const currentBranch = parseBranchLink(details.body);
1004
+ if (!currentBranch) return false;
1005
+ const newBody = removeBranchLinkFromBody(details.body);
1006
+ return this.api.updateIssueBody(repo, issueNumber, newBody);
998
1007
  }
999
1008
  /**
1000
1009
  * Get the branch linked to an issue.
1001
1010
  */
1002
1011
  async getBranchForIssue(repo, issueNumber) {
1003
- const links = await this.loadLinks();
1004
- const link = links.find((l) => l.repo === repo && l.issueNumber === issueNumber);
1005
- return link?.branch || null;
1012
+ const details = await this.api.getIssueDetails(repo, issueNumber);
1013
+ if (!details) return null;
1014
+ return parseBranchLink(details.body);
1006
1015
  }
1007
1016
  /**
1008
- * Get the full link info for a branch.
1017
+ * Check if an issue has a branch link.
1009
1018
  */
1010
- async getLinkForBranch(repo, branch) {
1011
- const links = await this.loadLinks();
1012
- return links.find((l) => l.repo === repo && l.branch === branch) || null;
1013
- }
1014
- /**
1015
- * Get the full link info for an issue.
1016
- */
1017
- async getLinkForIssue(repo, issueNumber) {
1018
- const links = await this.loadLinks();
1019
- return links.find((l) => l.repo === repo && l.issueNumber === issueNumber) || null;
1020
- }
1021
- /**
1022
- * Get all links for a repository.
1023
- */
1024
- async getLinksForRepo(repo) {
1025
- const links = await this.loadLinks();
1026
- return links.filter((l) => l.repo === repo);
1027
- }
1028
- /**
1029
- * Get all links.
1030
- */
1031
- async getAllLinks() {
1032
- return this.loadLinks();
1033
- }
1034
- /**
1035
- * Check if a branch has a link.
1036
- */
1037
- async hasLinkForBranch(repo, branch) {
1038
- const link = await this.getLinkForBranch(repo, branch);
1039
- return link !== null;
1040
- }
1041
- /**
1042
- * Check if an issue has a link.
1043
- */
1044
- async hasLinkForIssue(repo, issueNumber) {
1045
- const link = await this.getLinkForIssue(repo, issueNumber);
1046
- return link !== null;
1019
+ async hasLink(repo, issueNumber) {
1020
+ const branch = await this.getBranchForIssue(repo, issueNumber);
1021
+ return branch !== null;
1047
1022
  }
1048
1023
  };
1049
- function createInMemoryAdapter() {
1050
- const adapter = {
1051
- links: [],
1052
- load() {
1053
- return [...this.links];
1054
- },
1055
- save(links) {
1056
- this.links = [...links];
1057
- }
1058
- };
1059
- return adapter;
1060
- }
1061
1024
 
1062
1025
  // src/git-utils.ts
1063
1026
  import { exec } from "child_process";
@@ -1244,7 +1207,6 @@ export {
1244
1207
  buildRepoUrl,
1245
1208
  checkoutBranch,
1246
1209
  createBranch,
1247
- createInMemoryAdapter,
1248
1210
  detectRepository,
1249
1211
  fetchOrigin,
1250
1212
  generateBranchName,
@@ -1255,9 +1217,12 @@ export {
1255
1217
  getRepositoryRoot,
1256
1218
  hasUncommittedChanges,
1257
1219
  isGitRepository,
1220
+ parseBranchLink,
1258
1221
  parseGitHubUrl,
1259
1222
  parseIssueUrl,
1260
1223
  pullLatest,
1261
1224
  queries_exports as queries,
1262
- sanitizeForBranchName
1225
+ removeBranchLinkFromBody,
1226
+ sanitizeForBranchName,
1227
+ setBranchLinkInBody
1263
1228
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bretwardjames/ghp-core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Shared core library for GitHub Projects tools",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",