@@ -19,9 +19,67 @@ import (
1919)
2020
2121type installOptions struct {
22+ root string
23+ quiet bool
2224 toInstall []string
2325}
2426
27+ // EnsureInstalled installs or updates configured tools when their installed
28+ // symlink version does not match the repository configuration.
29+ func EnsureInstalled (root string , apps ... string ) error {
30+ return ensureInstalled (root , false , apps ... )
31+ }
32+
33+ // EnsureInstalledQuiet is like EnsureInstalled, but suppresses installer
34+ // progress output. This is useful when stdout has a protocol-level meaning.
35+ func EnsureInstalledQuiet (root string , apps ... string ) error {
36+ return ensureInstalled (root , true , apps ... )
37+ }
38+
39+ func ensureInstalled (root string , quiet bool , apps ... string ) error {
40+ o := & installOptions {root : root , quiet : quiet }
41+
42+ currentTools , err := (& listOptions {root : root }).getCurrentToolsMap ()
43+ if err != nil {
44+ return ee .Wrap (err , "cannot load current tools" )
45+ }
46+
47+ requiredTools := currentTools
48+ if len (apps ) > 0 {
49+ requiredTools = make (map [string ]string , len (apps ))
50+ for _ , app := range apps {
51+ if version , ok := currentTools [app ]; ok {
52+ requiredTools [app ] = version
53+ }
54+ }
55+ }
56+ if len (requiredTools ) == 0 {
57+ return nil
58+ }
59+
60+ installedTools , err := (& listOptions {root : root }).getInstalledTools ()
61+ if err != nil {
62+ return ee .Wrap (err , "cannot load installed tools" )
63+ }
64+
65+ toInstallOrUpdate := getToolsToInstallOrUpdate (requiredTools , installedTools )
66+ if len (toInstallOrUpdate ) == 0 {
67+ return nil
68+ }
69+
70+ results , err := o .installTools (currentTools , toInstallOrUpdate )
71+ if err != nil {
72+ return err
73+ }
74+
75+ newToolsInfo := cloneToolsMap (currentTools )
76+ for _ , app := range toInstallOrUpdate {
77+ newToolsInfo [app ] = results [app ].version
78+ }
79+
80+ return o .writeToolsInfo (newToolsInfo )
81+ }
82+
2583func InstallCommand () * cobra.Command {
2684 o := & installOptions {}
2785 cmd := & cobra.Command {
@@ -44,7 +102,7 @@ func InstallCommand() *cobra.Command {
44102
45103func (o * installOptions ) install () error {
46104 // load tools from config
47- currentTools , err := (& listOptions {}).getCurrentToolsMap ()
105+ currentTools , err := (& listOptions {root : o . root }).getCurrentToolsMap ()
48106 if err != nil {
49107 return ee .Wrap (err , "cannot load current tools" )
50108 }
@@ -59,15 +117,55 @@ func (o *installOptions) install() error {
59117 }
60118
61119 // load installed tools
62- installedTools , err := (& listOptions {}).getInstalledTools ()
120+ installedTools , err := (& listOptions {root : o . root }).getInstalledTools ()
63121 if err != nil {
64122 return ee .Wrap (err , "cannot load installed tools" )
65123 }
66124
67125 // diff
126+ var toRemove []string
127+ for app := range installedTools {
128+ if _ , ok := currentTools [app ]; ! ok {
129+ toRemove = append (toRemove , app )
130+ }
131+ }
132+
133+ // install needed (toInstall + toUpdate)
134+ toInstallOrUpdate := getToolsToInstallOrUpdate (currentTools , installedTools )
135+ results , err := o .installTools (currentTools , toInstallOrUpdate )
136+ if err != nil {
137+ return err
138+ }
139+
140+ // remove unneeded (toRemove)
141+ for _ , app := range toRemove {
142+ pp .BluePrintln (">>> Remove" , app )
143+
144+ // remove from .bin
145+ _ = os .Remove (filepath .Join (o .root , ".kitty" , ".bin" , app ))
146+ }
147+
148+ // generate new tools info
149+ newToolsInfo := make (map [string ]string , len (currentTools ))
150+ for app , version := range currentTools {
151+ newToolsInfo [app ] = version
152+ }
153+ for _ , app := range toInstallOrUpdate {
154+ newToolsInfo [app ] = results [app ].version
155+ }
156+
157+ // save tools info
158+ err = o .writeToolsInfo (newToolsInfo )
159+ if err != nil {
160+ return ee .Wrap (err , "cannot save tools info" )
161+ }
162+
163+ return nil
164+ }
165+
166+ func getToolsToInstallOrUpdate (currentTools map [string ]string , installedTools map [string ]string ) []string {
68167 var toInstall []string
69168 var toUpdate []string
70- var toRemove []string
71169
72170 for app , version := range currentTools {
73171 if version == "-" { // ignore
@@ -82,66 +180,44 @@ func (o *installOptions) install() error {
82180 toInstall = append (toInstall , app )
83181 }
84182 }
85- for app := range installedTools {
86- if _ , ok := currentTools [app ]; ! ok {
87- toRemove = append (toRemove , app )
88- }
89- }
90183
91- // install needed (toInstall + toUpdate)
92184 toInstallOrUpdate := mr .Flats (toInstall , toUpdate )
93185 sort .Strings (toInstallOrUpdate )
186+ return toInstallOrUpdate
187+ }
188+
189+ func (o * installOptions ) installTools (currentTools map [string ]string , toInstallOrUpdate []string ) (map [string ]* installResult , error ) {
94190 var installFailedApps []string
95191 results := make (map [string ]* installResult , len (toInstallOrUpdate ))
96192 for _ , app := range toInstallOrUpdate {
97193 version := currentTools [app ]
98194
99- pp .BluePrintln (">>> Install" , app )
195+ if ! o .quiet {
196+ pp .BluePrintln (">>> Install" , app )
197+ }
100198
101- o := & installOneOptions {app : app , version : version }
199+ one := & installOneOptions {root : o . root , quiet : o . quiet , app : app , version : version }
102200
103- result , err := o .install ()
201+ result , err := one .install ()
104202 if err != nil {
105- pp .RedPrintln ("ERROR:" , err .Error ())
203+ if ! o .quiet {
204+ pp .RedPrintln ("ERROR:" , err .Error ())
205+ }
106206 installFailedApps = append (installFailedApps , app )
107207 continue
108208 }
109209 results [app ] = result
110210 }
111211
112- // remove unneeded (toRemove)
113- for _ , app := range toRemove {
114- pp .BluePrintln (">>> Remove" , app )
115-
116- // remove from .bin
117- _ = os .Remove (filepath .Join (".kitty" , ".bin" , app ))
118- }
119-
120- // report error
121212 if len (installFailedApps ) > 0 {
122- return ee .Errorf ("failed to install %s" , strings .Join (installFailedApps , ", " ))
213+ return nil , ee .Errorf ("failed to install %s" , strings .Join (installFailedApps , ", " ))
123214 }
124215
125- // generate new tools info
126- newToolsInfo := make (map [string ]string , len (currentTools ))
127- for app , version := range currentTools {
128- newToolsInfo [app ] = version
129- }
130- for _ , app := range toInstallOrUpdate {
131- newToolsInfo [app ] = results [app ].version
132- }
133-
134- // save tools info
135- err = o .writeToolsInfo (newToolsInfo )
136- if err != nil {
137- return ee .Wrap (err , "cannot save tools info" )
138- }
139-
140- return nil
216+ return results , nil
141217}
142218
143219func (o * installOptions ) writeToolsInfo (tools map [string ]string ) error {
144- return config .PatchKittyConfig ("" , func (c map [string ]gson.JSON ) (save bool , err error ) {
220+ return config .PatchKittyConfig (o . root , func (c map [string ]gson.JSON ) (save bool , err error ) {
145221 // if c["tools"] not exist and tools is empty, do nothing
146222 if _ , toolsKeyExist := c ["tools" ]; ! toolsKeyExist && len (tools ) == 0 {
147223 return false , nil
@@ -153,6 +229,8 @@ func (o *installOptions) writeToolsInfo(tools map[string]string) error {
153229}
154230
155231type installOneOptions struct {
232+ root string
233+ quiet bool
156234 app string
157235 version string
158236}
@@ -168,6 +246,18 @@ func (o *installOneOptions) install() (*installResult, error) {
168246 if o .version == "" {
169247 o .version = "latest"
170248 }
249+ if o .root != "" {
250+ previousWd , err := os .Getwd ()
251+ if err != nil {
252+ return nil , ee .Wrap (err , "cannot get working directory" )
253+ }
254+ if err := os .Chdir (o .root ); err != nil {
255+ return nil , ee .Wrapf (err , "cannot change working directory to %s" , o .root )
256+ }
257+ defer func () {
258+ _ = os .Chdir (previousWd )
259+ }()
260+ }
171261
172262 app , version , err := extregistry .GetAppVersion (o .app , o .version )
173263 if err != nil {
@@ -180,23 +270,23 @@ func (o *installOneOptions) install() (*installResult, error) {
180270
181271 // download to .bin/[system-key]/[name]@[version]
182272 rel := filepath .Join ("." + string (osKey ), app .Name + "@" + version .Version )
183- dst := filepath .Join (".kitty" , ".bin" , rel )
273+ dst := filepath .Join (o . root , ".kitty" , ".bin" , rel )
184274
185275 // TODO 安装到中央工具仓库(而不是 .bin 下)
186276 if version .InstallOptions != nil { // download for version
187- err = version .InstallTo (dst )
277+ err = version .InstallToWithProgress (dst , ! o . quiet )
188278 if err != nil {
189279 return nil , ee .Wrapf (err , "cannot download %s@%s" , app .Name , version .Version )
190280 }
191281 } else { // download for unknown version
192- err := version .InstallUnknownVersionTo (dst )
282+ err := version .InstallUnknownVersionToWithProgress (dst , ! o . quiet )
193283 if err != nil {
194284 return nil , ee .Wrapf (err , "cannot download %s@%s" , app .Name , o .version )
195285 }
196286 }
197287
198288 // Create soft link .bin/[name] -> .bin/[system-key]/[name]@[version]
199- err = symlink (rel , filepath .Join (".kitty" , ".bin" , app .Name ))
289+ err = symlink (rel , filepath .Join (o . root , ".kitty" , ".bin" , app .Name ))
200290 if err != nil {
201291 return nil , err
202292 }
0 commit comments