@rk0429/agentic-relay 19.10.1 → 19.12.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.
@@ -0,0 +1,687 @@
1
+ import { cp, lstat, mkdir, mkdtemp, readdir, readFile, readlink, rename, rm, symlink, writeFile, } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { RelayError, ValidationError } from "../core/errors.js";
5
+ import { buildManifest, computeFileHash, DEFAULT_BRANCH, DEFAULT_SOURCE, MANIFEST_FILENAME, isRequiredDistFile, readManifest, writeManifest, } from "../domain/core-manifest.js";
6
+ export class CoreManagementService {
7
+ deps;
8
+ constructor(deps) {
9
+ this.deps = deps;
10
+ }
11
+ async setup(options) {
12
+ const agentsDir = this.agentsDir();
13
+ const source = options.source ?? DEFAULT_SOURCE;
14
+ const branch = options.branch ?? DEFAULT_BRANCH;
15
+ const state = await this.detectAgentsState();
16
+ let action;
17
+ let submoduleState = null;
18
+ if (state === "directory-with-manifest" && !options.force) {
19
+ throw new ValidationError("既にセットアップ済みです。relay core update を使用してください。", {
20
+ recoveryHint: "最新の dist を取り込みたい場合は relay core update を実行してください。再配置したい場合は relay core setup --force を使用してください。",
21
+ });
22
+ }
23
+ if (state === "submodule") {
24
+ submoduleState = await this.convertLegacySubmodule();
25
+ action = "converted-from-submodule";
26
+ }
27
+ else if (state === "directory-with-manifest") {
28
+ await this.prepareForceReinstall(agentsDir);
29
+ action = "force-reinstalled";
30
+ }
31
+ else if (state === "directory-without-manifest") {
32
+ action = "merged";
33
+ }
34
+ else {
35
+ await mkdir(agentsDir, { recursive: true });
36
+ action = "created";
37
+ }
38
+ let fetched = null;
39
+ let manifest = null;
40
+ try {
41
+ fetched = await this.fetchDist(source, branch);
42
+ const installResult = await this.installDistIntoAgents({
43
+ agentsDir,
44
+ conflictResolver: options.conflictResolver,
45
+ distDir: fetched.dir,
46
+ });
47
+ manifest = await buildManifest(fetched.dir, source, branch, fetched.commit, installResult.skippedFiles);
48
+ await writeManifest(agentsDir, manifest);
49
+ await this.ensureClaudeSymlink();
50
+ const backedUpFiles = [...installResult.backedUpFiles];
51
+ const conflicts = [...installResult.conflicts];
52
+ if (submoduleState) {
53
+ const restoreResult = await this.restoreBackedUpLocalFiles(submoduleState.backupDir);
54
+ backedUpFiles.push(...restoreResult.backedUpFiles);
55
+ conflicts.push(...restoreResult.conflicts);
56
+ await rm(submoduleState.backupDir, { recursive: true, force: true });
57
+ submoduleState = null;
58
+ }
59
+ return {
60
+ action,
61
+ agentsDir,
62
+ backedUpFiles,
63
+ conflicts,
64
+ manifest,
65
+ skippedFiles: installResult.skippedFiles,
66
+ };
67
+ }
68
+ catch (error) {
69
+ if (submoduleState) {
70
+ throw attachSubmoduleBackupHint(error, submoduleState.backupDir);
71
+ }
72
+ throw error;
73
+ }
74
+ finally {
75
+ if (fetched) {
76
+ await rm(fetched.dir, { recursive: true, force: true });
77
+ }
78
+ }
79
+ }
80
+ async update(options) {
81
+ const agentsDir = this.agentsDir();
82
+ const currentManifest = await readManifest(agentsDir);
83
+ if (!currentManifest) {
84
+ throw new ValidationError("マニフェストが見つかりません。relay core setup を先に実行してください。", {
85
+ recoveryHint: "relay core setup を実行して .agents を relay 管理下に置いてから relay core update を再実行してください。",
86
+ });
87
+ }
88
+ const source = options.source ?? currentManifest.source ?? DEFAULT_SOURCE;
89
+ const branch = options.branch ?? currentManifest.branch ?? DEFAULT_BRANCH;
90
+ const fetched = await this.fetchDist(source, branch);
91
+ try {
92
+ const distManifest = await buildManifest(fetched.dir, source, branch, fetched.commit);
93
+ const nextFiles = { ...currentManifest.files };
94
+ const added = [];
95
+ const updated = [];
96
+ const removedFromManifest = [];
97
+ const skipped = [];
98
+ const conflicts = [];
99
+ for (const [relativePath, distHash] of Object.entries(distManifest.files)) {
100
+ const targetPath = path.join(agentsDir, relativePath);
101
+ const trackedHash = currentManifest.files[relativePath];
102
+ const targetKind = await this.pathKind(targetPath);
103
+ if (trackedHash === undefined) {
104
+ if (targetKind === "missing") {
105
+ await this.copyEntry(path.join(fetched.dir, relativePath), targetPath);
106
+ nextFiles[relativePath] = distHash;
107
+ added.push(relativePath);
108
+ continue;
109
+ }
110
+ const resolution = await this.resolveConflict(targetPath, relativePath, isRequiredDistFile(relativePath), options.conflictResolver);
111
+ conflicts.push({ path: relativePath, resolution });
112
+ if (resolution === "skip") {
113
+ skipped.push(relativePath);
114
+ continue;
115
+ }
116
+ if (resolution === "backup") {
117
+ await this.backupExistingPath(targetPath);
118
+ }
119
+ await this.replaceEntryWithCopy(path.join(fetched.dir, relativePath), targetPath, {
120
+ targetAlreadyMoved: resolution === "backup",
121
+ });
122
+ nextFiles[relativePath] = distHash;
123
+ added.push(relativePath);
124
+ continue;
125
+ }
126
+ if (distHash === trackedHash) {
127
+ continue;
128
+ }
129
+ if (targetKind === "missing") {
130
+ await this.copyEntry(path.join(fetched.dir, relativePath), targetPath);
131
+ nextFiles[relativePath] = distHash;
132
+ updated.push(relativePath);
133
+ continue;
134
+ }
135
+ const localHash = await this.tryComputeFileHash(targetPath);
136
+ if (localHash === trackedHash) {
137
+ await this.replaceEntryWithCopy(path.join(fetched.dir, relativePath), targetPath);
138
+ nextFiles[relativePath] = distHash;
139
+ updated.push(relativePath);
140
+ continue;
141
+ }
142
+ const resolution = await this.resolveConflict(targetPath, relativePath, isRequiredDistFile(relativePath), options.conflictResolver);
143
+ conflicts.push({ path: relativePath, resolution });
144
+ if (resolution === "skip") {
145
+ skipped.push(relativePath);
146
+ continue;
147
+ }
148
+ if (resolution === "backup") {
149
+ await this.backupExistingPath(targetPath);
150
+ }
151
+ await this.replaceEntryWithCopy(path.join(fetched.dir, relativePath), targetPath, {
152
+ targetAlreadyMoved: resolution === "backup",
153
+ });
154
+ nextFiles[relativePath] = distHash;
155
+ updated.push(relativePath);
156
+ }
157
+ for (const relativePath of Object.keys(currentManifest.files)) {
158
+ if (distManifest.files[relativePath] !== undefined) {
159
+ continue;
160
+ }
161
+ delete nextFiles[relativePath];
162
+ removedFromManifest.push(relativePath);
163
+ }
164
+ const nextManifest = {
165
+ ...currentManifest,
166
+ branch,
167
+ commit: fetched.commit,
168
+ files: sortRecord(nextFiles),
169
+ source,
170
+ updatedAt: new Date().toISOString(),
171
+ };
172
+ await writeManifest(agentsDir, nextManifest);
173
+ await this.ensureClaudeSymlink();
174
+ return {
175
+ added,
176
+ agentsDir,
177
+ conflicts,
178
+ manifest: nextManifest,
179
+ removedFromManifest,
180
+ skipped,
181
+ updated,
182
+ };
183
+ }
184
+ finally {
185
+ await rm(fetched.dir, { recursive: true, force: true });
186
+ }
187
+ }
188
+ async remove(options) {
189
+ const agentsDir = this.agentsDir();
190
+ const keepLocal = options.keepLocal ?? false;
191
+ const manifest = await readManifest(agentsDir);
192
+ const removedFiles = [];
193
+ const keptFiles = [];
194
+ let agentsDirRemoved = false;
195
+ let symlinkRemoved = false;
196
+ if (!manifest && keepLocal) {
197
+ throw new ValidationError("マニフェストがないため dist 由来ファイルとユーザーファイルを区別できません。relay core setup --force で再配置するか、--keep-local なしで全削除してください。", {
198
+ recoveryHint: "ローカルファイルを保持したい場合は relay core setup --force でマニフェストを再生成してください。全削除でよければ relay core remove を --keep-local なしで実行してください。",
199
+ });
200
+ }
201
+ if (manifest) {
202
+ for (const relativePath of Object.keys(manifest.files)) {
203
+ const targetPath = path.join(agentsDir, relativePath);
204
+ if ((await this.pathKind(targetPath)) === "missing") {
205
+ continue;
206
+ }
207
+ await rm(targetPath, { recursive: true, force: true });
208
+ removedFiles.push(this.toWorkspaceRelative(targetPath));
209
+ }
210
+ const manifestPath = path.join(agentsDir, MANIFEST_FILENAME);
211
+ if ((await this.pathKind(manifestPath)) !== "missing") {
212
+ await rm(manifestPath, { force: true });
213
+ removedFiles.push(this.toWorkspaceRelative(manifestPath));
214
+ }
215
+ }
216
+ if (keepLocal) {
217
+ await this.collectKeptFiles(keptFiles);
218
+ }
219
+ else {
220
+ if ((await this.pathKind(agentsDir)) !== "missing") {
221
+ const existingFiles = await this.collectRelativeFiles(agentsDir);
222
+ for (const relativePath of existingFiles) {
223
+ const workspacePath = this.toWorkspaceRelative(path.join(agentsDir, relativePath));
224
+ if (!removedFiles.includes(workspacePath)) {
225
+ removedFiles.push(workspacePath);
226
+ }
227
+ }
228
+ await rm(agentsDir, { recursive: true, force: true });
229
+ agentsDirRemoved = true;
230
+ }
231
+ symlinkRemoved = await this.removeClaudeSymlink();
232
+ if (symlinkRemoved) {
233
+ removedFiles.push(".claude");
234
+ }
235
+ }
236
+ if (keepLocal) {
237
+ keptFiles.sort();
238
+ }
239
+ else {
240
+ removedFiles.sort();
241
+ }
242
+ return {
243
+ agentsDirRemoved,
244
+ keptFiles,
245
+ removedFiles,
246
+ symlinkRemoved,
247
+ };
248
+ }
249
+ agentsDir() {
250
+ return path.join(this.deps.cwd, ".agents");
251
+ }
252
+ claudePath() {
253
+ return path.join(this.deps.cwd, ".claude");
254
+ }
255
+ async detectAgentsState() {
256
+ const agentsDir = this.agentsDir();
257
+ const agentsKind = await this.pathKind(agentsDir);
258
+ if (agentsKind === "missing") {
259
+ return "not-found";
260
+ }
261
+ if (agentsKind !== "directory") {
262
+ throw new ValidationError(`Expected .agents to be a directory, found ${agentsKind}.`, {
263
+ recoveryHint: "Remove or rename the existing .agents entry, then rerun relay core setup.",
264
+ });
265
+ }
266
+ if (await this.isLegacySubmodule(agentsDir)) {
267
+ return "submodule";
268
+ }
269
+ if ((await this.pathKind(path.join(agentsDir, MANIFEST_FILENAME))) !== "missing") {
270
+ return "directory-with-manifest";
271
+ }
272
+ return "directory-without-manifest";
273
+ }
274
+ async isLegacySubmodule(agentsDir) {
275
+ const gitFilePath = path.join(agentsDir, ".git");
276
+ if ((await this.pathKind(gitFilePath)) !== "file") {
277
+ return false;
278
+ }
279
+ const gitFileContent = await readFile(gitFilePath, "utf8").catch(() => "");
280
+ if (!gitFileContent.startsWith("gitdir:")) {
281
+ return false;
282
+ }
283
+ if ((await this.pathKind(path.join(this.deps.cwd, ".git", "modules", ".agents"))) !== "missing") {
284
+ return true;
285
+ }
286
+ const gitmodulesPath = path.join(this.deps.cwd, ".gitmodules");
287
+ if ((await this.pathKind(gitmodulesPath)) === "missing") {
288
+ return false;
289
+ }
290
+ const gitmodules = await readFile(gitmodulesPath, "utf8");
291
+ return gitmodules.includes('[submodule ".agents"]') || gitmodules.includes("path = .agents");
292
+ }
293
+ async prepareForceReinstall(agentsDir) {
294
+ try {
295
+ const existingManifest = await readManifest(agentsDir);
296
+ if (existingManifest) {
297
+ await this.removeManifestTrackedFiles(existingManifest);
298
+ }
299
+ }
300
+ catch (error) {
301
+ if (!(error instanceof ValidationError)) {
302
+ throw error;
303
+ }
304
+ }
305
+ await rm(path.join(agentsDir, MANIFEST_FILENAME), { force: true });
306
+ }
307
+ async removeManifestTrackedFiles(manifest) {
308
+ const agentsDir = this.agentsDir();
309
+ for (const relativePath of Object.keys(manifest.files)) {
310
+ await rm(path.join(agentsDir, relativePath), { recursive: true, force: true });
311
+ }
312
+ }
313
+ async convertLegacySubmodule() {
314
+ const agentsDir = this.agentsDir();
315
+ const backupDir = await mkdtemp(path.join(os.tmpdir(), "agentic-relay-core-backup-"));
316
+ const localFiles = await this.collectSubmoduleLocalFiles();
317
+ for (const relativePath of localFiles) {
318
+ const sourcePath = path.join(agentsDir, relativePath);
319
+ if ((await this.pathKind(sourcePath)) === "missing") {
320
+ continue;
321
+ }
322
+ await this.copyEntry(sourcePath, path.join(backupDir, relativePath));
323
+ }
324
+ await this.executeGit(["submodule", "deinit", "-f", ".agents"], "deinitialize the .agents submodule");
325
+ await this.executeGit(["rm", "-f", ".agents"], "remove the .agents submodule");
326
+ await rm(path.join(this.deps.cwd, ".git", "modules", ".agents"), {
327
+ recursive: true,
328
+ force: true,
329
+ });
330
+ await this.removeAgentsSectionFromGitmodules();
331
+ await rm(agentsDir, { recursive: true, force: true });
332
+ await mkdir(agentsDir, { recursive: true });
333
+ return { backupDir };
334
+ }
335
+ async collectSubmoduleLocalFiles() {
336
+ const outputs = await Promise.all([
337
+ this.executeGit(["-C", ".agents", "ls-files", "--others", "--exclude-standard"], "list untracked files in .agents"),
338
+ this.executeGit(["-C", ".agents", "diff", "--name-only"], "list unstaged changes in .agents"),
339
+ this.executeGit(["-C", ".agents", "diff", "--name-only", "--cached"], "list staged changes in .agents"),
340
+ this.executeGit(["-C", ".agents", "ls-files", "--others", "--ignored", "--exclude-standard"], "list ignored local files in .agents"),
341
+ ]);
342
+ const files = new Set();
343
+ for (const output of outputs) {
344
+ for (const line of output.stdout.split(/\r?\n/)) {
345
+ const trimmed = normalizeRelativePath(line.trim());
346
+ if (trimmed.length > 0) {
347
+ files.add(trimmed);
348
+ }
349
+ }
350
+ }
351
+ return [...files].sort();
352
+ }
353
+ async removeAgentsSectionFromGitmodules() {
354
+ const gitmodulesPath = path.join(this.deps.cwd, ".gitmodules");
355
+ if ((await this.pathKind(gitmodulesPath)) === "missing") {
356
+ return;
357
+ }
358
+ const content = await readFile(gitmodulesPath, "utf8");
359
+ const lines = content.split(/\r?\n/);
360
+ const kept = [];
361
+ for (let index = 0; index < lines.length; index += 1) {
362
+ const currentLine = lines[index] ?? "";
363
+ if (currentLine.trim() !== '[submodule ".agents"]') {
364
+ kept.push(currentLine);
365
+ continue;
366
+ }
367
+ index += 1;
368
+ while (index < lines.length && !lines[index].trim().startsWith("[submodule ")) {
369
+ index += 1;
370
+ }
371
+ index -= 1;
372
+ }
373
+ const nextContent = trimTrailingBlankLines(kept).join("\n");
374
+ if (nextContent.trim().length === 0) {
375
+ const result = await this.deps.processExecutor.execute({
376
+ args: ["rm", "-f", ".gitmodules"],
377
+ command: "git",
378
+ cwd: this.deps.cwd,
379
+ env: this.deps.env,
380
+ });
381
+ if (result.exitCode !== 0) {
382
+ await rm(gitmodulesPath, { force: true });
383
+ return;
384
+ }
385
+ await rm(gitmodulesPath, { force: true });
386
+ return;
387
+ }
388
+ await writeFile(gitmodulesPath, `${nextContent}\n`, "utf8");
389
+ }
390
+ async fetchDist(source, branch) {
391
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "agentic-relay-core-dist-"));
392
+ try {
393
+ // Try git archive first (efficient — no VCS metadata)
394
+ const archiveResult = await this.tryGitArchive(source, branch, tempDir);
395
+ if (archiveResult) {
396
+ return { commit: archiveResult.commit, dir: tempDir };
397
+ }
398
+ // Fallback: git clone --depth 1 (GitHub disables git archive --remote)
399
+ await this.executeGit(["clone", "--depth", "1", "--branch", branch, "--single-branch", source, tempDir], `clone ${source}#${branch}`);
400
+ const revParse = await this.executeGit(["-C", tempDir, "rev-parse", "HEAD"], `resolve the commit for ${source}#${branch}`);
401
+ const commit = revParse.stdout.trim();
402
+ if (commit.length === 0) {
403
+ throw new RelayError("CORE_DIST_FETCH_FAILED", `Failed to resolve a commit hash for ${source}#${branch}.`);
404
+ }
405
+ await rm(path.join(tempDir, ".git"), { recursive: true, force: true });
406
+ return { commit, dir: tempDir };
407
+ }
408
+ catch (error) {
409
+ await rm(tempDir, { recursive: true, force: true });
410
+ throw error;
411
+ }
412
+ }
413
+ async tryGitArchive(source, branch, targetDir) {
414
+ try {
415
+ // git archive --remote produces a tar stream without .git metadata
416
+ // Use positional parameters to avoid command injection via source/branch
417
+ const result = await this.deps.processExecutor.execute({
418
+ command: "bash",
419
+ args: ["-c", 'git archive --remote="$1" "$2" | tar -x -C "$3"', "--", source, branch, targetDir],
420
+ cwd: this.deps.cwd,
421
+ env: this.deps.env,
422
+ });
423
+ if (result.exitCode !== 0) {
424
+ return null;
425
+ }
426
+ // git archive doesn't give us the commit hash; resolve it via ls-remote
427
+ const lsRemote = await this.deps.processExecutor.execute({
428
+ command: "git",
429
+ args: ["ls-remote", source, `refs/heads/${branch}`],
430
+ cwd: this.deps.cwd,
431
+ env: this.deps.env,
432
+ });
433
+ if (lsRemote.exitCode !== 0 || lsRemote.stdout.trim().length === 0) {
434
+ return null;
435
+ }
436
+ const commit = lsRemote.stdout.trim().split(/\s/)[0] ?? "";
437
+ if (commit.length === 0) {
438
+ return null;
439
+ }
440
+ return { commit };
441
+ }
442
+ catch {
443
+ // git archive --remote not supported by this host — fall through to clone
444
+ return null;
445
+ }
446
+ }
447
+ async installDistIntoAgents(input) {
448
+ const relativePaths = await this.collectRelativeFiles(input.distDir);
449
+ const backedUpFiles = [];
450
+ const skippedFiles = [];
451
+ const conflicts = [];
452
+ for (const relativePath of relativePaths) {
453
+ const sourcePath = path.join(input.distDir, relativePath);
454
+ const targetPath = path.join(input.agentsDir, relativePath);
455
+ if ((await this.pathKind(targetPath)) === "missing") {
456
+ await this.copyEntry(sourcePath, targetPath);
457
+ continue;
458
+ }
459
+ const resolution = await this.resolveConflict(targetPath, relativePath, isRequiredDistFile(relativePath), input.conflictResolver);
460
+ conflicts.push({ path: relativePath, resolution });
461
+ if (resolution === "skip") {
462
+ skippedFiles.push(relativePath);
463
+ continue;
464
+ }
465
+ if (resolution === "backup") {
466
+ const backupPath = await this.backupExistingPath(targetPath);
467
+ backedUpFiles.push(this.toWorkspaceRelative(backupPath));
468
+ }
469
+ await this.replaceEntryWithCopy(sourcePath, targetPath, {
470
+ targetAlreadyMoved: resolution === "backup",
471
+ });
472
+ }
473
+ return { backedUpFiles, conflicts, skippedFiles };
474
+ }
475
+ async restoreBackedUpLocalFiles(backupDir) {
476
+ const relativePaths = await this.collectRelativeFiles(backupDir);
477
+ const backedUpFiles = [];
478
+ const conflicts = [];
479
+ for (const relativePath of relativePaths) {
480
+ const restoreTarget = path.join(this.agentsDir(), relativePath);
481
+ if ((await this.pathKind(restoreTarget)) !== "missing") {
482
+ const backupPath = await this.backupExistingPath(restoreTarget);
483
+ backedUpFiles.push(this.toWorkspaceRelative(backupPath));
484
+ conflicts.push({ path: relativePath, resolution: "backup" });
485
+ }
486
+ await this.copyEntry(path.join(backupDir, relativePath), restoreTarget);
487
+ }
488
+ return { backedUpFiles, conflicts };
489
+ }
490
+ async ensureClaudeSymlink() {
491
+ const claudePath = this.claudePath();
492
+ const claudeKind = await this.pathKind(claudePath);
493
+ if (claudeKind === "symlink") {
494
+ const currentTarget = await readlink(claudePath);
495
+ const resolvedTarget = path.resolve(path.dirname(claudePath), currentTarget);
496
+ if (resolvedTarget === this.agentsDir()) {
497
+ return;
498
+ }
499
+ await rm(claudePath, { force: true });
500
+ }
501
+ else if (claudeKind !== "missing") {
502
+ throw new ValidationError("既存の .claude がシムリンクではありません。", {
503
+ recoveryHint: ".claude を削除または移動してから relay core setup/update を再実行してください。",
504
+ });
505
+ }
506
+ await symlink(".agents", claudePath, process.platform === "win32" ? "junction" : "dir");
507
+ }
508
+ async removeClaudeSymlink() {
509
+ const claudePath = this.claudePath();
510
+ if ((await this.pathKind(claudePath)) !== "symlink") {
511
+ return false;
512
+ }
513
+ await rm(claudePath, { force: true });
514
+ return true;
515
+ }
516
+ async collectKeptFiles(keptFiles) {
517
+ const agentsDir = this.agentsDir();
518
+ if ((await this.pathKind(agentsDir)) === "directory") {
519
+ const relativePaths = await this.collectRelativeFiles(agentsDir);
520
+ for (const relativePath of relativePaths) {
521
+ keptFiles.push(this.toWorkspaceRelative(path.join(agentsDir, relativePath)));
522
+ }
523
+ }
524
+ if ((await this.pathKind(this.claudePath())) === "symlink") {
525
+ keptFiles.push(".claude");
526
+ }
527
+ }
528
+ async executeGit(args, action) {
529
+ const result = await this.deps.processExecutor.execute({
530
+ args,
531
+ command: "git",
532
+ cwd: this.deps.cwd,
533
+ env: this.deps.env,
534
+ });
535
+ if (result.exitCode === 0) {
536
+ return result;
537
+ }
538
+ const details = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
539
+ throw new RelayError("CORE_GIT_COMMAND_FAILED", `Failed to ${action}: ${details}`, {
540
+ causeData: { args, cwd: this.deps.cwd },
541
+ recoveryHint: "Ensure git is installed and the working tree is accessible.",
542
+ });
543
+ }
544
+ async resolveConflict(targetPath, relativePath, isRequired, conflictResolver) {
545
+ if (isRequired) {
546
+ return "backup";
547
+ }
548
+ if (!conflictResolver) {
549
+ return "overwrite";
550
+ }
551
+ const requested = await conflictResolver(targetPath, isRequired);
552
+ if (requested === "skip" && isRequired) {
553
+ return "backup";
554
+ }
555
+ if (requested === "skip" || requested === "overwrite" || requested === "backup") {
556
+ return requested;
557
+ }
558
+ throw new ValidationError(`Unsupported conflict resolution for ${relativePath}: ${String(requested)}`, {
559
+ recoveryHint: "Conflict resolvers must return overwrite, skip, or backup.",
560
+ });
561
+ }
562
+ async backupExistingPath(targetPath) {
563
+ const backupPath = await this.nextBackupPath(targetPath);
564
+ await rename(targetPath, backupPath);
565
+ return backupPath;
566
+ }
567
+ async nextBackupPath(targetPath) {
568
+ for (let index = 0;; index += 1) {
569
+ const candidate = `${targetPath}.bak${index === 0 ? "" : `.${index}`}`;
570
+ if ((await this.pathKind(candidate)) === "missing") {
571
+ return candidate;
572
+ }
573
+ }
574
+ }
575
+ async replaceEntryWithCopy(sourcePath, targetPath, options) {
576
+ if (!options?.targetAlreadyMoved) {
577
+ await rm(targetPath, { recursive: true, force: true });
578
+ }
579
+ await this.copyEntry(sourcePath, targetPath);
580
+ }
581
+ async copyEntry(sourcePath, targetPath) {
582
+ await mkdir(path.dirname(targetPath), { recursive: true });
583
+ await cp(sourcePath, targetPath, {
584
+ force: true,
585
+ recursive: true,
586
+ verbatimSymlinks: true,
587
+ });
588
+ }
589
+ async collectRelativeFiles(rootDir) {
590
+ if ((await this.pathKind(rootDir)) !== "directory") {
591
+ return [];
592
+ }
593
+ const files = [];
594
+ await this.collectRelativeFilesRecursive(rootDir, rootDir, files);
595
+ files.sort();
596
+ return files;
597
+ }
598
+ async collectRelativeFilesRecursive(rootDir, currentDir, files) {
599
+ const entries = await readdir(currentDir, { withFileTypes: true });
600
+ entries.sort((left, right) => left.name.localeCompare(right.name));
601
+ for (const entry of entries) {
602
+ const absolutePath = path.join(currentDir, entry.name);
603
+ const relativePath = normalizeRelativePath(path.relative(rootDir, absolutePath));
604
+ if (entry.isDirectory()) {
605
+ await this.collectRelativeFilesRecursive(rootDir, absolutePath, files);
606
+ continue;
607
+ }
608
+ if (relativePath.length > 0) {
609
+ files.push(relativePath);
610
+ }
611
+ }
612
+ }
613
+ async tryComputeFileHash(filePath) {
614
+ try {
615
+ return await computeFileHash(filePath);
616
+ }
617
+ catch {
618
+ return null;
619
+ }
620
+ }
621
+ async pathKind(targetPath) {
622
+ try {
623
+ const stats = await lstat(targetPath);
624
+ if (stats.isDirectory()) {
625
+ return "directory";
626
+ }
627
+ if (stats.isFile()) {
628
+ return "file";
629
+ }
630
+ if (stats.isSymbolicLink()) {
631
+ return "symlink";
632
+ }
633
+ return "other";
634
+ }
635
+ catch (error) {
636
+ if (isErrnoException(error) && error.code === "ENOENT") {
637
+ return "missing";
638
+ }
639
+ throw error;
640
+ }
641
+ }
642
+ toWorkspaceRelative(targetPath) {
643
+ const relativePath = normalizeRelativePath(path.relative(this.deps.cwd, targetPath));
644
+ return relativePath.length === 0 ? "." : relativePath;
645
+ }
646
+ }
647
+ function sortRecord(input) {
648
+ return Object.fromEntries(Object.entries(input).sort(([left], [right]) => left.localeCompare(right)));
649
+ }
650
+ function normalizeRelativePath(value) {
651
+ return value.split(path.sep).join("/").replace(/^\.\//, "");
652
+ }
653
+ function trimTrailingBlankLines(lines) {
654
+ let lastNonBlank = lines.length - 1;
655
+ while (lastNonBlank >= 0 && lines[lastNonBlank].trim().length === 0) {
656
+ lastNonBlank -= 1;
657
+ }
658
+ return lines.slice(0, lastNonBlank + 1);
659
+ }
660
+ function isErrnoException(error) {
661
+ return typeof error === "object" && error !== null && "code" in error;
662
+ }
663
+ function attachSubmoduleBackupHint(error, backupDir) {
664
+ const suffix = ` Legacy submodule backup was preserved at "${backupDir}".`;
665
+ const recoveryHint = `Recover local files from "${backupDir}" after resolving the setup failure.`;
666
+ if (error instanceof ValidationError) {
667
+ return new ValidationError(`${error.message}${suffix}`, {
668
+ causeData: error.causeData,
669
+ recoveryHint: error.recoveryHint
670
+ ? `${error.recoveryHint} ${recoveryHint}`
671
+ : recoveryHint,
672
+ });
673
+ }
674
+ if (error instanceof RelayError) {
675
+ return new RelayError(error.code, `${error.message}${suffix}`, {
676
+ causeData: error.causeData,
677
+ recoveryHint: error.recoveryHint
678
+ ? `${error.recoveryHint} ${recoveryHint}`
679
+ : recoveryHint,
680
+ });
681
+ }
682
+ return new RelayError("CORE_SETUP_FAILED", `relay core setup failed.${suffix}`, {
683
+ causeData: error,
684
+ recoveryHint,
685
+ });
686
+ }
687
+ //# sourceMappingURL=core-management-service.js.map