symbio 0.7.1 → 0.9.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.
@@ -1,10 +1,13 @@
1
1
  // Worker workspaces. In a git project a worker gets its own worktree on its own
2
2
  // branch (isolated copy of the committed files), unless it is set to share the
3
3
  // project folder. Without git, workers always share it. Workers in the project
4
- // folder rely on shared-asset grants alone.
4
+ // folder rely on shared-asset grants alone. In a project made of several repositories
5
+ // (repos.ts), a helper's worktree comes from the repository it was given (agent.repo), and its
6
+ // merges happen there.
5
7
  import { existsSync, promises as fs } from "node:fs";
6
8
  import path from "node:path";
7
9
  import { git } from "./git.js";
10
+ import { repoRoot } from "./repos.js";
8
11
  import { globalDir } from "./store.js";
9
12
  export async function isGitRepo(dir) {
10
13
  return existsSync(path.join(dir, ".git"));
@@ -14,8 +17,9 @@ export function inProjectFolder(project, agent) {
14
17
  const norm = (p) => (process.platform === "win32" ? path.resolve(p).toLowerCase() : path.resolve(p));
15
18
  return norm(agent.directory) === norm(project.directory);
16
19
  }
17
- export async function createWorkspace(project, agentName, mode = "own") {
18
- if (mode === "shared" || !(await isGitRepo(project.directory))) {
20
+ export async function createWorkspace(project, agentName, mode = "own", repo) {
21
+ const root = repoRoot(project, { repo });
22
+ if (mode === "shared" || !(await isGitRepo(root))) {
19
23
  return { directory: project.directory, branch: null, base: null };
20
24
  }
21
25
  const slug = agentName.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -24,14 +28,14 @@ export async function createWorkspace(project, agentName, mode = "own") {
24
28
  if (existsSync(directory))
25
29
  throw new Error(`A workspace for "${agentName}" already exists.`);
26
30
  await fs.mkdir(path.dirname(directory), { recursive: true });
27
- const base = await git(project.directory, "rev-parse", "HEAD").catch(() => "");
31
+ const base = await git(root, "rev-parse", "HEAD").catch(() => "");
28
32
  if (!base)
29
- throw new Error("The project's git repository has no commits yet; commit the current files first.");
30
- const branchExists = await git(project.directory, "rev-parse", "--verify", "--quiet", branch).then(() => true, () => false);
31
- await git(project.directory, "worktree", "add", ...(branchExists ? [directory, branch] : ["-b", branch, directory, base]));
32
- const dirty = await git(project.directory, "status", "--porcelain").catch(() => "");
33
+ throw new Error(`The ${repo ? `"${repo}" ` : "project's "}git repository has no commits yet; commit the current files first.`);
34
+ const branchExists = await git(root, "rev-parse", "--verify", "--quiet", branch).then(() => true, () => false);
35
+ await git(root, "worktree", "add", ...(branchExists ? [directory, branch] : ["-b", branch, directory, base]));
36
+ const dirty = await git(root, "status", "--porcelain").catch(() => "");
33
37
  const warning = dirty
34
- ? "The project folder has uncommitted changes; the new agent's workspace does not include them. Commit first if the agent needs them."
38
+ ? `The ${repo ? `"${repo}" repository` : "project folder"} has uncommitted changes; the new agent's workspace does not include them. Commit first if the agent needs them.`
35
39
  : undefined;
36
40
  return { directory, branch, base, warning };
37
41
  }
@@ -66,19 +70,21 @@ export async function mergeWorkspace(project, agent) {
66
70
  }
67
71
  if (committed.length === 0)
68
72
  return { ok: false, reason: `${agent.name} has no committed changes to merge.` };
69
- const mainDirty = await git(project.directory, "status", "--porcelain").catch(() => "");
73
+ const root = repoRoot(project, agent);
74
+ const where = agent.repo ? `The "${agent.repo}" repository` : "The project folder";
75
+ const mainDirty = await git(root, "status", "--porcelain").catch(() => "");
70
76
  if (mainDirty) {
71
- return { ok: false, reason: "The project folder has uncommitted changes. Commit or stash them before merging." };
77
+ return { ok: false, reason: `${where} has uncommitted changes. Commit or stash them before merging.` };
72
78
  }
73
79
  try {
74
- await git(project.directory, "merge", "--no-ff", "--no-edit", "-m", `Merge work from ${agent.name} (Symbio)`, agent.branch);
80
+ await git(root, "merge", "--no-ff", "--no-edit", "-m", `Merge work from ${agent.name} (Symbio)`, agent.branch);
75
81
  }
76
82
  catch (err) {
77
- const conflicts = (await git(project.directory, "diff", "--name-only", "--diff-filter=U").catch(() => ""))
83
+ const conflicts = (await git(root, "diff", "--name-only", "--diff-filter=U").catch(() => ""))
78
84
  .split("\n")
79
85
  .filter(Boolean);
80
- await git(project.directory, "merge", "--abort").catch(() => { });
81
- const main = await git(project.directory, "rev-parse", "--abbrev-ref", "HEAD").catch(() => "the main branch");
86
+ await git(root, "merge", "--abort").catch(() => { });
87
+ const main = await git(root, "rev-parse", "--abbrev-ref", "HEAD").catch(() => "the main branch");
82
88
  return {
83
89
  ok: false,
84
90
  reason: conflicts.length
@@ -95,7 +101,7 @@ export async function mergeWorkspace(project, agent) {
95
101
  export async function syncWorkspace(project, agent) {
96
102
  if (!agent.branch)
97
103
  return null;
98
- const head = await git(project.directory, "rev-parse", "HEAD");
104
+ const head = await git(repoRoot(project, agent), "rev-parse", "HEAD");
99
105
  await git(agent.directory, "merge", "--ff-only", head).catch(() => { });
100
106
  return head;
101
107
  }
@@ -108,11 +114,12 @@ export async function restoreWorkspace(project, agent) {
108
114
  if (!agent.branch || existsSync(agent.directory)) {
109
115
  return { directory: agent.directory, branch: agent.branch ?? null, base: agent.base ?? null, reset: false };
110
116
  }
111
- const base = await git(project.directory, "rev-parse", "HEAD");
112
- await git(project.directory, "worktree", "prune").catch(() => { });
113
- await git(project.directory, "branch", "-f", agent.branch, base);
117
+ const root = repoRoot(project, agent);
118
+ const base = await git(root, "rev-parse", "HEAD");
119
+ await git(root, "worktree", "prune").catch(() => { });
120
+ await git(root, "branch", "-f", agent.branch, base);
114
121
  await fs.mkdir(path.dirname(agent.directory), { recursive: true });
115
- await git(project.directory, "worktree", "add", agent.directory, agent.branch);
122
+ await git(root, "worktree", "add", agent.directory, agent.branch);
116
123
  return { directory: agent.directory, branch: agent.branch, base, reset: true };
117
124
  }
118
125
  /**
@@ -121,18 +128,19 @@ export async function restoreWorkspace(project, agent) {
121
128
  * moved. Returns false when there was nothing to do.
122
129
  */
123
130
  export async function reattachWorkspace(project, agent) {
124
- if (!agent.branch || existsSync(agent.directory) || !(await isGitRepo(project.directory)))
131
+ const root = repoRoot(project, agent);
132
+ if (!agent.branch || existsSync(agent.directory) || !(await isGitRepo(root)))
125
133
  return false;
126
- await git(project.directory, "worktree", "prune").catch(() => { });
127
- const branchExists = await git(project.directory, "rev-parse", "--verify", "--quiet", agent.branch).then(() => true, () => false);
134
+ await git(root, "worktree", "prune").catch(() => { });
135
+ const branchExists = await git(root, "rev-parse", "--verify", "--quiet", agent.branch).then(() => true, () => false);
128
136
  await fs.mkdir(path.dirname(agent.directory), { recursive: true });
129
- await git(project.directory, "worktree", "add", ...(branchExists ? [agent.directory, agent.branch] : ["-b", agent.branch, agent.directory, "HEAD"]));
137
+ await git(root, "worktree", "add", ...(branchExists ? [agent.directory, agent.branch] : ["-b", agent.branch, agent.directory, "HEAD"]));
130
138
  return true;
131
139
  }
132
140
  export async function removeWorkspace(project, agent) {
133
141
  if (!agent.branch)
134
142
  return;
135
- await git(project.directory, "worktree", "remove", "--force", agent.directory).catch(() => { });
143
+ await git(repoRoot(project, agent), "worktree", "remove", "--force", agent.directory).catch(() => { });
136
144
  }
137
145
  /**
138
146
  * Takes a helper out of its own worktree so it can work in the project folder. Refuses while
@@ -148,9 +156,10 @@ export async function leaveWorkspace(project, agent) {
148
156
  throw new Error(`${agent.name}'s own folder has work that isn't in the project yet (${files.slice(0, 8).join(", ")}${files.length > 8 ? ", …" : ""}). ` +
149
157
  "Merge it (or discard it) first.");
150
158
  }
151
- await git(project.directory, "worktree", "remove", "--force", agent.directory).catch(() => { });
152
- await git(project.directory, "worktree", "prune").catch(() => { });
153
- await git(project.directory, "branch", "-d", agent.branch).catch(() => { });
159
+ const root = repoRoot(project, agent);
160
+ await git(root, "worktree", "remove", "--force", agent.directory).catch(() => { });
161
+ await git(root, "worktree", "prune").catch(() => { });
162
+ await git(root, "branch", "-d", agent.branch).catch(() => { });
154
163
  }
155
164
  /**
156
165
  * Deletes helper folders left behind by projects that no longer exist (removing a project