feat(manifest): add compile step and marketplace index builder
Some checks failed
Security Scan / security (push) Failing after 7s
Test / test (push) Failing after 2m47s

Add manifest compilation (.core/manifest.yaml → core.json) with build
metadata (commit, tag, timestamp, signature) and marketplace index
generation by crawling directories for compiled or source manifests.

New files:
- manifest/compile.go: CompiledManifest, Compile(), ParseCompiled(),
  WriteCompiled(), LoadCompiled(), MarshalJSON()
- marketplace/builder.go: Builder.BuildFromDirs(), BuildFromManifests(),
  WriteIndex()
- cmd/scm/: CLI commands — compile, index, export

Tests: 26 new (12 manifest, 14 marketplace), all passing.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-15 13:45:24 +00:00
parent b89228030b
commit 631ddd4887
8 changed files with 942 additions and 0 deletions

102
cmd/scm/cmd_compile.go Normal file
View file

@ -0,0 +1,102 @@
package scm
import (
"crypto/ed25519"
"encoding/hex"
"os/exec"
"strings"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/manifest"
)
func addCompileCommand(parent *cli.Command) {
var (
dir string
signKey string
builtBy string
)
cmd := &cli.Command{
Use: "compile",
Short: "Compile manifest.yaml into core.json",
Long: "Read .core/manifest.yaml, attach build metadata (commit, tag), and write core.json to the project root.",
RunE: func(cmd *cli.Command, args []string) error {
return runCompile(dir, signKey, builtBy)
},
}
cmd.Flags().StringVarP(&dir, "dir", "d", ".", "Project root directory")
cmd.Flags().StringVar(&signKey, "sign-key", "", "Hex-encoded ed25519 private key for signing")
cmd.Flags().StringVar(&builtBy, "built-by", "core scm compile", "Builder identity")
parent.AddCommand(cmd)
}
func runCompile(dir, signKeyHex, builtBy string) error {
medium, err := io.NewSandboxed(dir)
if err != nil {
return cli.WrapVerb(err, "open", dir)
}
m, err := manifest.Load(medium, ".")
if err != nil {
return cli.WrapVerb(err, "load", "manifest")
}
opts := manifest.CompileOptions{
Commit: gitCommit(dir),
Tag: gitTag(dir),
BuiltBy: builtBy,
}
if signKeyHex != "" {
keyBytes, err := hex.DecodeString(signKeyHex)
if err != nil {
return cli.WrapVerb(err, "decode", "sign key")
}
opts.SignKey = ed25519.PrivateKey(keyBytes)
}
cm, err := manifest.Compile(m, opts)
if err != nil {
return err
}
if err := manifest.WriteCompiled(medium, ".", cm); err != nil {
return err
}
cli.Blank()
cli.Print(" %s %s\n", successStyle.Render("compiled"), valueStyle.Render(m.Code))
cli.Print(" %s %s\n", dimStyle.Render("version:"), valueStyle.Render(m.Version))
if opts.Commit != "" {
cli.Print(" %s %s\n", dimStyle.Render("commit:"), valueStyle.Render(opts.Commit))
}
if opts.Tag != "" {
cli.Print(" %s %s\n", dimStyle.Render("tag:"), valueStyle.Render(opts.Tag))
}
cli.Print(" %s %s\n", dimStyle.Render("output:"), valueStyle.Render("core.json"))
cli.Blank()
return nil
}
// gitCommit returns the current HEAD commit hash, or empty on error.
func gitCommit(dir string) string {
out, err := exec.Command("git", "-C", dir, "rev-parse", "HEAD").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
// gitTag returns the tag pointing at HEAD, or empty if none.
func gitTag(dir string) string {
out, err := exec.Command("git", "-C", dir, "describe", "--tags", "--exact-match", "HEAD").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}

59
cmd/scm/cmd_export.go Normal file
View file

@ -0,0 +1,59 @@
package scm
import (
"fmt"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/manifest"
)
func addExportCommand(parent *cli.Command) {
var dir string
cmd := &cli.Command{
Use: "export",
Short: "Export compiled manifest as JSON",
Long: "Read core.json from the project root and print it to stdout. Falls back to compiling .core/manifest.yaml if core.json is not found.",
RunE: func(cmd *cli.Command, args []string) error {
return runExport(dir)
},
}
cmd.Flags().StringVarP(&dir, "dir", "d", ".", "Project root directory")
parent.AddCommand(cmd)
}
func runExport(dir string) error {
medium, err := io.NewSandboxed(dir)
if err != nil {
return cli.WrapVerb(err, "open", dir)
}
// Try core.json first.
cm, err := manifest.LoadCompiled(medium, ".")
if err != nil {
// Fall back to compiling from source.
m, loadErr := manifest.Load(medium, ".")
if loadErr != nil {
return cli.WrapVerb(loadErr, "load", "manifest")
}
cm, err = manifest.Compile(m, manifest.CompileOptions{
Commit: gitCommit(dir),
Tag: gitTag(dir),
BuiltBy: "core scm export",
})
if err != nil {
return err
}
}
data, err := manifest.MarshalJSON(cm)
if err != nil {
return cli.WrapVerb(err, "marshal", "manifest")
}
fmt.Println(string(data))
return nil
}

61
cmd/scm/cmd_index.go Normal file
View file

@ -0,0 +1,61 @@
package scm
import (
"fmt"
"path/filepath"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-scm/marketplace"
)
func addIndexCommand(parent *cli.Command) {
var (
dirs []string
output string
baseURL string
org string
)
cmd := &cli.Command{
Use: "index",
Short: "Build marketplace index from directories",
Long: "Scan directories for core.json or .core/manifest.yaml files and generate a marketplace index.json.",
RunE: func(cmd *cli.Command, args []string) error {
if len(dirs) == 0 {
dirs = []string{"."}
}
return runIndex(dirs, output, baseURL, org)
},
}
cmd.Flags().StringArrayVarP(&dirs, "dir", "d", nil, "Directories to scan (repeatable, default: current directory)")
cmd.Flags().StringVarP(&output, "output", "o", "index.json", "Output path for the index file")
cmd.Flags().StringVar(&baseURL, "base-url", "", "Base URL for repo links (e.g. https://forge.lthn.ai)")
cmd.Flags().StringVar(&org, "org", "", "Organisation for repo links")
parent.AddCommand(cmd)
}
func runIndex(dirs []string, output, baseURL, org string) error {
b := &marketplace.Builder{
BaseURL: baseURL,
Org: org,
}
idx, err := b.BuildFromDirs(dirs...)
if err != nil {
return cli.WrapVerb(err, "build", "index")
}
absOutput, _ := filepath.Abs(output)
if err := marketplace.WriteIndex(absOutput, idx); err != nil {
return err
}
cli.Blank()
cli.Print(" %s %s\n", successStyle.Render("index built"), valueStyle.Render(output))
cli.Print(" %s %s\n", dimStyle.Render("modules:"), numberStyle.Render(fmt.Sprintf("%d", len(idx.Modules))))
cli.Blank()
return nil
}

39
cmd/scm/cmd_scm.go Normal file
View file

@ -0,0 +1,39 @@
// Package scm provides CLI commands for manifest compilation and marketplace
// index generation.
//
// Commands:
// - compile: Compile .core/manifest.yaml into core.json
// - index: Build marketplace index from repository directories
// - export: Export a compiled manifest as JSON to stdout
package scm
import (
"forge.lthn.ai/core/cli/pkg/cli"
)
func init() {
cli.RegisterCommands(AddScmCommands)
}
// Style aliases from shared package.
var (
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
valueStyle = cli.ValueStyle
numberStyle = cli.NumberStyle
)
// AddScmCommands registers the 'scm' command and all subcommands.
func AddScmCommands(root *cli.Command) {
scmCmd := &cli.Command{
Use: "scm",
Short: "SCM manifest and marketplace operations",
Long: "Compile manifests, build marketplace indexes, and export distribution metadata.",
}
root.AddCommand(scmCmd)
addCompileCommand(scmCmd)
addIndexCommand(scmCmd)
addExportCommand(scmCmd)
}

98
manifest/compile.go Normal file
View file

@ -0,0 +1,98 @@
package manifest
import (
"crypto/ed25519"
"encoding/json"
"fmt"
"path/filepath"
"time"
"forge.lthn.ai/core/go-io"
)
// CompiledManifest is the distribution-ready form of a manifest, written as
// core.json at the repository root (not inside .core/). It embeds the
// original Manifest and adds build metadata stapled at compile time.
type CompiledManifest struct {
Manifest `json:",inline" yaml:",inline"`
// Build metadata — populated by Compile.
Commit string `json:"commit,omitempty" yaml:"commit,omitempty"`
Tag string `json:"tag,omitempty" yaml:"tag,omitempty"`
BuiltAt string `json:"built_at,omitempty" yaml:"built_at,omitempty"`
BuiltBy string `json:"built_by,omitempty" yaml:"built_by,omitempty"`
}
// CompileOptions controls how Compile populates the build metadata.
type CompileOptions struct {
Commit string // Git commit hash
Tag string // Git tag (e.g. v1.0.0)
BuiltBy string // Builder identity (e.g. "core build")
SignKey ed25519.PrivateKey // Optional — signs before compiling
}
// Compile produces a CompiledManifest from a source manifest and build
// options. If opts.SignKey is provided the manifest is signed first.
func Compile(m *Manifest, opts CompileOptions) (*CompiledManifest, error) {
if m == nil {
return nil, fmt.Errorf("manifest.Compile: nil manifest")
}
if m.Code == "" {
return nil, fmt.Errorf("manifest.Compile: missing code")
}
if m.Version == "" {
return nil, fmt.Errorf("manifest.Compile: missing version")
}
// Sign if a key is supplied.
if opts.SignKey != nil {
if err := Sign(m, opts.SignKey); err != nil {
return nil, fmt.Errorf("manifest.Compile: %w", err)
}
}
return &CompiledManifest{
Manifest: *m,
Commit: opts.Commit,
Tag: opts.Tag,
BuiltAt: time.Now().UTC().Format(time.RFC3339),
BuiltBy: opts.BuiltBy,
}, nil
}
// MarshalJSON serialises a CompiledManifest to JSON bytes.
func MarshalJSON(cm *CompiledManifest) ([]byte, error) {
return json.MarshalIndent(cm, "", " ")
}
// ParseCompiled decodes a core.json into a CompiledManifest.
func ParseCompiled(data []byte) (*CompiledManifest, error) {
var cm CompiledManifest
if err := json.Unmarshal(data, &cm); err != nil {
return nil, fmt.Errorf("manifest.ParseCompiled: %w", err)
}
return &cm, nil
}
const compiledPath = "core.json"
// WriteCompiled writes a CompiledManifest as core.json to the given root
// directory. The file lives at the distribution root, not inside .core/.
func WriteCompiled(medium io.Medium, root string, cm *CompiledManifest) error {
data, err := MarshalJSON(cm)
if err != nil {
return fmt.Errorf("manifest.WriteCompiled: %w", err)
}
path := filepath.Join(root, compiledPath)
return medium.Write(path, string(data))
}
// LoadCompiled reads and parses a core.json from the given root directory.
func LoadCompiled(medium io.Medium, root string) (*CompiledManifest, error) {
path := filepath.Join(root, compiledPath)
data, err := medium.Read(path)
if err != nil {
return nil, fmt.Errorf("manifest.LoadCompiled: %w", err)
}
return ParseCompiled([]byte(data))
}

181
manifest/compile_test.go Normal file
View file

@ -0,0 +1,181 @@
package manifest
import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"testing"
"forge.lthn.ai/core/go-io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCompile_Good(t *testing.T) {
m := &Manifest{
Code: "my-widget",
Name: "My Widget",
Version: "1.2.3",
Author: "tester",
}
cm, err := Compile(m, CompileOptions{
Commit: "abc1234",
Tag: "v1.2.3",
BuiltBy: "core build",
})
require.NoError(t, err)
assert.Equal(t, "my-widget", cm.Code)
assert.Equal(t, "My Widget", cm.Name)
assert.Equal(t, "1.2.3", cm.Version)
assert.Equal(t, "abc1234", cm.Commit)
assert.Equal(t, "v1.2.3", cm.Tag)
assert.Equal(t, "core build", cm.BuiltBy)
assert.NotEmpty(t, cm.BuiltAt)
}
func TestCompile_Good_WithSigning(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
m := &Manifest{
Code: "signed-mod",
Name: "Signed Module",
Version: "0.1.0",
}
cm, err := Compile(m, CompileOptions{
Commit: "def5678",
SignKey: priv,
})
require.NoError(t, err)
assert.NotEmpty(t, cm.Sign)
// Verify signature is valid.
ok, vErr := Verify(&cm.Manifest, pub)
require.NoError(t, vErr)
assert.True(t, ok)
}
func TestCompile_Bad_NilManifest(t *testing.T) {
_, err := Compile(nil, CompileOptions{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "nil manifest")
}
func TestCompile_Bad_MissingCode(t *testing.T) {
m := &Manifest{Version: "1.0.0"}
_, err := Compile(m, CompileOptions{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing code")
}
func TestCompile_Bad_MissingVersion(t *testing.T) {
m := &Manifest{Code: "test"}
_, err := Compile(m, CompileOptions{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing version")
}
func TestMarshalJSON_Good(t *testing.T) {
cm := &CompiledManifest{
Manifest: Manifest{
Code: "test-mod",
Name: "Test Module",
Version: "1.0.0",
},
Commit: "abc123",
Tag: "v1.0.0",
BuiltAt: "2026-03-15T10:00:00Z",
BuiltBy: "test",
}
data, err := MarshalJSON(cm)
require.NoError(t, err)
// Round-trip: parse back.
parsed, err := ParseCompiled(data)
require.NoError(t, err)
assert.Equal(t, "test-mod", parsed.Code)
assert.Equal(t, "abc123", parsed.Commit)
assert.Equal(t, "v1.0.0", parsed.Tag)
assert.Equal(t, "2026-03-15T10:00:00Z", parsed.BuiltAt)
}
func TestParseCompiled_Good(t *testing.T) {
raw := `{
"code": "demo",
"name": "Demo",
"version": "0.5.0",
"commit": "aaa111",
"tag": "v0.5.0",
"built_at": "2026-03-15T12:00:00Z",
"built_by": "ci"
}`
cm, err := ParseCompiled([]byte(raw))
require.NoError(t, err)
assert.Equal(t, "demo", cm.Code)
assert.Equal(t, "aaa111", cm.Commit)
assert.Equal(t, "ci", cm.BuiltBy)
}
func TestParseCompiled_Bad(t *testing.T) {
_, err := ParseCompiled([]byte("not json"))
assert.Error(t, err)
}
func TestWriteCompiled_Good(t *testing.T) {
medium := io.NewMockMedium()
cm := &CompiledManifest{
Manifest: Manifest{
Code: "write-test",
Name: "Write Test",
Version: "1.0.0",
},
Commit: "ccc333",
}
err := WriteCompiled(medium, "/project", cm)
require.NoError(t, err)
// Verify the file was written.
content, err := medium.Read("/project/core.json")
require.NoError(t, err)
var parsed CompiledManifest
require.NoError(t, json.Unmarshal([]byte(content), &parsed))
assert.Equal(t, "write-test", parsed.Code)
assert.Equal(t, "ccc333", parsed.Commit)
}
func TestLoadCompiled_Good(t *testing.T) {
medium := io.NewMockMedium()
raw := `{"code":"load-test","name":"Load Test","version":"2.0.0","commit":"ddd444"}`
medium.Files["/project/core.json"] = raw
cm, err := LoadCompiled(medium, "/project")
require.NoError(t, err)
assert.Equal(t, "load-test", cm.Code)
assert.Equal(t, "ddd444", cm.Commit)
}
func TestLoadCompiled_Bad_NotFound(t *testing.T) {
medium := io.NewMockMedium()
_, err := LoadCompiled(medium, "/missing")
assert.Error(t, err)
}
func TestCompile_Good_MinimalOptions(t *testing.T) {
m := &Manifest{
Code: "minimal",
Name: "Minimal",
Version: "0.0.1",
}
cm, err := Compile(m, CompileOptions{})
require.NoError(t, err)
assert.Empty(t, cm.Commit)
assert.Empty(t, cm.Tag)
assert.Empty(t, cm.BuiltBy)
assert.NotEmpty(t, cm.BuiltAt)
}

158
marketplace/builder.go Normal file
View file

@ -0,0 +1,158 @@
package marketplace
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"forge.lthn.ai/core/go-scm/manifest"
)
// IndexVersion is the current marketplace index format version.
const IndexVersion = 1
// Builder constructs a marketplace Index by crawling directories for
// core.json (compiled manifests) or .core/manifest.yaml files.
type Builder struct {
// BaseURL is the prefix for constructing repository URLs, e.g.
// "https://forge.lthn.ai". When set, module Repo is derived as
// BaseURL + "/" + org + "/" + code.
BaseURL string
// Org is the default organisation used when constructing Repo URLs.
Org string
}
// BuildFromDirs scans each directory for subdirectories containing either
// core.json (preferred) or .core/manifest.yaml. Each valid manifest is
// added to the resulting Index as a Module.
func (b *Builder) BuildFromDirs(dirs ...string) (*Index, error) {
var modules []Module
seen := make(map[string]bool)
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, fmt.Errorf("marketplace.Builder: read %s: %w", dir, err)
}
for _, e := range entries {
if !e.IsDir() {
continue
}
m, err := b.loadFromDir(filepath.Join(dir, e.Name()))
if err != nil {
log.Printf("marketplace: skipping %s: %v", e.Name(), err)
continue
}
if m == nil {
continue
}
if seen[m.Code] {
continue
}
seen[m.Code] = true
mod := Module{
Code: m.Code,
Name: m.Name,
Repo: b.repoURL(m.Code),
}
modules = append(modules, mod)
}
}
sort.Slice(modules, func(i, j int) bool {
return modules[i].Code < modules[j].Code
})
return &Index{
Version: IndexVersion,
Modules: modules,
}, nil
}
// BuildFromManifests constructs an Index from pre-loaded manifests.
// This is useful when manifests have already been collected (e.g. from
// a Forge API crawl).
func BuildFromManifests(manifests []*manifest.Manifest) *Index {
var modules []Module
seen := make(map[string]bool)
for _, m := range manifests {
if m == nil || m.Code == "" {
continue
}
if seen[m.Code] {
continue
}
seen[m.Code] = true
modules = append(modules, Module{
Code: m.Code,
Name: m.Name,
})
}
sort.Slice(modules, func(i, j int) bool {
return modules[i].Code < modules[j].Code
})
return &Index{
Version: IndexVersion,
Modules: modules,
}
}
// WriteIndex serialises an Index to JSON and writes it to the given path.
func WriteIndex(path string, idx *Index) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("marketplace.WriteIndex: mkdir: %w", err)
}
data, err := json.MarshalIndent(idx, "", " ")
if err != nil {
return fmt.Errorf("marketplace.WriteIndex: marshal: %w", err)
}
return os.WriteFile(path, data, 0644)
}
// loadFromDir tries core.json first, then falls back to .core/manifest.yaml.
func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) {
// Prefer compiled manifest (core.json).
coreJSON := filepath.Join(dir, "core.json")
if data, err := os.ReadFile(coreJSON); err == nil {
cm, err := manifest.ParseCompiled(data)
if err != nil {
return nil, fmt.Errorf("parse core.json: %w", err)
}
return &cm.Manifest, nil
}
// Fall back to source manifest.
manifestYAML := filepath.Join(dir, ".core", "manifest.yaml")
data, err := os.ReadFile(manifestYAML)
if err != nil {
return nil, nil // No manifest — skip silently.
}
m, err := manifest.Parse(data)
if err != nil {
return nil, fmt.Errorf("parse manifest.yaml: %w", err)
}
return m, nil
}
// repoURL constructs a module repository URL from the builder config.
func (b *Builder) repoURL(code string) string {
if b.BaseURL == "" || b.Org == "" {
return ""
}
return b.BaseURL + "/" + b.Org + "/" + code
}

