@helmiq/crew-git 0.1.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.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Validate a branch name against a project's configured naming convention.
3
+ *
4
+ * If no convention pattern is configured, any branch name is accepted.
5
+ * The pattern is a regex string from `project.conventions.branch_naming`.
6
+ */
7
+ export declare function validateBranchName(branchName: string, conventionPattern?: string): void;
8
+ export declare class BranchConventionError extends Error {
9
+ readonly code: "BRANCH_NAME_INVALID";
10
+ constructor(message: string);
11
+ }
12
+ //# sourceMappingURL=branch-conventions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-conventions.d.ts","sourceRoot":"","sources":["../src/branch-conventions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CASvF;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAG,qBAAqB,CAAU;gBACnC,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Validate a branch name against a project's configured naming convention.
3
+ *
4
+ * If no convention pattern is configured, any branch name is accepted.
5
+ * The pattern is a regex string from `project.conventions.branch_naming`.
6
+ */
7
+ export function validateBranchName(branchName, conventionPattern) {
8
+ if (!conventionPattern)
9
+ return;
10
+ const regex = new RegExp(conventionPattern);
11
+ if (!regex.test(branchName)) {
12
+ throw new BranchConventionError(`Branch name "${branchName}" does not match convention: ${conventionPattern}`);
13
+ }
14
+ }
15
+ export class BranchConventionError extends Error {
16
+ code = 'BRANCH_NAME_INVALID';
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'BranchConventionError';
20
+ }
21
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=git-tools.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-tools.test.d.ts","sourceRoot":"","sources":["../src/git-tools.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,177 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { simpleGit } from 'simple-git';
6
+ import { validateBranchName, BranchConventionError } from './branch-conventions.js';
7
+ import { gitBranch } from './tool-git-branch.js';
8
+ import { gitCommit } from './tool-git-commit.js';
9
+ import { gitDiff } from './tool-git-diff.js';
10
+ import { gitLog } from './tool-git-log.js';
11
+ import { createGithubPr, GitHubClientError } from './github-client.js';
12
+ describe('Git Tools', () => {
13
+ let repoDir;
14
+ beforeEach(async () => {
15
+ repoDir = await mkdtemp(join(tmpdir(), 'crew-git-tools-'));
16
+ const g = simpleGit(repoDir);
17
+ await g.init();
18
+ await g.addConfig('user.email', 'test@crew.dev');
19
+ await g.addConfig('user.name', 'Crew Test');
20
+ await writeFile(join(repoDir, 'README.md'), '# Test repo\n', 'utf-8');
21
+ await g.add('.');
22
+ await g.commit('initial commit');
23
+ });
24
+ afterEach(async () => {
25
+ await rm(repoDir, { recursive: true, force: true });
26
+ });
27
+ // T-02-003a: branch creation with naming convention
28
+ describe('branch conventions', () => {
29
+ it('accepts branch matching convention', () => {
30
+ expect(() => validateBranchName('feat/CREW-01-001-add-feature', '^(feat|fix|chore)/[A-Z]+-[0-9]+-.+$')).not.toThrow();
31
+ });
32
+ it('rejects branch not matching convention', () => {
33
+ expect(() => validateBranchName('bad-name', '^(feat|fix|chore)/[A-Z]+-[0-9]+-.+$')).toThrow(BranchConventionError);
34
+ });
35
+ it('accepts any branch when no convention is set', () => {
36
+ expect(() => validateBranchName('anything-goes')).not.toThrow();
37
+ });
38
+ });
39
+ describe('tool:git-branch', () => {
40
+ it('creates and checks out a new branch', async () => {
41
+ const result = await gitBranch({ branchName: 'feat/CREW-02-003-test' }, repoDir, '^feat/.+$');
42
+ expect(result.branch).toBe('feat/CREW-02-003-test');
43
+ expect(result.status).toBe('checked_out');
44
+ const g = simpleGit(repoDir);
45
+ const status = await g.status();
46
+ expect(status.current).toBe('feat/CREW-02-003-test');
47
+ });
48
+ it('rejects branch that violates convention', async () => {
49
+ await expect(gitBranch({ branchName: 'bad-name' }, repoDir, '^feat/.+$')).rejects.toThrow(BranchConventionError);
50
+ });
51
+ });
52
+ // T-02-003b: commit/push flow (commit only; push requires remote)
53
+ describe('tool:git-commit', () => {
54
+ it('stages and commits files', async () => {
55
+ await mkdir(join(repoDir, 'src'), { recursive: true });
56
+ await writeFile(join(repoDir, 'src/new.ts'), 'export const x = 1;\n', 'utf-8');
57
+ const result = await gitCommit({ filePaths: ['src/new.ts'], message: 'feat: add new file' }, repoDir);
58
+ expect(result.status).toBe('committed');
59
+ expect(result.commitSha).not.toBe('unknown');
60
+ const g = simpleGit(repoDir);
61
+ const log = await g.log({ maxCount: 1 });
62
+ expect(log.latest?.message).toBe('feat: add new file');
63
+ });
64
+ });
65
+ // T-02-003c: diff/log retrieval
66
+ describe('tool:git-diff', () => {
67
+ it('returns diff of unstaged changes', async () => {
68
+ await writeFile(join(repoDir, 'README.md'), '# Updated\n', 'utf-8');
69
+ const result = await gitDiff({}, repoDir);
70
+ expect(result.diff).toContain('Updated');
71
+ });
72
+ });
73
+ describe('tool:git-log', () => {
74
+ it('returns commit log entries', async () => {
75
+ const result = await gitLog({ maxCount: 5 }, repoDir);
76
+ expect(result.entries.length).toBeGreaterThanOrEqual(1);
77
+ expect(result.entries[0].message).toBe('initial commit');
78
+ expect(result.entries[0].hash).toBeTruthy();
79
+ });
80
+ });
81
+ // T-02-003d: GitHub PR creation payload
82
+ describe('github-client', () => {
83
+ afterEach(() => {
84
+ vi.restoreAllMocks();
85
+ });
86
+ it('creates a PR with correct payload', async () => {
87
+ const mockResponse = {
88
+ number: 42,
89
+ url: 'https://api.github.com/repos/test/repo/pulls/42',
90
+ html_url: 'https://github.com/test/repo/pull/42',
91
+ };
92
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
93
+ ok: true,
94
+ json: () => Promise.resolve(mockResponse),
95
+ }));
96
+ const result = await createGithubPr({
97
+ owner: 'test',
98
+ repo: 'repo',
99
+ title: 'feat: add feature',
100
+ body: 'Description here',
101
+ head: 'feat/branch',
102
+ base: 'main',
103
+ }, 'ghp_test_token');
104
+ expect(result.number).toBe(42);
105
+ expect(result.url).toBe('https://api.github.com/repos/test/repo/pulls/42');
106
+ expect(result.htmlUrl).toBe('https://github.com/test/repo/pull/42');
107
+ const fetchCalls = vi.mocked(fetch).mock.calls;
108
+ expect(fetchCalls.length).toBe(1);
109
+ const [url, options] = fetchCalls[0];
110
+ expect(url).toBe('https://api.github.com/repos/test/repo/pulls');
111
+ expect(options.method).toBe('POST');
112
+ const body = JSON.parse(options.body);
113
+ expect(body.title).toBe('feat: add feature');
114
+ expect(body.head).toBe('feat/branch');
115
+ expect(body.base).toBe('main');
116
+ });
117
+ it('throws GITHUB_TOKEN_MISSING when no token is provided', async () => {
118
+ const original = process.env['GITHUB_TOKEN'];
119
+ delete process.env['GITHUB_TOKEN'];
120
+ await expect(createGithubPr({
121
+ owner: 'test',
122
+ repo: 'repo',
123
+ title: 'test',
124
+ body: 'test',
125
+ head: 'branch',
126
+ base: 'main',
127
+ })).rejects.toThrow(GitHubClientError);
128
+ if (original)
129
+ process.env['GITHUB_TOKEN'] = original;
130
+ });
131
+ it('throws PR_CREATION_FAILED on non-ok response', async () => {
132
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
133
+ ok: false,
134
+ status: 422,
135
+ text: () => Promise.resolve('{"message":"Validation Failed"}'),
136
+ }));
137
+ await expect(createGithubPr({
138
+ owner: 'test',
139
+ repo: 'repo',
140
+ title: 'test',
141
+ body: 'test',
142
+ head: 'branch',
143
+ base: 'main',
144
+ }, 'ghp_test_token')).rejects.toThrow(/PR_CREATION_FAILED|422/);
145
+ });
146
+ it('sends labels in a separate request when provided', async () => {
147
+ const prResponse = {
148
+ number: 10,
149
+ url: 'https://api.github.com/repos/test/repo/pulls/10',
150
+ html_url: 'https://github.com/test/repo/pull/10',
151
+ };
152
+ vi.stubGlobal('fetch', vi
153
+ .fn()
154
+ .mockResolvedValueOnce({
155
+ ok: true,
156
+ json: () => Promise.resolve(prResponse),
157
+ })
158
+ .mockResolvedValueOnce({
159
+ ok: true,
160
+ json: () => Promise.resolve([]),
161
+ }));
162
+ const result = await createGithubPr({
163
+ owner: 'test',
164
+ repo: 'repo',
165
+ title: 'feat: labeled',
166
+ body: 'With labels',
167
+ head: 'branch',
168
+ base: 'main',
169
+ labels: ['bug', 'priority'],
170
+ }, 'ghp_test_token');
171
+ expect(result.number).toBe(10);
172
+ const fetchCalls = vi.mocked(fetch).mock.calls;
173
+ expect(fetchCalls.length).toBe(2);
174
+ expect(fetchCalls[1][0]).toContain('/issues/10/labels');
175
+ });
176
+ });
177
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Thin GitHub REST client for PR creation and check-run queries.
3
+ *
4
+ * Uses Node.js built-in `fetch` per ADR-0012. Zero external dependencies.
5
+ * Authentication via GITHUB_TOKEN environment variable as Bearer token.
6
+ */
7
+ export interface CreatePrParams {
8
+ owner: string;
9
+ repo: string;
10
+ title: string;
11
+ body: string;
12
+ head: string;
13
+ base: string;
14
+ labels?: string[];
15
+ }
16
+ export interface CreatePrResult {
17
+ number: number;
18
+ url: string;
19
+ htmlUrl: string;
20
+ }
21
+ export declare function createGithubPr(params: CreatePrParams, token?: string): Promise<CreatePrResult>;
22
+ export declare class GitHubClientError extends Error {
23
+ readonly code: string;
24
+ readonly statusCode?: number;
25
+ constructor(message: string, code: string, statusCode?: number);
26
+ }
27
+ //# sourceMappingURL=github-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-client.d.ts","sourceRoot":"","sources":["../src/github-client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,cAAc,CAAC,CAoDzB;AAgCD,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAM/D"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Thin GitHub REST client for PR creation and check-run queries.
3
+ *
4
+ * Uses Node.js built-in `fetch` per ADR-0012. Zero external dependencies.
5
+ * Authentication via GITHUB_TOKEN environment variable as Bearer token.
6
+ */
7
+ const GITHUB_API = 'https://api.github.com';
8
+ export async function createGithubPr(params, token) {
9
+ const authToken = token ?? process.env['GITHUB_TOKEN'];
10
+ if (!authToken) {
11
+ throw new GitHubClientError('GITHUB_TOKEN environment variable is not set. Required for PR creation.', 'GITHUB_TOKEN_MISSING');
12
+ }
13
+ const prResponse = await fetch(`${GITHUB_API}/repos/${params.owner}/${params.repo}/pulls`, {
14
+ method: 'POST',
15
+ headers: {
16
+ Authorization: `Bearer ${authToken}`,
17
+ 'Content-Type': 'application/json',
18
+ Accept: 'application/vnd.github+json',
19
+ 'X-GitHub-Api-Version': '2022-11-28',
20
+ },
21
+ body: JSON.stringify({
22
+ title: params.title,
23
+ body: params.body,
24
+ head: params.head,
25
+ base: params.base,
26
+ }),
27
+ });
28
+ if (!prResponse.ok) {
29
+ const errorBody = await prResponse.text();
30
+ throw new GitHubClientError(`GitHub PR creation failed (${prResponse.status}): ${errorBody}`, 'PR_CREATION_FAILED', prResponse.status);
31
+ }
32
+ const prData = (await prResponse.json());
33
+ const prNumber = prData['number'];
34
+ const prUrl = prData['url'];
35
+ const prHtmlUrl = prData['html_url'];
36
+ if (params.labels && params.labels.length > 0) {
37
+ try {
38
+ await addLabels(params.owner, params.repo, prNumber, params.labels, authToken);
39
+ }
40
+ catch {
41
+ // Labels are best-effort per design; PR was created successfully
42
+ }
43
+ }
44
+ return {
45
+ number: prNumber,
46
+ url: prUrl,
47
+ htmlUrl: prHtmlUrl,
48
+ };
49
+ }
50
+ async function addLabels(owner, repo, issueNumber, labels, token) {
51
+ const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/issues/${issueNumber}/labels`, {
52
+ method: 'POST',
53
+ headers: {
54
+ Authorization: `Bearer ${token}`,
55
+ 'Content-Type': 'application/json',
56
+ Accept: 'application/vnd.github+json',
57
+ 'X-GitHub-Api-Version': '2022-11-28',
58
+ },
59
+ body: JSON.stringify({ labels }),
60
+ });
61
+ if (!response.ok) {
62
+ throw new GitHubClientError(`Failed to add labels (${response.status})`, 'PR_LABELS_FAILED', response.status);
63
+ }
64
+ }
65
+ export class GitHubClientError extends Error {
66
+ code;
67
+ statusCode;
68
+ constructor(message, code, statusCode) {
69
+ super(message);
70
+ this.name = 'GitHubClientError';
71
+ this.code = code;
72
+ this.statusCode = statusCode;
73
+ }
74
+ }
@@ -0,0 +1,15 @@
1
+ export { validateBranchName, BranchConventionError } from './branch-conventions.js';
2
+ export { createGithubPr, GitHubClientError } from './github-client.js';
3
+ export type { CreatePrParams, CreatePrResult } from './github-client.js';
4
+ export { gitBranch, GitToolError } from './tool-git-branch.js';
5
+ export type { GitBranchParams, GitBranchResult } from './tool-git-branch.js';
6
+ export { gitCommit } from './tool-git-commit.js';
7
+ export type { GitCommitParams, GitCommitResult } from './tool-git-commit.js';
8
+ export { gitPush } from './tool-git-push.js';
9
+ export type { GitPushParams, GitPushResult } from './tool-git-push.js';
10
+ export { gitDiff } from './tool-git-diff.js';
11
+ export type { GitDiffParams, GitDiffResult } from './tool-git-diff.js';
12
+ export { gitLog } from './tool-git-log.js';
13
+ export type { GitLogParams, GitLogResult, GitLogEntry } from './tool-git-log.js';
14
+ export { createPr } from './tool-create-pr.js';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC/D,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { validateBranchName, BranchConventionError } from './branch-conventions.js';
2
+ export { createGithubPr, GitHubClientError } from './github-client.js';
3
+ export { gitBranch, GitToolError } from './tool-git-branch.js';
4
+ export { gitCommit } from './tool-git-commit.js';
5
+ export { gitPush } from './tool-git-push.js';
6
+ export { gitDiff } from './tool-git-diff.js';
7
+ export { gitLog } from './tool-git-log.js';
8
+ export { createPr } from './tool-create-pr.js';
@@ -0,0 +1,4 @@
1
+ import type { CreatePrParams, CreatePrResult } from './github-client.js';
2
+ export type { CreatePrParams, CreatePrResult };
3
+ export declare function createPr(params: CreatePrParams, token?: string): Promise<CreatePrResult>;
4
+ //# sourceMappingURL=tool-create-pr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-create-pr.d.ts","sourceRoot":"","sources":["../src/tool-create-pr.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC;AAE/C,wBAAsB,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAE9F"}
@@ -0,0 +1,4 @@
1
+ import { createGithubPr } from './github-client.js';
2
+ export async function createPr(params, token) {
3
+ return createGithubPr(params, token);
4
+ }
@@ -0,0 +1,13 @@
1
+ export interface GitBranchParams {
2
+ branchName: string;
3
+ }
4
+ export interface GitBranchResult {
5
+ branch: string;
6
+ status: 'checked_out';
7
+ }
8
+ export declare function gitBranch(params: GitBranchParams, repoPath: string, branchConvention?: string): Promise<GitBranchResult>;
9
+ export declare class GitToolError extends Error {
10
+ readonly code: string;
11
+ constructor(message: string, code: string);
12
+ }
13
+ //# sourceMappingURL=tool-git-branch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-git-branch.d.ts","sourceRoot":"","sources":["../src/tool-git-branch.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,eAAe,CAAC,CAa1B;AAED,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAK1C"}
@@ -0,0 +1,21 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import { validateBranchName } from './branch-conventions.js';
3
+ export async function gitBranch(params, repoPath, branchConvention) {
4
+ validateBranchName(params.branchName, branchConvention);
5
+ const g = simpleGit(repoPath);
6
+ try {
7
+ await g.checkoutLocalBranch(params.branchName);
8
+ return { branch: params.branchName, status: 'checked_out' };
9
+ }
10
+ catch (err) {
11
+ throw new GitToolError(`Failed to create branch '${params.branchName}': ${err instanceof Error ? err.message : String(err)}`, 'GIT_BRANCH_FAILED');
12
+ }
13
+ }
14
+ export class GitToolError extends Error {
15
+ code;
16
+ constructor(message, code) {
17
+ super(message);
18
+ this.name = 'GitToolError';
19
+ this.code = code;
20
+ }
21
+ }
@@ -0,0 +1,10 @@
1
+ export interface GitCommitParams {
2
+ filePaths: string[];
3
+ message: string;
4
+ }
5
+ export interface GitCommitResult {
6
+ commitSha: string;
7
+ status: 'committed';
8
+ }
9
+ export declare function gitCommit(params: GitCommitParams, repoPath: string): Promise<GitCommitResult>;
10
+ //# sourceMappingURL=tool-git-commit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-git-commit.d.ts","sourceRoot":"","sources":["../src/tool-git-commit.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,CAAC,CAc1B"}
@@ -0,0 +1,14 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import { GitToolError } from './tool-git-branch.js';
3
+ export async function gitCommit(params, repoPath) {
4
+ const g = simpleGit(repoPath);
5
+ try {
6
+ await g.add(params.filePaths);
7
+ const result = await g.commit(params.message, params.filePaths);
8
+ const sha = result.commit || 'unknown';
9
+ return { commitSha: sha, status: 'committed' };
10
+ }
11
+ catch (err) {
12
+ throw new GitToolError(`Git commit failed: ${err instanceof Error ? err.message : String(err)}`, 'GIT_COMMIT_FAILED');
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ export interface GitDiffParams {
2
+ staged?: boolean;
3
+ paths?: string[];
4
+ }
5
+ export interface GitDiffResult {
6
+ diff: string;
7
+ filesChanged: number;
8
+ }
9
+ export declare function gitDiff(params: GitDiffParams, repoPath: string): Promise<GitDiffResult>;
10
+ //# sourceMappingURL=tool-git-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-git-diff.d.ts","sourceRoot":"","sources":["../src/tool-git-diff.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2B7F"}
@@ -0,0 +1,26 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import { GitToolError } from './tool-git-branch.js';
3
+ export async function gitDiff(params, repoPath) {
4
+ const g = simpleGit(repoPath);
5
+ try {
6
+ const args = [];
7
+ if (params.staged)
8
+ args.push('--cached');
9
+ if (params.paths && params.paths.length > 0) {
10
+ args.push('--', ...params.paths);
11
+ }
12
+ const diffOutput = await g.diff(args);
13
+ const statArgs = [...args.filter((a) => a !== '--cached'), '--stat'];
14
+ if (params.staged)
15
+ statArgs.unshift('--cached');
16
+ const statOutput = await g.diff(statArgs);
17
+ const filesChanged = statOutput.match(/\d+ files? changed/)?.[0]?.match(/\d+/)?.[0] ?? '0';
18
+ return {
19
+ diff: diffOutput,
20
+ filesChanged: parseInt(filesChanged, 10),
21
+ };
22
+ }
23
+ catch (err) {
24
+ throw new GitToolError(`Git diff failed: ${err instanceof Error ? err.message : String(err)}`, 'GIT_DIFF_FAILED');
25
+ }
26
+ }
@@ -0,0 +1,15 @@
1
+ export interface GitLogParams {
2
+ maxCount?: number;
3
+ path?: string;
4
+ }
5
+ export interface GitLogEntry {
6
+ hash: string;
7
+ date: string;
8
+ message: string;
9
+ author: string;
10
+ }
11
+ export interface GitLogResult {
12
+ entries: GitLogEntry[];
13
+ }
14
+ export declare function gitLog(params: GitLogParams, repoPath: string): Promise<GitLogResult>;
15
+ //# sourceMappingURL=tool-git-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-git-log.d.ts","sourceRoot":"","sources":["../src/tool-git-log.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAID,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA0B1F"}
@@ -0,0 +1,24 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import { GitToolError } from './tool-git-branch.js';
3
+ const DEFAULT_MAX_COUNT = 20;
4
+ export async function gitLog(params, repoPath) {
5
+ const g = simpleGit(repoPath);
6
+ const maxCount = params.maxCount ?? DEFAULT_MAX_COUNT;
7
+ try {
8
+ const options = { maxCount };
9
+ if (params.path) {
10
+ options['file'] = params.path;
11
+ }
12
+ const log = await g.log(options);
13
+ const entries = (log.all ?? []).map((entry) => ({
14
+ hash: entry.hash,
15
+ date: entry.date,
16
+ message: entry.message,
17
+ author: entry.author_name,
18
+ }));
19
+ return { entries };
20
+ }
21
+ catch (err) {
22
+ throw new GitToolError(`Git log failed: ${err instanceof Error ? err.message : String(err)}`, 'GIT_LOG_FAILED');
23
+ }
24
+ }
@@ -0,0 +1,9 @@
1
+ export interface GitPushParams {
2
+ branch?: string;
3
+ }
4
+ export interface GitPushResult {
5
+ branch: string;
6
+ status: 'pushed';
7
+ }
8
+ export declare function gitPush(params: GitPushParams, repoPath: string): Promise<GitPushResult>;
9
+ //# sourceMappingURL=tool-git-push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-git-push.d.ts","sourceRoot":"","sources":["../src/tool-git-push.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,QAAQ,CAAC;CAClB;AAKD,wBAAsB,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqB7F"}
@@ -0,0 +1,28 @@
1
+ import { simpleGit } from 'simple-git';
2
+ import { GitToolError } from './tool-git-branch.js';
3
+ const PUSH_RETRIES = 2;
4
+ const PUSH_DELAY_MS = 2000;
5
+ export async function gitPush(params, repoPath) {
6
+ const g = simpleGit(repoPath);
7
+ const branch = params.branch ?? (await getCurrentBranch(g));
8
+ for (let attempt = 0; attempt <= PUSH_RETRIES; attempt++) {
9
+ try {
10
+ await g.push('origin', branch);
11
+ return { branch, status: 'pushed' };
12
+ }
13
+ catch (err) {
14
+ if (attempt === PUSH_RETRIES) {
15
+ throw new GitToolError(`Git push failed after ${PUSH_RETRIES + 1} attempts: ${err instanceof Error ? err.message : String(err)}`, 'GIT_PUSH_FAILED');
16
+ }
17
+ await sleep(PUSH_DELAY_MS);
18
+ }
19
+ }
20
+ throw new GitToolError('Git push failed: unexpected control flow', 'GIT_PUSH_FAILED');
21
+ }
22
+ async function getCurrentBranch(g) {
23
+ const status = await g.status();
24
+ return status.current ?? 'HEAD';
25
+ }
26
+ function sleep(ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@helmiq/crew-git",
3
+ "version": "0.1.0",
4
+ "description": "Git and GitHub tools for the Crew delivery runtime",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/helmiq/crew",
10
+ "directory": "packages/crew-git"
11
+ },
12
+ "keywords": [
13
+ "helmiq",
14
+ "crew",
15
+ "tools",
16
+ "git",
17
+ "github"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20.9.0"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "exports": {
26
+ ".": {
27
+ "import": "./dist/index.js",
28
+ "types": "./dist/index.d.ts"
29
+ }
30
+ },
31
+ "dependencies": {
32
+ "simple-git": "^3.33.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.5.0",
36
+ "typescript": "^6.0.2",
37
+ "@helmiq/typescript-config": "0.0.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc --project tsconfig.json",
41
+ "typecheck": "tsc --noEmit --project tsconfig.json"
42
+ }
43
+ }