@hyperdrive.bot/cli 1.0.13 → 1.0.17

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.
Files changed (157) hide show
  1. package/README.md +4526 -780
  2. package/dist/commands/deploy.d.ts +18 -0
  3. package/dist/commands/deploy.js +239 -0
  4. package/dist/commands/deployment/create.js +10 -2
  5. package/dist/commands/domain/{switch.d.ts → set-production.d.ts} +1 -1
  6. package/dist/commands/domain/set-production.js +27 -0
  7. package/dist/commands/git/list-open-prs.d.ts +12 -0
  8. package/dist/commands/git/list-open-prs.js +87 -0
  9. package/dist/commands/hook/add.d.ts +22 -0
  10. package/dist/commands/hook/add.js +299 -0
  11. package/dist/commands/hook/list.d.ts +11 -0
  12. package/dist/commands/hook/list.js +111 -0
  13. package/dist/commands/hook/logs.d.ts +13 -0
  14. package/dist/commands/hook/logs.js +124 -0
  15. package/dist/commands/hook/remove.d.ts +12 -0
  16. package/dist/commands/hook/remove.js +115 -0
  17. package/dist/commands/hook/toggle.d.ts +12 -0
  18. package/dist/commands/hook/toggle.js +125 -0
  19. package/dist/commands/init.d.ts +1 -1
  20. package/dist/commands/init.js +49 -9
  21. package/dist/commands/module/bindings.d.ts +14 -0
  22. package/dist/commands/module/bindings.js +125 -0
  23. package/dist/commands/module/create.d.ts +3 -0
  24. package/dist/commands/module/create.js +156 -78
  25. package/dist/commands/module/list.d.ts +1 -0
  26. package/dist/commands/module/list.js +22 -1
  27. package/dist/commands/module/sync.d.ts +29 -0
  28. package/dist/commands/module/sync.js +409 -0
  29. package/dist/commands/module/unlink.d.ts +11 -0
  30. package/dist/commands/module/unlink.js +77 -0
  31. package/dist/commands/module/update.d.ts +10 -0
  32. package/dist/commands/module/update.js +168 -5
  33. package/dist/commands/network/discover.d.ts +12 -0
  34. package/dist/commands/network/discover.js +210 -0
  35. package/dist/commands/network/get.d.ts +13 -0
  36. package/dist/commands/network/get.js +90 -0
  37. package/dist/commands/{auth/logout.d.ts → network/list.d.ts} +2 -9
  38. package/dist/commands/network/list.js +71 -0
  39. package/dist/commands/network/register.d.ts +16 -0
  40. package/dist/commands/network/register.js +144 -0
  41. package/dist/commands/parameter/sync.d.ts +13 -0
  42. package/dist/commands/parameter/sync.js +69 -1
  43. package/dist/commands/project/sync.d.ts +5 -11
  44. package/dist/commands/project/sync.js +12 -381
  45. package/dist/commands/seed.d.ts +93 -0
  46. package/dist/commands/seed.js +324 -0
  47. package/dist/commands/service/backup.d.ts +17 -0
  48. package/dist/commands/service/backup.js +156 -0
  49. package/dist/commands/service/backups.d.ts +14 -0
  50. package/dist/commands/service/backups.js +110 -0
  51. package/dist/commands/service/bind.d.ts +16 -0
  52. package/dist/commands/service/bind.js +106 -0
  53. package/dist/commands/service/bindings.d.ts +13 -0
  54. package/dist/commands/service/bindings.js +78 -0
  55. package/dist/commands/service/clone.d.ts +19 -0
  56. package/dist/commands/service/clone.js +153 -0
  57. package/dist/commands/service/create.d.ts +16 -0
  58. package/dist/commands/service/create.js +212 -0
  59. package/dist/commands/service/get.d.ts +13 -0
  60. package/dist/commands/service/get.js +97 -0
  61. package/dist/commands/service/list.d.ts +12 -0
  62. package/dist/commands/service/list.js +86 -0
  63. package/dist/commands/service/register.d.ts +21 -0
  64. package/dist/commands/service/register.js +215 -0
  65. package/dist/commands/service/restore.d.ts +19 -0
  66. package/dist/commands/service/restore.js +158 -0
  67. package/dist/commands/service/seed.d.ts +17 -0
  68. package/dist/commands/service/seed.js +173 -0
  69. package/dist/commands/service/templates.d.ts +10 -0
  70. package/dist/commands/service/templates.js +66 -0
  71. package/dist/commands/service/unbind.d.ts +15 -0
  72. package/dist/commands/service/unbind.js +74 -0
  73. package/dist/commands/stage/create.d.ts +23 -0
  74. package/dist/commands/stage/create.js +145 -6
  75. package/dist/commands/stage/delete.d.ts +11 -0
  76. package/dist/commands/stage/delete.js +85 -0
  77. package/dist/commands/stage/deploy.d.ts +34 -0
  78. package/dist/commands/stage/deploy.js +294 -0
  79. package/dist/commands/stage/ensure-branches.d.ts +23 -0
  80. package/dist/commands/stage/ensure-branches.js +101 -0
  81. package/dist/commands/stage/list.js +4 -0
  82. package/dist/commands/stage/status.d.ts +14 -0
  83. package/dist/commands/stage/status.js +100 -0
  84. package/dist/commands/{jira → tracker}/connect.js +32 -23
  85. package/dist/commands/tracker/hook/add.d.ts +25 -0
  86. package/dist/commands/tracker/hook/add.js +284 -0
  87. package/dist/commands/{jira → tracker}/hook/list.js +20 -11
  88. package/dist/commands/{jira/hook/add.d.ts → tracker/hook/logs.d.ts} +2 -3
  89. package/dist/commands/tracker/hook/logs.js +126 -0
  90. package/dist/commands/{jira → tracker}/hook/remove.js +9 -8
  91. package/dist/commands/{jira → tracker}/hook/toggle.js +14 -12
  92. package/dist/commands/tracker/project/init.d.ts +17 -0
  93. package/dist/commands/tracker/project/init.js +178 -0
  94. package/dist/commands/tracker/project/link-module.d.ts +17 -0
  95. package/dist/commands/tracker/project/link-module.js +287 -0
  96. package/dist/commands/tracker/project/list-modules.d.ts +11 -0
  97. package/dist/commands/tracker/project/list-modules.js +117 -0
  98. package/dist/commands/tracker/project/list.d.ts +10 -0
  99. package/dist/commands/tracker/project/list.js +90 -0
  100. package/dist/commands/tracker/project/status.d.ts +13 -0
  101. package/dist/commands/tracker/project/status.js +168 -0
  102. package/dist/commands/tracker/project/unlink-module.d.ts +13 -0
  103. package/dist/commands/tracker/project/unlink-module.js +251 -0
  104. package/dist/commands/{jira → tracker}/status.js +3 -3
  105. package/dist/lib/ensure-branches.d.ts +53 -0
  106. package/dist/lib/ensure-branches.js +149 -0
  107. package/dist/lib/git-providers/github.d.ts +16 -0
  108. package/dist/lib/git-providers/github.js +157 -0
  109. package/dist/lib/git-providers/gitlab.d.ts +16 -0
  110. package/dist/lib/git-providers/gitlab.js +148 -0
  111. package/dist/lib/git-providers/index.d.ts +67 -0
  112. package/dist/lib/git-providers/index.js +39 -0
  113. package/dist/lib/lambda-warmer.d.ts +106 -0
  114. package/dist/lib/lambda-warmer.js +189 -0
  115. package/dist/services/hyperdrive-sigv4.d.ts +359 -5
  116. package/dist/services/hyperdrive-sigv4.js +177 -12
  117. package/dist/utils/hook-flow.d.ts +60 -3
  118. package/dist/utils/hook-flow.js +437 -2
  119. package/dist/utils/hook-normalize.d.ts +6 -0
  120. package/dist/utils/hook-normalize.js +33 -0
  121. package/dist/utils/lifecycle-poller.d.ts +32 -0
  122. package/dist/utils/lifecycle-poller.js +72 -0
  123. package/dist/utils/retry.d.ts +43 -0
  124. package/dist/utils/retry.js +88 -0
  125. package/dist/utils/summary-display.js +1 -1
  126. package/dist/utils/tracker-project-flow.d.ts +84 -0
  127. package/dist/utils/tracker-project-flow.js +564 -0
  128. package/package.json +41 -13
  129. package/dist/commands/auth/login.d.ts +0 -16
  130. package/dist/commands/auth/login.js +0 -179
  131. package/dist/commands/auth/logout.js +0 -116
  132. package/dist/commands/auth/refresh.d.ts +0 -6
  133. package/dist/commands/auth/refresh.js +0 -66
  134. package/dist/commands/auth/status.d.ts +0 -6
  135. package/dist/commands/auth/status.js +0 -63
  136. package/dist/commands/config/get.d.ts +0 -9
  137. package/dist/commands/config/get.js +0 -37
  138. package/dist/commands/config/set.d.ts +0 -10
  139. package/dist/commands/config/set.js +0 -48
  140. package/dist/commands/config/show.d.ts +0 -6
  141. package/dist/commands/config/show.js +0 -10
  142. package/dist/commands/domain/current.d.ts +0 -6
  143. package/dist/commands/domain/current.js +0 -18
  144. package/dist/commands/domain/list.d.ts +0 -6
  145. package/dist/commands/domain/list.js +0 -42
  146. package/dist/commands/domain/switch.js +0 -40
  147. package/dist/commands/jira/hook/add.js +0 -147
  148. package/dist/services/tenant-service.d.ts +0 -127
  149. package/dist/services/tenant-service.js +0 -396
  150. package/dist/utils/auth-flow.d.ts +0 -147
  151. package/dist/utils/auth-flow.js +0 -479
  152. package/oclif.manifest.json +0 -3519
  153. /package/dist/commands/{jira → tracker}/connect.d.ts +0 -0
  154. /package/dist/commands/{jira → tracker}/hook/list.d.ts +0 -0
  155. /package/dist/commands/{jira → tracker}/hook/remove.d.ts +0 -0
  156. /package/dist/commands/{jira → tracker}/hook/toggle.d.ts +0 -0
  157. /package/dist/commands/{jira → tracker}/status.d.ts +0 -0
