@bretwardjames/ghp-core 0.1.7 → 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,123 +1,37 @@
1
- # GHP Tools
1
+ # @bretwardjames/ghp-core
2
2
 
3
- A suite of tools for managing GitHub Projects from your terminal and editor.
3
+ Shared core library for GHP tools - provides GitHub Projects API interactions, types, and utilities.
4
4
 
5
- ## What's Included
5
+ Part of the [GHP monorepo](https://github.com/bretwardjames/ghp).
6
6
 
7
- | Package | Description |
8
- |---------|-------------|
9
- | **[@bretwardjames/ghp-cli](https://github.com/bretwardjames/ghp-cli)** | Command-line interface for GitHub Projects |
10
- | **[vscode-gh-projects](https://github.com/bretwardjames/vscode-gh-projects)** | VS Code / Cursor extension with visual boards |
11
- | **@bretwardjames/ghp-core** | Shared library (this package) |
7
+ ## Installation
12
8
 
13
- Both the CLI and extension share the same underlying library and are designed to work together.
14
-
15
- ## Quick Install
16
-
17
- Install both the CLI and VS Code/Cursor extension with a single command:
18
-
19
- ```bash
20
- curl -fsSL https://raw.githubusercontent.com/bretwardjames/ghp-core/main/install.sh | bash
21
- ```
22
-
23
- This will:
24
- 1. Install the `ghp` CLI globally via npm
25
- 2. Install the VS Code/Cursor extension from the latest release
26
-
27
- ### Manual Installation
28
-
29
- **CLI only:**
30
9
  ```bash
31
- npm install -g @bretwardjames/ghp-cli
10
+ npm install @bretwardjames/ghp-core
32
11
  ```
33
12
 
34
- **Extension only:**
35
- Download the `.vsix` from [releases](https://github.com/bretwardjames/vscode-gh-projects/releases) and install:
36
- ```bash
37
- code --install-extension gh-projects-*.vsix
38
- # or for Cursor:
39
- cursor --install-extension gh-projects-*.vsix
40
- ```
41
-
42
- ## Getting Started
43
-
44
- 1. Authenticate with GitHub:
45
- ```bash
46
- ghp auth
47
- ```
48
-
49
- 2. View your assigned work:
50
- ```bash
51
- ghp work
52
- ```
53
-
54
- 3. Open VS Code/Cursor and find the GitHub Projects panel in the sidebar
55
-
56
- ## Features
57
-
58
- ### Shared Across Both Tools
13
+ ## Usage
59
14
 
60
- - **Branch Linking** - Link branches to issues, track which issues have active work
61
- - **Workflow Automation** - "Start Working" creates branches and updates status
62
- - **Project Board Views** - See your boards exactly as configured on GitHub
63
- - **Issue Templates** - Create issues using your repo's templates
15
+ This package is primarily used internally by:
16
+ - [@bretwardjames/ghp-cli](https://github.com/bretwardjames/ghp/tree/main/packages/cli) - Command-line tool
17
+ - [gh-projects](https://github.com/bretwardjames/ghp/tree/main/apps/vscode) - VS Code extension
64
18
 
65
- ### CLI-Specific
19
+ ## API
66
20
 
67
- - **Shortcuts** - Define named filter combinations (`ghp plan bugs`)
68
- - **Slice Filters** - Filter by any field (`--slice Priority=High`)
69
- - **Workspace Config** - Share settings with your team via `.ghp/config.json`
70
- - **Simple List Output** - Integration with fzf, rofi, and other pickers
21
+ ```typescript
22
+ import { GitHubAPI, parseIssueUrl, BranchLinker } from '@bretwardjames/ghp-core';
71
23
 
72
- ### Extension-Specific
24
+ // Create API client
25
+ const api = new GitHubAPI(token);
73
26
 
74
- - **Drag and Drop** - Move issues between columns visually
75
- - **Planning Board** - Full-screen kanban view
76
- - **Multi-Select** - Bulk operations on multiple items
77
- - **Real-Time Sync** - Stay in sync with GitHub
27
+ // Parse issue URLs
28
+ const { owner, repo, number } = parseIssueUrl('https://github.com/owner/repo/issues/123');
78
29
 
79
- ## Configuration
80
-
81
- Both tools share the same configuration concepts. The CLI uses JSON files, the extension uses VS Code settings.
82
-
83
- ### CLI Configuration (ghp-cli)
84
-
85
- ```bash
86
- # View all settings with their sources
87
- ghp config --show
88
-
89
- # Edit user config (opens $EDITOR)
90
- ghp config
91
-
92
- # Edit workspace config (shared with team)
93
- ghp config -w
94
-
95
- # Set individual value
96
- ghp config mainBranch develop
30
+ // Branch linking
31
+ const linker = new BranchLinker(api);
32
+ await linker.linkBranch(issueNumber, branchName);
97
33
  ```
98
34
 
99
- **Config files:**
100
- - User: `~/.config/ghp-cli/config.json` (personal overrides)
101
- - Workspace: `.ghp/config.json` (committed, shared with team)
102
-
103
- Merge order: defaults -> workspace -> user
104
-
105
- ### Extension Configuration (VS Code)
106
-
107
- Settings are in VS Code's settings UI under "GitHub Projects", or in your workspace `.vscode/settings.json`.
108
-
109
- ## Links
110
-
111
- - [ghp-cli Documentation](https://github.com/bretwardjames/ghp-cli)
112
- - [VS Code Extension Documentation](https://github.com/bretwardjames/vscode-gh-projects)
113
- - [Report Issues](https://github.com/bretwardjames/ghp-core/issues)
114
-
115
- ## Requirements
116
-
117
- - Node.js >= 18
118
- - GitHub account with Projects access
119
- - VS Code 1.85+ or Cursor (for extension)
120
-
121
35
  ## License
122
36
 
123
37
  MIT
package/dist/index.cjs CHANGED
@@ -36,10 +36,12 @@ __export(index_exports, {
36
36
  checkoutBranch: () => checkoutBranch,
37
37
  computeSettingsDiff: () => computeSettingsDiff,
38
38
  createBranch: () => createBranch,
39
+ createWorktree: () => createWorktree,
39
40
  detectRepository: () => detectRepository,
40
41
  fetchOrigin: () => fetchOrigin,
41
42
  formatConflict: () => formatConflict,
42
43
  generateBranchName: () => generateBranchName,
44
+ generateWorktreePath: () => generateWorktreePath,
43
45
  getAllBranches: () => getAllBranches,
44
46
  getCommitsAhead: () => getCommitsAhead,
45
47
  getCommitsBehind: () => getCommitsBehind,
@@ -49,9 +51,11 @@ __export(index_exports, {
49
51
  getLocalBranches: () => getLocalBranches,
50
52
  getRemoteBranches: () => getRemoteBranches,
51
53
  getRepositoryRoot: () => getRepositoryRoot,
54
+ getWorktreeForBranch: () => getWorktreeForBranch,
52
55
  hasDifferences: () => hasDifferences,
53
56
  hasUncommittedChanges: () => hasUncommittedChanges,
54
57
  isGitRepository: () => isGitRepository,
58
+ listWorktrees: () => listWorktrees,
55
59
  normalizeVSCodeSettings: () => normalizeVSCodeSettings,
56
60
  parseBranchLink: () => parseBranchLink,
57
61
  parseGitHubUrl: () => parseGitHubUrl,
@@ -59,6 +63,7 @@ __export(index_exports, {
59
63
  pullLatest: () => pullLatest,
60
64
  queries: () => queries_exports,
61
65
  removeBranchLinkFromBody: () => removeBranchLinkFromBody,
66
+ removeWorktree: () => removeWorktree,
62
67
  resolveConflicts: () => resolveConflicts,
63
68
  sanitizeForBranchName: () => sanitizeForBranchName,
64
69
  setBranchLinkInBody: () => setBranchLinkInBody,
@@ -66,7 +71,8 @@ __export(index_exports, {
66
71
  toVSCodeSettings: () => toVSCodeSettings,
67
72
  useCli: () => useCli,
68
73
  useCustom: () => useCustom,
69
- useVSCode: () => useVSCode
74
+ useVSCode: () => useVSCode,
75
+ worktreeExists: () => worktreeExists
70
76
  });
71
77
  module.exports = __toCommonJS(index_exports);
72
78
 
@@ -405,7 +411,7 @@ var REMOVE_LABELS_MUTATION = `
405
411
  var ISSUES_WITH_LABEL_QUERY = `
406
412
  query($owner: String!, $name: String!, $labels: [String!]) {
407
413
  repository(owner: $owner, name: $name) {
408
- issues(first: 10, labels: $labels, states: [OPEN]) {
414
+ issues(first: 10, labels: $labels, states: [OPEN, CLOSED]) {
409
415
  nodes {
410
416
  number
411
417
  }
@@ -556,8 +562,18 @@ var GitHubAPI = class {
556
562
  owner: repo.owner,
557
563
  name: repo.name
558
564
  });
565
+ if (!response.repository) {
566
+ throw new Error(`Repository not found: ${repo.owner}/${repo.name}`);
567
+ }
559
568
  return response.repository.projectsV2.nodes;
560
569
  } catch (error) {
570
+ if (error && typeof error === "object" && "errors" in error) {
571
+ const gqlError = error;
572
+ const notFound = gqlError.errors?.find((e) => e.type === "NOT_FOUND");
573
+ if (notFound) {
574
+ throw new Error(`Repository not found: ${repo.owner}/${repo.name}`);
575
+ }
576
+ }
561
577
  this.handleAuthError(error);
562
578
  }
563
579
  }
@@ -1148,6 +1164,7 @@ var BranchLinker = class {
1148
1164
  // src/git-utils.ts
1149
1165
  var import_child_process = require("child_process");
1150
1166
  var import_util = require("util");
1167
+ var import_os = require("os");
1151
1168
 
1152
1169
  // src/url-parser.ts
1153
1170
  function parseGitHubUrl(url) {
@@ -1200,6 +1217,28 @@ function buildOrgProjectUrl(org, projectNumber) {
1200
1217
  }
1201
1218
 
1202
1219
  // src/git-utils.ts
1220
+ function sanitizeForPath(input) {
1221
+ return String(input).replace(/\.\./g, "_").replace(/[;&|`$(){}[\]<>!]/g, "").replace(/\s+/g, "-").replace(/[^a-zA-Z0-9_\-./]/g, "_");
1222
+ }
1223
+ function validateBranchName(branch) {
1224
+ if (!branch || branch.trim().length === 0) {
1225
+ throw new Error("Branch name cannot be empty");
1226
+ }
1227
+ const dangerousChars = /[`$\\!;|&<>(){}[\]'"]/;
1228
+ if (dangerousChars.test(branch)) {
1229
+ throw new Error(`Branch name contains invalid characters: ${branch}`);
1230
+ }
1231
+ const gitInvalidChars = /[\s~^:?*\[\\]/;
1232
+ if (gitInvalidChars.test(branch)) {
1233
+ throw new Error(`Branch name contains invalid git characters: ${branch}`);
1234
+ }
1235
+ if (branch.includes("..")) {
1236
+ throw new Error(`Branch name cannot contain '..': ${branch}`);
1237
+ }
1238
+ if (/^[./]|[./]$/.test(branch)) {
1239
+ throw new Error(`Branch name cannot start or end with '/' or '.': ${branch}`);
1240
+ }
1241
+ }
1203
1242
  var execAsync = (0, import_util.promisify)(import_child_process.exec);
1204
1243
  async function execGit(command, options = {}) {
1205
1244
  const cwd = options.cwd || process.cwd();
@@ -1347,6 +1386,84 @@ async function getDefaultBranch(options = {}) {
1347
1386
  }
1348
1387
  return "master";
1349
1388
  }
1389
+ function validatePath(path) {
1390
+ if (!path || path.trim().length === 0) {
1391
+ throw new Error("Path cannot be empty");
1392
+ }
1393
+ const dangerousChars = /[`$;|&<>(){}[\]'"\n\r]/;
1394
+ if (dangerousChars.test(path)) {
1395
+ throw new Error(`Path contains invalid characters: ${path}`);
1396
+ }
1397
+ }
1398
+ async function createWorktree(worktreePath, branch, options = {}) {
1399
+ validateBranchName(branch);
1400
+ validatePath(worktreePath);
1401
+ const localExists = await branchExists(branch, options);
1402
+ if (localExists) {
1403
+ await execGit(`git worktree add "${worktreePath}" "${branch}"`, options);
1404
+ } else {
1405
+ try {
1406
+ await execGit(`git worktree add "${worktreePath}" -b "${branch}" "origin/${branch}"`, options);
1407
+ } catch {
1408
+ await execGit(`git worktree add -b "${branch}" "${worktreePath}"`, options);
1409
+ }
1410
+ }
1411
+ }
1412
+ async function removeWorktree(worktreePath, options = {}, force = false) {
1413
+ validatePath(worktreePath);
1414
+ const forceFlag = force ? "--force" : "";
1415
+ await execGit(`git worktree remove ${forceFlag} "${worktreePath}"`, options);
1416
+ }
1417
+ async function listWorktrees(options = {}) {
1418
+ try {
1419
+ const { stdout } = await execGit("git worktree list --porcelain", options);
1420
+ const worktrees = [];
1421
+ const entries = stdout.trim().split("\n\n");
1422
+ for (const entry of entries) {
1423
+ if (!entry.trim()) continue;
1424
+ const lines = entry.split("\n");
1425
+ const info = {
1426
+ isMain: false,
1427
+ branch: null
1428
+ };
1429
+ for (const line of lines) {
1430
+ if (line.startsWith("worktree ")) {
1431
+ info.path = line.substring(9);
1432
+ } else if (line.startsWith("HEAD ")) {
1433
+ info.head = line.substring(5);
1434
+ } else if (line.startsWith("branch ")) {
1435
+ info.branch = line.substring(7).replace(/^refs\/heads\//, "");
1436
+ } else if (line === "bare") {
1437
+ info.isMain = true;
1438
+ }
1439
+ }
1440
+ if (worktrees.length === 0) {
1441
+ info.isMain = true;
1442
+ }
1443
+ if (info.path && info.head) {
1444
+ worktrees.push(info);
1445
+ }
1446
+ }
1447
+ return worktrees;
1448
+ } catch {
1449
+ return [];
1450
+ }
1451
+ }
1452
+ async function getWorktreeForBranch(branch, options = {}) {
1453
+ const worktrees = await listWorktrees(options);
1454
+ return worktrees.find((wt) => wt.branch === branch) || null;
1455
+ }
1456
+ async function worktreeExists(worktreePath, options = {}) {
1457
+ const worktrees = await listWorktrees(options);
1458
+ return worktrees.some((wt) => wt.path === worktreePath);
1459
+ }
1460
+ function generateWorktreePath(basePath, repoName, identifier) {
1461
+ const safeRepoName = sanitizeForPath(repoName);
1462
+ const safeIdentifier = sanitizeForPath(String(identifier));
1463
+ const expandedBase = basePath.startsWith("~") ? basePath.replace("~", (0, import_os.homedir)()) : basePath;
1464
+ const cleanBase = expandedBase.replace(/\/+$/, "");
1465
+ return `${cleanBase}/${safeRepoName}/${safeIdentifier}`;
1466
+ }
1350
1467
 
1351
1468
  // src/sync.ts
1352
1469
  var SYNCABLE_KEYS = [
@@ -1527,10 +1644,12 @@ function getDiffSummary(diff) {
1527
1644
  checkoutBranch,
1528
1645
  computeSettingsDiff,
1529
1646
  createBranch,
1647
+ createWorktree,
1530
1648
  detectRepository,
1531
1649
  fetchOrigin,
1532
1650
  formatConflict,
1533
1651
  generateBranchName,
1652
+ generateWorktreePath,
1534
1653
  getAllBranches,
1535
1654
  getCommitsAhead,
1536
1655
  getCommitsBehind,
@@ -1540,9 +1659,11 @@ function getDiffSummary(diff) {
1540
1659
  getLocalBranches,
1541
1660
  getRemoteBranches,
1542
1661
  getRepositoryRoot,
1662
+ getWorktreeForBranch,
1543
1663
  hasDifferences,
1544
1664
  hasUncommittedChanges,
1545
1665
  isGitRepository,
1666
+ listWorktrees,
1546
1667
  normalizeVSCodeSettings,
1547
1668
  parseBranchLink,
1548
1669
  parseGitHubUrl,
@@ -1550,6 +1671,7 @@ function getDiffSummary(diff) {
1550
1671
  pullLatest,
1551
1672
  queries,
1552
1673
  removeBranchLinkFromBody,
1674
+ removeWorktree,
1553
1675
  resolveConflicts,
1554
1676
  sanitizeForBranchName,
1555
1677
  setBranchLinkInBody,
@@ -1557,5 +1679,6 @@ function getDiffSummary(diff) {
1557
1679
  toVSCodeSettings,
1558
1680
  useCli,
1559
1681
  useCustom,
1560
- useVSCode
1682
+ useVSCode,
1683
+ worktreeExists
1561
1684
  });
package/dist/index.d.cts CHANGED
@@ -645,6 +645,60 @@ declare function getAllBranches(options?: GitOptions): Promise<string[]>;
645
645
  * Get the default branch name (main or master)
646
646
  */
647
647
  declare function getDefaultBranch(options?: GitOptions): Promise<string>;
648
+ /**
649
+ * Information about a git worktree
650
+ */
651
+ interface WorktreeInfo {
652
+ /** Absolute path to the worktree directory */
653
+ path: string;
654
+ /** Commit SHA the worktree is at */
655
+ head: string;
656
+ /** Branch name (without refs/heads/ prefix), or null if detached */
657
+ branch: string | null;
658
+ /** Whether this is the main worktree (the original repo) */
659
+ isMain: boolean;
660
+ }
661
+ /**
662
+ * Create a new worktree for a branch
663
+ * @param worktreePath - Path where the worktree will be created
664
+ * @param branch - Branch to checkout in the worktree
665
+ * @param options - Git options (cwd determines the source repository)
666
+ */
667
+ declare function createWorktree(worktreePath: string, branch: string, options?: GitOptions): Promise<void>;
668
+ /**
669
+ * Remove a worktree
670
+ * @param worktreePath - Path to the worktree to remove
671
+ * @param options - Git options
672
+ * @param force - Force removal even if worktree has uncommitted changes
673
+ */
674
+ declare function removeWorktree(worktreePath: string, options?: GitOptions, force?: boolean): Promise<void>;
675
+ /**
676
+ * List all worktrees for the repository
677
+ * @param options - Git options
678
+ * @returns Array of worktree information
679
+ */
680
+ declare function listWorktrees(options?: GitOptions): Promise<WorktreeInfo[]>;
681
+ /**
682
+ * Get worktree for a specific branch
683
+ * @param branch - Branch name to find
684
+ * @param options - Git options
685
+ * @returns Worktree info if found, null otherwise
686
+ */
687
+ declare function getWorktreeForBranch(branch: string, options?: GitOptions): Promise<WorktreeInfo | null>;
688
+ /**
689
+ * Check if a worktree exists at the given path
690
+ * @param worktreePath - Path to check
691
+ * @param options - Git options
692
+ */
693
+ declare function worktreeExists(worktreePath: string, options?: GitOptions): Promise<boolean>;
694
+ /**
695
+ * Generate a worktree path based on repo and branch info
696
+ * @param basePath - Base directory for worktrees (e.g., ~/.ghp/worktrees)
697
+ * @param repoName - Repository name
698
+ * @param identifier - Issue number or branch name to use as identifier
699
+ * @returns Full path to the worktree directory
700
+ */
701
+ declare function generateWorktreePath(basePath: string, repoName: string, identifier: string | number): string;
648
702
 
649
703
  /**
650
704
  * URL parsing utilities for GitHub repositories and issues.
@@ -937,7 +991,7 @@ declare const REMOVE_LABELS_MUTATION = "\n mutation($issueId: ID!, $labelIds:
937
991
  /**
938
992
  * Query to find issues with a specific label
939
993
  */
940
- declare const ISSUES_WITH_LABEL_QUERY = "\n query($owner: String!, $name: String!, $labels: [String!]) {\n repository(owner: $owner, name: $name) {\n issues(first: 10, labels: $labels, states: [OPEN]) {\n nodes {\n number\n }\n }\n }\n }\n";
994
+ declare const ISSUES_WITH_LABEL_QUERY = "\n query($owner: String!, $name: String!, $labels: [String!]) {\n repository(owner: $owner, name: $name) {\n issues(first: 10, labels: $labels, states: [OPEN, CLOSED]) {\n nodes {\n number\n }\n }\n }\n }\n";
941
995
  /**
942
996
  * Query to get available issue types for a repository
943
997
  */
@@ -988,4 +1042,4 @@ declare namespace queries {
988
1042
  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 };
989
1043
  }
990
1044
 
991
- export { type AssigneeInfo, type AuthError, BranchLinker, CLI_TO_VSCODE_MAP, type Collaborator, type ConflictChoices, type ConflictResolution, DEFAULT_VALUES, 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 ResolvedSettings, SETTING_DISPLAY_NAMES, SYNCABLE_KEYS, type SettingConflict, type SettingsDiff, type SettingsSource, type SingleSelectFieldValue, type StatusField, type SyncableSettingKey, type SyncableSettings, type TextFieldValue, type TokenProvider, VSCODE_TO_CLI_MAP, branchExists, buildIssueUrl, buildOrgProjectUrl, buildProjectUrl, buildPullRequestUrl, buildRepoUrl, checkoutBranch, computeSettingsDiff, createBranch, detectRepository, fetchOrigin, formatConflict, generateBranchName, getAllBranches, getCommitsAhead, getCommitsBehind, getCurrentBranch, getDefaultBranch, getDiffSummary, getLocalBranches, getRemoteBranches, getRepositoryRoot, hasDifferences, hasUncommittedChanges, isGitRepository, normalizeVSCodeSettings, parseBranchLink, parseGitHubUrl, parseIssueUrl, pullLatest, queries, removeBranchLinkFromBody, resolveConflicts, sanitizeForBranchName, setBranchLinkInBody, skip, toVSCodeSettings, useCli, useCustom, useVSCode };
1045
+ export { type AssigneeInfo, type AuthError, BranchLinker, CLI_TO_VSCODE_MAP, type Collaborator, type ConflictChoices, type ConflictResolution, DEFAULT_VALUES, 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 ResolvedSettings, SETTING_DISPLAY_NAMES, SYNCABLE_KEYS, type SettingConflict, type SettingsDiff, type SettingsSource, type SingleSelectFieldValue, type StatusField, type SyncableSettingKey, type SyncableSettings, type TextFieldValue, type TokenProvider, VSCODE_TO_CLI_MAP, type WorktreeInfo, branchExists, buildIssueUrl, buildOrgProjectUrl, buildProjectUrl, buildPullRequestUrl, buildRepoUrl, checkoutBranch, computeSettingsDiff, createBranch, createWorktree, detectRepository, fetchOrigin, formatConflict, generateBranchName, generateWorktreePath, getAllBranches, getCommitsAhead, getCommitsBehind, getCurrentBranch, getDefaultBranch, getDiffSummary, getLocalBranches, getRemoteBranches, getRepositoryRoot, getWorktreeForBranch, hasDifferences, hasUncommittedChanges, isGitRepository, listWorktrees, normalizeVSCodeSettings, parseBranchLink, parseGitHubUrl, parseIssueUrl, pullLatest, queries, removeBranchLinkFromBody, removeWorktree, resolveConflicts, sanitizeForBranchName, setBranchLinkInBody, skip, toVSCodeSettings, useCli, useCustom, useVSCode, worktreeExists };
package/dist/index.d.ts CHANGED
@@ -645,6 +645,60 @@ declare function getAllBranches(options?: GitOptions): Promise<string[]>;
645
645
  * Get the default branch name (main or master)
646
646
  */
647
647
  declare function getDefaultBranch(options?: GitOptions): Promise<string>;
648
+ /**
649
+ * Information about a git worktree
650
+ */
651
+ interface WorktreeInfo {
652
+ /** Absolute path to the worktree directory */
653
+ path: string;
654
+ /** Commit SHA the worktree is at */
655
+ head: string;
656
+ /** Branch name (without refs/heads/ prefix), or null if detached */
657
+ branch: string | null;
658
+ /** Whether this is the main worktree (the original repo) */
659
+ isMain: boolean;
660
+ }
661
+ /**
662
+ * Create a new worktree for a branch
663
+ * @param worktreePath - Path where the worktree will be created
664
+ * @param branch - Branch to checkout in the worktree
665
+ * @param options - Git options (cwd determines the source repository)
666
+ */
667
+ declare function createWorktree(worktreePath: string, branch: string, options?: GitOptions): Promise<void>;
668
+ /**
669
+ * Remove a worktree
670
+ * @param worktreePath - Path to the worktree to remove
671
+ * @param options - Git options
672
+ * @param force - Force removal even if worktree has uncommitted changes
673
+ */
674
+ declare function removeWorktree(worktreePath: string, options?: GitOptions, force?: boolean): Promise<void>;
675
+ /**
676
+ * List all worktrees for the repository
677
+ * @param options - Git options
678
+ * @returns Array of worktree information
679
+ */
680
+ declare function listWorktrees(options?: GitOptions): Promise<WorktreeInfo[]>;
681
+ /**
682
+ * Get worktree for a specific branch
683
+ * @param branch - Branch name to find
684
+ * @param options - Git options
685
+ * @returns Worktree info if found, null otherwise
686
+ */
687
+ declare function getWorktreeForBranch(branch: string, options?: GitOptions): Promise<WorktreeInfo | null>;
688
+ /**
689
+ * Check if a worktree exists at the given path
690
+ * @param worktreePath - Path to check
691
+ * @param options - Git options
692
+ */
693
+ declare function worktreeExists(worktreePath: string, options?: GitOptions): Promise<boolean>;
694
+ /**
695
+ * Generate a worktree path based on repo and branch info
696
+ * @param basePath - Base directory for worktrees (e.g., ~/.ghp/worktrees)
697
+ * @param repoName - Repository name
698
+ * @param identifier - Issue number or branch name to use as identifier
699
+ * @returns Full path to the worktree directory
700
+ */
701
+ declare function generateWorktreePath(basePath: string, repoName: string, identifier: string | number): string;
648
702
 
649
703
  /**
650
704
  * URL parsing utilities for GitHub repositories and issues.
@@ -937,7 +991,7 @@ declare const REMOVE_LABELS_MUTATION = "\n mutation($issueId: ID!, $labelIds:
937
991
  /**
938
992
  * Query to find issues with a specific label
939
993
  */
940
- declare const ISSUES_WITH_LABEL_QUERY = "\n query($owner: String!, $name: String!, $labels: [String!]) {\n repository(owner: $owner, name: $name) {\n issues(first: 10, labels: $labels, states: [OPEN]) {\n nodes {\n number\n }\n }\n }\n }\n";
994
+ declare const ISSUES_WITH_LABEL_QUERY = "\n query($owner: String!, $name: String!, $labels: [String!]) {\n repository(owner: $owner, name: $name) {\n issues(first: 10, labels: $labels, states: [OPEN, CLOSED]) {\n nodes {\n number\n }\n }\n }\n }\n";
941
995
  /**
942
996
  * Query to get available issue types for a repository
943
997
  */
@@ -988,4 +1042,4 @@ declare namespace queries {
988
1042
  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 };
989
1043
  }
990
1044
 
991
- export { type AssigneeInfo, type AuthError, BranchLinker, CLI_TO_VSCODE_MAP, type Collaborator, type ConflictChoices, type ConflictResolution, DEFAULT_VALUES, 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 ResolvedSettings, SETTING_DISPLAY_NAMES, SYNCABLE_KEYS, type SettingConflict, type SettingsDiff, type SettingsSource, type SingleSelectFieldValue, type StatusField, type SyncableSettingKey, type SyncableSettings, type TextFieldValue, type TokenProvider, VSCODE_TO_CLI_MAP, branchExists, buildIssueUrl, buildOrgProjectUrl, buildProjectUrl, buildPullRequestUrl, buildRepoUrl, checkoutBranch, computeSettingsDiff, createBranch, detectRepository, fetchOrigin, formatConflict, generateBranchName, getAllBranches, getCommitsAhead, getCommitsBehind, getCurrentBranch, getDefaultBranch, getDiffSummary, getLocalBranches, getRemoteBranches, getRepositoryRoot, hasDifferences, hasUncommittedChanges, isGitRepository, normalizeVSCodeSettings, parseBranchLink, parseGitHubUrl, parseIssueUrl, pullLatest, queries, removeBranchLinkFromBody, resolveConflicts, sanitizeForBranchName, setBranchLinkInBody, skip, toVSCodeSettings, useCli, useCustom, useVSCode };
1045
+ export { type AssigneeInfo, type AuthError, BranchLinker, CLI_TO_VSCODE_MAP, type Collaborator, type ConflictChoices, type ConflictResolution, DEFAULT_VALUES, 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 ResolvedSettings, SETTING_DISPLAY_NAMES, SYNCABLE_KEYS, type SettingConflict, type SettingsDiff, type SettingsSource, type SingleSelectFieldValue, type StatusField, type SyncableSettingKey, type SyncableSettings, type TextFieldValue, type TokenProvider, VSCODE_TO_CLI_MAP, type WorktreeInfo, branchExists, buildIssueUrl, buildOrgProjectUrl, buildProjectUrl, buildPullRequestUrl, buildRepoUrl, checkoutBranch, computeSettingsDiff, createBranch, createWorktree, detectRepository, fetchOrigin, formatConflict, generateBranchName, generateWorktreePath, getAllBranches, getCommitsAhead, getCommitsBehind, getCurrentBranch, getDefaultBranch, getDiffSummary, getLocalBranches, getRemoteBranches, getRepositoryRoot, getWorktreeForBranch, hasDifferences, hasUncommittedChanges, isGitRepository, listWorktrees, normalizeVSCodeSettings, parseBranchLink, parseGitHubUrl, parseIssueUrl, pullLatest, queries, removeBranchLinkFromBody, removeWorktree, resolveConflicts, sanitizeForBranchName, setBranchLinkInBody, skip, toVSCodeSettings, useCli, useCustom, useVSCode, worktreeExists };
package/dist/index.js CHANGED
@@ -339,7 +339,7 @@ var REMOVE_LABELS_MUTATION = `
339
339
  var ISSUES_WITH_LABEL_QUERY = `
340
340
  query($owner: String!, $name: String!, $labels: [String!]) {
341
341
  repository(owner: $owner, name: $name) {
342
- issues(first: 10, labels: $labels, states: [OPEN]) {
342
+ issues(first: 10, labels: $labels, states: [OPEN, CLOSED]) {
343
343
  nodes {
344
344
  number
345
345
  }
@@ -490,8 +490,18 @@ var GitHubAPI = class {
490
490
  owner: repo.owner,
491
491
  name: repo.name
492
492
  });
493
+ if (!response.repository) {
494
+ throw new Error(`Repository not found: ${repo.owner}/${repo.name}`);
495
+ }
493
496
  return response.repository.projectsV2.nodes;
494
497
  } catch (error) {
498
+ if (error && typeof error === "object" && "errors" in error) {
499
+ const gqlError = error;
500
+ const notFound = gqlError.errors?.find((e) => e.type === "NOT_FOUND");
501
+ if (notFound) {
502
+ throw new Error(`Repository not found: ${repo.owner}/${repo.name}`);
503
+ }
504
+ }
495
505
  this.handleAuthError(error);
496
506
  }
497
507
  }
@@ -1082,6 +1092,7 @@ var BranchLinker = class {
1082
1092
  // src/git-utils.ts
1083
1093
  import { exec } from "child_process";
1084
1094
  import { promisify } from "util";
1095
+ import { homedir } from "os";
1085
1096
 
1086
1097
  // src/url-parser.ts
1087
1098
  function parseGitHubUrl(url) {
@@ -1134,6 +1145,28 @@ function buildOrgProjectUrl(org, projectNumber) {
1134
1145
  }
1135
1146
 
1136
1147
  // src/git-utils.ts
1148
+ function sanitizeForPath(input) {
1149
+ return String(input).replace(/\.\./g, "_").replace(/[;&|`$(){}[\]<>!]/g, "").replace(/\s+/g, "-").replace(/[^a-zA-Z0-9_\-./]/g, "_");
1150
+ }
1151
+ function validateBranchName(branch) {
1152
+ if (!branch || branch.trim().length === 0) {
1153
+ throw new Error("Branch name cannot be empty");
1154
+ }
1155
+ const dangerousChars = /[`$\\!;|&<>(){}[\]'"]/;
1156
+ if (dangerousChars.test(branch)) {
1157
+ throw new Error(`Branch name contains invalid characters: ${branch}`);
1158
+ }
1159
+ const gitInvalidChars = /[\s~^:?*\[\\]/;
1160
+ if (gitInvalidChars.test(branch)) {
1161
+ throw new Error(`Branch name contains invalid git characters: ${branch}`);
1162
+ }
1163
+ if (branch.includes("..")) {
1164
+ throw new Error(`Branch name cannot contain '..': ${branch}`);
1165
+ }
1166
+ if (/^[./]|[./]$/.test(branch)) {
1167
+ throw new Error(`Branch name cannot start or end with '/' or '.': ${branch}`);
1168
+ }
1169
+ }
1137
1170
  var execAsync = promisify(exec);
1138
1171
  async function execGit(command, options = {}) {
1139
1172
  const cwd = options.cwd || process.cwd();
@@ -1281,6 +1314,84 @@ async function getDefaultBranch(options = {}) {
1281
1314
  }
1282
1315
  return "master";
1283
1316
  }
1317
+ function validatePath(path) {
1318
+ if (!path || path.trim().length === 0) {
1319
+ throw new Error("Path cannot be empty");
1320
+ }
1321
+ const dangerousChars = /[`$;|&<>(){}[\]'"\n\r]/;
1322
+ if (dangerousChars.test(path)) {
1323
+ throw new Error(`Path contains invalid characters: ${path}`);
1324
+ }
1325
+ }
1326
+ async function createWorktree(worktreePath, branch, options = {}) {
1327
+ validateBranchName(branch);
1328
+ validatePath(worktreePath);
1329
+ const localExists = await branchExists(branch, options);
1330
+ if (localExists) {
1331
+ await execGit(`git worktree add "${worktreePath}" "${branch}"`, options);
1332
+ } else {
1333
+ try {
1334
+ await execGit(`git worktree add "${worktreePath}" -b "${branch}" "origin/${branch}"`, options);
1335
+ } catch {
1336
+ await execGit(`git worktree add -b "${branch}" "${worktreePath}"`, options);
1337
+ }
1338
+ }
1339
+ }
1340
+ async function removeWorktree(worktreePath, options = {}, force = false) {
1341
+ validatePath(worktreePath);
1342
+ const forceFlag = force ? "--force" : "";
1343
+ await execGit(`git worktree remove ${forceFlag} "${worktreePath}"`, options);
1344
+ }
1345
+ async function listWorktrees(options = {}) {
1346
+ try {
1347
+ const { stdout } = await execGit("git worktree list --porcelain", options);
1348
+ const worktrees = [];
1349
+ const entries = stdout.trim().split("\n\n");
1350
+ for (const entry of entries) {
1351
+ if (!entry.trim()) continue;
1352
+ const lines = entry.split("\n");
1353
+ const info = {
1354
+ isMain: false,
1355
+ branch: null
1356
+ };
1357
+ for (const line of lines) {
1358
+ if (line.startsWith("worktree ")) {
1359
+ info.path = line.substring(9);
1360
+ } else if (line.startsWith("HEAD ")) {
1361
+ info.head = line.substring(5);
1362
+ } else if (line.startsWith("branch ")) {
1363
+ info.branch = line.substring(7).replace(/^refs\/heads\//, "");
1364
+ } else if (line === "bare") {
1365
+ info.isMain = true;
1366
+ }
1367
+ }
1368
+ if (worktrees.length === 0) {
1369
+ info.isMain = true;
1370
+ }
1371
+ if (info.path && info.head) {
1372
+ worktrees.push(info);
1373
+ }
1374
+ }
1375
+ return worktrees;
1376
+ } catch {
1377
+ return [];
1378
+ }
1379
+ }
1380
+ async function getWorktreeForBranch(branch, options = {}) {
1381
+ const worktrees = await listWorktrees(options);
1382
+ return worktrees.find((wt) => wt.branch === branch) || null;
1383
+ }
1384
+ async function worktreeExists(worktreePath, options = {}) {
1385
+ const worktrees = await listWorktrees(options);
1386
+ return worktrees.some((wt) => wt.path === worktreePath);
1387
+ }
1388
+ function generateWorktreePath(basePath, repoName, identifier) {
1389
+ const safeRepoName = sanitizeForPath(repoName);
1390
+ const safeIdentifier = sanitizeForPath(String(identifier));
1391
+ const expandedBase = basePath.startsWith("~") ? basePath.replace("~", homedir()) : basePath;
1392
+ const cleanBase = expandedBase.replace(/\/+$/, "");
1393
+ return `${cleanBase}/${safeRepoName}/${safeIdentifier}`;
1394
+ }
1284
1395
 
1285
1396
  // src/sync.ts
1286
1397
  var SYNCABLE_KEYS = [
@@ -1460,10 +1571,12 @@ export {
1460
1571
  checkoutBranch,
1461
1572
  computeSettingsDiff,
1462
1573
  createBranch,
1574
+ createWorktree,
1463
1575
  detectRepository,
1464
1576
  fetchOrigin,
1465
1577
  formatConflict,
1466
1578
  generateBranchName,
1579
+ generateWorktreePath,
1467
1580
  getAllBranches,
1468
1581
  getCommitsAhead,
1469
1582
  getCommitsBehind,
@@ -1473,9 +1586,11 @@ export {
1473
1586
  getLocalBranches,
1474
1587
  getRemoteBranches,
1475
1588
  getRepositoryRoot,
1589
+ getWorktreeForBranch,
1476
1590
  hasDifferences,
1477
1591
  hasUncommittedChanges,
1478
1592
  isGitRepository,
1593
+ listWorktrees,
1479
1594
  normalizeVSCodeSettings,
1480
1595
  parseBranchLink,
1481
1596
  parseGitHubUrl,
@@ -1483,6 +1598,7 @@ export {
1483
1598
  pullLatest,
1484
1599
  queries_exports as queries,
1485
1600
  removeBranchLinkFromBody,
1601
+ removeWorktree,
1486
1602
  resolveConflicts,
1487
1603
  sanitizeForBranchName,
1488
1604
  setBranchLinkInBody,
@@ -1490,5 +1606,6 @@ export {
1490
1606
  toVSCodeSettings,
1491
1607
  useCli,
1492
1608
  useCustom,
1493
- useVSCode
1609
+ useVSCode,
1610
+ worktreeExists
1494
1611
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bretwardjames/ghp-core",
3
- "version": "0.1.7",
3
+ "version": "0.2.0-beta.1",
4
4
  "description": "Shared core library for GitHub Projects tools",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -16,12 +16,6 @@
16
16
  "files": [
17
17
  "dist"
18
18
  ],
19
- "scripts": {
20
- "build": "tsup src/index.ts --format cjs,esm --dts",
21
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
- "typecheck": "tsc --noEmit",
23
- "prepublishOnly": "npm run build"
24
- },
25
19
  "keywords": [
26
20
  "github",
27
21
  "projects",
@@ -44,5 +38,10 @@
44
38
  "@types/node": "^20.10.0",
45
39
  "tsup": "^8.0.0",
46
40
  "typescript": "^5.3.2"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup src/index.ts --format cjs,esm --dts",
44
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
45
+ "typecheck": "tsc --noEmit"
47
46
  }
48
- }
47
+ }