feat: add DaemonSpec to manifest, rename path to .core/manifest.yaml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-09 14:42:32 +00:00
parent 68379b6b58
commit 849221eb80
3 changed files with 134 additions and 15 deletions

View file

@ -9,14 +9,14 @@ import (
"gopkg.in/yaml.v3"
)
const manifestPath = ".core/view.yml"
const manifestPath = ".core/manifest.yaml"
// MarshalYAML serializes a manifest to YAML bytes.
func MarshalYAML(m *Manifest) ([]byte, error) {
return yaml.Marshal(m)
}
// Load reads and parses a .core/view.yml from the given root directory.
// Load reads and parses a .core/manifest.yaml from the given root directory.
func Load(medium io.Medium, root string) (*Manifest, error) {
path := filepath.Join(root, manifestPath)
data, err := medium.Read(path)

View file

@ -6,25 +6,35 @@ import (
"gopkg.in/yaml.v3"
)
// Manifest represents a .core/view.yml application manifest.
// Manifest represents a .core/manifest.yaml application manifest.
type Manifest struct {
Code string `yaml:"code"`
Name string `yaml:"name"`
Version string `yaml:"version"`
Sign string `yaml:"sign"`
Layout string `yaml:"layout"`
Slots map[string]string `yaml:"slots"`
Code string `yaml:"code" json:"code"`
Name string `yaml:"name" json:"name"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Version string `yaml:"version" json:"version"`
Sign string `yaml:"sign,omitempty" json:"sign,omitempty"`
Layout string `yaml:"layout,omitempty" json:"layout,omitempty"`
Slots map[string]string `yaml:"slots,omitempty" json:"slots,omitempty"`
Permissions Permissions `yaml:"permissions"`
Modules []string `yaml:"modules"`
Permissions Permissions `yaml:"permissions,omitempty" json:"permissions,omitempty"`
Modules []string `yaml:"modules,omitempty" json:"modules,omitempty"`
Daemons map[string]DaemonSpec `yaml:"daemons,omitempty" json:"daemons,omitempty"`
}
// Permissions declares the I/O capabilities a module requires.
type Permissions struct {
Read []string `yaml:"read"`
Write []string `yaml:"write"`
Net []string `yaml:"net"`
Run []string `yaml:"run"`
Read []string `yaml:"read" json:"read"`
Write []string `yaml:"write" json:"write"`
Net []string `yaml:"net" json:"net"`
Run []string `yaml:"run" json:"run"`
}
// DaemonSpec describes a long-running process managed by the runtime.
type DaemonSpec struct {
Binary string `yaml:"binary,omitempty" json:"binary,omitempty"`
Args []string `yaml:"args,omitempty" json:"args,omitempty"`
Health string `yaml:"health,omitempty" json:"health,omitempty"`
Default bool `yaml:"default,omitempty" json:"default,omitempty"`
}
// Parse decodes YAML bytes into a Manifest.
@ -48,3 +58,28 @@ func (m *Manifest) SlotNames() []string {
}
return names
}
// DefaultDaemon returns the name, spec, and true for the default daemon.
// A daemon is the default if it has Default:true, or if it is the only daemon
// in the map. Returns empty values and false if no default can be determined.
func (m *Manifest) DefaultDaemon() (string, DaemonSpec, bool) {
if len(m.Daemons) == 0 {
return "", DaemonSpec{}, false
}
// Look for an explicit default.
for name, spec := range m.Daemons {
if spec.Default {
return name, spec, true
}
}
// If exactly one daemon exists, treat it as the implicit default.
if len(m.Daemons) == 1 {
for name, spec := range m.Daemons {
return name, spec, true
}
}
return "", DaemonSpec{}, false
}

View file

@ -63,3 +63,87 @@ func TestManifest_SlotNames_Good(t *testing.T) {
assert.Contains(t, names, "main-content")
assert.Len(t, names, 2)
}
func TestParse_Good_WithDaemons(t *testing.T) {
raw := `
code: my-service
name: My Service
description: A test service with daemons
version: 1.0.0
daemons:
api:
binary: ./bin/api
args: ["--port", "8080"]
health: /healthz
default: true
worker:
binary: ./bin/worker
args: ["--concurrency", "4"]
health: /ready
`
m, err := Parse([]byte(raw))
require.NoError(t, err)
assert.Equal(t, "my-service", m.Code)
assert.Equal(t, "My Service", m.Name)
assert.Equal(t, "A test service with daemons", m.Description)
assert.Equal(t, "1.0.0", m.Version)
assert.Len(t, m.Daemons, 2)
api, ok := m.Daemons["api"]
require.True(t, ok)
assert.Equal(t, "./bin/api", api.Binary)
assert.Equal(t, []string{"--port", "8080"}, api.Args)
assert.Equal(t, "/healthz", api.Health)
assert.True(t, api.Default)
worker, ok := m.Daemons["worker"]
require.True(t, ok)
assert.Equal(t, "./bin/worker", worker.Binary)
assert.Equal(t, []string{"--concurrency", "4"}, worker.Args)
assert.Equal(t, "/ready", worker.Health)
assert.False(t, worker.Default)
}
func TestManifest_DefaultDaemon_Good(t *testing.T) {
m := Manifest{
Daemons: map[string]DaemonSpec{
"api": {
Binary: "./bin/api",
Default: true,
},
"worker": {
Binary: "./bin/worker",
},
},
}
name, spec, ok := m.DefaultDaemon()
assert.True(t, ok)
assert.Equal(t, "api", name)
assert.Equal(t, "./bin/api", spec.Binary)
assert.True(t, spec.Default)
}
func TestManifest_DefaultDaemon_Bad_NoDaemons(t *testing.T) {
m := Manifest{}
name, spec, ok := m.DefaultDaemon()
assert.False(t, ok)
assert.Empty(t, name)
assert.Empty(t, spec.Binary)
}
func TestManifest_DefaultDaemon_Good_SingleImplicit(t *testing.T) {
m := Manifest{
Daemons: map[string]DaemonSpec{
"server": {
Binary: "./bin/server",
Args: []string{"--port", "3000"},
},
},
}
name, spec, ok := m.DefaultDaemon()
assert.True(t, ok)
assert.Equal(t, "server", name)
assert.Equal(t, "./bin/server", spec.Binary)
assert.False(t, spec.Default)
}