@@ -33,7 +33,7 @@ export default class JiraStatus extends Command {
33
33
  // Fetch status
34
34
  spinner.start('Fetching Jira connection status...');
35
35
  try {
36
- const response = await apiService['makeSignedRequest']('GET', '/hyperdrive/jira/status');
36
+ const response = await apiService.jiraStatus();
37
37
  spinner.succeed('Status retrieved');
38
38
  // Display summary
39
39
  this.log('');
@@ -76,7 +76,7 @@ export default class JiraStatus extends Command {
76
76
  }
77
77
  else {
78
78
  this.log(chalk.yellow('⚠️ Action Required: Install the Forge app from the Atlassian Marketplace'));
79
- this.log(chalk.dim(` Run: ${chalk.cyan('hd jira connect')} to see installation instructions`));
79
+ this.log(chalk.dim(` Run: ${chalk.cyan('hd tracker connect')} to see installation instructions`));
80
80
  }
81
81
  this.log('');
82
82
  }
@@ -85,7 +85,7 @@ export default class JiraStatus extends Command {
85
85
  this.log(chalk.yellow('No Jira connections found'));
86
86
  this.log('');
87
87
  this.log(chalk.dim('To connect a Jira instance, run:'));
88
- this.log(chalk.dim(` ${chalk.cyan('hd jira connect')}`));
88
+ this.log(chalk.dim(` ${chalk.cyan('hd tracker connect')}`));
89
89
  this.log('');
90
90
  }
91
91
  // Success message if everything is connected
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared core logic for `hd stage ensure-branches`.
3
+ *
4
+ * Given a set of linked projects and a target branch name, this function
5
+ * iterates each project and guarantees the branch exists on its remote,
6
+ * creating it from the remote's current default branch when missing. It is
7
+ * strictly idempotent: detection always runs before mutation, so re-invoking
8
+ * with no missing branches makes zero API mutation calls.
9
+ */
10
+ import { type GitProvider } from './git-providers/index.js';
11
+ export type ProjectStatus = 'created' | 'exists' | 'failed' | 'skipped-no-remote';
12
+ export interface LinkedProjectInput {
13
+ /** Project slug (used for display/reporting). */
14
+ slug?: string;
15
+ /** Provider identifier — only `'gitlab'` is supported today. */
16
+ gitProvider?: string;
17
+ /** Full repo path, e.g. `dev_squad/repo/web-apps/sign` (no host, no `.git`). */
18
+ gitFullRepoPath?: string;
19
+ }
20
+ export interface EnsureBranchesProjectResult {
21
+ slug: string;
22
+ remoteUrl: string;
23
+ status: ProjectStatus;
24
+ defaultBranch?: string;
25
+ error?: string;
26
+ }
27
+ export interface EnsureBranchesResult {
28
+ total: number;
29
+ created: number;
30
+ alreadyExisted: number;
31
+ failed: number;
32
+ projects: EnsureBranchesProjectResult[];
33
+ }
34
+ export interface EnsureBranchesOptions {
35
+ stageName: string;
36
+ branchName: string;
37
+ linkedProjects: LinkedProjectInput[];
38
+ verbose?: boolean;
39
+ dryRun?: boolean;
40
+ logger: (msg: string) => void;
41
+ /**
42
+ * Optional provider override — primarily for tests. When omitted, the
43
+ * provider is resolved from each project's remote URL via the factory.
44
+ */
45
+ providerFactory?: (remoteUrl: string) => GitProvider;
46
+ }
47
+ /**
48
+ * Ensure the stage branch exists on every linked project's remote.
49
+ *
50
+ * Never throws — all errors are collected into `result.projects[i].error` so
51
+ * the caller can decide the final exit code based on `result.failed`.
52
+ */
53
+ export declare function ensureBranches(opts: EnsureBranchesOptions): Promise<EnsureBranchesResult>;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Shared core logic for `hd stage ensure-branches`.
3
+ *
4
+ * Given a set of linked projects and a target branch name, this function
5
+ * iterates each project and guarantees the branch exists on its remote,
6
+ * creating it from the remote's current default branch when missing. It is
7
+ * strictly idempotent: detection always runs before mutation, so re-invoking
8
+ * with no missing branches makes zero API mutation calls.
9
+ */
10
+ import { getProviderForRemote } from './git-providers/index.js';
11
+ /**
12
+ * Ensure the stage branch exists on every linked project's remote.
13
+ *
14
+ * Never throws — all errors are collected into `result.projects[i].error` so
15
+ * the caller can decide the final exit code based on `result.failed`.
16
+ */
17
+ export async function ensureBranches(opts) {
18
+ const { stageName, branchName, linkedProjects, verbose, dryRun, logger } = opts;
19
+ const resolveProvider = opts.providerFactory ?? getProviderForRemote;
20
+ const projects = [];
21
+ let created = 0;
22
+ let alreadyExisted = 0;
23
+ let failed = 0;
24
+ for (const project of linkedProjects) {
25
+ const slug = project.slug ?? '<unknown>';
26
+ if (!project.gitFullRepoPath) {
27
+ if (verbose) {
28
+ logger(`${slug}: skipped — no git remote configured`);
29
+ }
30
+ projects.push({ slug, remoteUrl: '', status: 'skipped-no-remote' });
31
+ continue;
32
+ }
33
+ const providerName = project.gitProvider ?? 'gitlab';
34
+ const remoteUrl = `https://${providerName}.com/${project.gitFullRepoPath}.git`;
35
+ let provider;
36
+ try {
37
+ provider = resolveProvider(remoteUrl);
38
+ }
39
+ catch (error) {
40
+ const message = error.message;
41
+ failed += 1;
42
+ projects.push({ slug, remoteUrl, status: 'failed', error: message });
43
+ if (verbose) {
44
+ logger(`${slug}: FAILED — ${message}`);
45
+ }
46
+ else {
47
+ logger(`⚠️ ${slug} (${remoteUrl}): ${message}`);
48
+ }
49
+ continue;
50
+ }
51
+ let defaultBranch;
52
+ try {
53
+ defaultBranch = await provider.detectDefaultBranch({ remoteUrl });
54
+ }
55
+ catch (error) {
56
+ const message = error.message;
57
+ failed += 1;
58
+ projects.push({ slug, remoteUrl, status: 'failed', error: message });
59
+ if (verbose) {
60
+ logger(`${slug}: FAILED — ${message}`);
61
+ }
62
+ else {
63
+ logger(`⚠️ ${slug} (${remoteUrl}): ${message}`);
64
+ }
65
+ continue;
66
+ }
67
+ // Idempotency check — never mutate if the branch already exists.
68
+ let stageBranchCheck;
69
+ try {
70
+ stageBranchCheck = await provider.getBranch({ remoteUrl, branchName });
71
+ }
72
+ catch (error) {
73
+ const message = error.message;
74
+ failed += 1;
75
+ projects.push({ slug, remoteUrl, status: 'failed', error: message, defaultBranch });
76
+ if (verbose) {
77
+ logger(`${slug}: FAILED — ${message}`);
78
+ }
79
+ else {
80
+ logger(`⚠️ ${slug} (${remoteUrl}): ${message}`);
81
+ }
82
+ continue;
83
+ }
84
+ if (stageBranchCheck.exists) {
85
+ alreadyExisted += 1;
86
+ projects.push({ slug, remoteUrl, status: 'exists', defaultBranch });
87
+ if (verbose) {
88
+ logger(`${slug}: branch exists, skipping`);
89
+ }
90
+ continue;
91
+ }
92
+ if (dryRun) {
93
+ created += 1;
94
+ projects.push({ slug, remoteUrl, status: 'created', defaultBranch });
95
+ logger(`[dry-run] ${slug}: would create ${branchName} from ${defaultBranch}`);
96
+ continue;
97
+ }
98
+ // Resolve the default branch SHA before creating.
99
+ let defaultBranchSha;
100
+ try {
101
+ const defaultBranchCheck = await provider.getBranch({ remoteUrl, branchName: defaultBranch });
102
+ if (!defaultBranchCheck.exists || !defaultBranchCheck.sha) {
103
+ throw new Error(`default branch ${defaultBranch} not found on remote`);
104
+ }
105
+ defaultBranchSha = defaultBranchCheck.sha;
106
+ }
107
+ catch (error) {
108
+ const message = error.message;
109
+ failed += 1;
110
+ projects.push({ slug, remoteUrl, status: 'failed', error: message, defaultBranch });
111
+ if (verbose) {
112
+ logger(`${slug}: FAILED — ${message}`);
113
+ }
114
+ else {
115
+ logger(`⚠️ ${slug} (${remoteUrl}): ${message}`);
116
+ }
117
+ continue;
118
+ }
119
+ try {
120
+ await provider.createBranch({ remoteUrl, branchName, fromSha: defaultBranchSha });
121
+ created += 1;
122
+ projects.push({ slug, remoteUrl, status: 'created', defaultBranch });
123
+ if (verbose) {
124
+ logger(`${slug}: created ${branchName} from ${defaultBranch}`);
125
+ }
126
+ }
127
+ catch (error) {
128
+ const message = error.message;
129
+ failed += 1;
130
+ projects.push({ slug, remoteUrl, status: 'failed', error: message, defaultBranch });
131
+ if (verbose) {
132
+ logger(`${slug}: FAILED — ${message}`);
133
+ }
134
+ else {
135
+ logger(`⚠️ ${slug} (${remoteUrl}): ${message}`);
136
+ }
137
+ }
138
+ }
139
+ // Suppress unused-var warning in build — `stageName` is kept on the options
140
+ // interface for future logging/telemetry, even if unused in the loop body.
141
+ void stageName;
142
+ return {
143
+ total: linkedProjects.length,
144
+ created,
145
+ alreadyExisted,
146
+ failed,
147
+ projects,
148
+ };
149
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * GitHub git provider — detects and creates branches, lists open PRs on github.com remotes.
3
+ *
4
+ * Detection (`detectDefaultBranch`, `getBranch`) shells out to `git ls-remote`
5
+ * and works with whatever SSH/https credentials git already has.
6
+ *
7
+ * Mutation (`createBranch`) and reads (`listOpenPullRequests`) call the GitHub
8
+ * REST API and require a personal access token in `GITHUB_TOKEN` with `repo` scope.
9
+ */
10
+ import type { CreateBranchOptions, CreateBranchResult, DetectDefaultBranchOptions, GetBranchOptions, GetBranchResult, GitProvider, ListOpenPullRequestsOptions, OpenPullRequest } from './index.js';
11
+ export declare class GitHubProvider implements GitProvider {
12
+ detectDefaultBranch({ remoteUrl }: DetectDefaultBranchOptions): Promise<string>;
13
+ getBranch({ remoteUrl, branchName }: GetBranchOptions): Promise<GetBranchResult>;
14
+ createBranch({ remoteUrl, branchName, fromSha, }: CreateBranchOptions): Promise<CreateBranchResult>;
15
+ listOpenPullRequests({ repo, token: explicitToken, }: ListOpenPullRequestsOptions): Promise<OpenPullRequest[]>;
16
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * GitHub git provider — detects and creates branches, lists open PRs on github.com remotes.
3
+ *
4
+ * Detection (`detectDefaultBranch`, `getBranch`) shells out to `git ls-remote`
5
+ * and works with whatever SSH/https credentials git already has.
6
+ *
7
+ * Mutation (`createBranch`) and reads (`listOpenPullRequests`) call the GitHub
8
+ * REST API and require a personal access token in `GITHUB_TOKEN` with `repo` scope.
9
+ */
10
+ import { execSync } from 'node:child_process';
11
+ import axios from 'axios';
12
+ const GITHUB_API = 'https://api.github.com';
13
+ export class GitHubProvider {
14
+ async detectDefaultBranch({ remoteUrl }) {
15
+ let stdout;
16
+ try {
17
+ stdout = execSync(`git ls-remote --symref ${remoteUrl} HEAD`, {
18
+ encoding: 'utf8',
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ });
21
+ }
22
+ catch (error) {
23
+ const err = error;
24
+ const stderr = err.stderr ? err.stderr.toString().trim() : (err.message ?? 'unknown error');
25
+ throw new Error(`Unable to detect default branch for ${remoteUrl}: ${stderr}`);
26
+ }
27
+ // First line format: "ref: refs/heads/<branch>\tHEAD"
28
+ const firstLine = stdout.split('\n')[0] ?? '';
29
+ const match = firstLine.match(/^ref:\s+refs\/heads\/([^\t\s]+)\s+HEAD/);
30
+ if (!match) {
31
+ throw new Error(`Unable to detect default branch for ${remoteUrl}: unexpected output "${firstLine}"`);
32
+ }
33
+ return match[1];
34
+ }
35
+ async getBranch({ remoteUrl, branchName }) {
36
+ let stdout;
37
+ try {
38
+ stdout = execSync(`git ls-remote ${remoteUrl} refs/heads/${branchName}`, {
39
+ encoding: 'utf8',
40
+ stdio: ['pipe', 'pipe', 'pipe'],
41
+ });
42
+ }
43
+ catch (error) {
44
+ const err = error;
45
+ const stderr = err.stderr ? err.stderr.toString().trim() : (err.message ?? 'unknown error');
46
+ throw new Error(`Unable to query branch ${branchName} on ${remoteUrl}: ${stderr}`);
47
+ }
48
+ const trimmed = stdout.trim();
49
+ if (trimmed.length === 0) {
50
+ return { exists: false };
51
+ }
52
+ // Line format: "<sha>\trefs/heads/<branch>"
53
+ const firstLine = trimmed.split('\n')[0] ?? '';
54
+ const [sha] = firstLine.split(/\s+/);
55
+ if (!sha) {
56
+ return { exists: false };
57
+ }
58
+ return { exists: true, sha };
59
+ }
60
+ async createBranch({ remoteUrl, branchName, fromSha, }) {
61
+ const token = process.env.GITHUB_TOKEN;
62
+ if (!token) {
63
+ throw new Error('GITHUB_TOKEN env var required for branch creation (set to a GitHub personal access token with repo scope)');
64
+ }
65
+ const { owner, repo } = extractOwnerRepo(remoteUrl);
66
+ const url = `${GITHUB_API}/repos/${owner}/${repo}/git/refs`;
67
+ try {
68
+ const response = await axios.post(url, { ref: `refs/heads/${branchName}`, sha: fromSha }, {
69
+ headers: {
70
+ 'Accept': 'application/vnd.github+json',
71
+ 'Authorization': `Bearer ${token}`,
72
+ 'X-GitHub-Api-Version': '2022-11-28',
73
+ },
74
+ });
75
+ return { created: true, sha: response.data.object.sha };
76
+ }
77
+ catch (error) {
78
+ const axiosError = error;
79
+ const status = axiosError.response?.status;
80
+ const bodyMessage = axiosError.response?.data?.message ?? axiosError.message ?? 'unknown error';
81
+ // GitHub returns 422 "Reference already exists" when the branch exists
82
+ if (status === 422 && typeof bodyMessage === 'string' && bodyMessage.includes('Reference already exists')) {
83
+ return { created: false, sha: '' };
84
+ }
85
+ throw new Error(`GitHub createBranch failed (${status ?? 'no-status'}): ${bodyMessage}`);
86
+ }
87
+ }
88
+ async listOpenPullRequests({ repo, token: explicitToken, }) {
89
+ const token = explicitToken || process.env.GITHUB_TOKEN;
90
+ if (!token) {
91
+ throw new Error('GITHUB_TOKEN env var or --token flag required for listing pull requests');
92
+ }
93
+ // repo is "owner/repo" format
94
+ const url = `${GITHUB_API}/repos/${repo}/pulls`;
95
+ const pullRequests = [];
96
+ let page = 1;
97
+ const perPage = 100;
98
+ // Paginate through all open PRs
99
+ while (true) {
100
+ const response = await axios.get(url, {
101
+ headers: {
102
+ 'Accept': 'application/vnd.github+json',
103
+ 'Authorization': `Bearer ${token}`,
104
+ 'X-GitHub-Api-Version': '2022-11-28',
105
+ },
106
+ params: {
107
+ state: 'open',
108
+ per_page: perPage,
109
+ page,
110
+ },
111
+ });
112
+ for (const pr of response.data) {
113
+ pullRequests.push({
114
+ number: pr.number,
115
+ title: pr.title,
116
+ sourceBranch: pr.head.ref,
117
+ author: pr.user?.login ?? 'unknown',
118
+ url: pr.html_url,
119
+ });
120
+ }
121
+ // If we got fewer results than perPage, we've reached the last page
122
+ if (response.data.length < perPage) {
123
+ break;
124
+ }
125
+ page++;
126
+ }
127
+ return pullRequests;
128
+ }
129
+ }
130
+ /**
131
+ * Extract owner and repo from a GitHub remote URL.
132
+ *
133
+ * Accepts both SSH and HTTPS forms:
134
+ * - `https://github.com/owner/repo.git`
135
+ * - `git@github.com:owner/repo.git`
136
+ * Both return `{ owner: 'owner', repo: 'repo' }`.
137
+ */
138
+ function extractOwnerRepo(remoteUrl) {
139
+ let path = remoteUrl.trim();
140
+ if (path.startsWith('git@github.com:')) {
141
+ path = path.slice('git@github.com:'.length);
142
+ }
143
+ else if (path.startsWith('https://github.com/')) {
144
+ path = path.slice('https://github.com/'.length);
145
+ }
146
+ else if (path.startsWith('http://github.com/')) {
147
+ path = path.slice('http://github.com/'.length);
148
+ }
149
+ if (path.endsWith('.git')) {
150
+ path = path.slice(0, -'.git'.length);
151
+ }
152
+ const parts = path.split('/');
153
+ if (parts.length < 2) {
154
+ throw new Error(`Invalid GitHub remote URL: ${remoteUrl} (expected owner/repo)`);
155
+ }
156
+ return { owner: parts[0], repo: parts[1] };
157
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * GitLab git provider — detects and creates branches on gitlab.com remotes.
3
+ *
4
+ * Detection (`detectDefaultBranch`, `getBranch`) shells out to `git ls-remote`
5
+ * and works with whatever SSH/https credentials git already has.
6
+ *
7
+ * Mutation (`createBranch`) calls the GitLab v4 REST API and requires a
8
+ * personal access token in `GITLAB_TOKEN` with `api` scope.
9
+ */
10
+ import type { CreateBranchOptions, CreateBranchResult, DetectDefaultBranchOptions, GetBranchOptions, GetBranchResult, GitProvider, ListOpenPullRequestsOptions, OpenPullRequest } from './index.js';
11
+ export declare class GitLabProvider implements GitProvider {
12
+ detectDefaultBranch({ remoteUrl }: DetectDefaultBranchOptions): Promise<string>;
13
+ getBranch({ remoteUrl, branchName }: GetBranchOptions): Promise<GetBranchResult>;
14
+ createBranch({ remoteUrl, branchName, fromSha, }: CreateBranchOptions): Promise<CreateBranchResult>;
15
+ listOpenPullRequests({ repo, token: explicitToken, }: ListOpenPullRequestsOptions): Promise<OpenPullRequest[]>;
16
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * GitLab git provider — detects and creates branches on gitlab.com remotes.
3
+ *
4
+ * Detection (`detectDefaultBranch`, `getBranch`) shells out to `git ls-remote`
5
+ * and works with whatever SSH/https credentials git already has.
6
+ *
7
+ * Mutation (`createBranch`) calls the GitLab v4 REST API and requires a
8
+ * personal access token in `GITLAB_TOKEN` with `api` scope.
9
+ */
10
+ import { execSync } from 'node:child_process';
11
+ import axios from 'axios';
12
+ export class GitLabProvider {
13
+ async detectDefaultBranch({ remoteUrl }) {
14
+ let stdout;
15
+ try {
16
+ stdout = execSync(`git ls-remote --symref ${remoteUrl} HEAD`, {
17
+ encoding: 'utf8',
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ });
20
+ }
21
+ catch (error) {
22
+ const err = error;
23
+ const stderr = err.stderr ? err.stderr.toString().trim() : (err.message ?? 'unknown error');
24
+ throw new Error(`Unable to detect default branch for ${remoteUrl}: ${stderr}`);
25
+ }
26
+ // First line format: "ref: refs/heads/<branch>\tHEAD"
27
+ const firstLine = stdout.split('\n')[0] ?? '';
28
+ const match = firstLine.match(/^ref:\s+refs\/heads\/([^\t\s]+)\s+HEAD/);
29
+ if (!match) {
30
+ throw new Error(`Unable to detect default branch for ${remoteUrl}: unexpected output "${firstLine}"`);
31
+ }
32
+ return match[1];
33
+ }
34
+ async getBranch({ remoteUrl, branchName }) {
35
+ let stdout;
36
+ try {
37
+ stdout = execSync(`git ls-remote ${remoteUrl} refs/heads/${branchName}`, {
38
+ encoding: 'utf8',
39
+ stdio: ['pipe', 'pipe', 'pipe'],
40
+ });
41
+ }
42
+ catch (error) {
43
+ const err = error;
44
+ const stderr = err.stderr ? err.stderr.toString().trim() : (err.message ?? 'unknown error');
45
+ throw new Error(`Unable to query branch ${branchName} on ${remoteUrl}: ${stderr}`);
46
+ }
47
+ const trimmed = stdout.trim();
48
+ if (trimmed.length === 0) {
49
+ return { exists: false };
50
+ }
51
+ // Line format: "<sha>\trefs/heads/<branch>"
52
+ const firstLine = trimmed.split('\n')[0] ?? '';
53
+ const [sha] = firstLine.split(/\s+/);
54
+ if (!sha) {
55
+ return { exists: false };
56
+ }
57
+ return { exists: true, sha };
58
+ }
59
+ async createBranch({ remoteUrl, branchName, fromSha, }) {
60
+ const token = process.env.GITLAB_TOKEN;
61
+ if (!token) {
62
+ throw new Error('GITLAB_TOKEN env var required for branch creation (set to a GitLab personal access token with api scope)');
63
+ }
64
+ const projectPath = extractProjectPath(remoteUrl);
65
+ const encodedPath = encodeURIComponent(projectPath);
66
+ const url = `https://gitlab.com/api/v4/projects/${encodedPath}/repository/branches`;
67
+ try {
68
+ const response = await axios.post(url, { branch: branchName, ref: fromSha }, { headers: { 'PRIVATE-TOKEN': token } });
69
+ return { created: true, sha: response.data.commit.id };
70
+ }
71
+ catch (error) {
72
+ const axiosError = error;
73
+ const status = axiosError.response?.status;
74
+ const body = axiosError.response?.data;
75
+ const bodyMessage = Array.isArray(body?.message)
76
+ ? body.message.join(', ')
77
+ : (body?.message ?? axiosError.message ?? 'unknown error');
78
+ // GitLab returns 400 with "Branch already exists" when the branch is
79
+ // present — treat as a non-error so the caller can mark it "already
80
+ // existed" and keep going.
81
+ if (status === 400 && typeof bodyMessage === 'string' && bodyMessage.includes('Branch already exists')) {
82
+ return { created: false, sha: '' };
83
+ }
84
+ throw new Error(`GitLab createBranch failed (${status ?? 'no-status'}): ${bodyMessage}`);
85
+ }
86
+ }
87
+ async listOpenPullRequests({ repo, token: explicitToken, }) {
88
+ const token = explicitToken || process.env.GITLAB_TOKEN;
89
+ if (!token) {
90
+ throw new Error('GITLAB_TOKEN env var or --token flag required for listing merge requests');
91
+ }
92
+ const encodedPath = encodeURIComponent(repo);
93
+ const mergeRequests = [];
94
+ let page = 1;
95
+ const perPage = 100;
96
+ // Paginate through all open MRs
97
+ while (true) {
98
+ const url = `https://gitlab.com/api/v4/projects/${encodedPath}/merge_requests`;
99
+ const response = await axios.get(url, {
100
+ headers: { 'PRIVATE-TOKEN': token },
101
+ params: {
102
+ state: 'opened',
103
+ per_page: perPage,
104
+ page,
105
+ },
106
+ });
107
+ for (const mr of response.data) {
108
+ mergeRequests.push({
109
+ number: mr.iid,
110
+ title: mr.title,
111
+ sourceBranch: mr.source_branch,
112
+ author: mr.author?.username ?? 'unknown',
113
+ url: mr.web_url,
114
+ });
115
+ }
116
+ // If we got fewer results than perPage, we've reached the last page
117
+ if (response.data.length < perPage) {
118
+ break;
119
+ }
120
+ page++;
121
+ }
122
+ return mergeRequests;
123
+ }
124
+ }
125
+ /**
126
+ * Extract the GitLab project path from a remote URL.
127
+ *
128
+ * Accepts both SSH and HTTPS forms:
129
+ * - `https://gitlab.com/dev_squad/repo/web-apps/sign.git`
130
+ * - `git@gitlab.com:dev_squad/repo/web-apps/sign.git`
131
+ * Both return `dev_squad/repo/web-apps/sign`.
132
+ */
133
+ function extractProjectPath(remoteUrl) {
134
+ let path = remoteUrl.trim();
135
+ if (path.startsWith('git@gitlab.com:')) {
136
+ path = path.slice('git@gitlab.com:'.length);
137
+ }
138
+ else if (path.startsWith('https://gitlab.com/')) {
139
+ path = path.slice('https://gitlab.com/'.length);
140
+ }
141
+ else if (path.startsWith('http://gitlab.com/')) {
142
+ path = path.slice('http://gitlab.com/'.length);
143
+ }
144
+ if (path.endsWith('.git')) {
145
+ path = path.slice(0, -'.git'.length);
146
+ }
147
+ return path;
148
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Git provider abstraction for branch detection and creation.
3
+ *
4
+ * Detection paths (`getBranch`, `detectDefaultBranch`) use `git ls-remote` over
5
+ * SSH/https and do not require any API token. Only `createBranch` performs a
6
+ * mutating API call and therefore requires provider credentials (e.g.
7
+ * `GITLAB_TOKEN`).
8
+ *
9
+ * No destructive semantics are exposed — there is no `force` flag anywhere in
10
+ * this surface. Idempotency is the caller's responsibility: callers must
11
+ * invoke `getBranch` before `createBranch`.
12
+ */
13
+ export interface GetBranchOptions {
14
+ remoteUrl: string;
15
+ branchName: string;
16
+ }
17
+ export interface GetBranchResult {
18
+ exists: boolean;
19
+ sha?: string;
20
+ }
21
+ export interface CreateBranchOptions {
22
+ remoteUrl: string;
23
+ branchName: string;
24
+ fromSha: string;
25
+ }
26
+ export interface CreateBranchResult {
27
+ created: boolean;
28
+ sha: string;
29
+ }
30
+ export interface DetectDefaultBranchOptions {
31
+ remoteUrl: string;
32
+ }
33
+ export interface ListOpenPullRequestsOptions {
34
+ /** Repository identifier: "owner/repo" for GitHub, project path or numeric ID for GitLab */
35
+ repo: string;
36
+ /** Optional API token — overrides env var (GITHUB_TOKEN / GITLAB_TOKEN) */
37
+ token?: string;
38
+ }
39
+ export interface OpenPullRequest {
40
+ /** PR/MR number (GitHub) or IID (GitLab) */
41
+ number: number;
42
+ title: string;
43
+ /** Source branch name */
44
+ sourceBranch: string;
45
+ /** Author username */
46
+ author: string;
47
+ /** Web URL to the PR/MR */
48
+ url: string;
49
+ }
50
+ export interface GitProvider {
51
+ detectDefaultBranch(opts: DetectDefaultBranchOptions): Promise<string>;
52
+ getBranch(opts: GetBranchOptions): Promise<GetBranchResult>;
53
+ createBranch(opts: CreateBranchOptions): Promise<CreateBranchResult>;
54
+ listOpenPullRequests(opts: ListOpenPullRequestsOptions): Promise<OpenPullRequest[]>;
55
+ }
56
+ /**
57
+ * Factory — returns the git provider implementation for a given remote URL.
58
+ *
59
+ * Supports both GitLab (gitlab.com) and GitHub (github.com).
60
+ */
61
+ export declare function getProviderForRemote(remoteUrl: string): GitProvider;
62
+ /**
63
+ * Factory — returns the git provider implementation for a provider name.
64
+ *
65
+ * Used by CLI commands that accept `--provider` flag instead of a remote URL.
66
+ */
67
+ export declare function getProviderByName(provider: 'github' | 'gitlab'): GitProvider;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Git provider abstraction for branch detection and creation.
3
+ *
4
+ * Detection paths (`getBranch`, `detectDefaultBranch`) use `git ls-remote` over
5
+ * SSH/https and do not require any API token. Only `createBranch` performs a
6
+ * mutating API call and therefore requires provider credentials (e.g.
7
+ * `GITLAB_TOKEN`).
8
+ *
9
+ * No destructive semantics are exposed — there is no `force` flag anywhere in
10
+ * this surface. Idempotency is the caller's responsibility: callers must
11
+ * invoke `getBranch` before `createBranch`.
12
+ */
13
+ import { GitHubProvider } from './github.js';
14
+ import { GitLabProvider } from './gitlab.js';
15
+ /**
16
+ * Factory — returns the git provider implementation for a given remote URL.
17
+ *
18
+ * Supports both GitLab (gitlab.com) and GitHub (github.com).
19
+ */
20
+ export function getProviderForRemote(remoteUrl) {
21
+ if (remoteUrl.includes('github.com')) {
22
+ return new GitHubProvider();
23
+ }
24
+ if (remoteUrl.includes('gitlab.com')) {
25
+ return new GitLabProvider();
26
+ }
27
+ throw new Error(`Unsupported git provider: ${remoteUrl}`);
28
+ }
29
+ /**
30
+ * Factory — returns the git provider implementation for a provider name.
31
+ *
32
+ * Used by CLI commands that accept `--provider` flag instead of a remote URL.
33
+ */
34
+ export function getProviderByName(provider) {
35
+ if (provider === 'github') {
36
+ return new GitHubProvider();
37
+ }
38
+ return new GitLabProvider();
39
+ }