@smithers-orchestrator/vcs 0.16.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/LICENSE +21 -0
- package/package.json +37 -0
- package/src/JjRevertResult.ts +4 -0
- package/src/RunJjOptions.ts +3 -0
- package/src/RunJjResult.ts +5 -0
- package/src/WorkspaceAddOptions.ts +4 -0
- package/src/WorkspaceInfo.ts +5 -0
- package/src/WorkspaceResult.ts +4 -0
- package/src/find-root.js +27 -0
- package/src/index.d.ts +119 -0
- package/src/index.js +8 -0
- package/src/jj.js +217 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 William Cory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smithers-orchestrator/vcs",
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "VCS discovery and jj workspace operations for Smithers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./*": {
|
|
14
|
+
"types": "./src/index.d.ts",
|
|
15
|
+
"import": "./src/*.js",
|
|
16
|
+
"default": "./src/*.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@effect/platform": "^0.96.0",
|
|
24
|
+
"effect": "^3.21.0",
|
|
25
|
+
"@smithers-orchestrator/observability": "0.16.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@effect/platform-bun": "^0.89.0",
|
|
29
|
+
"@types/bun": "latest",
|
|
30
|
+
"typescript": "~5.9.3"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup --dts-only",
|
|
34
|
+
"test": "bun test tests",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/find-root.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname, parse } from "node:path";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
/**
|
|
5
|
+
* Walk up from `startDir` to find the nearest directory containing `.jj` or `.git`.
|
|
6
|
+
* Prefers `.jj` over `.git` so colocated repos (both exist) use jj semantics.
|
|
7
|
+
* Returns the VCS type and root path, or null if neither is found.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} startDir
|
|
10
|
+
* @returns {Effect.Effect<{ type: "jj"; root: string } | { type: "git"; root: string } | null, never, never>}
|
|
11
|
+
*/
|
|
12
|
+
export function findVcsRoot(startDir) {
|
|
13
|
+
return Effect.sync(() => {
|
|
14
|
+
let dir = resolve(startDir);
|
|
15
|
+
const { root: fsRoot } = parse(dir);
|
|
16
|
+
while (true) {
|
|
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
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir || dir === fsRoot)
|
|
23
|
+
return null;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import * as _effect_platform_CommandExecutor from '@effect/platform/CommandExecutor';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walk up from `startDir` to find the nearest directory containing `.jj` or `.git`.
|
|
6
|
+
* Prefers `.jj` over `.git` so colocated repos (both exist) use jj semantics.
|
|
7
|
+
* Returns the VCS type and root path, or null if neither is found.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} startDir
|
|
10
|
+
* @returns {Effect.Effect<{ type: "jj"; root: string } | { type: "git"; root: string } | null, never, never>}
|
|
11
|
+
*/
|
|
12
|
+
declare function findVcsRoot(startDir: string): Effect.Effect<{
|
|
13
|
+
type: "jj";
|
|
14
|
+
root: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "git";
|
|
17
|
+
root: string;
|
|
18
|
+
} | null, never, never>;
|
|
19
|
+
|
|
20
|
+
type WorkspaceResult$1 = {
|
|
21
|
+
success: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type WorkspaceInfo$1 = {
|
|
26
|
+
name: string;
|
|
27
|
+
path: string | null;
|
|
28
|
+
selected: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type WorkspaceAddOptions$1 = {
|
|
32
|
+
cwd?: string;
|
|
33
|
+
atRev?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type RunJjResult$1 = {
|
|
37
|
+
code: number;
|
|
38
|
+
stdout: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type RunJjOptions$1 = {
|
|
43
|
+
cwd?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type JjRevertResult$1 = {
|
|
47
|
+
success: boolean;
|
|
48
|
+
error?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run a `jj` command and capture output.
|
|
53
|
+
* Minimal helper used by vcs features and safe to call when jj is missing.
|
|
54
|
+
*
|
|
55
|
+
* @param {string[]} args
|
|
56
|
+
* @param {RunJjOptions} [opts]
|
|
57
|
+
* @returns {Effect.Effect<RunJjResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
58
|
+
*/
|
|
59
|
+
declare function runJj(args: string[], opts?: RunJjOptions): Effect.Effect<RunJjResult, never, _effect_platform_CommandExecutor.CommandExecutor>;
|
|
60
|
+
/**
|
|
61
|
+
* Returns the current workspace change id (jj `change_id`) or null on failure.
|
|
62
|
+
* Accepts optional `cwd` to run inside a target repository.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} [cwd]
|
|
65
|
+
* @returns {Effect.Effect<string | null, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
66
|
+
*/
|
|
67
|
+
declare function getJjPointer(cwd?: string): Effect.Effect<string | null, never, _effect_platform_CommandExecutor.CommandExecutor>;
|
|
68
|
+
/**
|
|
69
|
+
* Restore the working copy to a previously recorded jujutsu `change_id`.
|
|
70
|
+
* Used by the engine to revert attempts within the correct repo/worktree (via `cwd`).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} pointer
|
|
73
|
+
* @param {string} [cwd]
|
|
74
|
+
* @returns {Effect.Effect<JjRevertResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
75
|
+
*/
|
|
76
|
+
declare function revertToJjPointer(pointer: string, cwd?: string): Effect.Effect<JjRevertResult, never, _effect_platform_CommandExecutor.CommandExecutor>;
|
|
77
|
+
/**
|
|
78
|
+
* Quick repo detection by executing a read-only jj command.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} [cwd]
|
|
81
|
+
* @returns {Effect.Effect<boolean, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
82
|
+
*/
|
|
83
|
+
declare function isJjRepo(cwd?: string): Effect.Effect<boolean, never, _effect_platform_CommandExecutor.CommandExecutor>;
|
|
84
|
+
/**
|
|
85
|
+
* Create a new JJ workspace at `path` with a friendly `name`.
|
|
86
|
+
* NOTE: Syntax may vary between JJ versions; this helper aims to be permissive.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} name
|
|
89
|
+
* @param {string} path
|
|
90
|
+
* @param {WorkspaceAddOptions} [opts]
|
|
91
|
+
* @returns {Effect.Effect<WorkspaceResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
92
|
+
*/
|
|
93
|
+
declare function workspaceAdd(name: string, path: string, opts?: WorkspaceAddOptions): Effect.Effect<WorkspaceResult, never, _effect_platform_CommandExecutor.CommandExecutor>;
|
|
94
|
+
/**
|
|
95
|
+
* List existing workspaces using a JJ template for structured output.
|
|
96
|
+
* Falls back to parsing human output if `-T` is unavailable.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} [cwd]
|
|
99
|
+
* @returns {Effect.Effect<WorkspaceInfo[], never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
100
|
+
*/
|
|
101
|
+
declare function workspaceList(cwd?: string): Effect.Effect<WorkspaceInfo[], never, _effect_platform_CommandExecutor.CommandExecutor>;
|
|
102
|
+
/**
|
|
103
|
+
* Close the given workspace by name.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} name
|
|
106
|
+
* @param {{ cwd?: string }} [opts]
|
|
107
|
+
* @returns {Effect.Effect<WorkspaceResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
108
|
+
*/
|
|
109
|
+
declare function workspaceClose(name: string, opts?: {
|
|
110
|
+
cwd?: string;
|
|
111
|
+
}): Effect.Effect<WorkspaceResult, never, _effect_platform_CommandExecutor.CommandExecutor>;
|
|
112
|
+
type JjRevertResult = JjRevertResult$1;
|
|
113
|
+
type RunJjOptions = RunJjOptions$1;
|
|
114
|
+
type RunJjResult = RunJjResult$1;
|
|
115
|
+
type WorkspaceAddOptions = WorkspaceAddOptions$1;
|
|
116
|
+
type WorkspaceInfo = WorkspaceInfo$1;
|
|
117
|
+
type WorkspaceResult = WorkspaceResult$1;
|
|
118
|
+
|
|
119
|
+
export { findVcsRoot, getJjPointer, isJjRepo, revertToJjPointer, runJj, workspaceAdd, workspaceClose, workspaceList };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./find-root.js";
|
|
2
|
+
export * from "./jj.js";
|
|
3
|
+
export * from "./JjRevertResult.js";
|
|
4
|
+
export * from "./RunJjOptions.js";
|
|
5
|
+
export * from "./RunJjResult.js";
|
|
6
|
+
export * from "./WorkspaceAddOptions.js";
|
|
7
|
+
export * from "./WorkspaceInfo.js";
|
|
8
|
+
export * from "./WorkspaceResult.js";
|
package/src/jj.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// @smithers-type-exports-begin
|
|
2
|
+
/** @typedef {import("./JjRevertResult.js").JjRevertResult} JjRevertResult */
|
|
3
|
+
/** @typedef {import("./RunJjOptions.js").RunJjOptions} RunJjOptions */
|
|
4
|
+
/** @typedef {import("./RunJjResult.js").RunJjResult} RunJjResult */
|
|
5
|
+
/** @typedef {import("./WorkspaceAddOptions.js").WorkspaceAddOptions} WorkspaceAddOptions */
|
|
6
|
+
/** @typedef {import("./WorkspaceInfo.js").WorkspaceInfo} WorkspaceInfo */
|
|
7
|
+
/** @typedef {import("./WorkspaceResult.js").WorkspaceResult} WorkspaceResult */
|
|
8
|
+
// @smithers-type-exports-end
|
|
9
|
+
|
|
10
|
+
import * as Command from "@effect/platform/Command";
|
|
11
|
+
import { Duration, Effect, Fiber, Metric, Stream } from "effect";
|
|
12
|
+
import { vcsDuration } from "@smithers-orchestrator/observability/metrics";
|
|
13
|
+
|
|
14
|
+
const JJ_POINTER_TIMEOUT_MS = 1_500;
|
|
15
|
+
/**
|
|
16
|
+
* @param {Stream.Stream<Uint8Array, unknown, never>} stream
|
|
17
|
+
* @returns {Effect.Effect<string, unknown, never>}
|
|
18
|
+
*/
|
|
19
|
+
function collectUtf8(stream) {
|
|
20
|
+
const decoder = new TextDecoder("utf-8");
|
|
21
|
+
return Stream.runFold(stream, "", (acc, chunk) => acc + decoder.decode(chunk, { stream: true })).pipe(Effect.map((acc) => acc + decoder.decode()));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run a `jj` command and capture output.
|
|
25
|
+
* Minimal helper used by vcs features and safe to call when jj is missing.
|
|
26
|
+
*
|
|
27
|
+
* @param {string[]} args
|
|
28
|
+
* @param {RunJjOptions} [opts]
|
|
29
|
+
* @returns {Effect.Effect<RunJjResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
30
|
+
*/
|
|
31
|
+
export function runJj(args, opts = {}) {
|
|
32
|
+
let command = Command.make("jj", ...args);
|
|
33
|
+
if (opts.cwd) {
|
|
34
|
+
command = Command.workingDirectory(command, opts.cwd);
|
|
35
|
+
}
|
|
36
|
+
return Effect.scoped(Effect.gen(function* () {
|
|
37
|
+
const start = performance.now();
|
|
38
|
+
yield* Effect.logDebug(`jj ${args.join(" ")}`);
|
|
39
|
+
const process = yield* Command.start(command);
|
|
40
|
+
const stdoutFiber = yield* Effect.fork(collectUtf8(process.stdout));
|
|
41
|
+
const stderrFiber = yield* Effect.fork(collectUtf8(process.stderr));
|
|
42
|
+
const exitCode = yield* process.exitCode;
|
|
43
|
+
const stdout = yield* Fiber.join(stdoutFiber);
|
|
44
|
+
const stderr = yield* Fiber.join(stderrFiber);
|
|
45
|
+
yield* Metric.update(vcsDuration, performance.now() - start);
|
|
46
|
+
return {
|
|
47
|
+
code: Number(exitCode),
|
|
48
|
+
stdout,
|
|
49
|
+
stderr,
|
|
50
|
+
};
|
|
51
|
+
})).pipe(Effect.annotateLogs({
|
|
52
|
+
vcs: "jj",
|
|
53
|
+
cwd: opts.cwd ?? "",
|
|
54
|
+
args: args.join(" "),
|
|
55
|
+
}), Effect.withLogSpan("vcs:jj"), Effect.catchAll((error) => Effect.succeed({
|
|
56
|
+
code: 127,
|
|
57
|
+
stdout: "",
|
|
58
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
59
|
+
})));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @param {RunJjResult} res
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function jjError(res) {
|
|
66
|
+
return res.stderr.trim() || `jj exited with code ${res.code}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns the current workspace change id (jj `change_id`) or null on failure.
|
|
70
|
+
* Accepts optional `cwd` to run inside a target repository.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} [cwd]
|
|
73
|
+
* @returns {Effect.Effect<string | null, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
74
|
+
*/
|
|
75
|
+
export function getJjPointer(cwd) {
|
|
76
|
+
return runJj(["log", "-r", "@", "--no-graph", "--template", "change_id"], { cwd }).pipe(Effect.timeoutTo({
|
|
77
|
+
duration: Duration.millis(JJ_POINTER_TIMEOUT_MS),
|
|
78
|
+
onSuccess: (res) => res,
|
|
79
|
+
onTimeout: () => ({
|
|
80
|
+
code: 124,
|
|
81
|
+
stdout: "",
|
|
82
|
+
stderr: `jj pointer timed out after ${JJ_POINTER_TIMEOUT_MS}ms`,
|
|
83
|
+
}),
|
|
84
|
+
}), Effect.map((res) => {
|
|
85
|
+
if (res.code !== 0)
|
|
86
|
+
return null;
|
|
87
|
+
const out = res.stdout.trim();
|
|
88
|
+
return out ? out : null;
|
|
89
|
+
}), Effect.annotateLogs({ cwd: cwd ?? "" }), Effect.withLogSpan("vcs:jj-pointer"));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Restore the working copy to a previously recorded jujutsu `change_id`.
|
|
93
|
+
* Used by the engine to revert attempts within the correct repo/worktree (via `cwd`).
|
|
94
|
+
*
|
|
95
|
+
* @param {string} pointer
|
|
96
|
+
* @param {string} [cwd]
|
|
97
|
+
* @returns {Effect.Effect<JjRevertResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
98
|
+
*/
|
|
99
|
+
export function revertToJjPointer(pointer, cwd) {
|
|
100
|
+
return runJj(["restore", "--from", pointer], { cwd }).pipe(Effect.map((res) => res.code === 0
|
|
101
|
+
? { success: true }
|
|
102
|
+
: { success: false, error: jjError(res) }), Effect.annotateLogs({ cwd: cwd ?? "", pointer }), Effect.withLogSpan("vcs:jj-revert"));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Quick repo detection by executing a read-only jj command.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} [cwd]
|
|
108
|
+
* @returns {Effect.Effect<boolean, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
109
|
+
*/
|
|
110
|
+
export function isJjRepo(cwd) {
|
|
111
|
+
return runJj(["log", "-r", "@", "-n", "1", "--no-graph"], {
|
|
112
|
+
cwd,
|
|
113
|
+
}).pipe(Effect.map((res) => res.code === 0), Effect.annotateLogs({ cwd: cwd ?? "" }), Effect.withLogSpan("vcs:jj-is-repo"));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a new JJ workspace at `path` with a friendly `name`.
|
|
117
|
+
* NOTE: Syntax may vary between JJ versions; this helper aims to be permissive.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} name
|
|
120
|
+
* @param {string} path
|
|
121
|
+
* @param {WorkspaceAddOptions} [opts]
|
|
122
|
+
* @returns {Effect.Effect<WorkspaceResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
123
|
+
*/
|
|
124
|
+
export function workspaceAdd(name, path, opts = {}) {
|
|
125
|
+
const attempts = [];
|
|
126
|
+
const revTail = opts.atRev ? ["-r", opts.atRev] : [];
|
|
127
|
+
attempts.push(["workspace", "add", path, "--name", name, ...revTail]);
|
|
128
|
+
if (opts.atRev) {
|
|
129
|
+
attempts.push(["workspace", "add", "-r", opts.atRev, path, "--name", name]);
|
|
130
|
+
}
|
|
131
|
+
attempts.push(["workspace", "add", name, path, ...revTail]);
|
|
132
|
+
attempts.push(["workspace", "add", "--wc-path", path, name, ...revTail]);
|
|
133
|
+
return Effect.gen(function* () {
|
|
134
|
+
// Pre-check: forget stale workspace + ensure parent dir exists
|
|
135
|
+
const listRes = yield* runJj(["workspace", "list"], { cwd: opts.cwd });
|
|
136
|
+
if (listRes.code === 0 && listRes.stdout.includes(`${name}:`)) {
|
|
137
|
+
yield* runJj(["workspace", "forget", name], { cwd: opts.cwd });
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const fs = require("node:fs");
|
|
141
|
+
const nodePath = require("node:path");
|
|
142
|
+
if (fs.existsSync(path)) {
|
|
143
|
+
fs.rmSync(path, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
const parentDir = nodePath.dirname(path);
|
|
146
|
+
if (!fs.existsSync(parentDir)) {
|
|
147
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch { }
|
|
151
|
+
let lastErr = "";
|
|
152
|
+
for (const args of attempts) {
|
|
153
|
+
const res = yield* runJj(args, { cwd: opts.cwd });
|
|
154
|
+
if (res.code === 0) {
|
|
155
|
+
return { success: true };
|
|
156
|
+
}
|
|
157
|
+
lastErr = jjError(res);
|
|
158
|
+
}
|
|
159
|
+
const hint = ` (partial state may exist at ${path}; consider removing it before retrying)`;
|
|
160
|
+
return { success: false, error: lastErr + hint };
|
|
161
|
+
}).pipe(Effect.annotateLogs({
|
|
162
|
+
cwd: opts.cwd ?? "",
|
|
163
|
+
workspaceName: name,
|
|
164
|
+
workspacePath: path,
|
|
165
|
+
workspaceAtRev: opts.atRev ?? "",
|
|
166
|
+
}), Effect.withLogSpan("vcs:jj-workspace-add"));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* List existing workspaces using a JJ template for structured output.
|
|
170
|
+
* Falls back to parsing human output if `-T` is unavailable.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} [cwd]
|
|
173
|
+
* @returns {Effect.Effect<WorkspaceInfo[], never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
174
|
+
*/
|
|
175
|
+
export function workspaceList(cwd) {
|
|
176
|
+
return Effect.gen(function* () {
|
|
177
|
+
let res = yield* runJj(["workspace", "list", "-T", 'name ++ "\\n"'], {
|
|
178
|
+
cwd,
|
|
179
|
+
});
|
|
180
|
+
if (res.code === 0) {
|
|
181
|
+
const lines = res.stdout
|
|
182
|
+
.split(/\r?\n/)
|
|
183
|
+
.map((line) => line.trim())
|
|
184
|
+
.filter(Boolean);
|
|
185
|
+
return lines.map((name) => ({ name, path: /** @type {string | null} */ (null), selected: false }));
|
|
186
|
+
}
|
|
187
|
+
res = yield* runJj(["workspace", "list"], { cwd });
|
|
188
|
+
if (res.code !== 0)
|
|
189
|
+
return [];
|
|
190
|
+
/** @type {WorkspaceInfo[]} */
|
|
191
|
+
const rows = [];
|
|
192
|
+
for (const raw of res.stdout.split(/\r?\n/)) {
|
|
193
|
+
const line = raw.trim();
|
|
194
|
+
if (!line)
|
|
195
|
+
continue;
|
|
196
|
+
const selected = line.startsWith("*");
|
|
197
|
+
const rawName = selected ? line.replace(/^\*\s*/, "").trim() : line;
|
|
198
|
+
const name = rawName.split(/\s+/)[0] ?? "";
|
|
199
|
+
if (!name)
|
|
200
|
+
continue;
|
|
201
|
+
rows.push({ name, path: null, selected });
|
|
202
|
+
}
|
|
203
|
+
return rows;
|
|
204
|
+
}).pipe(Effect.annotateLogs({ cwd: cwd ?? "" }), Effect.withLogSpan("vcs:jj-workspace-list"));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Close the given workspace by name.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} name
|
|
210
|
+
* @param {{ cwd?: string }} [opts]
|
|
211
|
+
* @returns {Effect.Effect<WorkspaceResult, never, import("@effect/platform/CommandExecutor").CommandExecutor>}
|
|
212
|
+
*/
|
|
213
|
+
export function workspaceClose(name, opts = {}) {
|
|
214
|
+
return runJj(["workspace", "forget", name], { cwd: opts.cwd }).pipe(Effect.map((res) => res.code === 0
|
|
215
|
+
? { success: true }
|
|
216
|
+
: { success: false, error: jjError(res) }), Effect.annotateLogs({ cwd: opts.cwd ?? "", workspaceName: name }), Effect.withLogSpan("vcs:jj-workspace-close"));
|
|
217
|
+
}
|