migrateToExternalState() moved .gsd/ to ~/.gsd/projects/<hash>/ and created a symlink, but never verified the symlink resolved correctly. On Windows, junction creation can silently fail or resolve to the wrong target. If the symlink was broken, the backup (.gsd.migrating) was deleted anyway, losing all project state. Changes: - migrate-external.ts: After creating symlink, verify it resolves to the expected path and is readable. If verification fails, restore from backup. - repo-identity-worktree.test.ts: Canonicalize temp dirs with realpathSync to fix macOS /var → /private/var mismatch in path assertions. - resource-loader.ts: Check for agents/ subdir before using dist/resources as source — partial builds (tsc without copy-resources) create an incomplete dist/resources that's missing agents/ and skills/. Fixes #1377
This commit is contained in:
parent
bdeec039c0
commit
b720e7e15c
2 changed files with 27 additions and 2 deletions
|
|
@ -19,6 +19,9 @@ import { loadRegistry, readManifestFromEntryPath, isExtensionEnabled, ensureRegi
|
|||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const distResources = join(packageRoot, 'dist', 'resources')
|
||||
const srcResources = join(packageRoot, 'src', 'resources')
|
||||
// Use dist/resources only if it has the full expected structure.
|
||||
// A partial build (tsc without copy-resources) creates dist/resources/extensions/
|
||||
// but not agents/ or skills/, causing initResources to sync from an incomplete source.
|
||||
const resourcesDir = (existsSync(distResources) && existsSync(join(distResources, 'agents')))
|
||||
? distResources
|
||||
: srcResources
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* symlink replaces the original directory so all paths remain valid.
|
||||
*/
|
||||
|
||||
import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
||||
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { externalGsdRoot } from "./repo-identity.js";
|
||||
import { getErrorMessage } from "./error-utils.js";
|
||||
|
|
@ -99,7 +99,29 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|||
// Create symlink .gsd -> external path
|
||||
symlinkSync(externalPath, localGsd, "junction");
|
||||
|
||||
// Remove .gsd.migrating
|
||||
// Verify the symlink resolves correctly before removing the backup (#1377).
|
||||
// On Windows, junction creation can silently succeed but resolve to the wrong
|
||||
// target, or the external dir may not be accessible. If verification fails,
|
||||
// restore from the backup.
|
||||
try {
|
||||
const resolved = realpathSync(localGsd);
|
||||
const resolvedExternal = realpathSync(externalPath);
|
||||
if (resolved !== resolvedExternal) {
|
||||
// Symlink points to wrong target — restore backup
|
||||
try { rmSync(localGsd, { force: true }); } catch { /* may not exist */ }
|
||||
renameSync(migratingPath, localGsd);
|
||||
return { migrated: false, error: `Migration verification failed: symlink resolves to ${resolved}, expected ${resolvedExternal}` };
|
||||
}
|
||||
// Verify we can read through the symlink
|
||||
readdirSync(localGsd);
|
||||
} catch (verifyErr) {
|
||||
// Symlink broken or unreadable — restore backup
|
||||
try { rmSync(localGsd, { force: true }); } catch { /* may not exist */ }
|
||||
try { renameSync(migratingPath, localGsd); } catch { /* best-effort restore */ }
|
||||
return { migrated: false, error: `Migration verification failed: ${getErrorMessage(verifyErr)}` };
|
||||
}
|
||||
|
||||
// Remove .gsd.migrating only after symlink is verified
|
||||
rmSync(migratingPath, { recursive: true, force: true });
|
||||
|
||||
return { migrated: true };
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue