hivectl 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/dist/cli.cjs +133 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +133 -0
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# hivectl
|
|
2
2
|
|
|
3
|
-
`hivectl` is a Bun-first CLI for a small set of local and GitHub workflows.
|
|
3
|
+
`hivectl` is a Bun-first CLI for a small set of local Git and GitHub workflows.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
### Requirements
|
|
19
19
|
|
|
20
20
|
* [Bun](https://bun.sh/)
|
|
21
|
+
* [Git](https://git-scm.com/)
|
|
21
22
|
* [GitHub CLI](https://cli.github.com/) installed and authenticated for GitHub-backed commands
|
|
22
23
|
* macOS or Linux
|
|
23
24
|
|
|
@@ -92,6 +93,28 @@ Exit codes:
|
|
|
92
93
|
* `1`: Unresolved review threads found or an operational error occurred
|
|
93
94
|
* `2`: No pull request found for the current branch
|
|
94
95
|
|
|
96
|
+
### `sync-upstream`
|
|
97
|
+
|
|
98
|
+
Syncs any of `dev`, `develop`, `main`, and `master` that exist on a source remote to a destination remote, then restores your original checkout.
|
|
99
|
+
|
|
100
|
+
By default, `sync-upstream` reads from `upstream` and pushes to `origin`. Use `--destination` and `--source` to override either remote name.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
hivectl sync-upstream
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Sync from `source` to `fork` instead:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
hivectl sync-upstream --destination fork --source source
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Exit codes:
|
|
113
|
+
|
|
114
|
+
* `0`: At least one conventional branch was synced successfully
|
|
115
|
+
* `1`: A git or operational error occurred
|
|
116
|
+
* `2`: No syncable `dev`, `develop`, `main`, or `master` branches were found on the source remote
|
|
117
|
+
|
|
95
118
|
### Publishing
|
|
96
119
|
|
|
97
120
|
This project uses Changesets for versioning and publishing.
|
|
@@ -122,7 +145,7 @@ This project uses Changesets for versioning and publishing.
|
|
|
122
145
|
│ ├── cli.ts # CLI entry point
|
|
123
146
|
│ └── index.ts # Main library entry point
|
|
124
147
|
├── test/ # Unit tests
|
|
125
|
-
│ ├── cli.test.ts # CLI tests with
|
|
148
|
+
│ ├── cli.test.ts # CLI tests with fake gh and git executables
|
|
126
149
|
│ └── index.test.ts # Library tests
|
|
127
150
|
├── tsdown.config.ts # Configuration for tsdown (bundling)
|
|
128
151
|
├── biome.json # Biome linter/formatter configuration
|
package/dist/cli.cjs
CHANGED
|
@@ -4,9 +4,18 @@ let commander = require("commander");
|
|
|
4
4
|
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
const NO_PR_MESSAGE = "No pull request found for current branch";
|
|
7
|
+
const NO_SYNCABLE_BRANCHES_EXIT_CODE = 2;
|
|
7
8
|
const ANSI_ESCAPE_SEQUENCES = new RegExp(String.raw`\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))`, "gu");
|
|
8
9
|
const CONTROL_CHARACTERS = new RegExp(String.raw`[\u0000-\u001f\u007f]`, "gu");
|
|
9
10
|
const MAX_PREVIEW_LENGTH = 120;
|
|
11
|
+
const SYNC_UPSTREAM_BRANCHES = [
|
|
12
|
+
"dev",
|
|
13
|
+
"develop",
|
|
14
|
+
"main",
|
|
15
|
+
"master"
|
|
16
|
+
];
|
|
17
|
+
const SYNC_UPSTREAM_DEFAULT_DESTINATION = "origin";
|
|
18
|
+
const SYNC_UPSTREAM_DEFAULT_SOURCE = "upstream";
|
|
10
19
|
const REVIEW_THREADS_QUERY = `
|
|
11
20
|
query($id: ID!, $after: String) {
|
|
12
21
|
node(id: $id) {
|
|
@@ -101,6 +110,21 @@ function runGh(args) {
|
|
|
101
110
|
stdout: result.stdout ?? ""
|
|
102
111
|
};
|
|
103
112
|
}
|
|
113
|
+
function runGit(args) {
|
|
114
|
+
const result = (0, node_child_process.spawnSync)("git", args, {
|
|
115
|
+
encoding: "utf8",
|
|
116
|
+
env: process.env
|
|
117
|
+
});
|
|
118
|
+
if (result.error) {
|
|
119
|
+
if ("code" in result.error && result.error.code === "ENOENT") throw new Error("Failed to run git: git is not installed or not available on PATH");
|
|
120
|
+
throw new Error(`Failed to run git: ${result.error.message}`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
status: result.status ?? 1,
|
|
124
|
+
stderr: result.stderr ?? "",
|
|
125
|
+
stdout: result.stdout ?? ""
|
|
126
|
+
};
|
|
127
|
+
}
|
|
104
128
|
function isNoPullRequestFailure(result) {
|
|
105
129
|
const detail = `${normalizeOutput(result.stderr)} ${normalizeOutput(result.stdout)}`.toLowerCase();
|
|
106
130
|
return detail.includes("could not determine current branch") || detail.includes("no pull requests found for branch") || detail.includes("not on any branch");
|
|
@@ -222,12 +246,121 @@ function runGhPrUnresolved(options) {
|
|
|
222
246
|
else printOutput(pullRequest, unresolvedThreads, Boolean(options.verbose));
|
|
223
247
|
return unresolvedThreads.length > 0 ? 1 : 0;
|
|
224
248
|
}
|
|
249
|
+
function getAvailableRemotesLabel(remotes) {
|
|
250
|
+
return remotes.length > 0 ? remotes.join(", ") : "(none)";
|
|
251
|
+
}
|
|
252
|
+
function getCurrentCheckoutState() {
|
|
253
|
+
const branchResult = runGit(["branch", "--show-current"]);
|
|
254
|
+
if (branchResult.status !== 0) throw formatOperationalError("Failed to resolve current checkout", branchResult);
|
|
255
|
+
const branch = normalizeOutput(branchResult.stdout);
|
|
256
|
+
if (branch.length > 0) return {
|
|
257
|
+
kind: "branch",
|
|
258
|
+
ref: branch
|
|
259
|
+
};
|
|
260
|
+
const detachedHeadResult = runGit([
|
|
261
|
+
"rev-parse",
|
|
262
|
+
"--verify",
|
|
263
|
+
"HEAD"
|
|
264
|
+
]);
|
|
265
|
+
if (detachedHeadResult.status !== 0) throw formatOperationalError("Failed to resolve current checkout", detachedHeadResult);
|
|
266
|
+
const commit = normalizeOutput(detachedHeadResult.stdout);
|
|
267
|
+
if (commit.length === 0) throw new Error("Failed to resolve current checkout: HEAD did not resolve to a commit");
|
|
268
|
+
return {
|
|
269
|
+
kind: "detached",
|
|
270
|
+
ref: commit
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function getGitRemotes() {
|
|
274
|
+
const result = runGit(["remote"]);
|
|
275
|
+
if (result.status !== 0) throw formatOperationalError("Failed to list git remotes", result);
|
|
276
|
+
return normalizeOutput(result.stdout).split(/\r?\n/u).map((remote) => remote.trim()).filter((remote) => remote.length > 0).sort((left, right) => left.localeCompare(right));
|
|
277
|
+
}
|
|
278
|
+
function getSyncRemoteLabel(role) {
|
|
279
|
+
return role === "destination" ? "Destination" : "Source";
|
|
280
|
+
}
|
|
281
|
+
function getSyncableBranches(source) {
|
|
282
|
+
return SYNC_UPSTREAM_BRANCHES.filter((branch) => hasFetchedRemoteBranch(source, branch));
|
|
283
|
+
}
|
|
284
|
+
function hasFetchedRemoteBranch(source, branch) {
|
|
285
|
+
const result = runGit([
|
|
286
|
+
"show-ref",
|
|
287
|
+
"--verify",
|
|
288
|
+
"--quiet",
|
|
289
|
+
`refs/remotes/${source}/${branch}`
|
|
290
|
+
]);
|
|
291
|
+
if (result.status === 0) return true;
|
|
292
|
+
if (result.status === 1) return false;
|
|
293
|
+
throw formatOperationalError(`Failed to resolve ${source}/${branch}`, result);
|
|
294
|
+
}
|
|
295
|
+
function ensureSyncRemoteExists(remote, remotes, role) {
|
|
296
|
+
if (remotes.includes(remote)) return;
|
|
297
|
+
throw new Error(`${getSyncRemoteLabel(role)} remote "${remote}" not found. Available remotes: ${getAvailableRemotesLabel(remotes)}`);
|
|
298
|
+
}
|
|
299
|
+
function fetchRemote(remote) {
|
|
300
|
+
const result = runGit(["fetch", remote]);
|
|
301
|
+
if (result.status !== 0) throw formatOperationalError(`Failed to fetch ${remote}`, result);
|
|
302
|
+
}
|
|
303
|
+
function restoreOriginalCheckout(checkoutState) {
|
|
304
|
+
const result = checkoutState.kind === "branch" ? runGit(["checkout", checkoutState.ref]) : runGit([
|
|
305
|
+
"checkout",
|
|
306
|
+
"--detach",
|
|
307
|
+
checkoutState.ref
|
|
308
|
+
]);
|
|
309
|
+
if (result.status === 0) return null;
|
|
310
|
+
return formatOperationalError(`Failed to restore original checkout to ${checkoutState.kind === "branch" ? `branch "${checkoutState.ref}"` : `detached HEAD at ${checkoutState.ref}`}`, result);
|
|
311
|
+
}
|
|
312
|
+
function syncBranch(branch, destination, source) {
|
|
313
|
+
const checkoutResult = runGit([
|
|
314
|
+
"checkout",
|
|
315
|
+
"-B",
|
|
316
|
+
branch,
|
|
317
|
+
`refs/remotes/${source}/${branch}`
|
|
318
|
+
]);
|
|
319
|
+
if (checkoutResult.status !== 0) throw formatOperationalError(`Failed to check out ${branch} from ${source}/${branch}`, checkoutResult);
|
|
320
|
+
const pushResult = runGit([
|
|
321
|
+
"push",
|
|
322
|
+
destination,
|
|
323
|
+
`${branch}:${branch}`
|
|
324
|
+
]);
|
|
325
|
+
if (pushResult.status !== 0) throw formatOperationalError(`Failed to push ${branch} to ${destination}`, pushResult);
|
|
326
|
+
}
|
|
327
|
+
function runSyncUpstream(destinationOption, sourceOption) {
|
|
328
|
+
const destination = normalizeOutput(destinationOption) || SYNC_UPSTREAM_DEFAULT_DESTINATION;
|
|
329
|
+
const source = normalizeOutput(sourceOption) || SYNC_UPSTREAM_DEFAULT_SOURCE;
|
|
330
|
+
const remotes = getGitRemotes();
|
|
331
|
+
ensureSyncRemoteExists(destination, remotes, "destination");
|
|
332
|
+
ensureSyncRemoteExists(source, remotes, "source");
|
|
333
|
+
fetchRemote(source);
|
|
334
|
+
const branches = getSyncableBranches(source);
|
|
335
|
+
if (branches.length === 0) {
|
|
336
|
+
console.log(`No syncable branches found on ${source}. Checked: ${SYNC_UPSTREAM_BRANCHES.join(", ")}`);
|
|
337
|
+
return NO_SYNCABLE_BRANCHES_EXIT_CODE;
|
|
338
|
+
}
|
|
339
|
+
const originalCheckout = getCurrentCheckoutState();
|
|
340
|
+
let syncError = null;
|
|
341
|
+
console.log(`Syncing ${branches.join(", ")} from ${source} to ${destination}`);
|
|
342
|
+
for (const branch of branches) try {
|
|
343
|
+
syncBranch(branch, destination, source);
|
|
344
|
+
console.log(`Synced ${branch} to ${destination}`);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
syncError = error instanceof Error ? error : new Error(String(error));
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
const restoreError = restoreOriginalCheckout(originalCheckout);
|
|
350
|
+
if (syncError && restoreError) throw new Error(`${syncError.message}\n${restoreError.message}`);
|
|
351
|
+
if (syncError) throw syncError;
|
|
352
|
+
if (restoreError) throw restoreError;
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
225
355
|
function createProgram() {
|
|
226
356
|
const program = new commander.Command();
|
|
227
357
|
program.name("hivectl").description("Common local and GitHub workflow helpers").exitOverride();
|
|
228
358
|
program.command("gh-pr-unresolved").description("Show unresolved review threads on the pull request for the current branch").option("--json", "show unresolved review threads as JSON").option("-v, --verbose", "show unresolved review threads in detail").action((options) => {
|
|
229
359
|
process.exitCode = runGhPrUnresolved(options);
|
|
230
360
|
});
|
|
361
|
+
program.command("sync-upstream").description("Sync dev, develop, main, and master from a source remote to a destination remote").option("--destination <remote>", "destination remote name", SYNC_UPSTREAM_DEFAULT_DESTINATION).option("--source <remote>", "source remote name", SYNC_UPSTREAM_DEFAULT_SOURCE).action((options) => {
|
|
362
|
+
process.exitCode = runSyncUpstream(options.destination, options.source);
|
|
363
|
+
});
|
|
231
364
|
return program;
|
|
232
365
|
}
|
|
233
366
|
async function main(argv = process.argv) {
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.cjs","names":["Command","CommanderError"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawnSync } from 'node:child_process'\nimport { Command, CommanderError } from 'commander'\n\ntype ReviewComment = {\n author?: {\n login?: string | null\n } | null\n body?: string | null\n outdated?: boolean | null\n path?: string | null\n url?: string | null\n}\n\ntype ReviewThreadNode = {\n comments?: {\n nodes?: Array<ReviewComment | null> | null\n } | null\n isOutdated?: boolean | null\n isResolved?: boolean | null\n}\n\ntype ReviewThreadsResponse = {\n data?: {\n node?: {\n reviewThreads?: {\n nodes?: Array<ReviewThreadNode | null> | null\n pageInfo?: {\n endCursor?: string | null\n hasNextPage?: boolean | null\n } | null\n } | null\n } | null\n } | null\n errors?: Array<{\n message?: string | null\n } | null> | null\n}\n\ntype PullRequestState = 'closed' | 'merged' | 'open'\n\ntype PullRequestResponse = {\n id: string\n number: number\n state: PullRequestState\n title: string\n url: string\n}\n\ntype PullRequestThread = {\n author: string\n outdated: boolean\n path: string\n preview: string\n url: string\n}\n\ntype CommandOptions = {\n json?: boolean\n verbose?: boolean\n}\n\ntype GhResult = {\n status: number\n stderr: string\n stdout: string\n}\n\ntype JsonOutput = {\n pullRequest: {\n number: number\n state: PullRequestState\n title: string\n url: string\n } | null\n status: 'clean' | 'no_pr' | 'unresolved'\n threads: PullRequestThread[]\n unresolvedCount: number\n}\n\nconst NO_PR_MESSAGE = 'No pull request found for current branch'\n// biome-ignore lint/complexity/useRegexLiterals: The constructor avoids embedding control characters in a regex literal.\nconst ANSI_ESCAPE_SEQUENCES = new RegExp(\n String.raw`\\u001b(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~]|\\][^\\u0007]*(?:\\u0007|\\u001b\\\\))`,\n 'gu',\n)\n// biome-ignore lint/complexity/useRegexLiterals: The constructor avoids embedding control characters in a regex literal.\nconst CONTROL_CHARACTERS = new RegExp(String.raw`[\\u0000-\\u001f\\u007f]`, 'gu')\nconst MAX_PREVIEW_LENGTH = 120\nconst REVIEW_THREADS_QUERY = `\n query($id: ID!, $after: String) {\n node(id: $id) {\n ... on PullRequest {\n reviewThreads(first: 100, after: $after) {\n nodes {\n isOutdated\n isResolved\n comments(first: 1) {\n nodes {\n author {\n login\n }\n body\n outdated\n path\n url\n }\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n }\n`\n\nfunction normalizeOutput(value: string | null | undefined): string {\n return value?.trim() ?? ''\n}\n\nfunction formatOperationalError(prefix: string, result: GhResult): Error {\n const detail = normalizeOutput(result.stderr) || normalizeOutput(result.stdout)\n\n return new Error(detail ? `${prefix}: ${detail}` : prefix)\n}\n\nfunction parseJson<T>(value: string, context: string): T {\n try {\n return JSON.parse(value) as T\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(`${context}: ${message}`)\n }\n}\n\nfunction parsePullRequestState(value: unknown): PullRequestState | null {\n if (typeof value !== 'string') {\n return null\n }\n\n switch (value.toLowerCase()) {\n case 'closed':\n return 'closed'\n case 'merged':\n return 'merged'\n case 'open':\n return 'open'\n default:\n return null\n }\n}\n\nfunction toPullRequestResponse(value: unknown): PullRequestResponse | null {\n const pullRequest = value as\n | {\n id?: unknown\n number?: unknown\n state?: unknown\n title?: unknown\n url?: unknown\n }\n | null\n | undefined\n const state = parsePullRequestState(pullRequest?.state)\n\n if (\n !pullRequest ||\n typeof pullRequest !== 'object' ||\n typeof pullRequest.id !== 'string' ||\n pullRequest.id.length === 0 ||\n typeof pullRequest.number !== 'number' ||\n !state ||\n typeof pullRequest.title !== 'string' ||\n typeof pullRequest.url !== 'string'\n ) {\n return null\n }\n\n return {\n id: pullRequest.id,\n number: pullRequest.number,\n state,\n title: pullRequest.title,\n url: pullRequest.url,\n }\n}\n\nfunction parsePullRequestResponse(value: string): PullRequestResponse {\n const pullRequest = toPullRequestResponse(parseJson<unknown>(value, 'Failed to parse pull request response'))\n\n if (!pullRequest) {\n throw new Error('Failed to parse pull request response: Response is missing required pull request fields')\n }\n\n return pullRequest\n}\n\nfunction getPreview(body: string): string {\n const firstLine = body\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .find((line) => line.length > 0)\n\n if (!firstLine) {\n return '(no preview available)'\n }\n\n if (firstLine.length <= MAX_PREVIEW_LENGTH) {\n return firstLine\n }\n\n return `${firstLine.slice(0, MAX_PREVIEW_LENGTH - 3)}...`\n}\n\nfunction sanitizeTerminalText(value: string): string {\n return value.replace(ANSI_ESCAPE_SEQUENCES, '').replace(CONTROL_CHARACTERS, '')\n}\n\nfunction runGh(args: string[]): GhResult {\n const result = spawnSync('gh', args, {\n encoding: 'utf8',\n env: process.env,\n })\n\n if (result.error) {\n if ('code' in result.error && result.error.code === 'ENOENT') {\n throw new Error('Failed to run gh: gh is not installed or not available on PATH')\n }\n\n throw new Error(`Failed to run gh: ${result.error.message}`)\n }\n\n return {\n status: result.status ?? 1,\n stderr: result.stderr ?? '',\n stdout: result.stdout ?? '',\n }\n}\n\nfunction isNoPullRequestFailure(result: GhResult): boolean {\n const detail = `${normalizeOutput(result.stderr)} ${normalizeOutput(result.stdout)}`.toLowerCase()\n\n return (\n detail.includes('could not determine current branch') ||\n detail.includes('no pull requests found for branch') ||\n detail.includes('not on any branch')\n )\n}\n\nfunction getCurrentPullRequest(): PullRequestResponse | null {\n const result = runGh(['pr', 'view', '--json', 'id,number,state,title,url'])\n\n if (result.status === 0) {\n return parsePullRequestResponse(result.stdout)\n }\n\n if (isNoPullRequestFailure(result)) {\n return null\n }\n\n throw formatOperationalError('Failed to resolve pull request for current branch', result)\n}\n\nfunction getPullRequestHostname(url: string): string | null {\n try {\n const hostname = new URL(url).hostname\n\n return hostname === 'github.com' ? null : hostname\n } catch {\n return null\n }\n}\n\nfunction getReviewThreadsPage(id: string, hostname: string | null, after: string | null): ReviewThreadsResponse {\n const args = ['api', 'graphql']\n\n if (hostname) {\n args.push('--hostname', hostname)\n }\n\n args.push('-f', `query=${REVIEW_THREADS_QUERY}`, '-F', `id=${id}`)\n\n if (after) {\n args.push('-F', `after=${after}`)\n }\n\n const result = runGh(args)\n\n if (result.status !== 0) {\n throw formatOperationalError('Failed to fetch review threads', result)\n }\n\n return parseJson<ReviewThreadsResponse>(result.stdout, 'Failed to parse review threads response')\n}\n\nfunction getUnresolvedThreads(id: string, hostname: string | null): PullRequestThread[] {\n const unresolvedThreads: PullRequestThread[] = []\n let after: string | null = null\n\n do {\n const response = getReviewThreadsPage(id, hostname, after)\n const errorMessage =\n response.errors\n ?.map((error) => normalizeOutput(error?.message))\n .filter((message) => message.length > 0)\n .join('; ') ?? ''\n\n if (errorMessage.length > 0) {\n throw new Error(`Failed to fetch review threads: ${errorMessage}`)\n }\n\n const reviewThreads = response.data?.node?.reviewThreads\n const nodes = reviewThreads?.nodes\n const hasNextPage = reviewThreads?.pageInfo?.hasNextPage\n\n if (!Array.isArray(nodes) || typeof hasNextPage !== 'boolean') {\n throw new Error('Failed to fetch review threads: Pull request review threads were not returned')\n }\n\n for (const thread of nodes) {\n if (!thread || thread.isResolved === true) {\n continue\n }\n\n const comments = Array.isArray(thread.comments?.nodes) ? thread.comments.nodes : []\n const reviewComment = comments[0] ?? null\n\n unresolvedThreads.push({\n author: normalizeOutput(reviewComment?.author?.login) || '(unknown author)',\n outdated: thread.isOutdated === true || reviewComment?.outdated === true,\n path: normalizeOutput(reviewComment?.path) || '(unknown file)',\n preview: getPreview(reviewComment?.body ?? ''),\n url: normalizeOutput(reviewComment?.url) || '(missing comment url)',\n })\n }\n\n const endCursor = normalizeOutput(reviewThreads?.pageInfo?.endCursor)\n after = hasNextPage ? endCursor || null : null\n } while (after)\n\n return unresolvedThreads.sort((left, right) => {\n const pathComparison = left.path.localeCompare(right.path)\n\n if (pathComparison !== 0) {\n return pathComparison\n }\n\n return left.url.localeCompare(right.url)\n })\n}\n\nfunction printSummaryThreads(threads: PullRequestThread[]): void {\n for (const thread of threads) {\n console.log(thread.url)\n }\n}\n\nfunction printVerboseThreads(threads: PullRequestThread[]): void {\n for (const thread of threads) {\n const author = sanitizeTerminalText(thread.author)\n const outdatedMarker = thread.outdated ? ' (outdated)' : ''\n const path = sanitizeTerminalText(thread.path)\n const preview = sanitizeTerminalText(thread.preview)\n\n console.log(`${thread.url} | ${author} | ${path}${outdatedMarker} | ${preview}`)\n }\n}\n\nfunction printSummary(pullRequest: PullRequestResponse, unresolvedCount: number): void {\n const stateLabel = pullRequest.state === 'open' ? '' : ` (${pullRequest.state})`\n console.log(\n `PR #${pullRequest.number}${stateLabel} has ${unresolvedCount} unresolved review thread(s): ${pullRequest.url}`,\n )\n}\n\nfunction getJsonOutput(\n pullRequest: PullRequestResponse | null,\n unresolvedThreads: PullRequestThread[],\n status: JsonOutput['status'],\n): JsonOutput {\n return {\n pullRequest: pullRequest\n ? {\n number: pullRequest.number,\n state: pullRequest.state,\n title: pullRequest.title,\n url: pullRequest.url,\n }\n : null,\n status,\n threads: unresolvedThreads,\n unresolvedCount: unresolvedThreads.length,\n }\n}\n\nfunction printJsonOutput(\n pullRequest: PullRequestResponse | null,\n unresolvedThreads: PullRequestThread[],\n status: JsonOutput['status'],\n): void {\n console.log(JSON.stringify(getJsonOutput(pullRequest, unresolvedThreads, status), null, 2))\n}\n\nfunction printThreads(verbose: boolean, unresolvedThreads: PullRequestThread[]): void {\n if (verbose) {\n printVerboseThreads(unresolvedThreads)\n return\n }\n\n printSummaryThreads(unresolvedThreads)\n}\n\nfunction printOutput(pullRequest: PullRequestResponse, unresolvedThreads: PullRequestThread[], verbose: boolean): void {\n printSummary(pullRequest, unresolvedThreads.length)\n\n if (unresolvedThreads.length > 0) {\n printThreads(verbose, unresolvedThreads)\n }\n}\n\nfunction runGhPrUnresolved(options: CommandOptions): number {\n const pullRequest = getCurrentPullRequest()\n\n if (!pullRequest) {\n if (options.json) {\n printJsonOutput(null, [], 'no_pr')\n return 2\n }\n\n console.log(NO_PR_MESSAGE)\n return 2\n }\n\n const unresolvedThreads = getUnresolvedThreads(pullRequest.id, getPullRequestHostname(pullRequest.url))\n\n if (options.json) {\n printJsonOutput(pullRequest, unresolvedThreads, unresolvedThreads.length === 0 ? 'clean' : 'unresolved')\n } else {\n printOutput(pullRequest, unresolvedThreads, Boolean(options.verbose))\n }\n\n return unresolvedThreads.length > 0 ? 1 : 0\n}\n\nfunction createProgram(): Command {\n const program = new Command()\n\n program.name('hivectl').description('Common local and GitHub workflow helpers').exitOverride()\n\n program\n .command('gh-pr-unresolved')\n .description('Show unresolved review threads on the pull request for the current branch')\n .option('--json', 'show unresolved review threads as JSON')\n .option('-v, --verbose', 'show unresolved review threads in detail')\n .action((options: CommandOptions) => {\n process.exitCode = runGhPrUnresolved(options)\n })\n\n return program\n}\n\nasync function main(argv = process.argv): Promise<void> {\n const program = createProgram()\n\n try {\n await program.parseAsync(argv)\n\n if (typeof process.exitCode !== 'number') {\n process.exitCode = 0\n }\n } catch (error) {\n if (error instanceof CommanderError) {\n process.exitCode = error.code === 'commander.helpDisplayed' ? 0 : error.exitCode\n return\n }\n\n const message = error instanceof Error ? error.message : String(error)\n console.error(message)\n process.exitCode = 1\n }\n}\n\nvoid main()\n"],"mappings":";;;;;AAiFA,MAAM,gBAAgB;AAEtB,MAAM,wBAAwB,IAAI,OAChC,OAAO,GAAG,2EACV,KACD;AAED,MAAM,qBAAqB,IAAI,OAAO,OAAO,GAAG,yBAAyB,KAAK;AAC9E,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8B7B,SAAS,gBAAgB,OAA0C;AACjE,QAAO,OAAO,MAAM,IAAI;;AAG1B,SAAS,uBAAuB,QAAgB,QAAyB;CACvE,MAAM,SAAS,gBAAgB,OAAO,OAAO,IAAI,gBAAgB,OAAO,OAAO;AAE/E,QAAO,IAAI,MAAM,SAAS,GAAG,OAAO,IAAI,WAAW,OAAO;;AAG5D,SAAS,UAAa,OAAe,SAAoB;AACvD,KAAI;AACF,SAAO,KAAK,MAAM,MAAM;UACjB,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,QAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,UAAU;;;AAI7C,SAAS,sBAAsB,OAAyC;AACtE,KAAI,OAAO,UAAU,SACnB,QAAO;AAGT,SAAQ,MAAM,aAAa,EAA3B;EACE,KAAK,SACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,OACH,QAAO;EACT,QACE,QAAO;;;AAIb,SAAS,sBAAsB,OAA4C;CACzE,MAAM,cAAc;CAUpB,MAAM,QAAQ,sBAAsB,aAAa,MAAM;AAEvD,KACE,CAAC,eACD,OAAO,gBAAgB,YACvB,OAAO,YAAY,OAAO,YAC1B,YAAY,GAAG,WAAW,KAC1B,OAAO,YAAY,WAAW,YAC9B,CAAC,SACD,OAAO,YAAY,UAAU,YAC7B,OAAO,YAAY,QAAQ,SAE3B,QAAO;AAGT,QAAO;EACL,IAAI,YAAY;EAChB,QAAQ,YAAY;EACpB;EACA,OAAO,YAAY;EACnB,KAAK,YAAY;EAClB;;AAGH,SAAS,yBAAyB,OAAoC;CACpE,MAAM,cAAc,sBAAsB,UAAmB,OAAO,wCAAwC,CAAC;AAE7G,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,0FAA0F;AAG5G,QAAO;;AAGT,SAAS,WAAW,MAAsB;CACxC,MAAM,YAAY,KACf,MAAM,SAAS,CACf,KAAK,SAAS,KAAK,MAAM,CAAC,CAC1B,MAAM,SAAS,KAAK,SAAS,EAAE;AAElC,KAAI,CAAC,UACH,QAAO;AAGT,KAAI,UAAU,UAAU,mBACtB,QAAO;AAGT,QAAO,GAAG,UAAU,MAAM,GAAG,qBAAqB,EAAE,CAAC;;AAGvD,SAAS,qBAAqB,OAAuB;AACnD,QAAO,MAAM,QAAQ,uBAAuB,GAAG,CAAC,QAAQ,oBAAoB,GAAG;;AAGjF,SAAS,MAAM,MAA0B;CACvC,MAAM,2CAAmB,MAAM,MAAM;EACnC,UAAU;EACV,KAAK,QAAQ;EACd,CAAC;AAEF,KAAI,OAAO,OAAO;AAChB,MAAI,UAAU,OAAO,SAAS,OAAO,MAAM,SAAS,SAClD,OAAM,IAAI,MAAM,iEAAiE;AAGnF,QAAM,IAAI,MAAM,qBAAqB,OAAO,MAAM,UAAU;;AAG9D,QAAO;EACL,QAAQ,OAAO,UAAU;EACzB,QAAQ,OAAO,UAAU;EACzB,QAAQ,OAAO,UAAU;EAC1B;;AAGH,SAAS,uBAAuB,QAA2B;CACzD,MAAM,SAAS,GAAG,gBAAgB,OAAO,OAAO,CAAC,GAAG,gBAAgB,OAAO,OAAO,GAAG,aAAa;AAElG,QACE,OAAO,SAAS,qCAAqC,IACrD,OAAO,SAAS,oCAAoC,IACpD,OAAO,SAAS,oBAAoB;;AAIxC,SAAS,wBAAoD;CAC3D,MAAM,SAAS,MAAM;EAAC;EAAM;EAAQ;EAAU;EAA4B,CAAC;AAE3E,KAAI,OAAO,WAAW,EACpB,QAAO,yBAAyB,OAAO,OAAO;AAGhD,KAAI,uBAAuB,OAAO,CAChC,QAAO;AAGT,OAAM,uBAAuB,qDAAqD,OAAO;;AAG3F,SAAS,uBAAuB,KAA4B;AAC1D,KAAI;EACF,MAAM,WAAW,IAAI,IAAI,IAAI,CAAC;AAE9B,SAAO,aAAa,eAAe,OAAO;SACpC;AACN,SAAO;;;AAIX,SAAS,qBAAqB,IAAY,UAAyB,OAA6C;CAC9G,MAAM,OAAO,CAAC,OAAO,UAAU;AAE/B,KAAI,SACF,MAAK,KAAK,cAAc,SAAS;AAGnC,MAAK,KAAK,MAAM,SAAS,wBAAwB,MAAM,MAAM,KAAK;AAElE,KAAI,MACF,MAAK,KAAK,MAAM,SAAS,QAAQ;CAGnC,MAAM,SAAS,MAAM,KAAK;AAE1B,KAAI,OAAO,WAAW,EACpB,OAAM,uBAAuB,kCAAkC,OAAO;AAGxE,QAAO,UAAiC,OAAO,QAAQ,0CAA0C;;AAGnG,SAAS,qBAAqB,IAAY,UAA8C;CACtF,MAAM,oBAAyC,EAAE;CACjD,IAAI,QAAuB;AAE3B,IAAG;EACD,MAAM,WAAW,qBAAqB,IAAI,UAAU,MAAM;EAC1D,MAAM,eACJ,SAAS,QACL,KAAK,UAAU,gBAAgB,OAAO,QAAQ,CAAC,CAChD,QAAQ,YAAY,QAAQ,SAAS,EAAE,CACvC,KAAK,KAAK,IAAI;AAEnB,MAAI,aAAa,SAAS,EACxB,OAAM,IAAI,MAAM,mCAAmC,eAAe;EAGpE,MAAM,gBAAgB,SAAS,MAAM,MAAM;EAC3C,MAAM,QAAQ,eAAe;EAC7B,MAAM,cAAc,eAAe,UAAU;AAE7C,MAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,OAAO,gBAAgB,UAClD,OAAM,IAAI,MAAM,gFAAgF;AAGlG,OAAK,MAAM,UAAU,OAAO;AAC1B,OAAI,CAAC,UAAU,OAAO,eAAe,KACnC;GAIF,MAAM,iBADW,MAAM,QAAQ,OAAO,UAAU,MAAM,GAAG,OAAO,SAAS,QAAQ,EAAE,EACpD,MAAM;AAErC,qBAAkB,KAAK;IACrB,QAAQ,gBAAgB,eAAe,QAAQ,MAAM,IAAI;IACzD,UAAU,OAAO,eAAe,QAAQ,eAAe,aAAa;IACpE,MAAM,gBAAgB,eAAe,KAAK,IAAI;IAC9C,SAAS,WAAW,eAAe,QAAQ,GAAG;IAC9C,KAAK,gBAAgB,eAAe,IAAI,IAAI;IAC7C,CAAC;;EAGJ,MAAM,YAAY,gBAAgB,eAAe,UAAU,UAAU;AACrE,UAAQ,cAAc,aAAa,OAAO;UACnC;AAET,QAAO,kBAAkB,MAAM,MAAM,UAAU;EAC7C,MAAM,iBAAiB,KAAK,KAAK,cAAc,MAAM,KAAK;AAE1D,MAAI,mBAAmB,EACrB,QAAO;AAGT,SAAO,KAAK,IAAI,cAAc,MAAM,IAAI;GACxC;;AAGJ,SAAS,oBAAoB,SAAoC;AAC/D,MAAK,MAAM,UAAU,QACnB,SAAQ,IAAI,OAAO,IAAI;;AAI3B,SAAS,oBAAoB,SAAoC;AAC/D,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,SAAS,qBAAqB,OAAO,OAAO;EAClD,MAAM,iBAAiB,OAAO,WAAW,gBAAgB;EACzD,MAAM,OAAO,qBAAqB,OAAO,KAAK;EAC9C,MAAM,UAAU,qBAAqB,OAAO,QAAQ;AAEpD,UAAQ,IAAI,GAAG,OAAO,IAAI,KAAK,OAAO,KAAK,OAAO,eAAe,KAAK,UAAU;;;AAIpF,SAAS,aAAa,aAAkC,iBAA+B;CACrF,MAAM,aAAa,YAAY,UAAU,SAAS,KAAK,KAAK,YAAY,MAAM;AAC9E,SAAQ,IACN,OAAO,YAAY,SAAS,WAAW,OAAO,gBAAgB,gCAAgC,YAAY,MAC3G;;AAGH,SAAS,cACP,aACA,mBACA,QACY;AACZ,QAAO;EACL,aAAa,cACT;GACE,QAAQ,YAAY;GACpB,OAAO,YAAY;GACnB,OAAO,YAAY;GACnB,KAAK,YAAY;GAClB,GACD;EACJ;EACA,SAAS;EACT,iBAAiB,kBAAkB;EACpC;;AAGH,SAAS,gBACP,aACA,mBACA,QACM;AACN,SAAQ,IAAI,KAAK,UAAU,cAAc,aAAa,mBAAmB,OAAO,EAAE,MAAM,EAAE,CAAC;;AAG7F,SAAS,aAAa,SAAkB,mBAA8C;AACpF,KAAI,SAAS;AACX,sBAAoB,kBAAkB;AACtC;;AAGF,qBAAoB,kBAAkB;;AAGxC,SAAS,YAAY,aAAkC,mBAAwC,SAAwB;AACrH,cAAa,aAAa,kBAAkB,OAAO;AAEnD,KAAI,kBAAkB,SAAS,EAC7B,cAAa,SAAS,kBAAkB;;AAI5C,SAAS,kBAAkB,SAAiC;CAC1D,MAAM,cAAc,uBAAuB;AAE3C,KAAI,CAAC,aAAa;AAChB,MAAI,QAAQ,MAAM;AAChB,mBAAgB,MAAM,EAAE,EAAE,QAAQ;AAClC,UAAO;;AAGT,UAAQ,IAAI,cAAc;AAC1B,SAAO;;CAGT,MAAM,oBAAoB,qBAAqB,YAAY,IAAI,uBAAuB,YAAY,IAAI,CAAC;AAEvG,KAAI,QAAQ,KACV,iBAAgB,aAAa,mBAAmB,kBAAkB,WAAW,IAAI,UAAU,aAAa;KAExG,aAAY,aAAa,mBAAmB,QAAQ,QAAQ,QAAQ,CAAC;AAGvE,QAAO,kBAAkB,SAAS,IAAI,IAAI;;AAG5C,SAAS,gBAAyB;CAChC,MAAM,UAAU,IAAIA,mBAAS;AAE7B,SAAQ,KAAK,UAAU,CAAC,YAAY,2CAA2C,CAAC,cAAc;AAE9F,SACG,QAAQ,mBAAmB,CAC3B,YAAY,4EAA4E,CACxF,OAAO,UAAU,yCAAyC,CAC1D,OAAO,iBAAiB,2CAA2C,CACnE,QAAQ,YAA4B;AACnC,UAAQ,WAAW,kBAAkB,QAAQ;GAC7C;AAEJ,QAAO;;AAGT,eAAe,KAAK,OAAO,QAAQ,MAAqB;CACtD,MAAM,UAAU,eAAe;AAE/B,KAAI;AACF,QAAM,QAAQ,WAAW,KAAK;AAE9B,MAAI,OAAO,QAAQ,aAAa,SAC9B,SAAQ,WAAW;UAEd,OAAO;AACd,MAAI,iBAAiBC,0BAAgB;AACnC,WAAQ,WAAW,MAAM,SAAS,4BAA4B,IAAI,MAAM;AACxE;;EAGF,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,UAAQ,MAAM,QAAQ;AACtB,UAAQ,WAAW;;;AAIlB,MAAM"}
|
|
1
|
+
{"version":3,"file":"cli.cjs","names":["Command","CommanderError"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawnSync } from 'node:child_process'\nimport { Command, CommanderError } from 'commander'\n\ntype ReviewComment = {\n author?: {\n login?: string | null\n } | null\n body?: string | null\n outdated?: boolean | null\n path?: string | null\n url?: string | null\n}\n\ntype ReviewThreadNode = {\n comments?: {\n nodes?: Array<ReviewComment | null> | null\n } | null\n isOutdated?: boolean | null\n isResolved?: boolean | null\n}\n\ntype ReviewThreadsResponse = {\n data?: {\n node?: {\n reviewThreads?: {\n nodes?: Array<ReviewThreadNode | null> | null\n pageInfo?: {\n endCursor?: string | null\n hasNextPage?: boolean | null\n } | null\n } | null\n } | null\n } | null\n errors?: Array<{\n message?: string | null\n } | null> | null\n}\n\ntype PullRequestState = 'closed' | 'merged' | 'open'\n\ntype PullRequestResponse = {\n id: string\n number: number\n state: PullRequestState\n title: string\n url: string\n}\n\ntype PullRequestThread = {\n author: string\n outdated: boolean\n path: string\n preview: string\n url: string\n}\n\ntype CheckoutState =\n | {\n kind: 'branch'\n ref: string\n }\n | {\n kind: 'detached'\n ref: string\n }\n\ntype CommandOptions = {\n json?: boolean\n verbose?: boolean\n}\n\ntype CommandResult = {\n status: number\n stderr: string\n stdout: string\n}\n\ntype JsonOutput = {\n pullRequest: {\n number: number\n state: PullRequestState\n title: string\n url: string\n } | null\n status: 'clean' | 'no_pr' | 'unresolved'\n threads: PullRequestThread[]\n unresolvedCount: number\n}\n\nconst NO_PR_MESSAGE = 'No pull request found for current branch'\nconst NO_SYNCABLE_BRANCHES_EXIT_CODE = 2\n// biome-ignore lint/complexity/useRegexLiterals: The constructor avoids embedding control characters in a regex literal.\nconst ANSI_ESCAPE_SEQUENCES = new RegExp(\n String.raw`\\u001b(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~]|\\][^\\u0007]*(?:\\u0007|\\u001b\\\\))`,\n 'gu',\n)\n// biome-ignore lint/complexity/useRegexLiterals: The constructor avoids embedding control characters in a regex literal.\nconst CONTROL_CHARACTERS = new RegExp(String.raw`[\\u0000-\\u001f\\u007f]`, 'gu')\nconst MAX_PREVIEW_LENGTH = 120\nconst SYNC_UPSTREAM_BRANCHES = ['dev', 'develop', 'main', 'master'] as const\nconst SYNC_UPSTREAM_DEFAULT_DESTINATION = 'origin'\nconst SYNC_UPSTREAM_DEFAULT_SOURCE = 'upstream'\nconst REVIEW_THREADS_QUERY = `\n query($id: ID!, $after: String) {\n node(id: $id) {\n ... on PullRequest {\n reviewThreads(first: 100, after: $after) {\n nodes {\n isOutdated\n isResolved\n comments(first: 1) {\n nodes {\n author {\n login\n }\n body\n outdated\n path\n url\n }\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n }\n`\n\nfunction normalizeOutput(value: string | null | undefined): string {\n return value?.trim() ?? ''\n}\n\nfunction formatOperationalError(prefix: string, result: CommandResult): Error {\n const detail = normalizeOutput(result.stderr) || normalizeOutput(result.stdout)\n\n return new Error(detail ? `${prefix}: ${detail}` : prefix)\n}\n\nfunction parseJson<T>(value: string, context: string): T {\n try {\n return JSON.parse(value) as T\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(`${context}: ${message}`)\n }\n}\n\nfunction parsePullRequestState(value: unknown): PullRequestState | null {\n if (typeof value !== 'string') {\n return null\n }\n\n switch (value.toLowerCase()) {\n case 'closed':\n return 'closed'\n case 'merged':\n return 'merged'\n case 'open':\n return 'open'\n default:\n return null\n }\n}\n\nfunction toPullRequestResponse(value: unknown): PullRequestResponse | null {\n const pullRequest = value as\n | {\n id?: unknown\n number?: unknown\n state?: unknown\n title?: unknown\n url?: unknown\n }\n | null\n | undefined\n const state = parsePullRequestState(pullRequest?.state)\n\n if (\n !pullRequest ||\n typeof pullRequest !== 'object' ||\n typeof pullRequest.id !== 'string' ||\n pullRequest.id.length === 0 ||\n typeof pullRequest.number !== 'number' ||\n !state ||\n typeof pullRequest.title !== 'string' ||\n typeof pullRequest.url !== 'string'\n ) {\n return null\n }\n\n return {\n id: pullRequest.id,\n number: pullRequest.number,\n state,\n title: pullRequest.title,\n url: pullRequest.url,\n }\n}\n\nfunction parsePullRequestResponse(value: string): PullRequestResponse {\n const pullRequest = toPullRequestResponse(parseJson<unknown>(value, 'Failed to parse pull request response'))\n\n if (!pullRequest) {\n throw new Error('Failed to parse pull request response: Response is missing required pull request fields')\n }\n\n return pullRequest\n}\n\nfunction getPreview(body: string): string {\n const firstLine = body\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .find((line) => line.length > 0)\n\n if (!firstLine) {\n return '(no preview available)'\n }\n\n if (firstLine.length <= MAX_PREVIEW_LENGTH) {\n return firstLine\n }\n\n return `${firstLine.slice(0, MAX_PREVIEW_LENGTH - 3)}...`\n}\n\nfunction sanitizeTerminalText(value: string): string {\n return value.replace(ANSI_ESCAPE_SEQUENCES, '').replace(CONTROL_CHARACTERS, '')\n}\n\nfunction runGh(args: string[]): CommandResult {\n const result = spawnSync('gh', args, {\n encoding: 'utf8',\n env: process.env,\n })\n\n if (result.error) {\n if ('code' in result.error && result.error.code === 'ENOENT') {\n throw new Error('Failed to run gh: gh is not installed or not available on PATH')\n }\n\n throw new Error(`Failed to run gh: ${result.error.message}`)\n }\n\n return {\n status: result.status ?? 1,\n stderr: result.stderr ?? '',\n stdout: result.stdout ?? '',\n }\n}\n\nfunction runGit(args: string[]): CommandResult {\n const result = spawnSync('git', args, {\n encoding: 'utf8',\n env: process.env,\n })\n\n if (result.error) {\n if ('code' in result.error && result.error.code === 'ENOENT') {\n throw new Error('Failed to run git: git is not installed or not available on PATH')\n }\n\n throw new Error(`Failed to run git: ${result.error.message}`)\n }\n\n return {\n status: result.status ?? 1,\n stderr: result.stderr ?? '',\n stdout: result.stdout ?? '',\n }\n}\n\nfunction isNoPullRequestFailure(result: CommandResult): boolean {\n const detail = `${normalizeOutput(result.stderr)} ${normalizeOutput(result.stdout)}`.toLowerCase()\n\n return (\n detail.includes('could not determine current branch') ||\n detail.includes('no pull requests found for branch') ||\n detail.includes('not on any branch')\n )\n}\n\nfunction getCurrentPullRequest(): PullRequestResponse | null {\n const result = runGh(['pr', 'view', '--json', 'id,number,state,title,url'])\n\n if (result.status === 0) {\n return parsePullRequestResponse(result.stdout)\n }\n\n if (isNoPullRequestFailure(result)) {\n return null\n }\n\n throw formatOperationalError('Failed to resolve pull request for current branch', result)\n}\n\nfunction getPullRequestHostname(url: string): string | null {\n try {\n const hostname = new URL(url).hostname\n\n return hostname === 'github.com' ? null : hostname\n } catch {\n return null\n }\n}\n\nfunction getReviewThreadsPage(id: string, hostname: string | null, after: string | null): ReviewThreadsResponse {\n const args = ['api', 'graphql']\n\n if (hostname) {\n args.push('--hostname', hostname)\n }\n\n args.push('-f', `query=${REVIEW_THREADS_QUERY}`, '-F', `id=${id}`)\n\n if (after) {\n args.push('-F', `after=${after}`)\n }\n\n const result = runGh(args)\n\n if (result.status !== 0) {\n throw formatOperationalError('Failed to fetch review threads', result)\n }\n\n return parseJson<ReviewThreadsResponse>(result.stdout, 'Failed to parse review threads response')\n}\n\nfunction getUnresolvedThreads(id: string, hostname: string | null): PullRequestThread[] {\n const unresolvedThreads: PullRequestThread[] = []\n let after: string | null = null\n\n do {\n const response = getReviewThreadsPage(id, hostname, after)\n const errorMessage =\n response.errors\n ?.map((error) => normalizeOutput(error?.message))\n .filter((message) => message.length > 0)\n .join('; ') ?? ''\n\n if (errorMessage.length > 0) {\n throw new Error(`Failed to fetch review threads: ${errorMessage}`)\n }\n\n const reviewThreads = response.data?.node?.reviewThreads\n const nodes = reviewThreads?.nodes\n const hasNextPage = reviewThreads?.pageInfo?.hasNextPage\n\n if (!Array.isArray(nodes) || typeof hasNextPage !== 'boolean') {\n throw new Error('Failed to fetch review threads: Pull request review threads were not returned')\n }\n\n for (const thread of nodes) {\n if (!thread || thread.isResolved === true) {\n continue\n }\n\n const comments = Array.isArray(thread.comments?.nodes) ? thread.comments.nodes : []\n const reviewComment = comments[0] ?? null\n\n unresolvedThreads.push({\n author: normalizeOutput(reviewComment?.author?.login) || '(unknown author)',\n outdated: thread.isOutdated === true || reviewComment?.outdated === true,\n path: normalizeOutput(reviewComment?.path) || '(unknown file)',\n preview: getPreview(reviewComment?.body ?? ''),\n url: normalizeOutput(reviewComment?.url) || '(missing comment url)',\n })\n }\n\n const endCursor = normalizeOutput(reviewThreads?.pageInfo?.endCursor)\n after = hasNextPage ? endCursor || null : null\n } while (after)\n\n return unresolvedThreads.sort((left, right) => {\n const pathComparison = left.path.localeCompare(right.path)\n\n if (pathComparison !== 0) {\n return pathComparison\n }\n\n return left.url.localeCompare(right.url)\n })\n}\n\nfunction printSummaryThreads(threads: PullRequestThread[]): void {\n for (const thread of threads) {\n console.log(thread.url)\n }\n}\n\nfunction printVerboseThreads(threads: PullRequestThread[]): void {\n for (const thread of threads) {\n const author = sanitizeTerminalText(thread.author)\n const outdatedMarker = thread.outdated ? ' (outdated)' : ''\n const path = sanitizeTerminalText(thread.path)\n const preview = sanitizeTerminalText(thread.preview)\n\n console.log(`${thread.url} | ${author} | ${path}${outdatedMarker} | ${preview}`)\n }\n}\n\nfunction printSummary(pullRequest: PullRequestResponse, unresolvedCount: number): void {\n const stateLabel = pullRequest.state === 'open' ? '' : ` (${pullRequest.state})`\n console.log(\n `PR #${pullRequest.number}${stateLabel} has ${unresolvedCount} unresolved review thread(s): ${pullRequest.url}`,\n )\n}\n\nfunction getJsonOutput(\n pullRequest: PullRequestResponse | null,\n unresolvedThreads: PullRequestThread[],\n status: JsonOutput['status'],\n): JsonOutput {\n return {\n pullRequest: pullRequest\n ? {\n number: pullRequest.number,\n state: pullRequest.state,\n title: pullRequest.title,\n url: pullRequest.url,\n }\n : null,\n status,\n threads: unresolvedThreads,\n unresolvedCount: unresolvedThreads.length,\n }\n}\n\nfunction printJsonOutput(\n pullRequest: PullRequestResponse | null,\n unresolvedThreads: PullRequestThread[],\n status: JsonOutput['status'],\n): void {\n console.log(JSON.stringify(getJsonOutput(pullRequest, unresolvedThreads, status), null, 2))\n}\n\nfunction printThreads(verbose: boolean, unresolvedThreads: PullRequestThread[]): void {\n if (verbose) {\n printVerboseThreads(unresolvedThreads)\n return\n }\n\n printSummaryThreads(unresolvedThreads)\n}\n\nfunction printOutput(pullRequest: PullRequestResponse, unresolvedThreads: PullRequestThread[], verbose: boolean): void {\n printSummary(pullRequest, unresolvedThreads.length)\n\n if (unresolvedThreads.length > 0) {\n printThreads(verbose, unresolvedThreads)\n }\n}\n\nfunction runGhPrUnresolved(options: CommandOptions): number {\n const pullRequest = getCurrentPullRequest()\n\n if (!pullRequest) {\n if (options.json) {\n printJsonOutput(null, [], 'no_pr')\n return 2\n }\n\n console.log(NO_PR_MESSAGE)\n return 2\n }\n\n const unresolvedThreads = getUnresolvedThreads(pullRequest.id, getPullRequestHostname(pullRequest.url))\n\n if (options.json) {\n printJsonOutput(pullRequest, unresolvedThreads, unresolvedThreads.length === 0 ? 'clean' : 'unresolved')\n } else {\n printOutput(pullRequest, unresolvedThreads, Boolean(options.verbose))\n }\n\n return unresolvedThreads.length > 0 ? 1 : 0\n}\n\nfunction getAvailableRemotesLabel(remotes: string[]): string {\n return remotes.length > 0 ? remotes.join(', ') : '(none)'\n}\n\nfunction getCurrentCheckoutState(): CheckoutState {\n const branchResult = runGit(['branch', '--show-current'])\n\n if (branchResult.status !== 0) {\n throw formatOperationalError('Failed to resolve current checkout', branchResult)\n }\n\n const branch = normalizeOutput(branchResult.stdout)\n\n if (branch.length > 0) {\n return {\n kind: 'branch',\n ref: branch,\n }\n }\n\n const detachedHeadResult = runGit(['rev-parse', '--verify', 'HEAD'])\n\n if (detachedHeadResult.status !== 0) {\n throw formatOperationalError('Failed to resolve current checkout', detachedHeadResult)\n }\n\n const commit = normalizeOutput(detachedHeadResult.stdout)\n\n if (commit.length === 0) {\n throw new Error('Failed to resolve current checkout: HEAD did not resolve to a commit')\n }\n\n return {\n kind: 'detached',\n ref: commit,\n }\n}\n\nfunction getGitRemotes(): string[] {\n const result = runGit(['remote'])\n\n if (result.status !== 0) {\n throw formatOperationalError('Failed to list git remotes', result)\n }\n\n return normalizeOutput(result.stdout)\n .split(/\\r?\\n/u)\n .map((remote) => remote.trim())\n .filter((remote) => remote.length > 0)\n .sort((left, right) => left.localeCompare(right))\n}\n\nfunction getSyncRemoteLabel(role: 'destination' | 'source'): string {\n return role === 'destination' ? 'Destination' : 'Source'\n}\n\nfunction getSyncableBranches(source: string): string[] {\n return SYNC_UPSTREAM_BRANCHES.filter((branch) => hasFetchedRemoteBranch(source, branch))\n}\n\nfunction hasFetchedRemoteBranch(source: string, branch: string): boolean {\n const ref = `refs/remotes/${source}/${branch}`\n const result = runGit(['show-ref', '--verify', '--quiet', ref])\n\n if (result.status === 0) {\n return true\n }\n\n if (result.status === 1) {\n return false\n }\n\n throw formatOperationalError(`Failed to resolve ${source}/${branch}`, result)\n}\n\nfunction ensureSyncRemoteExists(remote: string, remotes: string[], role: 'destination' | 'source'): void {\n if (remotes.includes(remote)) {\n return\n }\n\n throw new Error(\n `${getSyncRemoteLabel(role)} remote \"${remote}\" not found. Available remotes: ${getAvailableRemotesLabel(remotes)}`,\n )\n}\n\nfunction fetchRemote(remote: string): void {\n const result = runGit(['fetch', remote])\n\n if (result.status !== 0) {\n throw formatOperationalError(`Failed to fetch ${remote}`, result)\n }\n}\n\nfunction restoreOriginalCheckout(checkoutState: CheckoutState): Error | null {\n const result =\n checkoutState.kind === 'branch'\n ? runGit(['checkout', checkoutState.ref])\n : runGit(['checkout', '--detach', checkoutState.ref])\n\n if (result.status === 0) {\n return null\n }\n\n const destination =\n checkoutState.kind === 'branch' ? `branch \"${checkoutState.ref}\"` : `detached HEAD at ${checkoutState.ref}`\n\n return formatOperationalError(`Failed to restore original checkout to ${destination}`, result)\n}\n\nfunction syncBranch(branch: string, destination: string, source: string): void {\n const checkoutResult = runGit(['checkout', '-B', branch, `refs/remotes/${source}/${branch}`])\n\n if (checkoutResult.status !== 0) {\n throw formatOperationalError(`Failed to check out ${branch} from ${source}/${branch}`, checkoutResult)\n }\n\n const pushResult = runGit(['push', destination, `${branch}:${branch}`])\n\n if (pushResult.status !== 0) {\n throw formatOperationalError(`Failed to push ${branch} to ${destination}`, pushResult)\n }\n}\n\nfunction runSyncUpstream(destinationOption: string | undefined, sourceOption: string | undefined): number {\n const destination = normalizeOutput(destinationOption) || SYNC_UPSTREAM_DEFAULT_DESTINATION\n const source = normalizeOutput(sourceOption) || SYNC_UPSTREAM_DEFAULT_SOURCE\n const remotes = getGitRemotes()\n\n ensureSyncRemoteExists(destination, remotes, 'destination')\n ensureSyncRemoteExists(source, remotes, 'source')\n\n fetchRemote(source)\n\n const branches = getSyncableBranches(source)\n\n if (branches.length === 0) {\n console.log(`No syncable branches found on ${source}. Checked: ${SYNC_UPSTREAM_BRANCHES.join(', ')}`)\n return NO_SYNCABLE_BRANCHES_EXIT_CODE\n }\n\n const originalCheckout = getCurrentCheckoutState()\n let syncError: Error | null = null\n\n console.log(`Syncing ${branches.join(', ')} from ${source} to ${destination}`)\n\n for (const branch of branches) {\n try {\n syncBranch(branch, destination, source)\n console.log(`Synced ${branch} to ${destination}`)\n } catch (error) {\n syncError = error instanceof Error ? error : new Error(String(error))\n break\n }\n }\n\n const restoreError = restoreOriginalCheckout(originalCheckout)\n\n if (syncError && restoreError) {\n throw new Error(`${syncError.message}\\n${restoreError.message}`)\n }\n\n if (syncError) {\n throw syncError\n }\n\n if (restoreError) {\n throw restoreError\n }\n\n return 0\n}\n\nfunction createProgram(): Command {\n const program = new Command()\n\n program.name('hivectl').description('Common local and GitHub workflow helpers').exitOverride()\n\n program\n .command('gh-pr-unresolved')\n .description('Show unresolved review threads on the pull request for the current branch')\n .option('--json', 'show unresolved review threads as JSON')\n .option('-v, --verbose', 'show unresolved review threads in detail')\n .action((options: CommandOptions) => {\n process.exitCode = runGhPrUnresolved(options)\n })\n\n program\n .command('sync-upstream')\n .description('Sync dev, develop, main, and master from a source remote to a destination remote')\n .option('--destination <remote>', 'destination remote name', SYNC_UPSTREAM_DEFAULT_DESTINATION)\n .option('--source <remote>', 'source remote name', SYNC_UPSTREAM_DEFAULT_SOURCE)\n .action((options: { destination?: string; source?: string }) => {\n process.exitCode = runSyncUpstream(options.destination, options.source)\n })\n\n return program\n}\n\nasync function main(argv = process.argv): Promise<void> {\n const program = createProgram()\n\n try {\n await program.parseAsync(argv)\n\n if (typeof process.exitCode !== 'number') {\n process.exitCode = 0\n }\n } catch (error) {\n if (error instanceof CommanderError) {\n process.exitCode = error.code === 'commander.helpDisplayed' ? 0 : error.exitCode\n return\n }\n\n const message = error instanceof Error ? error.message : String(error)\n console.error(message)\n process.exitCode = 1\n }\n}\n\nvoid main()\n"],"mappings":";;;;;AA2FA,MAAM,gBAAgB;AACtB,MAAM,iCAAiC;AAEvC,MAAM,wBAAwB,IAAI,OAChC,OAAO,GAAG,2EACV,KACD;AAED,MAAM,qBAAqB,IAAI,OAAO,OAAO,GAAG,yBAAyB,KAAK;AAC9E,MAAM,qBAAqB;AAC3B,MAAM,yBAAyB;CAAC;CAAO;CAAW;CAAQ;CAAS;AACnE,MAAM,oCAAoC;AAC1C,MAAM,+BAA+B;AACrC,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8B7B,SAAS,gBAAgB,OAA0C;AACjE,QAAO,OAAO,MAAM,IAAI;;AAG1B,SAAS,uBAAuB,QAAgB,QAA8B;CAC5E,MAAM,SAAS,gBAAgB,OAAO,OAAO,IAAI,gBAAgB,OAAO,OAAO;AAE/E,QAAO,IAAI,MAAM,SAAS,GAAG,OAAO,IAAI,WAAW,OAAO;;AAG5D,SAAS,UAAa,OAAe,SAAoB;AACvD,KAAI;AACF,SAAO,KAAK,MAAM,MAAM;UACjB,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,QAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,UAAU;;;AAI7C,SAAS,sBAAsB,OAAyC;AACtE,KAAI,OAAO,UAAU,SACnB,QAAO;AAGT,SAAQ,MAAM,aAAa,EAA3B;EACE,KAAK,SACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,OACH,QAAO;EACT,QACE,QAAO;;;AAIb,SAAS,sBAAsB,OAA4C;CACzE,MAAM,cAAc;CAUpB,MAAM,QAAQ,sBAAsB,aAAa,MAAM;AAEvD,KACE,CAAC,eACD,OAAO,gBAAgB,YACvB,OAAO,YAAY,OAAO,YAC1B,YAAY,GAAG,WAAW,KAC1B,OAAO,YAAY,WAAW,YAC9B,CAAC,SACD,OAAO,YAAY,UAAU,YAC7B,OAAO,YAAY,QAAQ,SAE3B,QAAO;AAGT,QAAO;EACL,IAAI,YAAY;EAChB,QAAQ,YAAY;EACpB;EACA,OAAO,YAAY;EACnB,KAAK,YAAY;EAClB;;AAGH,SAAS,yBAAyB,OAAoC;CACpE,MAAM,cAAc,sBAAsB,UAAmB,OAAO,wCAAwC,CAAC;AAE7G,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,0FAA0F;AAG5G,QAAO;;AAGT,SAAS,WAAW,MAAsB;CACxC,MAAM,YAAY,KACf,MAAM,SAAS,CACf,KAAK,SAAS,KAAK,MAAM,CAAC,CAC1B,MAAM,SAAS,KAAK,SAAS,EAAE;AAElC,KAAI,CAAC,UACH,QAAO;AAGT,KAAI,UAAU,UAAU,mBACtB,QAAO;AAGT,QAAO,GAAG,UAAU,MAAM,GAAG,qBAAqB,EAAE,CAAC;;AAGvD,SAAS,qBAAqB,OAAuB;AACnD,QAAO,MAAM,QAAQ,uBAAuB,GAAG,CAAC,QAAQ,oBAAoB,GAAG;;AAGjF,SAAS,MAAM,MAA+B;CAC5C,MAAM,2CAAmB,MAAM,MAAM;EACnC,UAAU;EACV,KAAK,QAAQ;EACd,CAAC;AAEF,KAAI,OAAO,OAAO;AAChB,MAAI,UAAU,OAAO,SAAS,OAAO,MAAM,SAAS,SAClD,OAAM,IAAI,MAAM,iEAAiE;AAGnF,QAAM,IAAI,MAAM,qBAAqB,OAAO,MAAM,UAAU;;AAG9D,QAAO;EACL,QAAQ,OAAO,UAAU;EACzB,QAAQ,OAAO,UAAU;EACzB,QAAQ,OAAO,UAAU;EAC1B;;AAGH,SAAS,OAAO,MAA+B;CAC7C,MAAM,2CAAmB,OAAO,MAAM;EACpC,UAAU;EACV,KAAK,QAAQ;EACd,CAAC;AAEF,KAAI,OAAO,OAAO;AAChB,MAAI,UAAU,OAAO,SAAS,OAAO,MAAM,SAAS,SAClD,OAAM,IAAI,MAAM,mEAAmE;AAGrF,QAAM,IAAI,MAAM,sBAAsB,OAAO,MAAM,UAAU;;AAG/D,QAAO;EACL,QAAQ,OAAO,UAAU;EACzB,QAAQ,OAAO,UAAU;EACzB,QAAQ,OAAO,UAAU;EAC1B;;AAGH,SAAS,uBAAuB,QAAgC;CAC9D,MAAM,SAAS,GAAG,gBAAgB,OAAO,OAAO,CAAC,GAAG,gBAAgB,OAAO,OAAO,GAAG,aAAa;AAElG,QACE,OAAO,SAAS,qCAAqC,IACrD,OAAO,SAAS,oCAAoC,IACpD,OAAO,SAAS,oBAAoB;;AAIxC,SAAS,wBAAoD;CAC3D,MAAM,SAAS,MAAM;EAAC;EAAM;EAAQ;EAAU;EAA4B,CAAC;AAE3E,KAAI,OAAO,WAAW,EACpB,QAAO,yBAAyB,OAAO,OAAO;AAGhD,KAAI,uBAAuB,OAAO,CAChC,QAAO;AAGT,OAAM,uBAAuB,qDAAqD,OAAO;;AAG3F,SAAS,uBAAuB,KAA4B;AAC1D,KAAI;EACF,MAAM,WAAW,IAAI,IAAI,IAAI,CAAC;AAE9B,SAAO,aAAa,eAAe,OAAO;SACpC;AACN,SAAO;;;AAIX,SAAS,qBAAqB,IAAY,UAAyB,OAA6C;CAC9G,MAAM,OAAO,CAAC,OAAO,UAAU;AAE/B,KAAI,SACF,MAAK,KAAK,cAAc,SAAS;AAGnC,MAAK,KAAK,MAAM,SAAS,wBAAwB,MAAM,MAAM,KAAK;AAElE,KAAI,MACF,MAAK,KAAK,MAAM,SAAS,QAAQ;CAGnC,MAAM,SAAS,MAAM,KAAK;AAE1B,KAAI,OAAO,WAAW,EACpB,OAAM,uBAAuB,kCAAkC,OAAO;AAGxE,QAAO,UAAiC,OAAO,QAAQ,0CAA0C;;AAGnG,SAAS,qBAAqB,IAAY,UAA8C;CACtF,MAAM,oBAAyC,EAAE;CACjD,IAAI,QAAuB;AAE3B,IAAG;EACD,MAAM,WAAW,qBAAqB,IAAI,UAAU,MAAM;EAC1D,MAAM,eACJ,SAAS,QACL,KAAK,UAAU,gBAAgB,OAAO,QAAQ,CAAC,CAChD,QAAQ,YAAY,QAAQ,SAAS,EAAE,CACvC,KAAK,KAAK,IAAI;AAEnB,MAAI,aAAa,SAAS,EACxB,OAAM,IAAI,MAAM,mCAAmC,eAAe;EAGpE,MAAM,gBAAgB,SAAS,MAAM,MAAM;EAC3C,MAAM,QAAQ,eAAe;EAC7B,MAAM,cAAc,eAAe,UAAU;AAE7C,MAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,OAAO,gBAAgB,UAClD,OAAM,IAAI,MAAM,gFAAgF;AAGlG,OAAK,MAAM,UAAU,OAAO;AAC1B,OAAI,CAAC,UAAU,OAAO,eAAe,KACnC;GAIF,MAAM,iBADW,MAAM,QAAQ,OAAO,UAAU,MAAM,GAAG,OAAO,SAAS,QAAQ,EAAE,EACpD,MAAM;AAErC,qBAAkB,KAAK;IACrB,QAAQ,gBAAgB,eAAe,QAAQ,MAAM,IAAI;IACzD,UAAU,OAAO,eAAe,QAAQ,eAAe,aAAa;IACpE,MAAM,gBAAgB,eAAe,KAAK,IAAI;IAC9C,SAAS,WAAW,eAAe,QAAQ,GAAG;IAC9C,KAAK,gBAAgB,eAAe,IAAI,IAAI;IAC7C,CAAC;;EAGJ,MAAM,YAAY,gBAAgB,eAAe,UAAU,UAAU;AACrE,UAAQ,cAAc,aAAa,OAAO;UACnC;AAET,QAAO,kBAAkB,MAAM,MAAM,UAAU;EAC7C,MAAM,iBAAiB,KAAK,KAAK,cAAc,MAAM,KAAK;AAE1D,MAAI,mBAAmB,EACrB,QAAO;AAGT,SAAO,KAAK,IAAI,cAAc,MAAM,IAAI;GACxC;;AAGJ,SAAS,oBAAoB,SAAoC;AAC/D,MAAK,MAAM,UAAU,QACnB,SAAQ,IAAI,OAAO,IAAI;;AAI3B,SAAS,oBAAoB,SAAoC;AAC/D,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,SAAS,qBAAqB,OAAO,OAAO;EAClD,MAAM,iBAAiB,OAAO,WAAW,gBAAgB;EACzD,MAAM,OAAO,qBAAqB,OAAO,KAAK;EAC9C,MAAM,UAAU,qBAAqB,OAAO,QAAQ;AAEpD,UAAQ,IAAI,GAAG,OAAO,IAAI,KAAK,OAAO,KAAK,OAAO,eAAe,KAAK,UAAU;;;AAIpF,SAAS,aAAa,aAAkC,iBAA+B;CACrF,MAAM,aAAa,YAAY,UAAU,SAAS,KAAK,KAAK,YAAY,MAAM;AAC9E,SAAQ,IACN,OAAO,YAAY,SAAS,WAAW,OAAO,gBAAgB,gCAAgC,YAAY,MAC3G;;AAGH,SAAS,cACP,aACA,mBACA,QACY;AACZ,QAAO;EACL,aAAa,cACT;GACE,QAAQ,YAAY;GACpB,OAAO,YAAY;GACnB,OAAO,YAAY;GACnB,KAAK,YAAY;GAClB,GACD;EACJ;EACA,SAAS;EACT,iBAAiB,kBAAkB;EACpC;;AAGH,SAAS,gBACP,aACA,mBACA,QACM;AACN,SAAQ,IAAI,KAAK,UAAU,cAAc,aAAa,mBAAmB,OAAO,EAAE,MAAM,EAAE,CAAC;;AAG7F,SAAS,aAAa,SAAkB,mBAA8C;AACpF,KAAI,SAAS;AACX,sBAAoB,kBAAkB;AACtC;;AAGF,qBAAoB,kBAAkB;;AAGxC,SAAS,YAAY,aAAkC,mBAAwC,SAAwB;AACrH,cAAa,aAAa,kBAAkB,OAAO;AAEnD,KAAI,kBAAkB,SAAS,EAC7B,cAAa,SAAS,kBAAkB;;AAI5C,SAAS,kBAAkB,SAAiC;CAC1D,MAAM,cAAc,uBAAuB;AAE3C,KAAI,CAAC,aAAa;AAChB,MAAI,QAAQ,MAAM;AAChB,mBAAgB,MAAM,EAAE,EAAE,QAAQ;AAClC,UAAO;;AAGT,UAAQ,IAAI,cAAc;AAC1B,SAAO;;CAGT,MAAM,oBAAoB,qBAAqB,YAAY,IAAI,uBAAuB,YAAY,IAAI,CAAC;AAEvG,KAAI,QAAQ,KACV,iBAAgB,aAAa,mBAAmB,kBAAkB,WAAW,IAAI,UAAU,aAAa;KAExG,aAAY,aAAa,mBAAmB,QAAQ,QAAQ,QAAQ,CAAC;AAGvE,QAAO,kBAAkB,SAAS,IAAI,IAAI;;AAG5C,SAAS,yBAAyB,SAA2B;AAC3D,QAAO,QAAQ,SAAS,IAAI,QAAQ,KAAK,KAAK,GAAG;;AAGnD,SAAS,0BAAyC;CAChD,MAAM,eAAe,OAAO,CAAC,UAAU,iBAAiB,CAAC;AAEzD,KAAI,aAAa,WAAW,EAC1B,OAAM,uBAAuB,sCAAsC,aAAa;CAGlF,MAAM,SAAS,gBAAgB,aAAa,OAAO;AAEnD,KAAI,OAAO,SAAS,EAClB,QAAO;EACL,MAAM;EACN,KAAK;EACN;CAGH,MAAM,qBAAqB,OAAO;EAAC;EAAa;EAAY;EAAO,CAAC;AAEpE,KAAI,mBAAmB,WAAW,EAChC,OAAM,uBAAuB,sCAAsC,mBAAmB;CAGxF,MAAM,SAAS,gBAAgB,mBAAmB,OAAO;AAEzD,KAAI,OAAO,WAAW,EACpB,OAAM,IAAI,MAAM,uEAAuE;AAGzF,QAAO;EACL,MAAM;EACN,KAAK;EACN;;AAGH,SAAS,gBAA0B;CACjC,MAAM,SAAS,OAAO,CAAC,SAAS,CAAC;AAEjC,KAAI,OAAO,WAAW,EACpB,OAAM,uBAAuB,8BAA8B,OAAO;AAGpE,QAAO,gBAAgB,OAAO,OAAO,CAClC,MAAM,SAAS,CACf,KAAK,WAAW,OAAO,MAAM,CAAC,CAC9B,QAAQ,WAAW,OAAO,SAAS,EAAE,CACrC,MAAM,MAAM,UAAU,KAAK,cAAc,MAAM,CAAC;;AAGrD,SAAS,mBAAmB,MAAwC;AAClE,QAAO,SAAS,gBAAgB,gBAAgB;;AAGlD,SAAS,oBAAoB,QAA0B;AACrD,QAAO,uBAAuB,QAAQ,WAAW,uBAAuB,QAAQ,OAAO,CAAC;;AAG1F,SAAS,uBAAuB,QAAgB,QAAyB;CAEvE,MAAM,SAAS,OAAO;EAAC;EAAY;EAAY;EADnC,gBAAgB,OAAO,GAAG;EACwB,CAAC;AAE/D,KAAI,OAAO,WAAW,EACpB,QAAO;AAGT,KAAI,OAAO,WAAW,EACpB,QAAO;AAGT,OAAM,uBAAuB,qBAAqB,OAAO,GAAG,UAAU,OAAO;;AAG/E,SAAS,uBAAuB,QAAgB,SAAmB,MAAsC;AACvG,KAAI,QAAQ,SAAS,OAAO,CAC1B;AAGF,OAAM,IAAI,MACR,GAAG,mBAAmB,KAAK,CAAC,WAAW,OAAO,kCAAkC,yBAAyB,QAAQ,GAClH;;AAGH,SAAS,YAAY,QAAsB;CACzC,MAAM,SAAS,OAAO,CAAC,SAAS,OAAO,CAAC;AAExC,KAAI,OAAO,WAAW,EACpB,OAAM,uBAAuB,mBAAmB,UAAU,OAAO;;AAIrE,SAAS,wBAAwB,eAA4C;CAC3E,MAAM,SACJ,cAAc,SAAS,WACnB,OAAO,CAAC,YAAY,cAAc,IAAI,CAAC,GACvC,OAAO;EAAC;EAAY;EAAY,cAAc;EAAI,CAAC;AAEzD,KAAI,OAAO,WAAW,EACpB,QAAO;AAMT,QAAO,uBAAuB,0CAF5B,cAAc,SAAS,WAAW,WAAW,cAAc,IAAI,KAAK,oBAAoB,cAAc,SAEjB,OAAO;;AAGhG,SAAS,WAAW,QAAgB,aAAqB,QAAsB;CAC7E,MAAM,iBAAiB,OAAO;EAAC;EAAY;EAAM;EAAQ,gBAAgB,OAAO,GAAG;EAAS,CAAC;AAE7F,KAAI,eAAe,WAAW,EAC5B,OAAM,uBAAuB,uBAAuB,OAAO,QAAQ,OAAO,GAAG,UAAU,eAAe;CAGxG,MAAM,aAAa,OAAO;EAAC;EAAQ;EAAa,GAAG,OAAO,GAAG;EAAS,CAAC;AAEvE,KAAI,WAAW,WAAW,EACxB,OAAM,uBAAuB,kBAAkB,OAAO,MAAM,eAAe,WAAW;;AAI1F,SAAS,gBAAgB,mBAAuC,cAA0C;CACxG,MAAM,cAAc,gBAAgB,kBAAkB,IAAI;CAC1D,MAAM,SAAS,gBAAgB,aAAa,IAAI;CAChD,MAAM,UAAU,eAAe;AAE/B,wBAAuB,aAAa,SAAS,cAAc;AAC3D,wBAAuB,QAAQ,SAAS,SAAS;AAEjD,aAAY,OAAO;CAEnB,MAAM,WAAW,oBAAoB,OAAO;AAE5C,KAAI,SAAS,WAAW,GAAG;AACzB,UAAQ,IAAI,iCAAiC,OAAO,aAAa,uBAAuB,KAAK,KAAK,GAAG;AACrG,SAAO;;CAGT,MAAM,mBAAmB,yBAAyB;CAClD,IAAI,YAA0B;AAE9B,SAAQ,IAAI,WAAW,SAAS,KAAK,KAAK,CAAC,QAAQ,OAAO,MAAM,cAAc;AAE9E,MAAK,MAAM,UAAU,SACnB,KAAI;AACF,aAAW,QAAQ,aAAa,OAAO;AACvC,UAAQ,IAAI,UAAU,OAAO,MAAM,cAAc;UAC1C,OAAO;AACd,cAAY,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;AACrE;;CAIJ,MAAM,eAAe,wBAAwB,iBAAiB;AAE9D,KAAI,aAAa,aACf,OAAM,IAAI,MAAM,GAAG,UAAU,QAAQ,IAAI,aAAa,UAAU;AAGlE,KAAI,UACF,OAAM;AAGR,KAAI,aACF,OAAM;AAGR,QAAO;;AAGT,SAAS,gBAAyB;CAChC,MAAM,UAAU,IAAIA,mBAAS;AAE7B,SAAQ,KAAK,UAAU,CAAC,YAAY,2CAA2C,CAAC,cAAc;AAE9F,SACG,QAAQ,mBAAmB,CAC3B,YAAY,4EAA4E,CACxF,OAAO,UAAU,yCAAyC,CAC1D,OAAO,iBAAiB,2CAA2C,CACnE,QAAQ,YAA4B;AACnC,UAAQ,WAAW,kBAAkB,QAAQ;GAC7C;AAEJ,SACG,QAAQ,gBAAgB,CACxB,YAAY,mFAAmF,CAC/F,OAAO,0BAA0B,2BAA2B,kCAAkC,CAC9F,OAAO,qBAAqB,sBAAsB,6BAA6B,CAC/E,QAAQ,YAAuD;AAC9D,UAAQ,WAAW,gBAAgB,QAAQ,aAAa,QAAQ,OAAO;GACvE;AAEJ,QAAO;;AAGT,eAAe,KAAK,OAAO,QAAQ,MAAqB;CACtD,MAAM,UAAU,eAAe;AAE/B,KAAI;AACF,QAAM,QAAQ,WAAW,KAAK;AAE9B,MAAI,OAAO,QAAQ,aAAa,SAC9B,SAAQ,WAAW;UAEd,OAAO;AACd,MAAI,iBAAiBC,0BAAgB;AACnC,WAAQ,WAAW,MAAM,SAAS,4BAA4B,IAAI,MAAM;AACxE;;EAGF,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,UAAQ,MAAM,QAAQ;AACtB,UAAQ,WAAW;;;AAIlB,MAAM"}
|
package/dist/cli.mjs
CHANGED
|
@@ -4,9 +4,18 @@ import { Command, CommanderError } from "commander";
|
|
|
4
4
|
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
const NO_PR_MESSAGE = "No pull request found for current branch";
|
|
7
|
+
const NO_SYNCABLE_BRANCHES_EXIT_CODE = 2;
|
|
7
8
|
const ANSI_ESCAPE_SEQUENCES = new RegExp(String.raw`\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))`, "gu");
|
|
8
9
|
const CONTROL_CHARACTERS = new RegExp(String.raw`[\u0000-\u001f\u007f]`, "gu");
|
|
9
10
|
const MAX_PREVIEW_LENGTH = 120;
|
|
11
|
+
const SYNC_UPSTREAM_BRANCHES = [
|
|
12
|
+
"dev",
|
|
13
|
+
"develop",
|
|
14
|
+
"main",
|
|
15
|
+
"master"
|
|
16
|
+
];
|
|
17
|
+
const SYNC_UPSTREAM_DEFAULT_DESTINATION = "origin";
|
|
18
|
+
const SYNC_UPSTREAM_DEFAULT_SOURCE = "upstream";
|
|
10
19
|
const REVIEW_THREADS_QUERY = `
|
|
11
20
|
query($id: ID!, $after: String) {
|
|
12
21
|
node(id: $id) {
|
|
@@ -101,6 +110,21 @@ function runGh(args) {
|
|
|
101
110
|
stdout: result.stdout ?? ""
|
|
102
111
|
};
|
|
103
112
|
}
|
|
113
|
+
function runGit(args) {
|
|
114
|
+
const result = spawnSync("git", args, {
|
|
115
|
+
encoding: "utf8",
|
|
116
|
+
env: process.env
|
|
117
|
+
});
|
|
118
|
+
if (result.error) {
|
|
119
|
+
if ("code" in result.error && result.error.code === "ENOENT") throw new Error("Failed to run git: git is not installed or not available on PATH");
|
|
120
|
+
throw new Error(`Failed to run git: ${result.error.message}`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
status: result.status ?? 1,
|
|
124
|
+
stderr: result.stderr ?? "",
|
|
125
|
+
stdout: result.stdout ?? ""
|
|
126
|
+
};
|
|
127
|
+
}
|
|
104
128
|
function isNoPullRequestFailure(result) {
|
|
105
129
|
const detail = `${normalizeOutput(result.stderr)} ${normalizeOutput(result.stdout)}`.toLowerCase();
|
|
106
130
|
return detail.includes("could not determine current branch") || detail.includes("no pull requests found for branch") || detail.includes("not on any branch");
|
|
@@ -222,12 +246,121 @@ function runGhPrUnresolved(options) {
|
|
|
222
246
|
else printOutput(pullRequest, unresolvedThreads, Boolean(options.verbose));
|
|
223
247
|
return unresolvedThreads.length > 0 ? 1 : 0;
|
|
224
248
|
}
|
|
249
|
+
function getAvailableRemotesLabel(remotes) {
|
|
250
|
+
return remotes.length > 0 ? remotes.join(", ") : "(none)";
|
|
251
|
+
}
|
|
252
|
+
function getCurrentCheckoutState() {
|
|
253
|
+
const branchResult = runGit(["branch", "--show-current"]);
|
|
254
|
+
if (branchResult.status !== 0) throw formatOperationalError("Failed to resolve current checkout", branchResult);
|
|
255
|
+
const branch = normalizeOutput(branchResult.stdout);
|
|
256
|
+
if (branch.length > 0) return {
|
|
257
|
+
kind: "branch",
|
|
258
|
+
ref: branch
|
|
259
|
+
};
|
|
260
|
+
const detachedHeadResult = runGit([
|
|
261
|
+
"rev-parse",
|
|
262
|
+
"--verify",
|
|
263
|
+
"HEAD"
|
|
264
|
+
]);
|
|
265
|
+
if (detachedHeadResult.status !== 0) throw formatOperationalError("Failed to resolve current checkout", detachedHeadResult);
|
|
266
|
+
const commit = normalizeOutput(detachedHeadResult.stdout);
|
|
267
|
+
if (commit.length === 0) throw new Error("Failed to resolve current checkout: HEAD did not resolve to a commit");
|
|
268
|
+
return {
|
|
269
|
+
kind: "detached",
|
|
270
|
+
ref: commit
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function getGitRemotes() {
|
|
274
|
+
const result = runGit(["remote"]);
|
|
275
|
+
if (result.status !== 0) throw formatOperationalError("Failed to list git remotes", result);
|
|
276
|
+
return normalizeOutput(result.stdout).split(/\r?\n/u).map((remote) => remote.trim()).filter((remote) => remote.length > 0).sort((left, right) => left.localeCompare(right));
|
|
277
|
+
}
|
|
278
|
+
function getSyncRemoteLabel(role) {
|
|
279
|
+
return role === "destination" ? "Destination" : "Source";
|
|
280
|
+
}
|
|
281
|
+
function getSyncableBranches(source) {
|
|
282
|
+
return SYNC_UPSTREAM_BRANCHES.filter((branch) => hasFetchedRemoteBranch(source, branch));
|
|
283
|
+
}
|
|
284
|
+
function hasFetchedRemoteBranch(source, branch) {
|
|
285
|
+
const result = runGit([
|
|
286
|
+
"show-ref",
|
|
287
|
+
"--verify",
|
|
288
|
+
"--quiet",
|
|
289
|
+
`refs/remotes/${source}/${branch}`
|
|
290
|
+
]);
|
|
291
|
+
if (result.status === 0) return true;
|
|
292
|
+
if (result.status === 1) return false;
|
|
293
|
+
throw formatOperationalError(`Failed to resolve ${source}/${branch}`, result);
|
|
294
|
+
}
|
|
295
|
+
function ensureSyncRemoteExists(remote, remotes, role) {
|
|
296
|
+
if (remotes.includes(remote)) return;
|
|
297
|
+
throw new Error(`${getSyncRemoteLabel(role)} remote "${remote}" not found. Available remotes: ${getAvailableRemotesLabel(remotes)}`);
|
|
298
|
+
}
|
|
299
|
+
function fetchRemote(remote) {
|
|
300
|
+
const result = runGit(["fetch", remote]);
|
|
301
|
+
if (result.status !== 0) throw formatOperationalError(`Failed to fetch ${remote}`, result);
|
|
302
|
+
}
|
|
303
|
+
function restoreOriginalCheckout(checkoutState) {
|
|
304
|
+
const result = checkoutState.kind === "branch" ? runGit(["checkout", checkoutState.ref]) : runGit([
|
|
305
|
+
"checkout",
|
|
306
|
+
"--detach",
|
|
307
|
+
checkoutState.ref
|
|
308
|
+
]);
|
|
309
|
+
if (result.status === 0) return null;
|
|
310
|
+
return formatOperationalError(`Failed to restore original checkout to ${checkoutState.kind === "branch" ? `branch "${checkoutState.ref}"` : `detached HEAD at ${checkoutState.ref}`}`, result);
|
|
311
|
+
}
|
|
312
|
+
function syncBranch(branch, destination, source) {
|
|
313
|
+
const checkoutResult = runGit([
|
|
314
|
+
"checkout",
|
|
315
|
+
"-B",
|
|
316
|
+
branch,
|
|
317
|
+
`refs/remotes/${source}/${branch}`
|
|
318
|
+
]);
|
|
319
|
+
if (checkoutResult.status !== 0) throw formatOperationalError(`Failed to check out ${branch} from ${source}/${branch}`, checkoutResult);
|
|
320
|
+
const pushResult = runGit([
|
|
321
|
+
"push",
|
|
322
|
+
destination,
|
|
323
|
+
`${branch}:${branch}`
|
|
324
|
+
]);
|
|
325
|
+
if (pushResult.status !== 0) throw formatOperationalError(`Failed to push ${branch} to ${destination}`, pushResult);
|
|
326
|
+
}
|
|
327
|
+
function runSyncUpstream(destinationOption, sourceOption) {
|
|
328
|
+
const destination = normalizeOutput(destinationOption) || SYNC_UPSTREAM_DEFAULT_DESTINATION;
|
|
329
|
+
const source = normalizeOutput(sourceOption) || SYNC_UPSTREAM_DEFAULT_SOURCE;
|
|
330
|
+
const remotes = getGitRemotes();
|
|
331
|
+
ensureSyncRemoteExists(destination, remotes, "destination");
|
|
332
|
+
ensureSyncRemoteExists(source, remotes, "source");
|
|
333
|
+
fetchRemote(source);
|
|
334
|
+
const branches = getSyncableBranches(source);
|
|
335
|
+
if (branches.length === 0) {
|
|
336
|
+
console.log(`No syncable branches found on ${source}. Checked: ${SYNC_UPSTREAM_BRANCHES.join(", ")}`);
|
|
337
|
+
return NO_SYNCABLE_BRANCHES_EXIT_CODE;
|
|
338
|
+
}
|
|
339
|
+
const originalCheckout = getCurrentCheckoutState();
|
|
340
|
+
let syncError = null;
|
|
341
|
+
console.log(`Syncing ${branches.join(", ")} from ${source} to ${destination}`);
|
|
342
|
+
for (const branch of branches) try {
|
|
343
|
+
syncBranch(branch, destination, source);
|
|
344
|
+
console.log(`Synced ${branch} to ${destination}`);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
syncError = error instanceof Error ? error : new Error(String(error));
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
const restoreError = restoreOriginalCheckout(originalCheckout);
|
|
350
|
+
if (syncError && restoreError) throw new Error(`${syncError.message}\n${restoreError.message}`);
|
|
351
|
+
if (syncError) throw syncError;
|
|
352
|
+
if (restoreError) throw restoreError;
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
225
355
|
function createProgram() {
|
|
226
356
|
const program = new Command();
|
|
227
357
|
program.name("hivectl").description("Common local and GitHub workflow helpers").exitOverride();
|
|
228
358
|
program.command("gh-pr-unresolved").description("Show unresolved review threads on the pull request for the current branch").option("--json", "show unresolved review threads as JSON").option("-v, --verbose", "show unresolved review threads in detail").action((options) => {
|
|
229
359
|
process.exitCode = runGhPrUnresolved(options);
|
|
230
360
|
});
|
|
361
|
+
program.command("sync-upstream").description("Sync dev, develop, main, and master from a source remote to a destination remote").option("--destination <remote>", "destination remote name", SYNC_UPSTREAM_DEFAULT_DESTINATION).option("--source <remote>", "source remote name", SYNC_UPSTREAM_DEFAULT_SOURCE).action((options) => {
|
|
362
|
+
process.exitCode = runSyncUpstream(options.destination, options.source);
|
|
363
|
+
});
|
|
231
364
|
return program;
|
|
232
365
|
}
|
|
233
366
|
async function main(argv = process.argv) {
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawnSync } from 'node:child_process'\nimport { Command, CommanderError } from 'commander'\n\ntype ReviewComment = {\n author?: {\n login?: string | null\n } | null\n body?: string | null\n outdated?: boolean | null\n path?: string | null\n url?: string | null\n}\n\ntype ReviewThreadNode = {\n comments?: {\n nodes?: Array<ReviewComment | null> | null\n } | null\n isOutdated?: boolean | null\n isResolved?: boolean | null\n}\n\ntype ReviewThreadsResponse = {\n data?: {\n node?: {\n reviewThreads?: {\n nodes?: Array<ReviewThreadNode | null> | null\n pageInfo?: {\n endCursor?: string | null\n hasNextPage?: boolean | null\n } | null\n } | null\n } | null\n } | null\n errors?: Array<{\n message?: string | null\n } | null> | null\n}\n\ntype PullRequestState = 'closed' | 'merged' | 'open'\n\ntype PullRequestResponse = {\n id: string\n number: number\n state: PullRequestState\n title: string\n url: string\n}\n\ntype PullRequestThread = {\n author: string\n outdated: boolean\n path: string\n preview: string\n url: string\n}\n\ntype CommandOptions = {\n json?: boolean\n verbose?: boolean\n}\n\ntype GhResult = {\n status: number\n stderr: string\n stdout: string\n}\n\ntype JsonOutput = {\n pullRequest: {\n number: number\n state: PullRequestState\n title: string\n url: string\n } | null\n status: 'clean' | 'no_pr' | 'unresolved'\n threads: PullRequestThread[]\n unresolvedCount: number\n}\n\nconst NO_PR_MESSAGE = 'No pull request found for current branch'\n// biome-ignore lint/complexity/useRegexLiterals: The constructor avoids embedding control characters in a regex literal.\nconst ANSI_ESCAPE_SEQUENCES = new RegExp(\n String.raw`\\u001b(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~]|\\][^\\u0007]*(?:\\u0007|\\u001b\\\\))`,\n 'gu',\n)\n// biome-ignore lint/complexity/useRegexLiterals: The constructor avoids embedding control characters in a regex literal.\nconst CONTROL_CHARACTERS = new RegExp(String.raw`[\\u0000-\\u001f\\u007f]`, 'gu')\nconst MAX_PREVIEW_LENGTH = 120\nconst REVIEW_THREADS_QUERY = `\n query($id: ID!, $after: String) {\n node(id: $id) {\n ... on PullRequest {\n reviewThreads(first: 100, after: $after) {\n nodes {\n isOutdated\n isResolved\n comments(first: 1) {\n nodes {\n author {\n login\n }\n body\n outdated\n path\n url\n }\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n }\n`\n\nfunction normalizeOutput(value: string | null | undefined): string {\n return value?.trim() ?? ''\n}\n\nfunction formatOperationalError(prefix: string, result: GhResult): Error {\n const detail = normalizeOutput(result.stderr) || normalizeOutput(result.stdout)\n\n return new Error(detail ? `${prefix}: ${detail}` : prefix)\n}\n\nfunction parseJson<T>(value: string, context: string): T {\n try {\n return JSON.parse(value) as T\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(`${context}: ${message}`)\n }\n}\n\nfunction parsePullRequestState(value: unknown): PullRequestState | null {\n if (typeof value !== 'string') {\n return null\n }\n\n switch (value.toLowerCase()) {\n case 'closed':\n return 'closed'\n case 'merged':\n return 'merged'\n case 'open':\n return 'open'\n default:\n return null\n }\n}\n\nfunction toPullRequestResponse(value: unknown): PullRequestResponse | null {\n const pullRequest = value as\n | {\n id?: unknown\n number?: unknown\n state?: unknown\n title?: unknown\n url?: unknown\n }\n | null\n | undefined\n const state = parsePullRequestState(pullRequest?.state)\n\n if (\n !pullRequest ||\n typeof pullRequest !== 'object' ||\n typeof pullRequest.id !== 'string' ||\n pullRequest.id.length === 0 ||\n typeof pullRequest.number !== 'number' ||\n !state ||\n typeof pullRequest.title !== 'string' ||\n typeof pullRequest.url !== 'string'\n ) {\n return null\n }\n\n return {\n id: pullRequest.id,\n number: pullRequest.number,\n state,\n title: pullRequest.title,\n url: pullRequest.url,\n }\n}\n\nfunction parsePullRequestResponse(value: string): PullRequestResponse {\n const pullRequest = toPullRequestResponse(parseJson<unknown>(value, 'Failed to parse pull request response'))\n\n if (!pullRequest) {\n throw new Error('Failed to parse pull request response: Response is missing required pull request fields')\n }\n\n return pullRequest\n}\n\nfunction getPreview(body: string): string {\n const firstLine = body\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .find((line) => line.length > 0)\n\n if (!firstLine) {\n return '(no preview available)'\n }\n\n if (firstLine.length <= MAX_PREVIEW_LENGTH) {\n return firstLine\n }\n\n return `${firstLine.slice(0, MAX_PREVIEW_LENGTH - 3)}...`\n}\n\nfunction sanitizeTerminalText(value: string): string {\n return value.replace(ANSI_ESCAPE_SEQUENCES, '').replace(CONTROL_CHARACTERS, '')\n}\n\nfunction runGh(args: string[]): GhResult {\n const result = spawnSync('gh', args, {\n encoding: 'utf8',\n env: process.env,\n })\n\n if (result.error) {\n if ('code' in result.error && result.error.code === 'ENOENT') {\n throw new Error('Failed to run gh: gh is not installed or not available on PATH')\n }\n\n throw new Error(`Failed to run gh: ${result.error.message}`)\n }\n\n return {\n status: result.status ?? 1,\n stderr: result.stderr ?? '',\n stdout: result.stdout ?? '',\n }\n}\n\nfunction isNoPullRequestFailure(result: GhResult): boolean {\n const detail = `${normalizeOutput(result.stderr)} ${normalizeOutput(result.stdout)}`.toLowerCase()\n\n return (\n detail.includes('could not determine current branch') ||\n detail.includes('no pull requests found for branch') ||\n detail.includes('not on any branch')\n )\n}\n\nfunction getCurrentPullRequest(): PullRequestResponse | null {\n const result = runGh(['pr', 'view', '--json', 'id,number,state,title,url'])\n\n if (result.status === 0) {\n return parsePullRequestResponse(result.stdout)\n }\n\n if (isNoPullRequestFailure(result)) {\n return null\n }\n\n throw formatOperationalError('Failed to resolve pull request for current branch', result)\n}\n\nfunction getPullRequestHostname(url: string): string | null {\n try {\n const hostname = new URL(url).hostname\n\n return hostname === 'github.com' ? null : hostname\n } catch {\n return null\n }\n}\n\nfunction getReviewThreadsPage(id: string, hostname: string | null, after: string | null): ReviewThreadsResponse {\n const args = ['api', 'graphql']\n\n if (hostname) {\n args.push('--hostname', hostname)\n }\n\n args.push('-f', `query=${REVIEW_THREADS_QUERY}`, '-F', `id=${id}`)\n\n if (after) {\n args.push('-F', `after=${after}`)\n }\n\n const result = runGh(args)\n\n if (result.status !== 0) {\n throw formatOperationalError('Failed to fetch review threads', result)\n }\n\n return parseJson<ReviewThreadsResponse>(result.stdout, 'Failed to parse review threads response')\n}\n\nfunction getUnresolvedThreads(id: string, hostname: string | null): PullRequestThread[] {\n const unresolvedThreads: PullRequestThread[] = []\n let after: string | null = null\n\n do {\n const response = getReviewThreadsPage(id, hostname, after)\n const errorMessage =\n response.errors\n ?.map((error) => normalizeOutput(error?.message))\n .filter((message) => message.length > 0)\n .join('; ') ?? ''\n\n if (errorMessage.length > 0) {\n throw new Error(`Failed to fetch review threads: ${errorMessage}`)\n }\n\n const reviewThreads = response.data?.node?.reviewThreads\n const nodes = reviewThreads?.nodes\n const hasNextPage = reviewThreads?.pageInfo?.hasNextPage\n\n if (!Array.isArray(nodes) || typeof hasNextPage !== 'boolean') {\n throw new Error('Failed to fetch review threads: Pull request review threads were not returned')\n }\n\n for (const thread of nodes) {\n if (!thread || thread.isResolved === true) {\n continue\n }\n\n const comments = Array.isArray(thread.comments?.nodes) ? thread.comments.nodes : []\n const reviewComment = comments[0] ?? null\n\n unresolvedThreads.push({\n author: normalizeOutput(reviewComment?.author?.login) || '(unknown author)',\n outdated: thread.isOutdated === true || reviewComment?.outdated === true,\n path: normalizeOutput(reviewComment?.path) || '(unknown file)',\n preview: getPreview(reviewComment?.body ?? ''),\n url: normalizeOutput(reviewComment?.url) || '(missing comment url)',\n })\n }\n\n const endCursor = normalizeOutput(reviewThreads?.pageInfo?.endCursor)\n after = hasNextPage ? endCursor || null : null\n } while (after)\n\n return unresolvedThreads.sort((left, right) => {\n const pathComparison = left.path.localeCompare(right.path)\n\n if (pathComparison !== 0) {\n return pathComparison\n }\n\n return left.url.localeCompare(right.url)\n })\n}\n\nfunction printSummaryThreads(threads: PullRequestThread[]): void {\n for (const thread of threads) {\n console.log(thread.url)\n }\n}\n\nfunction printVerboseThreads(threads: PullRequestThread[]): void {\n for (const thread of threads) {\n const author = sanitizeTerminalText(thread.author)\n const outdatedMarker = thread.outdated ? ' (outdated)' : ''\n const path = sanitizeTerminalText(thread.path)\n const preview = sanitizeTerminalText(thread.preview)\n\n console.log(`${thread.url} | ${author} | ${path}${outdatedMarker} | ${preview}`)\n }\n}\n\nfunction printSummary(pullRequest: PullRequestResponse, unresolvedCount: number): void {\n const stateLabel = pullRequest.state === 'open' ? '' : ` (${pullRequest.state})`\n console.log(\n `PR #${pullRequest.number}${stateLabel} has ${unresolvedCount} unresolved review thread(s): ${pullRequest.url}`,\n )\n}\n\nfunction getJsonOutput(\n pullRequest: PullRequestResponse | null,\n unresolvedThreads: PullRequestThread[],\n status: JsonOutput['status'],\n): JsonOutput {\n return {\n pullRequest: pullRequest\n ? {\n number: pullRequest.number,\n state: pullRequest.state,\n title: pullRequest.title,\n url: pullRequest.url,\n }\n : null,\n status,\n threads: unresolvedThreads,\n unresolvedCount: unresolvedThreads.length,\n }\n}\n\nfunction printJsonOutput(\n pullRequest: PullRequestResponse | null,\n unresolvedThreads: PullRequestThread[],\n status: JsonOutput['status'],\n): void {\n console.log(JSON.stringify(getJsonOutput(pullRequest, unresolvedThreads, status), null, 2))\n}\n\nfunction printThreads(verbose: boolean, unresolvedThreads: PullRequestThread[]): void {\n if (verbose) {\n printVerboseThreads(unresolvedThreads)\n return\n }\n\n printSummaryThreads(unresolvedThreads)\n}\n\nfunction printOutput(pullRequest: PullRequestResponse, unresolvedThreads: PullRequestThread[], verbose: boolean): void {\n printSummary(pullRequest, unresolvedThreads.length)\n\n if (unresolvedThreads.length > 0) {\n printThreads(verbose, unresolvedThreads)\n }\n}\n\nfunction runGhPrUnresolved(options: CommandOptions): number {\n const pullRequest = getCurrentPullRequest()\n\n if (!pullRequest) {\n if (options.json) {\n printJsonOutput(null, [], 'no_pr')\n return 2\n }\n\n console.log(NO_PR_MESSAGE)\n return 2\n }\n\n const unresolvedThreads = getUnresolvedThreads(pullRequest.id, getPullRequestHostname(pullRequest.url))\n\n if (options.json) {\n printJsonOutput(pullRequest, unresolvedThreads, unresolvedThreads.length === 0 ? 'clean' : 'unresolved')\n } else {\n printOutput(pullRequest, unresolvedThreads, Boolean(options.verbose))\n }\n\n return unresolvedThreads.length > 0 ? 1 : 0\n}\n\nfunction createProgram(): Command {\n const program = new Command()\n\n program.name('hivectl').description('Common local and GitHub workflow helpers').exitOverride()\n\n program\n .command('gh-pr-unresolved')\n .description('Show unresolved review threads on the pull request for the current branch')\n .option('--json', 'show unresolved review threads as JSON')\n .option('-v, --verbose', 'show unresolved review threads in detail')\n .action((options: CommandOptions) => {\n process.exitCode = runGhPrUnresolved(options)\n })\n\n return program\n}\n\nasync function main(argv = process.argv): Promise<void> {\n const program = createProgram()\n\n try {\n await program.parseAsync(argv)\n\n if (typeof process.exitCode !== 'number') {\n process.exitCode = 0\n }\n } catch (error) {\n if (error instanceof CommanderError) {\n process.exitCode = error.code === 'commander.helpDisplayed' ? 0 : error.exitCode\n return\n }\n\n const message = error instanceof Error ? error.message : String(error)\n console.error(message)\n process.exitCode = 1\n }\n}\n\nvoid main()\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawnSync } from 'node:child_process'\nimport { Command, CommanderError } from 'commander'\n\ntype ReviewComment = {\n author?: {\n login?: string | null\n } | null\n body?: string | null\n outdated?: boolean | null\n path?: string | null\n url?: string | null\n}\n\ntype ReviewThreadNode = {\n comments?: {\n nodes?: Array<ReviewComment | null> | null\n } | null\n isOutdated?: boolean | null\n isResolved?: boolean | null\n}\n\ntype ReviewThreadsResponse = {\n data?: {\n node?: {\n reviewThreads?: {\n nodes?: Array<ReviewThreadNode | null> | null\n pageInfo?: {\n endCursor?: string | null\n hasNextPage?: boolean | null\n } | null\n } | null\n } | null\n } | null\n errors?: Array<{\n message?: string | null\n } | null> | null\n}\n\ntype PullRequestState = 'closed' | 'merged' | 'open'\n\ntype PullRequestResponse = {\n id: string\n number: number\n state: PullRequestState\n title: string\n url: string\n}\n\ntype PullRequestThread = {\n author: string\n outdated: boolean\n path: string\n preview: string\n url: string\n}\n\ntype CheckoutState =\n | {\n kind: 'branch'\n ref: string\n }\n | {\n kind: 'detached'\n ref: string\n }\n\ntype CommandOptions = {\n json?: boolean\n verbose?: boolean\n}\n\ntype CommandResult = {\n status: number\n stderr: string\n stdout: string\n}\n\ntype JsonOutput = {\n pullRequest: {\n number: number\n state: PullRequestState\n title: string\n url: string\n } | null\n status: 'clean' | 'no_pr' | 'unresolved'\n threads: PullRequestThread[]\n unresolvedCount: number\n}\n\nconst NO_PR_MESSAGE = 'No pull request found for current branch'\nconst NO_SYNCABLE_BRANCHES_EXIT_CODE = 2\n// biome-ignore lint/complexity/useRegexLiterals: The constructor avoids embedding control characters in a regex literal.\nconst ANSI_ESCAPE_SEQUENCES = new RegExp(\n String.raw`\\u001b(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~]|\\][^\\u0007]*(?:\\u0007|\\u001b\\\\))`,\n 'gu',\n)\n// biome-ignore lint/complexity/useRegexLiterals: The constructor avoids embedding control characters in a regex literal.\nconst CONTROL_CHARACTERS = new RegExp(String.raw`[\\u0000-\\u001f\\u007f]`, 'gu')\nconst MAX_PREVIEW_LENGTH = 120\nconst SYNC_UPSTREAM_BRANCHES = ['dev', 'develop', 'main', 'master'] as const\nconst SYNC_UPSTREAM_DEFAULT_DESTINATION = 'origin'\nconst SYNC_UPSTREAM_DEFAULT_SOURCE = 'upstream'\nconst REVIEW_THREADS_QUERY = `\n query($id: ID!, $after: String) {\n node(id: $id) {\n ... on PullRequest {\n reviewThreads(first: 100, after: $after) {\n nodes {\n isOutdated\n isResolved\n comments(first: 1) {\n nodes {\n author {\n login\n }\n body\n outdated\n path\n url\n }\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n }\n`\n\nfunction normalizeOutput(value: string | null | undefined): string {\n return value?.trim() ?? ''\n}\n\nfunction formatOperationalError(prefix: string, result: CommandResult): Error {\n const detail = normalizeOutput(result.stderr) || normalizeOutput(result.stdout)\n\n return new Error(detail ? `${prefix}: ${detail}` : prefix)\n}\n\nfunction parseJson<T>(value: string, context: string): T {\n try {\n return JSON.parse(value) as T\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(`${context}: ${message}`)\n }\n}\n\nfunction parsePullRequestState(value: unknown): PullRequestState | null {\n if (typeof value !== 'string') {\n return null\n }\n\n switch (value.toLowerCase()) {\n case 'closed':\n return 'closed'\n case 'merged':\n return 'merged'\n case 'open':\n return 'open'\n default:\n return null\n }\n}\n\nfunction toPullRequestResponse(value: unknown): PullRequestResponse | null {\n const pullRequest = value as\n | {\n id?: unknown\n number?: unknown\n state?: unknown\n title?: unknown\n url?: unknown\n }\n | null\n | undefined\n const state = parsePullRequestState(pullRequest?.state)\n\n if (\n !pullRequest ||\n typeof pullRequest !== 'object' ||\n typeof pullRequest.id !== 'string' ||\n pullRequest.id.length === 0 ||\n typeof pullRequest.number !== 'number' ||\n !state ||\n typeof pullRequest.title !== 'string' ||\n typeof pullRequest.url !== 'string'\n ) {\n return null\n }\n\n return {\n id: pullRequest.id,\n number: pullRequest.number,\n state,\n title: pullRequest.title,\n url: pullRequest.url,\n }\n}\n\nfunction parsePullRequestResponse(value: string): PullRequestResponse {\n const pullRequest = toPullRequestResponse(parseJson<unknown>(value, 'Failed to parse pull request response'))\n\n if (!pullRequest) {\n throw new Error('Failed to parse pull request response: Response is missing required pull request fields')\n }\n\n return pullRequest\n}\n\nfunction getPreview(body: string): string {\n const firstLine = body\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .find((line) => line.length > 0)\n\n if (!firstLine) {\n return '(no preview available)'\n }\n\n if (firstLine.length <= MAX_PREVIEW_LENGTH) {\n return firstLine\n }\n\n return `${firstLine.slice(0, MAX_PREVIEW_LENGTH - 3)}...`\n}\n\nfunction sanitizeTerminalText(value: string): string {\n return value.replace(ANSI_ESCAPE_SEQUENCES, '').replace(CONTROL_CHARACTERS, '')\n}\n\nfunction runGh(args: string[]): CommandResult {\n const result = spawnSync('gh', args, {\n encoding: 'utf8',\n env: process.env,\n })\n\n if (result.error) {\n if ('code' in result.error && result.error.code === 'ENOENT') {\n throw new Error('Failed to run gh: gh is not installed or not available on PATH')\n }\n\n throw new Error(`Failed to run gh: ${result.error.message}`)\n }\n\n return {\n status: result.status ?? 1,\n stderr: result.stderr ?? '',\n stdout: result.stdout ?? '',\n }\n}\n\nfunction runGit(args: string[]): CommandResult {\n const result = spawnSync('git', args, {\n encoding: 'utf8',\n env: process.env,\n })\n\n if (result.error) {\n if ('code' in result.error && result.error.code === 'ENOENT') {\n throw new Error('Failed to run git: git is not installed or not available on PATH')\n }\n\n throw new Error(`Failed to run git: ${result.error.message}`)\n }\n\n return {\n status: result.status ?? 1,\n stderr: result.stderr ?? '',\n stdout: result.stdout ?? '',\n }\n}\n\nfunction isNoPullRequestFailure(result: CommandResult): boolean {\n const detail = `${normalizeOutput(result.stderr)} ${normalizeOutput(result.stdout)}`.toLowerCase()\n\n return (\n detail.includes('could not determine current branch') ||\n detail.includes('no pull requests found for branch') ||\n detail.includes('not on any branch')\n )\n}\n\nfunction getCurrentPullRequest(): PullRequestResponse | null {\n const result = runGh(['pr', 'view', '--json', 'id,number,state,title,url'])\n\n if (result.status === 0) {\n return parsePullRequestResponse(result.stdout)\n }\n\n if (isNoPullRequestFailure(result)) {\n return null\n }\n\n throw formatOperationalError('Failed to resolve pull request for current branch', result)\n}\n\nfunction getPullRequestHostname(url: string): string | null {\n try {\n const hostname = new URL(url).hostname\n\n return hostname === 'github.com' ? null : hostname\n } catch {\n return null\n }\n}\n\nfunction getReviewThreadsPage(id: string, hostname: string | null, after: string | null): ReviewThreadsResponse {\n const args = ['api', 'graphql']\n\n if (hostname) {\n args.push('--hostname', hostname)\n }\n\n args.push('-f', `query=${REVIEW_THREADS_QUERY}`, '-F', `id=${id}`)\n\n if (after) {\n args.push('-F', `after=${after}`)\n }\n\n const result = runGh(args)\n\n if (result.status !== 0) {\n throw formatOperationalError('Failed to fetch review threads', result)\n }\n\n return parseJson<ReviewThreadsResponse>(result.stdout, 'Failed to parse review threads response')\n}\n\nfunction getUnresolvedThreads(id: string, hostname: string | null): PullRequestThread[] {\n const unresolvedThreads: PullRequestThread[] = []\n let after: string | null = null\n\n do {\n const response = getReviewThreadsPage(id, hostname, after)\n const errorMessage =\n response.errors\n ?.map((error) => normalizeOutput(error?.message))\n .filter((message) => message.length > 0)\n .join('; ') ?? ''\n\n if (errorMessage.length > 0) {\n throw new Error(`Failed to fetch review threads: ${errorMessage}`)\n }\n\n const reviewThreads = response.data?.node?.reviewThreads\n const nodes = reviewThreads?.nodes\n const hasNextPage = reviewThreads?.pageInfo?.hasNextPage\n\n if (!Array.isArray(nodes) || typeof hasNextPage !== 'boolean') {\n throw new Error('Failed to fetch review threads: Pull request review threads were not returned')\n }\n\n for (const thread of nodes) {\n if (!thread || thread.isResolved === true) {\n continue\n }\n\n const comments = Array.isArray(thread.comments?.nodes) ? thread.comments.nodes : []\n const reviewComment = comments[0] ?? null\n\n unresolvedThreads.push({\n author: normalizeOutput(reviewComment?.author?.login) || '(unknown author)',\n outdated: thread.isOutdated === true || reviewComment?.outdated === true,\n path: normalizeOutput(reviewComment?.path) || '(unknown file)',\n preview: getPreview(reviewComment?.body ?? ''),\n url: normalizeOutput(reviewComment?.url) || '(missing comment url)',\n })\n }\n\n const endCursor = normalizeOutput(reviewThreads?.pageInfo?.endCursor)\n after = hasNextPage ? endCursor || null : null\n } while (after)\n\n return unresolvedThreads.sort((left, right) => {\n const pathComparison = left.path.localeCompare(right.path)\n\n if (pathComparison !== 0) {\n return pathComparison\n }\n\n return left.url.localeCompare(right.url)\n })\n}\n\nfunction printSummaryThreads(threads: PullRequestThread[]): void {\n for (const thread of threads) {\n console.log(thread.url)\n }\n}\n\nfunction printVerboseThreads(threads: PullRequestThread[]): void {\n for (const thread of threads) {\n const author = sanitizeTerminalText(thread.author)\n const outdatedMarker = thread.outdated ? ' (outdated)' : ''\n const path = sanitizeTerminalText(thread.path)\n const preview = sanitizeTerminalText(thread.preview)\n\n console.log(`${thread.url} | ${author} | ${path}${outdatedMarker} | ${preview}`)\n }\n}\n\nfunction printSummary(pullRequest: PullRequestResponse, unresolvedCount: number): void {\n const stateLabel = pullRequest.state === 'open' ? '' : ` (${pullRequest.state})`\n console.log(\n `PR #${pullRequest.number}${stateLabel} has ${unresolvedCount} unresolved review thread(s): ${pullRequest.url}`,\n )\n}\n\nfunction getJsonOutput(\n pullRequest: PullRequestResponse | null,\n unresolvedThreads: PullRequestThread[],\n status: JsonOutput['status'],\n): JsonOutput {\n return {\n pullRequest: pullRequest\n ? {\n number: pullRequest.number,\n state: pullRequest.state,\n title: pullRequest.title,\n url: pullRequest.url,\n }\n : null,\n status,\n threads: unresolvedThreads,\n unresolvedCount: unresolvedThreads.length,\n }\n}\n\nfunction printJsonOutput(\n pullRequest: PullRequestResponse | null,\n unresolvedThreads: PullRequestThread[],\n status: JsonOutput['status'],\n): void {\n console.log(JSON.stringify(getJsonOutput(pullRequest, unresolvedThreads, status), null, 2))\n}\n\nfunction printThreads(verbose: boolean, unresolvedThreads: PullRequestThread[]): void {\n if (verbose) {\n printVerboseThreads(unresolvedThreads)\n return\n }\n\n printSummaryThreads(unresolvedThreads)\n}\n\nfunction printOutput(pullRequest: PullRequestResponse, unresolvedThreads: PullRequestThread[], verbose: boolean): void {\n printSummary(pullRequest, unresolvedThreads.length)\n\n if (unresolvedThreads.length > 0) {\n printThreads(verbose, unresolvedThreads)\n }\n}\n\nfunction runGhPrUnresolved(options: CommandOptions): number {\n const pullRequest = getCurrentPullRequest()\n\n if (!pullRequest) {\n if (options.json) {\n printJsonOutput(null, [], 'no_pr')\n return 2\n }\n\n console.log(NO_PR_MESSAGE)\n return 2\n }\n\n const unresolvedThreads = getUnresolvedThreads(pullRequest.id, getPullRequestHostname(pullRequest.url))\n\n if (options.json) {\n printJsonOutput(pullRequest, unresolvedThreads, unresolvedThreads.length === 0 ? 'clean' : 'unresolved')\n } else {\n printOutput(pullRequest, unresolvedThreads, Boolean(options.verbose))\n }\n\n return unresolvedThreads.length > 0 ? 1 : 0\n}\n\nfunction getAvailableRemotesLabel(remotes: string[]): string {\n return remotes.length > 0 ? remotes.join(', ') : '(none)'\n}\n\nfunction getCurrentCheckoutState(): CheckoutState {\n const branchResult = runGit(['branch', '--show-current'])\n\n if (branchResult.status !== 0) {\n throw formatOperationalError('Failed to resolve current checkout', branchResult)\n }\n\n const branch = normalizeOutput(branchResult.stdout)\n\n if (branch.length > 0) {\n return {\n kind: 'branch',\n ref: branch,\n }\n }\n\n const detachedHeadResult = runGit(['rev-parse', '--verify', 'HEAD'])\n\n if (detachedHeadResult.status !== 0) {\n throw formatOperationalError('Failed to resolve current checkout', detachedHeadResult)\n }\n\n const commit = normalizeOutput(detachedHeadResult.stdout)\n\n if (commit.length === 0) {\n throw new Error('Failed to resolve current checkout: HEAD did not resolve to a commit')\n }\n\n return {\n kind: 'detached',\n ref: commit,\n }\n}\n\nfunction getGitRemotes(): string[] {\n const result = runGit(['remote'])\n\n if (result.status !== 0) {\n throw formatOperationalError('Failed to list git remotes', result)\n }\n\n return normalizeOutput(result.stdout)\n .split(/\\r?\\n/u)\n .map((remote) => remote.trim())\n .filter((remote) => remote.length > 0)\n .sort((left, right) => left.localeCompare(right))\n}\n\nfunction getSyncRemoteLabel(role: 'destination' | 'source'): string {\n return role === 'destination' ? 'Destination' : 'Source'\n}\n\nfunction getSyncableBranches(source: string): string[] {\n return SYNC_UPSTREAM_BRANCHES.filter((branch) => hasFetchedRemoteBranch(source, branch))\n}\n\nfunction hasFetchedRemoteBranch(source: string, branch: string): boolean {\n const ref = `refs/remotes/${source}/${branch}`\n const result = runGit(['show-ref', '--verify', '--quiet', ref])\n\n if (result.status === 0) {\n return true\n }\n\n if (result.status === 1) {\n return false\n }\n\n throw formatOperationalError(`Failed to resolve ${source}/${branch}`, result)\n}\n\nfunction ensureSyncRemoteExists(remote: string, remotes: string[], role: 'destination' | 'source'): void {\n if (remotes.includes(remote)) {\n return\n }\n\n throw new Error(\n `${getSyncRemoteLabel(role)} remote \"${remote}\" not found. Available remotes: ${getAvailableRemotesLabel(remotes)}`,\n )\n}\n\nfunction fetchRemote(remote: string): void {\n const result = runGit(['fetch', remote])\n\n if (result.status !== 0) {\n throw formatOperationalError(`Failed to fetch ${remote}`, result)\n }\n}\n\nfunction restoreOriginalCheckout(checkoutState: CheckoutState): Error | null {\n const result =\n checkoutState.kind === 'branch'\n ? runGit(['checkout', checkoutState.ref])\n : runGit(['checkout', '--detach', checkoutState.ref])\n\n if (result.status === 0) {\n return null\n }\n\n const destination =\n checkoutState.kind === 'branch' ? `branch \"${checkoutState.ref}\"` : `detached HEAD at ${checkoutState.ref}`\n\n return formatOperationalError(`Failed to restore original checkout to ${destination}`, result)\n}\n\nfunction syncBranch(branch: string, destination: string, source: string): void {\n const checkoutResult = runGit(['checkout', '-B', branch, `refs/remotes/${source}/${branch}`])\n\n if (checkoutResult.status !== 0) {\n throw formatOperationalError(`Failed to check out ${branch} from ${source}/${branch}`, checkoutResult)\n }\n\n const pushResult = runGit(['push', destination, `${branch}:${branch}`])\n\n if (pushResult.status !== 0) {\n throw formatOperationalError(`Failed to push ${branch} to ${destination}`, pushResult)\n }\n}\n\nfunction runSyncUpstream(destinationOption: string | undefined, sourceOption: string | undefined): number {\n const destination = normalizeOutput(destinationOption) || SYNC_UPSTREAM_DEFAULT_DESTINATION\n const source = normalizeOutput(sourceOption) || SYNC_UPSTREAM_DEFAULT_SOURCE\n const remotes = getGitRemotes()\n\n ensureSyncRemoteExists(destination, remotes, 'destination')\n ensureSyncRemoteExists(source, remotes, 'source')\n\n fetchRemote(source)\n\n const branches = getSyncableBranches(source)\n\n if (branches.length === 0) {\n console.log(`No syncable branches found on ${source}. Checked: ${SYNC_UPSTREAM_BRANCHES.join(', ')}`)\n return NO_SYNCABLE_BRANCHES_EXIT_CODE\n }\n\n const originalCheckout = getCurrentCheckoutState()\n let syncError: Error | null = null\n\n console.log(`Syncing ${branches.join(', ')} from ${source} to ${destination}`)\n\n for (const branch of branches) {\n try {\n syncBranch(branch, destination, source)\n console.log(`Synced ${branch} to ${destination}`)\n } catch (error) {\n syncError = error instanceof Error ? error : new Error(String(error))\n break\n }\n }\n\n const restoreError = restoreOriginalCheckout(originalCheckout)\n\n if (syncError && restoreError) {\n throw new Error(`${syncError.message}\\n${restoreError.message}`)\n }\n\n if (syncError) {\n throw syncError\n }\n\n if (restoreError) {\n throw restoreError\n }\n\n return 0\n}\n\nfunction createProgram(): Command {\n const program = new Command()\n\n program.name('hivectl').description('Common local and GitHub workflow helpers').exitOverride()\n\n program\n .command('gh-pr-unresolved')\n .description('Show unresolved review threads on the pull request for the current branch')\n .option('--json', 'show unresolved review threads as JSON')\n .option('-v, --verbose', 'show unresolved review threads in detail')\n .action((options: CommandOptions) => {\n process.exitCode = runGhPrUnresolved(options)\n })\n\n program\n .command('sync-upstream')\n .description('Sync dev, develop, main, and master from a source remote to a destination remote')\n .option('--destination <remote>', 'destination remote name', SYNC_UPSTREAM_DEFAULT_DESTINATION)\n .option('--source <remote>', 'source remote name', SYNC_UPSTREAM_DEFAULT_SOURCE)\n .action((options: { destination?: string; source?: string }) => {\n process.exitCode = runSyncUpstream(options.destination, options.source)\n })\n\n return program\n}\n\nasync function main(argv = process.argv): Promise<void> {\n const program = createProgram()\n\n try {\n await program.parseAsync(argv)\n\n if (typeof process.exitCode !== 'number') {\n process.exitCode = 0\n }\n } catch (error) {\n if (error instanceof CommanderError) {\n process.exitCode = error.code === 'commander.helpDisplayed' ? 0 : error.exitCode\n return\n }\n\n const message = error instanceof Error ? error.message : String(error)\n console.error(message)\n process.exitCode = 1\n }\n}\n\nvoid main()\n"],"mappings":"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"}
|