(
workspaceName: string,
projectWorkspaces: ProjectWorkspaceEntry[]
)
| 19 | } |
| 20 | |
| 21 | async createContainer( |
| 22 | workspaceName: string, |
| 23 | projectWorkspaces: ProjectWorkspaceEntry[] |
| 24 | ): Promise<string> { |
| 25 | // Assert no duplicate project names — would cause symlink collisions. |
| 26 | const names = projectWorkspaces.map((projectWorkspace) => projectWorkspace.projectName); |
| 27 | const uniqueNames = new Set(names); |
| 28 | assert( |
| 29 | uniqueNames.size === names.length, |
| 30 | `Duplicate project names in multi-project workspace: ${names.join(", ")}` |
| 31 | ); |
| 32 | |
| 33 | for (const projectWorkspace of projectWorkspaces) { |
| 34 | const projectName = projectWorkspace.projectName; |
| 35 | // Check both basename implementations so separators are rejected on every platform. |
| 36 | const normalizedPosixName = path.posix.basename(projectName); |
| 37 | const normalizedWindowsName = path.win32.basename(projectName); |
| 38 | assert( |
| 39 | normalizedPosixName === projectName && |
| 40 | normalizedWindowsName === projectName && |
| 41 | !projectName.includes("..") && |
| 42 | projectName.length > 0, |
| 43 | `Invalid project name "${projectName}": must be a simple name without path separators` |
| 44 | ); |
| 45 | } |
| 46 | |
| 47 | const containerPath = this.getContainerPath(workspaceName); |
| 48 | await fs.mkdir(this.containerBase, { recursive: true }); |
| 49 | // Do not use recursive mkdir here: callers need EEXIST to mean a prior workspace already |
| 50 | // owns this container name so cleanup never deletes someone else's container. |
| 51 | await fs.mkdir(containerPath); |
| 52 | |
| 53 | for (const projectWorkspace of projectWorkspaces) { |
| 54 | const linkPath = path.join(containerPath, projectWorkspace.projectName); |
| 55 | // Validate target exists before symlinking. |
| 56 | await fs.access(projectWorkspace.workspacePath); |
| 57 | await fs.symlink(projectWorkspace.workspacePath, linkPath); |
| 58 | } |
| 59 | |
| 60 | return containerPath; |
| 61 | } |
| 62 | |
| 63 | async removeContainer(workspaceName: string): Promise<void> { |
| 64 | const containerPath = this.getContainerPath(workspaceName); |
no test coverage detected