diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a2bf2a..97548c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [5.4.2-dev.1](https://github.com/kaitranntt/ccs/compare/v5.4.1...v5.4.2-dev.1) (2025-12-03) + + +### Bug Fixes + +* **merge:** resolve conflicts between dev and main ([8347ea6](https://github.com/kaitranntt/ccs/commit/8347ea64c6b919a79f5ab63c398b6c36f012ca2d)) +* **sync:** implement copy fallback for windows when symlinks unavailable ([6b3f93a](https://github.com/kaitranntt/ccs/commit/6b3f93a80a0232e8c964d73e51aa0afb0768b00f)), closes [#45](https://github.com/kaitranntt/ccs/issues/45) + ## [5.4.1](https://github.com/kaitranntt/ccs/compare/v5.4.0...v5.4.1) (2025-12-03) @@ -5,6 +13,7 @@ * **cliproxy:** resolve windows auth browser not opening ([af4d6cf](https://github.com/kaitranntt/ccs/commit/af4d6cff89395a74e2eaf56551d3f56b95e0a6ce)), closes [#42](https://github.com/kaitranntt/ccs/issues/42) * **doctor:** resolve windows claude cli detection failure ([cfe9ba0](https://github.com/kaitranntt/ccs/commit/cfe9ba05a4351302fbb330ca00b6025cb65a8f20)), closes [#41](https://github.com/kaitranntt/ccs/issues/41) +* **sync:** implement copy fallback for windows when symlinks unavailable ([6b3f93a](https://github.com/kaitranntt/ccs/commit/6b3f93a80a0232e8c964d73e51aa0afb0768b00f)), closes [#45](https://github.com/kaitranntt/ccs/issues/45) ## [5.4.1-dev.1](https://github.com/kaitranntt/ccs/compare/v5.4.0...v5.4.1-dev.1) (2025-12-03) diff --git a/VERSION b/VERSION index ade65226..ae273b6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.4.1 +5.4.2-dev.1 diff --git a/installers/install.ps1 b/installers/install.ps1 index 9df8ae3a..8d09b784 100644 --- a/installers/install.ps1 +++ b/installers/install.ps1 @@ -83,7 +83,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or ( # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (irm | iex) # For git installations, VERSION file is read if available -$CcsVersion = "5.4.1" +$CcsVersion = "5.4.2-dev.1" # Try to read VERSION file for git installations if ($ScriptDir) { diff --git a/installers/install.sh b/installers/install.sh index 0d7e874d..2e981f0a 100755 --- a/installers/install.sh +++ b/installers/install.sh @@ -84,7 +84,7 @@ fi # IMPORTANT: Update this version when releasing new versions! # This hardcoded version is used for standalone installations (curl | bash) # For git installations, VERSION file is read if available -CCS_VERSION="5.4.1" +CCS_VERSION="5.4.2-dev.1" # Try to read VERSION file for git installations if [[ -f "$SCRIPT_DIR/VERSION" ]]; then diff --git a/package.json b/package.json index dcca315f..0dff8516 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "5.4.1", + "version": "5.4.2-dev.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", diff --git a/src/utils/claude-symlink-manager.ts b/src/utils/claude-symlink-manager.ts index 4011d007..29c98260 100644 --- a/src/utils/claude-symlink-manager.ts +++ b/src/utils/claude-symlink-manager.ts @@ -164,12 +164,9 @@ export class ClaudeSymlinkManager { if (!silent) console.log(`[OK] Symlinked ${item.target}`); return true; } catch (err) { - // Windows fallback: stub for now, full implementation in v4.2 + // Windows fallback: copy instead of symlink when symlinks unavailable if (process.platform === 'win32') { - if (!silent) { - console.log(`[!] Symlink failed for ${item.target} (Windows fallback deferred to v4.2)`); - console.log(`[i] Enable Developer Mode or wait for next update`); - } + return this.copyFallback(sourcePath, targetPath, item, silent); } else { const error = err as Error; if (!silent) console.log(`[!] Failed to symlink ${item.target}: ${error.message}`); @@ -178,6 +175,61 @@ export class ClaudeSymlinkManager { } } + /** + * Windows fallback: copy files/directories when symlinks unavailable + * Note: Changes won't auto-sync; user must run 'ccs sync' after updates + */ + private copyFallback( + sourcePath: string, + targetPath: string, + item: CcsItem, + silent = false + ): boolean { + try { + if (item.type === 'directory') { + // Copy directory recursively + this.copyDirRecursive(sourcePath, targetPath); + } else { + // Copy single file + fs.copyFileSync(sourcePath, targetPath); + } + if (!silent) { + console.log(`[OK] Copied ${item.target} (symlink unavailable)`); + console.log(`[i] Run 'ccs sync' after CCS updates to refresh`); + } + return true; + } catch (copyErr) { + const error = copyErr as Error; + if (!silent) { + console.log(`[!] Failed to copy ${item.target}: ${error.message}`); + console.log(`[i] Enable Developer Mode for symlinks, or check permissions`); + } + return false; + } + } + + /** + * Recursively copy directory (for Windows fallback) + */ + private copyDirRecursive(src: string, dest: string): void { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + this.copyDirRecursive(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } + } + /** * Check if target is already the correct symlink pointing to source */ @@ -224,8 +276,8 @@ export class ClaudeSymlinkManager { } /** - * Uninstall CCS items from ~/.claude/ (remove symlinks only) - * Safe: only removes items that are CCS symlinks + * Uninstall CCS items from ~/.claude/ (remove symlinks or copied files) + * Safe: only removes items that are CCS symlinks or valid copies */ uninstall(): void { let removed = 0; @@ -234,10 +286,20 @@ export class ClaudeSymlinkManager { const targetPath = path.join(this.userClaudeDir, item.target); const sourcePath = path.join(this.ccsClaudeDir, item.source); - // Only remove if it's our symlink - if (fs.existsSync(targetPath) && this.isOurSymlink(targetPath, sourcePath)) { + // Check if it's our symlink or a valid copy (Windows fallback) + const isSymlink = this.isOurSymlink(targetPath, sourcePath); + const isCopy = + process.platform === 'win32' && this.isCopiedItem(targetPath, sourcePath, item.type); + + if (fs.existsSync(targetPath) && (isSymlink || isCopy)) { try { - fs.unlinkSync(targetPath); + if (item.type === 'directory' && !isSymlink) { + // Remove copied directory recursively + fs.rmSync(targetPath, { recursive: true, force: true }); + } else { + // Remove symlink or file + fs.unlinkSync(targetPath); + } console.log(`[OK] Removed ${item.target}`); removed++; } catch (err) { @@ -286,14 +348,47 @@ export class ClaudeSymlinkManager { issues.push(`Not installed: ${item.target} (run 'ccs sync' to install)`); healthy = false; } else if (!this.isOurSymlink(targetPath, sourcePath)) { - issues.push(`Not a CCS symlink: ${item.target} (run 'ccs sync' to fix)`); - healthy = false; + // On Windows, copied files are valid (symlink fallback) + if (process.platform === 'win32' && this.isCopiedItem(targetPath, sourcePath, item.type)) { + // Copied file is valid on Windows, but note it's not a symlink + issues.push(`${item.target} is a copy (not symlink) - run 'ccs sync' after updates`); + // Still healthy, just a warning + } else { + issues.push(`Not a CCS symlink: ${item.target} (run 'ccs sync' to fix)`); + healthy = false; + } } } return { healthy, issues }; } + /** + * Check if target is a valid copy of source (Windows fallback check) + */ + private isCopiedItem( + targetPath: string, + sourcePath: string, + type: 'file' | 'directory' + ): boolean { + try { + const targetStats = fs.statSync(targetPath); + const sourceStats = fs.statSync(sourcePath); + + if (type === 'directory') { + // For directories, just check both exist and are directories + return targetStats.isDirectory() && sourceStats.isDirectory(); + } else { + // For files, compare size as basic validation + return ( + targetStats.isFile() && sourceStats.isFile() && targetStats.size === sourceStats.size + ); + } + } catch { + return false; + } + } + /** * Sync delegation commands and skills to ~/.claude/ (used by 'ccs sync' command) * Same as install() but with explicit sync message