@@ -25,10 +25,12 @@ import {
2525 homebrewOwnershipFromCommandPath ,
2626 invalidateLatestProviderVersion ,
2727 makeCachedProviderMaintenanceResolution ,
28+ makeManualOnlyProviderMaintenanceCapabilities ,
2829 makePackageManagedProviderMaintenanceResolver ,
2930 makeProviderMaintenanceCapabilities ,
3031 normalizeCommandPath ,
3132 npmGlobalPrefixFromCommandPath ,
33+ parseChocolateyLatestVersion ,
3234 parseHomebrewLatestVersion ,
3335 resolveLatestProviderVersion ,
3436 resolvePackageManagedProviderMaintenance ,
@@ -381,6 +383,26 @@ it.layer(
381383 } ) ;
382384 } ) ;
383385
386+ it ( "shows the Chocolatey upgrade command without offering to run it" , ( ) => {
387+ expect (
388+ createProviderVersionAdvisory ( {
389+ driver : driver ( "claudeAgent" ) ,
390+ currentVersion : "2.1.304" ,
391+ latestVersion : "2.1.306" ,
392+ maintenanceCapabilities : makeManualOnlyProviderMaintenanceCapabilities ( {
393+ provider : driver ( "claudeAgent" ) ,
394+ packageName : "@anthropic-ai/claude-code" ,
395+ installPath : "C:\\ProgramData\\chocolatey\\bin\\claude.exe" ,
396+ manualCommand : "choco upgrade claude-code" ,
397+ } ) ,
398+ } ) ,
399+ ) . toMatchObject ( {
400+ canUpdate : false ,
401+ canInstallVersion : false ,
402+ updateCommand : "choco upgrade claude-code" ,
403+ } ) ;
404+ } ) ;
405+
384406 it . effect ( "stays manual-only when the binary cannot be located" , ( ) =>
385407 resolveProviderMaintenanceCapabilitiesEffect ( packageToolUpdate , {
386408 binaryPath : "package-tool" ,
@@ -547,12 +569,13 @@ it.layer(
547569 readonly resolved : string ;
548570 readonly real : string ;
549571 readonly platform ?: NodeJS . Platform ;
572+ readonly env ?: NodeJS . ProcessEnv ;
550573 } ) =>
551574 resolvePackageManagedProviderMaintenance ( input . definition , {
552575 binaryPath : "claude" ,
553576 resolvedCommandPath : input . resolved ,
554577 realCommandPath : input . real ,
555- env : { } ,
578+ env : input . env ?? { } ,
556579 platform : input . platform ?? "win32" ,
557580 } ) . pipe (
558581 Effect . provideService ( HostProcess . Platform , input . platform ?? "win32" ) ,
@@ -585,6 +608,135 @@ it.layer(
585608 } ) ,
586609 ) ;
587610
611+ const chocolateyShim = "C:\\ProgramData\\chocolatey\\bin\\claude.exe" ;
612+ const chocolateyPackage = "C:\\ProgramData\\chocolatey\\lib\\claude-code\\tools\\claude.exe" ;
613+
614+ it . effect . each ( [
615+ { name : "the shimgen shim" , resolved : chocolateyShim , real : chocolateyShim } ,
616+ { name : "the package binary" , resolved : chocolateyPackage , real : chocolateyPackage } ,
617+ {
618+ name : "a custom root" ,
619+ resolved : "D:\\Tools\\Choco\\bin\\claude.exe" ,
620+ real : "D:\\Tools\\Choco\\bin\\claude.exe" ,
621+ env : { ChocolateyInstall : "D:\\Tools\\Choco" } ,
622+ } ,
623+ {
624+ name : "a custom root package" ,
625+ resolved : "D:\\Tools\\Choco\\lib\\claude-code\\tools\\claude.exe" ,
626+ real : "D:\\Tools\\Choco\\lib\\claude-code\\tools\\claude.exe" ,
627+ env : { ChocolateyInstall : "D:\\Tools\\Choco" } ,
628+ } ,
629+ {
630+ name : "a mixed-case custom root with a trailing separator" ,
631+ resolved : "D:\\Tools\\Choco\\bin\\Claude.EXE" ,
632+ real : "D:\\Tools\\Choco\\bin\\Claude.EXE" ,
633+ env : { ChocolateyInstall : "d:/tools/choco/" } ,
634+ } ,
635+ { name : "a native-looking alias" , resolved : nativeClaude , real : chocolateyPackage } ,
636+ ] as const ) ( "keeps a Chocolatey Claude install manual-only through $name" , ( fixture ) =>
637+ Effect . gen ( function * ( ) {
638+ const capabilities = yield * resolveInstall ( { definition : claudeUpdate , ...fixture } ) ;
639+
640+ expect ( capabilities . update ) . toBeNull ( ) ;
641+ expect ( capabilities . installPath ) . toBe ( fixture . real ) ;
642+ expect ( capabilities . latestVersion ) . toBeNull ( ) ;
643+ expect ( capabilities . lookupLatestVersion ) . toBeDefined ( ) ;
644+ expect ( capabilities . manualCommand ) . toBe ( "choco upgrade claude-code" ) ;
645+ } ) ,
646+ ) ;
647+
648+ it . effect ( "keeps a Chocolatey-shaped path off Windows on the native updater" , ( ) =>
649+ Effect . gen ( function * ( ) {
650+ const commandPath = "/home/me/chocolatey/bin/claude.exe" ;
651+ const capabilities = yield * resolveInstall ( {
652+ definition : claudeUpdate ,
653+ resolved : commandPath ,
654+ real : commandPath ,
655+ platform : "linux" ,
656+ } ) ;
657+
658+ expect ( capabilities . update ) . toMatchObject ( { lockKey : "claudeAgent-native" } ) ;
659+ } ) ,
660+ ) ;
661+
662+ // Chocolatey publishes behind npm, so comparing against the registry would
663+ // report an update `choco upgrade` cannot install yet.
664+ it . effect ( "compares a Chocolatey Claude install against choco search" , ( ) =>
665+ Effect . gen ( function * ( ) {
666+ const probes : Array < { command : string ; args : ReadonlyArray < string > } > = [ ] ;
667+ const capabilities = yield * resolvePackageManagedProviderMaintenance ( claudeUpdate , {
668+ binaryPath : "claude" ,
669+ resolvedCommandPath : chocolateyShim ,
670+ realCommandPath : chocolateyShim ,
671+ env : { } ,
672+ platform : "win32" ,
673+ } ) . pipe (
674+ Effect . provideService ( HostProcess . Platform , "win32" ) ,
675+ Effect . provideService (
676+ ChildProcessSpawner . ChildProcessSpawner ,
677+ stdoutSpawner ( ( command , args ) => {
678+ probes . push ( { command, args } ) ;
679+ return "claude-code|2.1.304\r\n" ;
680+ } ) ,
681+ ) ,
682+ ) ;
683+ expect ( probes ) . toEqual ( [ ] ) ;
684+
685+ const latest = yield * resolveLatestProviderVersion ( capabilities ) . pipe (
686+ Effect . provide ( ProviderLatestVersions . layer ) ,
687+ Effect . provideService (
688+ HttpClient . HttpClient ,
689+ HttpClient . make ( ( ) => Effect . die ( "the installer's latest should not come from npm" ) ) ,
690+ ) ,
691+ ) ;
692+
693+ expect ( latest ) . toBe ( "2.1.304" ) ;
694+ expect ( probes . map ( ( { command } ) => NodePath . basename ( command ) . replace ( / \. \w + $ / , "" ) ) ) . toEqual (
695+ [ "choco" ] ,
696+ ) ;
697+ expect ( probes [ 0 ] ?. args ) . toEqual ( [ "search" , "claude-code" , "--exact" , "--limit-output" ] ) ;
698+ expect (
699+ createProviderVersionAdvisory ( {
700+ driver : driver ( "claudeAgent" ) ,
701+ currentVersion : "2.1.304" ,
702+ latestVersion : latest ,
703+ maintenanceCapabilities : capabilities ,
704+ } ) ,
705+ ) . toMatchObject ( { status : "current" , canUpdate : false , canInstallVersion : false } ) ;
706+ } ) . pipe ( Effect . provide ( NodeServices . layer ) ) ,
707+ ) ;
708+
709+ it . effect ( "stays unknown when choco search fails instead of asking npm" , ( ) =>
710+ Effect . gen ( function * ( ) {
711+ const capabilities = yield * resolvePackageManagedProviderMaintenance ( claudeUpdate , {
712+ binaryPath : "claude" ,
713+ resolvedCommandPath : chocolateyShim ,
714+ realCommandPath : chocolateyShim ,
715+ env : { } ,
716+ platform : "win32" ,
717+ } ) . pipe (
718+ Effect . provideService ( HostProcess . Platform , "win32" ) ,
719+ Effect . provideService (
720+ ChildProcessSpawner . ChildProcessSpawner ,
721+ stdoutSpawner (
722+ ( ) => "" ,
723+ ( ) => 1 ,
724+ ) ,
725+ ) ,
726+ ) ;
727+
728+ const latest = yield * resolveLatestProviderVersion ( capabilities ) . pipe (
729+ Effect . provide ( ProviderLatestVersions . layer ) ,
730+ Effect . provideService (
731+ HttpClient . HttpClient ,
732+ HttpClient . make ( ( ) => Effect . die ( "a failed choco search should not fall back to npm" ) ) ,
733+ ) ,
734+ ) ;
735+
736+ expect ( latest ) . toBeNull ( ) ;
737+ } ) . pipe ( Effect . provide ( NodeServices . layer ) ) ,
738+ ) ;
739+
588740 it . effect . each ( [
589741 {
590742 name : "a native install" ,
@@ -617,6 +769,12 @@ it.layer(
617769 resolved : userWinGetLink ,
618770 real : userWinGetPackage ,
619771 } ,
772+ {
773+ name : "a claude-code tools path outside Chocolatey" ,
774+ definition : claudeUpdate ,
775+ resolved : "C:\\Work\\lib\\claude-code\\tools\\claude.exe" ,
776+ real : "C:\\Work\\lib\\claude-code\\tools\\claude.exe" ,
777+ } ,
620778 {
621779 name : "another WinGet package" ,
622780 definition : claudeUpdate ,
@@ -1052,6 +1210,13 @@ it.layer(
10521210 } ) ,
10531211 ) ;
10541212
1213+ it ( "reads the Chocolatey package version from choco search --limit-output" , ( ) => {
1214+ expect ( parseChocolateyLatestVersion ( "claude-code|2.1.304\r\n" , "claude-code" ) ) . toBe ( "2.1.304" ) ;
1215+ expect ( parseChocolateyLatestVersion ( "Claude-Code|2.1.304\r\n" , "claude-code" ) ) . toBe ( "2.1.304" ) ;
1216+ expect ( parseChocolateyLatestVersion ( "claude-code-other|9.9.9\r\n" , "claude-code" ) ) . toBeNull ( ) ;
1217+ expect ( parseChocolateyLatestVersion ( "" , "claude-code" ) ) . toBeNull ( ) ;
1218+ } ) ;
1219+
10551220 it ( "recognizes Homebrew kegs and casks from the real executable path" , ( ) => {
10561221 expect (
10571222 homebrewOwnershipFromCommandPath ( "/opt/homebrew/Cellar/claude-code@latest/2.1.0/bin/claude" ) ,
0 commit comments