|
1 | 1 | import type { Runtime, ExecOptions } from "@/node/runtime/Runtime"; |
2 | 2 | import { PlatformPaths } from "@/node/utils/paths.main"; |
| 3 | +import { getLegacyPlanFilePath, getPlanFilePath } from "@/common/utils/planStorage"; |
3 | 4 |
|
4 | 5 | /** |
5 | 6 | * Convenience helpers for working with streaming Runtime APIs. |
@@ -117,3 +118,76 @@ async function streamToString(stream: ReadableStream<Uint8Array>): Promise<strin |
117 | 118 | reader.releaseLock(); |
118 | 119 | } |
119 | 120 | } |
| 121 | + |
| 122 | +/** |
| 123 | + * Result from reading a plan file with legacy migration support |
| 124 | + */ |
| 125 | +export interface ReadPlanResult { |
| 126 | + /** Plan file content (empty string if file doesn't exist) */ |
| 127 | + content: string; |
| 128 | + /** Whether a plan file exists */ |
| 129 | + exists: boolean; |
| 130 | + /** The canonical plan file path (new format) */ |
| 131 | + path: string; |
| 132 | +} |
| 133 | + |
| 134 | +/** |
| 135 | + * Read plan file content, checking new path first then legacy, migrating if needed. |
| 136 | + * This handles the transparent migration from ~/.mux/plans/{id}.md to |
| 137 | + * ~/.mux/plans/{projectName}/{workspaceName}.md |
| 138 | + */ |
| 139 | +export async function readPlanFile( |
| 140 | + runtime: Runtime, |
| 141 | + workspaceName: string, |
| 142 | + projectName: string, |
| 143 | + workspaceId: string |
| 144 | +): Promise<ReadPlanResult> { |
| 145 | + const planPath = getPlanFilePath(workspaceName, projectName); |
| 146 | + const legacyPath = getLegacyPlanFilePath(workspaceId); |
| 147 | + |
| 148 | + // Try new path first |
| 149 | + try { |
| 150 | + const content = await readFileString(runtime, planPath); |
| 151 | + return { content, exists: true, path: planPath }; |
| 152 | + } catch { |
| 153 | + // Fall back to legacy path |
| 154 | + try { |
| 155 | + const content = await readFileString(runtime, legacyPath); |
| 156 | + // Migrate: move to new location |
| 157 | + try { |
| 158 | + const planDir = planPath.substring(0, planPath.lastIndexOf("/")); |
| 159 | + await execBuffered(runtime, `mkdir -p "${planDir}" && mv "${legacyPath}" "${planPath}"`, { |
| 160 | + cwd: "/tmp", |
| 161 | + timeout: 5, |
| 162 | + }); |
| 163 | + } catch { |
| 164 | + // Migration failed, but we have the content |
| 165 | + } |
| 166 | + return { content, exists: true, path: planPath }; |
| 167 | + } catch { |
| 168 | + // File doesn't exist at either location |
| 169 | + return { content: "", exists: false, path: planPath }; |
| 170 | + } |
| 171 | + } |
| 172 | +} |
| 173 | + |
| 174 | +/** |
| 175 | + * Move a plan file from one workspace name to another (e.g., during rename). |
| 176 | + * Silently succeeds if source file doesn't exist. |
| 177 | + */ |
| 178 | +export async function movePlanFile( |
| 179 | + runtime: Runtime, |
| 180 | + oldWorkspaceName: string, |
| 181 | + newWorkspaceName: string, |
| 182 | + projectName: string |
| 183 | +): Promise<void> { |
| 184 | + const oldPath = getPlanFilePath(oldWorkspaceName, projectName); |
| 185 | + const newPath = getPlanFilePath(newWorkspaceName, projectName); |
| 186 | + |
| 187 | + try { |
| 188 | + await runtime.stat(oldPath); |
| 189 | + await execBuffered(runtime, `mv "${oldPath}" "${newPath}"`, { cwd: "/tmp", timeout: 5 }); |
| 190 | + } catch { |
| 191 | + // No plan file to move, that's fine |
| 192 | + } |
| 193 | +} |
0 commit comments