vouchington-tooling 0.7.2 → 0.8.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 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, 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, resolveHeadBranch, } from './pr-create.mjs';
@@ -0,0 +1,47 @@
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
+ /** Resolves the current branch via `git branch --show-current`, trimmed. */
11
+ export declare function resolveHeadBranch(runGit: RunTextCommand): Promise<string>;
12
+ export type AssertHeadPushedOptions = {
13
+ branch: string;
14
+ remote?: string;
15
+ };
16
+ /**
17
+ * Confirms `branch` has a matching ref on `remote` (default `origin`) via
18
+ * `git ls-remote --heads`. `ls-remote --heads` exits `0` with empty stdout when there is no
19
+ * match, so this checks stdout rather than the exit code.
20
+ */
21
+ export declare function assertHeadPushed(runGit: RunTextCommand, { branch, remote }: AssertHeadPushedOptions): Promise<void>;
22
+ export type BuildGhPrCreateArgsOptions = {
23
+ base?: string;
24
+ bodyFile: string;
25
+ draft?: boolean;
26
+ head: string;
27
+ labels?: readonly string[];
28
+ reviewers?: readonly string[];
29
+ title: string;
30
+ };
31
+ /** Pure argv builder for `gh pr create`. `head` is always passed explicitly (never omitted). */
32
+ export declare function buildGhPrCreateArgs(options: BuildGhPrCreateArgsOptions): string[];
33
+ export type CreatePullRequestExecutors = {
34
+ runGh: RunTextCommand;
35
+ runGit: RunTextCommand;
36
+ };
37
+ export type CreatePullRequestOptions = Omit<BuildGhPrCreateArgsOptions, 'head'> & {
38
+ head?: string;
39
+ remote?: string;
40
+ };
41
+ /**
42
+ * Creates a pull request with `gh pr create --head <branch>`, resolving and verifying the head
43
+ * branch first so the "must first push the current branch" non-interactive abort can never
44
+ * happen — instead an unpushed branch fails fast with {@link HeadNotPushedError}. Returns the
45
+ * trimmed PR URL that `gh pr create` prints to stdout.
46
+ */
47
+ export declare function createPullRequest({ runGh, runGit }: CreatePullRequestExecutors, options: CreatePullRequestOptions): Promise<string>;
@@ -0,0 +1,58 @@
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
+ /** Resolves the current branch via `git branch --show-current`, trimmed. */
16
+ export async function resolveHeadBranch(runGit) {
17
+ const branch = (await runGit(['branch', '--show-current'])).trim();
18
+ if (branch === '')
19
+ throw new DetachedHeadError();
20
+ return branch;
21
+ }
22
+ /**
23
+ * Confirms `branch` has a matching ref on `remote` (default `origin`) via
24
+ * `git ls-remote --heads`. `ls-remote --heads` exits `0` with empty stdout when there is no
25
+ * match, so this checks stdout rather than the exit code.
26
+ */
27
+ export async function assertHeadPushed(runGit, { branch, remote = 'origin' }) {
28
+ const stdout = await runGit(['ls-remote', '--heads', remote, branch]);
29
+ if (stdout.trim() === '')
30
+ throw new HeadNotPushedError(branch, remote);
31
+ }
32
+ /** Pure argv builder for `gh pr create`. `head` is always passed explicitly (never omitted). */
33
+ export function buildGhPrCreateArgs(options) {
34
+ const { base, bodyFile, draft = false, head, labels = [], reviewers = [], title } = options;
35
+ const args = ['pr', 'create', '--title', title, '--body-file', bodyFile, '--head', head];
36
+ if (base !== undefined)
37
+ args.push('--base', base);
38
+ if (draft)
39
+ args.push('--draft');
40
+ for (const label of labels)
41
+ args.push('--label', label);
42
+ for (const reviewer of reviewers)
43
+ args.push('--reviewer', reviewer);
44
+ return args;
45
+ }
46
+ /**
47
+ * Creates a pull request with `gh pr create --head <branch>`, resolving and verifying the head
48
+ * branch first so the "must first push the current branch" non-interactive abort can never
49
+ * happen — instead an unpushed branch fails fast with {@link HeadNotPushedError}. Returns the
50
+ * trimmed PR URL that `gh pr create` prints to stdout.
51
+ */
52
+ export async function createPullRequest({ runGh, runGit }, options) {
53
+ const { remote = 'origin', head: suppliedHead, ...rest } = options;
54
+ const head = suppliedHead ?? (await resolveHeadBranch(runGit));
55
+ await assertHeadPushed(runGit, { branch: head, remote });
56
+ const stdout = await runGh(buildGhPrCreateArgs({ ...rest, head }));
57
+ return stdout.trim();
58
+ }
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, 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, resolveHeadBranch, } from './gh-cli/index.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
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": {