vouchington-tooling 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/gh-cli/diff.d.mts +6 -0
- package/dist/gh-cli/diff.mjs +7 -0
- package/dist/gh-cli/exec.d.mts +16 -0
- package/dist/gh-cli/exec.mjs +18 -0
- package/dist/gh-cli/index.d.mts +5 -0
- package/dist/gh-cli/index.mjs +3 -0
- package/dist/gh-cli/pr-create.d.mts +61 -0
- package/dist/gh-cli/pr-create.mjs +80 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +3 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -207,6 +207,7 @@ import {
|
|
|
207
207
|
import { runRetrospectiveTranscript } from 'vouchington-tooling/retrospective-transcript'
|
|
208
208
|
import { appendJournal, probeBlackboard } from 'vouchington-tooling/agent-blackboard'
|
|
209
209
|
import { buildSessionFrictionReport, recordFriction } from 'vouchington-tooling/session-friction'
|
|
210
|
+
import { createPullRequest, getDiffAgainstBase, runGh, runGit } from 'vouchington-tooling/gh-cli'
|
|
210
211
|
```
|
|
211
212
|
|
|
212
213
|
`checkWorkspaceGatesPolicy` rejects tracked test assertions that hard-code the exact version of a
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RunTextCommand } from './exec.mts';
|
|
2
|
+
/**
|
|
3
|
+
* Runs `git diff <base>...HEAD` and returns the raw diff text. The base is parameterized rather
|
|
4
|
+
* than hardcoded so callers can diff against any ref (`origin/main`, a release branch, etc.).
|
|
5
|
+
*/
|
|
6
|
+
export declare function getDiffAgainstBase(runGit: RunTextCommand, base: string): Promise<string>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `git diff <base>...HEAD` and returns the raw diff text. The base is parameterized rather
|
|
3
|
+
* than hardcoded so callers can diff against any ref (`origin/main`, a release branch, etc.).
|
|
4
|
+
*/
|
|
5
|
+
export async function getDiffAgainstBase(runGit, base) {
|
|
6
|
+
return runGit(['diff', `${base}...HEAD`]);
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A text-mode command runner: argv in, stdout out. Rejects on a non-zero exit. */
|
|
2
|
+
export type RunTextCommand = (args: string[]) => Promise<string>;
|
|
3
|
+
/** The `child_process.execFile` shape `createCommandRunner` wraps — injectable for tests. */
|
|
4
|
+
export type ExecFileText = (command: string, args: string[]) => Promise<{
|
|
5
|
+
stdout: string;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Builds a `RunTextCommand` bound to a fixed binary. The default `exec` wraps `execFile` via
|
|
9
|
+
* `promisify` so a test can inject a fake without spawning a real process; `runGh`/`runGit` below
|
|
10
|
+
* are this factory applied to the two binaries this module cares about.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createCommandRunner(command: string, exec?: ExecFileText): RunTextCommand;
|
|
13
|
+
/** Runs `gh` and returns its stdout. */
|
|
14
|
+
export declare const runGh: RunTextCommand;
|
|
15
|
+
/** Runs `git` and returns its stdout. */
|
|
16
|
+
export declare const runGit: RunTextCommand;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Builds a `RunTextCommand` bound to a fixed binary. The default `exec` wraps `execFile` via
|
|
6
|
+
* `promisify` so a test can inject a fake without spawning a real process; `runGh`/`runGit` below
|
|
7
|
+
* are this factory applied to the two binaries this module cares about.
|
|
8
|
+
*/
|
|
9
|
+
export function createCommandRunner(command, exec = (cmd, args) => execFileAsync(cmd, args)) {
|
|
10
|
+
return async (args) => {
|
|
11
|
+
const { stdout } = await exec(command, args);
|
|
12
|
+
return stdout;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Runs `gh` and returns its stdout. */
|
|
16
|
+
export const runGh = createCommandRunner('gh');
|
|
17
|
+
/** Runs `git` and returns its stdout. */
|
|
18
|
+
export const runGit = createCommandRunner('git');
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createCommandRunner, runGh, runGit } from './exec.mts';
|
|
2
|
+
export type { ExecFileText, RunTextCommand } from './exec.mts';
|
|
3
|
+
export { getDiffAgainstBase } from './diff.mts';
|
|
4
|
+
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './pr-create.mts';
|
|
5
|
+
export type { AssertHeadPushedOptions, BuildGhPrCreateArgsOptions, CreatePullRequestExecutors, CreatePullRequestOptions, } from './pr-create.mts';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createCommandRunner, runGh, runGit } from './exec.mjs';
|
|
2
|
+
export { getDiffAgainstBase } from './diff.mjs';
|
|
3
|
+
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './pr-create.mjs';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RunTextCommand } from './exec.mts';
|
|
2
|
+
/** Thrown by {@link resolveHeadBranch} when `git branch --show-current` reports a detached HEAD. */
|
|
3
|
+
export declare class DetachedHeadError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
/** Thrown by {@link assertHeadPushed} when the branch has no matching ref on the remote. */
|
|
7
|
+
export declare class HeadNotPushedError extends Error {
|
|
8
|
+
constructor(branch: string, remote: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Thrown by {@link assertHeadPushed} when `branch` exists on `remote` but at a different commit
|
|
12
|
+
* than the local branch — creating the pull request now would silently use the stale remote tip.
|
|
13
|
+
*/
|
|
14
|
+
export declare class HeadOutOfDateError extends Error {
|
|
15
|
+
constructor(branch: string, remote: string);
|
|
16
|
+
}
|
|
17
|
+
/** Resolves the current branch via `git branch --show-current`, trimmed. */
|
|
18
|
+
export declare function resolveHeadBranch(runGit: RunTextCommand): Promise<string>;
|
|
19
|
+
export type AssertHeadPushedOptions = {
|
|
20
|
+
branch: string;
|
|
21
|
+
remote?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Confirms `branch` has a matching, up-to-date ref on `remote` (default `origin`) via
|
|
25
|
+
* `git ls-remote --heads` and `git rev-parse`. Both use the full `refs/heads/<branch>` path
|
|
26
|
+
* rather than the bare branch name: `ls-remote`'s pattern otherwise matches any ref whose name
|
|
27
|
+
* ends with `branch` (not just an exact `refs/heads/<branch>`), and bare `rev-parse <branch>`
|
|
28
|
+
* resolves `refs/tags/<branch>` before `refs/heads/<branch>` when a same-named tag exists, either
|
|
29
|
+
* of which could compare the wrong commit. `ls-remote --heads` exits `0` with empty stdout when
|
|
30
|
+
* there is no match, so a missing branch is checked via stdout rather than the exit code. A
|
|
31
|
+
* remote ref that exists but points at a different commit than the local branch means local HEAD
|
|
32
|
+
* has commits the remote does not — that fails with {@link HeadOutOfDateError} rather than
|
|
33
|
+
* silently creating the pull request from the stale remote tip.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assertHeadPushed(runGit: RunTextCommand, { branch, remote }: AssertHeadPushedOptions): Promise<void>;
|
|
36
|
+
export type BuildGhPrCreateArgsOptions = {
|
|
37
|
+
base?: string;
|
|
38
|
+
bodyFile: string;
|
|
39
|
+
draft?: boolean;
|
|
40
|
+
head: string;
|
|
41
|
+
labels?: readonly string[];
|
|
42
|
+
reviewers?: readonly string[];
|
|
43
|
+
title: string;
|
|
44
|
+
};
|
|
45
|
+
/** Pure argv builder for `gh pr create`. `head` is always passed explicitly (never omitted). */
|
|
46
|
+
export declare function buildGhPrCreateArgs(options: BuildGhPrCreateArgsOptions): string[];
|
|
47
|
+
export type CreatePullRequestExecutors = {
|
|
48
|
+
runGh: RunTextCommand;
|
|
49
|
+
runGit: RunTextCommand;
|
|
50
|
+
};
|
|
51
|
+
export type CreatePullRequestOptions = Omit<BuildGhPrCreateArgsOptions, 'head'> & {
|
|
52
|
+
head?: string;
|
|
53
|
+
remote?: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Creates a pull request with `gh pr create --head <branch>`, resolving and verifying the head
|
|
57
|
+
* branch first so the "must first push the current branch" non-interactive abort can never
|
|
58
|
+
* happen — instead an unpushed branch fails fast with {@link HeadNotPushedError}. Returns the
|
|
59
|
+
* trimmed PR URL that `gh pr create` prints to stdout.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createPullRequest({ runGh, runGit }: CreatePullRequestExecutors, options: CreatePullRequestOptions): Promise<string>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Thrown by {@link resolveHeadBranch} when `git branch --show-current` reports a detached HEAD. */
|
|
2
|
+
export class DetachedHeadError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super('cannot resolve a pull request head branch from a detached HEAD');
|
|
5
|
+
this.name = 'DetachedHeadError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Thrown by {@link assertHeadPushed} when the branch has no matching ref on the remote. */
|
|
9
|
+
export class HeadNotPushedError extends Error {
|
|
10
|
+
constructor(branch, remote) {
|
|
11
|
+
super(`branch "${branch}" is not on remote "${remote}" — push it before creating the pull request`);
|
|
12
|
+
this.name = 'HeadNotPushedError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Thrown by {@link assertHeadPushed} when `branch` exists on `remote` but at a different commit
|
|
17
|
+
* than the local branch — creating the pull request now would silently use the stale remote tip.
|
|
18
|
+
*/
|
|
19
|
+
export class HeadOutOfDateError extends Error {
|
|
20
|
+
constructor(branch, remote) {
|
|
21
|
+
super(`branch "${branch}" on remote "${remote}" does not match the local branch — push the latest commits before creating the pull request`);
|
|
22
|
+
this.name = 'HeadOutOfDateError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Resolves the current branch via `git branch --show-current`, trimmed. */
|
|
26
|
+
export async function resolveHeadBranch(runGit) {
|
|
27
|
+
const branch = (await runGit(['branch', '--show-current'])).trim();
|
|
28
|
+
if (branch === '')
|
|
29
|
+
throw new DetachedHeadError();
|
|
30
|
+
return branch;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Confirms `branch` has a matching, up-to-date ref on `remote` (default `origin`) via
|
|
34
|
+
* `git ls-remote --heads` and `git rev-parse`. Both use the full `refs/heads/<branch>` path
|
|
35
|
+
* rather than the bare branch name: `ls-remote`'s pattern otherwise matches any ref whose name
|
|
36
|
+
* ends with `branch` (not just an exact `refs/heads/<branch>`), and bare `rev-parse <branch>`
|
|
37
|
+
* resolves `refs/tags/<branch>` before `refs/heads/<branch>` when a same-named tag exists, either
|
|
38
|
+
* of which could compare the wrong commit. `ls-remote --heads` exits `0` with empty stdout when
|
|
39
|
+
* there is no match, so a missing branch is checked via stdout rather than the exit code. A
|
|
40
|
+
* remote ref that exists but points at a different commit than the local branch means local HEAD
|
|
41
|
+
* has commits the remote does not — that fails with {@link HeadOutOfDateError} rather than
|
|
42
|
+
* silently creating the pull request from the stale remote tip.
|
|
43
|
+
*/
|
|
44
|
+
export async function assertHeadPushed(runGit, { branch, remote = 'origin' }) {
|
|
45
|
+
const ref = `refs/heads/${branch}`;
|
|
46
|
+
const remoteLine = (await runGit(['ls-remote', '--heads', remote, ref])).trim();
|
|
47
|
+
if (remoteLine === '')
|
|
48
|
+
throw new HeadNotPushedError(branch, remote);
|
|
49
|
+
const [remoteSha] = remoteLine.split(/\s+/);
|
|
50
|
+
const localSha = (await runGit(['rev-parse', ref])).trim();
|
|
51
|
+
if (localSha !== remoteSha)
|
|
52
|
+
throw new HeadOutOfDateError(branch, remote);
|
|
53
|
+
}
|
|
54
|
+
/** Pure argv builder for `gh pr create`. `head` is always passed explicitly (never omitted). */
|
|
55
|
+
export function buildGhPrCreateArgs(options) {
|
|
56
|
+
const { base, bodyFile, draft = false, head, labels = [], reviewers = [], title } = options;
|
|
57
|
+
const args = ['pr', 'create', '--title', title, '--body-file', bodyFile, '--head', head];
|
|
58
|
+
if (base !== undefined)
|
|
59
|
+
args.push('--base', base);
|
|
60
|
+
if (draft)
|
|
61
|
+
args.push('--draft');
|
|
62
|
+
for (const label of labels)
|
|
63
|
+
args.push('--label', label);
|
|
64
|
+
for (const reviewer of reviewers)
|
|
65
|
+
args.push('--reviewer', reviewer);
|
|
66
|
+
return args;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Creates a pull request with `gh pr create --head <branch>`, resolving and verifying the head
|
|
70
|
+
* branch first so the "must first push the current branch" non-interactive abort can never
|
|
71
|
+
* happen — instead an unpushed branch fails fast with {@link HeadNotPushedError}. Returns the
|
|
72
|
+
* trimmed PR URL that `gh pr create` prints to stdout.
|
|
73
|
+
*/
|
|
74
|
+
export async function createPullRequest({ runGh, runGit }, options) {
|
|
75
|
+
const { remote = 'origin', head: suppliedHead, ...rest } = options;
|
|
76
|
+
const head = suppliedHead ?? (await resolveHeadBranch(runGit));
|
|
77
|
+
await assertHeadPushed(runGit, { branch: head, remote });
|
|
78
|
+
const stdout = await runGh(buildGhPrCreateArgs({ ...rest, head }));
|
|
79
|
+
return stdout.trim();
|
|
80
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -84,3 +84,8 @@ export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mts';
|
|
|
84
84
|
export type { ResolvedDocument, ResolvedPin, ValidateResolvedPinDeltaOptions, } from './swift-resolved-pin-delta/index.mts';
|
|
85
85
|
export { DEFAULT_MAX_DIAGNOSTIC_REPORTS, DEFAULT_MAX_FORMATTED_DIAGNOSTIC_REPORTS, formatDiagnosticReportSummaries, HARD_MAX_DIAGNOSTIC_REPORTS, readDiagnosticReportSummaries, summarizeDiagnosticReport, } from './vitest-diagnostics/index.mts';
|
|
86
86
|
export type { DiagnosticReportLimitOptions, DiagnosticReportSummary, } from './vitest-diagnostics/index.mts';
|
|
87
|
+
export { createCommandRunner, runGh, runGit } from './gh-cli/index.mts';
|
|
88
|
+
export type { ExecFileText, RunTextCommand } from './gh-cli/index.mts';
|
|
89
|
+
export { getDiffAgainstBase } from './gh-cli/index.mts';
|
|
90
|
+
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './gh-cli/index.mts';
|
|
91
|
+
export type { AssertHeadPushedOptions, BuildGhPrCreateArgsOptions, CreatePullRequestExecutors, CreatePullRequestOptions, } from './gh-cli/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -47,3 +47,6 @@ export { normalizeSwiftSource } from './swift-semantic-equal/index.mjs';
|
|
|
47
47
|
export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from './swift-source-offset/index.mjs';
|
|
48
48
|
export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mjs';
|
|
49
49
|
export { DEFAULT_MAX_DIAGNOSTIC_REPORTS, DEFAULT_MAX_FORMATTED_DIAGNOSTIC_REPORTS, formatDiagnosticReportSummaries, HARD_MAX_DIAGNOSTIC_REPORTS, readDiagnosticReportSummaries, summarizeDiagnosticReport, } from './vitest-diagnostics/index.mjs';
|
|
50
|
+
export { createCommandRunner, runGh, runGit } from './gh-cli/index.mjs';
|
|
51
|
+
export { getDiffAgainstBase } from './gh-cli/index.mjs';
|
|
52
|
+
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './gh-cli/index.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -262,6 +262,11 @@
|
|
|
262
262
|
"import": "./dist/vitest-diagnostics/index.mjs",
|
|
263
263
|
"default": "./dist/vitest-diagnostics/index.mjs"
|
|
264
264
|
},
|
|
265
|
+
"./gh-cli": {
|
|
266
|
+
"types": "./dist/gh-cli/index.d.mts",
|
|
267
|
+
"import": "./dist/gh-cli/index.mjs",
|
|
268
|
+
"default": "./dist/gh-cli/index.mjs"
|
|
269
|
+
},
|
|
265
270
|
"./package.json": "./package.json"
|
|
266
271
|
},
|
|
267
272
|
"publishConfig": {
|