(
projectPath: string,
branchName: string | undefined,
trunkBranch: string | undefined,
title?: string,
runtimeConfig?: RuntimeConfig,
subProjectPath?: string,
pendingAutoTitle?: boolean,
tags?: Record<string, string>
)
| 3021 | } |
| 3022 | |
| 3023 | async create( |
| 3024 | projectPath: string, |
| 3025 | branchName: string | undefined, |
| 3026 | trunkBranch: string | undefined, |
| 3027 | title?: string, |
| 3028 | runtimeConfig?: RuntimeConfig, |
| 3029 | subProjectPath?: string, |
| 3030 | pendingAutoTitle?: boolean, |
| 3031 | tags?: Record<string, string> |
| 3032 | ): Promise<Result<{ metadata: FrontendWorkspaceMetadata }>> { |
| 3033 | if (tags != null) { |
| 3034 | for (const [tagKey, tagValue] of Object.entries(tags)) { |
| 3035 | assert(tagKey.trim().length > 0, "Workspace tag keys must be non-empty"); |
| 3036 | assert(typeof tagValue === "string", "Workspace tag values must be strings"); |
| 3037 | } |
| 3038 | } |
| 3039 | const configSnapshot = this.config.loadConfigOrDefault(); |
| 3040 | const requestedProjectPath = stripTrailingSlashes(projectPath); |
| 3041 | const requestedProjectConfig = configSnapshot.projects.get(requestedProjectPath); |
| 3042 | const owningProjectPath = requestedProjectConfig?.parentProjectPath ?? requestedProjectPath; |
| 3043 | const effectiveSubProjectPath = requestedProjectConfig?.parentProjectPath |
| 3044 | ? requestedProjectPath |
| 3045 | : subProjectPath; |
| 3046 | const projectConfig = configSnapshot.projects.get(owningProjectPath); |
| 3047 | |
| 3048 | if ( |
| 3049 | effectiveSubProjectPath && |
| 3050 | configSnapshot.projects.get(effectiveSubProjectPath)?.parentProjectPath !== owningProjectPath |
| 3051 | ) { |
| 3052 | return Err(`Sub-project not found under parent: ${effectiveSubProjectPath}`); |
| 3053 | } |
| 3054 | |
| 3055 | // Trust gate: block workspace creation for untrusted projects. |
| 3056 | // The frontend shows a confirmation dialog before reaching here, |
| 3057 | // but this guards secondary paths (slash commands, forking). Sub-projects |
| 3058 | // share their parent's checkout, so trust is owned by that parent project. |
| 3059 | if (!projectConfig?.trusted) { |
| 3060 | return Err( |
| 3061 | "This project must be trusted before creating workspaces. Trust the project in Settings → Security, or create a workspace from the project page." |
| 3062 | ); |
| 3063 | } |
| 3064 | |
| 3065 | // Auto-generate a branch name when the caller omits one (used by /new to |
| 3066 | // mirror /fork's seamless creation flow). Mirrors fork's auto-naming: scan |
| 3067 | // existing workspace names AND local git branches so numbering is stable. |
| 3068 | // Branches/worktrees are owned by the parent project, so always read from |
| 3069 | // owningProjectPath even when a sub-project initiated creation. |
| 3070 | let resolvedBranchName: string; |
| 3071 | if (branchName == null) { |
| 3072 | const existingNamesSet = new Set<string>(); |
| 3073 | for (const entry of projectConfig.workspaces ?? []) { |
| 3074 | if (typeof entry.name === "string") { |
| 3075 | existingNamesSet.add(entry.name); |
| 3076 | } |
| 3077 | } |
| 3078 | try { |
| 3079 | for (const localBranch of await listLocalBranches(owningProjectPath)) { |
| 3080 | existingNamesSet.add(localBranch); |
nothing calls this directly
no test coverage detected