Skip to content

Commit 43da4ab

Browse files
committed
Fix the default install dir and UI issues
1 parent 5d0c520 commit 43da4ab

8 files changed

Lines changed: 442 additions & 48 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ git clone https://github.com/git-hulk/clime.git && cd clime && make install
2525
```
2626

2727
The install script detects your OS (macOS / Linux) and architecture (amd64 / arm64) automatically.
28+
By default it installs `clime` to `~/.local/bin` and updates your shell profile if that directory is not already on `PATH`.
2829

2930
## How It Works
3031

cmd/skills.go

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,20 @@ func runInteractiveSkillsInstall(manifest *skill.Manifest) error {
155155
}
156156

157157
options := append(sources, newRepoOption)
158+
showSourceSpacer := true
158159
for {
159-
fmt.Println()
160+
if showSourceSpacer {
161+
fmt.Println()
162+
} else {
163+
showSourceSpacer = true
164+
}
160165
idx, err := selectPrompt(prompt.SelectConfig{
161166
Label: "Select a skill source",
162167
Options: options,
163168
})
164169
if err != nil {
165170
if errors.Is(err, prompt.ErrBack) {
171+
showSourceSpacer = false
166172
continue
167173
}
168174
return err
@@ -177,17 +183,20 @@ func runInteractiveSkillsInstall(manifest *skill.Manifest) error {
177183
}
178184

179185
repo := options[idx]
186+
showActionSpacer := true
180187
for {
181-
action, err := pickSourceAction(repo)
188+
action, err := pickSourceAction(repo, showActionSpacer)
182189
if err != nil {
183190
if errors.Is(err, prompt.ErrBack) {
191+
showSourceSpacer = false
184192
break
185193
}
186194
return err
187195
}
188196

189197
err = skillsActionRunner(manifest, repo, action)
190198
if errors.Is(err, prompt.ErrBack) {
199+
showActionSpacer = false
191200
continue
192201
}
193202
return err
@@ -196,7 +205,7 @@ func runInteractiveSkillsInstall(manifest *skill.Manifest) error {
196205
}
197206

198207
func uniqueSkillSources(manifest *skill.Manifest) []string {
199-
// Collect unique sources from installed skills, preserving order.
208+
// Collect unique sources from installed skills and tracked sources, preserving order.
200209
seen := make(map[string]bool)
201210
var sources []string
202211
for _, s := range manifest.Skills {
@@ -205,6 +214,12 @@ func uniqueSkillSources(manifest *skill.Manifest) []string {
205214
sources = append(sources, s.Source)
206215
}
207216
}
217+
for _, s := range manifest.Sources {
218+
if s != "" && !seen[s] {
219+
seen[s] = true
220+
sources = append(sources, s)
221+
}
222+
}
208223

209224
return sources
210225
}
@@ -239,14 +254,16 @@ func validateSkillRepoSource(repo string) error {
239254
return nil
240255
}
241256

242-
func pickSourceAction(repo string) (sourceAction, error) {
257+
func pickSourceAction(repo string, showSpacer bool) (sourceAction, error) {
243258
options := []string{
244259
"Browse and install skills",
245260
"Update installed skills",
246261
"Remove source and its installed skills",
247262
}
248263

249-
fmt.Println()
264+
if showSpacer {
265+
fmt.Println()
266+
}
250267
idx, err := selectPrompt(prompt.SelectConfig{
251268
Label: fmt.Sprintf("Action for %s", repo),
252269
Options: options,
@@ -265,7 +282,7 @@ func pickSourceAction(repo string) (sourceAction, error) {
265282
}
266283
}
267284

268-
// removeSource uninstalls all skills from the given source and removes them from the manifest.
285+
// removeSource uninstalls all skills from the given source and removes it from the manifest.
269286
func removeSource(manifest *skill.Manifest, repo string) error {
270287
var names []string
271288
for _, s := range manifest.Skills {
@@ -274,17 +291,21 @@ func removeSource(manifest *skill.Manifest, repo string) error {
274291
}
275292
}
276293

277-
if len(names) == 0 {
278-
terminal.Warningf("No skills installed from %s.", repo)
279-
return nil
280-
}
281-
282294
fmt.Println()
283295
for _, name := range names {
284296
if err := uninstallByName(manifest, name); err != nil {
285297
terminal.Errorf("Failed to uninstall %q: %v", name, err)
286298
}
287299
}
300+
301+
manifest.RemoveSource(repo)
302+
if err := manifest.Save(); err != nil {
303+
return fmt.Errorf("failed to update manifest: %w", err)
304+
}
305+
306+
if len(names) == 0 {
307+
terminal.Successf("Removed source %s.", repo)
308+
}
288309
return nil
289310
}
290311

@@ -384,6 +405,12 @@ func installFromRepo(manifest *skill.Manifest, repo string) error {
384405

385406
spinner.Success(fmt.Sprintf("Found %d skill(s) in %q", len(repoManifest.Skills), repo))
386407

408+
// Record the source so it appears in future interactive menus.
409+
manifest.AddSource(repo)
410+
if err := manifest.Save(); err != nil {
411+
return fmt.Errorf("failed to save skill source: %w", err)
412+
}
413+
387414
// Filter out already-installed skills.
388415
type candidate struct {
389416
entry skill.SkillEntry
@@ -504,14 +531,20 @@ func interactiveUninstall(manifest *skill.Manifest) error {
504531
options[i] = label
505532
}
506533

534+
showSpacer := true
507535
for {
508-
fmt.Println()
536+
if showSpacer {
537+
fmt.Println()
538+
} else {
539+
showSpacer = true
540+
}
509541
selectedIdxs, err := multiSelectPrompt(prompt.SelectConfig{
510542
Label: "Select skills to uninstall (space to toggle, enter to confirm)",
511543
Options: options,
512544
})
513545
if err != nil {
514546
if errors.Is(err, prompt.ErrBack) {
547+
showSpacer = false
515548
continue
516549
}
517550
return err

cmd/skills_interactive_test.go

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package cmd
22

33
import (
44
"errors"
5+
"io"
6+
"os"
7+
"strings"
58
"testing"
69

710
"github.com/git-hulk/clime/internal/prompt"
@@ -96,15 +99,20 @@ func TestRunInteractiveSkillsInstallEscAtTopLevelKeepsUIOpen(t *testing.T) {
9699
return nil
97100
}
98101

99-
if err := runInteractiveSkillsInstall(manifest); err != nil {
100-
t.Fatalf("runInteractiveSkillsInstall() error = %v", err)
102+
output := captureStdout(t, func() {
103+
if err := runInteractiveSkillsInstall(manifest); err != nil {
104+
t.Fatalf("runInteractiveSkillsInstall() error = %v", err)
105+
}
106+
})
107+
if output != "\n" {
108+
t.Fatalf("stdout = %q, want %q", output, "\n")
101109
}
102110
if !called {
103111
t.Fatal("expected skillsActionRunner to be called")
104112
}
105113
}
106114

107-
func TestRunInteractiveSkillsInstallEscFromInstallReturnsToActionMenu(t *testing.T) {
115+
func TestRunInteractiveSkillsInstallEscFromInstallDoesNotPrintExtraSpacer(t *testing.T) {
108116
manifest := &skill.Manifest{
109117
Skills: []skill.InstalledSkill{
110118
{Name: "alpha", Source: "owner/repo"},
@@ -121,7 +129,7 @@ func TestRunInteractiveSkillsInstallEscFromInstallReturnsToActionMenu(t *testing
121129
case 1:
122130
return 0, nil
123131
case 2:
124-
return 0, nil
132+
return 1, nil
125133
case 3:
126134
return 1, nil
127135
default:
@@ -130,23 +138,25 @@ func TestRunInteractiveSkillsInstallEscFromInstallReturnsToActionMenu(t *testing
130138
}
131139
}
132140

133-
var actions []sourceAction
141+
runs := 0
134142
skillsActionRunner = func(manifest *skill.Manifest, repo string, action sourceAction) error {
135-
actions = append(actions, action)
136-
if len(actions) == 1 {
143+
runs++
144+
if runs == 1 {
137145
return prompt.ErrBack
138146
}
139147
return nil
140148
}
141149

142-
if err := runInteractiveSkillsInstall(manifest); err != nil {
143-
t.Fatalf("runInteractiveSkillsInstall() error = %v", err)
150+
output := captureStdout(t, func() {
151+
if err := runInteractiveSkillsInstall(manifest); err != nil {
152+
t.Fatalf("runInteractiveSkillsInstall() error = %v", err)
153+
}
154+
})
155+
if output != "\n\n" {
156+
t.Fatalf("stdout = %q, want %q", output, "\n\n")
144157
}
145-
if len(actions) != 2 {
146-
t.Fatalf("actions length = %d, want 2", len(actions))
147-
}
148-
if actions[0] != actionBrowseInstall || actions[1] != actionUpdate {
149-
t.Fatalf("actions = %v, want [%v %v]", actions, actionBrowseInstall, actionUpdate)
158+
if runs != 2 {
159+
t.Fatalf("skillsActionRunner calls = %d, want 2", runs)
150160
}
151161
}
152162

@@ -169,8 +179,16 @@ func TestInteractiveUninstallEscKeepsMenuOpen(t *testing.T) {
169179
return nil, nil
170180
}
171181

172-
if err := interactiveUninstall(manifest); err != nil {
173-
t.Fatalf("interactiveUninstall() error = %v", err)
182+
output := captureStdout(t, func() {
183+
if err := interactiveUninstall(manifest); err != nil {
184+
t.Fatalf("interactiveUninstall() error = %v", err)
185+
}
186+
})
187+
if !strings.HasPrefix(output, "\n") {
188+
t.Fatalf("stdout = %q, want prefix %q", output, "\n")
189+
}
190+
if strings.HasPrefix(output, "\n\n") {
191+
t.Fatalf("stdout = %q, want a single leading spacer line", output)
174192
}
175193
if calls != 2 {
176194
t.Fatalf("multiSelectPrompt calls = %d, want 2", calls)
@@ -197,6 +215,83 @@ func TestInteractiveUninstallInterruptPropagates(t *testing.T) {
197215
}
198216
}
199217

218+
func captureStdout(t *testing.T, fn func()) string {
219+
t.Helper()
220+
221+
origStdout := os.Stdout
222+
r, w, err := os.Pipe()
223+
if err != nil {
224+
t.Fatalf("os.Pipe() error = %v", err)
225+
}
226+
os.Stdout = w
227+
228+
done := make(chan string, 1)
229+
go func() {
230+
data, _ := io.ReadAll(r)
231+
done <- string(data)
232+
}()
233+
234+
defer func() {
235+
os.Stdout = origStdout
236+
}()
237+
238+
fn()
239+
240+
if err := w.Close(); err != nil {
241+
t.Fatalf("stdout close error = %v", err)
242+
}
243+
output := <-done
244+
if err := r.Close(); err != nil {
245+
t.Fatalf("stdout reader close error = %v", err)
246+
}
247+
return output
248+
}
249+
func TestRunInteractiveSkillsInstallEscFromInstallReturnsToActionMenu(t *testing.T) {
250+
manifest := &skill.Manifest{
251+
Skills: []skill.InstalledSkill{
252+
{Name: "alpha", Source: "owner/repo"},
253+
},
254+
}
255+
256+
restore := stubSkillPrompts(t)
257+
defer restore()
258+
259+
selectCalls := 0
260+
selectPrompt = func(config prompt.SelectConfig) (int, error) {
261+
selectCalls++
262+
switch selectCalls {
263+
case 1:
264+
return 0, nil
265+
case 2:
266+
return 0, nil
267+
case 3:
268+
return 1, nil
269+
default:
270+
t.Fatalf("unexpected select call %d", selectCalls)
271+
return 0, nil
272+
}
273+
}
274+
275+
var actions []sourceAction
276+
skillsActionRunner = func(manifest *skill.Manifest, repo string, action sourceAction) error {
277+
actions = append(actions, action)
278+
if len(actions) == 1 {
279+
return prompt.ErrBack
280+
}
281+
return nil
282+
}
283+
284+
if err := runInteractiveSkillsInstall(manifest); err != nil {
285+
t.Fatalf("runInteractiveSkillsInstall() error = %v", err)
286+
}
287+
if len(actions) != 2 {
288+
t.Fatalf("actions length = %d, want 2", len(actions))
289+
}
290+
if actions[0] != actionBrowseInstall || actions[1] != actionUpdate {
291+
t.Fatalf("actions = %v, want [%v %v]", actions, actionBrowseInstall, actionUpdate)
292+
}
293+
}
294+
200295
func stubSkillPrompts(t *testing.T) func() {
201296
t.Helper()
202297

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ if [ -z "$INSTALL_DIR" ]; then
4343
if [ -n "$PLUGIN" ]; then
4444
INSTALL_DIR="${HOME}/.clime/plugins"
4545
else
46-
INSTALL_DIR="${HOME}/.clime/bin"
46+
INSTALL_DIR="${HOME}/.local/bin"
4747
fi
4848
fi
4949

0 commit comments

Comments
 (0)