@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.1
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/AGENTS.md +30 -0
- package/README.md +16 -2
- package/dist/agents.js +11 -0
- package/dist/cli-adapter.js +8 -0
- package/dist/component.js +65 -0
- package/dist/config-schema.js +265 -0
- package/dist/execution/activity.js +78 -0
- package/dist/execution/content.js +184 -0
- package/dist/execution/json-sections.js +231 -0
- package/dist/execution/presentation.js +79 -0
- package/dist/execution/preview.js +13 -0
- package/dist/execution/process.js +125 -0
- package/dist/execution/projection.js +327 -0
- package/dist/execution/query.js +18 -0
- package/dist/execution/routes.js +189 -0
- package/dist/execution/schema.js +322 -0
- package/dist/execution/service.js +1993 -0
- package/dist/features.js +22 -0
- package/dist/http.js +65 -0
- package/dist/performance/analysis.js +330 -0
- package/dist/performance/catalog.js +391 -0
- package/dist/performance/dashboard.js +483 -0
- package/dist/performance/error.js +15 -0
- package/dist/performance/index.js +6 -0
- package/dist/performance/inflight.js +43 -0
- package/dist/performance/projection.js +138 -0
- package/dist/performance/prompt/base.txt +38 -0
- package/dist/performance/prompt/builder.js +21 -0
- package/dist/performance/routes/performance-route.js +439 -0
- package/dist/performance/schema.js +628 -0
- package/dist/performance/timeline.js +98 -0
- package/dist/performance/trace-detail.js +91 -0
- package/dist/presentation/service.js +270 -0
- package/dist/presentation/tools/workbench-view.js +65 -0
- package/dist/presentation/tools.js +10 -0
- package/dist/project/directories.js +363 -0
- package/dist/project/git-health.js +498 -0
- package/dist/project/ide.js +66 -0
- package/dist/project/migration.js +22 -0
- package/dist/project/routes/directories.js +89 -0
- package/dist/project/routes/git.js +30 -0
- package/dist/project/routes/task-defaults.js +42 -0
- package/dist/project/session-health.js +9 -0
- package/dist/project/startup.js +28 -0
- package/dist/project/task-defaults.js +68 -0
- package/dist/project/tools/session-control.js +664 -0
- package/dist/project/tools/session-control.txt +60 -0
- package/dist/project/tools/worktree-archive.js +76 -0
- package/dist/project/tools/worktree-enter.js +175 -0
- package/dist/project/tools/worktree-leave.js +119 -0
- package/dist/project/tools/worktree-list.js +52 -0
- package/dist/project/tools.js +26 -0
- package/dist/project/vcs-branch-watcher.js +76 -0
- package/dist/project/vcs.js +114 -0
- package/dist/project/worktree-janitor.js +140 -0
- package/dist/project/worktrees.js +98 -0
- package/dist/push/bridge.js +171 -0
- package/dist/push/index.js +4 -0
- package/dist/push/routes/push.js +224 -0
- package/dist/push/service.js +93 -0
- package/dist/push/store.js +121 -0
- package/dist/push/types.js +58 -0
- package/dist/review/git.js +226 -0
- package/dist/review/routes.js +45 -0
- package/dist/review/schema.js +32 -0
- package/dist/review/state.js +79 -0
- package/dist/stats/aggregator.js +252 -0
- package/dist/stats/canonical.js +89 -0
- package/dist/stats/cli/stats.js +302 -0
- package/dist/stats/engine.js +176 -0
- package/dist/stats/index.js +5 -0
- package/dist/stats/rollup.js +460 -0
- package/dist/stats/routes/stats.js +94 -0
- package/dist/stats/routes/usage.js +83 -0
- package/dist/stats/storage.js +86 -0
- package/dist/stats/types.js +267 -0
- package/dist/worker.js +6 -0
- package/package.json +232 -4
- package/src/agents.ts +15 -0
- package/src/cli-adapter.ts +11 -0
- package/src/component.ts +73 -0
- package/src/config-schema.ts +289 -0
- package/src/execution/activity.ts +83 -0
- package/src/execution/content.ts +189 -0
- package/src/execution/json-sections.ts +230 -0
- package/src/execution/presentation.ts +91 -0
- package/src/execution/preview.ts +12 -0
- package/src/execution/process.ts +118 -0
- package/src/execution/projection.ts +330 -0
- package/src/execution/query.ts +19 -0
- package/src/execution/routes.ts +324 -0
- package/src/execution/schema.ts +336 -0
- package/src/execution/service.ts +2149 -0
- package/src/features.ts +22 -0
- package/src/http.ts +75 -0
- package/src/performance/analysis.ts +356 -0
- package/src/performance/catalog.ts +902 -0
- package/src/performance/dashboard.ts +521 -0
- package/src/performance/error.ts +32 -0
- package/src/performance/index.ts +6 -0
- package/src/performance/inflight.ts +46 -0
- package/src/performance/projection.ts +143 -0
- package/src/performance/prompt/base.txt +38 -0
- package/src/performance/prompt/builder.ts +24 -0
- package/src/performance/routes/performance-route.ts +541 -0
- package/src/performance/schema.ts +695 -0
- package/src/performance/timeline.ts +100 -0
- package/src/performance/trace-detail.ts +97 -0
- package/src/presentation/service.ts +303 -0
- package/src/presentation/tools/workbench-view.ts +75 -0
- package/src/presentation/tools.ts +11 -0
- package/src/project/directories.ts +391 -0
- package/src/project/git-health.ts +577 -0
- package/src/project/ide.ts +80 -0
- package/src/project/migration.ts +24 -0
- package/src/project/routes/directories.ts +124 -0
- package/src/project/routes/git.ts +42 -0
- package/src/project/routes/task-defaults.ts +50 -0
- package/src/project/session-health.ts +10 -0
- package/src/project/startup.ts +28 -0
- package/src/project/task-defaults.ts +70 -0
- package/src/project/tools/session-control.ts +725 -0
- package/src/project/tools/session-control.txt +60 -0
- package/src/project/tools/worktree-archive.ts +92 -0
- package/src/project/tools/worktree-enter.ts +227 -0
- package/src/project/tools/worktree-leave.ts +140 -0
- package/src/project/tools/worktree-list.ts +68 -0
- package/src/project/tools.ts +29 -0
- package/src/project/vcs-branch-watcher.ts +81 -0
- package/src/project/vcs.ts +124 -0
- package/src/project/worktree-janitor.ts +153 -0
- package/src/project/worktrees.ts +110 -0
- package/src/push/bridge.ts +182 -0
- package/src/push/index.ts +4 -0
- package/src/push/routes/push.ts +267 -0
- package/src/push/service.ts +108 -0
- package/src/push/store.ts +120 -0
- package/src/push/types.ts +67 -0
- package/src/review/git.ts +221 -0
- package/src/review/routes.ts +65 -0
- package/src/review/schema.ts +34 -0
- package/src/review/state.ts +77 -0
- package/src/stats/aggregator.ts +277 -0
- package/src/stats/canonical.ts +93 -0
- package/src/stats/cli/stats.ts +348 -0
- package/src/stats/engine.ts +197 -0
- package/src/stats/index.ts +5 -0
- package/src/stats/rollup.ts +522 -0
- package/src/stats/routes/stats.ts +107 -0
- package/src/stats/routes/usage.ts +103 -0
- package/src/stats/storage.ts +78 -0
- package/src/stats/types.ts +351 -0
- package/src/worker.ts +7 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { $ } from "bun";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { NamedError } from "@ericsanchezok/synergy-util/error";
|
|
7
|
+
import { Config } from "@ericsanchezok/synergy-harness/config/config";
|
|
8
|
+
import { Scope } from "@ericsanchezok/synergy-harness/scope";
|
|
9
|
+
import { Storage } from "@ericsanchezok/synergy-harness/storage/storage";
|
|
10
|
+
import { StoragePath } from "@ericsanchezok/synergy-harness/storage/path";
|
|
11
|
+
import { WorkspaceCatalog, WorkspaceBinding, WorkspaceLocation } from "@ericsanchezok/synergy-harness/workspace";
|
|
12
|
+
import { WorkspaceAccess } from "@ericsanchezok/synergy-harness/workspace/access";
|
|
13
|
+
export var ProjectDirectories;
|
|
14
|
+
(function (ProjectDirectories) {
|
|
15
|
+
ProjectDirectories.Record = z
|
|
16
|
+
.object({
|
|
17
|
+
version: z.literal(1),
|
|
18
|
+
scopeID: z.string(),
|
|
19
|
+
revision: z.number().int().nonnegative(),
|
|
20
|
+
mainWorkspaceID: z.string().nullable(),
|
|
21
|
+
additionalWorkspaceIDs: z.array(z.string()),
|
|
22
|
+
})
|
|
23
|
+
.meta({ ref: "ProjectDirectoryConfiguration" });
|
|
24
|
+
ProjectDirectories.Folder = z
|
|
25
|
+
.object({
|
|
26
|
+
workspaceID: z.string(),
|
|
27
|
+
generation: z.number().int(),
|
|
28
|
+
path: z.string(),
|
|
29
|
+
available: z.boolean(),
|
|
30
|
+
git: z.boolean(),
|
|
31
|
+
unavailable: WorkspaceCatalog.Unavailable.Schema.optional(),
|
|
32
|
+
})
|
|
33
|
+
.meta({ ref: "ProjectFolder" });
|
|
34
|
+
ProjectDirectories.Result = ProjectDirectories.Record.extend({ folders: z.array(ProjectDirectories.Folder) }).meta({ ref: "ProjectDirectories" });
|
|
35
|
+
const Selection = z.object({
|
|
36
|
+
directories: z.array(z.string().min(1)).min(1).max(32),
|
|
37
|
+
mainDirectory: z.string().min(1),
|
|
38
|
+
});
|
|
39
|
+
ProjectDirectories.CreateInput = Selection.extend({ name: z.string().trim().min(1).max(120) }).meta({
|
|
40
|
+
ref: "ProjectCreateInput",
|
|
41
|
+
});
|
|
42
|
+
ProjectDirectories.UpdateInput = Selection.extend({ revision: z.number().int().nonnegative() }).meta({
|
|
43
|
+
ref: "ProjectDirectoriesUpdate",
|
|
44
|
+
});
|
|
45
|
+
ProjectDirectories.Created = z
|
|
46
|
+
.object({ scope: Scope.Info, directories: ProjectDirectories.Result, existing: z.boolean() })
|
|
47
|
+
.meta({ ref: "ProjectCreated" });
|
|
48
|
+
ProjectDirectories.Conflict = NamedError.create("ProjectDirectoriesConflict", z.object({ message: z.string() }));
|
|
49
|
+
ProjectDirectories.Invalid = NamedError.create("ProjectDirectoriesInvalid", z.object({ message: z.string() }));
|
|
50
|
+
async function isGit(directory) {
|
|
51
|
+
try {
|
|
52
|
+
const result = await $ `git rev-parse --show-toplevel`.cwd(directory).quiet().nothrow();
|
|
53
|
+
return result.exitCode === 0 && path.resolve(result.text().trim()) === directory;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
ProjectDirectories.isGit = isGit;
|
|
60
|
+
async function validate(input) {
|
|
61
|
+
const source = WorkspaceLocation.source();
|
|
62
|
+
const locations = await Promise.all(input.directories.map(async (directory) => {
|
|
63
|
+
if (!path.isAbsolute(directory))
|
|
64
|
+
throw new ProjectDirectories.Invalid({ message: "Choose an absolute folder path." });
|
|
65
|
+
try {
|
|
66
|
+
const location = await source.identify(directory);
|
|
67
|
+
if (!(await fs.stat(location.path)).isDirectory())
|
|
68
|
+
throw new ProjectDirectories.Invalid({ message: "Choose a folder." });
|
|
69
|
+
return location;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof ProjectDirectories.Invalid)
|
|
73
|
+
throw error;
|
|
74
|
+
throw new ProjectDirectories.Invalid({ message: `Cannot access folder: ${directory}. Check that it exists and is readable.` });
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
const unique = [...new Map(locations.map((item) => [item.path, item])).values()];
|
|
78
|
+
if (!path.isAbsolute(input.mainDirectory))
|
|
79
|
+
throw new ProjectDirectories.Invalid({ message: "Choose an absolute main folder path." });
|
|
80
|
+
const main = await source.identify(input.mainDirectory).catch(() => {
|
|
81
|
+
throw new ProjectDirectories.Invalid({ message: "The main folder is unavailable. Choose a project folder again." });
|
|
82
|
+
});
|
|
83
|
+
if (!unique.some((item) => item.path === main.path))
|
|
84
|
+
throw new ProjectDirectories.Invalid({ message: "The main folder must be in the project." });
|
|
85
|
+
return { locations: unique, main: main.path, hostID: await source.hostID() };
|
|
86
|
+
}
|
|
87
|
+
async function create(input) {
|
|
88
|
+
const parsed = ProjectDirectories.CreateInput.parse(input);
|
|
89
|
+
const selection = await validate(parsed);
|
|
90
|
+
const owner = await findMainOwner(selection.main);
|
|
91
|
+
if (owner)
|
|
92
|
+
return { scope: owner, directories: await get(owner.id), existing: true };
|
|
93
|
+
const discovered = (await Scope.fromDirectory(selection.main, { persist: false })).scope;
|
|
94
|
+
if (discovered.type !== "project")
|
|
95
|
+
throw new ProjectDirectories.Invalid({ message: "A project folder is required." });
|
|
96
|
+
const existing = await Scope.fromID(discovered.id);
|
|
97
|
+
if (existing?.type === "project")
|
|
98
|
+
return { scope: existing, directories: await get(existing.id), existing: true };
|
|
99
|
+
const created = await WorkspaceAccess.exclusive(selection.locations.map((item) => item.path), async () => {
|
|
100
|
+
await revalidate(selection);
|
|
101
|
+
const mainOwner = await findMainOwner(selection.main);
|
|
102
|
+
if (mainOwner)
|
|
103
|
+
throw new ProjectDirectories.Conflict({ message: "This folder was just assigned to another project. Open that project." });
|
|
104
|
+
const concurrent = await Scope.fromID(discovered.id);
|
|
105
|
+
if (concurrent?.type === "project")
|
|
106
|
+
throw new ProjectDirectories.Conflict({ message: "This project was just created. Open the existing project." });
|
|
107
|
+
await Config.domainUpdate("general", { defaultSessionWorkspace: "main" }, { root: path.join(discovered.local.directory, ".synergy") });
|
|
108
|
+
return Storage.transaction(async () => {
|
|
109
|
+
const mainOwner = await findMainOwner(selection.main);
|
|
110
|
+
if (mainOwner)
|
|
111
|
+
throw new ProjectDirectories.Conflict({ message: "This folder was just assigned to another project. Open that project." });
|
|
112
|
+
const concurrent = await Scope.fromID(discovered.id);
|
|
113
|
+
if (concurrent?.type === "project")
|
|
114
|
+
throw new ProjectDirectories.Conflict({ message: "This project was just created. Open the existing project." });
|
|
115
|
+
const scope = await Scope.registerProject({ ...discovered, name: parsed.name });
|
|
116
|
+
const record = await save(scope.id, selection, 0);
|
|
117
|
+
return { scope, record };
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return { scope: created.scope, directories: await describe(created.record), existing: false };
|
|
121
|
+
}
|
|
122
|
+
ProjectDirectories.create = create;
|
|
123
|
+
async function findMainOwner(directory) {
|
|
124
|
+
const scopes = await Scope.list();
|
|
125
|
+
const configs = await Storage.readMany(scopes.map((scope) => StoragePath.projectDirectories(scope.id)));
|
|
126
|
+
for (const [index, config] of configs.entries()) {
|
|
127
|
+
if (!config?.mainWorkspaceID)
|
|
128
|
+
continue;
|
|
129
|
+
const [workspace] = await WorkspaceCatalog.readMany([config.mainWorkspaceID]);
|
|
130
|
+
if (workspace?.binding.path === directory)
|
|
131
|
+
return scopes[index];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function get(scopeID) {
|
|
135
|
+
const [stored] = await Storage.readMany([StoragePath.projectDirectories(scopeID)]);
|
|
136
|
+
const record = stored ? ProjectDirectories.Record.parse(stored) : await migrateProject(await Scope.resolve({ scopeID }));
|
|
137
|
+
return describe(record);
|
|
138
|
+
}
|
|
139
|
+
ProjectDirectories.get = get;
|
|
140
|
+
async function update(scopeID, input) {
|
|
141
|
+
const parsed = ProjectDirectories.UpdateInput.parse(input);
|
|
142
|
+
const current = await get(scopeID);
|
|
143
|
+
if (current.revision !== parsed.revision)
|
|
144
|
+
throw new ProjectDirectories.Conflict({ message: "Project folders changed. Reload before saving." });
|
|
145
|
+
const selection = await validate(parsed);
|
|
146
|
+
const all = await WorkspaceCatalog.list(scopeID);
|
|
147
|
+
const currentIDs = new Set(current.folders.map((item) => item.workspaceID));
|
|
148
|
+
const roots = [
|
|
149
|
+
...new Set([
|
|
150
|
+
...selection.locations.map((item) => item.path),
|
|
151
|
+
...all
|
|
152
|
+
.filter((item) => isActiveOnHost(item, selection.hostID) && (item.type === "git_worktree" || currentIDs.has(item.id)))
|
|
153
|
+
.flatMap((item) => (item.binding.path ? [item.binding.path] : [])),
|
|
154
|
+
]),
|
|
155
|
+
];
|
|
156
|
+
const saved = await WorkspaceAccess.exclusive(roots, async () => {
|
|
157
|
+
await revalidate(selection);
|
|
158
|
+
return Storage.transaction(async () => {
|
|
159
|
+
const [latest] = await Storage.readMany([StoragePath.projectDirectories(scopeID)]);
|
|
160
|
+
if (latest?.revision !== parsed.revision)
|
|
161
|
+
throw new ProjectDirectories.Conflict({ message: "Project folders changed. Reload before saving." });
|
|
162
|
+
const record = await save(scopeID, selection, parsed.revision, current);
|
|
163
|
+
return record;
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
return describe(saved);
|
|
167
|
+
}
|
|
168
|
+
ProjectDirectories.update = update;
|
|
169
|
+
async function revalidate(selection) {
|
|
170
|
+
for (const location of selection.locations) {
|
|
171
|
+
const current = await WorkspaceLocation.source().identify(location.path);
|
|
172
|
+
if (current.path !== location.path || current.physicalID !== location.physicalID)
|
|
173
|
+
throw new ProjectDirectories.Conflict({ message: "A selected folder changed. Choose it again before saving." });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function isActiveOnHost(item, hostID) {
|
|
177
|
+
return item.lifecycle === "active" && item.binding.state === "bound" && item.binding.hostID === hostID;
|
|
178
|
+
}
|
|
179
|
+
async function save(scopeID, selection, revision, previous) {
|
|
180
|
+
const folders = await Promise.all(selection.locations.map((location) => WorkspaceCatalog.register({
|
|
181
|
+
scopeID,
|
|
182
|
+
type: "directory",
|
|
183
|
+
hostID: selection.hostID,
|
|
184
|
+
...location,
|
|
185
|
+
})));
|
|
186
|
+
const main = folders.find((item) => item.binding.path === selection.main);
|
|
187
|
+
const record = {
|
|
188
|
+
version: 1,
|
|
189
|
+
scopeID,
|
|
190
|
+
revision: revision + 1,
|
|
191
|
+
mainWorkspaceID: main.id,
|
|
192
|
+
additionalWorkspaceIDs: folders.filter((item) => item.id !== main.id).map((item) => item.id),
|
|
193
|
+
};
|
|
194
|
+
const oldIDs = new Set(previous?.folders.map((item) => item.workspaceID) ?? []);
|
|
195
|
+
const all = await WorkspaceCatalog.list(scopeID);
|
|
196
|
+
const selected = new Set(folders.map((item) => item.id));
|
|
197
|
+
for (const item of all) {
|
|
198
|
+
if (!isActiveOnHost(item, selection.hostID))
|
|
199
|
+
continue;
|
|
200
|
+
const origin = typeof item.metadata.originalCheckout === "string" ? item.metadata.originalCheckout : undefined;
|
|
201
|
+
if (!selected.has(item.id) && !oldIDs.has(item.id) && !(item.type === "git_worktree" && origin))
|
|
202
|
+
continue;
|
|
203
|
+
const grants = item.type === "git_worktree"
|
|
204
|
+
? folders
|
|
205
|
+
.filter((folder) => folder.binding.path !== origin && folder.id !== item.id)
|
|
206
|
+
.map((folder) => folder.id)
|
|
207
|
+
: selected.has(item.id)
|
|
208
|
+
? folders.filter((folder) => folder.id !== item.id).map((folder) => folder.id)
|
|
209
|
+
: [];
|
|
210
|
+
if (JSON.stringify(item.sharedWritableWorkspaceIDs) === JSON.stringify(grants))
|
|
211
|
+
continue;
|
|
212
|
+
const changed = await WorkspaceCatalog.setSharing(item.id, {
|
|
213
|
+
scopeID,
|
|
214
|
+
expectedRevision: item.revision,
|
|
215
|
+
workspaceIDs: grants,
|
|
216
|
+
});
|
|
217
|
+
Storage.afterCommit(() => WorkspaceCatalog.publishUpdated(changed));
|
|
218
|
+
}
|
|
219
|
+
for (const folder of await WorkspaceCatalog.readMany(folders.map((item) => item.id)))
|
|
220
|
+
if (folder)
|
|
221
|
+
Storage.afterCommit(() => WorkspaceCatalog.publishUpdated(folder));
|
|
222
|
+
await Storage.write(StoragePath.projectDirectories(scopeID), record);
|
|
223
|
+
return record;
|
|
224
|
+
}
|
|
225
|
+
async function describe(record) {
|
|
226
|
+
const ids = [...(record.mainWorkspaceID ? [record.mainWorkspaceID] : []), ...record.additionalWorkspaceIDs];
|
|
227
|
+
const records = await WorkspaceCatalog.readMany(ids);
|
|
228
|
+
const folders = await Promise.all(records.map(async (item, index) => {
|
|
229
|
+
if (!item)
|
|
230
|
+
return {
|
|
231
|
+
workspaceID: ids[index],
|
|
232
|
+
generation: 1,
|
|
233
|
+
path: "",
|
|
234
|
+
available: false,
|
|
235
|
+
git: false,
|
|
236
|
+
unavailable: new WorkspaceCatalog.Unavailable({
|
|
237
|
+
workspaceID: ids[index],
|
|
238
|
+
message: "The project folder binding is unavailable",
|
|
239
|
+
reason: "binding_unavailable",
|
|
240
|
+
}).toObject(),
|
|
241
|
+
};
|
|
242
|
+
const directory = item.binding.path ?? "";
|
|
243
|
+
let unavailable;
|
|
244
|
+
const available = await WorkspaceBinding.validate(item.id, record.scopeID).then(() => true, (error) => {
|
|
245
|
+
const parsed = WorkspaceCatalog.Unavailable.Schema.safeParse(error);
|
|
246
|
+
if (!parsed.success)
|
|
247
|
+
throw error;
|
|
248
|
+
unavailable = parsed.data;
|
|
249
|
+
return false;
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
workspaceID: item.id,
|
|
253
|
+
generation: item.binding.generation,
|
|
254
|
+
path: directory,
|
|
255
|
+
available,
|
|
256
|
+
git: available && (await isGit(directory)),
|
|
257
|
+
...(unavailable ? { unavailable } : {}),
|
|
258
|
+
};
|
|
259
|
+
}));
|
|
260
|
+
return { ...record, folders };
|
|
261
|
+
}
|
|
262
|
+
async function migrateProject(scope) {
|
|
263
|
+
const [existing] = await Storage.readMany([StoragePath.projectDirectories(scope.id)]);
|
|
264
|
+
if (existing)
|
|
265
|
+
return ProjectDirectories.Record.parse(existing);
|
|
266
|
+
const hostID = await WorkspaceLocation.source().hostID();
|
|
267
|
+
const paths = scope.local ? [...new Set([scope.local.worktree, ...scope.local.sandboxes])] : [];
|
|
268
|
+
const worktrees = new Set();
|
|
269
|
+
if (scope.local) {
|
|
270
|
+
try {
|
|
271
|
+
const git = await $ `git worktree list --porcelain -z`.cwd(scope.local.worktree).quiet().nothrow();
|
|
272
|
+
if (git.exitCode === 0)
|
|
273
|
+
for (const line of git.text().split("\0"))
|
|
274
|
+
if (line.startsWith("worktree "))
|
|
275
|
+
worktrees.add(path.resolve(line.slice(9)));
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
}
|
|
279
|
+
if (scope.local)
|
|
280
|
+
worktrees.delete(path.resolve(scope.local.worktree));
|
|
281
|
+
const catalog = await WorkspaceCatalog.list(scope.id);
|
|
282
|
+
const retainedTrees = new Set(catalog.flatMap((record) => (record.type === "git_worktree" && record.binding.path ? [record.binding.path] : [])));
|
|
283
|
+
for (const directory of retainedTrees)
|
|
284
|
+
worktrees.add(directory);
|
|
285
|
+
const locate = (directory) => {
|
|
286
|
+
const requested = path.resolve(directory);
|
|
287
|
+
const retained = catalog.find((record) => record.binding.path === requested && record.binding.hostID === hostID && record.binding.state === "bound") ?? catalog.find((record) => record.type === "git_worktree" && record.binding.path === requested);
|
|
288
|
+
return retained
|
|
289
|
+
? { path: requested, hostID: retained.binding.hostID }
|
|
290
|
+
: WorkspaceBinding.locateHistory(scope.id, directory);
|
|
291
|
+
};
|
|
292
|
+
const locations = await Promise.all(paths.map(locate));
|
|
293
|
+
if (locations[0])
|
|
294
|
+
worktrees.delete(locations[0].path);
|
|
295
|
+
const candidates = [
|
|
296
|
+
...new Map(locations.filter((location) => !worktrees.has(location.path)).map((location) => [location.path, location])).values(),
|
|
297
|
+
];
|
|
298
|
+
const historical = await Promise.all([...worktrees].filter((directory) => !retainedTrees.has(directory)).map(locate));
|
|
299
|
+
return Storage.transaction(async () => {
|
|
300
|
+
const [concurrent] = await Storage.readMany([StoragePath.projectDirectories(scope.id)]);
|
|
301
|
+
if (concurrent)
|
|
302
|
+
return ProjectDirectories.Record.parse(concurrent);
|
|
303
|
+
const retain = async (input) => {
|
|
304
|
+
const existing = await WorkspaceCatalog.findByLocation(input);
|
|
305
|
+
if (existing)
|
|
306
|
+
return existing;
|
|
307
|
+
const retained = catalog.find((record) => record.type === "git_worktree" && record.binding.path === input.path);
|
|
308
|
+
return retained ? WorkspaceCatalog.get(retained.id, scope.id) : WorkspaceCatalog.register(input);
|
|
309
|
+
};
|
|
310
|
+
const records = await Promise.all(candidates.map((location) => retain({ scopeID: scope.id, type: "directory", ...location })));
|
|
311
|
+
for (const location of historical) {
|
|
312
|
+
await retain({
|
|
313
|
+
scopeID: scope.id,
|
|
314
|
+
type: "git_worktree",
|
|
315
|
+
...location,
|
|
316
|
+
metadata: {
|
|
317
|
+
originalCheckout: scope.local?.worktree,
|
|
318
|
+
sourceWorkspaceID: records[0]?.id,
|
|
319
|
+
worktreeID: "wt_" + createHash("sha256").update(location.path).digest("hex").slice(0, 16),
|
|
320
|
+
name: path.basename(location.path),
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const byID = new Map((await WorkspaceCatalog.list(scope.id)).map((record) => [record.id, record]));
|
|
325
|
+
const migratedTrees = [...byID.values()].filter((item) => item.type === "git_worktree");
|
|
326
|
+
const folders = new Map([...records, ...migratedTrees].map((record) => [record.id, record]));
|
|
327
|
+
for (const folder of folders.values()) {
|
|
328
|
+
if (folder.lifecycle !== "active" || folder.binding.state !== "bound")
|
|
329
|
+
continue;
|
|
330
|
+
const canShare = (other) => other.lifecycle === "active" &&
|
|
331
|
+
other.binding.state === "bound" &&
|
|
332
|
+
other.binding.hostID === folder.binding.hostID;
|
|
333
|
+
const grants = records
|
|
334
|
+
.filter((other) => other.id !== folder.id && other.binding.path !== folder.metadata.originalCheckout && canShare(other))
|
|
335
|
+
.map((other) => other.id);
|
|
336
|
+
const retainedGrants = folder.sharedWritableWorkspaceIDs.filter((id) => {
|
|
337
|
+
const target = byID.get(id);
|
|
338
|
+
return !target || canShare(target);
|
|
339
|
+
});
|
|
340
|
+
const workspaceIDs = [...new Set([...retainedGrants, ...grants])];
|
|
341
|
+
if (workspaceIDs.length === folder.sharedWritableWorkspaceIDs.length &&
|
|
342
|
+
workspaceIDs.every((id, index) => id === folder.sharedWritableWorkspaceIDs[index]))
|
|
343
|
+
continue;
|
|
344
|
+
const updated = await WorkspaceCatalog.setSharing(folder.id, {
|
|
345
|
+
scopeID: scope.id,
|
|
346
|
+
expectedRevision: folder.revision,
|
|
347
|
+
workspaceIDs,
|
|
348
|
+
});
|
|
349
|
+
Storage.afterCommit(() => WorkspaceCatalog.publishUpdated(updated));
|
|
350
|
+
}
|
|
351
|
+
const record = {
|
|
352
|
+
version: 1,
|
|
353
|
+
scopeID: scope.id,
|
|
354
|
+
revision: 1,
|
|
355
|
+
mainWorkspaceID: records[0]?.id ?? null,
|
|
356
|
+
additionalWorkspaceIDs: records.slice(1).map((item) => item.id),
|
|
357
|
+
};
|
|
358
|
+
await Storage.write(StoragePath.projectDirectories(scope.id), record);
|
|
359
|
+
return record;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
ProjectDirectories.migrateProject = migrateProject;
|
|
363
|
+
})(ProjectDirectories || (ProjectDirectories = {}));
|