scaffold

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 29, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package scaffold implements `dockyard new` — the no-template project scaffold (RFC §9.1, §10).

The no-template path is the first-class one (RFC §10): `dockyard new <name>` with no --template produces a blank but working MCP server — a manifest, one example contract-first tool, the generated contract artifacts, a runnable main, and a test. Templates (analytics-widgets, approval-flows, inspector) are optional product-pattern showcases layered on by Wave 9: the template- discovery seam (Phase 24) is implemented in this package alongside the no-template scaffold, and a named template is materialised via GenerateFromTemplate.

Contract-first by construction (P1, RFC §6.1). The example tool's input and output are typed Go structs in the scaffolded project's internal/contracts package. Their JSON Schema and TypeScript artifacts are GENERATED here, by internal/codegen, from those structs — never hand-written. The scaffold emits the generated files carrying the `Code generated … DO NOT EDIT.` header so a developer who runs `dockyard generate` (Phase 18) gets identical output: the scaffold is just the first generate.

Determinism. Generate writes byte-deterministic output for a fixed Options — the same project name always yields the same tree. That is what makes the golden test in scaffold_golden_test.go meaningful: an accidental change to a scaffolded file fails CI as a visible diff.

Index

Constants

This section is empty.

Variables

View Source
var ErrFileCollision = errors.New("dockyard/internal/scaffold: scaffold output would overwrite an existing file")

ErrFileCollision is the sentinel for a scaffold output that would overwrite an existing file in the target directory. It can only occur under Options.Here (without it, a non-empty target is refused outright by ErrTargetExists). `dockyard new` never silently overwrites a file.

View Source
var ErrInvalidName = errors.New("dockyard/internal/scaffold: invalid project name")

ErrInvalidName is the sentinel for a rejected project name. Callers branch with errors.Is(err, ErrInvalidName).

View Source
var ErrTargetExists = errors.New("dockyard/internal/scaffold: target directory already exists and is not empty")

ErrTargetExists is the sentinel for a non-empty / pre-existing target directory. `dockyard new` never overwrites an existing project (pass Options.Here to scaffold into a non-empty directory anyway).

View Source
var ErrUnknownTemplate = errors.New("dockyard/internal/scaffold: unknown template")

ErrUnknownTemplate is the sentinel for an unregistered template name. Callers branch with errors.Is(err, ErrUnknownTemplate).

Functions

func RegisterTemplate

func RegisterTemplate(t Template)

RegisterTemplate adds a Template to the package-wide default Registry. Call from an init() block in the builtin file next to a template's source tree.

func RequiresTasksEngine added in v1.1.0

func RequiresTasksEngine(m *manifest.Manifest) bool

RequiresTasksEngine reports whether any tool in m declares task_support: optional or required — the manifest-side signal that a project's main.go must construct a tasks.Engine and attach it via server.Options{Tasks: engine}.

It is the detection seam D-164 closes: the scaffold consults it at generation time to decide whether to emit the engine-wired main.go template, and `dockyard run` consults it at run time to surface a warning when the manifest declares task support but the project's main.go source does not appear to attach the engine.

A nil manifest, an empty Tools slice, and a Tools slice in which every tool declares task_support: forbidden (or the empty zero value, which the loader normalises to forbidden — RFC §8.4) all yield false. One tool with task_support: optional or required is enough to yield true; the engine attaches once per server, not per tool.

Types

type EmbeddedTemplate

type EmbeddedTemplate struct {
	// NameValue is the template's wire name.
	NameValue string
	// SummaryValue is the one-line CLI help.
	SummaryValue string
	// Source is the embed.FS containing the template's source tree.
	// PathPrefix below points at the directory within it that is the
	// project root (so a `//go:embed all:templates/analytics-widgets`
	// FS with PathPrefix `templates/analytics-widgets` materialises
	// starting from the project root).
	Source fs.FS
	// PathPrefix is the directory inside Source whose contents are the
	// project root. The empty string means "the FS root itself".
	PathPrefix string
	// TextExts is the set of file extensions whose contents are treated as
	// text and run through the substitution table. Anything not listed is
	// copied byte-exact. A trailing-dotless ".go", ".yaml" form is expected;
	// matching is case-sensitive.
	TextExts []string
	// SubstitutionsFor returns the substitution table for one materialisation.
	// Called once per Materialise; the returned slice is read-only.
	SubstitutionsFor func(opts Options) []Substitution
	// PathRemap rewrites a project-relative source path to a different
	// destination path in the materialised project. The pairs are applied
	// in order as prefix substitutions: the first matching `From` prefix
	// is replaced with `To`. Used by templates whose in-repo layout
	// differs from the materialised layout — typically because Go's
	// `internal/` barrier prevents the framework's own tests from
	// importing the template's contract package, so the in-repo layout
	// uses a non-internal path and PathRemap re-applies the `internal/`
	// prefix on materialisation. Optional; nil/empty means a 1:1 copy.
	PathRemap []PathSubstitution
}

