mirror of
https://github.com/lukaszraczylo/harness-sync.git
synced 2026-10-12 00:45:22 +00:00
feat(adapter): define Adapter interface and FileSet
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
// Package adapter defines the harness adapter interface and FileSet.
|
||||||
|
package adapter
|
||||||
|
|
||||||
|
import "github.com/lukaszraczylo/harness-sync/internal/canonical"
|
||||||
|
|
||||||
|
type Kind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
RenderedFile Kind = iota
|
||||||
|
SymlinkFile
|
||||||
|
SymlinkDir
|
||||||
|
)
|
||||||
|
|
||||||
|
func (k Kind) String() string {
|
||||||
|
switch k {
|
||||||
|
case RenderedFile:
|
||||||
|
return "rendered"
|
||||||
|
case SymlinkFile:
|
||||||
|
return "symlink-file"
|
||||||
|
case SymlinkDir:
|
||||||
|
return "symlink-dir"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Adapter interface {
|
||||||
|
Name() string
|
||||||
|
Detect() bool
|
||||||
|
Render(b *canonical.Bundle) (*FileSet, error)
|
||||||
|
Import(home string) (*ImportResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImportResult struct {
|
||||||
|
Instructions string
|
||||||
|
Skills []canonical.Skill
|
||||||
|
Agents []canonical.Agent
|
||||||
|
MCP []canonical.MCPServer
|
||||||
|
Profiles []canonical.Profile
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileSetAddAndIterate(t *testing.T) {
|
||||||
|
fs := NewFileSet()
|
||||||
|
fs.Add(File{Dest: "/tmp/a", Kind: RenderedFile, Content: []byte("a")})
|
||||||
|
fs.Add(File{Dest: "/tmp/b", Kind: SymlinkFile, SymlinkTarget: "/canon/b"})
|
||||||
|
|
||||||
|
seen := map[string]Kind{}
|
||||||
|
fs.ForEach(func(f File) {
|
||||||
|
seen[f.Dest] = f.Kind
|
||||||
|
})
|
||||||
|
assert.Equal(t, RenderedFile, seen["/tmp/a"])
|
||||||
|
assert.Equal(t, SymlinkFile, seen["/tmp/b"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKindString(t *testing.T) {
|
||||||
|
assert.Equal(t, "rendered", RenderedFile.String())
|
||||||
|
assert.Equal(t, "symlink-file", SymlinkFile.String())
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Dest string
|
||||||
|
SymlinkTarget string
|
||||||
|
Content []byte
|
||||||
|
Mode os.FileMode
|
||||||
|
Kind Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileSet struct {
|
||||||
|
files []File
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileSet() *FileSet { return &FileSet{} }
|
||||||
|
|
||||||
|
func (fs *FileSet) Add(f File) {
|
||||||
|
if f.Mode == 0 && f.Kind == RenderedFile {
|
||||||
|
f.Mode = 0o644
|
||||||
|
}
|
||||||
|
fs.files = append(fs.files, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileSet) ForEach(fn func(File)) {
|
||||||
|
for _, f := range fs.files {
|
||||||
|
fn(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileSet) Len() int { return len(fs.files) }
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Package secrets provides ${VAR} substitution for canonical configs.
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lookup resolves a placeholder name to a value. Returns false if missing.
|
||||||
|
type Lookup func(name string) (string, bool)
|
||||||
|
|
||||||
|
// OSEnv resolves via os.LookupEnv.
|
||||||
|
func OSEnv(name string) (string, bool) {
|
||||||
|
return os.LookupEnv(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute replaces ${NAME} placeholders in s using lookup. Escape with $$.
|
||||||
|
// Missing keys produce an error so secrets are never silently empty.
|
||||||
|
func Substitute(s string, lookup Lookup) (string, error) {
|
||||||
|
var b strings.Builder
|
||||||
|
i := 0
|
||||||
|
for i < len(s) {
|
||||||
|
if i+1 < len(s) && s[i] == '$' && s[i+1] == '$' {
|
||||||
|
b.WriteByte('$')
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i+1 < len(s) && s[i] == '$' && s[i+1] == '{' {
|
||||||
|
end := strings.IndexByte(s[i+2:], '}')
|
||||||
|
if end == -1 {
|
||||||
|
return "", fmt.Errorf("unterminated placeholder at offset %d", i)
|
||||||
|
}
|
||||||
|
name := s[i+2 : i+2+end]
|
||||||
|
val, ok := lookup(name)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("missing env var %q", name)
|
||||||
|
}
|
||||||
|
b.WriteString(val)
|
||||||
|
i += 2 + end + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteByte(s[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package secrets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubstituteSimple(t *testing.T) {
|
||||||
|
lookup := func(k string) (string, bool) {
|
||||||
|
if k == "FOO" {
|
||||||
|
return "bar", true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
out, err := Substitute("hello ${FOO}", lookup)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello bar", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteMissingStrict(t *testing.T) {
|
||||||
|
lookup := func(k string) (string, bool) { return "", false }
|
||||||
|
_, err := Substitute("${MISSING}", lookup)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubstituteEscaped(t *testing.T) {
|
||||||
|
lookup := func(k string) (string, bool) { return "should-not-see", true }
|
||||||
|
out, err := Substitute("$${LITERAL}", lookup)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "${LITERAL}", out)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user