Skip to content

Commit e45d0d3

Browse files
committed
fix: merge legacy data into new workspace dir
1 parent d4fc834 commit e45d0d3

4 files changed

Lines changed: 178 additions & 1 deletion

File tree

CODEBASE_DOCUMENTATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ server/utils/processUtils.js - Shared spawn/env hardening helpers
8484
server/utils/nodePtyCompat.js - Runtime compatibility shim for the bundled `node-pty` Windows ConPTY loader
8585
└─ Windows PTY guard: wraps the known-bad `node-pty@1.2.0-beta.12` `conpty.startProcess()` call in memory so packaged installs survive read-only app-resource layouts
8686
server/utils/pathUtils.js - Shared slash-normalization + data-directory compatibility helpers for repo/worktree labels
87-
└─ Legacy fallback: prefers `~/.orchestrator` over `~/.agent-workspace` when the renamed directory looks sparse but the legacy directory still contains the richer user workspace state
87+
└─ Legacy migration: renames `~/.orchestrator` when possible, otherwise merges richer legacy state into `~/.agent-workspace` with conflict backups before falling back to the old directory
8888
server/tokenCounter.js - Token usage tracking (if applicable)
8989
server/userSettingsService.js - User preferences and settings management
9090
server/sessionRecoveryService.js - Session recovery state persistence (CWD, agents, conversations)

server/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ const winston = require('winston');
1010
const { augmentProcessEnv, getHiddenProcessOptions } = require('./utils/processUtils');
1111
const {
1212
migrateFromOrchestratorDir,
13+
mergeLegacyDataDir,
1314
bootstrapProjectsRoot,
1415
getAgentWorkspaceDir,
1516
getLegacyCompatibilityState
1617
} = require('./utils/pathUtils');
1718

