diff --git a/.gitignore b/.gitignore index 9493657..37016d3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ bin .idea .DS_Store .aiignore -CLAUDE.md \ No newline at end of file +CLAUDE.md +syncai.exe \ No newline at end of file diff --git a/internal/generator/copilot.go b/internal/generator/copilot.go index 5904a2e..36acac3 100644 --- a/internal/generator/copilot.go +++ b/internal/generator/copilot.go @@ -2,7 +2,6 @@ package generator import ( "sort" - "strconv" "strings" "syncai/internal/model" ) @@ -12,15 +11,14 @@ type CopilotRulesGenerator struct{} func (g CopilotRulesGenerator) GenerateRules(metadata model.RulesMetadata, content []byte) []byte { var sb strings.Builder sb.WriteString("---\n") - // Copilot uses applyTo, but also include description for preservation sb.WriteString("description: ") - sb.WriteString(strconv.Quote(metadata.Description)) + sb.WriteString(quoteIfNeeded(metadata.Description)) sb.WriteString("\n") sb.WriteString("applyTo: ") if metadata.Globs == "" { - sb.WriteString(strconv.Quote("**")) + sb.WriteString(quoteIfNeeded("**")) } else { - sb.WriteString(strconv.Quote(metadata.Globs)) + sb.WriteString(quoteIfNeeded(metadata.Globs)) } sb.WriteString("\n") if len(metadata.ExtraFields) > 0 { @@ -35,7 +33,7 @@ func (g CopilotRulesGenerator) GenerateRules(metadata model.RulesMetadata, conte for _, k := range keys { sb.WriteString(k) sb.WriteString(": ") - sb.WriteString(strconv.Quote(metadata.ExtraFields[k])) + sb.WriteString(quoteIfNeeded(metadata.ExtraFields[k])) sb.WriteString("\n") } } diff --git a/internal/generator/cursor.go b/internal/generator/cursor.go index 59e4237..a113bd2 100644 --- a/internal/generator/cursor.go +++ b/internal/generator/cursor.go @@ -3,7 +3,6 @@ package generator import ( "fmt" "sort" - "strconv" "strings" "syncai/internal/model" ) @@ -14,7 +13,7 @@ func (g CursorRulesGenerator) GenerateRules(metadata model.RulesMetadata, conten var sb strings.Builder sb.WriteString("---\n") sb.WriteString("description: ") - sb.WriteString(strconv.Quote(metadata.Description)) + sb.WriteString(quoteIfNeeded(metadata.Description)) sb.WriteString("\n") sb.WriteString(fmt.Sprintf("alwaysApply: %t\n", metadata.IsAlwaysApply())) sb.WriteString("globs: ") @@ -33,7 +32,7 @@ func (g CursorRulesGenerator) GenerateRules(metadata model.RulesMetadata, conten for _, k := range keys { sb.WriteString(k) sb.WriteString(": ") - sb.WriteString(strconv.Quote(metadata.ExtraFields[k])) + sb.WriteString(quoteIfNeeded(metadata.ExtraFields[k])) sb.WriteString("\n") } } diff --git a/internal/generator/util.go b/internal/generator/util.go index b72c512..d3d9495 100644 --- a/internal/generator/util.go +++ b/internal/generator/util.go @@ -1,8 +1,26 @@ package generator -import "strings" +import ( + "strconv" + "strings" +) + +const yamlSpecialChars = " \":{}[]#&*!|>'%@`" func isReservedField(field string) bool { fl := strings.ToLower(field) return fl == "description" || fl == "globs" || fl == "applyto" || fl == "alwaysapply" } + +func quoteIfNeeded(s string) string { + if s == "" { + return strconv.Quote(s) + } + if strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") { + return s + } + if strings.ContainsAny(s, yamlSpecialChars) { + return strconv.Quote(s) + } + return s +} diff --git a/internal/util/parser.go b/internal/util/parser.go index 2bc1571..915a2a9 100644 --- a/internal/util/parser.go +++ b/internal/util/parser.go @@ -66,8 +66,9 @@ func ParseFile(path string) (model.Document, error) { } if foundEnd && yamlBuf.Len() > 0 { + raw := yamlBuf.Bytes() var m map[string]interface{} - if err := yaml.Unmarshal(yamlBuf.Bytes(), &m); err == nil { + if err := yaml.Unmarshal(raw, &m); err == nil { for k, v := range m { if v == nil { continue @@ -77,12 +78,37 @@ func ParseFile(path string) (model.Document, error) { if s, ok := cleanYAMLValue(vv); ok { metadata[k] = s } + case []interface{}: + // join sequence values into comma-separated metadata value + var parts []string + for _, item := range vv { + if s, ok := cleanYAMLValue(item); ok { + parts = append(parts, s) + } + } + if len(parts) > 0 { + metadata[k] = strings.Join(parts, ",") + } default: if s, ok := cleanYAMLValue(v); ok { metadata[k] = s } } } + } else { + for _, line := range strings.Split(string(raw), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + val := strings.TrimSpace(parts[1]) + metadata[key] = val + } } } else { // If we didn't find the end delimiter, reset body to full data