char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somadd command.enabled() and only show /workspace if jj5831c1d

main
4.4 KiB113 linesraw
1import type { Context } from "@earendil-works/chord";
2import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
3import { getOrThrow } from "@earendil-works/pi-durable/env";
4import type { Environment } from "@berry/api";
5
6async function jj(env: Environment, cwd: string, args: string[], context: Context) {
7  const streams = { stdout: "", stderr: "" };
8  const result = getOrThrow(
9    await env.exec(["jj", "--no-pager", "--color=never", ...args], {
10      cwd,
11      onOutput: (text, _, { stream }) => void (streams[stream] += text),
12    }, context),
13  );
14  if (result.exitCode !== 0) throw new Error(streams.stderr.trim());
15  return streams.stdout.trim();
16}
17
18/** On either kind of machine: `/` on unix, a drive or a share on windows. */
19const isAbsolute = (path: string) => /^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(path);
20
21/** The shared repository directory, regardless of which workspace names it. */
22async function repository(env: Environment, root: string, context: Context): Promise<string> {
23  const path = getOrThrow(await env.joinPath([root, ".jj", "repo"], context));
24  if (getOrThrow(await env.fileInfo(path, context)).kind === "directory") {
25    return getOrThrow(await env.canonicalPath(path, context));
26  }
27  // a secondary workspace's names the repository, relative to its .jj
28  const named = getOrThrow(await env.readTextFile(path, context)).trim();
29  const resolved = isAbsolute(named)
30    ? named
31    : getOrThrow(await env.joinPath([root, ".jj", named], context));
32  return getOrThrow(await env.canonicalPath(resolved, context));
33}
34
35/** The last component of a path, on either kind of machine. */
36const basename = (path: string) => path.replace(/[\\/]+$/, "").split(/[\\/]/).at(-1) ?? "";
37
38/** Whether the working directory belongs to a reachable jj workspace. */
39export async function hasWorkspace(env: Environment): Promise<boolean> {
40  try {
41    await jj(env, env.cwd, ["--ignore-working-copy", "workspace", "root"], BACKGROUND_CONTEXT);
42    return true;
43  } catch {
44    return false;
45  }
46}
47
48/** Create or reuse a conversation's workspace without rewriting existing work. */
49export async function workspace(env: Environment, name: string, context: Context): Promise<string> {
50  if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new Error("Invalid conversation ID for a workspace.");
51  let source: string;
52  try {
53    source = await jj(env, env.cwd, ["--ignore-working-copy", "workspace", "root"], context);
54  } catch (error) {
55    throw new Error(`Cannot create a workspace: cwd must be in a jj repository.\n${error}`);
56  }
57  const repo = await repository(env, source, context);
58  const names = await jj(env, source, [
59    "--ignore-working-copy",
60    "workspace",
61    "list",
62    "--template",
63    'name ++ "\n"',
64  ], context);
65  if (names.split("\n").includes(name)) {
66    const target = getOrThrow(
67      await env.canonicalPath(
68        await jj(
69          env,
70          source,
71          ["--ignore-working-copy", "workspace", "root", "--name", name],
72          context,
73        ),
74        context,
75      ),
76    );
77    const root = await jj(env, target, ["--ignore-working-copy", "workspace", "root"], context);
78    if (
79      getOrThrow(await env.canonicalPath(root, context)) !== target ||
80      await repository(env, target, context) !== repo
81    ) {
82      throw new Error(`Workspace ${name} no longer belongs to this repository: ${target}`);
83    }
84    return target;
85  }
86
87  const base = env.variables.BERRY_WORKSPACES_DIR ?? "/x/workspaces";
88  if (!isAbsolute(base)) throw new Error("BERRY_WORKSPACES_DIR must be an absolute path.");
89  // the repository is <project>/.jj/repo
90  const project = getOrThrow(await env.joinPath([repo, "..", ".."], context));
91  const parent = getOrThrow(await env.joinPath([base, basename(project)], context));
92  const target = getOrThrow(await env.joinPath([parent, name], context));
93  // empty working-copy commits are mutable staging areas, not shared task bases.
94  const parents = await jj(env, source, [
95    "log",
96    "--no-graph",
97    "--revisions",
98    "heads(ancestors(@-) & ~empty() & ~root())",
99    "--template",
100    'commit_id ++ "\n"',
101  ], context);
102  if (!parents) throw new Error("Cannot create a workspace: no nonempty ancestor other than root.");
103  getOrThrow(await env.createDir(parent, { recursive: true }, context));
104  await jj(env, source, [
105    "workspace",
106    "add",
107    "--name",
108    name,
109    ...parents.split("\n").flatMap((parent) => ["--revision", parent]),
110    target,
111  ], context);
112  return getOrThrow(await env.canonicalPath(target, context));
113}