@@ -30,7 +30,7 @@ public static Command CreateCommand()
3030
3131 var toolsOnlyOption = new Option < bool > (
3232 "--tools-only" ,
33- "Only update CLI and Bridge tools (skip Unity package)"
33+ "Only update CLI and Bridge tools (skip Unity package and skill )"
3434 ) ;
3535
3636 var packageOnlyOption = new Option < bool > (
@@ -50,11 +50,17 @@ public static Command CreateCommand()
5050 ) ;
5151 yesOption . AddAlias ( "-y" ) ;
5252
53+ var skipSkillOption = new Option < bool > (
54+ "--skip-skill" ,
55+ "Skip Claude Code skill update"
56+ ) ;
57+
5358 updateCommand . AddOption ( checkOption ) ;
5459 updateCommand . AddOption ( toolsOnlyOption ) ;
5560 updateCommand . AddOption ( packageOnlyOption ) ;
5661 updateCommand . AddOption ( versionOption ) ;
5762 updateCommand . AddOption ( yesOption ) ;
63+ updateCommand . AddOption ( skipSkillOption ) ;
5864
5965 updateCommand . SetHandler ( async ( InvocationContext context ) =>
6066 {
@@ -64,9 +70,10 @@ public static Command CreateCommand()
6470 var packageOnly = context . ParseResult . GetValueForOption ( packageOnlyOption ) ;
6571 var version = context . ParseResult . GetValueForOption ( versionOption ) ;
6672 var yes = context . ParseResult . GetValueForOption ( yesOption ) ;
73+ var skipSkill = context . ParseResult . GetValueForOption ( skipSkillOption ) ;
6774 var json = ContextHelper . GetJson ( context ) ;
6875
69- await ExecuteAsync ( projectPath , check , toolsOnly , packageOnly , version , yes , json ) ;
76+ await ExecuteAsync ( projectPath , check , toolsOnly , packageOnly , version , yes , skipSkill , json ) ;
7077 } ) ;
7178
7279 return updateCommand ;
@@ -100,12 +107,6 @@ private enum UpdateResult { Success, Scheduled, Failed }
100107
101108 private static async Task < UpdateResult > RunDotnetToolUpdateAsync ( string packageId , string ? version = null , bool isSelf = false )
102109 {
103- var args = $ "tool update -g { packageId } ";
104- if ( ! string . IsNullOrEmpty ( version ) )
105- {
106- args += $ " --version { version } ";
107- }
108-
109110 // On Windows, updating the currently-running CLI requires a workaround:
110111 // spawn a script that waits for this process to exit, then performs the update
111112 if ( isSelf && OperatingSystem . IsWindows ( ) )
@@ -114,6 +115,12 @@ private static async Task<UpdateResult> RunDotnetToolUpdateAsync(string packageI
114115 return scheduled ? UpdateResult . Scheduled : UpdateResult . Failed ;
115116 }
116117
118+ var args = $ "tool update -g { packageId } ";
119+ if ( ! string . IsNullOrEmpty ( version ) )
120+ {
121+ args += $ " --version { version } ";
122+ }
123+
117124 var psi = new ProcessStartInfo
118125 {
119126 FileName = "dotnet" ,
@@ -129,8 +136,71 @@ private static async Task<UpdateResult> RunDotnetToolUpdateAsync(string packageI
129136 using var process = Process . Start ( psi ) ;
130137 if ( process == null ) return UpdateResult . Failed ;
131138
139+ var stderr = await process . StandardError . ReadToEndAsync ( ) ;
132140 await process . WaitForExitAsync ( ) ;
133- return process . ExitCode == 0 ? UpdateResult . Success : UpdateResult . Failed ;
141+
142+ if ( process . ExitCode == 0 )
143+ return UpdateResult . Success ;
144+
145+ // If a specific version was requested and update failed, try uninstall+install
146+ // (dotnet tool update rejects versions lower than current)
147+ if ( ! string . IsNullOrEmpty ( version ) )
148+ {
149+ return await UninstallAndInstallAsync ( packageId , version ) ;
150+ }
151+
152+ return UpdateResult . Failed ;
153+ }
154+ catch
155+ {
156+ return UpdateResult . Failed ;
157+ }
158+ }
159+
160+ private static async Task < UpdateResult > UninstallAndInstallAsync ( string packageId , string version )
161+ {
162+ // Uninstall
163+ var uninstallPsi = new ProcessStartInfo
164+ {
165+ FileName = "dotnet" ,
166+ Arguments = $ "tool uninstall -g { packageId } ",
167+ UseShellExecute = false ,
168+ RedirectStandardOutput = true ,
169+ RedirectStandardError = true ,
170+ CreateNoWindow = true
171+ } ;
172+
173+ try
174+ {
175+ using var uninstallProcess = Process . Start ( uninstallPsi ) ;
176+ if ( uninstallProcess == null ) return UpdateResult . Failed ;
177+
178+ await uninstallProcess . WaitForExitAsync ( ) ;
179+ if ( uninstallProcess . ExitCode != 0 ) return UpdateResult . Failed ;
180+ }
181+ catch
182+ {
183+ return UpdateResult . Failed ;
184+ }
185+
186+ // Install specific version
187+ var installPsi = new ProcessStartInfo
188+ {
189+ FileName = "dotnet" ,
190+ Arguments = $ "tool install -g { packageId } --version { version } ",
191+ UseShellExecute = false ,
192+ RedirectStandardOutput = true ,
193+ RedirectStandardError = true ,
194+ CreateNoWindow = true
195+ } ;
196+
197+ try
198+ {
199+ using var installProcess = Process . Start ( installPsi ) ;
200+ if ( installProcess == null ) return UpdateResult . Failed ;
201+
202+ await installProcess . WaitForExitAsync ( ) ;
203+ return installProcess . ExitCode == 0 ? UpdateResult . Success : UpdateResult . Failed ;
134204 }
135205 catch
136206 {
@@ -150,7 +220,7 @@ private static async Task<bool> ScheduleSelfUpdateViaScriptAsync(string packageI
150220
151221 // Create a temporary PowerShell script that:
152222 // 1. Waits for the current process to exit
153- // 2. Runs the dotnet tool update command
223+ // 2. Runs the dotnet tool update command (with uninstall+install fallback for downgrades)
154224 // 3. Deletes itself
155225 var tempDir = Path . GetTempPath ( ) ;
156226 var scriptPath = Path . Combine ( tempDir , $ "unityctl-update-{ Guid . NewGuid ( ) : N} .ps1") ;
@@ -170,8 +240,13 @@ private static async Task<bool> ScheduleSelfUpdateViaScriptAsync(string packageI
170240 }}
171241}}
172242
173- # Run the update
243+ # Try update first
174244dotnet tool update -g { packageId } { versionArg } 2>&1 | Out-Null
245+ if ($LASTEXITCODE -ne 0 -and '{ version } ' -ne '') {{
246+ # Fallback to uninstall+install for downgrades
247+ dotnet tool uninstall -g { packageId } 2>&1 | Out-Null
248+ dotnet tool install -g { packageId } { versionArg } 2>&1 | Out-Null
249+ }}
175250
176251# Clean up this script
177252Remove-Item -Path $MyInvocation.MyCommand.Path -Force
@@ -217,6 +292,7 @@ private static async Task ExecuteAsync(
217292 bool packageOnly ,
218293 string ? targetVersion ,
219294 bool yes ,
295+ bool skipSkill ,
220296 bool json )
221297 {
222298 var currentVersion = VersionInfo . Version ;
@@ -241,6 +317,7 @@ private static async Task ExecuteAsync(
241317
242318 var isUpToDate = currentParsed >= targetParsed && targetVersion == null ;
243319
320+ // Display version info and handle --check
244321 if ( json )
245322 {
246323 Console . WriteLine ( JsonHelper . Serialize ( new
@@ -252,7 +329,7 @@ private static async Task ExecuteAsync(
252329 checkOnly
253330 } ) ) ;
254331
255- if ( checkOnly || isUpToDate )
332+ if ( checkOnly )
256333 return ;
257334 }
258335 else
@@ -267,22 +344,29 @@ private static async Task ExecuteAsync(
267344
268345 Console . WriteLine ( ) ;
269346
270- if ( isUpToDate )
347+ if ( checkOnly )
271348 {
272- Console . WriteLine ( "You are already running the latest version." ) ;
349+ if ( isUpToDate )
350+ Console . WriteLine ( "You are already running the latest version." ) ;
351+ else
352+ {
353+ Console . WriteLine ( $ "Update available: { currentVersion } -> { effectiveTargetVersion } ") ;
354+ Console . WriteLine ( "Run 'unityctl update' to install the update." ) ;
355+ }
273356 return ;
274357 }
275358
276- if ( checkOnly )
359+ if ( isUpToDate )
277360 {
278- Console . WriteLine ( $ "Update available: { currentVersion } -> { effectiveTargetVersion } ") ;
279- Console . WriteLine ( "Run 'unityctl update' to install the update." ) ;
280- return ;
361+ Console . WriteLine ( "CLI and Bridge are already up to date." ) ;
362+ if ( toolsOnly )
363+ return ;
364+ Console . WriteLine ( ) ;
281365 }
282366 }
283367
284- // Confirm update
285- if ( ! yes && ! json )
368+ // Confirm update (only when tools will be updated)
369+ if ( ! yes && ! json && ! isUpToDate )
286370 {
287371 Console . Write ( $ "Update to version { effectiveTargetVersion } ? [y/N]: ") ;
288372 var response = Console . ReadLine ( ) ? . Trim ( ) . ToLowerInvariant ( ) ;
@@ -297,26 +381,40 @@ private static async Task ExecuteAsync(
297381 var results = new List < UpdateStepResult > ( ) ;
298382
299383 // Update CLI and Bridge tools
300- if ( ! packageOnly )
384+ if ( ! packageOnly && ! isUpToDate )
301385 {
302386 Console . WriteLine ( "Updating CLI tools..." ) ;
303387 Console . WriteLine ( ) ;
304388
305- // Check if bridge is running and stop it
306- var bridgeWasRunning = false ;
389+ // Save current project's bridge config for restart later
307390 var projectRoot = projectPath ?? ProjectLocator . FindProjectRoot ( ) ;
308391 BridgeConfig ? bridgeConfig = null ;
309392
310393 if ( projectRoot != null )
311394 {
312395 bridgeConfig = ProjectLocator . ReadBridgeConfig ( projectRoot ) ;
313- if ( bridgeConfig != null )
396+ }
397+
398+ // Kill ALL bridge processes to release DLL locks
399+ var bridgeProcesses = Process . GetProcessesByName ( "unityctl-bridge" ) ;
400+ if ( bridgeProcesses . Length > 0 )
401+ {
402+ Console . WriteLine ( $ " Stopping { bridgeProcesses . Length } bridge process(es)...") ;
403+ foreach ( var proc in bridgeProcesses )
314404 {
315- Console . WriteLine ( " Stopping bridge daemon..." ) ;
316- await BridgeClient . StopBridgeAsync ( projectPath ) ;
317- bridgeWasRunning = true ;
318- // Give it a moment to shut down
319- await Task . Delay ( 1000 ) ;
405+ try { proc . Kill ( true ) ; } catch { }
406+ proc . Dispose ( ) ;
407+ }
408+
409+ // Wait for processes to fully exit (max 10s) to release DLL locks
410+ var sw = Stopwatch . StartNew ( ) ;
411+ while ( sw . Elapsed < TimeSpan . FromSeconds ( 10 ) )
412+ {
413+ var remaining = Process . GetProcessesByName ( "unityctl-bridge" ) ;
414+ var anyLeft = remaining . Length > 0 ;
415+ foreach ( var p in remaining ) p . Dispose ( ) ;
416+ if ( ! anyLeft ) break ;
417+ await Task . Delay ( 500 ) ;
320418 }
321419 }
322420
@@ -352,8 +450,8 @@ private static async Task ExecuteAsync(
352450 } ) ;
353451 Console . WriteLine ( bridgeSuccess ? $ " Updated { BridgePackageId } " : $ " Failed to update { BridgePackageId } ") ;
354452
355- // Restart bridge if it was running
356- if ( bridgeWasRunning && bridgeSuccess )
453+ // Restart bridge if it was running for the current project
454+ if ( bridgeConfig != null && bridgeSuccess )
357455 {
358456 Console . WriteLine ( " Restarting bridge daemon..." ) ;
359457 await BridgeClient . StartBridgeAsync ( projectPath ) ;
@@ -403,7 +501,74 @@ private static async Task ExecuteAsync(
403501 Console . WriteLine ( ) ;
404502 }
405503
406- // Summary
504+ // Update Claude Code skill
505+ if ( ! toolsOnly && ! packageOnly && ! skipSkill )
506+ {
507+ var home = Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ;
508+ var globalSkillPath = Path . Combine ( home , ".claude" , "skills" , "unity-editor" , "SKILL.md" ) ;
509+ var localSkillPath = Path . Combine ( Directory . GetCurrentDirectory ( ) , ".claude" , "skills" , "unity-editor" , "SKILL.md" ) ;
510+
511+ var globalExists = File . Exists ( globalSkillPath ) ;
512+ var localExists = File . Exists ( localSkillPath ) ;
513+
514+ if ( globalExists || localExists )
515+ {
516+ Console . WriteLine ( "Updating Claude Code skill..." ) ;
517+
518+ try
519+ {
520+ var updated = new List < string > ( ) ;
521+
522+ if ( globalExists )
523+ {
524+ if ( await SkillCommands . UpdateSkillFileAsync ( globalSkillPath ) )
525+ updated . Add ( "global" ) ;
526+ }
527+
528+ if ( localExists )
529+ {
530+ if ( await SkillCommands . UpdateSkillFileAsync ( localSkillPath ) )
531+ updated . Add ( "local" ) ;
532+ }
533+
534+ if ( updated . Count > 0 )
535+ {
536+ Console . WriteLine ( $ " Updated skill ({ string . Join ( " + " , updated ) } )") ;
537+ results . Add ( new UpdateStepResult
538+ {
539+ Step = "Claude Code Skill" ,
540+ Success = true
541+ } ) ;
542+ }
543+ else
544+ {
545+ Console . Error . WriteLine ( " Could not find embedded skill content" ) ;
546+ results . Add ( new UpdateStepResult
547+ {
548+ Step = "Claude Code Skill" ,
549+ Success = false ,
550+ Error = "Embedded SKILL.md resource not found"
551+ } ) ;
552+ }
553+ }
554+ catch ( Exception ex )
555+ {
556+ results . Add ( new UpdateStepResult
557+ {
558+ Step = "Claude Code Skill" ,
559+ Success = false ,
560+ Error = ex . Message
561+ } ) ;
562+ }
563+
564+ Console . WriteLine ( ) ;
565+ }
566+ }
567+
568+ // Summary (only if any steps ran)
569+ if ( results . Count == 0 )
570+ return ;
571+
407572 if ( ! json )
408573 {
409574 Console . WriteLine ( "========================================" ) ;
0 commit comments