diff --git a/cmd/get_config.go b/cmd/get_config.go index a337a35..cc7335b 100644 --- a/cmd/get_config.go +++ b/cmd/get_config.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" - "github.com/nov11/nacos-cli/internal/client" "github.com/nov11/nacos-cli/internal/help" "github.com/spf13/cobra" ) @@ -18,7 +17,7 @@ var getConfigCmd = &cobra.Command{ group := args[1] // Create Nacos client - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) + nacosClient := mustNewNacosClient() // Get config fmt.Printf("Fetching config: %s (%s)...\n\n", dataID, group) diff --git a/cmd/get_skill.go b/cmd/get_skill.go index f02e741..74940e4 100644 --- a/cmd/get_skill.go +++ b/cmd/get_skill.go @@ -6,14 +6,15 @@ import ( "path/filepath" "strings" - "github.com/nov11/nacos-cli/internal/client" "github.com/nov11/nacos-cli/internal/help" "github.com/nov11/nacos-cli/internal/skill" "github.com/spf13/cobra" ) var ( - getSkillOutput string + getSkillOutput string + getSkillVersion string + getSkillLabel string ) var getSkillCmd = &cobra.Command{ @@ -43,7 +44,7 @@ var getSkillCmd = &cobra.Command{ } // Create Nacos client - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) + nacosClient := mustNewNacosClient() // Create skill service skillService := skill.NewSkillService(nacosClient) @@ -58,7 +59,7 @@ var getSkillCmd = &cobra.Command{ fmt.Printf("\n[%d/%d] ", i+1, len(skillNames)) } fmt.Printf("Fetching skill: %s...\n", skillName) - err := skillService.GetSkill(skillName, getSkillOutput) + err := skillService.GetSkill(skillName, getSkillOutput, getSkillVersion, getSkillLabel) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to download skill '%s': %v\n", skillName, err) failCount++ @@ -89,5 +90,7 @@ var getSkillCmd = &cobra.Command{ func init() { getSkillCmd.Flags().StringVarP(&getSkillOutput, "output", "o", "", "Output directory (default: ~/.skills)") + getSkillCmd.Flags().StringVar(&getSkillVersion, "version", "", "Specific version to download (e.g. v1, v2)") + getSkillCmd.Flags().StringVar(&getSkillLabel, "label", "", "Route label to resolve version (e.g. latest, stable)") rootCmd.AddCommand(getSkillCmd) } diff --git a/cmd/interactive.go b/cmd/interactive.go index a4825f4..fcd4456 100644 --- a/cmd/interactive.go +++ b/cmd/interactive.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/nov11/nacos-cli/internal/client" "github.com/nov11/nacos-cli/internal/terminal" "github.com/spf13/cobra" ) @@ -12,7 +11,7 @@ var interactiveCmd = &cobra.Command{ Long: `Start an interactive terminal for managing Nacos configurations and skills`, Run: func(cmd *cobra.Command, args []string) { // Create Nacos client - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) + nacosClient := mustNewNacosClient() // Create and start terminal term := terminal.NewTerminal(nacosClient) diff --git a/cmd/list_config.go b/cmd/list_config.go index 00d223c..7287d0d 100644 --- a/cmd/list_config.go +++ b/cmd/list_config.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" - "github.com/nov11/nacos-cli/internal/client" "github.com/nov11/nacos-cli/internal/help" "github.com/spf13/cobra" ) @@ -21,7 +20,7 @@ var listConfigCmd = &cobra.Command{ Long: help.ConfigList.FormatForCLI("nacos-cli"), Run: func(cmd *cobra.Command, args []string) { // Create Nacos client - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) + nacosClient := mustNewNacosClient() // List configs configs, err := nacosClient.ListConfigs(configListDataID, configListGroup, "", configListPage, configListSize) diff --git a/cmd/list_skill.go b/cmd/list_skill.go index 94a5c54..5395ae4 100644 --- a/cmd/list_skill.go +++ b/cmd/list_skill.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" - "github.com/nov11/nacos-cli/internal/client" "github.com/nov11/nacos-cli/internal/help" "github.com/nov11/nacos-cli/internal/skill" "github.com/spf13/cobra" @@ -23,7 +22,7 @@ var listSkillCmd = &cobra.Command{ Long: help.SkillList.FormatForCLI("nacos-cli"), Run: func(cmd *cobra.Command, args []string) { // Create Nacos client - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) + nacosClient := mustNewNacosClient() // Create skill service skillService := skill.NewSkillService(nacosClient) diff --git a/cmd/profile.go b/cmd/profile.go index 21ed325..9097ab0 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -91,7 +91,7 @@ Examples: if input == "" || input == "y" || input == "yes" { fmt.Println() // Start interactive terminal with the edited config - nacosClient := client.NewNacosClient( + nacosClient, err := client.NewNacosClient( cfg.GetServerAddr(), cfg.Namespace, cfg.AuthType, @@ -99,7 +99,12 @@ Examples: cfg.Password, cfg.AccessKey, cfg.SecretKey, + cfg.Token, ) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } term := terminal.NewTerminal(nacosClient) if err := term.Start(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/cmd/upload_skill.go b/cmd/publish_skill.go similarity index 65% rename from cmd/upload_skill.go rename to cmd/publish_skill.go index 1a3a609..409fc84 100644 --- a/cmd/upload_skill.go +++ b/cmd/publish_skill.go @@ -6,20 +6,19 @@ import ( "path/filepath" "strings" - "github.com/nov11/nacos-cli/internal/client" "github.com/nov11/nacos-cli/internal/help" "github.com/nov11/nacos-cli/internal/skill" "github.com/spf13/cobra" ) var ( - uploadAll bool + publishAll bool ) -var uploadSkillCmd = &cobra.Command{ - Use: "skill-upload [skillPath]", - Short: "Upload a skill to Nacos", - Long: help.SkillUpload.FormatForCLI("nacos-cli"), +var publishSkillCmd = &cobra.Command{ + Use: "skill-publish [skillPath]", + Short: "Publish a skill to Nacos (upload as ZIP)", + Long: help.SkillPublish.FormatForCLI("nacos-cli"), Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { @@ -29,23 +28,23 @@ var uploadSkillCmd = &cobra.Command{ skillPath := args[0] // Create Nacos client - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) + nacosClient := mustNewNacosClient() // Create skill service skillService := skill.NewSkillService(nacosClient) - // Handle batch upload - if uploadAll { - uploadAllSkills(skillPath, skillService) + // Handle batch publish + if publishAll { + publishAllSkills(skillPath, skillService) return } - // Single skill upload - uploadSingleSkill(skillPath, skillService) + // Single skill publish + publishSingleSkill(skillPath, skillService) }, } -func uploadSingleSkill(skillPath string, skillService *skill.SkillService) { +func publishSingleSkill(skillPath string, skillService *skill.SkillService) { // Expand ~ to home directory if strings.HasPrefix(skillPath, "~") { homeDir, err := os.UserHomeDir() @@ -58,16 +57,16 @@ func uploadSingleSkill(skillPath string, skillService *skill.SkillService) { checkError(err) skillName := filepath.Base(absPath) - fmt.Printf("Uploading skill: %s...\n", skillName) + fmt.Printf("Publishing skill: %s...\n", skillName) err = skillService.UploadSkill(absPath) checkError(err) - fmt.Printf("Skill uploaded successfully!\n") - fmt.Printf(" Tip: Use 'skill-list' to verify or 'skill-get %s' to download\n", skillName) + fmt.Printf("Skill published successfully!\n") + fmt.Printf(" Tip: Use the Nacos console to review and go online, or use 'skill-list' to verify.\n") } -func uploadAllSkills(folderPath string, skillService *skill.SkillService) { +func publishAllSkills(folderPath string, skillService *skill.SkillService) { // Expand ~ to home directory if strings.HasPrefix(folderPath, "~") { homeDir, err := os.UserHomeDir() @@ -108,16 +107,16 @@ func uploadAllSkills(folderPath string, skillService *skill.SkillService) { for i, skillName := range skillDirs { fmt.Println(strings.Repeat("=", 80)) - fmt.Printf("[%d/%d] Uploading skill: %s\n", i+1, len(skillDirs), skillName) + fmt.Printf("[%d/%d] Publishing skill: %s\n", i+1, len(skillDirs), skillName) fmt.Println(strings.Repeat("=", 80)) skillPath := filepath.Join(folderPath, skillName) err := skillService.UploadSkill(skillPath) if err != nil { - fmt.Printf("Upload failed: %v\n", err) + fmt.Printf("Publish failed: %v\n", err) failedCount++ } else { - fmt.Printf("Upload successful!\n") + fmt.Printf("Publish successful!\n") successCount++ } fmt.Println() @@ -125,7 +124,7 @@ func uploadAllSkills(folderPath string, skillService *skill.SkillService) { // Summary fmt.Println(strings.Repeat("=", 80)) - fmt.Println("Batch Upload Complete") + fmt.Println("Batch Publish Complete") fmt.Println(strings.Repeat("=", 80)) fmt.Printf("Success: %d\n", successCount) if failedCount > 0 { @@ -133,10 +132,10 @@ func uploadAllSkills(folderPath string, skillService *skill.SkillService) { } fmt.Printf("Total: %d\n", len(skillDirs)) fmt.Println() - fmt.Println("Tip: Use 'skill-list' to view all uploaded skills") + fmt.Println("Tip: Use the Nacos console to review and go online, or use 'skill-list' to verify.") } func init() { - uploadSkillCmd.Flags().BoolVar(&uploadAll, "all", false, "Upload all skills in the directory") - rootCmd.AddCommand(uploadSkillCmd) + publishSkillCmd.Flags().BoolVar(&publishAll, "all", false, "Publish all skills in the directory") + rootCmd.AddCommand(publishSkillCmd) } diff --git a/cmd/root.go b/cmd/root.go index 8e1adca..8bbb15c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,6 +19,7 @@ var ( authType string username string password string + token string accessKey string secretKey string configFile string @@ -55,7 +56,7 @@ Examples: var err error // Check if any connection parameters are provided via command line - hasCommandLineConfig := host != "" || port > 0 || serverAddr != "" || username != "" || password != "" || accessKey != "" || secretKey != "" + hasCommandLineConfig := host != "" || port > 0 || serverAddr != "" || username != "" || password != "" || token != "" || accessKey != "" || secretKey != "" if configFile != "" { // Explicit config file specified @@ -105,31 +106,29 @@ Examples: namespace = fileConfig.Namespace } - // AuthType: command line > config file > default nacos - if authType == "" { - if fileConfig != nil && fileConfig.AuthType != "" { - authType = fileConfig.AuthType - } else { - authType = "nacos" - } + // AuthType: command line > config file > auto-detect by NewNacosClient + if authType == "" && fileConfig != nil && fileConfig.AuthType != "" { + authType = fileConfig.AuthType } - // Username: command line > config file > default - if username == "" { - if fileConfig != nil && fileConfig.Username != "" { - username = fileConfig.Username - } else { - username = "nacos" - } + // Username: command line > config file + if username == "" && fileConfig != nil && fileConfig.Username != "" { + username = fileConfig.Username } - // Password: command line > config file > default - if password == "" { - if fileConfig != nil && fileConfig.Password != "" { - password = fileConfig.Password - } else { - password = "nacos" - } + // Password: command line > config file + if password == "" && fileConfig != nil && fileConfig.Password != "" { + password = fileConfig.Password + } + + // Token: command line > config file (token takes priority over username/password when set) + if token == "" && fileConfig != nil && fileConfig.Token != "" { + token = fileConfig.Token + } + // If token is provided, clear username/password defaults to avoid unnecessary login attempts + if token != "" { + username = "" + password = "" } // AccessKey / SecretKey: command line > config file(AuthType=aliyun 时使用) @@ -147,7 +146,7 @@ Examples: }, Run: func(cmd *cobra.Command, args []string) { // Default behavior: start interactive terminal - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) + nacosClient := mustNewNacosClient() term := terminal.NewTerminal(nacosClient) if err := term.Start(); err != nil { checkError(err) @@ -173,6 +172,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&authType, "auth-type", "", "Auth type: nacos (username/password) or aliyun (AK/SK)") rootCmd.PersistentFlags().StringVarP(&username, "username", "u", "", "Username (nacos auth)") rootCmd.PersistentFlags().StringVarP(&password, "password", "p", "", "Password (nacos auth)") + rootCmd.PersistentFlags().StringVar(&token, "token", "", "Access token (skips username/password login)") rootCmd.PersistentFlags().StringVar(&accessKey, "access-key", "", "AccessKey (aliyun auth)") rootCmd.PersistentFlags().StringVar(&secretKey, "secret-key", "", "SecretKey (aliyun auth)") @@ -186,3 +186,13 @@ func checkError(err error) { os.Exit(1) } } + +// mustNewNacosClient creates a NacosClient and exits with a clear error message on failure (e.g. login failed). +func mustNewNacosClient() *client.NacosClient { + c, err := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey, token) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + return c +} diff --git a/cmd/set_config.go b/cmd/set_config.go index 1aaf5a8..e81bb57 100644 --- a/cmd/set_config.go +++ b/cmd/set_config.go @@ -5,7 +5,6 @@ import ( "fmt" "os" - "github.com/nov11/nacos-cli/internal/client" "github.com/nov11/nacos-cli/internal/help" "github.com/spf13/cobra" ) @@ -30,7 +29,7 @@ var setConfigCmd = &cobra.Command{ } // Create Nacos client - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) + nacosClient := mustNewNacosClient() fmt.Printf("Publishing config: %s (%s)...\n", dataID, group) err = nacosClient.PublishConfig(dataID, group, content) diff --git a/cmd/sync_skill.go b/cmd/sync_skill.go deleted file mode 100644 index b72b10d..0000000 --- a/cmd/sync_skill.go +++ /dev/null @@ -1,115 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "os/signal" - "path/filepath" - "strings" - "syscall" - - "github.com/nov11/nacos-cli/internal/client" - "github.com/nov11/nacos-cli/internal/help" - "github.com/nov11/nacos-cli/internal/skill" - "github.com/nov11/nacos-cli/internal/sync" - "github.com/spf13/cobra" -) - -var ( - syncAllSkills bool - syncOutputDir string -) - -var syncSkillCmd = &cobra.Command{ - Use: "skill-sync [skillName...]", - Short: "Synchronize a skill with Nacos (real-time updates)", - Long: help.SkillSync.FormatForCLI("nacos-cli"), - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - var skillNames []string - - // Handle --all flag - if syncAllSkills { - // Create Nacos client to fetch all skills - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) - skillService := skill.NewSkillService(nacosClient) - - fmt.Println("Fetching list of all skills...") - skills, _, err := skillService.ListSkills("", 1, 10000) - if err != nil { - fmt.Fprintf(os.Stderr, "Error fetching skills: %v\n", err) - os.Exit(1) - } - - if len(skills) == 0 { - fmt.Println("No skills found") - os.Exit(0) - } - - // Extract skill names from SkillListItem - for _, s := range skills { - skillNames = append(skillNames, s.Name) - } - fmt.Printf("Found %d skills\n\n", len(skillNames)) - } else if len(args) == 0 { - fmt.Fprintf(os.Stderr, "Error: skill name required (or use --all to sync all skills)\n") - fmt.Fprintf(os.Stderr, "\nUsage:\n") - fmt.Fprintf(os.Stderr, " nacos-cli skill-sync [skillName2...]\n") - fmt.Fprintf(os.Stderr, " nacos-cli skill-sync --all\n") - os.Exit(1) - } else { - skillNames = args - } - - // Expand ~ to home directory in syncOutputDir - if syncOutputDir != "" { - if strings.HasPrefix(syncOutputDir, "~/") { - homeDir, err := os.UserHomeDir() - checkError(err) - syncOutputDir = filepath.Join(homeDir, syncOutputDir[2:]) - } else if syncOutputDir == "~" { - homeDir, err := os.UserHomeDir() - checkError(err) - syncOutputDir = homeDir - } - } - - // Create Nacos client - nacosClient := client.NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey) - - // Create skill syncer - skillSyncer := sync.NewSkillSyncer(nacosClient, syncOutputDir) - - // Setup signal handling - stopCh := make(chan struct{}) - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - go func() { - <-sigCh - fmt.Println("\n\nStopping synchronization...") - close(stopCh) - }() - - // Start sync (single or multiple skills) - var err error - if len(skillNames) == 1 { - err = skillSyncer.StartSync(skillNames[0], stopCh) - } else { - err = skillSyncer.StartSyncMultiple(skillNames, stopCh) - } - - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - - fmt.Println("Synchronization stopped") - }, -} - -func init() { - syncSkillCmd.Flags().BoolVar(&syncAllSkills, "all", false, "Synchronize all skills") - syncSkillCmd.Flags().StringVarP(&syncOutputDir, "dir", "d", "", "Output directory for synced skills (default: ~/.skills)") - rootCmd.AddCommand(syncSkillCmd) -} diff --git a/internal/client/nacos_client.go b/internal/client/nacos_client.go index 1617651..7aa23bf 100644 --- a/internal/client/nacos_client.go +++ b/internal/client/nacos_client.go @@ -15,8 +15,10 @@ import ( ) const ( + AuthTypeNone = "none" // No authentication (public registry) AuthTypeNacos = "nacos" // Username/password authentication AuthTypeAliyun = "aliyun" // AccessKey/SecretKey authentication + AuthTypeToken = "token" // Pre-issued access token (no login required) ) // NacosClient represents a Nacos API client @@ -58,36 +60,101 @@ type V3Response struct { Data json.RawMessage `json:"data"` } -// NewNacosClient creates a new Nacos client with automatic authentication -func NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey string) *NacosClient { +// ParseHTTPError converts an HTTP error response into a user-friendly error message. +// It handles common HTTP status codes with actionable hints. +func ParseHTTPError(statusCode int, body []byte, operation string) error { + // Try to extract message from v3 response body + serverMsg := "" + if len(body) > 0 { + var v3 V3Response + if err := json.Unmarshal(body, &v3); err == nil && v3.Message != "" { + serverMsg = v3.Message + } + } + + switch statusCode { + case 401: + hint := "authentication required — please check your username/password or token" + if serverMsg != "" { + return fmt.Errorf("%s failed (401 Unauthorized): %s\nHint: %s", operation, serverMsg, hint) + } + return fmt.Errorf("%s failed (401 Unauthorized): %s", operation, hint) + case 403: + hint := "access denied — token may be expired or you lack permission for this operation" + if serverMsg != "" { + return fmt.Errorf("%s failed (403 Forbidden): %s\nHint: %s", operation, serverMsg, hint) + } + return fmt.Errorf("%s failed (403 Forbidden): %s", operation, hint) + case 404: + hint := "resource not found — check the name/namespace or whether it exists" + if serverMsg != "" { + return fmt.Errorf("%s failed (404 Not Found): %s\nHint: %s", operation, serverMsg, hint) + } + return fmt.Errorf("%s failed (404 Not Found): %s", operation, hint) + case 500: + hint := "server internal error — check Nacos server logs for details" + if serverMsg != "" { + return fmt.Errorf("%s failed (500 Internal Server Error): %s\nHint: %s", operation, serverMsg, hint) + } + return fmt.Errorf("%s failed (500 Internal Server Error): %s", operation, hint) + default: + if serverMsg != "" { + return fmt.Errorf("%s failed (HTTP %d): %s", operation, statusCode, serverMsg) + } + if len(body) > 0 { + // Truncate long bodies + bodyStr := string(body) + if len(bodyStr) > 200 { + bodyStr = bodyStr[:200] + "..." + } + return fmt.Errorf("%s failed (HTTP %d): %s", operation, statusCode, bodyStr) + } + return fmt.Errorf("%s failed (HTTP %d)", operation, statusCode) + } +} + +// NewNacosClient creates a new Nacos client with automatic authentication. +// If token is non-empty, it is used directly as the Bearer token and no login request is made. +// Returns an error if login is required but fails (e.g. wrong credentials). +func NewNacosClient(serverAddr, namespace, authType, username, password, accessKey, secretKey, token string) (*NacosClient, error) { if namespace == "" { namespace = "public" } if authType == "" { - if accessKey != "" && secretKey != "" { + if token != "" { + authType = AuthTypeToken + } else if accessKey != "" && secretKey != "" { authType = AuthTypeAliyun - } else { + } else if username != "" && password != "" { authType = AuthTypeNacos + } else { + authType = AuthTypeNone } } c := &NacosClient{ - ServerAddr: serverAddr, - Namespace: namespace, - AuthType: authType, - Username: username, - Password: password, - AccessKey: accessKey, - SecretKey: secretKey, - httpClient: resty.New(), + ServerAddr: serverAddr, + Namespace: namespace, + AuthType: authType, + Username: username, + Password: password, + AccessKey: accessKey, + SecretKey: secretKey, + AccessToken: token, + httpClient: resty.New(), + } + + // If a token is provided directly, skip login entirely. + if token != "" { + return c, nil } if c.AuthType == AuthTypeNacos { if err := c.login(); err != nil { - fmt.Printf("Warning: Login failed: %v\n", err) + return nil, fmt.Errorf("login failed: %w", err) } } - return c + return c, nil } // isLocalAddr checks if the server address is localhost @@ -178,6 +245,10 @@ func (c *NacosClient) applyLoginFromMap(m map[string]interface{}) bool { // ensureTokenValid ensures the access token is valid, refreshing if necessary func (c *NacosClient) ensureTokenValid() error { + // Token auth: user-supplied token, no refresh + if c.AuthType == AuthTypeToken { + return nil + } if c.AuthType != AuthTypeNacos { return nil } @@ -269,7 +340,7 @@ func (c *NacosClient) ListConfigs(dataID, groupName, namespaceID string, pageNo, } if resp.StatusCode() != 200 { - return nil, fmt.Errorf("list configs failed: status=%d, body=%s", resp.StatusCode(), string(resp.Body())) + return nil, ParseHTTPError(resp.StatusCode(), resp.Body(), "list configs") } var v3Resp V3Response @@ -320,7 +391,7 @@ func (c *NacosClient) listConfigsV1(dataID, groupName, namespace string, pageNo, } if resp.StatusCode() != 200 { - return nil, fmt.Errorf("v1 list configs failed: status=%d", resp.StatusCode()) + return nil, ParseHTTPError(resp.StatusCode(), resp.Body(), "list configs (v1)") } var configList ConfigListResponse @@ -362,7 +433,7 @@ func (c *NacosClient) GetConfig(dataID, group string) (string, error) { } if resp.StatusCode() != 200 { - return "", fmt.Errorf("get config failed: status=%d, body=%s", resp.StatusCode(), string(resp.Body())) + return "", ParseHTTPError(resp.StatusCode(), resp.Body(), "get config") } // Parse v3 response @@ -417,7 +488,7 @@ func (c *NacosClient) PublishConfig(dataID, group, content string) error { } if resp.StatusCode() != 200 { - return fmt.Errorf("publish config failed: status=%d, body=%s", resp.StatusCode(), string(resp.Body())) + return ParseHTTPError(resp.StatusCode(), resp.Body(), "publish config") } var v3Resp V3Response diff --git a/internal/config/config.go b/internal/config/config.go index 999077b..768913d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,9 +22,10 @@ const ( type Config struct { Host string `yaml:"host"` Port int `yaml:"port"` - AuthType string `yaml:"authType"` // nacos | aliyun + AuthType string `yaml:"authType"` // nacos | aliyun | token Username string `yaml:"username"` Password string `yaml:"password"` + Token string `yaml:"token"` // Pre-issued access token (skips username/password login) AccessKey string `yaml:"accessKey"` // Aliyun AK(AuthType=aliyun 时使用) SecretKey string `yaml:"secretKey"` // Aliyun SK Namespace string `yaml:"namespace"` @@ -126,14 +127,25 @@ func (c *Config) IsComplete() bool { return false } + // Token auth: only token is needed + if c.Token != "" { + return true + } + // Check based on auth type authType := strings.ToLower(c.AuthType) + + // No auth: only host is needed + if authType == "" || authType == "none" { + return true + } + if authType == "aliyun" { // Aliyun auth requires AccessKey and SecretKey return c.AccessKey != "" && c.SecretKey != "" } - // Nacos auth (default) requires username and password + // Nacos auth requires username and password return c.Username != "" && c.Password != "" } @@ -145,7 +157,18 @@ func (c *Config) GetMissingFields() []string { missing = append(missing, "host") } + // Token auth: no other fields required + if c.Token != "" { + return missing + } + authType := strings.ToLower(c.AuthType) + + // No auth: no credential fields required + if authType == "" || authType == "none" { + return missing + } + if authType == "aliyun" { if c.AccessKey == "" { missing = append(missing, "accessKey") @@ -154,7 +177,7 @@ func (c *Config) GetMissingFields() []string { missing = append(missing, "secretKey") } } else { - // Default to nacos auth + // Nacos auth if c.Username == "" { missing = append(missing, "username") } @@ -241,18 +264,18 @@ func (c *Config) PromptForMissingFields() error { // Prompt for auth type if not set if c.AuthType == "" { - fmt.Print("Enter auth type (nacos/aliyun) [nacos]: ") + fmt.Print("Enter auth type (none/nacos/aliyun) [none]: ") input, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("failed to read auth type: %w", err) } input = strings.TrimSpace(strings.ToLower(input)) if input == "" { - c.AuthType = "nacos" - } else if input == "nacos" || input == "aliyun" { + c.AuthType = "none" + } else if input == "none" || input == "nacos" || input == "aliyun" { c.AuthType = input } else { - return fmt.Errorf("invalid auth type: %s (must be 'nacos' or 'aliyun')", input) + return fmt.Errorf("invalid auth type: %s (must be 'none', 'nacos' or 'aliyun')", input) } } @@ -276,31 +299,29 @@ func (c *Config) PromptForMissingFields() error { return fmt.Errorf("secret key is required for aliyun auth") } } - } else { + } else if c.AuthType == "nacos" { // Nacos auth if c.Username == "" { - fmt.Print("Enter username [nacos]: ") + fmt.Print("Enter username: ") input, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("failed to read username: %w", err) } - input = strings.TrimSpace(input) - if input == "" { - c.Username = "nacos" - } else { - c.Username = input + c.Username = strings.TrimSpace(input) + if c.Username == "" { + return fmt.Errorf("username is required") } } if c.Password == "" { - fmt.Print("Enter password [nacos]: ") + fmt.Print("Enter password: ") password := readPassword(reader) if password == "" { - c.Password = "nacos" - } else { - c.Password = password + return fmt.Errorf("password is required") } + c.Password = password } } + // authType == "none": skip credential prompts // Optionally prompt for namespace if c.Namespace == "" { @@ -437,21 +458,21 @@ func (c *Config) PromptForUpdate() error { // Auth type currentAuthType := c.AuthType if currentAuthType == "" { - currentAuthType = "nacos" + currentAuthType = "none" } - fmt.Printf("Enter auth type (nacos/aliyun) [%s]: ", currentAuthType) + fmt.Printf("Enter auth type (none/nacos/aliyun) [%s]: ", currentAuthType) input, err = reader.ReadString('\n') if err != nil { return fmt.Errorf("failed to read auth type: %w", err) } input = strings.TrimSpace(strings.ToLower(input)) if input != "" { - if input != "nacos" && input != "aliyun" { - return fmt.Errorf("invalid auth type: %s (must be 'nacos' or 'aliyun')", input) + if input != "none" && input != "nacos" && input != "aliyun" { + return fmt.Errorf("invalid auth type: %s (must be 'none', 'nacos' or 'aliyun')", input) } c.AuthType = input } else if c.AuthType == "" { - c.AuthType = "nacos" + c.AuthType = "none" } // Credentials based on auth type @@ -488,13 +509,13 @@ func (c *Config) PromptForUpdate() error { if c.SecretKey == "" { return fmt.Errorf("secret key is required for aliyun auth") } - } else { + } else if c.AuthType == "nacos" { // Nacos auth - Username currentUser := formatCurrent(c.Username, false) if currentUser != "" { fmt.Printf("Enter username [%s]: ", currentUser) } else { - fmt.Print("Enter username [nacos]: ") + fmt.Print("Enter username: ") } input, err = reader.ReadString('\n') if err != nil { @@ -503,23 +524,26 @@ func (c *Config) PromptForUpdate() error { input = strings.TrimSpace(input) if input != "" { c.Username = input - } else if c.Username == "" { - c.Username = "nacos" + } + if c.Username == "" { + return fmt.Errorf("username is required") } // Password if c.Password != "" { fmt.Print("Enter password [******] (press Enter to keep current): ") } else { - fmt.Print("Enter password [nacos]: ") + fmt.Print("Enter password: ") } newPwd := readPassword(reader) if newPwd != "" { c.Password = newPwd - } else if c.Password == "" { - c.Password = "nacos" + } + if c.Password == "" { + return fmt.Errorf("password is required") } } + // authType == "none": skip credential prompts // Namespace currentNS := c.Namespace diff --git a/internal/help/help.go b/internal/help/help.go index c623ff3..bb04e3d 100644 --- a/internal/help/help.go +++ b/internal/help/help.go @@ -37,38 +37,48 @@ var ( SkillGet = CommandHelp{ Command: "skill-get", - Description: "Download a skill from Nacos to local ~/.skills directory.", + Description: "Download a skill from Nacos to local directory via the Client Skill API.", Parameters: []string{ - "skillName Required. The name of the skill to download", + "skillName... Required. One or more skill names to download", + "-o, --output Output directory (default: ~/.skills)", + "--version Specific version to download (e.g. v1, v2)", + "--label Route label to resolve version (e.g. latest, stable)", }, Examples: []string{ - "# Download a skill", + "# Download the latest version of a skill", "skill-get skill-creator", "", - "# Download will create ~/.skills/skill-creator/ with:", - "# - SKILL.md (documentation)", - "# - scripts/ (script files)", - "# - references/ (reference documents)", + "# Download a specific version", + "skill-get skill-creator --version v2", + "", + "# Download via label", + "skill-get skill-creator --label stable", + "", + "# Download to a custom directory", + "skill-get skill-creator -o ~/my-skills", + "", + "# Download multiple skills", + "skill-get skill-creator skill-analyzer", }, } - SkillUpload = CommandHelp{ - Command: "skill-upload", - Description: "Upload a skill directory to Nacos as a ZIP file.", + SkillPublish = CommandHelp{ + Command: "skill-publish", + Description: "Publish a skill to Nacos by uploading it as a ZIP file (creates a draft version).\nReview and go-online operations should be done via the Nacos console.", Parameters: []string{ "skillPath Required. Path to the skill directory", - "--all Upload all skills in the specified directory", + "--all Publish all skills in the specified directory", }, Examples: []string{ - "# Upload a single skill", - "skill-upload ./my-skill", + "# Publish a single skill", + "skill-publish ./my-skill", "", - "# Upload all skills in a directory", - "skill-upload --all ./skills-folder", + "# Publish all skills in a directory", + "skill-publish --all ./skills-folder", "", "Note:", " - Skill directory must contain SKILL.md", - " - Skill names: letters, underscores (_), hyphens (-) only", + " - After publishing, use the Nacos console to review and go online", }, } @@ -134,24 +144,9 @@ var ( SkillSync = CommandHelp{ Command: "skill-sync", - Description: "Synchronize skills with Nacos (real-time updates).", - Parameters: []string{ - "skillName... Optional. One or more skill names to synchronize", - "--all Synchronize all skills", - }, - Examples: []string{ - "# Sync a single skill", - "skill-sync skill-creator", - "", - "# Sync multiple skills", - "skill-sync skill-creator skill-analyzer skill-formatter", - "", - "# Sync all skills", - "skill-sync --all", - "", - "# Skills will be downloaded to ~/.skills/", - "# Press Ctrl+C to stop synchronization", - }, + Description: "(Removed) Skill sync is no longer supported.", + Parameters: []string{}, + Examples: []string{}, } ) diff --git a/internal/skill/skill_service.go b/internal/skill/skill_service.go index 74d5172..28516b1 100644 --- a/internal/skill/skill_service.go +++ b/internal/skill/skill_service.go @@ -12,7 +12,6 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/nov11/nacos-cli/internal/client" "gopkg.in/yaml.v3" @@ -35,13 +34,19 @@ type SkillListItem struct { Description string } -// Skill represents a complete skill +// Skill represents a complete skill (new API model) type Skill struct { - Name string `json:"name"` - Description string `json:"description"` - Instruction string `json:"instruction"` - UniformId interface{} `json:"uniformId"` // Can be string or number - Resources []map[string]string `json:"resources"` + Name string `json:"name"` + Description string `json:"description"` + Instruction string `json:"instruction"` + Resource map[string]*SkillResource `json:"resource,omitempty"` +} + +// SkillResource represents a single resource in a skill +type SkillResource struct { + Name string `json:"name"` + Type string `json:"type"` + Content string `json:"content"` } // NewSkillService creates a new skill service @@ -98,7 +103,7 @@ func (s *SkillService) ListSkills(skillName string, pageNo, pageSize int) ([]Ski if resp.StatusCode != 200 { respBody, _ := io.ReadAll(resp.Body) - return nil, 0, fmt.Errorf("list skills failed: status=%d, body=%s", resp.StatusCode, string(respBody)) + return nil, 0, client.ParseHTTPError(resp.StatusCode, respBody, "list skills") } respBody, err := io.ReadAll(resp.Body) @@ -123,47 +128,57 @@ func (s *SkillService) ListSkills(skillName string, pageNo, pageSize int) ([]Ski return skillList.PageItems, skillList.TotalCount, nil } -// GetSkill retrieves a skill and saves it to local directory -func (s *SkillService) GetSkill(skillName, outputDir string) error { - const maxRetries = 3 - const retryDelay = 3 * time.Second +// GetSkill retrieves a skill via the new Client Skill API and saves it to local directory. +// Priority for version resolution: label > version > latest. +func (s *SkillService) GetSkill(skillName, outputDir string, version, label string) error { + params := url.Values{} + params.Set("namespaceId", s.client.Namespace) + params.Set("name", skillName) + if version != "" { + params.Set("version", version) + } + if label != "" { + params.Set("label", label) + } - for attempt := 1; attempt <= maxRetries; attempt++ { - err := s.getSkillWithValidation(skillName, outputDir) - if err == nil { - return nil - } + apiURL := fmt.Sprintf("http://%s/nacos/v3/client/ai/skills?%s", + s.client.ServerAddr, params.Encode()) - // Check if it's a uniformId mismatch error - if strings.Contains(err.Error(), "uniformId mismatch") { - fmt.Printf("\nuniformId is inconsistent: %v\n", err) - if attempt < maxRetries { - fmt.Printf(" 等待 3 秒后重试 (%d/%d)...\n\n", attempt, maxRetries) - time.Sleep(retryDelay) - continue - } - } + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return fmt.Errorf("failed to build request: %w", err) + } + if s.client.AccessToken != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.client.AccessToken)) + } - return err + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to get skill: %w", err) } + defer resp.Body.Close() - return fmt.Errorf("重试 %d 次后仍失败", maxRetries) -} + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %w", err) + } -// getSkillWithValidation retrieves a skill with uniformId validation -func (s *SkillService) getSkillWithValidation(skillName, outputDir string) error { - group := fmt.Sprintf("skill_%s", skillName) + if resp.StatusCode != 200 { + return client.ParseHTTPError(resp.StatusCode, respBody, "get skill") + } - // Get skill.json - skillJSON, err := s.client.GetConfig("skill.json", group) - if err != nil { - return fmt.Errorf("failed to get skill.json: %w", err) + var v3Resp V3Response + if err := json.Unmarshal(respBody, &v3Resp); err != nil { + return fmt.Errorf("failed to parse response: %w", err) + } + if v3Resp.Code != 0 { + return fmt.Errorf("get skill failed: code=%d, message=%s", v3Resp.Code, v3Resp.Message) } - // Parse skill data var skill Skill - if err := json.Unmarshal([]byte(skillJSON), &skill); err != nil { - return fmt.Errorf("failed to parse skill.json: %w", err) + if err := json.Unmarshal(v3Resp.Data, &skill); err != nil { + return fmt.Errorf("failed to parse skill: %w", err) } // Create output directory @@ -172,93 +187,28 @@ func (s *SkillService) getSkillWithValidation(skillName, outputDir string) error return fmt.Errorf("failed to create directory: %w", err) } - // Download resources - resourceContents := make(map[string]map[string]interface{}) - downloadedDataIDs := make(map[string]bool) - downloadedDataIDs["skill.json"] = true - - for _, resourceInfo := range skill.Resources { - resourceName := resourceInfo["name"] - if resourceName == "" { - continue - } - - resourceType := resourceInfo["type"] - - // Construct dataId: resource_{type}_{name}.json or resource_{name}.json if type is empty - // Replace . with __ in name (e.g., init_skill.py -> init_skill__py) - normalizedName := strings.ReplaceAll(resourceName, ".", "__") - var resourceDataID string - if resourceType != "" { - resourceDataID = fmt.Sprintf("resource_%s_%s.json", resourceType, normalizedName) - } else { - resourceDataID = fmt.Sprintf("resource_%s.json", normalizedName) - } - resourceJSON, err := s.client.GetConfig(resourceDataID, group) - if err != nil { - continue - } - - downloadedDataIDs[resourceDataID] = true - - var resourceData map[string]interface{} - if err := json.Unmarshal([]byte(resourceJSON), &resourceData); err != nil { - continue - } - - // Validate uniformId consistency - skillUniformIdStr := normalizeUniformId(skill.UniformId) - resourceUniformIdStr := normalizeUniformId(resourceData["uniformId"]) - - if skillUniformIdStr != "" && resourceUniformIdStr != "" && resourceUniformIdStr != skillUniformIdStr { - return fmt.Errorf("uniformId mismatch: skill.json has '%s', but resource '%s' has '%s'", - skillUniformIdStr, resourceName, resourceUniformIdStr) - } - - resourceContents[resourceName] = resourceData - - // Save resource file - finalName, ok := resourceData["name"].(string) - if !ok { - continue - } - - finalType, ok := resourceData["type"].(string) - if !ok { + // Save resource files + for _, res := range skill.Resource { + if res == nil || res.Content == "" { continue } - - content, ok := resourceData["content"].(string) - if !ok { - continue - } - - // Determine file directory based on type var fileDir string - if finalType != "" { - // If type is specified, use it as subdirectory name - fileDir = filepath.Join(skillDir, finalType) + if res.Type != "" { + fileDir = filepath.Join(skillDir, res.Type) } else { - // If type is empty, save in skill root directory fileDir = skillDir } - if err := os.MkdirAll(fileDir, 0755); err != nil { - return err + return fmt.Errorf("failed to create resource directory: %w", err) } - - filePath := filepath.Join(fileDir, finalName) - if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { - return err + filePath := filepath.Join(fileDir, res.Name) + if err := os.WriteFile(filePath, []byte(res.Content), 0644); err != nil { + return fmt.Errorf("failed to write resource file %s: %w", res.Name, err) } } // Generate SKILL.md - if err := s.generateSkillMD(skillDir, &skill); err != nil { - return err - } - - return nil + return s.generateSkillMD(skillDir, &skill) } // generateSkillMD creates SKILL.md file @@ -280,53 +230,59 @@ func (s *SkillService) generateSkillMD(skillDir string, skill *Skill) error { return os.WriteFile(mdPath, []byte(md.String()), 0644) } -// UploadSkill uploads a skill from local directory +// UploadSkill uploads a skill from local directory or a pre-built zip file. +// If skillPath points to a .zip file it is uploaded directly; otherwise the +// directory is packed into a zip on-the-fly (skillName/... structure). func (s *SkillService) UploadSkill(skillPath string) error { - // Create ZIP file - zipBuffer := new(bytes.Buffer) - zipWriter := zip.NewWriter(zipBuffer) + var zipBuffer *bytes.Buffer + var skillName string - skillName := filepath.Base(skillPath) - - err := filepath.Walk(skillPath, func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(strings.ToLower(skillPath), ".zip") { + // Direct zip upload + data, err := os.ReadFile(skillPath) if err != nil { - return err - } - - if info.IsDir() { - return nil + return fmt.Errorf("failed to read zip file: %w", err) } - - // Get relative path - relPath, err := filepath.Rel(skillPath, path) - if err != nil { + zipBuffer = bytes.NewBuffer(data) + // Use the zip filename (without .zip) as the display name + base := filepath.Base(skillPath) + skillName = strings.TrimSuffix(base, filepath.Ext(base)) + } else { + // Pack directory into zip + skillName = filepath.Base(skillPath) + zipBuffer = new(bytes.Buffer) + zipWriter := zip.NewWriter(zipBuffer) + + err := filepath.Walk(skillPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + relPath, err := filepath.Rel(skillPath, path) + if err != nil { + return err + } + zipPath := filepath.Join(skillName, relPath) + writer, err := zipWriter.Create(zipPath) + if err != nil { + return err + } + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(writer, file) return err - } - - // Create file in ZIP with skill directory name - zipPath := filepath.Join(skillName, relPath) - writer, err := zipWriter.Create(zipPath) + }) if err != nil { - return err + return fmt.Errorf("failed to create ZIP: %w", err) } - - // Copy file content - file, err := os.Open(path) - if err != nil { + if err := zipWriter.Close(); err != nil { return err } - defer file.Close() - - _, err = io.Copy(writer, file) - return err - }) - - if err != nil { - return fmt.Errorf("failed to create ZIP: %w", err) - } - - if err := zipWriter.Close(); err != nil { - return err } // Upload ZIP via multipart form @@ -356,13 +312,12 @@ func (s *SkillService) UploadSkill(skillPath string) error { req.Header.Set("Content-Type", writer.FormDataContentType()) - // Add authentication header if s.client.AccessToken != "" { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.client.AccessToken)) } - client := &http.Client{} - resp, err := client.Do(req) + httpClient := &http.Client{} + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("upload failed: %w", err) } @@ -370,7 +325,7 @@ func (s *SkillService) UploadSkill(skillPath string) error { if resp.StatusCode != 200 { respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("upload failed: status=%d, body=%s", resp.StatusCode, string(respBody)) + return client.ParseHTTPError(resp.StatusCode, respBody, "upload skill") } return nil @@ -411,22 +366,3 @@ func (s *SkillService) ParseSkillMD(mdPath string) (*SkillInfo, error) { return &skillInfo, nil } -// normalizeUniformId converts uniformId to string (handles both string and number types) -func normalizeUniformId(uniformId interface{}) string { - if uniformId == nil { - return "" - } - - switch v := uniformId.(type) { - case string: - return v - case float64: - return fmt.Sprintf("%.0f", v) - case int: - return fmt.Sprintf("%d", v) - case int64: - return fmt.Sprintf("%d", v) - default: - return fmt.Sprintf("%v", v) - } -} diff --git a/internal/sync/skill_syncer.go b/internal/sync/skill_syncer.go deleted file mode 100644 index 1206d06..0000000 --- a/internal/sync/skill_syncer.go +++ /dev/null @@ -1,175 +0,0 @@ -package sync - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/nov11/nacos-cli/internal/client" - "github.com/nov11/nacos-cli/internal/listener" - "github.com/nov11/nacos-cli/internal/skill" -) - -// SkillSyncer handles skill synchronization with Nacos -type SkillSyncer struct { - client *client.NacosClient - skillService *skill.SkillService - outputDir string -} - -// NewSkillSyncer creates a new skill syncer -func NewSkillSyncer(nacosClient *client.NacosClient, outputDir string) *SkillSyncer { - if outputDir == "" { - homeDir, _ := os.UserHomeDir() - outputDir = filepath.Join(homeDir, ".skills") - } - - return &SkillSyncer{ - client: nacosClient, - skillService: skill.NewSkillService(nacosClient), - outputDir: outputDir, - } -} - -// StartSync starts synchronizing a skill with Nacos -func (s *SkillSyncer) StartSync(skillName string, stopCh <-chan struct{}) error { - return s.StartSyncMultiple([]string{skillName}, stopCh) -} - -// StartSyncMultiple starts synchronizing multiple skills with Nacos -func (s *SkillSyncer) StartSyncMultiple(skillNames []string, stopCh <-chan struct{}) error { - if len(skillNames) == 0 { - return fmt.Errorf("no skills specified") - } - - fmt.Printf("Initializing synchronization for %d skill(s)...\n\n", len(skillNames)) - - // Fetch initial configurations and build listening items - var items []listener.ConfigItem - var existingCount int - for _, skillName := range skillNames { - group := fmt.Sprintf("skill_%s", skillName) - - fmt.Printf("[%s] Fetching initial configuration\n", skillName) - content, err := s.client.GetConfig("skill.json", group) - if err != nil { - // Check if it's a 404 error - if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not exist") { - fmt.Printf(" - Skill not found in Nacos, will monitor for creation\n") - // Remove local copy if exists - skillPath := filepath.Join(s.outputDir, skillName) - if _, statErr := os.Stat(skillPath); statErr == nil { - fmt.Printf(" - Removing stale local copy...\n") - if rmErr := os.RemoveAll(skillPath); rmErr != nil { - fmt.Printf(" - Failed to remove: %v\n", rmErr) - } else { - fmt.Printf(" - Local copy removed\n") - } - } - // Still add to listening items with empty MD5 to monitor for creation - items = append(items, listener.ConfigItem{ - DataID: "skill.json", - Group: group, - Tenant: s.client.Namespace, - MD5: "", // Empty MD5 for non-existent config - }) - continue - } - fmt.Printf(" - Failed to fetch: %v\n", err) - continue - } - if content == "" { - fmt.Printf(" - Skill not found\n") - continue - } - - // Calculate initial MD5 - initialMD5 := calculateMD5(content) - fmt.Printf(" - MD5: %s\n", initialMD5[:8]) - - // Download skill initially - fmt.Printf(" - Downloading to %s...\n", s.outputDir) - if err := s.skillService.GetSkill(skillName, s.outputDir); err != nil { - fmt.Printf(" - Failed to download: %v\n", err) - continue - } - fmt.Printf(" - Downloaded successfully\n\n") - existingCount++ - - // Add to listening items - items = append(items, listener.ConfigItem{ - DataID: "skill.json", - Group: group, - Tenant: s.client.Namespace, - MD5: initialMD5, - }) - } - - if len(items) == 0 { - return fmt.Errorf("no skills specified for monitoring") - } - - if existingCount > 0 { - fmt.Printf("Successfully initialized %d existing skill(s)\n\n", existingCount) - } else { - fmt.Printf("No existing skills found, monitoring for creation\n\n") - } - - // Create config listener - configListener := listener.NewConfigListener(s.client.ServerAddr, s.client.Username, s.client.Password) - - // Define change handler - handler := func(dataID, grp, tenant string) error { - // Extract skill name from group (skill_xxx) - skillName := "" - if strings.HasPrefix(grp, "skill_") { - skillName = strings.TrimPrefix(grp, "skill_") - } - - fmt.Printf("\n[%s] Configuration changed\n", skillName) - if skillName != "" { - // First check if the skill exists - content, err := s.client.GetConfig(dataID, grp) - if err != nil || content == "" { - // Check if it's a 404 error (skill was deleted) - if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not exist") { - fmt.Printf(" - Skill deleted from Nacos, removing local copy...\n") - skillPath := filepath.Join(s.outputDir, skillName) - if err := os.RemoveAll(skillPath); err != nil { - fmt.Printf(" - Failed to remove local skill: %v\n", err) - } else { - fmt.Printf(" - Local skill removed: %s\n", skillPath) - } - return nil // Don't treat this as an error - } - return fmt.Errorf("failed to fetch config: %w", err) - } - - // Skill exists, sync it - fmt.Printf(" - Syncing skill...\n") - if err := s.skillService.GetSkill(skillName, s.outputDir); err != nil { - return fmt.Errorf("failed to sync skill: %w", err) - } - - skillPath := filepath.Join(s.outputDir, skillName) - fmt.Printf(" - Synced successfully to %s\n", skillPath) - } - return nil - } - - // Start listening - fmt.Printf("Listening for changes to %d skill(s):\n", len(items)) - for _, item := range items { - skillName := strings.TrimPrefix(item.Group, "skill_") - fmt.Printf(" - %s\n", skillName) - } - fmt.Printf("\nPress Ctrl+C to stop\n\n") - - return configListener.StartListening(items, handler, stopCh) -} - -// calculateMD5 is a helper function (exported from listener package) -func calculateMD5(content string) string { - return listener.CalculateMD5(content) -} diff --git a/internal/terminal/terminal.go b/internal/terminal/terminal.go index 74136e8..9eb5c35 100644 --- a/internal/terminal/terminal.go +++ b/internal/terminal/terminal.go @@ -45,11 +45,7 @@ func completer() *readline.PrefixCompleter { readline.PcItem("--help"), readline.PcItem("-h"), ), - readline.PcItem("skill-sync", - readline.PcItem("--help"), - readline.PcItem("-h"), - ), - readline.PcItem("skill-upload", + readline.PcItem("skill-publish", readline.PcItem("--help"), readline.PcItem("-h"), readline.PcItem("--all"), @@ -161,19 +157,15 @@ func (t *Terminal) handleCommand(input string) { } else { t.getSkill(args) } - case "skill-upload": + case "skill-publish": if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { - t.showSkillUploadHelp() + t.showSkillPublishHelp() } else { t.uploadSkill(args) } case "skill-sync": - if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { - t.showSkillSyncHelp() - } else { - fmt.Println("\033[33mskill-sync is not supported in terminal mode\033[0m") - fmt.Println("\033[90mUse CLI mode:\033[0m nacos-cli skill-sync ") - } + fmt.Println("\033[33mskill-sync has been removed.\033[0m") + fmt.Println("\033[90mUse 'skill-get' to download skills.\033[0m") case "config-list": if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { t.showConfigListHelp() @@ -216,10 +208,9 @@ func (t *Terminal) showHelp() { fmt.Println("\033[1;33mSkill Management\033[0m") fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "skill-list", "List all skills", "skill-list [options]") fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "", "Options: --name, --page, --size", "") - fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "skill-get", "Download a skill to ~/.skills", "skill-get ") - fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "skill-sync", "Sync skill with Nacos (CLI only)", "skill-sync (CLI mode)") - fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "skill-upload", "Upload a skill from local", "skill-upload ") - fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "", "Upload all skills in directory", "skill-upload --all ") + fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "skill-get", "Download a skill to ~/.skills", "skill-get [--version v1] [--label stable]") + fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "skill-publish", "Publish a skill from local", "skill-publish ") + fmt.Printf("\033[32m%-20s\033[0m %-40s %-30s\n", "", "Publish all skills in directory", "skill-publish --all ") fmt.Println() // Configuration Management @@ -379,7 +370,7 @@ func (t *Terminal) getSkill(args []string) { } fmt.Printf("\033[90mDownloading skill: \033[33m%s\033[90m...\033[0m\n", skillName) - err = t.skillService.GetSkill(skillName, outputDir) + err = t.skillService.GetSkill(skillName, outputDir, "", "") if err != nil { fmt.Printf("\033[31mError:\033[0m failed to download skill '%s': %v\n", skillName, err) failCount++ @@ -762,8 +753,8 @@ func (t *Terminal) showSkillGetHelp() { help.SkillGet.FormatForTerminal() } -func (t *Terminal) showSkillUploadHelp() { - help.SkillUpload.FormatForTerminal() +func (t *Terminal) showSkillPublishHelp() { + help.SkillPublish.FormatForTerminal() } func (t *Terminal) showConfigListHelp() { @@ -779,7 +770,8 @@ func (t *Terminal) showConfigSetHelp() { } func (t *Terminal) showSkillSyncHelp() { - help.SkillSync.FormatForTerminal() + fmt.Println("\033[33mskill-sync has been removed.\033[0m") + fmt.Println("\033[90mUse 'skill-get' to download skills.\033[0m") } // truncateDesc truncates description to maxLen and appends ...... if needed