(projectPath: string, force = false)
| 933 | } |
| 934 | |
| 935 | async remove(projectPath: string, force = false): Promise<Result<void, ProjectRemoveError>> { |
| 936 | try { |
| 937 | const normalizedPath = stripTrailingSlashes(projectPath); |
| 938 | let config = this.config.loadConfigOrDefault(); |
| 939 | let projectConfig = config.projects.get(normalizedPath); |
| 940 | |
| 941 | if (!projectConfig) { |
| 942 | return Err({ type: "project_not_found" as const }); |
| 943 | } |
| 944 | |
| 945 | if (projectConfig.parentProjectPath) { |
| 946 | const parentProject = config.projects.get(projectConfig.parentProjectPath); |
| 947 | if (parentProject) { |
| 948 | for (const workspace of parentProject.workspaces) { |
| 949 | if (workspace.subProjectPath === normalizedPath) { |
| 950 | workspace.subProjectPath = undefined; |
| 951 | } |
| 952 | } |
| 953 | } |
| 954 | try { |
| 955 | await this.config.updateProjectSecrets(normalizedPath, []); |
| 956 | } catch (error) { |
| 957 | log.error(`Failed to clean up secrets for sub-project ${normalizedPath}:`, error); |
| 958 | } |
| 959 | config.projects.delete(normalizedPath); |
| 960 | await this.config.saveConfig(config); |
| 961 | return Ok(undefined); |
| 962 | } |
| 963 | |
| 964 | // Self-healing: purge workspace entries whose backing directories no longer exist. |
| 965 | // This handles the case where a user manually deleted workspace dirs from ~/.mux/src/. |
| 966 | // Only check local/worktree runtimes — remote runtimes (SSH, Docker, devcontainer) |
| 967 | // have paths on the remote host that won't exist locally. |
| 968 | const localRuntimeTypes = new Set(["local", "worktree"]); |
| 969 | const survivingWorkspaces = []; |
| 970 | for (const ws of projectConfig.workspaces) { |
| 971 | const runtimeType = ws.runtimeConfig?.type; |
| 972 | const isLocal = runtimeType == null || localRuntimeTypes.has(runtimeType); |
| 973 | if (!isLocal) { |
| 974 | survivingWorkspaces.push(ws); |
| 975 | continue; |
| 976 | } |
| 977 | try { |
| 978 | await fsPromises.access(ws.path); |
| 979 | survivingWorkspaces.push(ws); |
| 980 | } catch (err: unknown) { |
| 981 | // Only prune when the directory is truly gone (ENOENT/ENOTDIR). |
| 982 | // Other errors (EACCES, transient I/O) mean the path may still exist. |
| 983 | const code = (err as NodeJS.ErrnoException).code; |
| 984 | if (code === "ENOENT" || code === "ENOTDIR") { |
| 985 | log.info(`Pruning stale workspace entry (directory missing): ${ws.path}`); |
| 986 | } else { |
| 987 | log.warn( |
| 988 | `Keeping workspace entry despite access error (${code ?? "unknown"}): ${ws.path}` |
| 989 | ); |
| 990 | survivingWorkspaces.push(ws); |
| 991 | } |
| 992 | } |
nothing calls this directly
no test coverage detected