@smithers-orchestrator/vcs 0.24.0 → 0.25.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/vcs",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "VCS discovery and jj workspace operations for Smithers",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@effect/platform": "^0.96.0",
24
24
  "effect": "^3.21.1",
25
- "@smithers-orchestrator/observability": "0.24.0"
25
+ "@smithers-orchestrator/observability": "0.25.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@effect/platform-bun": "^0.89.0",
@@ -30,11 +30,11 @@
30
30
  "typescript": "~5.9.3"
31
31
  },
32
32
  "optionalDependencies": {
33
- "@smithers-orchestrator/jj-darwin-x64": "0.24.0",
34
- "@smithers-orchestrator/jj-darwin-arm64": "0.24.0",
35
- "@smithers-orchestrator/jj-linux-x64": "0.24.0",
36
- "@smithers-orchestrator/jj-linux-arm64": "0.24.0",
37
- "@smithers-orchestrator/jj-win32-x64": "0.24.0"
33
+ "@smithers-orchestrator/jj-darwin-arm64": "0.25.0",
34
+ "@smithers-orchestrator/jj-darwin-x64": "0.25.0",
35
+ "@smithers-orchestrator/jj-linux-arm64": "0.25.0",
36
+ "@smithers-orchestrator/jj-win32-x64": "0.25.0",
37
+ "@smithers-orchestrator/jj-linux-x64": "0.25.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "rm -f src/index.d.ts && tsup --dts-only",
package/src/find-root.js CHANGED
@@ -1,29 +1,26 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve, dirname, parse } from "node:path";
3
- import { Effect } from "effect";
4
3
  /**
5
4
  * Walk up from `startDir` to find the nearest directory containing `.jj` or `.git`.
6
5
  * Prefers `.jj` over `.git` so colocated repos (both exist) use jj semantics.
7
6
  * Returns the VCS type and root path, or null if neither is found.
8
7
  *
9
8
  * @param {string} startDir
10
- * @returns {Effect.Effect<{ type: "jj"; root: string } | { type: "git"; root: string } | null, never, never>}
9
+ * @returns {{ type: "jj"; root: string } | { type: "git"; root: string } | null}
11
10
  */
12
11
  export function findVcsRoot(startDir) {
13
- return Effect.sync(() => {
14
- let dir = resolve(startDir);
15
- const { root: fsRoot } = parse(dir);
16
- while (dir !== fsRoot) {
17
- if (existsSync(resolve(dir, ".jj")))
18
- return /** @type {{ type: "jj"; root: string }} */ ({ type: "jj", root: dir });
19
- if (existsSync(resolve(dir, ".git")))
20
- return /** @type {{ type: "git"; root: string }} */ ({ type: "git", root: dir });
21
- dir = dirname(dir);
22
- }
12
+ let dir = resolve(startDir);
13
+ const { root: fsRoot } = parse(dir);
14
+ while (dir !== fsRoot) {
23
15
  if (existsSync(resolve(dir, ".jj")))
24
16
  return /** @type {{ type: "jj"; root: string }} */ ({ type: "jj", root: dir });
25
17
  if (existsSync(resolve(dir, ".git")))
26
18
  return /** @type {{ type: "git"; root: string }} */ ({ type: "git", root: dir });
27
- return null;
28
- });
19
+ dir = dirname(dir);
20
+ }
21
+ if (existsSync(resolve(dir, ".jj")))
22
+ return /** @type {{ type: "jj"; root: string }} */ ({ type: "jj", root: dir });
23
+ if (existsSync(resolve(dir, ".git")))
24
+ return /** @type {{ type: "git"; root: string }} */ ({ type: "git", root: dir });
25
+ return null;
29
26
  }
package/src/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Effect } from 'effect';
2
1
  import * as _effect_platform_CommandExecutor from '@effect/platform/CommandExecutor';
2
+ import { Effect } from 'effect';
3
+ import { existsSync } from 'node:fs';
3
4
 
