@bretwardjames/ghp-core 0.1.2 → 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
 
@@ -1005,138 +1007,67 @@ var GitHubAPI = class {
1005
1007
  };
1006
1008
 
1007
1009
  // src/branch-linker.ts
1008
- var BranchLinker = class {
1009
- storage;
1010
- constructor(storage) {
1011
- this.storage = storage;
1012
- }
1013
- /**
1014
- * Load links from storage (handles both sync and async adapters)
1015
- */
1016
- async loadLinks() {
1017
- const result = this.storage.load();
1018
- 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;
1019
1023
  }
1020
- /**
1021
- * Save links to storage (handles both sync and async adapters)
1022
- */
1023
- async saveLinks(links) {
1024
- const result = this.storage.save(links);
1025
- if (result instanceof Promise) {
1026
- await result;
1027
- }
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;
1028
1033
  }
1029
1034
  /**
1030
1035
  * Create a link between a branch and an issue.
1031
- * 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.
1032
1037
  */
1033
- async link(branch, issueNumber, issueTitle, itemId, repo) {
1034
- const links = await this.loadLinks();
1035
- const filtered = links.filter(
1036
- (l) => !(l.repo === repo && (l.branch === branch || l.issueNumber === issueNumber))
1037
- );
1038
- filtered.push({
1039
- branch,
1040
- issueNumber,
1041
- issueTitle,
1042
- itemId,
1043
- repo,
1044
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1045
- });
1046
- 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);
1047
1043
  }
1048
1044
  /**
1049
- * Remove the link for an issue.
1050
- * @returns true if a link was removed, false if no link existed
1045
+ * Remove the branch link from an issue.
1051
1046
  */
1052
1047
  async unlink(repo, issueNumber) {
1053
- const links = await this.loadLinks();
1054
- const filtered = links.filter(
1055
- (l) => !(l.repo === repo && l.issueNumber === issueNumber)
1056
- );
1057
- if (filtered.length === links.length) {
1058
- return false;
1059
- }
1060
- await this.saveLinks(filtered);
1061
- return true;
1062
- }
1063
- /**
1064
- * Remove the link for a branch.
1065
- * @returns true if a link was removed, false if no link existed
1066
- */
1067
- async unlinkBranch(repo, branch) {
1068
- const links = await this.loadLinks();
1069
- const filtered = links.filter(
1070
- (l) => !(l.repo === repo && l.branch === branch)
1071
- );
1072
- if (filtered.length === links.length) {
1073
- return false;
1074
- }
1075
- await this.saveLinks(filtered);
1076
- 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);
1077
1054
  }
1078
1055
  /**
1079
1056
  * Get the branch linked to an issue.
1080
1057
  */
1081
1058
  async getBranchForIssue(repo, issueNumber) {
1082
- const links = await this.loadLinks();
1083
- const link = links.find((l) => l.repo === repo && l.issueNumber === issueNumber);
1084
- return link?.branch || null;
1085
- }
1086
- /**
1087
- * Get the full link info for a branch.
1088
- */
1089
- async getLinkForBranch(repo, branch) {
1090
- const links = await this.loadLinks();
1091
- return links.find((l) => l.repo === repo && l.branch === branch) || null;
1092
- }
1093
- /**
1094
- * Get the full link info for an issue.
1095
- */
1096
- async getLinkForIssue(repo, issueNumber) {
1097
- const links = await this.loadLinks();
1098
- return links.find((l) => l.repo === repo && l.issueNumber === issueNumber) || null;
1099
- }
1100
- /**
1101
- * Get all links for a repository.
1102
- */
1103
- async getLinksForRepo(repo) {
1104
- const links = await this.loadLinks();
1105
- return links.filter((l) => l.repo === repo);
1106
- }
1107
- /**
1108
- * Get all links.
1109
- */
1110
- async getAllLinks() {
1111
- return this.loadLinks();
1059
+ const details = await this.api.getIssueDetails(repo, issueNumber);
1060
+ if (!details) return null;
1061
+ return parseBranchLink(details.body);
1112
1062
  }
1113
1063
  /**
1114
- * Check if a branch has a link.
1064
+ * Check if an issue has a branch link.
1115
1065
  */
1116
- async hasLinkForBranch(repo, branch) {
1117
- const link = await this.getLinkForBranch(repo, branch);
1118
- return link !== null;
1119
- }
1120
- /**
1121
- * Check if an issue has a link.
1122
- */
1123
- async hasLinkForIssue(repo, issueNumber) {
1124
- const link = await this.getLinkForIssue(repo, issueNumber);
1125
- return link !== null;
1066
+ async hasLink(repo, issueNumber) {
1067
+ const branch = await this.getBranchForIssue(repo, issueNumber);
1068
+ return branch !== null;
1126
1069
  }
1127
1070
  };
