@@ -6,6 +6,18 @@ import * as path from 'path';
66import { UpdateOptions , Language , RepositoryInfo } from '../types' ;
77import { scanForRepositories , hasMasterSetup , detectMonorepo , scanMonorepoPackages } from '../utils/scanner' ;
88import { getMonorepoToolName } from '../utils/monorepo-helpers' ;
9+ import {
10+ scanTemplateVersions ,
11+ getOutdatedTemplates ,
12+ TemplateStatus ,
13+ getCurrentVersion ,
14+ } from '../utils/template-version' ;
15+ import {
16+ upgradeTemplates ,
17+ getTemplateVarsFromContext ,
18+ formatUpgradeSummary ,
19+ UpgradeResult ,
20+ } from '../utils/template-upgrader' ;
921
1022export async function updateCommand ( options : UpdateOptions ) {
1123 const isDryRun = options . dryRun || false ;
@@ -777,10 +789,168 @@ rm .codesyncer/UPDATE_GUIDE.md
777789 }
778790 }
779791
792+ // Template Upgrade Check
793+ await checkAndOfferTemplateUpgrade ( currentDir , foundRepos , lang , isDryRun ) ;
794+
780795 if ( isDryRun ) {
781796 console . log ( chalk . bold . yellow ( '\n✅ Dry run complete! No files were modified.\n' ) ) ;
782797 console . log ( chalk . gray ( 'Run without --dry-run to apply changes.\n' ) ) ;
783798 } else {
784799 console . log ( chalk . bold . green ( '\n✅ Update complete!\n' ) ) ;
785800 }
786801}
802+
803+ /**
804+ * Check for outdated templates and offer upgrade option
805+ *
806+ * @codesyncer -context Template upgrade feature - checks version metadata in files
807+ * @codesyncer -decision [2026-01-17] Backup files before upgrade for user safety
808+ */
809+ async function checkAndOfferTemplateUpgrade (
810+ currentDir : string ,
811+ repos : RepositoryInfo [ ] ,
812+ lang : Language ,
813+ isDryRun : boolean
814+ ) : Promise < void > {
815+ const currentVersion = getCurrentVersion ( ) ;
816+
817+ // Collect all outdated templates across all repos
818+ const allOutdated : { repo : string ; claudeDir : string ; templates : TemplateStatus [ ] } [ ] = [ ] ;
819+
820+ for ( const repo of repos ) {
821+ const claudeDir = path . join ( repo . path , '.claude' ) ;
822+ if ( await fs . pathExists ( claudeDir ) ) {
823+ const outdated = await getOutdatedTemplates ( claudeDir ) ;
824+ if ( outdated . length > 0 ) {
825+ allOutdated . push ( {
826+ repo : repo . name ,
827+ claudeDir,
828+ templates : outdated ,
829+ } ) ;
830+ }
831+ }
832+ }
833+
834+ // Also check root .claude if it exists (for single repo setup)
835+ const rootClaudeDir = path . join ( currentDir , '.claude' ) ;
836+ if ( await fs . pathExists ( rootClaudeDir ) ) {
837+ const rootOutdated = await getOutdatedTemplates ( rootClaudeDir ) ;
838+ if ( rootOutdated . length > 0 ) {
839+ // Avoid duplicates
840+ const alreadyIncluded = allOutdated . some ( ( o ) => o . claudeDir === rootClaudeDir ) ;
841+ if ( ! alreadyIncluded ) {
842+ allOutdated . push ( {
843+ repo : path . basename ( currentDir ) ,
844+ claudeDir : rootClaudeDir ,
845+ templates : rootOutdated ,
846+ } ) ;
847+ }
848+ }
849+ }
850+
851+ if ( allOutdated . length === 0 ) {
852+ return ; // No outdated templates found
853+ }
854+
855+ // Count total outdated files
856+ const totalOutdated = allOutdated . reduce ( ( sum , o ) => sum + o . templates . length , 0 ) ;
857+
858+ // Display upgrade notification
859+ console . log ( chalk . bold . blue ( '\n📦 ' + ( lang === 'ko' ? '새 버전 감지' : 'New Version Detected' ) + `: v${ currentVersion } \n` ) ) ;
860+
861+ // Show outdated files grouped by repo
862+ allOutdated . forEach ( ( { repo, templates } ) => {
863+ console . log ( chalk . yellow ( ` 📁 ${ repo } /` ) ) ;
864+ templates . forEach ( ( t ) => {
865+ const fileName = path . basename ( t . file ) ;
866+ const versionInfo = t . currentVersion
867+ ? `v${ t . currentVersion } → v${ t . latestVersion } `
868+ : `(${ lang === 'ko' ? '버전 없음' : 'no version' } ) → v${ t . latestVersion } ` ;
869+ console . log ( chalk . gray ( ` • ${ fileName } (${ versionInfo } )` ) ) ;
870+ } ) ;
871+ } ) ;
872+ console . log ( ) ;
873+
874+ // Prompt for upgrade
875+ const { upgradeChoice } = await inquirer . prompt ( [
876+ {
877+ type : 'list' ,
878+ name : 'upgradeChoice' ,
879+ message : lang === 'ko'
880+ ? `${ totalOutdated } 개 템플릿을 업그레이드할까요?`
881+ : `Upgrade ${ totalOutdated } template(s)?` ,
882+ choices : [
883+ {
884+ name : lang === 'ko'
885+ ? '예 - 업그레이드 (기존 파일 .backup으로 백업)'
886+ : 'Yes - Upgrade (backup existing files to .backup)' ,
887+ value : 'yes' ,
888+ } ,
889+ {
890+ name : lang === 'ko' ? '아니오 - 건너뛰기' : 'No - Skip' ,
891+ value : 'no' ,
892+ } ,
893+ {
894+ name : lang === 'ko' ? '미리보기 - 변경 파일만 확인' : 'Preview - Show files only' ,
895+ value : 'preview' ,
896+ } ,
897+ ] ,
898+ default : 'yes' ,
899+ } ,
900+ ] ) ;
901+
902+ if ( upgradeChoice === 'preview' ) {
903+ console . log ( chalk . bold ( '\n' + ( lang === 'ko' ? '📋 업그레이드 대상 파일:' : '📋 Files to upgrade:' ) ) ) ;
904+ allOutdated . forEach ( ( { repo, templates } ) => {
905+ templates . forEach ( ( t ) => {
906+ console . log ( chalk . gray ( ` • ${ repo } /.claude/${ path . basename ( t . file ) } ` ) ) ;
907+ } ) ;
908+ } ) ;
909+ console . log ( ) ;
910+ return ;
911+ }
912+
913+ if ( upgradeChoice === 'no' ) {
914+ console . log ( chalk . gray ( lang === 'ko' ? ' 템플릿 업그레이드 건너뜀\n' : ' Template upgrade skipped\n' ) ) ;
915+ return ;
916+ }
917+
918+ // Perform upgrade
919+ if ( isDryRun ) {
920+ console . log ( chalk . yellow ( lang === 'ko'
921+ ? '\n [DRY RUN] 업그레이드 수행됨 (실제 파일 변경 없음)\n'
922+ : '\n [DRY RUN] Upgrade would be performed (no actual changes)\n' ) ) ;
923+ allOutdated . forEach ( ( { repo, templates } ) => {
924+ templates . forEach ( ( t ) => {
925+ console . log ( chalk . gray ( ` • ${ repo } /.claude/${ path . basename ( t . file ) } ` ) ) ;
926+ } ) ;
927+ } ) ;
928+ return ;
929+ }
930+
931+ const spinner = ora ( lang === 'ko' ? '템플릿 업그레이드 중...' : 'Upgrading templates...' ) . start ( ) ;
932+
933+ const allResults : UpgradeResult [ ] = [ ] ;
934+
935+ for ( const { claudeDir, templates } of allOutdated ) {
936+ const vars = await getTemplateVarsFromContext ( claudeDir , lang ) ;
937+ const results = await upgradeTemplates ( templates , { lang, vars, dryRun : isDryRun } ) ;
938+ allResults . push ( ...results ) ;
939+ }
940+
941+ spinner . succeed ( lang === 'ko' ? '템플릿 업그레이드 완료!' : 'Templates upgraded!' ) ;
942+
943+ // Show summary
944+ console . log ( ) ;
945+ console . log ( formatUpgradeSummary ( allResults , lang ) ) ;
946+ console . log ( ) ;
947+
948+ // Show backup notice
949+ const successfulWithBackup = allResults . filter ( ( r ) => r . success && r . backupPath ) ;
950+ if ( successfulWithBackup . length > 0 ) {
951+ console . log ( chalk . cyan ( lang === 'ko'
952+ ? '💡 백업 파일에서 커스터마이징 내용을 복사할 수 있습니다.'
953+ : '💡 You can copy customizations from backup files.' ) ) ;
954+ console . log ( ) ;
955+ }
956+ }
0 commit comments