From 849221eb80b7c52ba28debb5590d7425af5b5650 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 9 Mar 2026 14:42:32 +0000 Subject: [PATCH] feat: add DaemonSpec to manifest, rename path to .core/manifest.yaml Co-Authored-By: Claude Opus 4.6 --- manifest/loader.go | 4 +- manifest/manifest.go | 61 ++++++++++++++++++++++------ manifest/manifest_test.go | 84 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 15 deletions(-) diff --git a/manifest/loader.go b/manifest/loader.go index e47eda1..9bba76c 100644 --- a/manifest/loader.go +++ b/manifest/loader.go @@ -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) diff --git a/manifest/manifest.go b/manifest/manifest.go index 72ae785..731bbb4 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -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 +} diff --git a/manifest/manifest_test.go b/manifest/manifest_test.go index 63ca253..bcf551b 100644 --- a/manifest/manifest_test.go +++ b/manifest/manifest_test.go @@ -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) +}