Skip to content

Commit 104f7d7

Browse files
committed
Merge branch 'AlphaRyz3-develop' into develop
2 parents 1197296 + f55b823 commit 104f7d7

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

cmd/runtipi/main.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func main() {
7474
var restartArgs types.StartArgs
7575
var updateArgs types.UpdateArgs
7676
var appArgs types.AppArgs
77+
var appStoreArgs types.AppStoreArgs
7778

7879
// Start command
7980
startCmd := &cobra.Command{
@@ -289,6 +290,58 @@ func main() {
289290
appCmd.AddCommand(appDeleteBackupCmd)
290291
appCmd.AddCommand(appStartAllCmd)
291292

293+
// AppStore command and subcommands
294+
appStoreCmd := &cobra.Command{
295+
Use: "appstore",
296+
Short: "Manage Runtipi app stores",
297+
}
298+
299+
appStoreUpdateCmd := &cobra.Command{
300+
Use: "update",
301+
Short: "Update app stores",
302+
Run: func(cmd *cobra.Command, args []string) {
303+
appStoreArgs.Command = types.AppStoreCommandUpdate
304+
commands.RunAppStore(appStoreArgs)
305+
},
306+
}
307+
308+
appStoreListCmd := &cobra.Command{
309+
Use: "list",
310+
Short: "List configured app stores",
311+
Run: func(cmd *cobra.Command, args []string) {
312+
appStoreArgs.Command = types.AppStoreCommandList
313+
commands.RunAppStore(appStoreArgs)
314+
},
315+
}
316+
317+
appStoreAddCmd := &cobra.Command{
318+
Use: "add [name] [url]",
319+
Short: "Add a new app store",
320+
Args: cobra.ExactArgs(2),
321+
Run: func(cmd *cobra.Command, args []string) {
322+
appStoreArgs.Command = types.AppStoreCommandAdd
323+
appStoreArgs.Name = args[0]
324+
appStoreArgs.URL = args[1]
325+
commands.RunAppStore(appStoreArgs)
326+
},
327+
}
328+
329+
appStoreRemoveCmd := &cobra.Command{
330+
Use: "remove [name]",
331+
Short: "Remove an app store",
332+
Args: cobra.ExactArgs(1),
333+
Run: func(cmd *cobra.Command, args []string) {
334+
appStoreArgs.Command = types.AppStoreCommandRemove
335+
appStoreArgs.Name = args[0]
336+
commands.RunAppStore(appStoreArgs)
337+
},
338+
}
339+
340+
appStoreCmd.AddCommand(appStoreUpdateCmd)
341+
appStoreCmd.AddCommand(appStoreListCmd)
342+
appStoreCmd.AddCommand(appStoreAddCmd)
343+
appStoreCmd.AddCommand(appStoreRemoveCmd)
344+
292345
// Add commands to root command
293346
rootCmd.AddCommand(startCmd)
294347
rootCmd.AddCommand(stopCmd)
@@ -298,6 +351,7 @@ func main() {
298351
rootCmd.AddCommand(debugCmd)
299352
rootCmd.AddCommand(versionCmd)
300353
rootCmd.AddCommand(appCmd)
354+
rootCmd.AddCommand(appStoreCmd)
301355
rootCmd.AddCommand(installedCmd)
302356

303357
if err := rootCmd.Execute(); err != nil {

internal/commands/appstore.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package commands
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
10+
"github.com/runtipi/cli/internal/components"
11+
"github.com/runtipi/cli/internal/types"
12+
"github.com/runtipi/cli/internal/utils"
13+
)
14+
15+
const (
16+
colorRed = "\033[31m"
17+
colorGreen = "\033[32m"
18+
colorReset = "\033[0m" // Reset to default color
19+
)
20+
21+
// AppStore represents a single app store/repository
22+
type AppStore struct {
23+
Slug string `json:"slug"`
24+
Name string `json:"name"`
25+
URL string `json:"url"`
26+
Enabled bool `json:"enabled"`
27+
}
28+
29+
type emptyResponse struct{}
30+
31+
// AppStoresResponse represents the API response for listing app stores
32+
type AppStoresResponse struct {
33+
AppStores []AppStore `json:"appStores"`
34+
}
35+
36+
func handleAppStoreAPIResponse[R any](spin *components.Spinner, resp *http.Response, err error, successMessage, errorMessage string, response R) (R, error) {
37+
// 1. Check for errors in the HTTP request
38+
if err != nil {
39+
spin.Fail(errorMessage)
40+
spin.Finish()
41+
return response, err
42+
}
43+
44+
defer resp.Body.Close()
45+
46+
// 2. Read response body
47+
body, readErr := io.ReadAll(resp.Body)
48+
if readErr != nil {
49+
spin.Fail(errorMessage)
50+
spin.Finish()
51+
return response, readErr
52+
}
53+
54+
// 3. Check status code
55+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
56+
spin.Fail(errorMessage)
57+
spin.Finish()
58+
return response, fmt.Errorf("HTTP error %d", resp.StatusCode)
59+
}
60+
61+
// 4. Decode JSON Response into expected format
62+
if err := json.Unmarshal(body, &response); err != nil {
63+
spin.Fail(errorMessage)
64+
spin.Finish()
65+
return response, err
66+
}
67+
68+
// 5. Everything fine
69+
spin.Succeed(successMessage)
70+
spin.Finish()
71+
return response, nil
72+
}
73+
74+
func printAppStores(appStores []AppStore) {
75+
if len(appStores) == 0 {
76+
fmt.Println("No app stores found.")
77+
return
78+
}
79+
80+
fmt.Printf("Found %d app stores:\n\n", len(appStores))
81+
for i, appStore := range appStores {
82+
fmt.Printf("%d. ", i+1)
83+
84+
if appStore.Name == appStore.Slug {
85+
fmt.Printf("%s\n", appStore.Name)
86+
} else {
87+
fmt.Printf(" %s (%s)\n", appStore.Name, appStore.Slug)
88+
}
89+
90+
if appStore.URL != "" {
91+
fmt.Printf(" ├ %s\n", appStore.URL)
92+
}
93+
94+
if appStore.Enabled {
95+
fmt.Printf(" ╰ " + colorGreen + "✓" + colorReset + " Enabled \n")
96+
} else {
97+
fmt.Printf(" ╰ " + colorRed + "✗" + colorReset + " Disabled\n")
98+
}
99+
100+
fmt.Println()
101+
}
102+
}
103+
104+
func RunAppStore(args types.AppStoreArgs) {
105+
appStoresURL := utils.GetAPIBaseURL("marketplace")
106+
107+
switch args.Command {
108+
case types.AppStoreCommandUpdate:
109+
spin := components.NewSpinner("Updating app stores...")
110+
url := fmt.Sprintf("%s/pull", appStoresURL)
111+
resp, err := utils.APIRequest(url, "POST", "{}")
112+
_, apiErr := handleAppStoreAPIResponse(spin, resp, err,
113+
"App stores updated successfully!",
114+
"Failed to update app stores.",
115+
emptyResponse{})
116+
117+
if apiErr != nil {
118+
fmt.Printf("Error updating app stores: %v\n", apiErr)
119+
}
120+
121+
case types.AppStoreCommandList:
122+
spin := components.NewSpinner("Retrieving app stores...")
123+
url := fmt.Sprintf("%s/all", appStoresURL)
124+
resp, err := utils.APIRequest(url, "GET", "")
125+
result, apiErr := handleAppStoreAPIResponse(spin, resp, err,
126+
"App stores retrieved successfully!",
127+
"Failed to retrieve app stores.",
128+
AppStoresResponse{})
129+
130+
if apiErr != nil {
131+
fmt.Printf("Error retrieving app stores: %v\n", apiErr)
132+
} else {
133+
printAppStores(result.AppStores)
134+
}
135+
136+
case types.AppStoreCommandAdd:
137+
if args.Name == "" || args.URL == "" {
138+
fmt.Println("✗ Error: Both name and URL are required for adding an app store.")
139+
fmt.Println("Usage: runtipi appstore add <name> <url>")
140+
return
141+
}
142+
143+
spin := components.NewSpinner(fmt.Sprintf("Adding app store %s...", args.Name))
144+
url := fmt.Sprintf("%s/create", appStoresURL)
145+
146+
// Use proper JSON marshaling for safety
147+
payload := map[string]string{
148+
"name": args.Name,
149+
"url": args.URL,
150+
}
151+
payloadBytes, mErr := json.Marshal(payload)
152+
if mErr != nil {
153+
spin.Fail("Failed to prepare request payload.")
154+
fmt.Printf("Error: %v\n", mErr)
155+
spin.Finish()
156+
return
157+
}
158+
159+
resp, err := utils.APIRequest(url, "POST", string(payloadBytes))
160+
_, apiErr := handleAppStoreAPIResponse(spin, resp, err,
161+
fmt.Sprintf("App store %s added successfully!", args.Name),
162+
fmt.Sprintf("Failed to add app store %s.", args.Name),
163+
emptyResponse{})
164+
165+
if apiErr != nil {
166+
fmt.Printf("Error adding app store: %v\n", apiErr)
167+
}
168+
169+
case types.AppStoreCommandRemove:
170+
if args.Name == "" {
171+
fmt.Println("✗ Error: App store name is required for removal.")
172+
fmt.Println("Usage: runtipi appstore remove <name>")
173+
return
174+
}
175+
176+
spin := components.NewSpinner(fmt.Sprintf("Removing app store %s...", args.Name))
177+
// Use URL escaping for safety
178+
escapedName := url.PathEscape(args.Name)
179+
url := fmt.Sprintf("%s/%s", appStoresURL, escapedName)
180+
resp, err := utils.APIRequest(url, "DELETE", "")
181+
handleAppStoreAPIResponse(spin, resp, err,
182+
fmt.Sprintf("App store %s removed successfully! (Note: Shows success even if app store doesn't exist - check Runtipi logs for actual status)", args.Name),
183+
fmt.Sprintf("Failed to remove app store %s.", args.Name),
184+
emptyResponse{})
185+
186+
// For now we cannot handle errors for the Remove Command since runtipi always returns 200 even if the appstore doesn't exist and the error is only shown in the console.
187+
}
188+
}
189+

internal/types/args.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,19 @@ const (
3232
AppCommandDeleteBackup AppCommand = "delete-backup"
3333
AppCommandStartAll AppCommand = "start-all"
3434
)
35+
36+
type AppStoreArgs struct {
37+
Command AppStoreCommand
38+
URL string
39+
Name string
40+
}
41+
42+
// AppStoreCommand represents the subcommands available for the appstore command
43+
type AppStoreCommand string
44+
45+
const (
46+
AppStoreCommandUpdate AppStoreCommand = "update"
47+
AppStoreCommandList AppStoreCommand = "list"
48+
AppStoreCommandAdd AppStoreCommand = "add"
49+
AppStoreCommandRemove AppStoreCommand = "remove"
50+
)

0 commit comments

Comments
 (0)