* Scope note: unlike create(), this does not accept creation-time `tags` — * multi-project workspaces currently can't be tagged atomically (callers * would need a follow-up updateTags, reintroducing a crash-window gap). * Thread tags through here if orchestration loops ever target multi-pro
(
projects: ProjectRef[],
branchName: string,
trunkBranch: string | undefined,
title?: string,
runtimeConfig?: RuntimeConfig
)
| 3333 | * workspaces. |
| 3334 | */ |
| 3335 | async createMultiProject( |
| 3336 | projects: ProjectRef[], |
| 3337 | branchName: string, |
| 3338 | trunkBranch: string | undefined, |
| 3339 | title?: string, |
| 3340 | runtimeConfig?: RuntimeConfig |
| 3341 | ): Promise<Result<FrontendWorkspaceMetadata>> { |
| 3342 | assert(projects.length > 1, "createMultiProject requires at least two projects"); |
| 3343 | if (!this.isMultiProjectWorkspacesExperimentEnabled()) { |
| 3344 | return Err(MULTI_PROJECT_WORKSPACES_DISABLED_ERROR); |
| 3345 | } |
| 3346 | |
| 3347 | let initLogger: ReturnType<WorkspaceService["createInitLogger"]> | null = null; |
| 3348 | |
| 3349 | try { |
| 3350 | const validation = validateWorkspaceName(branchName); |
| 3351 | if (!validation.valid) { |
| 3352 | return Err(validation.error ?? "Invalid workspace name"); |
| 3353 | } |
| 3354 | |
| 3355 | const normalizedProjects = projects.map((project) => ({ |
| 3356 | projectPath: stripTrailingSlashes(project.projectPath), |
| 3357 | projectName: project.projectName, |
| 3358 | })); |
| 3359 | const primaryProject = normalizedProjects[0]; |
| 3360 | assert(primaryProject, "createMultiProject requires a primary project"); |
| 3361 | |
| 3362 | const configSnapshot = this.config.loadConfigOrDefault(); |
| 3363 | for (const project of normalizedProjects) { |
| 3364 | const projectConfig = configSnapshot.projects.get( |
| 3365 | stripTrailingSlashes(project.projectPath) |
| 3366 | ); |
| 3367 | if (projectConfig?.parentProjectPath) { |
| 3368 | return Err( |
| 3369 | `Sub-project ${project.projectName} cannot be added directly to a multi-project workspace. Add its parent project instead.` |
| 3370 | ); |
| 3371 | } |
| 3372 | } |
| 3373 | |
| 3374 | for (const project of normalizedProjects) { |
| 3375 | const projectConfig = configSnapshot.projects.get( |
| 3376 | stripTrailingSlashes(project.projectPath) |
| 3377 | ); |
| 3378 | if (!projectConfig?.trusted) { |
| 3379 | return Err( |
| 3380 | `Project ${project.projectName} must be trusted before creating workspaces. Trust the project in Settings → Security, or create a workspace from the project page.` |
| 3381 | ); |
| 3382 | } |
| 3383 | } |
| 3384 | |
| 3385 | const workspaceId = this.config.generateStableId(); |
| 3386 | |
| 3387 | let finalRuntimeConfig: RuntimeConfig = runtimeConfig ?? { |
| 3388 | type: "worktree", |
| 3389 | srcBaseDir: this.config.srcDir, |
| 3390 | }; |
| 3391 | |
| 3392 | if (this.policyService?.isEnforced()) { |
no test coverage detected