EmbeddedTemplate is a reusable Template implementation backed by an embed.FS snapshot of a `templates/<name>/` directory. The builtin analytics-widgets template uses it; future builtin templates use it too. A test stub can implement Template directly without going through this.

func (*EmbeddedTemplate) Materialise

func (t *EmbeddedTemplate) Materialise(opts Options) (map[string][]byte, error)

Materialise walks the embedded source tree, applies the substitution table to every textual file, and returns the in-memory project file set keyed by project-relative path. It is deterministic: a given (opts, embed.FS) always yields the same bytes — the golden test depends on it.

Filename convention: a file with the trailing `.tmpl` extension is materialised under the same path with `.tmpl` stripped (e.g. `cmd/server/main.go.tmpl` → `cmd/server/main.go`). The `.tmpl` marker exists so the template's Go source does not satisfy `go build ./...` from the framework root — the placeholder module path would not parse — while the materialised project's files are real `.go` files. The marker extension is always added to TextExts so substitution is applied.

func (*EmbeddedTemplate) Name

func (t *EmbeddedTemplate) Name() string

Name implements Template.

func (*EmbeddedTemplate) Summary

func (t *EmbeddedTemplate) Summary() string

Summary implements Template.

type GreetInput

type GreetInput struct {
	// Name is who to greet. Required.
	Name string `json:"name"`
	// Greeting is the salutation to use; defaults to "Hello" when empty.
	Greeting string `json:"greeting,omitempty"`
}

GreetInput is the example tool's typed input contract. Keep it minimal: one required string, one optional one — enough to show the contract-first shape (a typed struct, JSON tags) without modelling a real domain.

type GreetOutput

type GreetOutput struct {
	// Message is the assembled greeting.
	Message string `json:"message"`
	// Length is the rune length of Message — a trivial derived field so the
	// output struct is not a single-field shell.
	Length int `json:"length"`
}

GreetOutput is the example tool's typed output contract — the structured, UI-facing payload (RFC §6.3).

type Options

type Options struct {
	// Name is the project name — also the directory name, the manifest name,
	// and the MCP server identity. Required; validated against nameRE.
	Name string
	// Dir is the parent directory the project directory is created under.
	// Empty means the current working directory.
	Dir string
	// ModulePath is the Go module path for the scaffolded project's go.mod.
	// Empty falls back to "example.com/<name>" — a placeholder a developer
	// renames; the scaffold compiles either way.
	ModulePath string
	// DockyardReplace, when non-empty, adds a `replace` directive to the
	// scaffolded go.mod pointing the Dockyard runtime import at a local
	// checkout. It is the pre-release workflow: until Dockyard is published to
	// a module registry, a scaffolded project cannot `go get` it, so the CLI
	// and the integration test set this to a local Dockyard path. A released
	// Dockyard leaves it empty and the scaffold depends on the published
	// module version directly.
	DockyardReplace string
	// DockyardWebPath is the web/ sibling of DockyardReplace — an absolute path
	// to the Dockyard checkout's web/ directory. Templates that depend on the
	// in-repo @dockyard/bridge and @dockyard/ui packages substitute it into
	// their package.json `file:` dependencies so a scaffolded project can
	// `npm install` before the packages are published to npm. Empty leaves
	// published-version fallbacks in place (post-publish workflow).
	DockyardWebPath string
	// ExampleToolTaskSupport sets the no-template scaffold's example tool's
	// task_support declaration. The zero value ("") preserves the historical
	// behaviour — the example tool ships as task_support: forbidden, a plain
	// synchronous tool. Setting it to manifest.TaskSupportOptional or
	// TaskSupportRequired makes the scaffold both (a) declare the example
	// tool that way in the rendered manifest and (b) emit the engine-wired
	// shape of main.go — a `tasks.NewInMemoryStore()` + `tasks.NewEngine(...)`
	// block + `server.Options{Tasks: engine}` (D-164).
	//
	// This is the manifest-side knob the scaffold consumes; RequiresTasksEngine
	// is the corresponding read side that `dockyard run` consults at start time
	// against the project's loaded manifest.
	ExampleToolTaskSupport manifest.TaskSupport

	// DockyardVersion is the version of the Dockyard runtime module to pin in
	// the scaffolded go.mod's require directive — normally the version of the
	// `dockyard` CLI doing the scaffolding (cli.ResolvedVersion()). When it is
	// a real release version (vX.Y.Z) the require pins it, so a project that
	// drops the local `replace` resolves the published module from the proxy
	// without a hand edit. An empty value or the dev placeholder leaves the
	// historical `v0.0.0` placeholder (only ever resolved through the replace
	// directive — the build-from-source path).
	DockyardVersion string

	// Here permits scaffolding into a target directory that already has
	// content (the `dockyard new --here` flag). With Here false (the
	// default) a non-empty target is refused with ErrTargetExists. With
	// Here true the existing content is left in place, but a scaffold output
	// that would overwrite an existing file is still refused with
	// ErrFileCollision — `dockyard new` never silently overwrites a file.
	Here bool
}

