diff --git a/AGENTS.md b/AGENTS.md
index bfb3d41..e96b2e2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -99,10 +99,13 @@ The Go module ports the **architecture**, not every mechanism. The build
lifecycle (pre → reload → post), producers, model output, dryrun, and watch
semantics match TypeScript. Two things differ by necessity:
-- **Actions are a registry, not dynamic modules.** TypeScript declares actions
- in a config file and loads them with `require()`. Go cannot load code at
- runtime, so actions are registered programmatically via `ModelSpec.Actions`
- (`map[string]ActionDef`) — see `go/producer.go`.
+- **The config declares actions; the registry binds them.** Like TypeScript,
+ Go resolves `.model-config/model-config.jsonic` (auto-created when missing),
+ writes `model-config.json`, and takes the action order from
+ `sys.model.order.action` (`go/config.go`). But Go cannot load code at
+ runtime, so the action *functions* are registered programmatically via
+ `ModelSpec.Actions` (`map[string]ActionDef`) and bound to the
+ config-declared names — see `go/producer.go`.
- **Watching polls modification times** (`go/watch.go`) rather than using
chokidar.
diff --git a/docs/how-to.md b/docs/how-to.md
index 02f4252..67cd6c5 100644
--- a/docs/how-to.md
+++ b/docs/how-to.md
@@ -4,6 +4,7 @@ Focused recipes for specific tasks. Each is self-contained. If you are new to
the tool, do the [tutorial](./tutorial.md) first; for exhaustive detail see the
[reference](./reference.md).
+- [Initialize a new model](#initialize-a-new-model)
- [Build once vs. watch](#build-once-vs-watch)
- [Pass build arguments to actions](#pass-build-arguments-to-actions)
- [Write a build action](#write-a-build-action)
@@ -20,6 +21,31 @@ the tool, do the [tutorial](./tutorial.md) first; for exhaustive detail see the
- [Tune watch behavior](#tune-watch-behavior)
+## Initialize a new model
+
+Scaffold a starter `model/model.jsonic` and `model/.model-config/model-config.jsonic`:
+
+```bash
+voxgig-model init # in the current directory
+voxgig-model init my-project # under my-project/
+```
+
+Existing files are left untouched. Then build it:
+
+```bash
+voxgig-model model/model.jsonic
+```
+
+The Go CLI is identical (`go run github.com/voxgig/model/go/cmd/voxgig-model init`).
+From code, use `initModel(dir, fs)` (TypeScript) or `model.Init(dir)` (Go):
+
+```js
+const Fs = require('node:fs')
+const { initModel } = require('@voxgig/model')
+const { created, skipped } = initModel('.', Fs)
+```
+
+
## Build once vs. watch
Once, then exit:
diff --git a/docs/reference.md b/docs/reference.md
index 416fc11..f60e541 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -61,6 +61,22 @@ voxgig-model --dryrun model/model.jsonic
voxgig-model -b '{env:prod, region:eu-west-1}' model/model.jsonic
```
+### `init` — scaffold a new model
+
+```
+voxgig-model init [dir]
+```
+
+Creates a starter project under `
/model` (default `dir`: the current
+directory): `model/model.jsonic` and `model/.model-config/model-config.jsonic`.
+Existing files are left untouched. Both the TypeScript and Go CLIs support
+this and produce identical files.
+
+```bash
+voxgig-model init # scaffold ./model
+voxgig-model model/model.jsonic # build it
+```
+
## Project layout
diff --git a/go/README.md b/go/README.md
index c33b384..de0297f 100644
--- a/go/README.md
+++ b/go/README.md
@@ -52,8 +52,11 @@ This port mirrors the architecture — the build lifecycle (pre → reload →
post), producers, dryrun, and watch semantics — but adapts a few mechanisms to
Go:
-- **Actions are registered programmatically** (`ModelSpec.Actions`) instead of
- being loaded from a config file: Go cannot `require()` code at runtime.
+- **Actions are registered programmatically** (`ModelSpec.Actions`): the
+ `.model-config/model-config.jsonic` file still declares which actions run and
+ in what order (and is auto-created and written to `model-config.json`, as in
+ TypeScript), but Go binds each declared name to a registered func rather than
+ `require()`-ing a module.
- **Watching polls modification times** instead of using chokidar.
- **Imports** resolve relative to the model base directory; the resolver
briefly changes the working directory because the Go aontu `Generate(src)`
diff --git a/go/cmd/voxgig-model/main.go b/go/cmd/voxgig-model/main.go
index e4e8f05..3362e2c 100644
--- a/go/cmd/voxgig-model/main.go
+++ b/go/cmd/voxgig-model/main.go
@@ -27,6 +27,10 @@ func main() {
// separated from main (which only adds os.Exit and signal handling) so the
// behavior can be tested directly.
func run(args []string, stderr io.Writer) int {
+ if len(args) > 0 && args[0] == "init" {
+ return runInit(args[1:], stderr)
+ }
+
fs := flag.NewFlagSet("voxgig-model", flag.ContinueOnError)
fs.SetOutput(stderr)
watch := fs.Bool("w", false, "watch and rebuild on change")
@@ -79,6 +83,27 @@ func run(args []string, stderr io.Writer) int {
return 0
}
+// runInit scaffolds a starter model and config under /model.
+func runInit(args []string, out io.Writer) int {
+ dir := "."
+ if len(args) > 0 && args[0] != "" {
+ dir = args[0]
+ }
+ created, skipped, err := model.Init(dir)
+ if err != nil {
+ fmt.Fprintln(out, "ERROR:", err)
+ return 1
+ }
+ for _, p := range created {
+ fmt.Fprintln(out, "created:", p)
+ }
+ for _, p := range skipped {
+ fmt.Fprintln(out, "exists: ", p)
+ }
+ fmt.Fprintln(out, "Next: voxgig-model "+filepath.Join(dir, "model", "model.jsonic"))
+ return 0
+}
+
func reportErrs(br *model.BuildResult, stderr io.Writer) {
if br == nil {
return
diff --git a/go/cmd/voxgig-model/main_test.go b/go/cmd/voxgig-model/main_test.go
index a04d807..a11a55a 100644
--- a/go/cmd/voxgig-model/main_test.go
+++ b/go/cmd/voxgig-model/main_test.go
@@ -77,3 +77,25 @@ func TestCLIBadModel(t *testing.T) {
t.Fatalf("stderr = %q", out.String())
}
}
+
+func TestCLIInit(t *testing.T) {
+ dir := t.TempDir()
+
+ var out bytes.Buffer
+ if code := run([]string{"init", dir}, &out); code != 0 {
+ t.Fatalf("init exit %d: %s", code, out.String())
+ }
+ if !strings.Contains(out.String(), "created:") {
+ t.Fatalf("init output = %q", out.String())
+ }
+ root := filepath.Join(dir, "model", "model.jsonic")
+ if _, err := os.Stat(root); err != nil {
+ t.Fatalf("init did not scaffold the model: %v", err)
+ }
+
+ // The scaffold should then build through the CLI.
+ var out2 bytes.Buffer
+ if code := run([]string{"-g", "silent", root}, &out2); code != 0 {
+ t.Fatalf("building the scaffold exit %d: %s", code, out2.String())
+ }
+}
diff --git a/go/config.go b/go/config.go
new file mode 100644
index 0000000..c25d8c4
--- /dev/null
+++ b/go/config.go
@@ -0,0 +1,67 @@
+/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */
+
+package model
+
+import "path/filepath"
+
+// configStub is written when a model has no config file yet. It is
+// self-contained (no package import) so it resolves with the Go aontu engine,
+// and mirrors the effective shape of the TypeScript default config.
+const configStub = "# Model configuration. Declare build actions and their order here.\n" +
+ "#\n" +
+ "# Example (Go binds each declared action name to a func registered in\n" +
+ "# ModelSpec.Actions):\n" +
+ "# sys: model: action: { example: load: 'build/example' }\n" +
+ "# sys: model: order: action: 'example'\n" +
+ "\n" +
+ "sys: model: action: {}\n" +
+ "sys: model: order: action: *''\n"
+
+// Config is the build for a model's .model-config/model-config.jsonic. It
+// mirrors the TypeScript Config: it resolves the config model, writes
+// model-config.json, and is the source of the action order. The file is
+// auto-created from configStub when missing.
+type Config struct {
+ build *Build
+ log Log
+}
+
+// newConfig sets up (and bootstraps) the config build for a model base.
+func newConfig(base string, spec ModelSpec, log Log) *Config {
+ cbase := filepath.Join(base, ".model-config")
+ cpath := filepath.Join(cbase, "model-config.jsonic")
+
+ cb := NewBuild(BuildSpec{
+ Name: "config",
+ Path: cpath,
+ Base: cbase,
+ Dryrun: spec.Dryrun,
+ Resolver: spec.Resolver,
+ Log: log,
+ Res: []ProducerDef{{Path: "/", Build: ModelProducer}},
+ })
+
+ ensureConfigFile(cb.FS, cpath, cbase)
+ return &Config{build: cb, log: log}
+}
+
+// ensureConfigFile writes the default config stub if none exists. It uses the
+// build's filesystem, so a dryrun keeps the stub in memory.
+func ensureConfigFile(fs FS, cpath, cbase string) {
+ if _, err := fs.Stat(cpath); err == nil {
+ return
+ }
+ _ = fs.MkdirAll(cbase, 0o755)
+ _ = fs.WriteFile(cpath, []byte(configStub), 0o644)
+}
+
+// Run resolves the config model and writes model-config.json.
+func (c *Config) Run() *BuildResult { return c.build.Run(false) }
+
+// Model returns the resolved config model (valid after Run).
+func (c *Config) Model() map[string]any {
+ if c == nil || c.build == nil {
+ return nil
+ }
+ return c.build.Model
+}
diff --git a/go/config_test.go b/go/config_test.go
new file mode 100644
index 0000000..88ef8bd
--- /dev/null
+++ b/go/config_test.go
@@ -0,0 +1,64 @@
+/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */
+
+package model
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// New auto-creates the config file and writes model-config.json.
+func TestConfigAutoCreated(t *testing.T) {
+ dir := t.TempDir()
+ writeFile(t, dir, "model.jsonic", "x: 1\n")
+
+ m := New(ModelSpec{Path: filepath.Join(dir, "model.jsonic"), Base: dir})
+ if br := m.Run(); !br.OK {
+ t.Fatalf("run failed: %v", br.Errs)
+ }
+ if _, err := os.Stat(filepath.Join(dir, ".model-config", "model-config.jsonic")); err != nil {
+ t.Fatalf("config file not auto-created: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dir, ".model-config", "model-config.json")); err != nil {
+ t.Fatalf("model-config.json not written: %v", err)
+ }
+ if m.Config().Model() == nil {
+ t.Fatal("config model not resolved")
+ }
+}
+
+// The config's sys.model.order.action drives the action run order, overriding
+// the registry's default (sorted) order.
+func TestConfigDrivesActionOrder(t *testing.T) {
+ dir := t.TempDir()
+ mdir := filepath.Join(dir, "model")
+ cdir := filepath.Join(mdir, ".model-config")
+ if err := os.MkdirAll(cdir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ writeFile(t, mdir, "model.jsonic", "x: 1\n")
+ writeFile(t, cdir, "model-config.jsonic",
+ "sys: model: action: { a: load: 'x', b: load: 'y' }\n"+
+ "sys: model: order: action: 'b,a'\n")
+
+ var order []string
+ mk := func(n string) ActionDef {
+ return ActionDef{Run: func(_ map[string]any, _ *Build, _ *BuildContext) ActionResult {
+ order = append(order, n)
+ return ActionResult{OK: true}
+ }}
+ }
+ m := New(ModelSpec{
+ Path: filepath.Join(mdir, "model.jsonic"),
+ Base: mdir,
+ Actions: map[string]ActionDef{"a": mk("a"), "b": mk("b")},
+ })
+ if br := m.Run(); !br.OK {
+ t.Fatalf("run failed: %v", br.Errs)
+ }
+ if strings.Join(order, ",") != "b,a" {
+ t.Fatalf("action order = %v, want [b a] (from config order.action)", order)
+ }
+}
diff --git a/go/init.go b/go/init.go
new file mode 100644
index 0000000..5b312b6
--- /dev/null
+++ b/go/init.go
@@ -0,0 +1,52 @@
+/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */
+
+package model
+
+import (
+ "os"
+ "path/filepath"
+)
+
+const starterModel = "# Voxgig model. Edit this file, then build it:\n" +
+ "# voxgig-model model/model.jsonic\n" +
+ "#\n" +
+ "# Models are unified .jsonic - add types, defaults, references, imports.\n" +
+ "# Tutorial: https://github.com/voxgig/model/blob/main/docs/tutorial.md\n" +
+ "\n" +
+ "name: 'my-model'\n"
+
+const starterConfig = "# Model configuration. Declare build actions and their order here.\n" +
+ "#\n" +
+ "# Example (TypeScript loads the module; Go binds the name to a func\n" +
+ "# registered in ModelSpec.Actions):\n" +
+ "# sys: model: action: { example: load: 'build/example' }\n" +
+ "# sys: model: order: action: 'example'\n" +
+ "\n" +
+ "sys: model: action: {}\n" +
+ "sys: model: order: action: *''\n"
+
+// Init scaffolds a starter model and config under /model. Existing files
+// are left untouched. It returns the paths created and the paths skipped.
+func Init(dir string) (created, skipped []string, err error) {
+ if dir == "" {
+ dir = "."
+ }
+ files := []struct{ path, content string }{
+ {filepath.Join(dir, "model", "model.jsonic"), starterModel},
+ {filepath.Join(dir, "model", ".model-config", "model-config.jsonic"), starterConfig},
+ }
+ for _, f := range files {
+ if _, serr := os.Stat(f.path); serr == nil {
+ skipped = append(skipped, f.path)
+ continue
+ }
+ if merr := os.MkdirAll(filepath.Dir(f.path), 0o755); merr != nil {
+ return created, skipped, merr
+ }
+ if werr := os.WriteFile(f.path, []byte(f.content), 0o644); werr != nil {
+ return created, skipped, werr
+ }
+ created = append(created, f.path)
+ }
+ return created, skipped, nil
+}
diff --git a/go/init_test.go b/go/init_test.go
new file mode 100644
index 0000000..140823d
--- /dev/null
+++ b/go/init_test.go
@@ -0,0 +1,56 @@
+/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */
+
+package model
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// Init scaffolds the starter files, and skips them on a second run.
+func TestInitScaffolds(t *testing.T) {
+ dir := t.TempDir()
+
+ created, skipped, err := Init(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(created) != 2 || len(skipped) != 0 {
+ t.Fatalf("created=%v skipped=%v", created, skipped)
+ }
+ for _, p := range []string{
+ filepath.Join(dir, "model", "model.jsonic"),
+ filepath.Join(dir, "model", ".model-config", "model-config.jsonic"),
+ } {
+ if _, err := os.Stat(p); err != nil {
+ t.Fatalf("expected %s to exist: %v", p, err)
+ }
+ }
+
+ created2, skipped2, err := Init(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(created2) != 0 || len(skipped2) != 2 {
+ t.Fatalf("re-init created=%v skipped=%v (should skip existing)", created2, skipped2)
+ }
+}
+
+// A freshly scaffolded project builds successfully.
+func TestInitThenBuild(t *testing.T) {
+ dir := t.TempDir()
+ if _, _, err := Init(dir); err != nil {
+ t.Fatal(err)
+ }
+ m := New(ModelSpec{
+ Path: filepath.Join(dir, "model", "model.jsonic"),
+ Base: filepath.Join(dir, "model"),
+ })
+ if br := m.Run(); !br.OK {
+ t.Fatalf("scaffolded model failed to build: %v", br.Errs)
+ }
+ if m.Build().Model["name"] != "my-model" {
+ t.Fatalf("scaffolded model = %#v", m.Build().Model)
+ }
+}
diff --git a/go/model.go b/go/model.go
index cf111c0..923af7d 100644
--- a/go/model.go
+++ b/go/model.go
@@ -5,9 +5,10 @@
// over it, once or in a rebuild-on-change watch loop.
//
// The TypeScript implementation in ts/ is canonical; this package is kept in
-// architectural parity. Two mechanisms differ by necessity: actions are
-// registered programmatically (Go cannot require() code at runtime), and
-// watching polls modification times (rather than using chokidar).
+// architectural parity. Two mechanisms differ by necessity: action functions
+// are registered programmatically (Go cannot require() code at runtime),
+// though the .model-config file still declares which actions run and in what
+// order; and watching polls modification times (rather than using chokidar).
package model
import (
@@ -23,11 +24,14 @@ const Version = "0.1.0"
const DefaultIdle = 111 * time.Millisecond
// Model unifies a .jsonic model and runs producers (the model writer and any
-// registered actions) over it. It can build once or watch and rebuild.
+// registered actions) over it. It can build once or watch and rebuild, and
+// resolves a .model-config/model-config.jsonic config (auto-created when
+// missing) that declares the action order.
type Model struct {
- build *Build
- watch *Watch
- log Log
+ config *Config
+ build *Build
+ watch *Watch
+ log Log
}
// New creates a Model from a spec.
@@ -47,7 +51,9 @@ func New(spec ModelSpec) *Model {
idle = DefaultIdle
}
- bspec := BuildSpec{
+ config := newConfig(base, spec, log)
+
+ build := NewBuild(BuildSpec{
Name: "model",
Path: spec.Path,
Base: base,
@@ -63,25 +69,48 @@ func New(spec ModelSpec) *Model {
{Path: "/", Build: ModelProducer},
{Path: "/", Build: LocalProducer},
},
+ })
+ build.Use["config"] = config
+
+ m := &Model{
+ config: config,
+ build: build,
+ watch: NewWatch(build, "model", idle),
+ log: log,
}
- build := NewBuild(bspec)
- return &Model{
- build: build,
- watch: NewWatch(build, "model", idle),
- log: log,
+ // Re-resolve the config on each watch rebuild so config edits are picked up.
+ m.watch.reload = func() {
+ config.build.InvalidateCache()
+ config.Run()
}
+
+ return m
}
-// Run builds the model once and returns the result.
-func (m *Model) Run() *BuildResult { return m.watch.Run(false) }
+// Run builds the config and then the model once, returning the model result.
+// A failed config build is returned instead.
+func (m *Model) Run() *BuildResult {
+ if cr := m.config.Run(); !cr.OK {
+ return cr
+ }
+ return m.watch.Run(false)
+}
-// Start builds once, then watches and rebuilds until Stop is called. It
-// returns the initial build result.
-func (m *Model) Start() *BuildResult { return m.watch.Start() }
+// Start builds once (config then model), then watches and rebuilds until Stop
+// is called. It returns the initial model result.
+func (m *Model) Start() *BuildResult {
+ if cr := m.config.Run(); !cr.OK {
+ return cr
+ }
+ return m.watch.Start()
+}
// Stop ends watching and releases the watcher.
func (m *Model) Stop() { m.watch.Stop() }
-// Build returns the underlying Build (valid after Run or Start).
+// Build returns the underlying model Build (valid after Run or Start).
func (m *Model) Build() *Build { return m.build }
+
+// Config returns the model's config build.
+func (m *Model) Config() *Config { return m.config }
diff --git a/go/producer.go b/go/producer.go
index 8a5b11f..b391de9 100644
--- a/go/producer.go
+++ b/go/producer.go
@@ -51,21 +51,15 @@ func ModelProducer(b *Build, ctx *BuildContext) ProducerResult {
}
// LocalProducer runs the registered actions whose step matches the current
-// phase, in the configured order (or sorted action names when no order is
-// given). An order entry naming an unregistered action fails the build.
+// phase, in the order the config model declares (sys.model.order.action, then
+// the keys of sys.model.action). With no config, it falls back to the build
+// spec's order, then to the sorted registry keys. An order entry naming an
+// unregistered action fails the build.
func LocalProducer(b *Build, ctx *BuildContext) ProducerResult {
pr := ProducerResult{OK: true, Name: "local", Step: ctx.Step, Active: true}
- order := b.spec.Order
- if len(order) == 0 {
- for name := range b.spec.Actions {
- order = append(order, name)
- }
- sort.Strings(order)
- }
-
var ran []string
- for _, name := range order {
+ for _, name := range actionOrder(b) {
def, ok := b.spec.Actions[name]
if !ok {
pr.OK = false
@@ -102,6 +96,76 @@ func LocalProducer(b *Build, ctx *BuildContext) ProducerResult {
return pr
}
+// actionOrder resolves the ordered list of action names to run: from the
+// linked config model if present, else the build spec's Order, else the
+// sorted registry keys.
+func actionOrder(b *Build) []string {
+ if cfg, ok := b.Use["config"].(*Config); ok {
+ if o := configOrder(cfg.Model()); len(o) > 0 {
+ return o
+ }
+ }
+ if len(b.spec.Order) > 0 {
+ return b.spec.Order
+ }
+ names := make([]string, 0, len(b.spec.Actions))
+ for name := range b.spec.Actions {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ return names
+}
+
+// configOrder reads the action order from a config model: an explicit
+// sys.model.order.action string, otherwise the sorted keys of
+// sys.model.action.
+func configOrder(m map[string]any) []string {
+ model := nestedMap(m, "sys", "model")
+ if model == nil {
+ return nil
+ }
+ if order := asMap(model["order"]); order != nil {
+ if s, ok := order["action"].(string); ok && s != "" {
+ return splitOrder(s)
+ }
+ }
+ if action := asMap(model["action"]); len(action) > 0 {
+ names := make([]string, 0, len(action))
+ for name := range action {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ return names
+ }
+ return nil
+}
+
+func nestedMap(m map[string]any, keys ...string) map[string]any {
+ cur := m
+ for _, k := range keys {
+ if cur == nil {
+ return nil
+ }
+ cur = asMap(cur[k])
+ }
+ return cur
+}
+
+func asMap(v any) map[string]any {
+ m, _ := v.(map[string]any)
+ return m
+}
+
+func splitOrder(s string) []string {
+ var out []string
+ for _, p := range strings.Split(s, ",") {
+ if p = strings.TrimSpace(p); p != "" {
+ out = append(out, p)
+ }
+ }
+ return out
+}
+
func fail(name string, step Step, err error) ProducerResult {
return ProducerResult{Name: name, Step: step, Active: true, OK: false, Errs: []error{err}}
}
diff --git a/go/watch.go b/go/watch.go
index fa9cb83..90d52d3 100644
--- a/go/watch.go
+++ b/go/watch.go
@@ -22,6 +22,10 @@ type Watch struct {
name string
idle time.Duration
+ // reload, if set, runs before each change-triggered rebuild (used to
+ // re-resolve the config so config edits are picked up).
+ reload func()
+
mu sync.Mutex
last *BuildResult
sig map[string]int64
@@ -121,6 +125,9 @@ func (w *Watch) loop() {
if !changedAt.IsZero() && time.Since(changedAt) >= w.idle {
changedAt = time.Time{}
+ if w.reload != nil {
+ w.reload()
+ }
w.build.InvalidateCache()
w.Run(true)
w.mu.Lock()
diff --git a/ts/bin/voxgig-model b/ts/bin/voxgig-model
index b53af43..6c286b6 100755
--- a/ts/bin/voxgig-model
+++ b/ts/bin/voxgig-model
@@ -5,7 +5,7 @@ const Fs = require('node:fs')
const Path = require('node:path')
const Pkg = require('../package.json')
-const { Model } = require('../dist/model.js')
+const { Model, initModel } = require('../dist/model.js')
const { Jsonic } = require('jsonic')
const { Shape, Fault, One, Any } = require('shape')
@@ -20,6 +20,11 @@ run()
async function run() {
try {
+ if('init' === process.argv[2]) {
+ doInit(process.argv[3])
+ exit()
+ }
+
let options = resolveOptions()
if(options.version) {
@@ -150,6 +155,15 @@ function version() {
}
+function doInit(dir) {
+ dir = dir || '.'
+ const res = initModel(dir, Fs)
+ res.created.forEach(p => KONSOLE.log('created: ' + p))
+ res.skipped.forEach(p => KONSOLE.log('exists: ' + p))
+ KONSOLE.log('Next: voxgig-model ' + Path.join(dir, 'model', 'model.jsonic'))
+}
+
+
function exit(err) {
let code = 0
if(err) {
@@ -208,7 +222,10 @@ function help() {
const s = `
Build a Voxgig model and generate dependent artifacts.
-Usage: voxgig-model
+Usage:
+ voxgig-model [args] Build a model.
+ voxgig-model init [dir] Scaffold a new model and config under
+ /model (default: current directory).
is the root file of the model.
@@ -232,6 +249,10 @@ Usage: voxgig-model
Examples:
+# Scaffold a new model in ./model, then build it
+> voxgig-model init
+> voxgig-model model/model.jsonic
+
# Basic usage
> voxgig-model model/model.jsonic
# Builds a model defined in the file model/model.jsonic
diff --git a/ts/dist-test/init.test.js b/ts/dist-test/init.test.js
new file mode 100644
index 0000000..957fbe8
--- /dev/null
+++ b/ts/dist-test/init.test.js
@@ -0,0 +1,48 @@
+"use strict";
+/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const node_fs_1 = __importDefault(require("node:fs"));
+const promises_1 = require("node:fs/promises");
+const node_child_process_1 = require("node:child_process");
+const node_test_1 = require("node:test");
+const node_assert_1 = __importDefault(require("node:assert"));
+const model_1 = require("../dist/model");
+const GEN = __dirname + '/../test/_gen';
+const BIN = __dirname + '/../bin/voxgig-model';
+(0, node_test_1.describe)('init', () => {
+ (0, node_test_1.test)('scaffolds-and-skips-existing', async () => {
+ const dir = GEN + '/init01';
+ await (0, promises_1.rm)(dir, { recursive: true, force: true });
+ const r1 = (0, model_1.initModel)(dir, node_fs_1.default);
+ node_assert_1.default.strictEqual(r1.created.length, 2);
+ node_assert_1.default.strictEqual(r1.skipped.length, 0);
+ await (0, promises_1.stat)(dir + '/model/model.jsonic');
+ await (0, promises_1.stat)(dir + '/model/.model-config/model-config.jsonic');
+ // Second run leaves existing files untouched.
+ const r2 = (0, model_1.initModel)(dir, node_fs_1.default);
+ node_assert_1.default.strictEqual(r2.created.length, 0);
+ node_assert_1.default.strictEqual(r2.skipped.length, 2);
+ });
+ (0, node_test_1.test)('scaffold-builds', async () => {
+ const dir = GEN + '/init02';
+ await (0, promises_1.rm)(dir, { recursive: true, force: true });
+ (0, model_1.initModel)(dir, node_fs_1.default);
+ const model = new model_1.Model({
+ path: dir + '/model/model.jsonic', base: dir + '/model', debug: 'silent',
+ });
+ const br = await model.run();
+ node_assert_1.default.ok(br.ok, 'scaffolded model failed: ' + JSON.stringify(br.errs));
+ });
+ (0, node_test_1.test)('cli-init', async () => {
+ const dir = GEN + '/init03';
+ await (0, promises_1.rm)(dir, { recursive: true, force: true });
+ const res = (0, node_child_process_1.spawnSync)(process.execPath, [BIN, 'init', dir], { encoding: 'utf8' });
+ node_assert_1.default.strictEqual(res.status, 0, res.stderr);
+ node_assert_1.default.ok(res.stdout.includes('created:'), res.stdout);
+ await (0, promises_1.stat)(dir + '/model/model.jsonic');
+ });
+});
+//# sourceMappingURL=init.test.js.map
\ No newline at end of file
diff --git a/ts/dist-test/init.test.js.map b/ts/dist-test/init.test.js.map
new file mode 100644
index 0000000..3aaf447
--- /dev/null
+++ b/ts/dist-test/init.test.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"init.test.js","sourceRoot":"","sources":["../test/init.test.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/ts/dist/init.d.ts b/ts/dist/init.d.ts
new file mode 100644
index 0000000..e46a228
--- /dev/null
+++ b/ts/dist/init.d.ts
@@ -0,0 +1,7 @@
+type InitResult = {
+ created: string[];
+ skipped: string[];
+};
+declare function initModel(dir: string, fs: any): InitResult;
+export { initModel, };
+export type { InitResult, };
diff --git a/ts/dist/init.js b/ts/dist/init.js
new file mode 100644
index 0000000..0f718dc
--- /dev/null
+++ b/ts/dist/init.js
@@ -0,0 +1,48 @@
+"use strict";
+/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.initModel = initModel;
+const node_path_1 = __importDefault(require("node:path"));
+const STARTER_MODEL = `# Voxgig model. Edit this file, then build it:
+# voxgig-model model/model.jsonic
+#
+# Models are unified .jsonic - add types, defaults, references, imports.
+# Tutorial: https://github.com/voxgig/model/blob/main/docs/tutorial.md
+
+name: 'my-model'
+`;
+const STARTER_CONFIG = `# Model configuration. Declare build actions and their order here.
+#
+# Example (TypeScript loads the module; Go binds the name to a
+# registered action func):
+# sys: model: action: { example: load: 'build/example' }
+# sys: model: order: action: 'example'
+
+sys: model: action: {}
+sys: model: order: action: *''
+`;
+// Scaffold a starter model and config under /model. Existing files are
+// left untouched.
+function initModel(dir, fs) {
+ const d = dir || '.';
+ const files = [
+ [node_path_1.default.join(d, 'model', 'model.jsonic'), STARTER_MODEL],
+ [node_path_1.default.join(d, 'model', '.model-config', 'model-config.jsonic'), STARTER_CONFIG],
+ ];
+ const created = [];
+ const skipped = [];
+ for (const [p, content] of files) {
+ if (fs.existsSync(p)) {
+ skipped.push(p);
+ continue;
+ }
+ fs.mkdirSync(node_path_1.default.dirname(p), { recursive: true });
+ fs.writeFileSync(p, content);
+ created.push(p);
+ }
+ return { created, skipped };
+}
+//# sourceMappingURL=init.js.map
\ No newline at end of file
diff --git a/ts/dist/init.js.map b/ts/dist/init.js.map
new file mode 100644
index 0000000..ea066e3
--- /dev/null
+++ b/ts/dist/init.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;AA2DlD,8BAAS;AAzDX,0DAA4B;AAG5B,MAAM,aAAa,GAAG;;;;;;;CAOrB,CAAA;AAED,MAAM,cAAc,GAAG;;;;;;;;;CAStB,CAAA;AASD,4EAA4E;AAC5E,kBAAkB;AAClB,SAAS,SAAS,CAAC,GAAW,EAAE,EAAO;IACrC,MAAM,CAAC,GAAG,GAAG,IAAI,GAAG,CAAA;IACpB,MAAM,KAAK,GAAuB;QAChC,CAAC,mBAAI,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,EAAE,aAAa,CAAC;QACtD,CAAC,mBAAI,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,qBAAqB,CAAC,EAAE,cAAc,CAAC;KAChF,CAAA;IAED,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,OAAO,GAAa,EAAE,CAAA;IAE5B,KAAK,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC;QACjC,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACf,SAAQ;QACV,CAAC;QACD,EAAE,CAAC,SAAS,CAAC,mBAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAClD,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;QAC5B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;AAC7B,CAAC"}
\ No newline at end of file
diff --git a/ts/dist/model.d.ts b/ts/dist/model.d.ts
index c2c94ee..afc9e62 100644
--- a/ts/dist/model.d.ts
+++ b/ts/dist/model.d.ts
@@ -1,6 +1,7 @@
import type { BuildResult, BuildSpec, ModelSpec, Log } from './types';
import { Config } from './config';
import { Watch } from './watch';
+import { initModel } from './init';
declare class Model {
config: Config;
build: BuildSpec;
@@ -13,4 +14,4 @@ declare class Model {
start(): Promise;
stop(): Promise;
}
-export { Model, BuildSpec, };
+export { Model, BuildSpec, initModel, };
diff --git a/ts/dist/model.js b/ts/dist/model.js
index cae1e73..81df85d 100644
--- a/ts/dist/model.js
+++ b/ts/dist/model.js
@@ -34,7 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
-exports.Model = void 0;
+exports.initModel = exports.Model = void 0;
const NodeFs = __importStar(require("node:fs"));
const memfs_1 = require("memfs");
const util_1 = require("@voxgig/util");
@@ -42,6 +42,8 @@ const config_1 = require("./config");
const watch_1 = require("./watch");
const model_1 = require("./producer/model");
const local_1 = require("./producer/local");
+const init_1 = require("./init");
+Object.defineProperty(exports, "initModel", { enumerable: true, get: function () { return init_1.initModel; } });
class Model {
constructor(mspec) {
this.trigger_model = false;
diff --git a/ts/dist/model.js.map b/ts/dist/model.js.map
index 65ac2d8..f848bf5 100644
--- a/ts/dist/model.js.map
+++ b/ts/dist/model.js.map
@@ -1 +1 @@
-{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/ts/src/init.ts b/ts/src/init.ts
new file mode 100644
index 0000000..ec6080c
--- /dev/null
+++ b/ts/src/init.ts
@@ -0,0 +1,65 @@
+/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */
+
+import Path from 'node:path'
+
+
+const STARTER_MODEL = `# Voxgig model. Edit this file, then build it:
+# voxgig-model model/model.jsonic
+#
+# Models are unified .jsonic - add types, defaults, references, imports.
+# Tutorial: https://github.com/voxgig/model/blob/main/docs/tutorial.md
+
+name: 'my-model'
+`
+
+const STARTER_CONFIG = `# Model configuration. Declare build actions and their order here.
+#
+# Example (TypeScript loads the module; Go binds the name to a
+# registered action func):
+# sys: model: action: { example: load: 'build/example' }
+# sys: model: order: action: 'example'
+
+sys: model: action: {}
+sys: model: order: action: *''
+`
+
+
+type InitResult = {
+ created: string[]
+ skipped: string[]
+}
+
+
+// Scaffold a starter model and config under /model. Existing files are
+// left untouched.
+function initModel(dir: string, fs: any): InitResult {
+ const d = dir || '.'
+ const files: [string, string][] = [
+ [Path.join(d, 'model', 'model.jsonic'), STARTER_MODEL],
+ [Path.join(d, 'model', '.model-config', 'model-config.jsonic'), STARTER_CONFIG],
+ ]
+
+ const created: string[] = []
+ const skipped: string[] = []
+
+ for (const [p, content] of files) {
+ if (fs.existsSync(p)) {
+ skipped.push(p)
+ continue
+ }
+ fs.mkdirSync(Path.dirname(p), { recursive: true })
+ fs.writeFileSync(p, content)
+ created.push(p)
+ }
+
+ return { created, skipped }
+}
+
+
+export {
+ initModel,
+}
+
+export type {
+ InitResult,
+}
diff --git a/ts/src/model.ts b/ts/src/model.ts
index 12f7e7a..877d0ad 100644
--- a/ts/src/model.ts
+++ b/ts/src/model.ts
@@ -26,6 +26,8 @@ import { Watch } from './watch'
import { model_producer } from './producer/model'
import { local_producer } from './producer/local'
+import { initModel } from './init'
+
class Model {
config: Config
@@ -270,6 +272,7 @@ function makeReadOnly(fsm: FST) {
export {
Model,
BuildSpec,
+ initModel,
}
diff --git a/ts/test/init.test.ts b/ts/test/init.test.ts
new file mode 100644
index 0000000..9a8ac86
--- /dev/null
+++ b/ts/test/init.test.ts
@@ -0,0 +1,58 @@
+/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */
+
+import Fs from 'node:fs'
+import { rm, stat } from 'node:fs/promises'
+import { spawnSync } from 'node:child_process'
+import { test, describe } from 'node:test'
+import assert from 'node:assert'
+
+import { Model, initModel } from '../dist/model'
+
+
+const GEN = __dirname + '/../test/_gen'
+const BIN = __dirname + '/../bin/voxgig-model'
+
+
+describe('init', () => {
+
+ test('scaffolds-and-skips-existing', async () => {
+ const dir = GEN + '/init01'
+ await rm(dir, { recursive: true, force: true })
+
+ const r1 = initModel(dir, Fs)
+ assert.strictEqual(r1.created.length, 2)
+ assert.strictEqual(r1.skipped.length, 0)
+ await stat(dir + '/model/model.jsonic')
+ await stat(dir + '/model/.model-config/model-config.jsonic')
+
+ // Second run leaves existing files untouched.
+ const r2 = initModel(dir, Fs)
+ assert.strictEqual(r2.created.length, 0)
+ assert.strictEqual(r2.skipped.length, 2)
+ })
+
+
+ test('scaffold-builds', async () => {
+ const dir = GEN + '/init02'
+ await rm(dir, { recursive: true, force: true })
+ initModel(dir, Fs)
+
+ const model = new Model({
+ path: dir + '/model/model.jsonic', base: dir + '/model', debug: 'silent',
+ })
+ const br = await model.run()
+ assert.ok(br.ok, 'scaffolded model failed: ' + JSON.stringify(br.errs))
+ })
+
+
+ test('cli-init', async () => {
+ const dir = GEN + '/init03'
+ await rm(dir, { recursive: true, force: true })
+
+ const res = spawnSync(process.execPath, [BIN, 'init', dir], { encoding: 'utf8' })
+ assert.strictEqual(res.status, 0, res.stderr)
+ assert.ok(res.stdout.includes('created:'), res.stdout)
+ await stat(dir + '/model/model.jsonic')
+ })
+
+})