diff --git a/.github/workflows/netsukefile-test.yml b/.github/workflows/netsukefile-test.yml index 674346b5c..29f89ca55 100644 --- a/.github/workflows/netsukefile-test.yml +++ b/.github/workflows/netsukefile-test.yml @@ -74,7 +74,9 @@ jobs: command: "touch unused.txt" MANIFEST - name: Build dependent, inline, and foreach targets - run: ./target/debug/netsuke --verbose build --emit build.ninja dependent.txt inline-command.txt inline-script.txt a-foreach.txt b-foreach.txt + run: | + ./target/debug/netsuke --verbose generate --output build.ninja + ninja -f build.ninja dependent.txt inline-command.txt inline-script.txt a-foreach.txt b-foreach.txt - name: Assert dependent artefacts exist env: NINJA_MANIFEST: build.ninja @@ -100,7 +102,9 @@ jobs: NINJA_MANIFEST: build.ninja run: scripts/assert-file-absent.sh skip-foreach.txt - name: Run action target - run: ./target/debug/netsuke --verbose build --emit action.ninja say-hello + run: | + ./target/debug/netsuke --verbose generate --output action.ninja + ninja -f action.ninja say-hello - name: Assert action artefact exists env: NINJA_MANIFEST: action.ninja diff --git a/README.md b/README.md index b639ec278..9364aba6e 100644 --- a/README.md +++ b/README.md @@ -3,202 +3,177 @@ [![Ask DeepWiki](https://deepwiki.com/badge.svg)]( https://deepwiki.com/leynos/netsuke) -A modern, declarative build system compiler. YAML + Jinja in, Ninja out. -Nothing more. Nothing less. +*A friendly build-system compiler: YAML and Jinja in, Ninja out.* -## What is Netsuke? +Netsuke turns a readable `Netsukefile` into a validated, static Ninja build +graph. It keeps the dynamic work in a higher-level manifest and leaves fast, +incremental execution to [Ninja](https://ninja-build.org/). -**Netsuke** is a friendly build system that compiles structured manifests into -a Ninja build graph. It’s not a shell-script runner, a meta-task framework, or -a domain-specific CI layer. It’s `make`, if `make` hadn’t been invented in 1977. +______________________________________________________________________ -### Key properties +## Why Netsuke? -- **Declarative**: Targets, rules, and dependencies described explicitly. -- **Dynamic when needed**: Jinja templating for loops, macros, conditionals, - file globbing. -- **Static where required**: Always compiles to a reproducible, fully static - dependency graph. -- **Unopinionated**: No magic for C, Rust, Python, JavaScript, or any other - blessed language. -- **Safe**: All variable interpolation is securely shell-escaped by default. -- **Fast**: Builds executed by [Ninja](https://ninja-build.org/), the fastest - graph executor we know of. +- **Readable manifests**: Describe rules, targets, dependencies, and defaults + in YAML instead of a tab-sensitive language. +- **Dynamic planning**: Use Jinja variables, macros, `foreach`, `when`, and + globbing before Netsuke creates the build graph. +- **Static execution**: Inspect the generated Ninja file or render the graph + before running any build command. +- **Useful diagnostics**: Get source-aware errors, localized output, progress + reporting, and canonical `--json` machine-readable command output. +- **No blessed toolchain**: Use the same manifest model for Rust, C, Python, + web projects, or anything else a command can build. -## Quick Example +______________________________________________________________________ -```yaml -netsuke_version: "1.0" - -vars: - cc: clang - cflags: -Wall -Werror - -rules: - - name: compile - command: "{{ cc }} {{ cflags }} -c {{ ins }} -o {{ outs }}" +## Quick start - - name: link - command: "{{ cc }} {{ cflags }} {{ ins }} -o {{ outs }}" - -targets: - - foreach: glob('src/*.c') - name: "build/{{ item | basename | with_suffix('.o') }}" - rule: compile - sources: "{{ item }}" - - - name: app - rule: link - sources: "{{ glob('src/*.c') | map('basename') | map('with_suffix', '.o') }}" -``` +### Prerequisites -Yes, it’s just YAML. Yes, that’s a Jinja `foreach`. No, you don’t need to define -`.PHONY` or remember what `$@` means. This is the present day. You deserve -better. +Netsuke currently requires: -## Key Concepts +- [Ninja](https://ninja-build.org/) on `PATH`; +- Rust 1.89 or later when installing from source. -### 🔨 Rules +### Installation -Rules are reusable command templates. Each one has exactly one of: +Until the v0.1.0 release is published, install the current source checkout with +Cargo: -- `command:` - a single shell string -- `script:` - a multi-line block -- (or) can be declared inline on a target + -```yaml -rules: - - name: rasterise - script: | - inkscape --export-png={{ ins }} {{ outs }} +```sh +git clone https://github.com/leynos/netsuke.git +cd netsuke +cargo install --path . ``` -### 🎯 Targets +### Your first build -Targets are things you want to build. +Create a new directory and add a file named `Netsukefile`: -```yaml -- name: build/logo.png - rule: rasterise - sources: assets/logo.svg -``` + -Targets can also define: +```yaml +netsuke_version: "1.0.0" -- `deps`: implicit dependencies — trigger rebuilds but are not passed to - `$in`/`{{ ins }}`; map to Ninja `|` -- `order_only_deps`: e.g. `mkdir -p build` -- `vars`: per-target variables +targets: + - name: hello.txt + command: "echo 'Hello from Netsuke!' > hello.txt" -You may also use `command:` or `script:` instead of referencing a `rule`. +defaults: + - hello.txt +``` -## 🧪 Phony Targets and Actions +Run Netsuke, then inspect the result: -Phony targets behave like Make’s `.PHONY`: + -```yaml -- name: clean - phony: true - always: true - command: rm -rf build +```sh +netsuke +cat hello.txt ``` -For cleaner structure, you may also define phony targets under an `actions:` -block: +The second command prints `Hello from Netsuke!`. See the +[quick-start guide](docs/quickstart.md) for variables, templates, and `foreach`. -```yaml -actions: - - name: test - command: pytest -``` +______________________________________________________________________ -All `actions` are treated as `{ phony: true, always: false }` by default. +## What works today -## 🧠 Templating +The core build-system compiler is implemented: -Netsuke uses [MiniJinja](https://docs.rs/minijinja) to render your manifest -before parsing. +- YAML 1.2 manifest parsing with duplicate-key and schema validation; +- Jinja variables, macros, `foreach`, `when`, globbing, environment helpers, + executable discovery, and opt-in network helpers; +- reusable rules, targets, actions, defaults, and explicit, implicit, and + order-only dependencies; +- a deterministic intermediate build graph with duplicate-output, missing-rule, + and cycle checks; +- Ninja generation and execution, plus `clean` and standalone manifest + generation; +- reproducible dependency graphs as Graphviz DOT or self-contained, + accessible HTML; +- layered configuration, localized output, accessibility preferences, + progress reporting, stage timings, and versioned JSON results or diagnostics; +- unit, behavioural, integration, property, snapshot, and initial Kani + verification coverage. -You can: +Release automation is configured to build packages for Linux, macOS, and +Windows, including platform help artefacts. v0.1.0 will be the first public +release of this work. -- Glob files: `{{ glob('src/**/*.c') }}` -- Read environment vars: `{{ env('CC') }}` -- Use filters: `{{ path | basename | with_suffix('.o') }}` -- Define reusable macros: +______________________________________________________________________ - ```yaml - macros: - - signature: "shout(msg)" - body: | - echo "{{ msg | upper }}" - ``` +## v0.1.0 status -Templating happens **before** parsing, so any valid output must be valid YAML. +v0.1.0 is a useful preview for early adopters, not a declaration that Netsuke +is finished or that every interface is stable. The compiler pipeline and +ordinary local-build workflow are substantial; the command-line interface, +configuration vocabulary, and advanced recipe model are still evolving. -## 🔐 Safety +Pin the Netsuke version in automation and expect some command names, flags, +diagnostic schemas, and manifest details to change before 1.0. -Shell commands are automatically escaped. Interpolation into `command:` or -`script:` will never yield a command injection vulnerability unless you -explicitly ask for `| raw`. +Known limitations include: -```yaml -command: "echo {{ dangerous_value }}" # Safe -command: "echo {{ dangerous_value | raw }}" # Unsafe (your problem now) -``` +- recipes are shell strings; structured executable arguments and recipe + environment mappings are not implemented yet; +- literal shell dollar expressions still need Ninja-aware escaping in + manifests; +- compiler-generated dependency imports such as GCC depfiles are planned but + not yet part of the manifest model; +- `--json` emits exactly one versioned result or diagnostic document for each + command, but the schema may still change before 1.0; +- accessibility, terminal rendering, configuration precedence, and + cross-platform compiler invariants need broader verification. -## 🔧 CLI +A `Netsukefile` can execute commands and use impure template helpers. Treat it +with the same care as a `Makefile`: review untrusted manifests before running +them. Netsuke quotes supported path substitutions, but it is not a sandbox. -```shell -netsuke [build] [target1 target2 ...] -netsuke clean -netsuke graph - netsuke manifest FILE -``` +______________________________________________________________________ -- `netsuke` alone builds the `defaults:` targets from your manifest -- `netsuke graph` emits a Graphviz `.dot` of the build DAG -- `netsuke clean` runs `ninja -t clean` -- `netsuke manifest FILE` writes the Ninja manifest to `FILE` without invoking - Ninja +## The road ahead -You can also pass: +Work after the first release is organized around three priorities: -- `--file` to use an alternate manifest -- `--directory` to run in a different working dir -- `-j N` to control parallelism (passed through to Ninja) -- `-v`, `--verbose` to enable verbose logging +1. **Stabilize the command-line contract**: harden the canonical command and + flag names, non-interactive safeguards, stable exit codes, bounded output, + and versioned `--json` documents. +2. **Make recipes safer and clearer**: add structured executable arguments, + environment mappings, compiler dependency imports, backend dollar escaping, + and better conditional-action feedback. +3. **Strengthen confidence**: expand Kani and property-test coverage, verify + accessibility with assistive technology, and add regression coverage for + configuration precedence and terminal rendering. -Release builds include a `netsuke.1` manual page generated by `cargo-orthohelp` -from the CLI documentation metadata, providing the same flags and subcommands -documented via `--help`. Windows release artefacts also include PowerShell -external help for `Get-Help Netsuke -Full`. Manual page generation honours -`SOURCE_DATE_EPOCH` for reproducible dates. If the value is invalid, a warning -is emitted and the date falls back to `1970-01-01`. If `SOURCE_DATE_EPOCH` is -unset, the date deterministically falls back to `1970-01-01` without a warning. -The published crate does not include these files; packagers can source them -from release artefacts under `target/orthohelp//release/`. +Longer-term work explores machine-readable context, profiles, run history, +artefact delivery, and local-first feedback for human and agent workflows. The +[roadmap](docs/roadmap.md) tracks the detailed sequence and current progress. -## 🚧 Status +______________________________________________________________________ -Netsuke is **under active development**. It’s not finished, but it’s buildable, -usable, and increasingly delightful. +## Learn more -Coming soon: +- [Quick-start guide](docs/quickstart.md) — build something in five minutes. +- [Users' guide](docs/users-guide.md) — manifest and command reference. +- [Design document](docs/netsuke-design.md) — architecture and design + rationale. +- [Developers' guide](docs/developers-guide.md) — development workflow and + quality gates. +- [Roadmap](docs/roadmap.md) — completed foundations and planned work. -- `graph --html` for interactive DAGs -- Extensible plugin system for filters/functions -- Toolchain presets (`cargo`, `node`, etc.) +______________________________________________________________________ -## Why “Netsuke”? +## Licence -A **netsuke** is a small carved object used to fasten things securely to a -belt. It’s not the sword. It’s not the pouch. It’s the thing that connects them. +ISC — see [LICENSE](LICENSE) for details. -That’s what this is: a tidy connector between your intent and the tool that -gets it done. +______________________________________________________________________ -## Licence +## Contributing -Netsuke is distributed under the -[ISC licence](https://opensource.org/licenses/ISC). You don't need a legal -thesis to use a build tool. +Contributions are welcome. Start with the +[developers' guide](docs/developers-guide.md); automated contributors should +also follow [AGENTS.md](AGENTS.md). diff --git a/build.rs b/build.rs index 1b4f18920..8c8801471 100644 --- a/build.rs +++ b/build.rs @@ -6,61 +6,68 @@ //! - Audit localization keys declared in `src/localization/keys.rs` against the Fluent bundles //! in `locales/*/messages.ftl`, failing the build if any declared key is missing from a //! locale. -use clap::{ArgMatches, CommandFactory}; +use clap::CommandFactory; use clap_mangen::Man; use std::{ - env, - ffi::OsString, - fs, + env, fs, path::{Path, PathBuf}, - sync::Arc, }; use time::{OffsetDateTime, format_description::well_known::Iso8601}; const FALLBACK_DATE: &str = "1970-01-01"; -type OutputModeResolveWith = fn( - Option, - Option, - fn(&str) -> Option, -) -> output_mode::OutputMode; - +// The build script recompiles these library modules as its own crate so that +// `cli::Cli::command()` (used for man-page generation) can be constructed. Only +// a small slice of each module's public API is reachable from this binary, so +// the compiler reports the remainder as unused. Those items are not dead: their +// real call sites live in the library crate and are covered by its tests, where +// dead-code and unused-import analysis applies normally. Each shared module +// therefore carries an `#[expect]` for exactly the lints it triggers here, in +// preference to anchoring the symbols with artificial references. +#[expect( + dead_code, + unused_imports, + reason = "shared library source; the unreached API is exercised by the library crate" +)] #[path = "src/cli/mod.rs"] mod cli; #[path = "src/cli_localization.rs"] mod cli_localization; +#[expect( + dead_code, + reason = "shared library source; the unreached API is exercised by the library crate" +)] #[path = "src/cli_l10n.rs"] mod cli_l10n; +#[expect( + dead_code, + reason = "shared library source; the unreached API is exercised by the library crate" +)] #[path = "src/host_pattern.rs"] mod host_pattern; #[path = "src/localization/mod.rs"] mod localization; +#[expect( + dead_code, + reason = "shared library source; the unreached API is exercised by the library crate" +)] #[path = "src/output_mode.rs"] mod output_mode; +#[expect( + dead_code, + reason = "shared library source; the unreached API is exercised by the library crate" +)] #[path = "src/theme.rs"] mod theme; mod build_l10n_audit; -use host_pattern::{HostPattern, HostPatternError}; - -type LocalizedParseFn = fn( - Vec, - &Arc, -) -> Result<(cli::Cli, ArgMatches), clap::Error>; - -type ResolveThemeFn = fn( - Option, - theme::ThemeContext, - fn(&str) -> Option, -) -> theme::ResolvedTheme; - fn manual_date() -> String { let Ok(raw) = env::var("SOURCE_DATE_EPOCH") else { return FALLBACK_DATE.into(); @@ -106,40 +113,6 @@ fn write_man_page(data: &[u8], dir: &Path, page_name: &str) -> std::io::Result

(); - const _: usize = std::mem::size_of::(); - const _: usize = std::mem::size_of::(); - const _: usize = std::mem::size_of::(); - const _: usize = std::mem::size_of::(); - const _: usize = std::mem::size_of::(); - const _: fn(&[OsString]) -> Option = cli::locale_hint_from_args; - const _: fn(&[OsString]) -> Option = cli::diag_json_hint_from_args; - const _: fn(&str) -> Option = cli_l10n::parse_bool_hint; - const _: fn(&cli::Cli, &ArgMatches) -> bool = cli::resolve_merged_diag_json; - const _: fn(&cli::Cli, &ArgMatches) -> ortho_config::OrthoResult = - cli::merge_with_config; - const _: LocalizedParseFn = cli::parse_with_localizer_from; - const _: fn(&cli::Cli) -> Option = cli::Cli::no_emoji_override; - const _: fn(&cli::Cli) -> bool = cli::Cli::progress_enabled; - const _: fn(&cli::Cli) -> bool = cli::Cli::resolved_progress; - const _: fn(&cli::Cli) -> bool = cli::Cli::resolved_diag_json; - const _: fn(&str) -> Result = HostPattern::parse; - const _: fn(&HostPattern, host_pattern::HostCandidate<'_>) -> bool = HostPattern::matches; - const _: fn(Option, Option) -> output_mode::OutputMode = - output_mode::resolve; - const _: OutputModeResolveWith = output_mode::resolve_with; - const _: fn( - Option, - Option, - output_mode::OutputMode, - ) -> theme::ThemeContext = theme::ThemeContext::new; - const _: ResolveThemeFn = theme::resolve_theme; -} - fn emit_rerun_directives() { println!("cargo:rerun-if-changed=src/cli/mod.rs"); println!("cargo:rerun-if-changed=src/cli/config.rs"); @@ -198,7 +171,6 @@ fn generate_man_page(out_dir: &Path) -> Result<(), Box> { } fn main() -> Result<(), Box> { - verify_public_api_symbols(); emit_rerun_directives(); build_l10n_audit::audit_localization_keys()?; let out_dir = out_dir_for_target_profile(); diff --git a/docs/adr-004-explicit-config-selection-outside-orthoconfig.md b/docs/adr-004-explicit-config-selection-outside-orthoconfig.md index f33cf7bd9..f63bc7e9f 100644 --- a/docs/adr-004-explicit-config-selection-outside-orthoconfig.md +++ b/docs/adr-004-explicit-config-selection-outside-orthoconfig.md @@ -16,8 +16,7 @@ OrthoConfig's built-in discovery attributes. Netsuke needs an explicit configuration selector for operators who want one known configuration file to control a run. The public selector order is -`--config` > `NETSUKE_CONFIG` > `NETSUKE_CONFIG_PATH` > automatic discovery, -where `NETSUKE_CONFIG_PATH` remains a backward-compatible alias only. +`--config` > `NETSUKE_CONFIG` > automatic discovery. The existing merge pipeline is deliberately two-pass. It first resolves early diagnostic JSON preferences from file layers, so startup errors can be emitted @@ -27,11 +26,10 @@ requirements: project configuration must outrank user configuration, and a missed project `.netsuke.toml` requires a direct second-pass project load. OrthoConfig can discover configuration files, but its built-in discovery -attribute does not own Netsuke's `--config` spelling, legacy-environment -compatibility, early diagnostic merge, or project-over-user second pass. -Putting explicit selection into OrthoConfig would either expose -Netsuke-specific policy through a generic library API or force Netsuke to work -around library-owned behaviour in the CLI adapter. +attribute does not own Netsuke's `--config` spelling, early diagnostic merge, +or project-over-user second pass. Putting explicit selection into OrthoConfig +would either expose Netsuke-specific policy through a generic library API or +force Netsuke to work around library-owned behaviour in the CLI adapter. ## Decision drivers @@ -41,8 +39,7 @@ around library-owned behaviour in the CLI adapter. resolution and final configuration merging. - Keep `OrthoConfig` responsible for generic layer composition, not Netsuke-specific selector precedence. -- Support `NETSUKE_CONFIG_PATH` only as a compatibility alias behind - `NETSUKE_CONFIG`. +- Keep `NETSUKE_CONFIG` as the only environment selector. - Make explicit selection fail closed: an invalid selected file must not fall through to automatic discovery. @@ -54,18 +51,18 @@ This would let OrthoConfig own the config-path selector and merge discovered files as part of its normal derived merge behaviour. It was rejected because Netsuke needs the public spelling `--config`, the -environment precedence `NETSUKE_CONFIG` before `NETSUKE_CONFIG_PATH`, and the -two-pass diagnostic path. OrthoConfig's generic discovery machinery cannot -express those Netsuke-specific semantics without broadening its API around one -consumer's policy. +`NETSUKE_CONFIG` environment selector, and the two-pass diagnostic path. +OrthoConfig's generic discovery machinery cannot express those Netsuke-specific +semantics without broadening its API around one consumer's policy. ### Option B: add Netsuke-specific explicit selection to OrthoConfig -This would extend OrthoConfig, so Netsuke could delegate the selector order and -legacy alias handling to the library. +This would extend OrthoConfig, so Netsuke could delegate its selector policy to +the library. It was rejected because the policy is part of Netsuke's CLI contract rather -than OrthoConfig's domain. Baking `NETSUKE_CONFIG`, `NETSUKE_CONFIG_PATH`, or +than OrthoConfig's domain. The Netsuke CLI adapter owns selector precedence, the +`--config` spelling, and `NETSUKE_CONFIG` handling. Baking those details or Netsuke's project-scope fallback into OrthoConfig would invert the dependency: the generic merge library would know too much about one adapter. @@ -77,18 +74,16 @@ push the same layers into the full merge composer. It is accepted because it keeps the boundary clear. OrthoConfig remains the layer-composition engine, while Netsuke's CLI adapter owns how user input, -environment aliases, diagnostics, and automatic discovery are combined. +environment selection, diagnostics, and automatic discovery are combined. ## Decision outcome Netsuke resolves explicit configuration paths in `src/cli/discovery.rs`. -- `explicit_config_path` applies `--config` > `NETSUKE_CONFIG` > - `NETSUKE_CONFIG_PATH`, ignoring empty environment values. +- `explicit_config_path` applies `--config` > `NETSUKE_CONFIG`, ignoring empty + environment values. - `env_config_path(var_name)` reads one environment variable with `std::env::var_os`, so precedence tests use current-process values. -- `collect_diag_file_layers` mirrors the file-loading path for early diagnostic - JSON resolution and propagates explicit-load failures. - `push_file_layers` drains successful layer loads into the merge composer, or records the load error for final diagnostics. - Automatic discovery remains the fallback only when no explicit selector is diff --git a/docs/developers-guide.md b/docs/developers-guide.md index f794841d2..804dc536a 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -172,8 +172,8 @@ The caller passes two configuration inputs, each carrying intent: not evaluate that cfg, so mutants inserted there would compile to nothing and survive as noise rather than genuine test gaps. - `extra-args` — `--all-features`, so the mutation run matches the `make test` - CI baseline; a mismatch would report feature-gated code (the - `legacy-digests` feature) as untested. + CI baseline; a mismatch would report feature-gated code (the `legacy-digests` + feature) as untested. The caller does not set `extra-crate-dirs`, the input reserved for crate directories outside the Cargo workspace. `ambient_fs`, the repository's @@ -468,6 +468,29 @@ there. The tests require workflow YAML files under `.github/workflows` and ensure local composite action manifests under `.github/actions` are covered by the configured Dependabot directory patterns. +### User-facing documentation examples + +Every fenced example in `README.md` and `docs/users-guide.md` has a stable +`tested-example` marker immediately before its opening fence. The shared +`tests/documentation_examples/mod.rs` loader owns this marker format and may be +called only by documentation-focused integration or behavioural tests. It +rejects unmarked fences, duplicate identifiers and unterminated examples. + +`tests/documentation_examples_tests.rs` loads the exact fenced text, generates +Ninja for every manifest fence and each complete manifest linked from the +user's guide, and checks selected command and output contracts against the +current binary. On Unix, `tests/documentation_examples_e2e_tests.rs` uses real +Ninja to execute the documented first-run build and `cat hello.txt`, exercise +the configured default target, and verify the photo-edit and writing outputs. +`tests/documentation_examples_loader_tests.rs` covers concrete malformed-fence +and non-YAML failure cases. + +The first-run README and user's guide examples also run through the +`rstest-bdd` scenarios in `tests/features/documentation_examples.feature`. +These reuse the novice smoke tests' fake-Ninja flow to verify the Netsuke +invocation and status output. Tests must load fenced text through the shared +helper instead of maintaining copied fixtures. + ### Property-based testing with proptest `proptest` generates randomized inputs to verify invariants that must hold for @@ -491,8 +514,8 @@ unit tests where a small fixed set of cases must all be verified. - Annotate the test function with `#[rstest]` and supply cases via `#[case(...)]` parameters. - Canonical example: `src/cli/config_path_precedence_tests.rs` - - `resolve_config_path_precedence` enumerates all 2^3 = 8 combinations of - `--config`, `NETSUKE_CONFIG`, and `NETSUKE_CONFIG_PATH` presence. + `resolve_config_path_precedence` enumerates all four combinations of + `--config` and `NETSUKE_CONFIG` presence. ## IR dependency classes @@ -635,6 +658,10 @@ mutations. For locale-sensitive snapshot tests, use the `EnLocalizer` RAII pattern documented in the [snapshot testing guide](snapshot-testing-in-netsuke-using-insta.md#locale-pinned-snapshot-tests). +`src/snapshot_test_support.rs` owns output-oriented unit-test fixtures; +`no_color_env` is shared across output-preference and theme tests that exercise +optional `NO_COLOR` lookup behaviour. + ### `EnvLock` `test_support::env_lock::EnvLock` is a global mutex that serializes all @@ -663,7 +690,7 @@ use test_support::EnvVarGuard; let _env_lock = EnvLock::acquire(); let _guard = EnvVarGuard::set("HOME", temp.path().as_os_str()); -let _guard = EnvVarGuard::remove("NETSUKE_CONFIG_PATH"); +let _guard = EnvVarGuard::remove("NETSUKE_CONFIG"); ``` For BDD steps that need to track mutations through `TestWorld`, use @@ -756,10 +783,10 @@ use crate::bdd::helpers::env_mutation::mutate_env_var; use crate::bdd::types::EnvVarKey; // Set a variable -mutate_env_var(world, EnvVarKey::from("NETSUKE_THEME"), Some("ascii"))?; +mutate_env_var(world, EnvVarKey::from("NETSUKE_COLOR"), Some("never"))?; // Remove a variable -mutate_env_var(world, EnvVarKey::from("NETSUKE_CONFIG_PATH"), None)?; +mutate_env_var(world, EnvVarKey::from("NETSUKE_EMOJI"), None)?; ``` Do **not** call `std::env::set_var` directly in BDD steps — use @@ -829,16 +856,13 @@ a two-pass approach when no explicit config path is provided: 1. **First pass** — run `config_discovery()` to find whatever file exists first (typically user-scope). 2. **Second pass** — if the first pass did not find the project-scope file - and there is no explicit config path (`--config`, `NETSUKE_CONFIG`, - `NETSUKE_CONFIG_PATH`), load `.netsuke.toml` from the project root directly - via `load_config_file_as_chain` and push its layers last. + and there is no explicit config path (`--config` or `NETSUKE_CONFIG`), load + `.netsuke.toml` from the project root directly via + `load_config_file_as_chain` and push its layers last. Because `MergeComposer` uses last-wins semantics, pushing the project layers after user layers gives them higher precedence. -The same logic is mirrored in `collect_diag_file_layers` for early `diag_json` -resolution (before full merging). - ### Layer precedence The final merge order is: @@ -851,7 +875,7 @@ The final merge order is: ### Configuration merge helper functions -Private helper functions for config discovery and diagnostic-JSON resolution. +Private helper functions for config discovery and JSON-output resolution. Configuration merge helpers: @@ -868,7 +892,7 @@ Configuration merge helpers: environment variable, ignores empty values, and converts the value into a `PathBuf`. - `explicit_config_path(cli: &Cli) -> Option` resolves explicit config - selection from `--config`, `NETSUKE_CONFIG`, and `NETSUKE_CONFIG_PATH`. + selection from `--config` and `NETSUKE_CONFIG`. - `push_file_layers(cli, composer, errors) -> ()` pushes explicit or discovered file layers onto a `MergeComposer`. Explicit load errors are pushed into `errors`, and automatic discovery is not attempted after an explicit selector @@ -876,15 +900,12 @@ Configuration merge helpers: - `collect_file_layers(directory)` builds the fallback discovery layer chain, applies the project-layer second pass, and returns `OrthoResult>>`. -- `collect_diag_file_layers(cli: &Cli) -> OrthoResult>>` - mirrors the file-load path used by `resolve_merged_diag_json` so early - `diag_json` resolution sees the same explicit or discovered file layers. - `is_empty_value(value: &serde_json::Value) -> bool` detects an empty CLI override object. -- `diag_json_from_layer(layer: &MergeLayer) -> Option` extracts - `diag_json` from a config layer, preferring `output_format`. -- `diag_json_from_matches(cli, matches) -> OrthoResult` resolves final - `diag_json` from CLI matches with fallback. +- `json_from_layer(value: &serde_json::Value) -> Option` extracts `json` + from a configuration value. +- `json_from_matches(cli, matches, discovered) -> bool` applies an explicit + root `--json` override to the discovered value. - `cli_overrides_from_matches(matches: &ArgMatches) -> OrthoValue` extracts CLI-supplied fields, stripping defaults and non-CLI sources. - `env_provider() -> Figment` returns the `NETSUKE_` prefixed Figment @@ -897,74 +918,32 @@ selection. It evaluates the precedence chain in this order: 1. `cli.config` 2. `NETSUKE_CONFIG` -3. `NETSUKE_CONFIG_PATH` `env_config_path(var_name)` is the helper that reads `std::env::var_os`, discarding empty values and converting the value into `PathBuf`. For `merge` and -`resolve_merged_diag_json`, both `collect_diag_file_layers` and -`push_file_layers` therefore follow the same precedence by calling -`explicit_config_path`, so they return either the same explicit path layers or -the same fallback discovery path. +`resolve_merged_json`, `src/cli/discovery.rs` resolves only `NETSUKE_CONFIG`; +no other environment variable can select a configuration file. The public API remains two arguments: ```rust pub fn merge_with_config(cli: &Cli, matches: &ArgMatches) -> OrthoResult; -pub fn resolve_merged_diag_json(cli: &Cli, matches: &ArgMatches) -> OrthoResult; +pub fn resolve_merged_json(cli: &Cli, matches: &ArgMatches) -> bool; ``` Unit tests that need to verify explicit config path precedence should exercise -the public merge or diagnostic APIs with guarded environment variables. The +the public merge or JSON-output APIs with guarded environment variables. The explicit selector helper reads the process environment directly and remains private to `src/cli/discovery.rs`. -#### `diag_json` contract - -Tooling that wants a stable contract for early diagnostic-JSON resolution -should treat the input consumed by `collect_diag_file_layers`, -`diag_json_from_layer`, and `diag_json_from_matches` as versioned schema -`netsuke.diag-json-resolution.v1`: - -```json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:netsuke:diag-json-resolution:v1", - "title": "Netsuke diag_json resolution layer", - "type": "object", - "description": "Subset of merged configuration consulted before full CLI merging.", - "properties": { - "output_format": { - "type": "string", - "enum": ["human", "json"], - "description": "Preferred field. When present and valid, this decides diag_json." - }, - "diag_json": { - "type": "boolean", - "description": "Legacy fallback field used only when output_format is absent or invalid." - } - }, - "additionalProperties": true -} -``` +#### `json` contract -Versioning and compatibility rules: - -- Version `v1` has no required fields. Both `output_format` and `diag_json` - are optional. -- `output_format` is the preferred field. Valid `"json"` resolves to - `diag_json = true`; valid `"human"` resolves to `diag_json = false`. -- If `output_format` is present but invalid, resolution falls back to - `diag_json` when it is a boolean value. -- Non-object values, or objects that contain neither recognized field, produce - no `diag_json` decision. -- `cli_overrides_from_matches` must continue to emit a JSON object, even when - no CLI override is present. -- `is_empty_value` treats only the empty object `{}` as "no CLI overrides". - Downstream tooling must not replace an empty object with `null`, `[]`, or any - other sentinel. -- Additional properties are ignored by `diag_json` resolution and may be - present because the same layer object also participates in full config - merging. +Early JSON resolution reads only the boolean `json` field from each +configuration layer. File layers are applied in merge order, followed by +`NETSUKE_JSON`; an explicit root `--json` flag has the highest precedence. +Invalid or unavailable early layers fall back to the built-in `false` default, +allowing startup and merge failures to use the same output policy as normal +runtime diagnostics. ## BDD command helpers and environment handling @@ -1044,7 +1023,6 @@ sequenceDiagram participant ManifestCommandSteps participant AssertCmdCommand participant NetsukeBinary - participant NinjaTool Developer->>BddRunner: run bdd_tests advanced_usage BddRunner->>TestWorld: create TestWorld fixture @@ -1053,19 +1031,18 @@ sequenceDiagram AdvancedUsageSteps->>ManifestCommandSteps: reuse workspace_setup_steps ManifestCommandSteps->>TestWorld: create_workspace_with_manifest() - BddRunner->>AdvancedUsageSteps: execute When netsuke is run with args "manifest -" + BddRunner->>AdvancedUsageSteps: execute When netsuke is run with args "generate" AdvancedUsageSteps->>TestWorld: set_env_from_world() - TestWorld->>AssertCmdCommand: build_command_with_explicit_path() + TestWorld->>AssertCmdCommand: build_netsuke_command(world, args) AssertCmdCommand->>AssertCmdCommand: forward NETSUKE_NINJA override AssertCmdCommand->>AssertCmdCommand: apply_world_environment_overrides() AssertCmdCommand->>NetsukeBinary: spawn_with_env_and_path() - NetsukeBinary->>NinjaTool: optional_ninja_invocation() - NinjaTool-->>NetsukeBinary: build_status - NetsukeBinary-->>AssertCmdCommand: exit_code_stdout_stderr + NetsukeBinary->>NetsukeBinary: render_generated_ninja() + NetsukeBinary-->>AssertCmdCommand: exit_code_generated_stdout_stderr AssertCmdCommand-->>TestWorld: store_process_output() BddRunner->>AdvancedUsageSteps: execute Then stdout should contain Ninja_manifest - AdvancedUsageSteps->>TestWorld: assert_stdout_contains_manifest_markers() + AdvancedUsageSteps->>TestWorld: assert_stdout_contains_generated_ninja() BddRunner->>AdvancedUsageSteps: execute And stderr should be empty AdvancedUsageSteps->>TestWorld: assert_stderr_empty() @@ -1159,13 +1136,22 @@ whitespace-only `when` values, or type mismatches in the iterable. ## Runner process execution +`src/runner/dispatch.rs` is private to `runner::run` and owns command routing +plus successful JSON-result emission. `src/result_json.rs` owns only the +success envelope; diagnostic serialization remains in `src/diagnostic_json.rs`. +Both modules reuse only schema-version and generator metadata from the private +`src/json_envelope.rs` module. Within process execution, `forward_stdout` is +the single composition point for choosing status-aware or plain child-output +draining, and its callers select either the terminal or a JSON-mode sink. + ### Module: `runner::process::ninja_program` `src/runner/process/ninja_program.rs` owns the executable-resolution boundary. It is the only runner adapter that reads `NETSUKE_NINJA`, validates empty and -non-UTF-8 values, selects the default `ninja` fallback, and records the selected -source at debug level. Process construction uses the resolved path exported by -this module and must not interpret the environment override independently. +non-UTF-8 values, selects the default `ninja` fallback, and records the +selected source at debug level. Process construction uses the resolved path +exported by this module and must not interpret the environment override +independently. ### Module: `runner::process::command_logging` @@ -1185,8 +1171,8 @@ All command events share these structured fields: - `operation`: caller-provided operation label such as `"build"` or tool name. - `ninja_program`: command program after UTF-8 normalization. -- `suppress_stderr`: derived from `cli.resolved_diag_json()`, true when JSON - diagnostics suppress direct stderr logging. +- `suppress_stderr`: derived from `cli.json`, true when JSON output suppresses + direct child-process streams. Phase-specific fields supplement that shared set. The informational execution event includes `arg_count`. Spawn- and exit-failure events instead set @@ -1209,7 +1195,7 @@ paths: 1. Create `Command` with `Command::new(request.program)`. 2. Pass it into a closure that applies operation-specific configuration. 3. Call `run_command_and_stream_with_context` with optional status observer, - `cli.resolved_diag_json()` as `suppress_stderr`, and the chosen `operation`. + `cli.json` as `suppress_stderr`, and the chosen `operation`. 4. Let `run_command_and_stream_with_context` handle span creation, execution logging, failure logging, and exit-status enforcement via context helpers. diff --git a/docs/execplans/netsuke-cli-overhaul.md b/docs/execplans/netsuke-cli-overhaul.md index a13b49441..cf4076690 100644 --- a/docs/execplans/netsuke-cli-overhaul.md +++ b/docs/execplans/netsuke-cli-overhaul.md @@ -225,11 +225,11 @@ Observable success means: recorded above. - [x] Commit the roadmap-fidelity revision. - [x] Push the roadmap-fidelity revision. -- [ ] Stage A: create the governing design record. -- [ ] Stage B: update the core design document. -- [ ] Stage C: rewrite the CLI design document around the contract. +- [x] Stage A: create the governing design record. +- [x] Stage B: update the core design document. +- [x] Stage C: rewrite the CLI design document around the contract. - [x] Stage D: rewrite the roadmap. -- [ ] Stage E: update user-facing documentation. +- [x] Stage E: update user-facing documentation. - [ ] Stage F: add follow-on ExecPlans for implementation slices. ## Surprises & discoveries diff --git a/docs/netsuke-cli-design-document.md b/docs/netsuke-cli-design-document.md index 70b264713..ffd0dc1bf 100644 --- a/docs/netsuke-cli-design-document.md +++ b/docs/netsuke-cli-design-document.md @@ -51,7 +51,7 @@ Netsuke’s philosophy that error messages should guide the user constructively. From the very first run, Netsuke aims to be welcoming. The **help output** (invoked via `netsuke --help` or `netsuke help`) will be **concise and useful**, listing available subcommands (e.g. `build`, `clean`, `graph`, -`manifest`) with one-line descriptions of each. Clap auto-generates this usage +`generate`) with one-line descriptions of each. Clap auto-generates this usage text, and the wording remains newcomer-friendly. For example, the help summary for the default `build` command might say: “`build` – Build specified targets (or the default targets defined in the Netsukefile).” A **“Hello World”** @@ -153,7 +153,7 @@ cli-help-description = A modern build system for intuitive, fast builds. cli-help-build = Build specified targets (or defaults if none are specified). cli-help-clean = Remove build artifacts and temporary files. cli-help-graph = Visualize the build dependency graph. -cli-help-manifest = Generate the Ninja build file without executing it. +cli-help-generate = Generate the Ninja build file without executing it. status-parsing = Parsing manifest... status-generating = Generating build plan... @@ -268,26 +268,27 @@ detail how Netsuke’s CLI will meet key accessibility criteria: Netsuke offers **alternate output formats** for certain information, such as JSON or HTML. Large volumes of text in a terminal can be hard to navigate with a screen reader (users often resort to copying output to a text editor - or browser for easier navigation). Support for a `--diag-json` flag for error - output (as noted in the core design) emits structured error details in JSON, - which can be parsed or presented by external tools in a more accessible way. - For example, an IDE integration could catch the JSON and display errors in an - interface with headings and links. Similarly, for potentially lengthy outputs - like dependency graphs or build plan data, output can be redirected to a file - or formatted in HTML/CSV. The research strongly recommends providing ways to - translate CLI output (especially tables or complex structures) into - accessible formats like CSV or HTML. In line with this, the `netsuke graph` - command outputs the dependency graph in DOT format by default, but - `netsuke graph --html` (shipped in milestone 3.4.5) produces a self-contained - HTML visualization for sighted users; the HTML page also embeds a `

` - textual outline so screen-reader users get a structured fallback. A `--json` - view of the same graph data is the open follow-up (roadmap item `3.15.6`); - when it ships, screen-reader users will be able to take that JSON and - navigate the structure with a JSON viewer, or script custom queries, rather - than parsing the visually-structured DOT text dump. Documentation describes - the structure of such outputs clearly so users know what to expect (e.g., - which fields appear in the JSON), per Recommendation 3 about documenting - output structure in advance. + or browser for easier navigation). For commands that produce Netsuke result + or diagnostic envelopes, the root `--json` flag emits exactly one structured + result or diagnostic document, which external tools can parse or present in + a more accessible way. For example, an IDE integration could catch + the JSON and display errors in an interface with headings and links. + Similarly, for potentially lengthy outputs like dependency graphs or build + plan data, output can be redirected to a file or formatted in HTML/CSV. The + research strongly recommends providing ways to translate CLI output + (especially tables or complex structures) into accessible formats like CSV or + HTML. In line with this, the `netsuke graph` command outputs the dependency + graph in DOT format by default, but `netsuke graph --html` (shipped in + milestone 3.4.5) produces a self-contained HTML visualization for sighted + users; the HTML page also embeds a `
` textual outline so + screen-reader users get a structured fallback. A `--json` view of the same + graph data is the open follow-up (roadmap item `3.15.6`); when it ships, + screen-reader users will be able to take that JSON and navigate the structure + with a JSON viewer, or script custom queries, rather than parsing the + visually-structured DOT text dump. Documentation describes the structure of + such outputs clearly so users know what to expect (e.g., which fields appear + in the JSON), per Recommendation 3 about documenting output structure in + advance. - **Accessible Documentation:** Netsuke ensures that all documentation is available in an accessible format, not solely within the CLI. Unix release @@ -456,8 +457,9 @@ A screen reader will likely read `✔` as “check mark”, which along with the “succeeded” remains clear. If certain symbols are read poorly (e.g., the spinner might be read as “vertical bar, slash, dash…” each frame), accessible mode falls back to simpler characters. The CLI also respects user preferences: -for instance, if the environment variable `NETSUKE_NO_EMOJI` is set (an example -feature that can be supported), plain text “OK”/“FAIL” replaces check/cross. +for instance, if emoji output is disabled (via `--emoji never`, env +`NETSUKE_EMOJI=never`, or the legacy `NETSUKE_NO_EMOJI` alias), plain text +“OK”/“FAIL” replaces check/cross. Similarly, if `NO_COLOR` is set, coloured bars or coloured symbols are omitted [^2]. @@ -676,18 +678,17 @@ there are codes like `netsuke::yaml::parse`. These can be displayed to the user as well (perhaps in verbose mode or in machine-readable output) to help with documentation and support. -**JSON and Machine Readable Diagnostics:** As mentioned earlier, a flag like -`--diag-json` outputs errors in JSON form. When this flag is used, instead of -printing the fancy human-readable error, Netsuke outputs a JSON object -describing the error(s), including file paths, line/col, error code, message, -and even the snippet of source if applicable. This is extremely useful for -editor integrations or other tools (and aligns with recommendation to provide -alternate output formats). For instance, an IDE plugin could run -`netsuke --diag-json build` and get a JSON array of any errors which it can -then display with proper UI elements (the user wouldn’t even see the CLI text -in that case). The JSON schema is documented (so it’s an official interface), -and it includes all the information that is in the text diagnostic (error code, -message, labels, suggestions). +**JSON and Machine Readable Output:** For commands that reach Netsuke's result +and diagnostic rendering, the root `--json` flag makes each command emit +exactly one versioned document: a result document on success or a diagnostic +document on failure. Clap's informational early-exit paths, `netsuke --json +--help` and `netsuke --json --version`, are the exception: they retain +ordinary human-readable Clap output rather than a Netsuke JSON envelope. +Generated artefacts that would normally use stdout are embedded in the +successful result. Diagnostic documents include +paths, locations, codes, messages, source snippets, and suggestions where +available. This single-document boundary supports editor integrations, CI, and +other machine callers without mixing structured and human output. **No Hidden Meaning in Symbols/Colors:** Caution is exercised so that any symbols used (checkmarks, crosses, arrows, etc.) are simply decorative or @@ -731,18 +732,20 @@ fields such as: - `quiet: bool` – for noise-free mode -- `color: Option` – colour mode (“always”, “never”, or “auto”) - -- `output_format: Option` – format for output/diagnostics (“text” or - “json”) +- `color: ColourPolicy` – colour policy (“auto”, “always”, or “never”) - `default_target: Option` – default target(s) to build if none specified -- `spinner: bool` – whether to show spinner/progress (default true for - interactive) +- `progress: ProgressPolicy` – progress-display policy (“auto”, “always”, or + “never”) + +- `emoji: EmojiPolicy` – emoji policy (“auto”, “always”, or “never”) + +- `accessibility: AccessibilityPolicy` – accessibility policy (“auto”, “on”, or + “off”) -- `theme: Option` – theme for output (e.g., “unicode” vs “ascii”) +- `json: bool` – whether to emit machine-readable output This is an illustrative set – actual fields will be determined by what is needed. Using OrthoConfig’s derive macros, each field can automatically map to @@ -750,8 +753,8 @@ a CLI flag, an env var, and a config file entry. For example, `verbose: bool` can map to a `--verbose` flag (already in Clap), an env var like `NETSUKE_VERBOSE=true`, and a config file entry `verbose = true`. OrthoConfig’s **orthographic naming** feature will handle the naming conventions (so -`--no-color` flag might correspond to env `NETSUKE_NO_COLOR` and config file key -`color = "never"` etc.) without a lot of manual wiring[^3][^3]. A prefix like +the `--color never` flag corresponds to env `NETSUKE_COLOR=never` and config +file key `color = "never"`) without a lot of manual wiring[^3][^3]. A prefix like `NETSUKE_` is used for environment variables to avoid conflicts (the OrthoConfig derive allows specifying a prefix for env vars and file sections [^3] [^3]). @@ -770,11 +773,13 @@ not present, default settings apply. **Environment Variables:** Each config option will also map to an environment variable. For instance, to force colour off globally, a user could set -`NETSUKE_NO_COLOR=1` in their shell profile. Or to always get JSON diagnostic -output in a CI environment, one could set `NETSUKE_OUTPUT_FORMAT=json`. -Environment vars are convenient for CI and also for users who prefer them over -config files. They override the config file but are themselves overridden by -explicit CLI flags[^3]. +`NO_COLOR=1` in their shell profile. Or to emit exactly one versioned JSON +document – a result on success or a diagnostic on failure – from +envelope-producing commands in a CI environment, one can set +`NETSUKE_JSON=true` (`--help` and `--version` still print ordinary Clap +output). Environment vars are convenient +for CI and also for users who prefer them over config files. They override the +config file but are themselves overridden by explicit CLI flags[^3]. **Command-Line Flags:** OrthoConfig integrates with Clap such that flags parsed by Clap feed into the config struct. Since Clap is already being used for @@ -803,10 +808,10 @@ unified `config` object with all settings resolved in the right precedence. (default), “always”, “never”.) - **Spinner/Progress Display:** Some users might find spinners distracting or - might want more compact output. Configuration allows `spinner = false` (or - `progress = "none"`) in config to disable live progress indicators globally, - equivalent to always using a quiet mode. This could be useful for screen - reader users who want to opt-out entirely. + might want more compact output. Configuration allows `progress = "never"` in + config to disable live progress indicators globally, equivalent to always + using a quiet mode. This could be useful for screen reader users who want to + opt-out entirely. - **Default Targets or Profiles:** If a user frequently wants to build a specific target or use certain options, config can help. For example, @@ -816,11 +821,10 @@ unified `config` object with all settings resolved in the right precedence. supports things like debug vs release modes in the future, that could go here). -- **Output Format:** A user could set `output_format = "json"` in their config - for a CI environment, so that every Netsuke invocation automatically gives - JSON errors (unless they override with `--diag-json=false` on a specific - run). This is safer than expecting every developer to remember to pass - `--diag-json` in CI scripts. +- **Output Format:** A user can set `json = true` in configuration so every + envelope-producing Netsuke command emits one JSON result or diagnostic + document. An explicit root `--json` flag selects the same mode for one + invocation. - **Themes and Symbols:** Some users might prefer ASCII-only output (no Unicode symbols) due to font or locale issues. Support can include `theme = "ascii"` @@ -849,12 +853,13 @@ like: verbose = false quiet = true color = "never" -output_format = "text" -spinner = false +emoji = "never" +progress = "never" +accessibility = "on" ``` This ensures that by default, Netsuke will not use colour, will suppress -non-essential output, and will not show spinners or progress bars (quiet mode). +non-essential output, and will not show progress indicators (quiet mode). When they do need to see more details, they can still run `netsuke -v` to temporarily override quiet mode. OrthoConfig will merge that flag appropriately. @@ -945,9 +950,9 @@ like introspection commands and diagnostic modes come into play. `graph` clearly states what it does and mentions the `--html` option, guiding users to advanced usage. -- `netsuke manifest`: The user can output the Ninja file via - `netsuke manifest output.ninja` or stream it to stdout with - `netsuke manifest -`. The CLI confirms by writing the file and printing a +- `netsuke generate`: The user can output the Ninja file via + `netsuke generate --output output.ninja` or stream it to stdout with + `netsuke generate`. The CLI confirms by writing the file and printing a message like “Ninja file generated at output.ninja (not executed)”. If verbose, it might also print some stats (how many rules, how many targets). This command is for advanced debugging – the user sees exactly what build @@ -972,16 +977,15 @@ like introspection commands and diagnostic modes come into play. - **Customization and Environment Integration:** As the user integrates Netsuke into bigger projects and teams, they utilize the config ability. For example, - in a CI pipeline, they add `NETSUKE_OUTPUT_FORMAT=json` so that the CI can - parse errors. They might use `netsuke --diag-json` locally when working with - an editor that reads JSON diagnostics to highlight errors in code (some - editors could invoke netsuke on save to get live error feedback). They also - might set up a global config to turn off colour if the CI logs were getting - escape codes (they recall from documentation that `NO_COLOR` is respected or - use `NETSUKE_NO_COLOR`). In a team, one developer might prefer very quiet - output while another wants to see the full commands; each can use their - config to set that without affecting others, since it can be in their home - directory. + in a CI pipeline, they add `NETSUKE_JSON=true` so that the CI can parse + results and errors. They might use `netsuke --json` locally when working with + an editor that reads JSON output to highlight errors in code (some editors + could invoke netsuke on save to get live error feedback). They also might set + up a global config to turn off colour if the CI logs were getting escape + codes (they recall from documentation that `NO_COLOR` is respected). In a team, + one developer might prefer very quiet output + while another wants to see the full commands; each can use their config to + set that without affecting others, since it can be in their home directory. - **Advanced Debugging and Introspection:** A power user will find ways to debug complex build scenarios. For instance, if a build is not producing @@ -1190,15 +1194,18 @@ OPTIONS: -C, --directory Change to this directory before doing anything -j, --jobs Set the number of parallel build jobs (passed to Ninja) -v, --verbose Enable verbose logging output - -q, --quiet Minimal output (only errors and essential info) - --diag-json Output error diagnostics in JSON format - --no-color Disable coloured output + --no-input Never read interactive input + --json Emit one versioned JSON result or diagnostic document + --color Colour policy: auto, always, or never + --progress Progress policy: auto, always, or never + --emoji Emoji policy: auto, always, or never + --accessibility Accessibility policy: auto, on, or off SUBCOMMANDS: build Build specified targets (or default targets if none given) [default] - clean Remove build artifacts and intermediate files + clean Remove build artefacts and intermediate files graph Display the build dependency graph (DOT format or visual) - manifest Write the generated Ninja manifest to a file without running Ninja + generate Write the generated Ninja manifest without running Ninja help Print this help information ``` @@ -1207,7 +1214,10 @@ interface. The layout uses columns where possible (Clap does that automatically, wrapping descriptions nicely). Double-checking ensures that these help messages are themselves localizable (Clap supports localization, but manual overrides may be applied to route text through Fluent, depending on how -Clap’s derive macro interacts with internationalization). +Clap’s derive macro interacts with internationalization). The `--json` line +summarizes the flag; in practice it applies to commands that render result or +diagnostic envelopes, while `netsuke --json --help` and `netsuke --json +--version` keep Clap's ordinary informational output. **Ephemeral vs Persistent Output:** When using `indicatif` progress bars, by default once a progress bar finishes, it can either persist (leave the bar on diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 71a1a966a..ef4d7be73 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -2274,25 +2274,31 @@ enrichment: the user with a rich, layered explanation of the failure, from the general to the specific. -For automation use cases, Netsuke supports a `--diag-json` flag layered through -OrthoConfig as `--diag-json`, `NETSUKE_DIAG_JSON`, and `diag_json = true`. When -enabled, Netsuke emits a Netsuke-owned JSON document on `stderr` instead of -relying on upstream formatter output directly. The current schema is versioned -with `schema_version = 1` and an envelope of: - -- `generator`: `name` and `version` -- `diagnostics`: an array of entries containing `message`, `code`, `severity`, - `help`, `url`, `causes`, `source`, `primary_span`, `labels`, and `related` +For automation use cases, Netsuke supports a root `--json` flag layered through +OrthoConfig as `--json`, `NETSUKE_JSON`, and `json = true`. When enabled, every +invocation emits exactly one versioned JSON document: on success, a result +document on `stdout`; on failure, a diagnostic document on `stderr`. Both +documents share `schema_version = 1` and a `generator` object of `name` and +`version`: + +- The success result document carries a `result` object of `command` and + (optionally) `content`--the generated artefact, such as the Ninja text from + `generate`, when the command would otherwise write it to stdout. +- The failure diagnostic document carries a `diagnostics` array of entries + containing `message`, `code`, `severity`, `help`, `url`, `causes`, `source`, + `primary_span`, `labels`, and `related`. Design decisions for this mode: - Netsuke owns the schema rather than exposing `miette`'s raw JSON formatter, so compatibility can be documented and guarded by snapshot tests. -- JSON mode reserves `stderr` for one machine-readable document only. Progress - updates, verbose timing summaries, emoji prefixes, and tracing logs are - suppressed while the mode is active. -- `stdout` semantics do not change. Commands such as `manifest -` and `graph` - keep streaming their normal artefacts to `stdout`. +- JSON mode reserves its output stream for one machine-readable document only. + Progress updates, verbose timing summaries, emoji prefixes, and tracing logs + are suppressed while the mode is active. +- Outside JSON mode, `stdout` semantics do not change. Commands such as + `generate` keep streaming their normal artefacts to `stdout`; in JSON mode + that generated content is carried in the success result document's `content` + field instead. - Early startup failures honour only the CLI flag and environment variable. Configuration files cannot request JSON for errors raised while those same files are still being located or parsed. @@ -2417,9 +2423,9 @@ the targets listed in the `defaults` section of the manifest are built. guaranteed because the projection sorts every collection at the IR boundary. Ninja is not invoked. -- `Netsuke manifest FILE`: This command performs the pipeline up to Ninja - synthesis and writes the resulting Ninja file to `FILE` without invoking - Ninja. Supplying `-` for `FILE` streams the generated Ninja file to stdout. +- `netsuke generate`: This command performs the pipeline up to Ninja synthesis + and writes the resulting Ninja file to stdout without invoking Ninja. + Supplying `--output FILE` writes the generated Ninja file to `FILE` instead. ### 8.4 Design Decisions @@ -2435,18 +2441,16 @@ files or `NETSUKE_CMDS__BUILD__*` environment variables. Explicit CLI targets or `--emit` values still override those defaults. Configuration is layered in the order defaults -> configuration files -> -environment variables -> CLI overrides. Discovery honours `NETSUKE_CONFIG_PATH` -and the standard OrthoConfig search order; environment variables use the -`NETSUKE_` prefix with `__` as a nesting separator. The schema now explicitly -covers verbosity, locale, accessible mode, progress, colour policy, spinner -mode, output format, theme selection, fetch policy, and build defaults. `theme` -is the canonical presentation setting; the older `no_emoji` field remains as a -compatibility alias that canonicalizes to the ASCII theme. Conflicting -combinations such as `theme = "unicode"` together with `no_emoji = true` fail -during merge. `spinner_mode` likewise validates against the legacy `progress` -boolean so contradictory inputs are rejected early. `output_format` is typed -now, but only `human` is accepted until the future JSON diagnostics milestone -lands. +environment variables -> CLI overrides. Explicit discovery honours +`NETSUKE_CONFIG`; environment variables use the `NETSUKE_` prefix with `__` as +a nesting separator. The schema now explicitly covers verbosity, locale, +fetch policy, and build defaults alongside four typed output policies: +`color` (auto | always | never), `emoji` (auto | always | never), `progress` +(auto | always | never), and `accessibility` (auto | on | off). Each policy is +validated during configuration merge, so an unrecognized enum value from any +layer is rejected early rather than silently ignored. The `json` setting +selects machine-readable output: each invocation emits one versioned result +document on success or one versioned diagnostic document on failure. CLI help and clap errors are localized via Fluent resources; locale resolution is handled in `src/locale_resolution.rs` with the precedence `--locale` -> @@ -2465,7 +2469,7 @@ redaction, and the temporary file helpers reside in `src/runner/process.rs`, allowing the runner entry point to delegate low-level concerns. The working directory flag mirrors Ninja's `-C` option but is resolved internally: Netsuke runs Ninja with a configured working directory and resolves relative output -paths (for example `build --emit` and `manifest`) under the same directory so +paths (for example `generate --output`) under the same directory so behaviour matches a real directory change. Error scenarios are validated using clap's `ErrorKind` enumeration in unit tests and via rstest-bdd behavioural steps/scenarios. @@ -2482,8 +2486,9 @@ to textual output when stdout is not a teletype terminal (TTY), ensuring deterministic continuous integration (CI) logs; accessible mode always uses textual output. Accessible output remains text-first and static; it does not animate. The standard reporter is configurable through OrthoConfig layering via -`progress: Option` (`--progress`, `NETSUKE_PROGRESS`, or config file), -with accessible mode taking precedence when enabled. Verbose mode (`--verbose` +`progress: ProgressPolicy` (auto, always, or never), resolved from `--progress`, +`NETSUKE_PROGRESS`, or a config file, with accessible mode taking precedence when +enabled. Verbose mode (`--verbose` through OrthoConfig layers) wraps the resolved reporter with a timing recorder that emits a localized completion summary on successful runs: @@ -2496,15 +2501,16 @@ mode is off and also suppressed on failed runs so failures do not imply a successful pipeline completion. Theme resolution for CLI output is centralized in `src/theme.rs`. Netsuke -resolves one theme through OrthoConfig layers (`--theme`, `NETSUKE_THEME`, -config file, then mode defaults) and hands the resulting symbol and spacing -tokens to reporters through the `OutputPrefs` compatibility façade. This keeps -reporter code focused on status semantics rather than glyph choice, preserves -`no_emoji` as a legacy ASCII-forcing alias when no explicit theme is supplied, -and gives later roadmap items a stable snapshot surface for validating ASCII -and Unicode renderings without duplicating formatting rules. Colour policy is -resolved alongside theme and output-mode detection so `--colour-policy never` -behaves like an internal `NO_COLOR`, while `always` bypasses `NO_COLOR` +derives an internal theme preference from the `--emoji` policy (`emoji = +always` selects Unicode, `never` selects ASCII, and `auto` falls back to the +mode default) and hands the resulting symbol and spacing tokens to reporters +through the `OutputPrefs` compatibility façade. This keeps reporter code +focused on status semantics rather than glyph choice, preserves `no_emoji` as +a legacy ASCII-forcing alias when no explicit emoji policy is supplied, and +gives later roadmap items a stable snapshot surface for validating ASCII and +Unicode renderings without duplicating formatting rules. Colour policy is +resolved alongside theme and output-mode detection so `--color never` behaves +like an internal `NO_COLOR`, while `always` bypasses `NO_COLOR` auto-detection. Build dispatch also consults OrthoConfig `[cmds.build]` defaults before falling back to manifest `defaults`, letting operators set user- or workspace-level build defaults without editing the manifest itself. @@ -2514,7 +2520,7 @@ regressions fail with reviewable diffs instead of drifting silently. The Advanced Usage chapter in `docs/users-guide.md` is validated by behavioural tests in `tests/features/advanced_usage.feature`. Netsuke treats those -scenarios as executable documentation for the `clean`, `graph`, and `manifest` +scenarios as executable documentation for the `clean`, `graph`, and `generate` subcommands, configuration layering, and JSON diagnostics so the guide stays synchronized with runtime behaviour rather than drifting behind it. @@ -2579,27 +2585,25 @@ flowchart LR Netsuke configuration discovery is implemented in `src/cli/discovery.rs`. Explicit file selection is handled by `explicit_config_path(...)`, which -applies the precedence `--config` > `NETSUKE_CONFIG` > `NETSUKE_CONFIG_PATH`. -Layer loading and automatic discovery are handled by `push_file_layers(...)`, -which also applies the `-C/--directory` flag as the project-discovery root. +applies the precedence `--config` > `NETSUKE_CONFIG`. Layer loading and +automatic discovery are handled by `push_file_layers(...)`, which also applies +the `-C/--directory` flag as the project-discovery root. **Figure: Explicit Config Selector Resolution** — This diagram shows how Netsuke chooses the configuration file before automatic discovery. Netsuke -first checks `--config`, then `NETSUKE_CONFIG`, then `NETSUKE_CONFIG_PATH`. The -first explicit selector found is loaded directly; if that file is missing or -invalid, Netsuke reports the explicit-file error instead of falling back to -discovery. Only when no explicit selector is present does Netsuke run two-pass -config discovery and then proceed with the merged configuration. +first checks `--config`, then `NETSUKE_CONFIG`. The first explicit selector +found is loaded directly; if that file is missing or invalid, Netsuke reports +the explicit-file error instead of falling back to discovery. Only when no +explicit selector is present does Netsuke run two-pass config discovery and +then proceed with the merged configuration. ```mermaid flowchart TD Start(["Start netsuke invocation"]) HasCliConfig{"CLI flag
--config set?"} HasEnvConfig{"Env NETSUKE_CONFIG
set?"} - HasEnvConfigPath{"Env NETSUKE_CONFIG_PATH
set?"} UseCliConfig[["Use CLI --config path
as config file"]] UseEnvConfig[["Use NETSUKE_CONFIG path
as config file"]] - UseEnvConfigPath[["Use NETSUKE_CONFIG_PATH path
as config file"]] RunDiscovery[["Run two-pass
config discovery"]] LoadConfig[["Load selected config
into merge pipeline"]] ErrorMissing[["Error: explicit config
file missing or invalid"]] @@ -2609,10 +2613,7 @@ flowchart TD HasCliConfig -- No --> HasEnvConfig HasEnvConfig -- Yes --> UseEnvConfig - HasEnvConfig -- No --> HasEnvConfigPath - - HasEnvConfigPath -- Yes --> UseEnvConfigPath - HasEnvConfigPath -- No --> RunDiscovery + HasEnvConfig -- No --> RunDiscovery UseCliConfig --> CheckCliFile{"File exists
and parses?"} CheckCliFile -- Yes --> LoadConfig @@ -2622,10 +2623,6 @@ flowchart TD CheckEnvFile -- Yes --> LoadConfig CheckEnvFile -- No --> ErrorMissing - UseEnvConfigPath --> CheckEnvPathFile{"File exists
and parses?"} - CheckEnvPathFile -- Yes --> LoadConfig - CheckEnvPathFile -- No --> ErrorMissing - RunDiscovery --> LoadConfig LoadConfig --> End(["Proceed with merged config"]) ErrorMissing --> End @@ -2642,11 +2639,10 @@ override earlier ones—meaning project-scope has highest precedence among file layers. After file layers are merged, environment variables and CLI arguments override the merged result, ensuring explicit user intent always wins. -1. **Explicit override**: `--config `, `NETSUKE_CONFIG`, and - `NETSUKE_CONFIG_PATH` are evaluated in that precedence order before - discovery. These explicit selectors bypass automatic discovery and ignore - the project-root anchor supplied by `-C/--directory`. `NETSUKE_CONFIG_PATH` - remains a backward-compatible alias of `NETSUKE_CONFIG`. +1. **Explicit override**: `--config ` and `NETSUKE_CONFIG` are evaluated + in that precedence order before discovery. These explicit selectors bypass + automatic discovery and ignore the project-root anchor supplied by + `-C/--directory`. 2. **Project scope**: Configuration files in the current working directory (or the directory specified via `-C/--directory`): diff --git a/docs/roadmap.md b/docs/roadmap.md index 9d52d1586..03fd8e4f3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -127,8 +127,8 @@ and agents. - [ ] 3.11.4. Add OrthoConfig precedence-ladder regression tests. - [x] Explicit config-path selector precedence (`--config` > - `NETSUKE_CONFIG` > `NETSUKE_CONFIG_PATH`) verified by exhaustive rstest - cases and a proptest property test (PR `#327`, closes `#291`). + `NETSUKE_CONFIG`) verified by exhaustive rstest cases and a proptest + property test (PR `#327`, closes `#291`). - [ ] Depend on OrthoConfig `5.2.3` for consumer boundary guidance. - [ ] Preserve Netsuke-specific precedence expectations for manifest path, display policies, locale, and profile selection. @@ -251,8 +251,8 @@ and agents. ### 3.15. Canonical CLI redesign - [ ] 3.15.1. Replace the pre-0.1.0 command surface with canonical names. - - [ ] Rename `manifest` to `generate`. - - [ ] Remove `build --emit`; use `generate --output`. + - [x] Rename `manifest` to `generate`. + - [x] Remove `build --emit`; use `generate --output`. - [ ] Add `check`, `context`, `skill-path`, `runs`, `profile`, and `feedback`. - [ ] Rename `--file` to `--manifest`, keeping `-f` as an intentional @@ -261,8 +261,8 @@ and agents. and global option glossary. - [ ] 3.15.2. Add non-interactive and mutation-safety guarantees. - - [ ] Add root `--no-input`. - - [ ] Make prompts impossible unless a future explicit interactive mode is + - [x] Add root `--no-input`. + - [x] Make prompts impossible unless a future explicit interactive mode is added. - [ ] Require `--force` for destructive operations. - [ ] Require or support `--dry-run` for consequential operations. @@ -271,23 +271,23 @@ and agents. non-interactive and mutation metadata. - [ ] 3.15.3. Replace diagnostics-only JSON with canonical structured output. - - [ ] Remove `--diag-json` and `--output-format`. - - [ ] Add root `--json`. - - [ ] Emit exactly one JSON result document on successful JSON-mode commands. - - [ ] Emit exactly one JSON diagnostic document on failing JSON-mode commands. - - [ ] Suppress progress, colour, emoji, tracing, and timing text in JSON mode. + - [x] Remove `--diag-json` and `--output-format`. + - [x] Add root `--json`. + - [x] Emit exactly one JSON result document on successful JSON-mode commands. + - [x] Emit exactly one JSON diagnostic document on failing JSON-mode commands. + - [x] Suppress progress, colour, emoji, tracing, and timing text in JSON mode. - [ ] Snapshot every v1 JSON schema. - [ ] Depend on OrthoConfig `7.2.3` to `7.2.5`, `7.3.1`, `8.1.1`, and `8.1.2` for shared result, stream, exit-code, and enumerable-error metadata. - [ ] 3.15.4. Replace legacy output preferences with canonical policy flags. - - [ ] Replace `--colour-policy` with `--color auto|always|never`. - - [ ] Replace `--spinner-mode` and boolean `--progress` with + - [x] Replace `--colour-policy` with `--color auto|always|never`. + - [x] Replace `--spinner-mode` and boolean `--progress` with `--progress auto|always|never`. - - [ ] Replace `--no-emoji` with `--emoji auto|always|never`. - - [ ] Add `--accessibility auto|on|off`. - - [ ] Update OrthoConfig field integration, environment names, config + - [x] Replace `--no-emoji` with `--emoji auto|always|never`. + - [x] Add `--accessibility auto|on|off`. + - [x] Update OrthoConfig field integration, environment names, config examples, localization keys, and tests. - [ ] Depend on OrthoConfig `7.1.2`, `7.1.3`, and `7.2.3` for shared flag vocabulary and dual-renderer metadata. diff --git a/docs/sample-netsuke.toml b/docs/sample-netsuke.toml index 865f087aa..95fc7d446 100644 --- a/docs/sample-netsuke.toml +++ b/docs/sample-netsuke.toml @@ -18,29 +18,24 @@ # Locale for CLI messages, for example `en-US` or `es-ES`. # locale = "en-US" -# Force accessible output mode on or off. -# accessible = true +# Emit one versioned JSON document: a result on success or diagnostic on failure. +# json = false -# Suppress emoji glyphs in output. -# no_emoji = false - -# Theme preset: `auto`, `unicode`, or `ascii`. -# theme = "auto" +# Never read interactive input. This must remain true until an explicit +# interactive mode exists. +# no_input = true # Colour output policy: `auto`, `always`, or `never`. -# colour_policy = "auto" - -# Force standard stage and task progress summaries on or off. -# progress = true +# color = "auto" -# Spinner mode: `enabled` or `disabled`. -# spinner_mode = "enabled" +# Emoji policy: `auto`, `always`, or `never`. +# emoji = "auto" -# Emit machine-readable diagnostics on stderr. -# diag_json = false +# Progress policy: `auto`, `always`, or `never`. +# progress = "auto" -# Diagnostic output format: `human` or `json`. -# output_format = "human" +# Accessibility policy: `auto`, `on`, or `off`. +# accessibility = "auto" # Default build targets when the CLI does not name any targets. # default_targets = ["fmt", "lint", "test"] diff --git a/docs/users-guide.md b/docs/users-guide.md index a406cdccc..b7e750bd0 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -1,879 +1,606 @@ -# Netsuke User Guide +# Netsuke user's guide -## 1\. Introduction: What is Netsuke? +This guide is for people evaluating or using Netsuke v0.1.0. It covers the +first build, the manifest format, templating, command-line usage, +configuration, diagnostics, accessibility, and the current safety boundary. -Netsuke is a modern, declarative build system designed to be intuitive, fast, -and safe. Think of it not just as a `make` replacement, but as a **build system -compiler**. You describe your build process in a human-readable YAML manifest -(`Netsukefile`), leveraging the power of Jinja templating for dynamic logic. -Netsuke then compiles this high-level description into an optimized build plan -executed by the high-performance [Ninja](https://ninja-build.org/ "null") build -system. +Netsuke v0.1.0 is an early-adopter release. The compiler pipeline is useful, +but command names, flags, diagnostic schemas, and some manifest details may +change before 1.0. Pin the Netsuke version in automated workflows. -**Core Philosophy:** +## Install Netsuke -- **Declarative:** Define *what* you want to build, not *how* step-by-step. +Netsuke requires [Ninja](https://ninja-build.org/) on `PATH`. A source build +also requires Rust 1.89 or later. -- **Dynamic where needed:** Use Jinja for variables, loops (`foreach`), - conditionals (`when`), file globbing (`glob`), and more. +Until v0.1.0 is published, the current checkout can be installed with Cargo: -- **Static Execution Plan:** All dynamic logic is resolved *before* - execution, resulting in a static Ninja file for fast, reproducible builds. - -- **Safety First:** Automatic shell escaping prevents command injection - vulnerabilities. - -- **Fast Execution:** Leverages Ninja for efficient dependency tracking and - parallel execution. - -## 2\. Getting Started - -### Installation - -Netsuke is typically built from source using Cargo: + ```sh -cargo build --release -# The executable will be in target/release/netsuke - +git clone https://github.com/leynos/netsuke.git +cd netsuke +cargo install --path . ``` -Refer to the project's `README.md` or release pages for pre-compiled binaries -if available. Ensure the `ninja` executable is also installed and available in -your system's `PATH`. - -### Release help artefacts +Release archives contain platform-specific packages and help artefacts. Unix +archives include a `netsuke.1` manual page. Windows archives include PowerShell +external help: -Release archives include platform help files generated from the same command -metadata that powers `netsuke --help`. Unix-like release artefacts include the -`netsuke.1` manual page. Windows release artefacts include PowerShell external -help in a `Netsuke` module layout, so installed or unpacked artefacts can be -inspected with: + ```powershell Get-Help Netsuke -Full ``` -This change affects release packaging only. The Netsuke command-line flags, -subcommands, output, and exit statuses are unchanged. - -### Basic Usage - -The primary way to use Netsuke is through its command-line interface (CLI). The -default command is `build`. - -1. **Create a `Netsukefile`:** Define your build rules and targets in a YAML - file named `Netsukefile` in your project root. - -2. **Run Netsuke:** Execute the `netsuke` command. - -```sh -netsuke # Builds default targets defined in Netsukefile -netsuke build target_name another_target # Builds specific targets - -``` - -If no `Netsukefile` is found, Netsuke will provide a helpful error message: - -```text -Error: Manifest 'Netsukefile' not found in the current directory. -help: Ensure the manifest exists or pass `--file` with the correct path. -``` - -A different manifest path can be specified using the `-f` or `--file` option: - -```sh -netsuke -f path/to/manifest.yml -``` - -For a step-by-step introduction, see the [Quick Start guide](quickstart.md). - -## 3\. The Netsukefile Manifest - -The `Netsukefile` is a YAML file describing the build process. +## Run the first build -Netsuke targets YAML 1.2 and forbids duplicate keys in manifests. If the same -mapping key appears more than once (even if a YAML parser would normally accept -it with “last key wins” behaviour), Netsuke treats this as an error. +Create an empty project directory and add a file named `Netsukefile` with the +following complete manifest: -### Top-Level Structure + ```yaml -# Mandatory: Specifies the manifest format version netsuke_version: "1.0.0" -# Optional: Global variables accessible throughout the manifest -vars: - cc: gcc - src_dir: src - -# Optional: Reusable Jinja macros -macros: - - signature: "compile_cmd(input, output)" - body: | - {{ cc }} -c {{ input }} -o {{ output }} - -# Optional: Reusable command templates -rules: - - name: link - command: "{{ cc }} {{ ins }} -o {{ outs }}" - -# Optional: Phony targets often used for setup or meta-tasks -# Implicitly phony: true, always: false -actions: - - name: clean - command: "rm -rf build *.o" - -# Required: Defines the build targets (artefacts to produce) targets: - - name: build/main.o - # A target can define its command inline... - command: "{{ compile_cmd('src/main.c', 'build/main.o') }}" - sources: src/main.c - - - name: my_app - # ...or reference a rule - rule: link - sources: build/main.o + - name: hello.txt + command: "echo 'Hello from Netsuke!' > hello.txt" -# Optional: Targets to build if none are specified on the command line defaults: - - my_app - + - hello.txt ``` -### Key Sections Explained +Run Netsuke without a subcommand to build the manifest's `defaults`, then read +the generated file: -- `netsuke_version` (**Required**): Semantic version string (e.g., `"1.0.0"`) - indicating the manifest schema version. Parsed using `semver`. + -- `vars` (Optional): A mapping (dictionary) of global variables. Values can - be strings, numbers, booleans, or lists, accessible within Jinja expressions. +```sh +netsuke +cat hello.txt +``` -- `macros` (Optional): A list of Jinja macro definitions. Each item has a - `signature` (e.g., `"my_macro(arg1, arg2='default')"` ) and a multi-line - `body`. +The second command prints `Hello from Netsuke!`. -- `rules` (Optional): A list of named, reusable command templates. +If Netsuke cannot find `Netsukefile`, it reports the missing file and suggests +`--file`. A different path can be selected with +`netsuke --file path/to/manifest.yml build`. -- `targets` (**Required**): A list defining the primary build outputs (files - or logical targets). +The [quick-start guide](quickstart.md) provides a longer walkthrough. -- `actions` (Optional): Similar to `targets`, but entries here are implicitly - treated as `phony: true` (meaning they don't necessarily correspond to a file - and should run when requested). Useful for tasks like `clean` or `test`. +## Understand the build model -- `defaults` (Optional): A list of target names (from `targets` or `actions`) - to build when `netsuke` is run without specific targets. +Netsuke is a build-system compiler. A build moves through six stages: -## 4\. Defining Rules +1. Read the manifest. +2. Parse YAML 1.2. +3. Expand manifest-time `foreach` and `when` expressions. +4. Deserialize the typed manifest and render string fields. +5. Build and validate a static intermediate representation (IR). +6. Generate Ninja and, for `build` or `clean`, run Ninja. -Rules encapsulate reusable build commands or scripts. +All manifest-time decisions finish before Ninja starts. The generated graph is +therefore static and inspectable. -```yaml -rules: - - name: compile # Unique identifier for the rule - # Recipe: Exactly one of 'command', 'script', or 'rule' - command: "{{ cc }} {{ cflags }} -c {{ ins }} -o {{ outs }}" - # Optional: Displayed during the build - description: "Compiling {{ outs }}" +A `Netsukefile` is executable build configuration, not passive data. Commands, +scripts, and impure template helpers can access the host. Review an untrusted +manifest with the same care as an untrusted `Makefile`. -``` +## Author a manifest -- `name`: Unique string identifier. +`Netsukefile` is a YAML mapping. Unknown fields and duplicate mapping keys are +errors. `netsuke_version` and `targets` are required; all other top-level +collections are optional. -- `recipe`: The action to perform. Defined by one of: +The following complete example shows every top-level section: - - `command`: A single shell command string. May contain `{{ ins }}` - (space-separated inputs) and `{{ outs }}` (space-separated outputs). These - specific placeholders are substituted *after* Jinja rendering but *before* - hashing the action. All other Jinja interpolations happen first. The final - command must be parseable by `shlex` (POSIX mode). + - - `script`: A multi-line script (using YAML `|`). If it starts with `#!`, - it's executed directly. Otherwise, it's run via `/bin/sh -e` (or PowerShell - on Windows) by default. Interpolated variables are automatically - shell-escaped unless `| raw` is used. +```yaml +netsuke_version: "1.0.0" - - `rule`: References another rule by name (less common within a rule - definition). +vars: + greeting: Hello -- `description` (Optional): A user-friendly message printed by Ninja when - this rule runs. Can contain `{{ ins }}` / `{{ outs }}`. +macros: + - signature: "message(name)" + body: | + {{ greeting }}, {{ name }}! -## 5\. Defining Targets and Actions +rules: + - name: write_message + command: "echo '{{ message('Netsuke') }}' > {{ outs }}" + description: "Writing greeting" -Targets define *what* to build or *what action* to perform. +actions: + - name: greet + command: "echo '{{ message('builder') }}'" -```yaml targets: - # Example 1: Building an object file using a rule - - name: build/utils.o # Output file(s). Can be a string or list. - rule: compile # Rule to use (mutually exclusive with command/script) - sources: src/utils.c # Input file(s). String or list. - deps: # Implicit dependencies: trigger rebuilds but - # not passed to $in/{{ ins }} - - build/utils.h - vars: # Target-local variables, override globals - cflags: "-O0 -g" - - # Example 2: Linking an executable using an inline command - - name: my_app - command: "{{ cc }} build/main.o build/utils.o -o my_app" - sources: # Explicit recipe inputs: passed to - # $in/{{ ins }} and trigger rebuilds - - build/main.o - - build/utils.o - order_only_deps: # Dependencies built before, but changes - # do not trigger rebuild - - build_directory # e.g., Ensure 'build/' exists - - # Example 3: A phony action (can also be in top-level 'actions:') - - name: package - phony: true # Doesn't represent a file, always considered out-of-date - command: "tar czf package.tar.gz my_app" - deps: my_app # Depends on the 'my_app' target - - # Example 4: A target that always runs - - name: timestamp - always: true # Runs every time, regardless of inputs/outputs - command: "date > build/timestamp.txt" + - name: greeting.txt + rule: write_message +defaults: + - greeting.txt ``` -- `name`: Output file path(s). Can be a string or a list (`StringOrList`). +The top-level fields are: -- `recipe`: How to build the target. Defined by one of `rule`, `command`, or - `script` (mutually exclusive). +- `netsuke_version`: required semantic version for the manifest schema. +- `vars`: global strings, numbers, booleans, or lists available to Jinja. +- `macros`: named Jinja macro definitions registered before other fields + render. +- `rules`: reusable recipes referenced by targets or actions. +- `actions`: implicitly phony operations, such as `test` or `lint`. +- `targets`: required list of file-producing or logical build nodes. An empty + list is valid for an action-only manifest. +- `defaults`: target or action names used when `build` receives no explicit + targets. -- `sources`: Input file path(s) (`StringOrList`). Sources are explicit recipe - inputs: they are passed to `$in` and `{{ ins }}` and trigger rebuilds when - changed. +`defaults` entries are literal names in v0.1.0; Jinja expressions are not +rendered in this field. -- `deps` (Optional): Implicit target dependencies (`StringOrList`). Changes - trigger rebuilds, but these paths are not passed to `$in` or `{{ ins }}`. - Maps to Ninja `|`. +### Rules and recipes -- `order_only_deps` (Optional): Dependencies that must run first but whose - changes don't trigger rebuilds (`StringOrList`). Maps to Ninja `||`. +A rule or target must provide exactly one recipe: -Cycle detection traverses `sources` and `deps`, because both classes affect the -build graph and rebuild freshness. `order_only_deps` only enforce build -ordering and do not participate in Netsuke's cycle detection. +- `command`: one shell command. +- `script`: a multi-line POSIX shell script. +- `rule`: the name of another rule to use. -- `vars` (Optional): Target-specific variables that override global `vars`. +Rules may also provide `description`, text used for Ninja's progress display. -- `phony` (Optional, default `false`): Treat target as logical, not a file. - Always out-of-date if requested. +The v0.1.0 `script` implementation invokes `/bin/sh -e`; it is not currently a +portable PowerShell abstraction. Prefer `command` or platform-selected actions +when a manifest must work on Windows. -- `always` (Optional, default `false`): Re-run the command every time - `netsuke` is invoked, regardless of dependency changes. +### Targets, inputs, and dependencies -`StringOrList`: Fields like `name`, `sources`, `deps`, and `order_only_deps` -accept either a single string or a YAML list of strings for convenience. +A target supports these fields: -## 6\. Jinja Templating in Netsuke +- `name`: one output path or a list of output paths. +- `rule`, `command`, or `script`: exactly one recipe. +- `sources`: explicit inputs. They affect freshness and become `{{ ins }}`. +- `deps`: implicit dependencies. They affect freshness but do not become + recipe arguments. Declare them on each target; reusable rules reject `deps`. + The planned rule-level `deps_from` contract is not implemented in v0.1.0. +- `order_only_deps`: ordering dependencies. Their changes do not rebuild the + dependent target. +- `vars`: values that override global variables for this target. +- `phony`: marks a logical target that does not represent a file. +- `always`: forces the recipe to run whenever the target is requested. -Netsuke uses the [MiniJinja](https://docs.rs/minijinja "null") engine to add -dynamic capabilities to your manifest. +`name`, `sources`, `deps`, and `order_only_deps` accept either one string or a +list of strings. -### Basic Syntax +Netsuke quotes paths inserted through `{{ ins }}` and `{{ outs }}`. Other Jinja +values render as ordinary command text and are not automatically shell-quoted. +The `shell_escape` filter described in older drafts is not implemented in +v0.1.0. -- Variables: `{{ my_variable }}` +Cycle detection follows `sources` and `deps`. Order-only dependencies enforce +ordering but do not participate in cycle detection. -- Expressions: `{{ 1 + 1 }}`, `{{ sources | map('basename') }}` +## Use Jinja safely -- Control Structures (within specific keys like `foreach`, `when`, or inside - `macros`): `{% if enable %}…{% endif %}`, - `{% for item in list %}…{% endfor %}` +Jinja expressions are allowed in renderable string fields, including variables, +target fields, and rule recipes. Structural Jinja blocks cannot reshape the +YAML document. Use the dedicated `foreach` and `when` keys for manifest-time +expansion. -**Important:** Structural Jinja (`{% %}`) is generally **not** allowed directly -within the YAML structure outside of `macros`. Logic should primarily be within -string values or dedicated keys like `foreach` and `when`. +### Generate targets with `foreach` and `when` -### Processing Order +The next complete manifest creates two targets and excludes the disabled item: -Netsuke processes the manifest in stages: - -1. Initial YAML Parse: Load the raw file into an intermediate structure (like - `serde_json::Value`). - -2. Template Expansion (`foreach`, `when`): Evaluate `foreach` expressions to - generate multiple target or action definitions. The `item` (and optional - `index`) become available in the context. Evaluate `when` expressions to - conditionally include/exclude entries (`when` can exclude both targets and - actions). This is a manifest-time selection step; skipped entries are - removed before Netsuke builds the typed manifest AST, creates its IR, emits - `build.ninja`, or runs Ninja. - -3. Deserialization to AST: Convert the expanded intermediate structure into - Netsuke's typed Rust structs (`NetsukeManifest`, `Target`, etc.). - -4. Final Rendering: Render Jinja expressions **only within string fields** - (like `command`, `description`, `name`, `sources` etc.) using the combined - context (globals + target vars + iteration vars). - -### `foreach` and `when` - -These keys enable generating multiple similar targets or actions -programmatically. + ```yaml -targets: - # Generate a target for each .c file in src/ - - foreach: glob('src/*.c') # Jinja expression returning an iterable - # Only include if the filename isn't 'skip.c' - when: item | basename != 'skip.c' - # 'item' and 'index' (0-based) are available in the context - name: "build/{{ item | basename | with_suffix('.o') }}" - rule: compile - sources: "{{ item }}" - vars: - compile_flags: "-O{{ index + 1 }}" # Example using index +netsuke_version: "1.0.0" -``` +vars: + reports: + - daily + - weekly + - disabled -```yaml -actions: - # Generate named test actions, skipping disabled suites. - - foreach: - - unit - - integration - - disabled +targets: + - foreach: reports when: item != 'disabled' - name: "test-{{ item }}" - command: "cargo test --test {{ item }}" + name: "{{ item }}.txt" + command: "echo {{ index }} > {{ outs }}" + +defaults: + - daily.txt + - weekly.txt ``` -- `foreach`: A Jinja expression evaluating to a list (or any iterable). A - target or action definition will be generated for each item. +Each expansion receives `item` and a zero-based `index`. Target-local variables +take precedence over global variables, and iteration values take precedence +over both. -- `when` (Optional): A Jinja expression evaluating to a boolean. If false, - the target or action generated for the current `item` is skipped. +`when` is evaluated while Netsuke loads the manifest. It does not create a +runtime branch. Runtime decisions belong in a command or script. -`foreach` and `when` do not create build-time branches. They decide which -manifest entries exist while Netsuke loads the manifest. The generated Ninja -file contains only the selected targets and actions, so a skipped entry cannot -contribute rules, outputs, dependencies, defaults, or command text later in the -build pipeline. +Top-level actions support the same `foreach` and `when` keys. -When a decision must happen while a target is being built, put that branching -inside the recipe command or script: +### Define reusable macros -```yaml -targets: - - name: report - script: | - if test -f report.in; then - ./render report.in > report - else - ./render-empty > report - fi -``` +Macros return rendered text and can accept default arguments: -A future runtime-condition feature may model build-time branching directly, but -current manifests should treat recipe commands and scripts as the build-time -decision point. + -### User-defined Macros +```yaml +netsuke_version: "1.0.0" -Define reusable Jinja logic in the top-level `macros` section. +vars: + greeting: Hello -```yaml macros: - - signature: "cc_cmd(src, obj, flags='')" # Jinja macro signature - body: | # Multi-line body - {{ cc }} {{ flags }} -c {{ src | shell_escape }} \ - -o {{ obj | shell_escape }} + - signature: "say(name, punctuation='!')" + body: | + {{ greeting }}, {{ name }}{{ punctuation }} targets: - - name: build/main.o - command: "{{ cc_cmd('src/main.c', 'build/main.o', flags=cflags) }}" - sources: src/main.c + - name: greeting.txt + command: "echo '{{ say('Netsuke') }}' > {{ outs }}" +defaults: + - greeting.txt ``` -## 7\. Netsuke Standard Library (Stdlib) - -Netsuke provides a rich set of built-in Jinja functions, filters, and tests to -simplify common build tasks. These are automatically available in your manifest -templates. +### Select optional tools -### Key Functions +`which(name, **kwargs)` returns an executable path and fails when the command +is absent. The same helper is also available as a filter. -- `env(name, default=None)`: Reads an environment variable. Fails if `name` - is unset and no `default` is provided. Example: `{{ env('CC', 'gcc') }}` +`command_available(name, **kwargs)` returns a boolean and is better for +complementary branches: -- `glob(pattern)`: Expands a filesystem glob pattern into a sorted list of - *files* (directories are excluded). Handles `*`, `**`, `?`, `[]`. - Case-sensitive. Example: `{{ glob('src/**/*.c') }}` + -- `fetch(url, cache=False)`: Downloads content from a URL. If `cache=True`, - caches the result in `.netsuke/fetch` within the workspace based on URL hash. - Enforces a configurable maximum response size (default 8 MiB); requests abort - with an error quoting the configured threshold when the limit is exceeded. - Cached downloads stream directly to disk and remove partial files on error. - Configure the limit with `StdlibConfig::with_fetch_max_response_bytes`. Marks - template as impure. - -- `now(offset=None)`: Returns the current time as a timezone-aware object - (defaults to UTC). `offset` can be '+HH:MM' or 'Z'. Exposes `.iso8601`, - `.unix_timestamp`, `.offset`. - -- `timedelta(...)`: Creates a duration object (e.g., for age comparisons). - Accepts `weeks`, `days`, `hours`, `minutes`, `seconds`, `milliseconds`, - `microseconds`, `nanoseconds`. Exposes `.iso8601`, `.seconds`, `.nanoseconds`. - -### Key Filters +```yaml +netsuke_version: "1.0.0" -Apply filters using the pipe `|` operator: `{{ value | filter_name(args...) }}` +actions: + - name: test-fast + command: "cargo nextest run" + when: command_available("cargo-nextest") -**Path & File Filters:** + - name: test-fast + command: "cargo test" + when: not command_available("cargo-nextest") -- `basename`: `{{ 'path/to/file.txt' | basename }}` -> `"file.txt"` +targets: [] -- `dirname`: `{{ 'path/to/file.txt' | dirname }}` -> `"path/to"` +defaults: + - test-fast +``` -- `with_suffix(new_suffix, count=1, sep='.')`: Replaces the last `count` - dot-separated extensions. `{{ 'archive.tar.gz' | with_suffix('.zip', 2) }}` -> - `"archive.zip"` +Both helpers accept: -- `relative_to(base_path)`: Makes a path relative. - `{{ '/a/b/c' | relative_to('/a/b') }}` -> `"c"` +- `all=true`: return all `which` matches. It does not change the boolean result + from `command_available`. +- `canonical=true`: canonicalize matching paths. +- `fresh=true`: bypass the resolver cache for this lookup. +- `cwd_mode="auto"|"always"|"never"`: control bounded project-directory + fallback searching. -- `realpath`: Canonicalizes path, resolving symlinks. +The `env(name)` function reads one required environment variable. v0.1.0 does +not accept a default argument; an absent or non-Unicode value is an error. -- `expanduser`: Expands `~` to the home directory. +## Use the template standard library -- `contents(encoding='utf-8')`: Reads file content as a string. +Netsuke registers focused path, collection, command, network, and time helpers +alongside MiniJinja's built-ins. -- `size`: File size in bytes. +### Path filters -- `linecount`: Number of lines in a text file. +The path filters are: -- `hash(alg='sha256')`: Full hex digest of file content (supports - `sha256`, `sha512`; `md5`, `sha1` if `legacy-digests` feature enabled). +- `basename` +- `dirname` +- `with_suffix(suffix[, count[, separator]])` +- `relative_to(root)` +- `realpath` +- `expanduser` +- `contents([encoding])` +- `size` +- `linecount` +- `hash([algorithm])` +- `digest([length[, algorithm]])` -- `digest(len=8, alg='sha256')`: Truncated hex digest. +`with_suffix` defaults to replacing one dot-separated suffix. `contents` +defaults to UTF-8, `hash` defaults to SHA-256, and `digest` defaults to the +first eight characters of a SHA-256 digest. Hashing supports SHA-256 and +SHA-512. MD5 and SHA-1 require the `legacy-digests` Cargo feature. -- `shell_escape`: **Crucial for security.** Safely quotes a string for use as - a single shell argument. *Use this whenever interpolating paths or variables - into commands unless you are certain they are safe.* +### Collection filters -**Collection Filters:** +Netsuke adds `uniq`, `flatten`, and `group_by(attribute)`. MiniJinja also +provides general filters such as `join`, `map`, `select`, and `sort`. -- `uniq`: Removes duplicate items from a list, preserving order. +This complete manifest exercises string-only helpers without depending on +external files: -- `flatten`: Flattens a nested list. `{{ [[1], [2, 3]] | flatten }}` -> - `[1, 2, 3]` + -- `group_by(attribute)`: Groups items in a list of dicts/objects by an - attribute's value. +```yaml +netsuke_version: "1.0.0" -- `map(attribute='...')` / `map('filter', ...)`: Applies attribute access or - another filter to each item in a list. +vars: + names: + - alpha + - alpha + - beta -- `filter(attribute='...')` / `filter('test', ...)`: Selects items based on - attribute value or a test function. +targets: + - name: "{{ 'report.tmp' | with_suffix('.txt') }}" + command: "echo {{ names | uniq | join(',') }} > {{ outs }}" -- `join(sep)`: Joins list items into a string. +defaults: + - report.txt +``` -- `sort`: Sorts list items. +### File tests -**Command Filters (Impure):** +Jinja `is` expressions can test `file`, `dir`, `symlink`, `pipe`, +`block_device`, `char_device`, and `device`. Filesystem tests inspect the host, +so results depend on the current workspace and platform. -- `shell(command_string)`: Pipes the input value (string or bytes) as stdin - to `command_string` executed via the system shell (`sh -c` or `cmd /C`). - Returns stdout. **Marks the template as impure.** Example: - `{{ user_list | shell('grep admin') }}`. The captured stdout is limited to 1 - MiB by default; configure a different budget with - `StdlibConfig::with_command_max_output_bytes`. Exceeding the limit raises an - `InvalidOperation` error that quotes the configured threshold. Templates can - pass an options mapping such as `{'mode': 'tempfile'}` to stream stdout into - a temporary file instead. The file path is returned to the template and - remains bounded by `StdlibConfig::with_command_max_stream_bytes` (default 64 - MiB). +### Time helpers -- `grep(pattern, flags=None)`: Filters input lines matching `pattern`. - `flags` can be a string (e.g., `'-i'`) or list of strings. Implemented via - `shell`. Marks template as impure. The same output and streaming limits apply - when `grep` emits large result sets. +`now(offset=...)` returns the current timestamp, optionally at an offset such as +`"+01:00"`. `timedelta(...)` constructs a duration from keyword components: +weeks, days, hours, minutes, seconds, milliseconds, microseconds, and +nanoseconds. These helpers read the clock or perform duration arithmetic; they +do not schedule work. -#### Executable discovery +### Impure helpers -The `which` filter and function resolve executables using the current `PATH` -without marking the template as impure. `{{ 'clang++' | which }}` returns the -first matching binary; `{{ which('clang++') }}` is available when a function -call is clearer than piping. Missing executables raise -`netsuke::jinja::which::not_found` with a preview of the searched directories. +The following helpers can observe or modify the outside world: -Use `command_available(name, **kwargs)` when absence should select another -manifest-time branch instead of failing the render. It uses the same resolver -and cache as `which`, returns `true` when at least one executable is found, and -returns `false` for absent commands. Invalid arguments still raise -`netsuke::jinja::which::args`. +- `fetch(url, cache=false)` retrieves a URL. HTTPS is the only allowed scheme + by default. +- `value | shell(command, options)` sends a value to a host shell command. +- `value | grep(pattern, flags, options)` filters lines through `grep`. +- `now(offset=...)` reads the clock. +- File-reading and path-canonicalization filters inspect the filesystem. -| Kwarg | Default | Effect on `command_available` | -| ----------- | ------- | ---------------------------------------------------------------------------- | -| `all` | `false` | Accepted for kwarg symmetry with `which`; does not change the bool return. | -| `canonical` | `false` | Canonicalizes discovered paths before deciding whether any match exists. | -| `fresh` | `false` | Bypasses the resolver cache for this lookup and refreshes the cached result. | -| `cwd_mode` | `auto` | Controls current-directory search: `auto`, `always`, or `never`. | +`fetch`, `shell`, and `grep` enforce bounded output. These internal limits are +not currently user-configurable from `Netsukefile`. -When `PATH` is empty and `cwd_mode="auto"`, Netsuke can still discover a -project-local executable through the bounded workspace fallback: +Use impure helpers only in trusted manifests. Netsuke does not sandbox them. -```yaml -actions: - - name: lint - command: ./tools/project-lint - when: command_available("project-lint", cwd_mode="auto") - - name: lint - command: cargo clippy --workspace --all-targets --all-features -- -D warnings - when: not command_available("project-lint", cwd_mode="auto") -``` +## Use the command-line interface -The `which` filter remains the right choice when a missing tool should stop the -manifest render. The predicate is for optional toolchains and complementary -branches. +The top-level command shape is: -Use `command_available` in manifest-time `when` clauses when optional tooling -selects between actions: + -```yaml -actions: - - name: test-fast - command: cargo nextest run - when: command_available("cargo-nextest") - - name: test-fast - command: cargo test - when: not command_available("cargo-nextest") +```plaintext +netsuke [OPTIONS] [COMMAND] +netsuke [OPTIONS] build [TARGETS]... ``` -Only the selected action reaches the typed manifest and generated Ninja file. -Top-level actions selected this way still keep the normal implicit -`phony: true` behaviour. +Global options must appear before the subcommand. For example, +`netsuke --color always build` is valid; `netsuke build --color always` is not. -**Impurity:** Filters like `shell` and functions like `fetch` interact with the -outside world. Netsuke tracks this "impurity". Impure templates might affect -caching or reproducibility analysis in future versions. Use impure helpers -judiciously. +The commands are: -### Key Tests +- `build [TARGETS]...`: generate Ninja and build the named targets. With no + targets, use configured defaults and then manifest defaults. +- `clean`: generate a temporary Ninja file and run `ninja -t clean`. +- `graph`: render the build graph as DOT or self-contained HTML without + invoking Ninja. +- `generate`: write Ninja without invoking it. Outside JSON mode, the generated + Ninja manifest is the only content written to stdout; use `--output ` to + write it to a file instead. In JSON mode (`--json`) the manifest is carried in + the result document's `result.content` field instead. -Use tests with the `is` keyword: `{% if path is file %}` +Running `netsuke` without a subcommand is the same as `netsuke build` with no +explicit targets. A bare target such as `netsuke hello` is not accepted; use +`netsuke build hello`. -- `file`, `dir`, `symlink`: Checks filesystem object type (without following - links). +Important global options include: -- `readable`, `writable`, `executable`: Checks permissions for the current - user. +- `-f, --file ` +- `-C, --directory ` +- `--config ` +- `-j, --jobs <1..64>` +- `-v, --verbose` +- `--locale ` +- `--no-input` +- `--json` +- `--color ` +- `--emoji ` +- `--progress ` +- `--accessibility ` +- `--default-target ` -- `absolute`, `relative`: Checks path type. +Run `netsuke --help` or `netsuke --help` for the complete current +surface. -## 8\. Command-Line Interface (CLI) +### Anchor a project with `--directory` -Netsuke's CLI provides commands to manage your build. +`--directory` changes manifest lookup, project configuration discovery and +relative output paths: -```text -netsuke [OPTIONS] [COMMAND] [TARGETS...] + +```sh +netsuke --directory /path/to/project build ``` -### Global Options +An explicit `--config` path remains relative to the shell's original working +directory. -- `-f, --file `: Path to the `Netsukefile` (default: `Netsukefile`). +### Generate and inspect artefacts -- `-C, --directory `: Change to directory `DIR` before doing anything. +These commands cover the non-default utility workflows: -- `-j, --jobs `: Set the number of parallel jobs Ninja should run - (default: Ninja's default). + -- `-v, --verbose`: Enable verbose diagnostic logging and completion timing - summaries. - -- `--locale `: Localize CLI help and error messages (for example - `en-US` or `es-ES`). +```sh +netsuke clean +netsuke graph --output build.dot +netsuke graph --html --output graph.html +netsuke generate +netsuke generate --output build.ninja +``` -### Network Policy Options +`graph` is rendered in-process and does not require Ninja. DOT goes to stdout +unless `--output` is supplied. HTML output contains a server-rendered SVG, a +textual outline and a `