@smithers-orchestrator/vcs 0.21.0 → 0.23.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/package.json +9 -2
- package/src/ResolvedBinary.ts +11 -0
- package/src/WorkspaceSnapshot.ts +16 -0
- package/src/index.d.ts +78 -1
- package/src/index.js +5 -0
- package/src/jj.js +58 -2
- package/src/resolveGitBinary.js +21 -0
- package/src/resolveJjBinary.js +69 -0
- package/src/vcsToolingStatus.js +55 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/vcs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "VCS discovery and jj workspace operations for Smithers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -22,13 +22,20 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@effect/platform": "^0.96.0",
|
|
24
24
|
"effect": "^3.21.1",
|
|
25
|
-
"@smithers-orchestrator/observability": "0.
|
|
25
|
+
"@smithers-orchestrator/observability": "0.23.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@effect/platform-bun": "^0.89.0",
|
|
29
29
|
"@types/bun": "latest",
|
|
30
30
|
"typescript": "~5.9.3"
|
|
31
31
|
},
|
|
32
|
+
"optionalDependencies": {
|
|
33
|
+
"@smithers-orchestrator/jj-darwin-arm64": "0.23.0",
|
|
34
|
+
"@smithers-orchestrator/jj-darwin-x64": "0.23.0",
|
|
35
|
+
"@smithers-orchestrator/jj-linux-x64": "0.23.0",
|
|
36
|
+
"@smithers-orchestrator/jj-win32-x64": "0.23.0",
|
|
37
|
+
"@smithers-orchestrator/jj-linux-arm64": "0.23.0"
|
|
38
|
+
},
|
|
32
39
|
"scripts": {
|
|
33
40
|
"build": "tsup --dts-only",
|
|
34
41
|
"test": "bun test tests",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A resolved VCS executable plus where Smithers found it.
|
|
3
|
+
*
|
|
4
|
+
* - `env`: an explicit override (e.g. `SMITHERS_JJ_PATH`)
|
|
5
|
+
* - `bundled`: a binary shipped inside a `@smithers-orchestrator/jj-<platform>` package
|
|
6
|
+
* - `path`: the bare command name, left for the OS to resolve against `PATH`
|
|
7
|
+
*/
|
|
8
|
+
export type ResolvedBinary = {
|
|
9
|
+
path: string;
|
|
10
|
+
source: "env" | "bundled" | "path";
|
|
11
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type WorkspaceSnapshot = {
|
|
2
|
+
/**
|
|
3
|
+
* Working-copy commit id at this snapshot. Advances on every snapshot, so it
|
|
4
|
+
* addresses an individual working-copy state (unlike `changeId`, which is
|
|
5
|
+
* stable across edits to the same working copy).
|
|
6
|
+
*/
|
|
7
|
+
commitId: string;
|
|
8
|
+
/** Change id of `@`. Stable across edits to one working copy; grouping metadata only. */
|
|
9
|
+
changeId: string;
|
|
10
|
+
/**
|
|
11
|
+
* jj operation id for this snapshot. The durable restore handle: the commit id
|
|
12
|
+
* of an abandoned working-copy commit can be garbage-collected, while the
|
|
13
|
+
* operation log is retained under the configured gc policy.
|
|
14
|
+
*/
|
|
15
|
+
operationId: string;
|
|
16
|
+
};
|
package/src/index.d.ts
CHANGED
|
@@ -116,4 +116,81 @@ type WorkspaceAddOptions = WorkspaceAddOptions$1;
|
|
|
116
116
|
type WorkspaceInfo = WorkspaceInfo$1;
|
|
117
117
|
type WorkspaceResult = WorkspaceResult$1;
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* A resolved VCS executable plus where Smithers found it.
|
|
121
|
+
*
|
|
122
|
+
* - `env`: an explicit override (e.g. `SMITHERS_JJ_PATH`)
|
|
123
|
+
* - `bundled`: a binary shipped inside a `@smithers-orchestrator/jj-<platform>` package
|
|
124
|
+
* - `path`: the bare command name, left for the OS to resolve against `PATH`
|
|
125
|
+
*/
|
|
126
|
+
type ResolvedBinary$3 = {
|
|
127
|
+
path: string;
|
|
128
|
+
source: "env" | "bundled" | "path";
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** @typedef {import("./ResolvedBinary.js").ResolvedBinary} ResolvedBinary */
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the `git` executable Smithers should spawn.
|
|
134
|
+
*
|
|
135
|
+
* Order of preference:
|
|
136
|
+
* 1. `SMITHERS_GIT_PATH` — an explicit override pointing at a real file.
|
|
137
|
+
* 2. The bare `"git"`, left for the OS to resolve against `PATH`.
|
|
138
|
+
*
|
|
139
|
+
* Git is never bundled (only jj is); this mirrors {@link resolveJjBinary} so the
|
|
140
|
+
* override and the tooling preflight share one source of truth for where git is.
|
|
141
|
+
*
|
|
142
|
+
* @returns {ResolvedBinary}
|
|
143
|
+
*/
|
|
144
|
+
declare function resolveGitBinary(): ResolvedBinary$2;
|
|
145
|
+
type ResolvedBinary$2 = ResolvedBinary$3;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve the `jj` executable Smithers should spawn.
|
|
149
|
+
*
|
|
150
|
+
* Order of preference:
|
|
151
|
+
* 1. `SMITHERS_JJ_PATH` — an explicit override pointing at a real file.
|
|
152
|
+
* 2. A binary bundled via `@smithers-orchestrator/jj-<platform>`.
|
|
153
|
+
* 3. The bare `"jj"`, left for the OS to resolve against `PATH`.
|
|
154
|
+
*
|
|
155
|
+
* Always returns a spawnable command. When jj is genuinely absent the bare
|
|
156
|
+
* `"jj"` simply fails to spawn, which `runJj` already normalizes to exit code
|
|
157
|
+
* 127, so callers keep their soft-failure behavior.
|
|
158
|
+
*
|
|
159
|
+
* @returns {ResolvedBinary}
|
|
160
|
+
*/
|
|
161
|
+
declare function resolveJjBinary(): ResolvedBinary$1;
|
|
162
|
+
type ResolvedBinary$1 = ResolvedBinary$3;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Probe whether a usable `jj` and/or `git` exists for the current host, using
|
|
166
|
+
* the override → bundled → PATH resolution for jj and override → PATH for git.
|
|
167
|
+
*
|
|
168
|
+
* Synchronous and best-effort: used by `smithers doctor` and run preflights to
|
|
169
|
+
* tell the user — before a run fails deep in worktree creation — that no VCS
|
|
170
|
+
* tooling is installed, and which knob (bundled package, PATH install, or
|
|
171
|
+
* `SMITHERS_JJ_PATH`) would fix it.
|
|
172
|
+
*
|
|
173
|
+
* @returns {VcsToolingStatus}
|
|
174
|
+
*/
|
|
175
|
+
declare function vcsToolingStatus(): VcsToolingStatus;
|
|
176
|
+
type ResolvedBinary = ResolvedBinary$3;
|
|
177
|
+
/**
|
|
178
|
+
* Whether a usable `jj` and/or `git` exists for the current host. Each field is
|
|
179
|
+
* the resolved binary when `<bin> --version` runs, or null when it does not.
|
|
180
|
+
*/
|
|
181
|
+
type VcsToolingStatus = {
|
|
182
|
+
/**
|
|
183
|
+
* a usable jj (override, bundled, or PATH), else null
|
|
184
|
+
*/
|
|
185
|
+
jj: ResolvedBinary | null;
|
|
186
|
+
/**
|
|
187
|
+
* a usable git (override or PATH), else null
|
|
188
|
+
*/
|
|
189
|
+
git: ResolvedBinary | null;
|
|
190
|
+
/**
|
|
191
|
+
* true when at least one of jj or git is usable
|
|
192
|
+
*/
|
|
193
|
+
ok: boolean;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export { type VcsToolingStatus, findVcsRoot, getJjPointer, isJjRepo, resolveGitBinary, resolveJjBinary, revertToJjPointer, runJj, vcsToolingStatus, workspaceAdd, workspaceClose, workspaceList };
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export * from "./find-root.js";
|
|
2
2
|
export * from "./jj.js";
|
|
3
|
+
export * from "./resolveGitBinary.js";
|
|
4
|
+
export * from "./resolveJjBinary.js";
|
|
5
|
+
export * from "./ResolvedBinary.js";
|
|
6
|
+
export * from "./vcsToolingStatus.js";
|
|
3
7
|
export * from "./JjRevertResult.js";
|
|
4
8
|
export * from "./RunJjOptions.js";
|
|
5
9
|
export * from "./RunJjResult.js";
|
|
6
10
|
export * from "./WorkspaceAddOptions.js";
|
|
7
11
|
export * from "./WorkspaceInfo.js";
|
|
8
12
|
export * from "./WorkspaceResult.js";
|
|
13
|
+
export * from "./WorkspaceSnapshot.js";
|
package/src/jj.js
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
/** @typedef {import("./WorkspaceAddOptions.js").WorkspaceAddOptions} WorkspaceAddOptions */
|
|
6
6
|
/** @typedef {import("./WorkspaceInfo.js").WorkspaceInfo} WorkspaceInfo */
|
|
7
7
|
/** @typedef {import("./WorkspaceResult.js").WorkspaceResult} WorkspaceResult */
|
|
8
|
+
/** @typedef {import("./WorkspaceSnapshot.js").WorkspaceSnapshot} WorkspaceSnapshot */
|
|
8
9
|
// @smithers-type-exports-end
|
|
9
10
|
|
|
10
11
|
import * as Command from "@effect/platform/Command";
|
|
11
12
|
import { Duration, Effect, Fiber, Metric, Stream } from "effect";
|
|
12
13
|
import { vcsDuration } from "@smithers-orchestrator/observability/metrics";
|
|
14
|
+
import { resolveJjBinary } from "./resolveJjBinary.js";
|
|
13
15
|
|
|
14
16
|
const JJ_POINTER_TIMEOUT_MS = 1_500;
|
|
17
|
+
const WORKSPACE_SNAPSHOT_TIMEOUT_MS = 1_500;
|
|
15
18
|
/**
|
|
16
19
|
* @param {Stream.Stream<Uint8Array, unknown, never>} stream
|
|
17
20
|
* @returns {Effect.Effect<string, unknown, never>}
|
|
@@ -29,7 +32,7 @@ function collectUtf8(stream) {
|
|
|
29
32
|
* @returns {Effect.Effect<RunJjResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
30
33
|
*/
|
|
31
34
|
export function runJj(args, opts = {}) {
|
|
32
|
-
let command = Command.make(
|
|
35
|
+
let command = Command.make(resolveJjBinary().path, ...args);
|
|
33
36
|
if (opts.cwd) {
|
|
34
37
|
command = Command.workingDirectory(command, opts.cwd);
|
|
35
38
|
}
|
|
@@ -88,6 +91,55 @@ export function getJjPointer(cwd) {
|
|
|
88
91
|
return out ? out : null;
|
|
89
92
|
}), Effect.annotateLogs({ cwd: cwd ?? "" }), Effect.withLogSpan("vcs:jj-pointer"));
|
|
90
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Wrap a snapshot jj call with a bounded timeout so a slow or hung jj cannot
|
|
96
|
+
* block the agent. On timeout it returns a sentinel non-zero result, which the
|
|
97
|
+
* caller treats as a durability gap rather than a value.
|
|
98
|
+
*
|
|
99
|
+
* @param {Effect.Effect<RunJjResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>} effect
|
|
100
|
+
* @param {string} label
|
|
101
|
+
* @returns {Effect.Effect<RunJjResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
102
|
+
*/
|
|
103
|
+
function withSnapshotTimeout(effect, label) {
|
|
104
|
+
return effect.pipe(Effect.timeoutTo({
|
|
105
|
+
duration: Duration.millis(WORKSPACE_SNAPSHOT_TIMEOUT_MS),
|
|
106
|
+
onSuccess: (res) => res,
|
|
107
|
+
onTimeout: () => ({
|
|
108
|
+
code: 124,
|
|
109
|
+
stdout: "",
|
|
110
|
+
stderr: `${label} timed out after ${WORKSPACE_SNAPSHOT_TIMEOUT_MS}ms`,
|
|
111
|
+
}),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Capture the current working-copy state as a restorable handle.
|
|
116
|
+
*
|
|
117
|
+
* Step 1 (`jj log -r @`) forces exactly one working-copy snapshot and returns the
|
|
118
|
+
* resulting `commit_id` and `change_id`. Step 2 reads the latest operation id
|
|
119
|
+
* WITHOUT taking a second snapshot (`--ignore-working-copy`), so both ids describe
|
|
120
|
+
* the same snapshot from step 1. Returns null on any failure or timeout (a
|
|
121
|
+
* durability gap the caller records); it never throws into the agent path.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} [cwd]
|
|
124
|
+
* @returns {Effect.Effect<WorkspaceSnapshot | null, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
125
|
+
*/
|
|
126
|
+
export function captureWorkspaceSnapshot(cwd) {
|
|
127
|
+
return Effect.gen(function* () {
|
|
128
|
+
const logRes = yield* withSnapshotTimeout(runJj(["log", "-r", "@", "--no-graph", "-T", 'commit_id ++ "\\n" ++ change_id'], { cwd }), "jj snapshot log");
|
|
129
|
+
if (logRes.code !== 0)
|
|
130
|
+
return null;
|
|
131
|
+
const [commitId, changeId] = logRes.stdout.split("\n").map((part) => part.trim());
|
|
132
|
+
if (!commitId)
|
|
133
|
+
return null;
|
|
134
|
+
const opRes = yield* withSnapshotTimeout(runJj(["--ignore-working-copy", "operation", "log", "--no-graph", "--limit", "1", "-T", "self.id()"], { cwd }), "jj snapshot op");
|
|
135
|
+
if (opRes.code !== 0)
|
|
136
|
+
return null;
|
|
137
|
+
const operationId = opRes.stdout.trim();
|
|
138
|
+
if (!operationId)
|
|
139
|
+
return null;
|
|
140
|
+
return { commitId, changeId: changeId ?? "", operationId };
|
|
141
|
+
}).pipe(Effect.annotateLogs({ cwd: cwd ?? "" }), Effect.withLogSpan("vcs:jj-snapshot"));
|
|
142
|
+
}
|
|
91
143
|
/**
|
|
92
144
|
* Restore the working copy to a previously recorded jujutsu `change_id`.
|
|
93
145
|
* Used by the engine to revert attempts within the correct repo/worktree (via `cwd`).
|
|
@@ -147,7 +199,11 @@ export function workspaceAdd(name, path, opts = {}) {
|
|
|
147
199
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
148
200
|
}
|
|
149
201
|
}
|
|
150
|
-
catch {
|
|
202
|
+
catch (error) {
|
|
203
|
+
// Best-effort cleanup: do not abort workspace creation if it fails,
|
|
204
|
+
// but log a warning so cleanup failures are diagnosable rather than silent.
|
|
205
|
+
yield* Effect.logWarning(`jj workspace pre-create cleanup failed at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
}
|
|
151
207
|
let lastErr = "";
|
|
152
208
|
for (const args of attempts) {
|
|
153
209
|
const res = yield* runJj(args, { cwd: opts.cwd });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./ResolvedBinary.js").ResolvedBinary} ResolvedBinary */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the `git` executable Smithers should spawn.
|
|
7
|
+
*
|
|
8
|
+
* Order of preference:
|
|
9
|
+
* 1. `SMITHERS_GIT_PATH` — an explicit override pointing at a real file.
|
|
10
|
+
* 2. The bare `"git"`, left for the OS to resolve against `PATH`.
|
|
11
|
+
*
|
|
12
|
+
* Git is never bundled (only jj is); this mirrors {@link resolveJjBinary} so the
|
|
13
|
+
* override and the tooling preflight share one source of truth for where git is.
|
|
14
|
+
*
|
|
15
|
+
* @returns {ResolvedBinary}
|
|
16
|
+
*/
|
|
17
|
+
export function resolveGitBinary() {
|
|
18
|
+
const override = process.env.SMITHERS_GIT_PATH;
|
|
19
|
+
if (override && existsSync(override)) return { path: override, source: "env" };
|
|
20
|
+
return { path: "git", source: "path" };
|
|
21
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** @typedef {import("./ResolvedBinary.js").ResolvedBinary} ResolvedBinary */
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `${process.platform}-${process.arch}` → the npm package that vendors a `jj`
|
|
11
|
+
* binary for that target. Each package carries `os`/`cpu` fields so a package
|
|
12
|
+
* manager only installs the one matching the host, and exposes the binary at
|
|
13
|
+
* `bin/jj` (`bin/jj.exe` on Windows).
|
|
14
|
+
*
|
|
15
|
+
* Kept in sync with the platform packages under `packages/jj-binaries/` and the
|
|
16
|
+
* `optionalDependencies` of `@smithers-orchestrator/vcs`.
|
|
17
|
+
*
|
|
18
|
+
* @type {Record<string, string>}
|
|
19
|
+
*/
|
|
20
|
+
const BUNDLED_PACKAGES = {
|
|
21
|
+
"darwin-arm64": "@smithers-orchestrator/jj-darwin-arm64",
|
|
22
|
+
"darwin-x64": "@smithers-orchestrator/jj-darwin-x64",
|
|
23
|
+
"linux-arm64": "@smithers-orchestrator/jj-linux-arm64",
|
|
24
|
+
"linux-x64": "@smithers-orchestrator/jj-linux-x64",
|
|
25
|
+
"win32-x64": "@smithers-orchestrator/jj-win32-x64",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Locate the bundled `jj` binary for the current host, or null when no platform
|
|
30
|
+
* package is installed (unsupported target, `--no-optional` install, or not yet
|
|
31
|
+
* published). Resolution goes through the package's `package.json` so it works
|
|
32
|
+
* regardless of hoisting layout.
|
|
33
|
+
*
|
|
34
|
+
* @returns {string | null}
|
|
35
|
+
*/
|
|
36
|
+
function bundledJjPath() {
|
|
37
|
+
const pkg = BUNDLED_PACKAGES[`${process.platform}-${process.arch}`];
|
|
38
|
+
if (!pkg) return null;
|
|
39
|
+
const binary = process.platform === "win32" ? "jj.exe" : "jj";
|
|
40
|
+
try {
|
|
41
|
+
const manifest = require.resolve(`${pkg}/package.json`);
|
|
42
|
+
const candidate = join(manifest, "..", "bin", binary);
|
|
43
|
+
return existsSync(candidate) ? candidate : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the `jj` executable Smithers should spawn.
|
|
51
|
+
*
|
|
52
|
+
* Order of preference:
|
|
53
|
+
* 1. `SMITHERS_JJ_PATH` — an explicit override pointing at a real file.
|
|
54
|
+
* 2. A binary bundled via `@smithers-orchestrator/jj-<platform>`.
|
|
55
|
+
* 3. The bare `"jj"`, left for the OS to resolve against `PATH`.
|
|
56
|
+
*
|
|
57
|
+
* Always returns a spawnable command. When jj is genuinely absent the bare
|
|
58
|
+
* `"jj"` simply fails to spawn, which `runJj` already normalizes to exit code
|
|
59
|
+
* 127, so callers keep their soft-failure behavior.
|
|
60
|
+
*
|
|
61
|
+
* @returns {ResolvedBinary}
|
|
62
|
+
*/
|
|
63
|
+
export function resolveJjBinary() {
|
|
64
|
+
const override = process.env.SMITHERS_JJ_PATH;
|
|
65
|
+
if (override && existsSync(override)) return { path: override, source: "env" };
|
|
66
|
+
const bundled = bundledJjPath();
|
|
67
|
+
if (bundled) return { path: bundled, source: "bundled" };
|
|
68
|
+
return { path: "jj", source: "path" };
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { resolveJjBinary } from "./resolveJjBinary.js";
|
|
3
|
+
import { resolveGitBinary } from "./resolveGitBinary.js";
|
|
4
|
+
|
|
5
|
+
/** @typedef {import("./ResolvedBinary.js").ResolvedBinary} ResolvedBinary */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether a usable `jj` and/or `git` exists for the current host. Each field is
|
|
9
|
+
* the resolved binary when `<bin> --version` runs, or null when it does not.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {object} VcsToolingStatus
|
|
12
|
+
* @property {ResolvedBinary | null} jj a usable jj (override, bundled, or PATH), else null
|
|
13
|
+
* @property {ResolvedBinary | null} git a usable git (override or PATH), else null
|
|
14
|
+
* @property {boolean} ok true when at least one of jj or git is usable
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const VERSION_PROBE_TIMEOUT_MS = 2_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether `<bin> --version` exits 0. Best-effort: a missing binary, a non-zero
|
|
21
|
+
* exit, or a spawn error all read as "not usable".
|
|
22
|
+
*
|
|
23
|
+
* @param {ResolvedBinary} bin
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function runsVersion(bin) {
|
|
27
|
+
try {
|
|
28
|
+
const res = spawnSync(bin.path, ["--version"], {
|
|
29
|
+
stdio: "ignore",
|
|
30
|
+
timeout: VERSION_PROBE_TIMEOUT_MS,
|
|
31
|
+
});
|
|
32
|
+
return res.status === 0;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Probe whether a usable `jj` and/or `git` exists for the current host, using
|
|
40
|
+
* the override → bundled → PATH resolution for jj and override → PATH for git.
|
|
41
|
+
*
|
|
42
|
+
* Synchronous and best-effort: used by `smithers doctor` and run preflights to
|
|
43
|
+
* tell the user — before a run fails deep in worktree creation — that no VCS
|
|
44
|
+
* tooling is installed, and which knob (bundled package, PATH install, or
|
|
45
|
+
* `SMITHERS_JJ_PATH`) would fix it.
|
|
46
|
+
*
|
|
47
|
+
* @returns {VcsToolingStatus}
|
|
48
|
+
*/
|
|
49
|
+
export function vcsToolingStatus() {
|
|
50
|
+
const jjBin = resolveJjBinary();
|
|
51
|
+
const gitBin = resolveGitBinary();
|
|
52
|
+
const jj = runsVersion(jjBin) ? jjBin : null;
|
|
53
|
+
const git = runsVersion(gitBin) ? gitBin : null;
|
|
54
|
+
return { jj, git, ok: Boolean(jj || git) };
|
|
55
|
+
}
|