4
5
  /**
5
6
  * Walk up from `startDir` to find the nearest directory containing `.jj` or `.git`.
@@ -7,15 +8,15 @@ import * as _effect_platform_CommandExecutor from '@effect/platform/CommandExecu
7
8
  * Returns the VCS type and root path, or null if neither is found.
8
9
  *
9
10
  * @param {string} startDir
10
- * @returns {Effect.Effect<{ type: "jj"; root: string } | { type: "git"; root: string } | null, never, never>}
11
+ * @returns {{ type: "jj"; root: string } | { type: "git"; root: string } | null}
11
12
  */
12
- declare function findVcsRoot(startDir: string): Effect.Effect<{
13
+ declare function findVcsRoot(startDir: string): {
13
14
  type: "jj";
14
15
  root: string;
15
16
  } | {
16
17
  type: "git";
17
18
  root: string;
18
- } | null, never, never>;
19
+ } | null;
19
20
 
20
21
  type WorkspaceResult$1 = {
21
22
  success: boolean;
@@ -65,6 +66,15 @@ declare function runJj(args: string[], opts?: RunJjOptions): Effect.Effect<RunJj
65
66
  * @returns {Effect.Effect<string | null, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
66
67
  */
67
68
  declare function getJjPointer(cwd?: string): Effect.Effect<string | null, never, _effect_platform_CommandExecutor.CommandExecutor>;
69
+ /**
70
+ * Parse the snapshot values returned by the two jj commands in
71
+ * {@link captureWorkspaceSnapshot}.
72
+ *
73
+ * @param {string} logStdout stdout from `jj log -r @ ...`
74
+ * @param {string} opStdout stdout from `jj operation log ...`
75
+ * @returns {WorkspaceSnapshot | null}
76
+ */
77
+ declare function parseWorkspaceSnapshot(logStdout: string, opStdout: string): WorkspaceSnapshot | null;
68
78
  /**
69
79
  * Capture the current working-copy state as a restorable handle.
70
80
  *
@@ -169,6 +179,20 @@ type ResolvedBinary = {
169
179
  */
170
180
  declare function resolveGitBinary(): ResolvedBinary;
171
181
 
182
+ /**
183
+ * Locate the bundled `jj` binary for the current host, or null when no platform
184
+ * package is installed (unsupported target, `--no-optional` install, or not yet
185
+ * published). Resolution goes through the package's `package.json` so it works
186
+ * regardless of hoisting layout.
187
+ *
188
+ * @returns {string | null}
189
+ */
190
+ declare function resolveBundledJjPath({ platform, arch, resolvePackage, fileExists, }?: {
191
+ platform?: NodeJS.Platform | undefined;
192
+ arch?: NodeJS.Architecture | undefined;
193
+ resolvePackage?: NodeJS.RequireResolve | undefined;
194
+ fileExists?: typeof existsSync | undefined;
195
+ }): string | null;
172
196
  /**
173
197
  * Resolve the `jj` executable Smithers should spawn.
174
198
  *
@@ -185,6 +209,14 @@ declare function resolveGitBinary(): ResolvedBinary;
185
209
  */
186
210
  declare function resolveJjBinary(): ResolvedBinary;
187
211
 
212
+ /**
213
+ * Whether `<bin> --version` exits 0. Best-effort: a missing binary, a non-zero
214
+ * exit, or a spawn error all read as "not usable".
215
+ *
216
+ * @param {import("./ResolvedBinary.js").ResolvedBinary} bin
217
+ * @returns {boolean}
218
+ */
219
+ declare function runsVersion(bin: ResolvedBinary): boolean;
188
220
  /**
189
221
  * Probe whether a usable `jj` and/or `git` exists for the current host, using
190
222
  * the override → bundled → PATH resolution for jj and override → PATH for git.
@@ -216,4 +248,4 @@ type VcsToolingStatus = {
216
248
  ok: boolean;
217
249
  };
218
250
 
219
- export { type JjRevertResult, type RunJjOptions, type RunJjResult, type VcsToolingStatus, type WorkspaceAddOptions, type WorkspaceInfo, type WorkspaceResult, type WorkspaceSnapshot, captureWorkspaceSnapshot, findVcsRoot, getJjPointer, isJjRepo, resolveGitBinary, resolveJjBinary, revertToJjPointer, runJj, vcsToolingStatus, workspaceAdd, workspaceClose, workspaceList };
251
+ export { type JjRevertResult, type RunJjOptions, type RunJjResult, type VcsToolingStatus, type WorkspaceAddOptions, type WorkspaceInfo, type WorkspaceResult, type WorkspaceSnapshot, captureWorkspaceSnapshot, findVcsRoot, getJjPointer, isJjRepo, parseWorkspaceSnapshot, resolveBundledJjPath, resolveGitBinary, resolveJjBinary, revertToJjPointer, runJj, runsVersion, vcsToolingStatus, workspaceAdd, workspaceClose, workspaceList };
package/src/jj.js CHANGED
@@ -14,6 +14,8 @@
14
14
  // @smithers-type-exports-end
15
15
 
16
16
  import * as Command from "@effect/platform/Command";
17
+ import * as fs from "node:fs";
18
+ import * as nodePath from "node:path";
17
19
  import { Duration, Effect, Fiber, Metric, Stream } from "effect";
18
20
  import { vcsDuration } from "@smithers-orchestrator/observability/metrics";
19
21
  import { resolveJjBinary } from "./resolveJjBinary.js";
@@ -43,19 +45,20 @@ export function runJj(args, opts = {}) {
43
45
  }
44
46
  return Effect.scoped(Effect.gen(function* () {
45
47
  const start = performance.now();
46
- yield* Effect.logDebug(`jj ${args.join(" ")}`);
47
- const process = yield* Command.start(command);
48
- const stdoutFiber = yield* Effect.fork(collectUtf8(process.stdout));
49
- const stderrFiber = yield* Effect.fork(collectUtf8(process.stderr));
50
- const exitCode = yield* process.exitCode;
51
- const stdout = yield* Fiber.join(stdoutFiber);
52
- const stderr = yield* Fiber.join(stderrFiber);
53
- yield* Metric.update(vcsDuration, performance.now() - start);
54
- return {
55
- code: Number(exitCode),
56
- stdout,
57
- stderr,
58
- };
48
+ return yield* Effect.gen(function* () {
49
+ yield* Effect.logDebug(`jj ${args.join(" ")}`);
50
+ const process = yield* Command.start(command);
51
+ const stdoutFiber = yield* Effect.fork(collectUtf8(process.stdout));
52
+ const stderrFiber = yield* Effect.fork(collectUtf8(process.stderr));
53
+ const exitCode = yield* process.exitCode;
54
+ const stdout = yield* Fiber.join(stdoutFiber);
55
+ const stderr = yield* Fiber.join(stderrFiber);
56
+ return {
57
+ code: Number(exitCode),
58
+ stdout,
59
+ stderr,
60
+ };
61
+ }).pipe(Effect.ensuring(Effect.suspend(() => Metric.update(vcsDuration, performance.now() - start))));
59
62
  })).pipe(Effect.annotateLogs({
60
63
  vcs: "jj",
61
64
  cwd: opts.cwd ?? "",
@@ -116,6 +119,26 @@ function withSnapshotTimeout(effect, label) {
116
119
  }),
117
120
  }));
118
121
  }
122
+ /**
123
+ * Parse the snapshot values returned by the two jj commands in
124
+ * {@link captureWorkspaceSnapshot}.
125
+ *
126
+ * @param {string} logStdout stdout from `jj log -r @ ...`
127
+ * @param {string} opStdout stdout from `jj operation log ...`
128
+ * @returns {WorkspaceSnapshot | null}
129
+ */
130
+ export function parseWorkspaceSnapshot(logStdout, opStdout) {
131
+ const [commitId, changeId] = logStdout.split("\n").map((part) => part.trim());
132
+ if (!commitId)
133
+ return null;
134
+ const operationId = opStdout
135
+ .split(/\r?\n/)
136
+ .map((part) => part.trim())
137
+ .find(Boolean);
138
+ if (!operationId)
139
+ return null;
140
+ return { commitId, changeId: changeId ?? "", operationId };
141
+ }
119
142
  /**
120
143
  * Capture the current working-copy state as a restorable handle.
121
144
  *
@@ -133,16 +156,10 @@ export function captureWorkspaceSnapshot(cwd) {
133
156
  const logRes = yield* withSnapshotTimeout(runJj(["log", "-r", "@", "--no-graph", "-T", 'commit_id ++ "\\n" ++ change_id'], { cwd }), "jj snapshot log");
134
157
  if (logRes.code !== 0)
135
158
  return null;
136
- const [commitId, changeId] = logRes.stdout.split("\n").map((part) => part.trim());
137
- if (!commitId)
138
- return null;
139
159
  const opRes = yield* withSnapshotTimeout(runJj(["--ignore-working-copy", "operation", "log", "--no-graph", "--limit", "1", "-T", "self.id()"], { cwd }), "jj snapshot op");
140
160
  if (opRes.code !== 0)
141
161
  return null;
142
- const operationId = opRes.stdout.trim();
143
- if (!operationId)
144
- return null;
145
- return { commitId, changeId: changeId ?? "", operationId };
162
+ return parseWorkspaceSnapshot(logRes.stdout, opRes.stdout);
146
163
  }).pipe(Effect.annotateLogs({ cwd: cwd ?? "" }), Effect.withLogSpan("vcs:jj-snapshot"));
147
164
  }
148
165
  /**
@@ -194,8 +211,6 @@ export function workspaceAdd(name, path, opts = {}) {
194
211
  yield* runJj(["workspace", "forget", name], { cwd: opts.cwd });
195
212
  }
196
213
  try {
197
- const fs = require("node:fs");
198
- const nodePath = require("node:path");
199
214
  if (fs.existsSync(path)) {
200
215
  fs.rmSync(path, { recursive: true, force: true });
201
216
  }
@@ -31,14 +31,19 @@ const BUNDLED_PACKAGES = {
31
31
  *
32
32
  * @returns {string | null}
33
33
  */
34
- function bundledJjPath() {
35
- const pkg = BUNDLED_PACKAGES[`${process.platform}-${process.arch}`];
34
+ export function resolveBundledJjPath({
35
+ platform = process.platform,
36
+ arch = process.arch,
37
+ resolvePackage = require.resolve,
38
+ fileExists = existsSync,
39
+ } = {}) {
40
+ const pkg = BUNDLED_PACKAGES[`${platform}-${arch}`];
36
41
  if (!pkg) return null;
37
- const binary = process.platform === "win32" ? "jj.exe" : "jj";
42
+ const binary = platform === "win32" ? "jj.exe" : "jj";
38
43
  try {
39
- const manifest = require.resolve(`${pkg}/package.json`);
44
+ const manifest = resolvePackage(`${pkg}/package.json`);
40
45
  const candidate = join(manifest, "..", "bin", binary);
41
- return existsSync(candidate) ? candidate : null;
46
+ return fileExists(candidate) ? candidate : null;
42
47
  } catch {
43
48
  return null;
44
49
  }
@@ -61,7 +66,7 @@ function bundledJjPath() {
61
66
  export function resolveJjBinary() {
62
67
  const override = process.env.SMITHERS_JJ_PATH;
63
68
  if (override && existsSync(override)) return { path: override, source: "env" };
64
- const bundled = bundledJjPath();
69
+ const bundled = resolveBundledJjPath();
65
70
  if (bundled) return { path: bundled, source: "bundled" };
66
71
  return { path: "jj", source: "path" };
67
72
  }
@@ -21,7 +21,7 @@ const VERSION_PROBE_TIMEOUT_MS = 2_000;
21
21
  * @param {import("./ResolvedBinary.js").ResolvedBinary} bin
22
22
  * @returns {boolean}
23
23
  */
24
- function runsVersion(bin) {
24
+ export function runsVersion(bin) {
25
25
  try {
26
26
  const res = spawnSync(bin.path, ["--version"], {
27
27
  stdio: "ignore",
@@ -1,16 +0,0 @@
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
- };