Options configures one `dockyard new` invocation.

type PathSubstitution

type PathSubstitution struct {
	From string
	To   string
}

PathSubstitution is one entry of an EmbeddedTemplate's PathRemap. From is a project-relative path prefix in the embed.FS; To is the prefix in the materialised project.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds the process-wide Template set. RegisterTemplate / LookupTemplate / ListTemplates are the seam Phases 25, 26, and any post-V1 template plug into; nothing about a specific template's name lives in the CLI or in this file.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry. Exposed for tests that want an isolated Registry (the package-wide RegisterTemplate is shared).

func (*Registry) List

func (r *Registry) List() []Template

List returns the registered Templates ordered by name — a stable order for the CLI's `dockyard new --help` listing and the smoke script's grep.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (Template, bool)

Lookup returns the Template registered under name and true, or a zero value and false. Concurrent-safe: ConcurrentRegisterAndLookup proves it under -race.

func (*Registry) Register

func (r *Registry) Register(t Template)

Register adds a Template to this Registry. A duplicate name panics — a template's name is part of its public CLI surface, so a collision is a program error rather than a runtime user error.

type Result

type Result struct {
	// Dir is the project directory that was created.
	Dir string
	// Files is the project-relative path of every file written, sorted.
	Files []string
}

Result reports what Generate produced.

func Generate

func Generate(opts Options) (Result, error)

Generate scaffolds a blank, working MCP server project (RFC §9.1, §10). It validates the name, refuses a non-empty target, builds the file set in memory (so a generation failure leaves nothing half-written), then writes the tree to disk.

The returned Result lists every file written. Generate is deterministic: the same Options always yields the same bytes.

func GenerateFromTemplate

func GenerateFromTemplate(opts Options, templateName string) (Result, error)

GenerateFromTemplate materialises the named Template into a working project (RFC §10). It validates the project name, refuses a non-empty target, looks up the Template by name (typed ErrUnknownTemplate on miss), builds the project file set in memory via the Template, then writes the tree to disk.

The returned Result lists every file written. GenerateFromTemplate is deterministic: identical (opts, templateName) always yields the same bytes.

type Substitution

type Substitution struct {
	// From is the placeholder token a template author writes into the source
	// tree (e.g. "__PROJECT_NAME__"). Tokens are double-underscored on each
	// side so a real source line never accidentally collides with one.
	From string
	// To is the value the From token is replaced with — the user's choice
	// (the project name) or a derived value (the module path).
	To string
}

Substitution is one find-and-replace applied to every textual file in an embedded template tree (the substitution table is empty for binary or fixture-exact files — they round-trip byte-for-byte).

type Template

type Template interface {
	// Name is the wire name a developer passes to `dockyard new --template <name>`.
	// It matches the Registry key and is short, kebab-case.
	Name() string
	// Summary is one-line help text the CLI prints next to the template name.
	Summary() string
	// Materialise builds the project file set in memory, keyed by project-
	// relative path. The returned map is the entire scaffolded project — the
	// CLI writes it to disk verbatim. Identical Options must yield identical
	// bytes (the materialiser is deterministic — the golden test depends on it).
	Materialise(opts Options) (map[string][]byte, error)
}

Template is one product-pattern showcase a developer can scaffold with `dockyard new --template <name>`. Implementations are registered with RegisterTemplate, typically from an init() block in a small builtin file that lives next to the template's source tree (CLAUDE.md §4.4: interface + Registry + driver-style init registration).

A Template's Materialise produces the project-relative file set the scaffolder writes to disk. Substitutions (project name, module path, the pre-release Dockyard replace directive) are the Template's responsibility: each Template knows which of its files are textual and which must stay byte-exact (a binary asset, a fixture, a checked-in built artifact).

func ListTemplates

func ListTemplates() []Template

ListTemplates returns every Template registered in the package-wide default Registry, sorted by name.

func LookupTemplate

func LookupTemplate(name string) (Template, bool)

LookupTemplate is the package-wide default Registry's Lookup.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL