mirror of
https://github.com/lukaszraczylo/harness-sync.git
synced 2026-10-12 00:45:22 +00:00
feat(canonical): load YAML/markdown into Bundle
This commit is contained in:
@@ -5,6 +5,7 @@ go 1.26.3
|
|||||||
require (
|
require (
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -12,5 +13,4 @@ require (
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package canonical
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Load(root string) (*Bundle, error) {
|
||||||
|
info, err := os.Stat(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("canonical root %s: %w", root, err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return nil, fmt.Errorf("canonical root %s is not a directory", root)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &Bundle{Root: root}
|
||||||
|
|
||||||
|
err = loadYAML(filepath.Join(root, "config.yaml"), &b.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if b.Config.ActiveProfile == "" {
|
||||||
|
return nil, fmt.Errorf("config.yaml: active_profile is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
profPath := filepath.Join(root, "profiles", b.Config.ActiveProfile+".yaml")
|
||||||
|
err = loadYAML(profPath, &b.Profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpPath := filepath.Join(root, "mcp.yaml")
|
||||||
|
if fileExists(mcpPath) {
|
||||||
|
err = loadYAML(mcpPath, &b.MCP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skills, err := loadMarkdownDir(filepath.Join(root, "skills"), "SKILL.md")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, m := range skills {
|
||||||
|
b.Skills = append(b.Skills, Skill(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
agents, err := loadMarkdownDir(filepath.Join(root, "agents"), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, m := range agents {
|
||||||
|
b.Agents = append(b.Agents, Agent(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Instructions.PerHarness = map[string]string{}
|
||||||
|
body, err := readFileIfExists(filepath.Join(root, "instructions", "global.md"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.Instructions.Global = body
|
||||||
|
|
||||||
|
perHarnessDir := filepath.Join(root, "instructions", "per-harness")
|
||||||
|
if entries, err := os.ReadDir(perHarnessDir); err == nil {
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
body, err := os.ReadFile(filepath.Join(perHarnessDir, e.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
harness := strings.TrimSuffix(e.Name(), ".md")
|
||||||
|
b.Instructions.PerHarness[harness] = string(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type markdownDoc struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Body string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMarkdownDir(dir, requiredFilename string) ([]markdownDoc, error) {
|
||||||
|
var docs []markdownDoc
|
||||||
|
if !dirExists(dir) {
|
||||||
|
return docs, nil
|
||||||
|
}
|
||||||
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if requiredFilename != "" && filepath.Base(path) != requiredFilename {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if requiredFilename == "" && !strings.HasSuffix(path, ".md") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rel, _ := filepath.Rel(dir, path)
|
||||||
|
name, desc := parseFrontmatter(body)
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSuffix(filepath.Base(path), ".md")
|
||||||
|
}
|
||||||
|
docs = append(docs, markdownDoc{
|
||||||
|
Name: name,
|
||||||
|
Description: desc,
|
||||||
|
Body: string(body),
|
||||||
|
Path: rel,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return docs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFrontmatter(b []byte) (name, description string) {
|
||||||
|
s := string(b)
|
||||||
|
if !strings.HasPrefix(s, "---\n") {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
end := strings.Index(s[4:], "\n---")
|
||||||
|
if end == -1 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
fm := s[4 : 4+end]
|
||||||
|
var meta struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
}
|
||||||
|
_ = yaml.Unmarshal([]byte(fm), &meta)
|
||||||
|
return meta.Name, meta.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadYAML(path string, out interface{}) error {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(b, out); err != nil {
|
||||||
|
return fmt.Errorf("parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFileIfExists(path string) (string, error) {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
return err == nil && !info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func dirExists(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package canonical
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad(t *testing.T) {
|
||||||
|
b, err := Load("testdata/sample")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "home", b.Config.ActiveProfile)
|
||||||
|
assert.Equal(t, []string{"claude-code", "crush"}, b.Config.EnabledHarnesses)
|
||||||
|
|
||||||
|
assert.Equal(t, "home", b.Profile.Name)
|
||||||
|
assert.Equal(t, "https://gw.lan", b.Profile.Gateway.URL)
|
||||||
|
|
||||||
|
require.Len(t, b.MCP.Servers, 1)
|
||||||
|
assert.Equal(t, "filepuff", b.MCP.Servers[0].Name)
|
||||||
|
|
||||||
|
require.Len(t, b.Skills, 1)
|
||||||
|
assert.Equal(t, "hello", b.Skills[0].Name)
|
||||||
|
|
||||||
|
require.Len(t, b.Agents, 1)
|
||||||
|
assert.Equal(t, "sample", b.Agents[0].Name)
|
||||||
|
|
||||||
|
assert.Contains(t, b.Instructions.Global, "Be helpful.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadMissingProfile(t *testing.T) {
|
||||||
|
_, err := Load("testdata/missing")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
name: sample
|
||||||
|
description: sample agent
|
||||||
|
---
|
||||||
|
sample body
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
enabled_harnesses: [claude-code, crush]
|
||||||
|
active_profile: home
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Global instructions
|
||||||
|
|
||||||
|
Be helpful.
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
servers:
|
||||||
|
- name: filepuff
|
||||||
|
command: /usr/local/bin/filepuff
|
||||||
|
transport: stdio
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
name: home
|
||||||
|
gateway:
|
||||||
|
url: https://gw.lan
|
||||||
|
token: dummy
|
||||||
|
default_model: sonnet
|
||||||
|
models:
|
||||||
|
- id: claude-sonnet-4-6
|
||||||
|
alias: sonnet
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
name: hello
|
||||||
|
description: example skill
|
||||||
|
---
|
||||||
|
hello body
|
||||||
Reference in New Issue
Block a user