244
marketplace/builder_test.go Normal file
View file

@ -0,0 +1,244 @@
package marketplace
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"forge.lthn.ai/core/go-scm/manifest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// writeManifestYAML writes a .core/manifest.yaml for a module directory.
func writeManifestYAML(t *testing.T, dir, code, name, version string) {
t.Helper()
coreDir := filepath.Join(dir, ".core")
require.NoError(t, os.MkdirAll(coreDir, 0755))
yaml := "code: " + code + "\nname: " + name + "\nversion: " + version + "\n"
require.NoError(t, os.WriteFile(filepath.Join(coreDir, "manifest.yaml"), []byte(yaml), 0644))
}
// writeCoreJSON writes a core.json for a module directory.
func writeCoreJSON(t *testing.T, dir, code, name, version string) {
t.Helper()
cm := manifest.CompiledManifest{
Manifest: manifest.Manifest{
Code: code,
Name: name,
Version: version,
},
Commit: "abc123",
}
data, err := json.Marshal(cm)
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(dir, "core.json"), data, 0644))
}
func TestBuildFromDirs_Good_ManifestYAML(t *testing.T) {
root := t.TempDir()
modDir := filepath.Join(root, "my-widget")
require.NoError(t, os.MkdirAll(modDir, 0755))
writeManifestYAML(t, modDir, "my-widget", "My Widget", "1.0.0")
b := &Builder{BaseURL: "https://forge.lthn.ai", Org: "core"}
idx, err := b.BuildFromDirs(root)
require.NoError(t, err)
require.Len(t, idx.Modules, 1)
assert.Equal(t, "my-widget", idx.Modules[0].Code)
assert.Equal(t, "My Widget", idx.Modules[0].Name)
assert.Equal(t, "https://forge.lthn.ai/core/my-widget", idx.Modules[0].Repo)
assert.Equal(t, IndexVersion, idx.Version)
}
func TestBuildFromDirs_Good_CoreJSON(t *testing.T) {
root := t.TempDir()
modDir := filepath.Join(root, "compiled-mod")
require.NoError(t, os.MkdirAll(modDir, 0755))
writeCoreJSON(t, modDir, "compiled-mod", "Compiled Module", "2.0.0")
b := &Builder{}
idx, err := b.BuildFromDirs(root)
require.NoError(t, err)
require.Len(t, idx.Modules, 1)
assert.Equal(t, "compiled-mod", idx.Modules[0].Code)
assert.Equal(t, "Compiled Module", idx.Modules[0].Name)
}
func TestBuildFromDirs_Good_PrefersCompiledOverSource(t *testing.T) {
root := t.TempDir()
modDir := filepath.Join(root, "dual-mod")
require.NoError(t, os.MkdirAll(modDir, 0755))
writeManifestYAML(t, modDir, "source-code", "Source Name", "1.0.0")
writeCoreJSON(t, modDir, "compiled-code", "Compiled Name", "2.0.0")
b := &Builder{}
idx, err := b.BuildFromDirs(root)
require.NoError(t, err)
// core.json is preferred — its code should appear.
require.Len(t, idx.Modules, 1)
assert.Equal(t, "compiled-code", idx.Modules[0].Code)
}
func TestBuildFromDirs_Good_SkipsNoManifest(t *testing.T) {
root := t.TempDir()
// Directory with no manifest.
require.NoError(t, os.MkdirAll(filepath.Join(root, "no-manifest"), 0755))
// Directory with a manifest.
modDir := filepath.Join(root, "has-manifest")
require.NoError(t, os.MkdirAll(modDir, 0755))
writeManifestYAML(t, modDir, "has-manifest", "Has Manifest", "0.1.0")
b := &Builder{}
idx, err := b.BuildFromDirs(root)
require.NoError(t, err)
assert.Len(t, idx.Modules, 1)
}
func TestBuildFromDirs_Good_Deduplicates(t *testing.T) {
dir1 := t.TempDir()
dir2 := t.TempDir()
mod1 := filepath.Join(dir1, "shared")
mod2 := filepath.Join(dir2, "shared")
require.NoError(t, os.MkdirAll(mod1, 0755))
require.NoError(t, os.MkdirAll(mod2, 0755))
writeManifestYAML(t, mod1, "shared", "Shared V1", "1.0.0")
writeManifestYAML(t, mod2, "shared", "Shared V2", "2.0.0")
b := &Builder{}
idx, err := b.BuildFromDirs(dir1, dir2)
require.NoError(t, err)
// First occurrence wins.
assert.Len(t, idx.Modules, 1)
assert.Equal(t, "shared", idx.Modules[0].Code)
}
func TestBuildFromDirs_Good_SortsByCode(t *testing.T) {
root := t.TempDir()
for _, name := range []string{"charlie", "alpha", "bravo"} {
d := filepath.Join(root, name)
require.NoError(t, os.MkdirAll(d, 0755))
writeManifestYAML(t, d, name, name, "1.0.0")
}
b := &Builder{}
idx, err := b.BuildFromDirs(root)
require.NoError(t, err)
require.Len(t, idx.Modules, 3)
assert.Equal(t, "alpha", idx.Modules[0].Code)
assert.Equal(t, "bravo", idx.Modules[1].Code)
assert.Equal(t, "charlie", idx.Modules[2].Code)
}
func TestBuildFromDirs_Good_EmptyDir(t *testing.T) {
root := t.TempDir()
b := &Builder{}
idx, err := b.BuildFromDirs(root)
require.NoError(t, err)
assert.Empty(t, idx.Modules)
assert.Equal(t, IndexVersion, idx.Version)
}
func TestBuildFromDirs_Good_NonexistentDir(t *testing.T) {
b := &Builder{}
idx, err := b.BuildFromDirs("/nonexistent/path")
require.NoError(t, err)
assert.Empty(t, idx.Modules)
}
func TestBuildFromDirs_Good_NoRepoURLWithoutConfig(t *testing.T) {
root := t.TempDir()
modDir := filepath.Join(root, "mod")
require.NoError(t, os.MkdirAll(modDir, 0755))
writeManifestYAML(t, modDir, "mod", "Module", "1.0.0")
b := &Builder{} // No BaseURL or Org.
idx, err := b.BuildFromDirs(root)
require.NoError(t, err)
assert.Empty(t, idx.Modules[0].Repo)
}
func TestBuildFromManifests_Good(t *testing.T) {
manifests := []*manifest.Manifest{
{Code: "bravo", Name: "Bravo"},
{Code: "alpha", Name: "Alpha"},
}
idx := BuildFromManifests(manifests)
require.Len(t, idx.Modules, 2)
assert.Equal(t, "alpha", idx.Modules[0].Code)
assert.Equal(t, "bravo", idx.Modules[1].Code)
assert.Equal(t, IndexVersion, idx.Version)
}
func TestBuildFromManifests_Good_SkipsNil(t *testing.T) {
manifests := []*manifest.Manifest{
nil,
{Code: "valid", Name: "Valid"},
{Code: "", Name: "Empty Code"},
}
idx := BuildFromManifests(manifests)
assert.Len(t, idx.Modules, 1)
assert.Equal(t, "valid", idx.Modules[0].Code)
}
func TestBuildFromManifests_Good_Deduplicates(t *testing.T) {
manifests := []*manifest.Manifest{
{Code: "dup", Name: "First"},
{Code: "dup", Name: "Second"},
}
idx := BuildFromManifests(manifests)
assert.Len(t, idx.Modules, 1)
}
func TestWriteIndex_Good(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "marketplace", "index.json")
idx := &Index{
Version: 1,
Modules: []Module{
{Code: "test-mod", Name: "Test Module"},
},
}
err := WriteIndex(path, idx)
require.NoError(t, err)
data, err := os.ReadFile(path)
require.NoError(t, err)
var parsed Index
require.NoError(t, json.Unmarshal(data, &parsed))
assert.Len(t, parsed.Modules, 1)
assert.Equal(t, "test-mod", parsed.Modules[0].Code)
}
func TestWriteIndex_Good_RoundTrip(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "index.json")
root := t.TempDir()
modDir := filepath.Join(root, "roundtrip")
require.NoError(t, os.MkdirAll(modDir, 0755))
writeManifestYAML(t, modDir, "roundtrip", "Roundtrip Module", "3.0.0")
b := &Builder{BaseURL: "https://forge.lthn.ai", Org: "core"}
idx, err := b.BuildFromDirs(root)
require.NoError(t, err)
require.NoError(t, WriteIndex(path, idx))
data, err := os.ReadFile(path)
require.NoError(t, err)
parsed, err := ParseIndex(data)
require.NoError(t, err)
require.Len(t, parsed.Modules, 1)
assert.Equal(t, "roundtrip", parsed.Modules[0].Code)
assert.Equal(t, "https://forge.lthn.ai/core/roundtrip", parsed.Modules[0].Repo)
}