From 563be4dd60739862a87f3a03887f556cf9d637bf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 19:32:19 +0000 Subject: [PATCH] feat: add init CLI for both langs; give Go a real model config init (parity across both CLIs + library): - `voxgig-model init [dir]` scaffolds /model/model.jsonic and model/.model-config/model-config.jsonic, skipping existing files; identical output in TypeScript and Go. Library: initModel(dir, fs) in TS, model.Init(dir) in Go. Go model config (true parity, not just a registry): - Add a Config build (go/config.go): resolves .model-config/ model-config.jsonic, auto-creates a self-contained stub when missing, and writes model-config.json - mirroring the TypeScript Config. - Model runs the config build before the model build and re-resolves it on each watch rebuild (go/model.go + a watch reload hook). - LocalProducer takes the action order from the config model (sys.model.order.action, then sys.model.action keys), falling back to the build spec / sorted registry. Action funcs are still bound from ModelSpec.Actions (Go can't require()), so the config file is now the source of truth for which actions run and in what order, like TS. Tests: TS 22 (init scaffolds/skips, scaffold builds, CLI init); Go adds config (auto-create, order-from-config) and init (scaffold then build, CLI init). make test green; gofmt/vet clean. Docs: how-to "Initialize a new model", reference init subcommand, CLI help, and updated Go parity notes in AGENTS.md / go/README.md. https://claude.ai/code/session_01HxXpZNrKj3qonocwAtEP8r --- AGENTS.md | 11 ++-- docs/how-to.md | 26 ++++++++++ docs/reference.md | 16 ++++++ go/README.md | 7 ++- go/cmd/voxgig-model/main.go | 25 ++++++++++ go/cmd/voxgig-model/main_test.go | 22 ++++++++ go/config.go | 67 +++++++++++++++++++++++++ go/config_test.go | 64 ++++++++++++++++++++++++ go/init.go | 52 +++++++++++++++++++ go/init_test.go | 56 +++++++++++++++++++++ go/model.go | 67 ++++++++++++++++++------- go/producer.go | 86 ++++++++++++++++++++++++++++---- go/watch.go | 7 +++ ts/bin/voxgig-model | 25 +++++++++- ts/dist-test/init.test.js | 48 ++++++++++++++++++ ts/dist-test/init.test.js.map | 1 + ts/dist/init.d.ts | 7 +++ ts/dist/init.js | 48 ++++++++++++++++++ ts/dist/init.js.map | 1 + ts/dist/model.d.ts | 3 +- ts/dist/model.js | 4 +- ts/dist/model.js.map | 2 +- ts/src/init.ts | 65 ++++++++++++++++++++++++ ts/src/model.ts | 3 ++ ts/test/init.test.ts | 58 +++++++++++++++++++++ 25 files changed, 730 insertions(+), 41 deletions(-) create mode 100644 go/config.go create mode 100644 go/config_test.go create mode 100644 go/init.go create mode 100644 go/init_test.go create mode 100644 ts/dist-test/init.test.js create mode 100644 ts/dist-test/init.test.js.map create mode 100644 ts/dist/init.d.ts create mode 100644 ts/dist/init.js create mode 100644 ts/dist/init.js.map create mode 100644 ts/src/init.ts create mode 100644 ts/test/init.test.ts 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') + }) + +})