@@ -7,10 +7,36 @@ import writeFileAtomic from "write-file-atomic";
77import type { WorkspaceMetadata } from "./types/workspace" ;
88import type { Secret , SecretsConfig } from "./types/secrets" ;
99
10+ /**
11+ * Workspace configuration in config.json.
12+ *
13+ * NEW FORMAT (preferred, used for all new workspaces):
14+ * {
15+ * "path": "~/.cmux/src/project/workspace-id", // Kept for backward compat
16+ * "id": "a1b2c3d4e5", // Stable workspace ID
17+ * "name": "feature-branch", // User-facing name
18+ * "createdAt": "2024-01-01T00:00:00Z" // Creation timestamp
19+ * }
20+ *
21+ * LEGACY FORMAT (old workspaces, still supported):
22+ * {
23+ * "path": "~/.cmux/src/project/workspace-id" // Only field present
24+ * }
25+ *
26+ * For legacy entries, metadata is read from ~/.cmux/sessions/{workspaceId}/metadata.json
27+ */
1028export interface Workspace {
11- path : string ; // Absolute path to workspace worktree (format: ~/.cmux/src/{projectName}/{workspaceId})
12- // NOTE: The workspace ID is the basename of this path (stable random ID like 'a1b2c3d4e5').
13- // Use config.getWorkspacePath(projectPath, workspaceId) to construct paths consistently.
29+ /** Absolute path to workspace worktree - REQUIRED for backward compatibility */
30+ path : string ;
31+
32+ /** Stable workspace ID (10 hex chars for new workspaces) - optional for legacy */
33+ id ?: string ;
34+
35+ /** User-facing workspace name - optional for legacy */
36+ name ?: string ;
37+
38+ /** ISO 8601 creation timestamp - optional for legacy */
39+ createdAt ?: string ;
1440}
1541
1642export interface ProjectConfig {
@@ -292,20 +318,19 @@ export class Config {
292318
293319 /**
294320 * Get all workspace metadata by loading config and metadata files.
295- * Performs eager migration for legacy workspaces on startup.
296- *
297- * Migration strategy:
298- * - For each workspace in config, try to load metadata.json from session dir
299- * - If metadata exists, use it (already migrated or new workspace)
300- * - If metadata doesn't exist, this is a legacy workspace:
301- * - Generate legacy ID from path (for backward compatibility)
302- * - Extract name from workspace path
303- * - Create and save metadata.json
304- * - Create symlink if name differs from ID (new workspaces only)
321+ *
322+ * NEW BEHAVIOR: Config is the primary source of truth
323+ * - If workspace has id/name/createdAt in config, use those directly
324+ * - If workspace only has path, fall back to reading metadata.json
325+ * - Migrate old workspaces by copying metadata from files to config
326+ *
327+ * This centralizes workspace metadata in config.json and eliminates the need
328+ * for scattered metadata.json files (kept for backward compat with older versions).
305329 */
306330 getAllWorkspaceMetadata ( ) : WorkspaceMetadata [ ] {
307331 const config = this . loadConfigOrDefault ( ) ;
308332 const workspaceMetadata : WorkspaceMetadata [ ] = [ ] ;
333+ let configModified = false ;
309334
310335 for ( const [ projectPath , projectConfig ] of config . projects ) {
311336 const projectName = this . getProjectName ( projectPath ) ;
@@ -316,68 +341,95 @@ export class Config {
316341 workspace . path . split ( "/" ) . pop ( ) ?? workspace . path . split ( "\\" ) . pop ( ) ?? "unknown" ;
317342
318343 try {
319- // Try to load metadata using workspace basename as ID (works for new workspaces with stable IDs)
344+ // NEW FORMAT: If workspace has metadata in config, use it directly
345+ if ( workspace . id && workspace . name ) {
346+ const metadata : WorkspaceMetadata = {
347+ id : workspace . id ,
348+ name : workspace . name ,
349+ projectName,
350+ projectPath,
351+ createdAt : workspace . createdAt ,
352+ } ;
353+ workspaceMetadata . push ( metadata ) ;
354+ continue ; // Skip metadata file lookup
355+ }
356+
357+ // LEGACY FORMAT: Fall back to reading metadata.json
358+ // Try workspace basename first (for new stable ID workspaces)
320359 let metadataPath = path . join ( this . getSessionDir ( workspaceBasename ) , "metadata.json" ) ;
360+ let metadataFound = false ;
321361
322362 if ( fs . existsSync ( metadataPath ) ) {
323363 const data = fs . readFileSync ( metadataPath , "utf-8" ) ;
324364 let metadata = JSON . parse ( data ) as WorkspaceMetadata ;
325365
326- // Migrate legacy metadata that's missing required fields
366+ // Ensure required fields are present
327367 if ( ! metadata . name || ! metadata . projectPath ) {
328368 metadata = {
329369 ...metadata ,
330370 name : metadata . name ?? workspaceBasename ,
331371 projectPath : metadata . projectPath ?? projectPath ,
332372 projectName : metadata . projectName ?? projectName ,
333373 } ;
334- // Save migrated metadata
335- fs . writeFileSync ( metadataPath , JSON . stringify ( metadata , null , 2 ) ) ;
336374 }
337375
376+ // Migrate to config for next load
377+ workspace . id = metadata . id ;
378+ workspace . name = metadata . name ;
379+ workspace . createdAt = metadata . createdAt ;
380+ configModified = true ;
381+
338382 workspaceMetadata . push ( metadata ) ;
339- } else {
340- // Try legacy ID format (project-workspace)
383+ metadataFound = true ;
384+ }
385+
386+ // Try legacy ID format (project-workspace)
387+ if ( ! metadataFound ) {
341388 const legacyId = this . generateWorkspaceId ( projectPath , workspace . path ) ;
342389 metadataPath = path . join ( this . getSessionDir ( legacyId ) , "metadata.json" ) ;
343390
344391 if ( fs . existsSync ( metadataPath ) ) {
345392 const data = fs . readFileSync ( metadataPath , "utf-8" ) ;
346393 let metadata = JSON . parse ( data ) as WorkspaceMetadata ;
347394
348- // Migrate legacy metadata that's missing required fields
395+ // Ensure required fields are present
349396 if ( ! metadata . name || ! metadata . projectPath ) {
350397 metadata = {
351398 ...metadata ,
352399 name : metadata . name ?? workspaceBasename ,
353400 projectPath : metadata . projectPath ?? projectPath ,
354401 projectName : metadata . projectName ?? projectName ,
355402 } ;
356- // Save migrated metadata
357- fs . writeFileSync ( metadataPath , JSON . stringify ( metadata , null , 2 ) ) ;
358403 }
359404
360- workspaceMetadata . push ( metadata ) ;
361- } else {
362- // No metadata found - create it for legacy workspace
363- const metadata : WorkspaceMetadata = {
364- id : legacyId , // Use legacy ID format for backward compatibility
365- name : workspaceBasename ,
366- projectName,
367- projectPath, // Add full project path
368- // No createdAt for legacy workspaces (unknown)
369- } ;
370-
371- // Save metadata for future loads
372- const sessionDir = this . getSessionDir ( legacyId ) ;
373- if ( ! fs . existsSync ( sessionDir ) ) {
374- fs . mkdirSync ( sessionDir , { recursive : true } ) ;
375- }
376- fs . writeFileSync ( metadataPath , JSON . stringify ( metadata , null , 2 ) ) ;
405+ // Migrate to config for next load
406+ workspace . id = metadata . id ;
407+ workspace . name = metadata . name ;
408+ workspace . createdAt = metadata . createdAt ;
409+ configModified = true ;
377410
378411 workspaceMetadata . push ( metadata ) ;
412+ metadataFound = true ;
379413 }
380414 }
415+
416+ // No metadata found anywhere - create basic metadata
417+ if ( ! metadataFound ) {
418+ const legacyId = this . generateWorkspaceId ( projectPath , workspace . path ) ;
419+ const metadata : WorkspaceMetadata = {
420+ id : legacyId ,
421+ name : workspaceBasename ,
422+ projectName,
423+ projectPath,
424+ } ;
425+
426+ // Save to config for next load
427+ workspace . id = metadata . id ;
428+ workspace . name = metadata . name ;
429+ configModified = true ;
430+
431+ workspaceMetadata . push ( metadata ) ;
432+ }
381433 } catch ( error ) {
382434 console . error ( `Failed to load/migrate workspace metadata:` , error ) ;
383435 // Fallback to basic metadata if migration fails
@@ -386,12 +438,17 @@ export class Config {
386438 id : legacyId ,
387439 name : workspaceBasename ,
388440 projectName,
389- projectPath, // Add full project path
441+ projectPath,
390442 } ) ;
391443 }
392444 }
393445 }
394446
447+ // Save config if we migrated any workspaces
448+ if ( configModified ) {
449+ this . saveConfig ( config ) ;
450+ }
451+
395452 return workspaceMetadata ;
396453 }
397454
0 commit comments