1128
- function createInMemoryAdapter() {
1129
- const adapter = {
1130
- links: [],
1131
- load() {
1132
- return [...this.links];
1133
- },
1134
- save(links) {
1135
- this.links = [...links];
1136
- }
1137
- };
1138
- return adapter;
1139
- }
1140
1071
 
1141
1072
  // src/git-utils.ts
1142
1073
  var import_child_process = require("child_process");
@@ -1324,7 +1255,6 @@ async function getDefaultBranch(options = {}) {
1324
1255
  buildRepoUrl,
1325
1256
  checkoutBranch,
1326
1257
  createBranch,
1327
- createInMemoryAdapter,
1328
1258
  detectRepository,
1329
1259
  fetchOrigin,
1330
1260
  generateBranchName,
@@ -1335,9 +1265,12 @@ async function getDefaultBranch(options = {}) {
1335
1265
  getRepositoryRoot,
1336
1266
  hasUncommittedChanges,
1337
1267
  isGitRepository,
1268
+ parseBranchLink,
1338
1269
  parseGitHubUrl,
1339
1270
  parseIssueUrl,
1340
1271
  pullLatest,
1341
1272
  queries,
1342
- sanitizeForBranchName
1273
+ removeBranchLinkFromBody,
1274
+ sanitizeForBranchName,
1275
+ setBranchLinkInBody
1343
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
  */
@@ -527,120 +508,53 @@ declare class GitHubAPI {
527
508
  }
528
509
 
529
510
  /**
530
- * Branch-issue linking with pluggable storage.
531
- *
532
- * The BranchLinker class manages associations between git branches and GitHub issues.
533
- * Storage is abstracted via the StorageAdapter interface, allowing different backends:
534
- * - File system (for CLI)
535
- * - VSCode workspaceState (for extensions)
536
- * - In-memory (for testing)
537
- *
538
- * @example CLI usage with file storage:
539
- * ```typescript
540
- * import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
541
- * import { homedir } from 'os';
542
- * import { join } from 'path';
511
+ * Branch-issue linking stored directly in GitHub issue bodies.
543
512
  *
544
- * const DATA_DIR = join(homedir(), '.config', 'ghp-cli');
545
- * 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 -->
546
515
  *
547
- * const fileAdapter: StorageAdapter = {
548
- * load() {
549
- * if (existsSync(LINKS_FILE)) {
550
- * return JSON.parse(readFileSync(LINKS_FILE, 'utf-8'));
551
- * }
552
- * return [];
553
- * },
554
- * save(links) {
555
- * if (!existsSync(DATA_DIR)) {
556
- * mkdirSync(DATA_DIR, { recursive: true });
557
- * }
558
- * writeFileSync(LINKS_FILE, JSON.stringify(links, null, 2));
559
- * }
560
- * };
561
- *
562
- * const linker = new BranchLinker(fileAdapter);
563
- * ```
564
- *
565
- * @example VSCode usage with workspaceState:
566
- * ```typescript
567
- * const vscodeAdapter: StorageAdapter = {
568
- * load() {
569
- * return context.workspaceState.get<BranchLink[]>('branchLinks', []);
570
- * },
571
- * save(links) {
572
- * context.workspaceState.update('branchLinks', links);
573
- * }
574
- * };
575
- *
576
- * const linker = new BranchLinker(vscodeAdapter);
577
- * ```
516
+ * This allows branch links to be shared across all consumers (CLI, VSCode, etc.)
517
+ * since they're stored on GitHub itself.
578
518
  */
579
519
 
580
520
  /**
581
- * 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.
582
536
  */
583
537
  declare class BranchLinker {
584
- private storage;
585
- constructor(storage: StorageAdapter);
586
- /**
587
- * Load links from storage (handles both sync and async adapters)
588
- */
589
- private loadLinks;
590
- /**
591
- * Save links to storage (handles both sync and async adapters)
592
- */
593
- private saveLinks;
538
+ private api;
539
+ constructor(api: GitHubAPI);
594
540
  /**
595
541
  * Create a link between a branch and an issue.
596
- * If a link already exists for this branch or issue in this repo, it will be replaced.
542
+ * Stores the link as a hidden comment in the issue body.
597
543
  */
598
- link(branch: string, issueNumber: number, issueTitle: string, itemId: string, repo: string): Promise<void>;
544
+ link(repo: RepoInfo, issueNumber: number, branch: string): Promise<boolean>;
599
545
  /**
600
- * Remove the link for an issue.
601
- * @returns true if a link was removed, false if no link existed
546
+ * Remove the branch link from an issue.
602
547
  */
603
- unlink(repo: string, issueNumber: number): Promise<boolean>;
604
- /**
605
- * Remove the link for a branch.
606
- * @returns true if a link was removed, false if no link existed
607
- */
608
- unlinkBranch(repo: string, branch: string): Promise<boolean>;
548
+ unlink(repo: RepoInfo, issueNumber: number): Promise<boolean>;
609
549
  /**
610
550
  * Get the branch linked to an issue.
611
551
  */
612
- getBranchForIssue(repo: string, issueNumber: number): Promise<string | null>;
552
+ getBranchForIssue(repo: RepoInfo, issueNumber: number): Promise<string | null>;
613
553
  /**
614
- * Get the full link info for a branch.
554
+ * Check if an issue has a branch link.
615
555
  */
616
- getLinkForBranch(repo: string, branch: string): Promise<BranchLink | null>;
617
- /**
618
- * Get the full link info for an issue.
619
- */
620
- getLinkForIssue(repo: string, issueNumber: number): Promise<BranchLink | null>;
621
- /**
622
- * Get all links for a repository.
623
- */
624
- getLinksForRepo(repo: string): Promise<BranchLink[]>;
625
- /**
626
- * Get all links.
627
- */
628
- getAllLinks(): Promise<BranchLink[]>;
629
- /**
630
- * Check if a branch has a link.
631
- */
632
- hasLinkForBranch(repo: string, branch: string): Promise<boolean>;
633
- /**
634
- * Check if an issue has a link.
635
- */
636
- hasLinkForIssue(repo: string, issueNumber: number): Promise<boolean>;
556
+ hasLink(repo: RepoInfo, issueNumber: number): Promise<boolean>;
637
557
  }
638
- /**
639
- * Create an in-memory storage adapter for testing.
640
- */
641
- declare function createInMemoryAdapter(): StorageAdapter & {
642
- links: BranchLink[];
643
- };
644
558
 
645
559
  /**
646
560
  * Git utility functions for working with local repositories.
@@ -903,4 +817,4 @@ declare namespace queries {
903
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 };
904
818
  }
905
819
 
906
- 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
  */
@@ -527,120 +508,53 @@ declare class GitHubAPI {
527
508
  }
528
509
 
529
510
  /**
530
- * Branch-issue linking with pluggable storage.
531
- *
532
- * The BranchLinker class manages associations between git branches and GitHub issues.
533
- * Storage is abstracted via the StorageAdapter interface, allowing different backends:
534
- * - File system (for CLI)
535
- * - VSCode workspaceState (for extensions)
536
- * - In-memory (for testing)
537
- *
538
- * @example CLI usage with file storage:
539
- * ```typescript
540
- * import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
541
- * import { homedir } from 'os';
542
- * import { join } from 'path';
511
+ * Branch-issue linking stored directly in GitHub issue bodies.
543
512
  *
544
- * const DATA_DIR = join(homedir(), '.config', 'ghp-cli');
545
- * 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 -->
546
515
  *
547
- * const fileAdapter: StorageAdapter = {
548
- * load() {
549
- * if (existsSync(LINKS_FILE)) {
550
- * return JSON.parse(readFileSync(LINKS_FILE, 'utf-8'));
551
- * }
552
- * return [];
553
- * },
554
- * save(links) {
555
- * if (!existsSync(DATA_DIR)) {
556
- * mkdirSync(DATA_DIR, { recursive: true });
557
- * }
558
- * writeFileSync(LINKS_FILE, JSON.stringify(links, null, 2));
559
- * }
560
- * };
561
- *
562
- * const linker = new BranchLinker(fileAdapter);
563
- * ```
564
- *
565
- * @example VSCode usage with workspaceState:
566
- * ```typescript
567
- * const vscodeAdapter: StorageAdapter = {
568
- * load() {
569
- * return context.workspaceState.get<BranchLink[]>('branchLinks', []);
570
- * },
571
- * save(links) {
572
- * context.workspaceState.update('branchLinks', links);
573
- * }
574
- * };
575
- *
576
- * const linker = new BranchLinker(vscodeAdapter);
577
- * ```
516
+ * This allows branch links to be shared across all consumers (CLI, VSCode, etc.)
517
+ * since they're stored on GitHub itself.
578
518
  */
579
519
 
580
520
  /**
581
- * 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.
582
536
  */
583
537
  declare class BranchLinker {
584
- private storage;
585
- constructor(storage: StorageAdapter);
586
- /**
587
- * Load links from storage (handles both sync and async adapters)
588
- */
589
- private loadLinks;
590
- /**
591
- * Save links to storage (handles both sync and async adapters)
592
- */
593
- private saveLinks;
538
+ private api;
539
+ constructor(api: GitHubAPI);
594
540
  /**
595
541
  * Create a link between a branch and an issue.
596
- * If a link already exists for this branch or issue in this repo, it will be replaced.
542
+ * Stores the link as a hidden comment in the issue body.
597
543
  */
598
- link(branch: string, issueNumber: number, issueTitle: string, itemId: string, repo: string): Promise<void>;
544
+ link(repo: RepoInfo, issueNumber: number, branch: string): Promise<boolean>;
599
545
  /**
600
- * Remove the link for an issue.
601
- * @returns true if a link was removed, false if no link existed
546
+ * Remove the branch link from an issue.
602
547
  */
603
- unlink(repo: string, issueNumber: number): Promise<boolean>;
604
- /**
605
- * Remove the link for a branch.
606
- * @returns true if a link was removed, false if no link existed
607
- */
608
- unlinkBranch(repo: string, branch: string): Promise<boolean>;
548
+ unlink(repo: RepoInfo, issueNumber: number): Promise<boolean>;
609
549
  /**
610
550
  * Get the branch linked to an issue.
611
551
  */
612
- getBranchForIssue(repo: string, issueNumber: number): Promise<string | null>;
552
+ getBranchForIssue(repo: RepoInfo, issueNumber: number): Promise<string | null>;
613
553
  /**
614
- * Get the full link info for a branch.
554
+ * Check if an issue has a branch link.
615
555
  */
616
- getLinkForBranch(repo: string, branch: string): Promise<BranchLink | null>;
617
- /**
618
- * Get the full link info for an issue.
619
- */
620
- getLinkForIssue(repo: string, issueNumber: number): Promise<BranchLink | null>;
621
- /**
622
- * Get all links for a repository.
623
- */
624
- getLinksForRepo(repo: string): Promise<BranchLink[]>;
625
- /**
626
- * Get all links.
627
- */
628
- getAllLinks(): Promise<BranchLink[]>;
629
- /**
630
- * Check if a branch has a link.
631
- */
632
- hasLinkForBranch(repo: string, branch: string): Promise<boolean>;
633
- /**
634
- * Check if an issue has a link.
635
- */
636
- hasLinkForIssue(repo: string, issueNumber: number): Promise<boolean>;
556
+ hasLink(repo: RepoInfo, issueNumber: number): Promise<boolean>;
637
557
  }
638
- /**
639
- * Create an in-memory storage adapter for testing.
640
- */
641
- declare function createInMemoryAdapter(): StorageAdapter & {
642
- links: BranchLink[];
643
- };
644
558
 
645
559
  /**
646
560
  * Git utility functions for working with local repositories.
@@ -903,4 +817,4 @@ declare namespace queries {
903
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 };
904
818
  }
905
819
 
906
- 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
@@ -960,138 +960,67 @@ var GitHubAPI = class {
960
960
  };
961
961
 
962
962
  // src/branch-linker.ts
963
- var BranchLinker = class {
964
- storage;
965
- constructor(storage) {
966
- this.storage = storage;
967
- }
968
- /**
969
- * Load links from storage (handles both sync and async adapters)
970
- */
971
- async loadLinks() {
972
- const result = this.storage.load();
973
- 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;
974
976
  }
975
- /**
976
- * Save links to storage (handles both sync and async adapters)
977
- */
978
- async saveLinks(links) {
979
- const result = this.storage.save(links);
980
- if (result instanceof Promise) {
981
- await result;
982
- }
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;
983
986
  }
984
987
  /**
985
988
  * Create a link between a branch and an issue.
986
- * 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.
987
990
  */
988
- async link(branch, issueNumber, issueTitle, itemId, repo) {
989
- const links = await this.loadLinks();
990
- const filtered = links.filter(
991
- (l) => !(l.repo === repo && (l.branch === branch || l.issueNumber === issueNumber))
992
- );
993
- filtered.push({
994
- branch,
995
- issueNumber,
996
- issueTitle,
997
- itemId,
998
- repo,
999
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1000
- });
1001
- 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);
1002
996
  }
1003
997
  /**
1004
- * Remove the link for an issue.
1005
- * @returns true if a link was removed, false if no link existed
998
+ * Remove the branch link from an issue.
1006
999
  */
1007
1000
  async unlink(repo, issueNumber) {
1008
- const links = await this.loadLinks();
1009
- const filtered = links.filter(
1010
- (l) => !(l.repo === repo && l.issueNumber === issueNumber)
1011
- );
1012
- if (filtered.length === links.length) {
1013
- return false;
1014
- }
1015
- await this.saveLinks(filtered);
1016
- return true;
1017
- }
1018
- /**
1019
- * Remove the link for a branch.
1020
- * @returns true if a link was removed, false if no link existed
1021
- */
1022
- async unlinkBranch(repo, branch) {
1023
- const links = await this.loadLinks();
1024
- const filtered = links.filter(
1025
- (l) => !(l.repo === repo && l.branch === branch)
1026
- );
1027
- if (filtered.length === links.length) {
1028
- return false;
1029
- }
1030
- await this.saveLinks(filtered);
1031
- 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);
1032
1007
  }
