@smoothbricks/cli 0.10.0 → 0.10.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/dist/cli.js +5 -2
- package/dist/monorepo/lockfile.d.ts +2 -0
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +13 -2
- package/managed/raw/tooling/git-hooks/pre-commit.sh +6 -0
- package/package.json +2 -2
- package/src/cli.ts +7 -4
- package/src/monorepo/lockfile.test.ts +157 -0
- package/src/monorepo/lockfile.ts +15 -2
- package/src/release/__tests__/fixture-repo.test.ts +8 -0
- package/src/release/__tests__/helpers/fixture-repo.ts +28 -8
package/dist/cli.js
CHANGED
|
@@ -67,9 +67,12 @@ function buildProgram() {
|
|
|
67
67
|
const { validateCommitMessageFile } = await import('./monorepo/index.js');
|
|
68
68
|
validateCommitMessageFile(commitMsgFile, options, await findRepoRoot());
|
|
69
69
|
});
|
|
70
|
-
monorepo
|
|
70
|
+
monorepo
|
|
71
|
+
.command('sync-bun-lockfile-versions')
|
|
72
|
+
.option('--stage', 'stage bun.lock when versions were resynced (pre-commit self-heal); quiet when clean')
|
|
73
|
+
.action(async (options) => {
|
|
71
74
|
const { syncBunLockfileVersions } = await import('./monorepo/index.js');
|
|
72
|
-
syncBunLockfileVersions(await findRepoRoot());
|
|
75
|
+
syncBunLockfileVersions(await findRepoRoot(), options.stage ? { log: false, stage: true } : {});
|
|
73
76
|
});
|
|
74
77
|
monorepo
|
|
75
78
|
.command('list-release-packages')
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface SyncBunLockfileVersionsOptions {
|
|
2
2
|
log?: boolean;
|
|
3
|
+
/** `git add` bun.lock after rewriting it, for the pre-commit self-heal path. */
|
|
4
|
+
stage?: boolean;
|
|
3
5
|
}
|
|
4
6
|
export declare function syncBunLockfileVersions(root: string, options?: SyncBunLockfileVersionsOptions): number;
|
|
5
7
|
export declare function validateBunLockfileVersions(root: string): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/monorepo/lockfile.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,8BAA8B;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/monorepo/lockfile.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,8BAA8B;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,gFAAgF;IAChF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,8BAAmC,GAAG,MAAM,CAuD1G;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiChE"}
|
|
@@ -11,6 +11,12 @@ import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
|
|
|
11
11
|
// - https://github.com/oven-sh/bun/issues/18906
|
|
12
12
|
// - https://github.com/oven-sh/bun/issues/20477
|
|
13
13
|
// - https://github.com/oven-sh/bun/issues/20829
|
|
14
|
+
//
|
|
15
|
+
// Any `bun install` rewrites the workspace versions from package.json, reverting
|
|
16
|
+
// this sync between releases. The pre-commit hook therefore runs
|
|
17
|
+
// `smoo monorepo sync-bun-lockfile-versions --stage` on every commit so drift is
|
|
18
|
+
// healed at the commit that would otherwise carry it — validation must never
|
|
19
|
+
// fail a committer for drift a routine `bun install` introduced.
|
|
14
20
|
export function syncBunLockfileVersions(root, options = {}) {
|
|
15
21
|
const log = options.log ?? true;
|
|
16
22
|
const lockfilePath = join(root, 'bun.lock');
|
|
@@ -53,9 +59,14 @@ export function syncBunLockfileVersions(root, options = {}) {
|
|
|
53
59
|
}
|
|
54
60
|
if (updated > 0) {
|
|
55
61
|
writeFileSync(lockfilePath, lockfile);
|
|
62
|
+
if (options.stage) {
|
|
63
|
+
execSync('git add bun.lock', { cwd: root, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
|
-
if (log) {
|
|
58
|
-
console.log(updated > 0
|
|
66
|
+
if (log || updated > 0) {
|
|
67
|
+
console.log(updated > 0
|
|
68
|
+
? `Updated ${updated} workspace version(s) in bun.lock${options.stage ? ' (staged)' : ''}`
|
|
69
|
+
: 'All workspace versions already in sync.');
|
|
59
70
|
}
|
|
60
71
|
return updated;
|
|
61
72
|
}
|
|
@@ -9,6 +9,12 @@ set -e -o pipefail
|
|
|
9
9
|
# Format exactly what is staged so the commit includes the formatter output.
|
|
10
10
|
git-format-staged
|
|
11
11
|
|
|
12
|
+
# Any `bun install` rewrites bun.lock workspace versions from package.json,
|
|
13
|
+
# reverting the stable-tag versions the release flow syncs (see
|
|
14
|
+
# packages/cli/src/monorepo/lockfile.ts). Self-heal here instead of failing a
|
|
15
|
+
# later commit on drift it didn't cause.
|
|
16
|
+
smoo monorepo sync-bun-lockfile-versions --stage
|
|
17
|
+
|
|
12
18
|
# Adding a workspace package is rare and easy to leave half-wired. Run the
|
|
13
19
|
# full monorepo validator only for newly staged package manifests.
|
|
14
20
|
smoo monorepo validate --fail-fast --only-if-new-workspace-package
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@arethetypeswrong/core": "^0.18.2",
|
|
55
|
-
"@smoothbricks/nx-plugin": "0.
|
|
55
|
+
"@smoothbricks/nx-plugin": "0.3.0",
|
|
56
56
|
"@smoothbricks/validation": "0.1.1",
|
|
57
57
|
"commander": "^14.0.3",
|
|
58
58
|
"publint": "^0.3.18",
|
package/src/cli.ts
CHANGED
|
@@ -76,10 +76,13 @@ function buildProgram(): Command {
|
|
|
76
76
|
const { validateCommitMessageFile } = await import('./monorepo/index.js');
|
|
77
77
|
validateCommitMessageFile(commitMsgFile, options, await findRepoRoot());
|
|
78
78
|
});
|
|
79
|
-
monorepo
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
monorepo
|
|
80
|
+
.command('sync-bun-lockfile-versions')
|
|
81
|
+
.option('--stage', 'stage bun.lock when versions were resynced (pre-commit self-heal); quiet when clean')
|
|
82
|
+
.action(async (options: { stage?: boolean }) => {
|
|
83
|
+
const { syncBunLockfileVersions } = await import('./monorepo/index.js');
|
|
84
|
+
syncBunLockfileVersions(await findRepoRoot(), options.stage ? { log: false, stage: true } : {});
|
|
85
|
+
});
|
|
83
86
|
monorepo
|
|
84
87
|
.command('list-release-packages')
|
|
85
88
|
.option('--fail-empty', 'fail when no owned release packages are found')
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { afterAll, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import fc from 'fast-check';
|
|
7
|
+
import { syncBunLockfileVersions, validateBunLockfileVersions } from './lockfile.js';
|
|
8
|
+
|
|
9
|
+
const cleanupRoots: string[] = [];
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
for (const root of cleanupRoots) {
|
|
12
|
+
rmSync(root, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function git(root: string, args: string): string {
|
|
17
|
+
return execSync(`git ${args}`, {
|
|
18
|
+
cwd: root,
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
23
|
+
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
24
|
+
GIT_AUTHOR_NAME: 'fixture',
|
|
25
|
+
GIT_AUTHOR_EMAIL: 'fixture@invalid',
|
|
26
|
+
GIT_COMMITTER_NAME: 'fixture',
|
|
27
|
+
GIT_COMMITTER_EMAIL: 'fixture@invalid',
|
|
28
|
+
},
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FixtureOptions {
|
|
34
|
+
packageVersion: string;
|
|
35
|
+
lockVersion: string;
|
|
36
|
+
stableTag?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeFixtureRepo(options: FixtureOptions): string {
|
|
40
|
+
const root = mkdtempSync(join(tmpdir(), 'smoo-lockfile-'));
|
|
41
|
+
cleanupRoots.push(root);
|
|
42
|
+
git(root, 'init -q');
|
|
43
|
+
git(root, 'commit -q --allow-empty -m init');
|
|
44
|
+
writeFileSync(
|
|
45
|
+
join(root, 'package.json'),
|
|
46
|
+
JSON.stringify({ name: 'fixture-root', version: '0.0.0', private: true, workspaces: ['packages/*'] }),
|
|
47
|
+
);
|
|
48
|
+
mkdirSync(join(root, 'packages', 'foo'), { recursive: true });
|
|
49
|
+
writeFixturePackage(root, options.packageVersion);
|
|
50
|
+
writeFixtureLock(root, options.lockVersion);
|
|
51
|
+
if (options.stableTag) {
|
|
52
|
+
git(root, `tag ${options.stableTag}`);
|
|
53
|
+
}
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeFixturePackage(root: string, version: string): void {
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(root, 'packages', 'foo', 'package.json'),
|
|
60
|
+
JSON.stringify({ name: '@fixture/foo', version, nx: { name: 'foo' } }),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeFixtureLock(root: string, version: string): void {
|
|
65
|
+
writeFileSync(
|
|
66
|
+
join(root, 'bun.lock'),
|
|
67
|
+
[
|
|
68
|
+
'{',
|
|
69
|
+
' "lockfileVersion": 1,',
|
|
70
|
+
' "workspaces": {',
|
|
71
|
+
' "": { "name": "fixture-root" },',
|
|
72
|
+
' "packages/foo": {',
|
|
73
|
+
' "name": "@fixture/foo",',
|
|
74
|
+
` "version": "${version}",`,
|
|
75
|
+
' },',
|
|
76
|
+
' },',
|
|
77
|
+
'}',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n'),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function lockVersionIn(root: string): string {
|
|
84
|
+
const match = readFileSync(join(root, 'bun.lock'), 'utf8').match(/"packages\/foo":\s*\{[^}]*"version":\s*"([^"]+)"/);
|
|
85
|
+
if (!match) throw new Error('fixture lockfile lost its workspace entry');
|
|
86
|
+
return match[1] as string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('bun.lock workspace version sync', () => {
|
|
90
|
+
it('a bun-install revert of the release-synced version fails validation', () => {
|
|
91
|
+
// The b3298fc4c shape: package.json holds the next prerelease, a stable tag
|
|
92
|
+
// exists, and `bun install` rewrote the lockfile back to the prerelease.
|
|
93
|
+
const root = makeFixtureRepo({
|
|
94
|
+
packageVersion: '0.1.3-next.0',
|
|
95
|
+
lockVersion: '0.1.3-next.0',
|
|
96
|
+
stableTag: 'foo@0.1.2',
|
|
97
|
+
});
|
|
98
|
+
expect(validateBunLockfileVersions(root)).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('sync restores the latest stable tag version and validation passes', () => {
|
|
102
|
+
const root = makeFixtureRepo({
|
|
103
|
+
packageVersion: '0.1.3-next.0',
|
|
104
|
+
lockVersion: '0.1.3-next.0',
|
|
105
|
+
stableTag: 'foo@0.1.2',
|
|
106
|
+
});
|
|
107
|
+
expect(syncBunLockfileVersions(root, { log: false })).toBe(1);
|
|
108
|
+
expect(lockVersionIn(root)).toBe('0.1.2');
|
|
109
|
+
expect(validateBunLockfileVersions(root)).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('a never-published prerelease package falls back to its manifest version', () => {
|
|
113
|
+
// The new-workspace-package shape (e.g. cowshed): no stable tag exists yet.
|
|
114
|
+
const root = makeFixtureRepo({ packageVersion: '0.2.0-next.0', lockVersion: '0.2.0-next.0' });
|
|
115
|
+
expect(validateBunLockfileVersions(root)).toBe(0);
|
|
116
|
+
expect(syncBunLockfileVersions(root, { log: false })).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('a stable manifest version is required verbatim in the lockfile', () => {
|
|
120
|
+
const root = makeFixtureRepo({ packageVersion: '0.1.0', lockVersion: '0.0.9' });
|
|
121
|
+
expect(validateBunLockfileVersions(root)).toBe(1);
|
|
122
|
+
expect(syncBunLockfileVersions(root, { log: false })).toBe(1);
|
|
123
|
+
expect(lockVersionIn(root)).toBe('0.1.0');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('stage: true stages the healed bun.lock', () => {
|
|
127
|
+
const root = makeFixtureRepo({
|
|
128
|
+
packageVersion: '0.1.3-next.0',
|
|
129
|
+
lockVersion: '0.1.3-next.0',
|
|
130
|
+
stableTag: 'foo@0.1.2',
|
|
131
|
+
});
|
|
132
|
+
expect(syncBunLockfileVersions(root, { log: false, stage: true })).toBe(1);
|
|
133
|
+
expect(git(root, 'diff --cached --name-only')).toContain('bun.lock');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('stage: true leaves a clean lockfile unstaged', () => {
|
|
137
|
+
const root = makeFixtureRepo({ packageVersion: '0.1.2', lockVersion: '0.1.2', stableTag: 'foo@0.1.2' });
|
|
138
|
+
expect(syncBunLockfileVersions(root, { log: false, stage: true })).toBe(0);
|
|
139
|
+
expect(git(root, 'diff --cached --name-only').trim()).toBe('');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('property: after sync, validation always passes', () => {
|
|
143
|
+
const version = fc
|
|
144
|
+
.tuple(fc.nat({ max: 20 }), fc.nat({ max: 20 }), fc.nat({ max: 20 }), fc.option(fc.nat({ max: 5 })))
|
|
145
|
+
.map(([major, minor, patch, next]) => `${major}.${minor}.${patch}${next === null ? '' : `-next.${next}`}`);
|
|
146
|
+
const root = makeFixtureRepo({ packageVersion: '0.1.2', lockVersion: '0.1.2', stableTag: 'foo@0.1.2' });
|
|
147
|
+
fc.assert(
|
|
148
|
+
fc.property(version, version, (packageVersion, lockVersion) => {
|
|
149
|
+
writeFixturePackage(root, packageVersion);
|
|
150
|
+
writeFixtureLock(root, lockVersion);
|
|
151
|
+
syncBunLockfileVersions(root, { log: false });
|
|
152
|
+
return validateBunLockfileVersions(root) === 0;
|
|
153
|
+
}),
|
|
154
|
+
{ numRuns: 25 },
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
});
|
package/src/monorepo/lockfile.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
|
|
|
5
5
|
|
|
6
6
|
export interface SyncBunLockfileVersionsOptions {
|
|
7
7
|
log?: boolean;
|
|
8
|
+
/** `git add` bun.lock after rewriting it, for the pre-commit self-heal path. */
|
|
9
|
+
stage?: boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
// Temporary Bun workaround. Delete this sync function, validateBunLockfileVersions,
|
|
@@ -16,6 +18,12 @@ export interface SyncBunLockfileVersionsOptions {
|
|
|
16
18
|
// - https://github.com/oven-sh/bun/issues/18906
|
|
17
19
|
// - https://github.com/oven-sh/bun/issues/20477
|
|
18
20
|
// - https://github.com/oven-sh/bun/issues/20829
|
|
21
|
+
//
|
|
22
|
+
// Any `bun install` rewrites the workspace versions from package.json, reverting
|
|
23
|
+
// this sync between releases. The pre-commit hook therefore runs
|
|
24
|
+
// `smoo monorepo sync-bun-lockfile-versions --stage` on every commit so drift is
|
|
25
|
+
// healed at the commit that would otherwise carry it — validation must never
|
|
26
|
+
// fail a committer for drift a routine `bun install` introduced.
|
|
19
27
|
export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVersionsOptions = {}): number {
|
|
20
28
|
const log = options.log ?? true;
|
|
21
29
|
const lockfilePath = join(root, 'bun.lock');
|
|
@@ -59,10 +67,15 @@ export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVe
|
|
|
59
67
|
}
|
|
60
68
|
if (updated > 0) {
|
|
61
69
|
writeFileSync(lockfilePath, lockfile);
|
|
70
|
+
if (options.stage) {
|
|
71
|
+
execSync('git add bun.lock', { cwd: root, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
72
|
+
}
|
|
62
73
|
}
|
|
63
|
-
if (log) {
|
|
74
|
+
if (log || updated > 0) {
|
|
64
75
|
console.log(
|
|
65
|
-
updated > 0
|
|
76
|
+
updated > 0
|
|
77
|
+
? `Updated ${updated} workspace version(s) in bun.lock${options.stage ? ' (staged)' : ''}`
|
|
78
|
+
: 'All workspace versions already in sync.',
|
|
66
79
|
);
|
|
67
80
|
}
|
|
68
81
|
return updated;
|
|
@@ -226,6 +226,14 @@ describe('release planning with fixture git repositories', () => {
|
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
it('kills timed-out fixture Git process groups so descendants cannot hold output pipes open', async () => {
|
|
230
|
+
await withFixtureRepo(async (root) => {
|
|
231
|
+
await expect(
|
|
232
|
+
git(root, ['-c', "alias.hold-pipes=!sh -c 'sleep 30 & exit 0'", 'hold-pipes'], undefined, 100),
|
|
233
|
+
).rejects.toThrow('timed out after 100ms');
|
|
234
|
+
});
|
|
235
|
+
}, 5_000);
|
|
236
|
+
|
|
229
237
|
it('repairs a scoped package from its project-name release tag without creating a package-name tag', async () => {
|
|
230
238
|
await withFixtureRepo(async (author) => {
|
|
231
239
|
await writeWorkspace(author);
|
|
@@ -82,9 +82,14 @@ export async function writeBuildablePackage(
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export async function git(
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
export async function git(
|
|
86
|
+
root: string,
|
|
87
|
+
args: string[],
|
|
88
|
+
env?: Record<string, string>,
|
|
89
|
+
timeoutMs = GIT_TIMEOUT_MS,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
const result = await gitResult(root, args, env, timeoutMs);
|
|
92
|
+
if (result.timedOut || result.exitCode !== 0) {
|
|
88
93
|
throw new Error(gitErrorMessage(root, args, result));
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -101,9 +106,15 @@ export async function gitSucceeds(root: string, args: string[]): Promise<boolean
|
|
|
101
106
|
return (await gitResult(root, args)).exitCode === 0;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
async function gitResult(
|
|
109
|
+
async function gitResult(
|
|
110
|
+
root: string,
|
|
111
|
+
args: string[],
|
|
112
|
+
env?: Record<string, string>,
|
|
113
|
+
timeoutMs = GIT_TIMEOUT_MS,
|
|
114
|
+
): Promise<GitResult> {
|
|
105
115
|
const proc = Bun.spawn(['git', ...args], {
|
|
106
116
|
cwd: root,
|
|
117
|
+
detached: true,
|
|
107
118
|
env: { ...process.env, ...GIT_FIXTURE_ENV, ...env },
|
|
108
119
|
stdout: 'pipe',
|
|
109
120
|
stderr: 'pipe',
|
|
@@ -111,15 +122,23 @@ async function gitResult(root: string, args: string[], env?: Record<string, stri
|
|
|
111
122
|
let timedOut = false;
|
|
112
123
|
const timeout = setTimeout(() => {
|
|
113
124
|
timedOut = true;
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
try {
|
|
126
|
+
// `git` may exit after spawning a transport or hook which still owns its
|
|
127
|
+
// pipes. Killing the detached process group closes every inherited pipe.
|
|
128
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ESRCH')) {
|
|
131
|
+
proc.kill('SIGKILL');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}, timeoutMs);
|
|
116
135
|
try {
|
|
117
136
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
118
137
|
proc.exited,
|
|
119
138
|
streamBytes(proc.stdout),
|
|
120
139
|
streamBytes(proc.stderr),
|
|
121
140
|
]);
|
|
122
|
-
return { exitCode, stdout, stderr, timedOut };
|
|
141
|
+
return { exitCode, stdout, stderr, timedOut, timeoutMs };
|
|
123
142
|
} finally {
|
|
124
143
|
clearTimeout(timeout);
|
|
125
144
|
}
|
|
@@ -151,12 +170,13 @@ interface GitResult {
|
|
|
151
170
|
stdout: Uint8Array;
|
|
152
171
|
stderr: Uint8Array;
|
|
153
172
|
timedOut: boolean;
|
|
173
|
+
timeoutMs: number;
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
function gitErrorMessage(root: string, args: string[], result: GitResult): string {
|
|
157
177
|
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
158
178
|
const timeoutText = result.timedOut
|
|
159
|
-
? ` timed out after ${
|
|
179
|
+
? ` timed out after ${result.timeoutMs}ms`
|
|
160
180
|
: ` failed with exit code ${result.exitCode}`;
|
|
161
181
|
return [`git ${args.join(' ')}${timeoutText}`, `cwd: ${root}`, stderr].filter(Boolean).join('\n');
|
|
162
182
|
}
|