1819
const migratedDataDir = migrateFromOrchestratorDir();
20+
const mergedLegacyDataDir = mergeLegacyDataDir();
1921
const legacyCompatibilityState = getLegacyCompatibilityState();
2022
const resolvedDataDir = getAgentWorkspaceDir();
2123
const projectsRootBootstrap = bootstrapProjectsRoot();
@@ -57,6 +59,16 @@ const logger = winston.createLogger({
5759
if (migratedDataDir) {
5860
logger.info('Migrated data directory from ~/.orchestrator to ~/.agent-workspace');
5961
}
62+
if (mergedLegacyDataDir.merged) {
63+
logger.info('Merged legacy ~/.orchestrator data into ~/.agent-workspace', {
64+
reason: mergedLegacyDataDir.reason,
65+
sourceDir: mergedLegacyDataDir.sourceDir,
66+
targetDir: mergedLegacyDataDir.targetDir,
67+
backupDir: mergedLegacyDataDir.backupDir,
68+
copiedCount: mergedLegacyDataDir.copied.length,
69+
overwrittenCount: mergedLegacyDataDir.overwritten.length
70+
});
71+
}
6072
if (legacyCompatibilityState.shouldUseLegacyDir) {
6173
logger.warn('Using legacy ~/.orchestrator data directory for backward compatibility', {
6274
reason: legacyCompatibilityState.reason,

server/utils/pathUtils.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,129 @@ function getWorkspaceEntryCount(baseDir) {
3131
return countVisibleEntries(path.join(baseDir, 'workspaces'));
3232
}
3333

34+
function isSameFileContent(sourcePath, targetPath) {
35+
const fs = require('fs');
36+
try {
37+
const sourceStat = fs.statSync(sourcePath);
38+
const targetStat = fs.statSync(targetPath);
39+
if (sourceStat.size !== targetStat.size) return false;
40+
const sourceContent = fs.readFileSync(sourcePath);
41+
const targetContent = fs.readFileSync(targetPath);
42+
return sourceContent.equals(targetContent);
43+
} catch {
44+
return false;
45+
}
46+
}
47+
48+
function copyTreeSync(sourcePath, targetPath) {
49+
const fs = require('fs');
50+
const stat = fs.lstatSync(sourcePath);
51+
if (stat.isDirectory()) {
52+
fs.mkdirSync(targetPath, { recursive: true });
53+
for (const entry of fs.readdirSync(sourcePath)) {
54+
copyTreeSync(path.join(sourcePath, entry), path.join(targetPath, entry));
55+
}
56+
return;
57+
}
58+
59+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
60+
fs.copyFileSync(sourcePath, targetPath);
61+
}
62+
63+
function mergeLegacyDataDir() {
64+
const fs = require('fs');
65+
66+
// Skip if env override is set (user chose a custom path)
67+
if (String(process.env.AGENT_WORKSPACE_DIR || '').trim()) {
68+
return { merged: false, reason: 'env-override' };
69+
}
70+
71+
const state = getLegacyCompatibilityState();
72+
if (!state.shouldUseLegacyDir) {
73+
return {
74+
merged: false,
75+
reason: state.reason,
76+
sourceDir: state.oldDir,
77+
targetDir: state.newDir
78+
};
79+
}
80+
81+
const sourceDir = state.oldDir;
82+
const targetDir = state.newDir;
83+
const backupRoot = path.join(
84+
targetDir,
85+
'migration-backups',
86+
`from-orchestrator-${new Date().toISOString().replace(/[:.]/g, '-')}`
87+
);
88+
const report = {
89+
merged: false,
90+
reason: state.reason,
91+
sourceDir,
92+
targetDir,
93+
backupDir: backupRoot,
94+
copied: [],
95+
overwritten: []
96+
};
97+
98+
if (!fs.existsSync(sourceDir)) {
99+
return { ...report, merged: false, reason: 'legacy-missing' };
100+
}
101+
102+
fs.mkdirSync(targetDir, { recursive: true });
103+
104+
const mergeEntry = (srcPath, dstPath, relPath = '') => {
105+
const srcStat = fs.lstatSync(srcPath);
106+
const dstExists = fs.existsSync(dstPath);
107+
108+
if (srcStat.isDirectory()) {
109+
if (!dstExists) {
110+
copyTreeSync(srcPath, dstPath);
111+
report.copied.push(relPath || path.basename(srcPath));
112+
return;
113+
}
114+
115+
const dstStat = fs.lstatSync(dstPath);
116+
if (!dstStat.isDirectory()) {
117+
const backupPath = path.join(backupRoot, relPath);
118+
copyTreeSync(dstPath, backupPath);
119+
report.overwritten.push(relPath);
120+
fs.rmSync(dstPath, { recursive: true, force: true });
121+
copyTreeSync(srcPath, dstPath);
122+
return;
123+
}
124+
125+
for (const entry of fs.readdirSync(srcPath)) {
126+
const nextRel = relPath ? path.join(relPath, entry) : entry;
127+
mergeEntry(path.join(srcPath, entry), path.join(dstPath, entry), nextRel);
128+
}
129+
return;
130+
}
131+
132+
if (!dstExists) {
133+
copyTreeSync(srcPath, dstPath);
134+
report.copied.push(relPath || path.basename(srcPath));
135+
return;
136+
}
137+
138+
const dstStat = fs.lstatSync(dstPath);
139+
if (dstStat.isDirectory() || !isSameFileContent(srcPath, dstPath)) {
140+
const backupPath = path.join(backupRoot, relPath);
141+
copyTreeSync(dstPath, backupPath);
142+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
143+
fs.copyFileSync(srcPath, dstPath);
144+
report.overwritten.push(relPath || path.basename(srcPath));
145+
}
146+
};
147+
148+
for (const entry of fs.readdirSync(sourceDir)) {
149+
if (entry === 'migration-backups') continue;
150+
mergeEntry(path.join(sourceDir, entry), path.join(targetDir, entry), entry);
151+
}
152+
153+
report.merged = report.copied.length > 0 || report.overwritten.length > 0;
154+
return report;
155+
}
156+
34157
function getLegacyCompatibilityState() {
35158
const fs = require('fs');
36159
const newDir = getDefaultAgentWorkspaceDir();
@@ -275,6 +398,7 @@ module.exports = {
275398
getDefaultAgentWorkspaceDir,
276399
getLegacyAgentWorkspaceDir,
277400
getLegacyCompatibilityState,
401+
mergeLegacyDataDir,
278402
getProjectsRoot,
279403
getLegacyProjectsRoot,
280404
migrateFromOrchestratorDir,

tests/unit/pathUtils.test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,45 @@ describe('pathUtils', () => {
126126
newWorkspaceCount: 2
127127
});
128128
});
129+
130+
test('mergeLegacyDataDir copies legacy data into the new directory and backs up conflicts', () => {
131+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'path-utils-home-'));
132+
fs.mkdirSync(path.join(tmpHome, '.agent-workspace', 'workspaces'), { recursive: true });
133+
fs.mkdirSync(path.join(tmpHome, '.orchestrator', 'workspaces'), { recursive: true });
134+
fs.writeFileSync(path.join(tmpHome, '.agent-workspace', 'workspaces', 'workspace-1.json'), '{"name":"new"}');
135+
fs.writeFileSync(path.join(tmpHome, '.orchestrator', 'workspaces', 'workspace-1.json'), '{"name":"old"}');
136+
fs.writeFileSync(path.join(tmpHome, '.orchestrator', 'workspaces', 'workspace-2.json'), '{"name":"legacy-only"}');
137+
fs.writeFileSync(path.join(tmpHome, '.orchestrator', 'quick-links.json'), '{"links":[1]}');
138+
139+
const {
140+
mergeLegacyDataDir,
141+
getAgentWorkspaceDir,
142+
getLegacyCompatibilityState
143+
} = loadPathUtils(tmpHome);
144+
145+
const result = mergeLegacyDataDir();
146+
147+
expect(result.merged).toBe(true);
148+
expect(result.overwritten).toContain(path.join('workspaces', 'workspace-1.json'));
149+
expect(result.copied).toContain(path.join('workspaces', 'workspace-2.json'));
150+
expect(result.copied).toContain('quick-links.json');
151+
expect(fs.readFileSync(path.join(tmpHome, '.agent-workspace', 'workspaces', 'workspace-1.json'), 'utf8')).toBe('{"name":"old"}');
152+
expect(fs.readFileSync(path.join(tmpHome, '.agent-workspace', 'workspaces', 'workspace-2.json'), 'utf8')).toBe('{"name":"legacy-only"}');
153+
expect(fs.readFileSync(path.join(tmpHome, '.agent-workspace', 'quick-links.json'), 'utf8')).toBe('{"links":[1]}');
154+
155+
const backupRoot = path.join(tmpHome, '.agent-workspace', 'migration-backups');
156+
const backupDirs = fs.readdirSync(backupRoot);
157+
expect(backupDirs.length).toBe(1);
158+
expect(
159+
fs.readFileSync(
160+
path.join(backupRoot, backupDirs[0], 'workspaces', 'workspace-1.json'),
161+
'utf8'
162+
)
163+
).toBe('{"name":"new"}');
164+
165+
expect(getLegacyCompatibilityState()).toMatchObject({
166+
shouldUseLegacyDir: false
167+
});
168+
expect(getAgentWorkspaceDir()).toBe(path.join(tmpHome, '.agent-workspace'));
169+
});
129170
});

0 commit comments

Comments
 (0)