1033
1008
  /**
1034
1009
  * Get the branch linked to an issue.
1035
1010
  */
1036
1011
  async getBranchForIssue(repo, issueNumber) {
1037
- const links = await this.loadLinks();
1038
- const link = links.find((l) => l.repo === repo && l.issueNumber === issueNumber);
1039
- return link?.branch || null;
1040
- }
1041
- /**
1042
- * Get the full link info for a branch.
1043
- */
1044
- async getLinkForBranch(repo, branch) {
1045
- const links = await this.loadLinks();
1046
- return links.find((l) => l.repo === repo && l.branch === branch) || null;
1047
- }
1048
- /**
1049
- * Get the full link info for an issue.
1050
- */
1051
- async getLinkForIssue(repo, issueNumber) {
1052
- const links = await this.loadLinks();
1053
- return links.find((l) => l.repo === repo && l.issueNumber === issueNumber) || null;
1054
- }
1055
- /**
1056
- * Get all links for a repository.
1057
- */
1058
- async getLinksForRepo(repo) {
1059
- const links = await this.loadLinks();
1060
- return links.filter((l) => l.repo === repo);
1061
- }
1062
- /**
1063
- * Get all links.
1064
- */
1065
- async getAllLinks() {
1066
- return this.loadLinks();
1012
+ const details = await this.api.getIssueDetails(repo, issueNumber);
1013
+ if (!details) return null;
1014
+ return parseBranchLink(details.body);
1067
1015
  }
1068
1016
  /**
1069
- * Check if a branch has a link.
1017
+ * Check if an issue has a branch link.
1070
1018
  */
1071
- async hasLinkForBranch(repo, branch) {
1072
- const link = await this.getLinkForBranch(repo, branch);
1073
- return link !== null;
1074
- }
1075
- /**
1076
- * Check if an issue has a link.
1077
- */
1078
- async hasLinkForIssue(repo, issueNumber) {
1079
- const link = await this.getLinkForIssue(repo, issueNumber);
1080
- return link !== null;
1019
+ async hasLink(repo, issueNumber) {
1020
+ const branch = await this.getBranchForIssue(repo, issueNumber);
1021
+ return branch !== null;
1081
1022
  }
1082
1023
  };
1083
- function createInMemoryAdapter() {
1084
- const adapter = {
1085
- links: [],
1086
- load() {
1087
- return [...this.links];
1088
- },
1089
- save(links) {
1090
- this.links = [...links];
1091
- }
1092
- };
1093
- return adapter;
1094
- }
1095
1024
 
1096
1025
  // src/git-utils.ts
1097
1026
  import { exec } from "child_process";
@@ -1278,7 +1207,6 @@ export {
1278
1207
  buildRepoUrl,
1279
1208
  checkoutBranch,
1280
1209
  createBranch,
1281
- createInMemoryAdapter,
1282
1210
  detectRepository,
1283
1211
  fetchOrigin,
1284
1212
  generateBranchName,
@@ -1289,9 +1217,12 @@ export {
1289
1217
  getRepositoryRoot,
1290
1218
  hasUncommittedChanges,
1291
1219
  isGitRepository,
1220
+ parseBranchLink,
1292
1221
  parseGitHubUrl,
1293
1222
  parseIssueUrl,
1294
1223
  pullLatest,
1295
1224
  queries_exports as queries,
1296
- sanitizeForBranchName
1225
+ removeBranchLinkFromBody,
1226
+ sanitizeForBranchName,
1227
+ setBranchLinkInBody
1297
1228
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bretwardjames/ghp-core",
3
- "version": "0.1.2",
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",