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') + }) + +})