@evo-dev/core 0.0.1-alpha.13 → 0.0.1-alpha.15
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/dist/config/index.js +400 -47
- package/dist/index.js +6424 -5987
- package/package.json +1 -1
- package/src/evolution/knowledge/index.ts +93 -18
- package/src/projects/index.ts +490 -9
- package/src/runtime-logs/index.ts +96 -1
package/src/projects/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, parse } from "node:path";
|
|
3
3
|
import { resolveEvoDevPaths } from "../config/paths.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { resolvePathProjectLogKey, resolveProjectLogIdentity } from "../runtime-logs/index.ts";
|
|
5
5
|
import {
|
|
6
6
|
isFileExistsError,
|
|
7
7
|
isNotFoundError,
|
|
@@ -16,9 +16,13 @@ export type ProjectDiscoveryTarget = "claude" | "codex";
|
|
|
16
16
|
|
|
17
17
|
export interface ResolvedProjectWorkspace {
|
|
18
18
|
projectKey: string;
|
|
19
|
+
workspaceKey: string;
|
|
19
20
|
displayName: string;
|
|
21
|
+
projectRoot: string;
|
|
20
22
|
workspaceRoot: string;
|
|
21
23
|
workspaceKind: ProjectWorkspaceKind;
|
|
24
|
+
gitCommonDir: string | null;
|
|
25
|
+
linkedWorktree: boolean;
|
|
22
26
|
cwd: string;
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -51,6 +55,40 @@ export interface RegisteredProjectRecordV1 {
|
|
|
51
55
|
sourceContentStored: false;
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
export interface ProjectWorkspaceRecordV1 {
|
|
59
|
+
schemaVersion: 1;
|
|
60
|
+
kind: "project-workspace";
|
|
61
|
+
workspaceKey: string;
|
|
62
|
+
projectKey: string;
|
|
63
|
+
displayName: string;
|
|
64
|
+
workspaceRoot: string;
|
|
65
|
+
workspaceKind: ProjectWorkspaceKind;
|
|
66
|
+
gitCommonDir: string | null;
|
|
67
|
+
sourceTargets: ProjectDiscoveryTarget[];
|
|
68
|
+
firstSeenAt: string;
|
|
69
|
+
lastSeenAt: string;
|
|
70
|
+
localOnly: true;
|
|
71
|
+
sourceContentStored: false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ProjectWorkspaceSummary extends ProjectWorkspaceRecordV1 {
|
|
75
|
+
available: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ProjectAliasRecordV1 {
|
|
79
|
+
schemaVersion: 1;
|
|
80
|
+
kind: "project-alias";
|
|
81
|
+
aliasProjectKey: string;
|
|
82
|
+
canonicalProjectKey: string;
|
|
83
|
+
workspaceRoot: string;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
updatedAt: string;
|
|
86
|
+
localOnly: true;
|
|
87
|
+
sourceContentStored: false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type ProjectAliasMap = Record<string, string>;
|
|
91
|
+
|
|
54
92
|
export class ProjectRegistrationError extends Error {
|
|
55
93
|
readonly code: "invalid" | "not-found" | "conflict";
|
|
56
94
|
|
|
@@ -64,17 +102,27 @@ export class ProjectRegistrationError extends Error {
|
|
|
64
102
|
export function resolveProjectRegistryPaths(homeDir: string): {
|
|
65
103
|
discoveredDir: string;
|
|
66
104
|
registeredDir: string;
|
|
105
|
+
workspaceDir: string;
|
|
106
|
+
aliasDir: string;
|
|
67
107
|
discoveredPath: (projectKey: string) => string;
|
|
68
108
|
registeredPath: (projectKey: string) => string;
|
|
109
|
+
workspacePath: (workspaceKey: string) => string;
|
|
110
|
+
aliasPath: (aliasProjectKey: string) => string;
|
|
69
111
|
} {
|
|
70
112
|
const paths = resolveEvoDevPaths(homeDir);
|
|
71
113
|
const discoveredDir = join(paths.stateDir, "projects", "discovered");
|
|
72
114
|
const registeredDir = join(paths.rootDir, "projects");
|
|
115
|
+
const workspaceDir = join(paths.stateDir, "projects", "workspaces");
|
|
116
|
+
const aliasDir = join(paths.stateDir, "projects", "aliases");
|
|
73
117
|
return {
|
|
74
118
|
discoveredDir,
|
|
75
119
|
registeredDir,
|
|
120
|
+
workspaceDir,
|
|
121
|
+
aliasDir,
|
|
76
122
|
discoveredPath: (projectKey) => join(discoveredDir, `${projectKey}.json`),
|
|
77
123
|
registeredPath: (projectKey) => join(registeredDir, projectKey, "workspace.json"),
|
|
124
|
+
workspacePath: (workspaceKey) => join(workspaceDir, `${workspaceKey}.json`),
|
|
125
|
+
aliasPath: (aliasProjectKey) => join(aliasDir, `${aliasProjectKey}.json`),
|
|
78
126
|
};
|
|
79
127
|
}
|
|
80
128
|
|
|
@@ -96,11 +144,16 @@ export async function resolveProjectWorkspaceFromCwd(input: {
|
|
|
96
144
|
|
|
97
145
|
const gitRoot = await findNearestGitRoot(cwd);
|
|
98
146
|
const workspaceRoot = gitRoot ?? cwd;
|
|
147
|
+
const identity = resolveProjectLogIdentity(input.homeDir, workspaceRoot);
|
|
99
148
|
return {
|
|
100
|
-
projectKey: sanitizeStorageId(
|
|
101
|
-
|
|
149
|
+
projectKey: sanitizeStorageId(identity.projectKey, "project"),
|
|
150
|
+
workspaceKey: sanitizeStorageId(identity.workspaceKey, "workspace"),
|
|
151
|
+
displayName: basename(identity.projectRoot) || parse(identity.projectRoot).root,
|
|
152
|
+
projectRoot: identity.projectRoot,
|
|
102
153
|
workspaceRoot,
|
|
103
154
|
workspaceKind: gitRoot === null ? "directory" : "git",
|
|
155
|
+
gitCommonDir: identity.gitCommonDir,
|
|
156
|
+
linkedWorktree: identity.linkedWorktree,
|
|
104
157
|
cwd,
|
|
105
158
|
};
|
|
106
159
|
}
|
|
@@ -119,12 +172,27 @@ export async function recordDiscoveredProject(input: {
|
|
|
119
172
|
const paths = resolveProjectRegistryPaths(input.homeDir);
|
|
120
173
|
const path = paths.discoveredPath(workspace.projectKey);
|
|
121
174
|
const existing = await readDiscoveredProject(path);
|
|
175
|
+
await writeProjectWorkspaceRecord({
|
|
176
|
+
paths,
|
|
177
|
+
workspace,
|
|
178
|
+
target: input.target,
|
|
179
|
+
now,
|
|
180
|
+
});
|
|
181
|
+
if (workspace.workspaceKey !== workspace.projectKey) {
|
|
182
|
+
await writeProjectAliasRecord({
|
|
183
|
+
paths,
|
|
184
|
+
aliasProjectKey: workspace.workspaceKey,
|
|
185
|
+
canonicalProjectKey: workspace.projectKey,
|
|
186
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
187
|
+
now,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
122
190
|
const record: DiscoveredProjectRecordV1 = {
|
|
123
191
|
schemaVersion: 1,
|
|
124
192
|
kind: "discovered-project",
|
|
125
193
|
projectKey: workspace.projectKey,
|
|
126
194
|
displayName: workspace.displayName,
|
|
127
|
-
workspaceRoot: workspace.
|
|
195
|
+
workspaceRoot: workspace.projectRoot,
|
|
128
196
|
lastCwd: workspace.cwd,
|
|
129
197
|
workspaceKind: workspace.workspaceKind,
|
|
130
198
|
sourceTargets: mergeTargets(existing?.sourceTargets ?? [], [input.target]),
|
|
@@ -143,10 +211,16 @@ export async function listDiscoveredProjects(input: {
|
|
|
143
211
|
}): Promise<DiscoveredProjectRecordV1[]> {
|
|
144
212
|
const paths = resolveProjectRegistryPaths(input.homeDir);
|
|
145
213
|
const records = new Map<string, DiscoveredProjectRecordV1>();
|
|
214
|
+
const aliases = await listProjectAliases(input);
|
|
146
215
|
|
|
147
216
|
for (const path of await listJsonFilePaths(paths.discoveredDir)) {
|
|
148
217
|
const record = await readDiscoveredProject(path);
|
|
149
|
-
if (record !== null)
|
|
218
|
+
if (record !== null) {
|
|
219
|
+
mergeDiscoveredRecord(
|
|
220
|
+
records,
|
|
221
|
+
await canonicalizeDiscoveredRecord(input.homeDir, record, aliases),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
150
224
|
}
|
|
151
225
|
|
|
152
226
|
for (const binding of await listLegacyHookBindings(input.homeDir)) {
|
|
@@ -161,7 +235,7 @@ export async function listDiscoveredProjects(input: {
|
|
|
161
235
|
kind: "discovered-project",
|
|
162
236
|
projectKey: workspace.projectKey,
|
|
163
237
|
displayName: workspace.displayName,
|
|
164
|
-
workspaceRoot: workspace.
|
|
238
|
+
workspaceRoot: workspace.projectRoot,
|
|
165
239
|
lastCwd: workspace.cwd,
|
|
166
240
|
workspaceKind: workspace.workspaceKind,
|
|
167
241
|
sourceTargets: [binding.target],
|
|
@@ -184,18 +258,125 @@ export async function listRegisteredProjects(input: {
|
|
|
184
258
|
homeDir: string;
|
|
185
259
|
}): Promise<RegisteredProjectRecordV1[]> {
|
|
186
260
|
const { registeredDir } = resolveProjectRegistryPaths(input.homeDir);
|
|
187
|
-
const
|
|
261
|
+
const aliases = await listProjectAliases(input);
|
|
262
|
+
const records = new Map<string, RegisteredProjectRecordV1>();
|
|
188
263
|
for (const projectKey of await listDirectoryNames(registeredDir)) {
|
|
189
264
|
const record = await readRegisteredProject(join(registeredDir, projectKey, "workspace.json"));
|
|
190
|
-
if (record
|
|
265
|
+
if (record === null || record.projectKey !== projectKey) continue;
|
|
266
|
+
mergeRegisteredRecord(
|
|
267
|
+
records,
|
|
268
|
+
await canonicalizeRegisteredRecord(input.homeDir, record, aliases),
|
|
269
|
+
);
|
|
191
270
|
}
|
|
192
|
-
return records.sort(
|
|
271
|
+
return [...records.values()].sort(
|
|
193
272
|
(left, right) =>
|
|
194
273
|
right.lastSeenAt.localeCompare(left.lastSeenAt) ||
|
|
195
274
|
left.projectKey.localeCompare(right.projectKey),
|
|
196
275
|
);
|
|
197
276
|
}
|
|
198
277
|
|
|
278
|
+
export async function listProjectAliases(input: {
|
|
279
|
+
homeDir: string;
|
|
280
|
+
}): Promise<ProjectAliasMap> {
|
|
281
|
+
const paths = resolveProjectRegistryPaths(input.homeDir);
|
|
282
|
+
const aliases: ProjectAliasMap = {};
|
|
283
|
+
for (const path of await listJsonFilePaths(paths.aliasDir)) {
|
|
284
|
+
const record = await readProjectAlias(path);
|
|
285
|
+
if (record !== null) aliases[record.aliasProjectKey] = record.canonicalProjectKey;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const path of await listJsonFilePaths(paths.discoveredDir)) {
|
|
289
|
+
const record = await readDiscoveredProject(path);
|
|
290
|
+
if (record !== null) {
|
|
291
|
+
await collectAvailableProjectAlias(input.homeDir, record, aliases);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const projectKey of await listDirectoryNames(paths.registeredDir)) {
|
|
295
|
+
const record = await readRegisteredProject(
|
|
296
|
+
join(paths.registeredDir, projectKey, "workspace.json"),
|
|
297
|
+
);
|
|
298
|
+
if (record !== null) {
|
|
299
|
+
await collectAvailableProjectAlias(input.homeDir, record, aliases);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return normalizeProjectAliases(aliases);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function canonicalizeProjectKey(projectKey: string, aliases: ProjectAliasMap): string {
|
|
306
|
+
let current = projectKey;
|
|
307
|
+
const seen = new Set<string>();
|
|
308
|
+
while (!seen.has(current)) {
|
|
309
|
+
seen.add(current);
|
|
310
|
+
const next = aliases[current];
|
|
311
|
+
if (next === undefined || next === current) return current;
|
|
312
|
+
current = next;
|
|
313
|
+
}
|
|
314
|
+
return projectKey;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function listEquivalentProjectKeys(input: {
|
|
318
|
+
homeDir: string;
|
|
319
|
+
projectKey: string;
|
|
320
|
+
}): Promise<string[]> {
|
|
321
|
+
const aliases = await listProjectAliases({ homeDir: input.homeDir });
|
|
322
|
+
const canonical = canonicalizeProjectKey(input.projectKey, aliases);
|
|
323
|
+
return [
|
|
324
|
+
...new Set([
|
|
325
|
+
canonical,
|
|
326
|
+
input.projectKey,
|
|
327
|
+
...Object.keys(aliases).filter(
|
|
328
|
+
(alias) => canonicalizeProjectKey(alias, aliases) === canonical,
|
|
329
|
+
),
|
|
330
|
+
]),
|
|
331
|
+
].sort();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function listProjectWorkspaces(input: {
|
|
335
|
+
homeDir: string;
|
|
336
|
+
}): Promise<ProjectWorkspaceSummary[]> {
|
|
337
|
+
const paths = resolveProjectRegistryPaths(input.homeDir);
|
|
338
|
+
const aliases = await listProjectAliases(input);
|
|
339
|
+
const records = new Map<string, ProjectWorkspaceSummary>();
|
|
340
|
+
|
|
341
|
+
for (const path of await listJsonFilePaths(paths.workspaceDir)) {
|
|
342
|
+
const record = await readProjectWorkspace(path);
|
|
343
|
+
if (record !== null) {
|
|
344
|
+
mergeProjectWorkspaceSummary(
|
|
345
|
+
records,
|
|
346
|
+
await summarizeProjectWorkspace(input.homeDir, record, aliases),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const path of await listJsonFilePaths(paths.discoveredDir)) {
|
|
351
|
+
const record = await readDiscoveredProject(path);
|
|
352
|
+
if (record !== null) {
|
|
353
|
+
mergeProjectWorkspaceSummary(
|
|
354
|
+
records,
|
|
355
|
+
await summarizeLegacyWorkspace(input.homeDir, record, aliases),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const projectKey of await listDirectoryNames(paths.registeredDir)) {
|
|
360
|
+
const record = await readRegisteredProject(
|
|
361
|
+
join(paths.registeredDir, projectKey, "workspace.json"),
|
|
362
|
+
);
|
|
363
|
+
if (record !== null) {
|
|
364
|
+
mergeProjectWorkspaceSummary(
|
|
365
|
+
records,
|
|
366
|
+
await summarizeLegacyWorkspace(input.homeDir, record, aliases),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return [...records.values()].sort(
|
|
372
|
+
(left, right) =>
|
|
373
|
+
left.projectKey.localeCompare(right.projectKey) ||
|
|
374
|
+
Number(right.available) - Number(left.available) ||
|
|
375
|
+
right.lastSeenAt.localeCompare(left.lastSeenAt) ||
|
|
376
|
+
left.workspaceKey.localeCompare(right.workspaceKey),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
199
380
|
export async function registerDiscoveredProject(input: {
|
|
200
381
|
homeDir: string;
|
|
201
382
|
projectKey: string;
|
|
@@ -240,6 +421,229 @@ export async function registerDiscoveredProject(input: {
|
|
|
240
421
|
}
|
|
241
422
|
}
|
|
242
423
|
|
|
424
|
+
async function writeProjectWorkspaceRecord(input: {
|
|
425
|
+
paths: ReturnType<typeof resolveProjectRegistryPaths>;
|
|
426
|
+
workspace: ResolvedProjectWorkspace;
|
|
427
|
+
target: ProjectDiscoveryTarget;
|
|
428
|
+
now: string;
|
|
429
|
+
}): Promise<void> {
|
|
430
|
+
const path = input.paths.workspacePath(input.workspace.workspaceKey);
|
|
431
|
+
const existing = await readProjectWorkspace(path);
|
|
432
|
+
const record: ProjectWorkspaceRecordV1 = {
|
|
433
|
+
schemaVersion: 1,
|
|
434
|
+
kind: "project-workspace",
|
|
435
|
+
workspaceKey: input.workspace.workspaceKey,
|
|
436
|
+
projectKey: input.workspace.projectKey,
|
|
437
|
+
displayName:
|
|
438
|
+
basename(input.workspace.workspaceRoot) || parse(input.workspace.workspaceRoot).root,
|
|
439
|
+
workspaceRoot: input.workspace.workspaceRoot,
|
|
440
|
+
workspaceKind: input.workspace.workspaceKind,
|
|
441
|
+
gitCommonDir: input.workspace.gitCommonDir,
|
|
442
|
+
sourceTargets: mergeTargets(existing?.sourceTargets ?? [], [input.target]),
|
|
443
|
+
firstSeenAt: existing?.firstSeenAt ?? input.now,
|
|
444
|
+
lastSeenAt: input.now,
|
|
445
|
+
localOnly: true,
|
|
446
|
+
sourceContentStored: false,
|
|
447
|
+
};
|
|
448
|
+
await writeJsonFile(path, record);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function writeProjectAliasRecord(input: {
|
|
452
|
+
paths: ReturnType<typeof resolveProjectRegistryPaths>;
|
|
453
|
+
aliasProjectKey: string;
|
|
454
|
+
canonicalProjectKey: string;
|
|
455
|
+
workspaceRoot: string;
|
|
456
|
+
now: string;
|
|
457
|
+
}): Promise<void> {
|
|
458
|
+
const path = input.paths.aliasPath(input.aliasProjectKey);
|
|
459
|
+
const existing = await readProjectAlias(path);
|
|
460
|
+
const record: ProjectAliasRecordV1 = {
|
|
461
|
+
schemaVersion: 1,
|
|
462
|
+
kind: "project-alias",
|
|
463
|
+
aliasProjectKey: input.aliasProjectKey,
|
|
464
|
+
canonicalProjectKey: input.canonicalProjectKey,
|
|
465
|
+
workspaceRoot: input.workspaceRoot,
|
|
466
|
+
createdAt: existing?.createdAt ?? input.now,
|
|
467
|
+
updatedAt: input.now,
|
|
468
|
+
localOnly: true,
|
|
469
|
+
sourceContentStored: false,
|
|
470
|
+
};
|
|
471
|
+
await writeJsonFile(path, record);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function canonicalizeDiscoveredRecord(
|
|
475
|
+
homeDir: string,
|
|
476
|
+
record: DiscoveredProjectRecordV1,
|
|
477
|
+
aliases: ProjectAliasMap,
|
|
478
|
+
): Promise<DiscoveredProjectRecordV1> {
|
|
479
|
+
const workspace = await resolveProjectWorkspaceFromCwd({
|
|
480
|
+
homeDir,
|
|
481
|
+
cwd: record.workspaceRoot,
|
|
482
|
+
}).catch(() => null);
|
|
483
|
+
if (workspace === null) {
|
|
484
|
+
return {
|
|
485
|
+
...record,
|
|
486
|
+
projectKey: canonicalizeProjectKey(record.projectKey, aliases),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
...record,
|
|
491
|
+
projectKey: workspace.projectKey,
|
|
492
|
+
displayName: workspace.displayName,
|
|
493
|
+
workspaceRoot: workspace.projectRoot,
|
|
494
|
+
workspaceKind: workspace.workspaceKind,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function canonicalizeRegisteredRecord(
|
|
499
|
+
homeDir: string,
|
|
500
|
+
record: RegisteredProjectRecordV1,
|
|
501
|
+
aliases: ProjectAliasMap,
|
|
502
|
+
): Promise<RegisteredProjectRecordV1> {
|
|
503
|
+
const workspace = await resolveProjectWorkspaceFromCwd({
|
|
504
|
+
homeDir,
|
|
505
|
+
cwd: record.workspaceRoot,
|
|
506
|
+
}).catch(() => null);
|
|
507
|
+
if (workspace === null) {
|
|
508
|
+
return {
|
|
509
|
+
...record,
|
|
510
|
+
projectKey: canonicalizeProjectKey(record.projectKey, aliases),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
...record,
|
|
515
|
+
projectKey: workspace.projectKey,
|
|
516
|
+
displayName: workspace.displayName,
|
|
517
|
+
workspaceRoot: workspace.projectRoot,
|
|
518
|
+
workspaceKind: workspace.workspaceKind,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function collectAvailableProjectAlias(
|
|
523
|
+
homeDir: string,
|
|
524
|
+
record: Pick<DiscoveredProjectRecordV1, "projectKey" | "workspaceRoot">,
|
|
525
|
+
aliases: ProjectAliasMap,
|
|
526
|
+
): Promise<void> {
|
|
527
|
+
const workspace = await resolveProjectWorkspaceFromCwd({
|
|
528
|
+
homeDir,
|
|
529
|
+
cwd: record.workspaceRoot,
|
|
530
|
+
}).catch(() => null);
|
|
531
|
+
if (workspace === null) return;
|
|
532
|
+
if (record.projectKey !== workspace.projectKey) {
|
|
533
|
+
aliases[record.projectKey] = workspace.projectKey;
|
|
534
|
+
}
|
|
535
|
+
if (workspace.workspaceKey !== workspace.projectKey) {
|
|
536
|
+
aliases[workspace.workspaceKey] = workspace.projectKey;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function normalizeProjectAliases(aliases: ProjectAliasMap): ProjectAliasMap {
|
|
541
|
+
return Object.fromEntries(
|
|
542
|
+
Object.keys(aliases)
|
|
543
|
+
.sort()
|
|
544
|
+
.flatMap((alias) => {
|
|
545
|
+
const canonical = canonicalizeProjectKey(alias, aliases);
|
|
546
|
+
return canonical === alias ? [] : [[alias, canonical]];
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function summarizeProjectWorkspace(
|
|
552
|
+
homeDir: string,
|
|
553
|
+
record: ProjectWorkspaceRecordV1,
|
|
554
|
+
aliases: ProjectAliasMap,
|
|
555
|
+
): Promise<ProjectWorkspaceSummary> {
|
|
556
|
+
const workspace = await resolveProjectWorkspaceFromCwd({
|
|
557
|
+
homeDir,
|
|
558
|
+
cwd: record.workspaceRoot,
|
|
559
|
+
}).catch(() => null);
|
|
560
|
+
if (workspace === null) {
|
|
561
|
+
return {
|
|
562
|
+
...record,
|
|
563
|
+
projectKey: canonicalizeProjectKey(record.projectKey, aliases),
|
|
564
|
+
available: false,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
...record,
|
|
569
|
+
workspaceKey: workspace.workspaceKey,
|
|
570
|
+
projectKey: workspace.projectKey,
|
|
571
|
+
displayName: basename(workspace.workspaceRoot) || parse(workspace.workspaceRoot).root,
|
|
572
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
573
|
+
workspaceKind: workspace.workspaceKind,
|
|
574
|
+
gitCommonDir: workspace.gitCommonDir,
|
|
575
|
+
available: true,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function summarizeLegacyWorkspace(
|
|
580
|
+
homeDir: string,
|
|
581
|
+
record: DiscoveredProjectRecordV1 | RegisteredProjectRecordV1,
|
|
582
|
+
aliases: ProjectAliasMap,
|
|
583
|
+
): Promise<ProjectWorkspaceSummary> {
|
|
584
|
+
const workspace = await resolveProjectWorkspaceFromCwd({
|
|
585
|
+
homeDir,
|
|
586
|
+
cwd: record.workspaceRoot,
|
|
587
|
+
}).catch(() => null);
|
|
588
|
+
const sourceTargets = record.kind === "discovered-project" ? record.sourceTargets : [];
|
|
589
|
+
const firstSeenAt =
|
|
590
|
+
record.kind === "discovered-project" ? record.firstSeenAt : record.registeredAt;
|
|
591
|
+
if (workspace === null) {
|
|
592
|
+
return {
|
|
593
|
+
schemaVersion: 1,
|
|
594
|
+
kind: "project-workspace",
|
|
595
|
+
workspaceKey: resolvePathProjectLogKey(homeDir, record.workspaceRoot),
|
|
596
|
+
projectKey: canonicalizeProjectKey(record.projectKey, aliases),
|
|
597
|
+
displayName: record.displayName,
|
|
598
|
+
workspaceRoot: record.workspaceRoot,
|
|
599
|
+
workspaceKind: record.workspaceKind,
|
|
600
|
+
gitCommonDir: null,
|
|
601
|
+
sourceTargets,
|
|
602
|
+
firstSeenAt,
|
|
603
|
+
lastSeenAt: record.lastSeenAt,
|
|
604
|
+
localOnly: true,
|
|
605
|
+
sourceContentStored: false,
|
|
606
|
+
available: false,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
schemaVersion: 1,
|
|
611
|
+
kind: "project-workspace",
|
|
612
|
+
workspaceKey: workspace.workspaceKey,
|
|
613
|
+
projectKey: workspace.projectKey,
|
|
614
|
+
displayName: basename(workspace.workspaceRoot) || parse(workspace.workspaceRoot).root,
|
|
615
|
+
workspaceRoot: workspace.workspaceRoot,
|
|
616
|
+
workspaceKind: workspace.workspaceKind,
|
|
617
|
+
gitCommonDir: workspace.gitCommonDir,
|
|
618
|
+
sourceTargets,
|
|
619
|
+
firstSeenAt,
|
|
620
|
+
lastSeenAt: record.lastSeenAt,
|
|
621
|
+
localOnly: true,
|
|
622
|
+
sourceContentStored: false,
|
|
623
|
+
available: true,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function mergeProjectWorkspaceSummary(
|
|
628
|
+
records: Map<string, ProjectWorkspaceSummary>,
|
|
629
|
+
next: ProjectWorkspaceSummary,
|
|
630
|
+
): void {
|
|
631
|
+
const previous = records.get(next.workspaceKey);
|
|
632
|
+
if (previous === undefined) {
|
|
633
|
+
records.set(next.workspaceKey, next);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const newer = next.lastSeenAt >= previous.lastSeenAt ? next : previous;
|
|
637
|
+
records.set(next.workspaceKey, {
|
|
638
|
+
...newer,
|
|
639
|
+
projectKey: next.projectKey,
|
|
640
|
+
available: previous.available || next.available,
|
|
641
|
+
sourceTargets: mergeTargets(previous.sourceTargets, next.sourceTargets),
|
|
642
|
+
firstSeenAt: previous.firstSeenAt <= next.firstSeenAt ? previous.firstSeenAt : next.firstSeenAt,
|
|
643
|
+
lastSeenAt: previous.lastSeenAt >= next.lastSeenAt ? previous.lastSeenAt : next.lastSeenAt,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
243
647
|
async function findNearestGitRoot(cwd: string): Promise<string | null> {
|
|
244
648
|
let current = cwd;
|
|
245
649
|
for (;;) {
|
|
@@ -273,6 +677,24 @@ async function readRegisteredProject(path: string): Promise<RegisteredProjectRec
|
|
|
273
677
|
}
|
|
274
678
|
}
|
|
275
679
|
|
|
680
|
+
async function readProjectWorkspace(path: string): Promise<ProjectWorkspaceRecordV1 | null> {
|
|
681
|
+
try {
|
|
682
|
+
return parseProjectWorkspace(JSON.parse(await readFile(path, "utf8")) as unknown);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (isNotFoundError(error) || error instanceof ProjectRegistrationError) return null;
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function readProjectAlias(path: string): Promise<ProjectAliasRecordV1 | null> {
|
|
690
|
+
try {
|
|
691
|
+
return parseProjectAlias(JSON.parse(await readFile(path, "utf8")) as unknown);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (isNotFoundError(error) || error instanceof ProjectRegistrationError) return null;
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
276
698
|
function parseDiscoveredProject(value: unknown): DiscoveredProjectRecordV1 {
|
|
277
699
|
const record = requireRecord(value);
|
|
278
700
|
if (
|
|
@@ -314,6 +736,47 @@ function parseRegisteredProject(value: unknown): RegisteredProjectRecordV1 {
|
|
|
314
736
|
return record as unknown as RegisteredProjectRecordV1;
|
|
315
737
|
}
|
|
316
738
|
|
|
739
|
+
function parseProjectWorkspace(value: unknown): ProjectWorkspaceRecordV1 {
|
|
740
|
+
const record = requireRecord(value);
|
|
741
|
+
if (
|
|
742
|
+
record.schemaVersion !== 1 ||
|
|
743
|
+
record.kind !== "project-workspace" ||
|
|
744
|
+
!isProjectKey(record.workspaceKey) ||
|
|
745
|
+
!isProjectKey(record.projectKey) ||
|
|
746
|
+
!isNonEmptyString(record.displayName) ||
|
|
747
|
+
!isAbsoluteString(record.workspaceRoot) ||
|
|
748
|
+
!isWorkspaceKind(record.workspaceKind) ||
|
|
749
|
+
!(record.gitCommonDir === null || isAbsoluteString(record.gitCommonDir)) ||
|
|
750
|
+
!isDiscoveryTargetArray(record.sourceTargets) ||
|
|
751
|
+
!isNonEmptyString(record.firstSeenAt) ||
|
|
752
|
+
!isNonEmptyString(record.lastSeenAt) ||
|
|
753
|
+
record.localOnly !== true ||
|
|
754
|
+
record.sourceContentStored !== false
|
|
755
|
+
) {
|
|
756
|
+
throw new ProjectRegistrationError("invalid", "Project workspace record is invalid.");
|
|
757
|
+
}
|
|
758
|
+
return record as unknown as ProjectWorkspaceRecordV1;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function parseProjectAlias(value: unknown): ProjectAliasRecordV1 {
|
|
762
|
+
const record = requireRecord(value);
|
|
763
|
+
if (
|
|
764
|
+
record.schemaVersion !== 1 ||
|
|
765
|
+
record.kind !== "project-alias" ||
|
|
766
|
+
!isProjectKey(record.aliasProjectKey) ||
|
|
767
|
+
!isProjectKey(record.canonicalProjectKey) ||
|
|
768
|
+
record.aliasProjectKey === record.canonicalProjectKey ||
|
|
769
|
+
!isAbsoluteString(record.workspaceRoot) ||
|
|
770
|
+
!isNonEmptyString(record.createdAt) ||
|
|
771
|
+
!isNonEmptyString(record.updatedAt) ||
|
|
772
|
+
record.localOnly !== true ||
|
|
773
|
+
record.sourceContentStored !== false
|
|
774
|
+
) {
|
|
775
|
+
throw new ProjectRegistrationError("invalid", "Project alias record is invalid.");
|
|
776
|
+
}
|
|
777
|
+
return record as unknown as ProjectAliasRecordV1;
|
|
778
|
+
}
|
|
779
|
+
|
|
317
780
|
function mergeDiscoveredRecord(
|
|
318
781
|
records: Map<string, DiscoveredProjectRecordV1>,
|
|
319
782
|
next: DiscoveredProjectRecordV1,
|
|
@@ -332,6 +795,24 @@ function mergeDiscoveredRecord(
|
|
|
332
795
|
});
|
|
333
796
|
}
|
|
334
797
|
|
|
798
|
+
function mergeRegisteredRecord(
|
|
799
|
+
records: Map<string, RegisteredProjectRecordV1>,
|
|
800
|
+
next: RegisteredProjectRecordV1,
|
|
801
|
+
): void {
|
|
802
|
+
const previous = records.get(next.projectKey);
|
|
803
|
+
if (previous === undefined) {
|
|
804
|
+
records.set(next.projectKey, next);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const newer = next.lastSeenAt >= previous.lastSeenAt ? next : previous;
|
|
808
|
+
records.set(next.projectKey, {
|
|
809
|
+
...newer,
|
|
810
|
+
registeredAt:
|
|
811
|
+
previous.registeredAt <= next.registeredAt ? previous.registeredAt : next.registeredAt,
|
|
812
|
+
lastSeenAt: previous.lastSeenAt >= next.lastSeenAt ? previous.lastSeenAt : next.lastSeenAt,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
335
816
|
function mergeTargets(
|
|
336
817
|
left: ProjectDiscoveryTarget[],
|
|
337
818
|
right: ProjectDiscoveryTarget[],
|