chore(schema): generate the whole schema from a top-level MiseTomlDecl (#14632)

<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/mise/trails/850
<!-- entire-trail-link-end -->

Contributors no longer edit `schema/mise.json` by hand. mise-config now
generates all three published schemas (`schema/mise.json`,
`schema/mise-task.json` and `schema/miserc.json`) from Rust types and
`settings.toml`, and `mise run render:schema` writes them. For users,
the schemas that editors and the JSON Schema Store load are unchanged
apart from key order.

## What changes for contributors

- **The top level is a type.** `MiseTomlDecl`
(`crates/mise-config/src/mise_toml.rs`) declares every top-level
`mise.toml` key with its description, deprecation and schema. A new
top-level key goes in `MiseTomlDecl` and in `MiseToml`. The new
`schema_keys_match_parser` test fails until both read the same keys. It
takes `MiseToml`'s key list from the field names serde passes to
`deserialize_struct`.
- **No more stitching.** `xtasks/render/schema.ts` is gone, along with
its Node dependency (`docs:setup`). Before, it took the hand-written
`mise.json`, swapped in the generated fragments at JSON pointers, built
`[settings]` from `settings.toml`, and crawled the task `$defs` into
`mise-task.json`. All of that now lives in
`mise_config::schema::render`. The task is a two-line
`xtasks/render/schema.sh` that runs the `render-schema` example, then
oxfmt.
- **The hand-written parts sit next to their types.** Some schemas were
still hand-written JSON in `mise.json` because a derive can't describe
them. Those moved into Rust `json_schema!` functions:
- `[hooks]` in `hooks/schema.rs`. The hook declaration types (`Hooks`,
`HookDef`, `HookDefItem`, …) move from mise's `src/hooks.rs` into
`mise_config::hooks`.
  - `[deps]` in `deps/schema.rs`.
  - `[dotfiles]` and `[dotfile_groups]` in `dotfiles/schema.rs`.
  - `[daemons]` and `[daemon_providers]` in `daemons/schema.rs`.
  - The raw `[bootstrap]` values in `bootstrap/schema.rs`.
  - The shared `os_filter` in `schema/shared.rs`.
  - `[secrets]` and the plain keys in `mise_toml.rs`.
- **`[settings]` and `miserc.json`** are built by `schema/settings.rs`,
a Rust port of the old TypeScript `buildElement`.
- A hand-written schema embedded in a derived type is marked so the
generator's transforms skip it, and the marker is stripped before
output.

## Generated output

The generated files are semantically identical to the ones they replace.
This prints nothing:

```bash
for f in mise mise-task miserc; do diff <(git show origin/claude/mise-config-tools:schema/$f.json | jq -S .) <(jq -S . schema/$f.json); done
```

`mise.json`'s text diff is large because `$defs` are now sorted by name,
which keeps each section's definitions together whichever module writes
them. A few keys inside entries also change order. `mise-task.json` and
`miserc.json` are byte-identical.

`MiseToml` still parses files itself. Parsing through `MiseTomlDecl` is
the next PR.

## Validation

- `cargo +1.95 clippy --workspace --all-features --all-targets -- -D
warnings`
- `cargo test -p mise-config`. The freshness test now compares all three
files and reports the first differing JSON pointer. A mutation of each
kind (top-level property, setting, `$defs` entry) was caught at its
pointer.
- `cargo test --all-features -p mise --lib -- config::config_file hooks
schema_` (181 tests). The parity test fails when a key is removed from
the schema.
- `mise run render:schema` is idempotent.

Stacked on #14631.

*AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5;
version: 2.1.299.*

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Contributor and schema-generation workflow only; published schemas are
intended to stay semantically identical, with editor-facing output
limited to key ordering.
> 
> **Overview**
> **mise’s published JSON schemas are now fully generated from Rust**,
so contributors no longer patch `schema/mise.json` or run the old Node
stitcher.
> 
> `mise_config::schema::render` builds **`schema/mise.json`**,
**`schema/mise-task.json`**, and **`schema/miserc.json`** from
**`MiseTomlDecl`** (every top-level `mise.toml` key), derived
`JsonSchema` sections, and **`settings.toml`** via new
**`schema/settings.rs`**. The **`render-schema`** example writes those
files directly; **`xtasks/render/schema.ts`** is removed in favor of a
small **`schema.sh`** (`cargo run … render-schema`, then `oxfmt`).
**`render:schema`** no longer depends on **`docs:setup`**.
> 
> Hand-maintained schema that used to live in JSON is **colocated in
`mise-config`** (`hooks/`, `deps/`, `dotfiles/`, `daemons/`,
`bootstrap/schema.rs`, etc.). **`[hooks]` declaration types** move into
**`mise-config`**; mise keeps runtime hook execution and calls
**`hooks_from_def`**. A **`schema_keys_match_parser`** test ties
**`MiseTomlDecl`** to **`MiseToml`** field names. Freshness tests
compare all three schema files and report the first differing JSON
pointer.
> 
> **AGENTS.md**, architecture, and contributing docs describe the new
workflow (edit Rust, run **`mise run render:schema`**, never edit the
JSON).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c8d29d7f66. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Expanded configuration validation for bootstrap settings, hooks,
dependencies, dotfiles, daemons, and other sections. Schemas now cover
more supported options and better identify invalid values and
combinations.
* Updated schemas for mise configuration, tasks, and rc settings to
provide more accurate validation and suggestions in schema-aware tools.
* **Documentation**
* Clarified which schemas are generated and where to update their source
definitions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jdxandClaude Opus 5.5 authored and GitHub committed 2026-10-17 22:18:23 -05:00
1 parent 2035092ee8
commit 67bafb068e
33 files changed
+5809 -4117

No files matched your search

+1 -1
View File
@@ -261,7 +261,7 @@ If you think you need to pick "the newest installed version" at a new call site,
### Configuration Parsing
The configuration system supports multiple file formats and environment-specific configs. Changes to settings require updating `settings.toml` and running `mise run render:schema`. The `Settings` types are generated from `settings.toml` in `crates/mise-settings`; loading them (config discovery, trust, CLI flags) stays in `src/config/settings.rs`, whose `SettingsExt` trait holds the methods that need the rest of mise.
When changing configuration syntax or tool options, update `schema/mise.json` and run `mise run render:schema`. The render task also regenerates `schema/mise-task.json` from the main schema. Sections whose types live in `crates/mise-config` (so far `[bootstrap]`, `[daemons_settings]`, `[daemon_groups]`, `[doctor]`, `[history]`, `[monorepo]`, `[oci]`, `[task_config]`, `[tasks]`, `[task_templates]`, `[tool_config]` and `[wrappers]`) are generated from those types and their doc comments: edit the Rust, not the JSON, then run `mise run render:schema`. `[env]`, `[vars]`, `[[watch_files]]`, `[tool_alias]` and `min_version` accept shapes a derive cannot describe, so their schema is a hand-written `JsonSchema` in the same crate: `crates/mise-config/src/env/schema.rs` next to the `env.rs` parser, and the impls in `watch_files.rs`, `alias.rs` and `min_version.rs`. Task fields with several accepted shapes take theirs from `crates/mise-config/src/task/schema.rs`. Change it together with the parser, then run `mise run render:schema`. `[tools]` picks per-backend options by key pattern, so its schema is hand-written in `crates/mise-config/src/tools/schema.rs`; change it together with `ToolVersionOptions::insert_option` and the backend's option reader, then run `mise run render:schema`. A field that stays raw TOML keeps a hand-written `$defs` entry that the generated part references.
`schema/mise.json`, `schema/mise-task.json` and `schema/miserc.json` are generated from `crates/mise-config` and `settings.toml`: never edit the JSON. Change the Rust, then run `mise run render:schema`; a `mise-config` test fails while the committed files are stale. The top-level keys are the fields of `MiseTomlDecl` in `crates/mise-config/src/mise_toml.rs`, and the `schema_keys_match_parser` test fails until `MiseToml` reads the same keys, so a new key goes in both. Sections whose types derive `JsonSchema` (so far `[bootstrap]`, `[daemons_settings]`, `[daemon_groups]`, `[doctor]`, `[history]`, `[monorepo]`, `[oci]`, `[task_config]`, `[tasks]`, `[task_templates]`, `[tool_config]` and `[wrappers]`) are generated from those types and their doc comments. `[env]`, `[vars]`, `[[watch_files]]`, `[tool_alias]` and `min_version` accept shapes a derive cannot describe, so their schema is a hand-written `JsonSchema` in the same crate: `crates/mise-config/src/env/schema.rs` next to the `env.rs` parser, and the impls in `watch_files.rs`, `alias.rs` and `min_version.rs`. Task fields with several accepted shapes take theirs from `crates/mise-config/src/task/schema.rs`. Change it together with the parser. `[tools]` picks per-backend options by key pattern, so its schema is hand-written in `crates/mise-config/src/tools/schema.rs`; change it together with `ToolVersionOptions::insert_option` and the backend's option reader. `[hooks]` and `[deps]` are written by hand next to their types (`hooks/schema.rs`, `deps/schema.rs`), and so are the values that stay raw TOML so newer shapes warn instead of failing: `[dotfiles]`, `[dotfile_groups]`, `[daemons]`, `[daemon_providers]`, `[secrets]` and parts of `[bootstrap]` (`dotfiles/schema.rs`, `daemons/schema.rs`, `mise_toml.rs`, `bootstrap/schema.rs`). `[settings]` and `miserc.json` come from `settings.toml` through `crates/mise-config/src/schema/settings.rs`.
The `schema_matches_parser` unit test (`src/config/config_file/mise_toml/schema_conformance.rs`) validates every `toml` block in `docs/` and every mise config an e2e heredoc writes against both the schema and `MiseToml`'s parser, and fails when they disagree. Fix the schema rather than the corpus. Mark a config that mise refuses on purpose with `# schema: invalid` (e2e) or `<!-- schema: invalid -->` (docs) on the line above it, and an excerpt that is not a whole config with `schema: skip`. List deliberate differences in its `GAPS` with a reason.
+10 -7
View File
@@ -1,11 +1,14 @@
//! Prints the generated schema fragments as one JSON object, keyed by JSON pointer.
//! Writes `schema/mise.json`, `schema/mise-task.json` and `schema/miserc.json`.
//!
//! `mise run render:schema` runs this and writes each fragment into `schema/mise.json`.
//! `mise run render:schema` runs this, then formats the files.
use std::path::Path;
fn main() {
let fragments: serde_json::Map<String, serde_json::Value> = mise_config::schema::fragments()
.into_iter()
.map(|(pointer, schema)| (pointer.to_string(), schema))
.collect();
println!("{}", serde_json::to_string_pretty(&fragments).unwrap());
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let settings = std::fs::read_to_string(root.join("settings.toml")).unwrap();
for (path, schema) in mise_config::schema::render(&settings) {
let json = serde_json::to_string_pretty(&schema).unwrap();
std::fs::write(root.join(path), json + "\n").unwrap();
}
}
+2
View File
@@ -1,3 +1,5 @@
pub(crate) mod schema;
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::Deserialize;
+369
View File
@@ -0,0 +1,369 @@
//! The JSON schema for the `[bootstrap]` fields that stay raw TOML, written by hand.
//!
//! Hooks, shell activation, the macOS defaults domains and the apt and dnf repositories are kept
//! as TOML values so a shape from a newer mise warns instead of failing the whole file, so there
//! is no Rust type to derive them from. The derived `[bootstrap]` schema points at these through
//! [`refs`](crate::schema::refs). Change them together with the code that reads those values, then
//! run `mise run render:schema`.
use schemars::{Schema, json_schema};
/// Each definition, keyed by its name under `$defs` in `schema/mise.json`.
pub(crate) fn defs() -> Vec<(&'static str, Schema)> {
vec![
("bootstrap_file_phase", bootstrap_file_phase()),
("bootstrap_hook", bootstrap_hook()),
("bootstrap_shell_activation", bootstrap_shell_activation()),
("bootstrap_apt_sources", bootstrap_apt_sources()),
("bootstrap_dnf_repos", bootstrap_dnf_repos()),
("bootstrap_hooks", bootstrap_hooks()),
("bootstrap_macos_defaults", bootstrap_macos_defaults()),
("bootstrap_macos_dock", bootstrap_macos_dock()),
("bootstrap_macos_finder", bootstrap_macos_finder()),
("bootstrap_macos_keyboard", bootstrap_macos_keyboard()),
("bootstrap_macos_trackpad", bootstrap_macos_trackpad()),
(
"bootstrap_mise_shell_activate",
bootstrap_mise_shell_activate(),
),
("macos_defaults_value", macos_defaults_value()),
]
}
fn bootstrap_file_phase() -> Schema {
json_schema!({
"type": "string",
"enum": ["pre-packages", "post-packages"],
"default": "post-packages",
"description": "When to apply the managed path during bootstrap; pre-packages runs before the pre-packages hook"
})
}
fn bootstrap_hook() -> Schema {
json_schema!({
"description": "command or commands to run for a bootstrap phase",
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}},
{
"type": "object",
"properties": {
"run": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
}
},
"required": ["run"],
"additionalProperties": true
}
]
})
}
fn bootstrap_shell_activation() -> Schema {
json_schema!({
"oneOf": [
{
"type": "boolean",
"description": "enable the target with its default mode, or disable it"
},
{
"type": "string",
"enum": ["activate", "shims", "auto"],
"description": "enable the target with the selected activation mode"
},
{
"type": "object",
"description": "shell activation settings for the target",
"properties": {
"enabled": {
"type": "boolean",
"description": "whether to manage the activation block"
},
"mode": {
"type": "string",
"enum": ["activate", "shims", "auto"],
"description": "activation mode; defaults according to the target"
}
},
"required": ["enabled"],
"additionalProperties": false
}
]
})
}
fn bootstrap_apt_sources() -> Schema {
json_schema!({
"type": "object",
"description": "apt repositories added before apt packages are installed, keyed by a name used in the files mise writes",
"propertyNames": {"pattern": "^[A-Za-z0-9_-][A-Za-z0-9_.-]*$"},
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"properties": {
"ppa": {
"type": "string",
"description": "Launchpad PPA as owner/archive, such as git-core/ppa; added on Ubuntu and Ubuntu-based releases only"
},
"uris": {"type": "string", "description": "repository URL(s), space-separated"},
"suites": {
"type": "string",
"description": "suite(s), space-separated; defaults to the release codename"
},
"components": {
"type": "string",
"description": "component(s), space-separated; defaults to main, or none when suites is a path ending in /"
},
"architectures": {
"type": "string",
"description": "architecture(s) to fetch, space-separated"
},
"key": {
"type": "string",
"description": "http(s) URL of the repository's signing key, armored or binary"
},
"fingerprint": {
"description": "full primary key fingerprint(s) the signing key must have; required with key",
"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]
}
},
"oneOf": [
{
"required": ["ppa"],
"properties": {"fingerprint": {"type": "string"}},
"not": {
"anyOf": [
{"required": ["uris"]},
{"required": ["suites"]},
{"required": ["components"]},
{"required": ["architectures"]},
{"required": ["key"]}
]
}
},
{"required": ["uris", "key", "fingerprint"], "not": {"required": ["ppa"]}}
]
}
})
}
fn bootstrap_dnf_repos() -> Schema {
json_schema!({
"type": "object",
"description": "dnf repositories enabled before dnf packages are installed, keyed by a name for the entry",
"propertyNames": {"pattern": "^[A-Za-z0-9_-][A-Za-z0-9_.-]*$"},
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"required": ["copr"],
"properties": {
"copr": {
"type": "string",
"description": "COPR project as owner/project or @group/project, such as scottames/ghostty; enabled as dnf copr enable would"
}
}
}
})
}
fn bootstrap_hooks() -> Schema {
json_schema!({
"type": "object",
"description": "commands to run before and after bootstrap phases",
"properties": {
"pre-packages": {"$ref": "#/$defs/bootstrap_hook"},
"pre_packages": {"$ref": "#/$defs/bootstrap_hook"},
"post-packages": {"$ref": "#/$defs/bootstrap_hook"},
"post_packages": {"$ref": "#/$defs/bootstrap_hook"},
"pre-repos": {"$ref": "#/$defs/bootstrap_hook"},
"pre_repos": {"$ref": "#/$defs/bootstrap_hook"},
"post-repos": {"$ref": "#/$defs/bootstrap_hook"},
"post_repos": {"$ref": "#/$defs/bootstrap_hook"},
"pre-dotfiles": {"$ref": "#/$defs/bootstrap_hook"},
"pre_dotfiles": {"$ref": "#/$defs/bootstrap_hook"},
"post-dotfiles": {"$ref": "#/$defs/bootstrap_hook"},
"post_dotfiles": {"$ref": "#/$defs/bootstrap_hook"},
"pre-defaults": {"$ref": "#/$defs/bootstrap_hook"},
"pre_defaults": {"$ref": "#/$defs/bootstrap_hook"},
"post-defaults": {"$ref": "#/$defs/bootstrap_hook"},
"post_defaults": {"$ref": "#/$defs/bootstrap_hook"},
"pre-user": {"$ref": "#/$defs/bootstrap_hook"},
"pre_user": {"$ref": "#/$defs/bootstrap_hook"},
"post-user": {"$ref": "#/$defs/bootstrap_hook"},
"post_user": {"$ref": "#/$defs/bootstrap_hook"},
"pre-tools": {"$ref": "#/$defs/bootstrap_hook"},
"pre_tools": {"$ref": "#/$defs/bootstrap_hook"},
"post-tools": {"$ref": "#/$defs/bootstrap_hook"},
"post_tools": {"$ref": "#/$defs/bootstrap_hook"},
"final": {"$ref": "#/$defs/bootstrap_hook"}
},
"additionalProperties": false
})
}
fn bootstrap_macos_defaults() -> Schema {
json_schema!({
"type": "object",
"description": "macOS user defaults to apply with `mise bootstrap macos defaults apply`, keyed by preferences domain (e.g. \"com.apple.dock\", \"NSGlobalDomain\")",
"additionalProperties": {
"type": "object",
"description": "defaults keys and their desired values for this domain",
"additionalProperties": {
"$ref": "#/$defs/macos_defaults_value",
"description": "desired value, written with the matching `defaults write` type; arrays and tables become property list arrays and dictionaries"
}
}
})
}
fn bootstrap_macos_dock() -> Schema {
json_schema!({
"type": "object",
"description": "curated Dock preferences that compile into macOS defaults",
"properties": {
"apps": {
"type": "array",
"items": {"type": "string", "pattern": "^(?:/|~/(?!/)).+\\.[aA][pP][pP]/*$"},
"uniqueItems": true,
"description": "Pinned application paths in order; absolute or ~/ paths ending in .app. An empty array removes application tiles."
},
"autohide_delay": {"type": "number", "description": "delay before showing the Dock"},
"autohide_time_modifier": {
"type": "number",
"description": "Dock autohide animation time modifier"
},
"autohide": {"type": "boolean", "description": "hide and show the Dock automatically"},
"orientation": {
"type": "string",
"enum": ["bottom", "left", "right"],
"description": "Dock screen edge"
},
"tilesize": {"type": "integer", "description": "Dock icon size"},
"magnification": {"type": "boolean", "description": "enable Dock magnification"},
"largesize": {"type": "integer", "description": "Dock magnified icon size"},
"show_recents": {
"type": "boolean",
"description": "show recent applications in the Dock"
},
"mru_spaces": {
"type": "boolean",
"description": "automatically rearrange Spaces based on most recent use"
}
},
"additionalProperties": false
})
}
fn bootstrap_macos_finder() -> Schema {
json_schema!({
"type": "object",
"description": "curated Finder preferences that compile into macOS defaults",
"properties": {
"sort_folders_first": {
"type": "boolean",
"description": "keep folders first when sorting by name"
},
"save_new_documents_to_cloud": {
"type": "boolean",
"description": "save new documents to iCloud by default (applies globally to application save dialogs)"
},
"show_all_files": {"type": "boolean", "description": "show hidden files in Finder"},
"show_pathbar": {"type": "boolean", "description": "show the Finder path bar"},
"show_status_bar": {"type": "boolean", "description": "show the Finder status bar"},
"show_extensions_warning": {
"type": "boolean",
"description": "show the warning when changing file extensions"
},
"preferred_view_style": {
"type": "string",
"enum": ["icon", "list", "column", "gallery"],
"description": "Finder preferred view style"
}
},
"additionalProperties": false
})
}
fn bootstrap_macos_keyboard() -> Schema {
json_schema!({
"type": "object",
"description": "curated keyboard preferences that compile into macOS defaults",
"properties": {
"automatic_capitalization": {
"type": "boolean",
"description": "automatically capitalize words"
},
"automatic_spelling_correction": {
"type": "boolean",
"description": "automatically correct spelling"
},
"key_repeat": {"type": "integer", "description": "keyboard repeat interval"},
"initial_key_repeat": {
"type": "integer",
"description": "delay before key repeat starts"
},
"press_and_hold": {
"type": "boolean",
"description": "enable press-and-hold accent picker"
},
"fn_state": {
"type": "boolean",
"description": "use F1, F2, etc. as standard function keys"
}
},
"additionalProperties": false
})
}
fn bootstrap_macos_trackpad() -> Schema {
json_schema!({
"type": "object",
"description": "curated trackpad preferences that compile into macOS defaults",
"properties": {
"tap_to_click": {
"type": "boolean",
"description": "enable tap to click for built-in and Bluetooth trackpads"
},
"three_finger_drag": {
"type": "boolean",
"description": "enable three finger drag for built-in and Bluetooth trackpads"
}
},
"additionalProperties": false
})
}
fn bootstrap_mise_shell_activate() -> Schema {
json_schema!({
"type": "object",
"description": "declarative mise shell activation snippets, applied with `mise bootstrap mise-shell-activate apply`",
"properties": {
"bash": {"$ref": "#/$defs/bootstrap_shell_activation"},
"zsh": {"$ref": "#/$defs/bootstrap_shell_activation"},
"fish": {"$ref": "#/$defs/bootstrap_shell_activation"},
"bash_profile": {"$ref": "#/$defs/bootstrap_shell_activation"},
"bashrc": {"$ref": "#/$defs/bootstrap_shell_activation"},
"zshenv": {"$ref": "#/$defs/bootstrap_shell_activation"},
"zprofile": {"$ref": "#/$defs/bootstrap_shell_activation"},
"zshrc": {"$ref": "#/$defs/bootstrap_shell_activation"}
},
"additionalProperties": false
})
}
fn macos_defaults_value() -> Schema {
json_schema!({
"description": "a property list value: boolean, integer, real, string, or an array or dictionary of these",
"anyOf": [
{"type": "boolean"},
{"type": "integer"},
{"type": "number"},
{"type": "string"},
{"type": "array", "items": {"$ref": "#/$defs/macos_defaults_value"}},
{"type": "object", "additionalProperties": {"$ref": "#/$defs/macos_defaults_value"}}
]
})
}
+3 -1
View File
@@ -1,5 +1,7 @@
//! `[daemons]`, `[daemons_settings]` and `[daemon_groups]`.
pub(crate) mod schema;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -7,7 +9,7 @@ use serde::Deserialize;
// to users. Notes for Rust readers go in plain comments.
// A `[daemons]` entry. A definition stays a raw table that mise-daemons checks when it builds the
// daemon, so its schema is still written by hand in schema/mise.json.
// daemon, so its schema is written by hand in `schema.rs`.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum Declaration {
+261
View File
@@ -0,0 +1,261 @@
//! The JSON schema for `[daemons]` and `[daemon_providers]`, written by hand.
//!
//! A daemon definition and a provider stay raw tables that mise-daemons checks when it builds the
//! daemon, so there is no Rust type to derive them from. Change them together with mise-daemons,
//! then run `mise run render:schema`.
use schemars::{Schema, json_schema};
/// Each definition, keyed by its name under `$defs` in `schema/mise.json`.
pub(crate) fn defs() -> Vec<(&'static str, Schema)> {
vec![
("daemon_port", daemon_port()),
("daemon_proxy", daemon_proxy()),
("daemon_proxy_tls", daemon_proxy_tls()),
("daemon_proxy_idle_timeout", daemon_proxy_idle_timeout()),
("daemon_init", daemon_init()),
]
}
fn daemon_port() -> Schema {
json_schema!({
"description": "A fixed port, or \"auto\" to derive one from the project root so the primary checkout keeps the base port and each linked git worktree gets its own.",
"oneOf": [
{"type": "integer", "minimum": 1, "maximum": 65535},
{
"type": "string",
"const": "auto",
"description": "Offset the preset's default port by this project root's worktree slot."
},
{
"type": "object",
"required": ["auto"],
"properties": {
"auto": {"const": true, "description": "Enable per-worktree port allocation."},
"base": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Port used by the primary checkout. Defaults to the preset's port; required for custom daemons."
},
"stride": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"description": "Distance between consecutive worktree slots. Defaults to 1."
}
},
"additionalProperties": false
}
]
})
}
fn daemon_proxy() -> Schema {
json_schema!({
"description": "Hostname label the reverse proxy routes to this daemon, false to opt out, or true to use the daemon's name. Only a daemon with a port is routed. The hostname is <proxy>.<worktree>.<project>.<tld> in a linked git worktree and <proxy>.<project>.<tld> in the primary checkout.",
"oneOf": [
{"type": "string", "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", "maxLength": 63},
{
"type": "boolean",
"description": "false opts out of the reverse proxy; true uses the daemon's name, which re-enables routing for a preset that opts out."
}
]
})
}
fn daemon_proxy_tls() -> Schema {
json_schema!({
"description": "Whether the proxy terminates TLS for this daemon or passes the connection through to it.",
"type": "string",
"enum": ["terminate", "passthrough"]
})
}
fn daemon_proxy_idle_timeout() -> Schema {
json_schema!({
"description": "Stop this daemon after this long without proxy traffic when the proxy started it, for example \"30m\". false exempts it from idle shutdown even when pitchfork's proxy.idle_timeout is set; \"0\" does the same. Idle shutdown is off unless this or pitchfork's proxy.idle_timeout is set, and a daemon started with `mise daemons start` is never stopped for inactivity. Requires pitchfork 2.27.0.",
"oneOf": [{"type": "string", "pattern": "^\\s*[0-9]"}, {"type": "boolean", "enum": [false]}]
})
}
fn daemon_init() -> Schema {
json_schema!({
"description": "idempotent setup commands run before the daemon's long-running process",
"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]
})
}
pub(crate) fn daemons() -> Schema {
json_schema!({
"type": "object",
"additionalProperties": {
"oneOf": [
{
"type": "string",
"description": "Version request for a preset matching the daemon name."
},
{
"type": "object",
"required": ["run"],
"not": {
"anyOf": [
{"required": ["preset"]},
{"required": ["project"]},
{"required": ["task"]},
{"required": ["args"]},
{"required": ["provider"]},
{"required": ["resource"]}
]
},
"properties": {
"run": {"type": "string"},
"init": {"$ref": "#/$defs/daemon_init"},
"mise": {"type": "boolean"},
"port": {
"oneOf": [
{"$ref": "#/$defs/daemon_port"},
{
"type": "object",
"description": "Pitchfork's structured port configuration, forwarded verbatim.",
"not": {"required": ["auto"]}
}
]
},
"proxy": {"$ref": "#/$defs/daemon_proxy"},
"proxy_tls": {"$ref": "#/$defs/daemon_proxy_tls"},
"proxy_idle_timeout": {"$ref": "#/$defs/daemon_proxy_idle_timeout"}
},
"additionalProperties": true
},
{
"type": "object",
"required": ["task"],
"not": {
"anyOf": [
{"required": ["run"]},
{"required": ["preset"]},
{"required": ["project"]},
{"required": ["provider"]},
{"required": ["resource"]}
]
},
"properties": {
"task": {
"description": "mise task run as this daemon's long-running process",
"type": "string"
},
"args": {
"description": "arguments passed to the task after `--`",
"type": "array",
"items": {"type": "string"}
},
"init": {"$ref": "#/$defs/daemon_init"},
"proxy": {"$ref": "#/$defs/daemon_proxy"},
"proxy_tls": {"$ref": "#/$defs/daemon_proxy_tls"},
"proxy_idle_timeout": {"$ref": "#/$defs/daemon_proxy_idle_timeout"}
},
"additionalProperties": true
},
{
"type": "object",
"required": ["preset", "version"],
"not": {
"anyOf": [
{"required": ["project"]},
{"required": ["task"]},
{"required": ["args"]},
{"required": ["provider"]},
{"required": ["resource"]}
]
},
"properties": {
"data_dir": {
"type": "string",
"minLength": 1,
"description": "Persistent preset data directory, relative to the declaring project root or absolute. Defaults to mise state storage."
},
"preset": {
"type": "string",
"enum": ["cockroachdb", "nats", "postgres", "redis", "spicedb"]
},
"version": {"type": "string"},
"port": {"$ref": "#/$defs/daemon_port"},
"ports": {
"type": "object",
"description": "Overrides for the preset's additional named ports.",
"additionalProperties": {
"type": "integer",
"minimum": 1,
"maximum": 65535
}
},
"options": {"type": "object"},
"run": {"type": "string"},
"init": {"$ref": "#/$defs/daemon_init"},
"proxy": {"$ref": "#/$defs/daemon_proxy"},
"proxy_tls": {"$ref": "#/$defs/daemon_proxy_tls"},
"proxy_idle_timeout": {"$ref": "#/$defs/daemon_proxy_idle_timeout"}
},
"additionalProperties": true
},
{
"type": "object",
"description": "Daemon declared by another project, run under that project's namespace and root.",
"required": ["project"],
"properties": {
"project": {
"type": "string",
"description": "Directory of a project whose mise configuration declares the daemon. A relative path resolves against this project root."
},
"name": {
"type": "string",
"description": "Daemon name inside the referenced project, when it differs from this key."
}
},
"additionalProperties": false
},
{
"type": "object",
"required": ["provider"],
"additionalProperties": false,
"properties": {
"provider": {
"type": "string",
"description": "Name of a globally defined shared server provider."
},
"resource": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{0,62}$",
"description": "Database or NATS account shared by consumers choosing this name. Omit for a checkout-specific resource."
}
}
}
]
}
})
}
pub(crate) fn daemon_providers() -> Schema {
json_schema!({
"type": "object",
"propertyNames": {"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"},
"additionalProperties": {
"type": "object",
"required": ["preset", "version"],
"additionalProperties": false,
"properties": {
"preset": {"enum": ["postgres", "cockroachdb", "nats"]},
"version": {"type": "string"},
"port": {"$ref": "#/$defs/daemon_port"},
"ports": {
"type": "object",
"additionalProperties": {"type": "integer", "minimum": 1, "maximum": 65535}
},
"options": {"type": "object"},
"data_dir": {"type": "string", "minLength": 1},
"tool": {"type": "string"}
}
}
})
}
+2
View File
@@ -1,5 +1,7 @@
//! `[deps]`: the providers that bring a project's dependencies up to date.
pub(crate) mod schema;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
+83
View File
@@ -0,0 +1,83 @@
//! The JSON schema for `[deps]`, written by hand next to its declaration types.
//!
//! [`DepsDecl`](super::DepsDecl) flattens every provider into one map, but the schema lists the
//! built-in providers by name so an editor can offer them. Change it together with the types in
//! `super` and mise's built-in providers, then run `mise run render:schema`.
use schemars::{Schema, json_schema};
/// Each definition, keyed by its name under `$defs` in `schema/mise.json`.
pub(crate) fn defs() -> Vec<(&'static str, Schema)> {
vec![("deps_provider", deps_provider())]
}
pub(crate) fn deps() -> Schema {
json_schema!({
"type": "object",
"properties": {
"disable": {
"type": "array",
"items": {"type": "string"},
"description": "Disable specific deps providers"
},
"npm": {"$ref": "#/$defs/deps_provider"},
"yarn": {"$ref": "#/$defs/deps_provider"},
"pnpm": {"$ref": "#/$defs/deps_provider"},
"bun": {"$ref": "#/$defs/deps_provider"},
"aube": {"$ref": "#/$defs/deps_provider"},
"deno": {"$ref": "#/$defs/deps_provider"},
"go": {"$ref": "#/$defs/deps_provider"},
"pip": {"$ref": "#/$defs/deps_provider"},
"poetry": {"$ref": "#/$defs/deps_provider"},
"uv": {"$ref": "#/$defs/deps_provider"},
"bundler": {"$ref": "#/$defs/deps_provider"},
"composer": {"$ref": "#/$defs/deps_provider"},
"dart": {"$ref": "#/$defs/deps_provider"},
"flutter": {"$ref": "#/$defs/deps_provider"},
"git-submodule": {"$ref": "#/$defs/deps_provider"}
},
"additionalProperties": {"$ref": "#/$defs/deps_provider"}
})
}
fn deps_provider() -> Schema {
json_schema!({
"type": "object",
"properties": {
"auto": {
"type": "boolean",
"default": false,
"description": "Auto-run before `mise x` and `mise run`"
},
"sources": {
"type": "array",
"items": {"type": "string"},
"description": "Files/patterns to check for changes"
},
"outputs": {
"type": "array",
"items": {"type": "string"},
"description": "Files/directories that should be newer than sources"
},
"run": {"type": "string", "description": "Command to run when stale"},
"env": {
"type": "object",
"additionalProperties": {"type": "string"},
"description": "Environment variables to set"
},
"dir": {"type": "string", "description": "Working directory for the command"},
"description": {"type": "string", "description": "Description shown in output"},
"depends": {
"type": "array",
"items": {"type": "string"},
"description": "Other deps providers that must complete before this one runs"
},
"timeout": {
"type": "string",
"description": "Timeout for the run command (e.g., \"30s\", \"5m\", \"1h\")"
}
},
"unevaluatedProperties": false,
"description": "Prepare provider configuration"
})
}
+2
View File
@@ -1,3 +1,5 @@
pub(crate) mod schema;
use indexmap::IndexMap;
use serde::Deserialize;
+405
View File
@@ -0,0 +1,405 @@
//! The JSON schema for `[dotfiles]` and `[dotfile_groups]`, written by hand.
//!
//! Each entry stays a TOML value until mise expands it, so a malformed one warns and is skipped
//! instead of failing the whole file, and there is no Rust type to derive the schema from. Change
//! it together with mise's dotfiles expansion, then run `mise run render:schema`.
use schemars::{Schema, json_schema};
pub(crate) fn dotfiles() -> Schema {
json_schema!({
"type": "object",
"additionalProperties": {
"oneOf": [
{"type": "string", "description": "source path for a whole-file dotfile entry"},
entry()
]
}
})
}
/// A table entry: a whole file, or an edit to part of one.
fn entry() -> Schema {
json_schema!({
"type": "object",
"description": "whole-file dotfile entry or file edit entry",
"properties": {
"encrypt": {
"type": "boolean",
"default": false,
"description": "encrypt tracked file contents before storing them in Git; the live file remains plaintext"
},
"allow_plaintext": {
"type": "boolean",
"default": false,
"description": "allow this directly tracked credential-named file to be saved in plaintext history and any connected origin"
},
"source": {"type": "string", "description": "source file or directory"},
"content": {
"type": "string",
"description": "literal inline file content; alternative to source"
},
"mode": {
"type": "string",
"description": "how to apply a whole-file source to the target; omitted uses dotfiles.default_mode. \"absent\" removes a file or symlink at the target and takes no source. \"track\" saves the file's history where it is, and \"track-local\" saves it in this machine's local-only history, which is never shared",
"anyOf": [
{
"enum": [
"symlink",
"symlink-each",
"copy",
"template",
"absent",
"track",
"track-local"
]
},
{"type": "string"}
]
},
"exclude": {
"type": "array",
"description": "glob patterns to skip when the mode walks a directory (symlink-each, copy with a directory source, or a tracked directory, where they are relative to the tracked path); a pattern without \"/\" matches any path component, one with \"/\" is anchored to the root",
"items": {"type": "string"}
},
"include": {
"type": "array",
"description": "history only (mode = \"track\"): the only paths of the tracked directory that are captured, relative to it and matched like \"exclude\"; without it the whole tree is captured with the builtin credential filtering, and an explicit \"exclude\" still wins. A selected file is captured even when it looks like a credential store, in plaintext unless the entry sets encrypt",
"items": {"type": "string"}
},
"manifest": {
"type": "string",
"description": "source manifest used to select managed files from a directory",
"enum": ["git"]
},
"remove_empty": {
"type": "boolean",
"default": false,
"description": "template mode only: remove the target when the template renders empty or whitespace-only; a target that changed since mise last wrote it needs --force"
},
"dot_prefix": {
"type": "boolean",
"default": false,
"description": "symlink-each and directory copy only: deploy each source path component named \"dot-<name>\" as \".<name>\", like GNU Stow's --dotfiles; exclude still matches source names"
},
"relative": {
"type": "boolean",
"description": "symlink and symlink-each only: link by a path relative to the link's directory instead of an absolute one; overrides the dotfiles.relative_symlinks setting (ignored on Windows)"
},
"permissions": {
"type": "string",
"description": "octal permissions for the target, such as \"0600\"; overrides what copy, template, and inline content entries set, and on its own manages only the permissions of an existing target (Unix only)",
"pattern": "^(0o)?[0-7]{1,4}$"
},
"block": {
"type": "string",
"description": "block content, maintained between marker comments"
},
"template": {
"type": "string",
"description": "template engine to render block content with; currently only \"tera\"",
"anyOf": [{"enum": ["tera"]}, {"type": "string"}]
},
"line": {"type": "string", "description": "exact line to ensure exists"},
"position": {
"type": "string",
"default": "append",
"description": "where to insert a missing line",
"anyOf": [{"enum": ["append", "prepend"]}, {"type": "string"}]
},
"comment": {
"type": "string",
"description": "comment prefix for edit marker lines; inferred from the file extension when omitted"
},
"merge": {
"oneOf": [{"type": "boolean"}, {"const": "missing"}],
"description": "edit entry: true sets the keys of source (default: the target's path under dotfiles.root) on a JSON, TOML, or YAML target and leaves the target's other keys to the application that also writes it; \"missing\" sets only the keys the target has no value for; the format comes from the target's extension"
},
"variants": {
"type": "array",
"description": "OS/profile variants: history streams for track mode, or optional destinations for deployment modes",
"items": variant()
}
},
"allOf": [
{
"if": {"required": ["merge"], "properties": {"merge": {"enum": [true, "missing"]}}},
"then": {
"properties": {
"mode": false,
"content": false,
"permissions": false,
"exclude": false,
"include": false,
"manifest": false,
"autosave": false,
"encrypt": false,
"allow_plaintext": false,
"variants": false,
"enabled": false,
"remove_empty": false,
"relative": false,
"dot_prefix": false,
"group": false,
"block": false,
"line": false,
"position": false,
"comment": false
}
}
},
{
"anyOf": [
{"properties": {"relative": {"const": false}}},
{
"required": ["mode"],
"properties": {"mode": {"enum": ["symlink", "symlink-each"]}}
},
{
"properties": {
"mode": false,
"content": false,
"permissions": false,
"block": false,
"line": false
}
}
]
},
{
"anyOf": [
{"properties": {"remove_empty": {"const": false}}},
{
"properties": {
"mode": {"const": "template"},
"content": false,
"block": false,
"line": false
},
"anyOf": [
{"required": ["source"]},
{"required": ["mode"]},
{"properties": {"permissions": false}}
]
}
],
"if": {"required": ["mode"], "properties": {"mode": {"const": "absent"}}},
"then": {
"properties": {
"source": false,
"content": false,
"exclude": false,
"manifest": false,
"permissions": false,
"remove_empty": false,
"dot_prefix": {"const": false},
"encrypt": {"const": false},
"block": false,
"line": false,
"position": false,
"template": false,
"comment": false
}
}
},
{
"oneOf": [
{
"required": ["manifest"],
"properties": {
"source": {},
"content": false,
"permissions": false,
"mode": {"enum": ["copy", "symlink-each"]},
"exclude": {},
"manifest": {},
"block": false,
"line": false,
"template": false,
"comment": false
}
},
{"properties": {"manifest": false}}
]
},
{
"if": {"required": ["dot_prefix"], "properties": {"dot_prefix": {"const": true}}},
"then": {
"properties": {
"content": false,
"mode": {"enum": ["copy", "symlink-each"]},
"block": false,
"line": false,
"template": false,
"comment": false
},
"anyOf": [
{"required": ["source"]},
{"required": ["mode"]},
{"properties": {"permissions": false}}
]
}
},
{
"oneOf": [
{
"required": ["permissions"],
"properties": {
"mode": {"enum": ["copy", "template"]},
"block": false,
"template": false,
"comment": false
},
"anyOf": [
{"required": ["source"]},
{"required": ["content"]},
{"required": ["mode"]},
{"properties": {"exclude": false, "manifest": false, "encrypt": false}}
]
},
{"properties": {"permissions": false}}
]
},
{
"oneOf": [
{
"required": ["content"],
"properties": {
"source": false,
"manifest": false,
"mode": false,
"exclude": false,
"block": false,
"line": false,
"position": false,
"template": false,
"comment": false
}
},
{
"required": ["line"],
"properties": {
"content": false,
"permissions": false,
"mode": false,
"exclude": false,
"source": false,
"block": false,
"template": false,
"comment": false
}
},
{"properties": {"content": false, "line": false, "position": false}}
]
},
{
"oneOf": [
{
"required": ["include", "mode"],
"properties": {"mode": {"enum": ["track", "track-local"]}}
},
{"properties": {"include": false}}
]
},
{
"oneOf": [
{
"required": ["allow_plaintext", "mode"],
"properties": {"mode": {"enum": ["track", "track-local"]}}
},
{"properties": {"allow_plaintext": false}}
]
}
],
"anyOf": [
{"properties": {"encrypt": {"const": false}}},
{"properties": {"content": false, "block": false, "line": false, "template": false}}
]
})
}
/// One OS or profile variant of an entry.
fn variant() -> Schema {
json_schema!({
"type": "object",
"additionalProperties": false,
"properties": {
"os": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]},
"profile": {
"type": "string",
"description": "mise environment selected with -E or MISE_ENV"
},
"default": {"type": "boolean"},
"machine": {
"type": "boolean",
"description": "track mode only: a separate stream on every machine, never applied on another machine; must be the only variant and cannot be combined with encrypt"
},
"target": {
"type": "string",
"description": "deployment destination override; requires either an explicit source or a safe relative entry key when every variant has a target, and is not supported in track mode"
}
}
})
}
pub(crate) fn dotfile_groups() -> Schema {
json_schema!({
"type": "object",
"description": "named directory trees of dotfiles, applied with `mise dotfiles apply` or `mise bootstrap` while [bootstrap] dotfile_groups selects them (or is unset)",
"propertyNames": {"pattern": "^[A-Za-z0-9_.-]+$"},
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"required": ["root"],
"properties": {
"root": {
"type": "string",
"description": "the directory tree the group deploys; a relative path resolves against dotfiles.root"
},
"target": {
"type": "string",
"description": "target directory, absolute or starting with ~/",
"default": "~"
},
"mode": {
"type": "string",
"description": "how the tree is deployed",
"enum": ["symlink-each", "copy", "symlink"],
"default": "symlink-each"
},
"exclude": {
"type": "array",
"description": "glob patterns of source paths to skip, as for [dotfiles] exclude",
"items": {"type": "string"}
},
"dot_prefix": {
"type": "boolean",
"default": false,
"description": "deploy each source path component named \"dot-<name>\" as \".<name>\", like GNU Stow's --dotfiles"
},
"manifest": {
"type": "string",
"description": "source manifest used to select managed files from the tree",
"enum": ["git"]
},
"relative": {
"type": "boolean",
"description": "link by a path relative to the link's directory; overrides the dotfiles.relative_symlinks setting (ignored on Windows)"
},
"entries": {
"type": "object",
"description": "whole-file entries for parts of the tree, keyed by target path inside the group's target and written like [dotfiles] entries; each is cut out of the walk. A missing source is found under the root at the entry's path inside the target, and a relative source starts at the root",
"additionalProperties": {
"oneOf": [
{
"type": "string",
"description": "source path, relative to the group root"
},
{"type": "object", "description": "whole-file entry, as in [dotfiles]"}
]
}
}
}
}
})
}
+1 -1
View File
@@ -12,7 +12,7 @@ use serde::de::Visitor;
use serde::{Deserialize, Deserializer, de};
mod schema;
pub(crate) use schema::fragments as schema_fragments;
pub(crate) use schema::defs as schema_defs;
static DISPLAY_PATH: OnceLock<fn(&str) -> String> = OnceLock::new();
+17 -17
View File
@@ -7,24 +7,24 @@
use schemars::{Schema, json_schema};
/// Each definition, keyed by the JSON pointer it replaces in `schema/mise.json`.
pub(crate) fn fragments() -> Vec<(&'static str, Schema)> {
/// Each definition, keyed by its name under `$defs` in `schema/mise.json`.
pub(crate) fn defs() -> Vec<(&'static str, Schema)> {
vec![
("/$defs/env", env()),
("/$defs/vars", vars()),
("/$defs/env_directive", env_directive()),
("/$defs/env_value", env_value()),
("/$defs/env_path", env_path()),
("/$defs/env_paths", env_paths()),
("/$defs/env_tools", env_tools()),
("/$defs/env_file_expand", env_file_expand()),
("/$defs/env_file_find_up", env_file_find_up()),
("/$defs/env_redact", env_redact()),
("/$defs/env_required", env_required()),
("/$defs/env_default", env_default()),
("/$defs/env_age", env_age()),
("/$defs/env_age_value", env_age_value()),
("/$defs/env_age_format", env_age_format()),
("env", env()),
("vars", vars()),
("env_directive", env_directive()),
("env_value", env_value()),
("env_path", env_path()),
("env_paths", env_paths()),
("env_tools", env_tools()),
("env_file_expand", env_file_expand()),
("env_file_find_up", env_file_find_up()),
("env_redact", env_redact()),
("env_required", env_required()),
("env_default", env_default()),
("env_age", env_age()),
("env_age_value", env_age_value()),
("env_age_format", env_age_format()),
]
}
+125
View File
@@ -0,0 +1,125 @@
//! `[hooks]`: commands, scripts and tasks run on `cd`, on entering or leaving a project, and
//! around tool installs.
mod schema;
pub(crate) use schema::defs as schema_defs;
use serde::Deserialize;
/// The event a hook runs on.
#[derive(
Debug,
Clone,
Copy,
serde::Serialize,
Deserialize,
strum::Display,
Ord,
PartialOrd,
Eq,
PartialEq,
Hash,
)]
#[serde(rename_all = "lowercase")]
pub enum Hooks {
Enter,
Leave,
Cd,
Preinstall,
Postinstall,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum HookScripts {
One(String),
Many(Vec<String>),
}
impl HookScripts {
pub fn into_script(self) -> String {
match self {
Self::One(script) => script,
Self::Many(scripts) => scripts.join("\n"),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum HookDef {
/// Array of hook definitions: `enter = ["echo hello", { task = "setup" }]`
Array(Vec<HookDefItem>),
One(HookDefItem),
}
impl HookDef {
/// This definition followed by the hooks of `later`, so both run.
pub fn then(self, later: HookDef) -> HookDef {
let mut all = self.into_items();
all.extend(later.into_items());
HookDef::Array(all)
}
pub fn into_items(self) -> Vec<HookDefItem> {
match self {
HookDef::Array(items) => items,
HookDef::One(item) => vec![item],
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum HookDefItem {
/// Simple run string: `enter = "echo hello"`
RunString(String),
/// Table with run: `enter = { run = "echo hello" }`
Run(HookRunTable),
/// Table with script and optional shell: `enter = { script = "echo hello", shell = "bash" }`
ScriptTable {
script: HookScripts,
shell: Option<String>,
},
/// Table with scripts and optional shell: `enter = { scripts = ["echo hello"], shell = "bash" }`
ScriptsTable {
scripts: Vec<String>,
shell: Option<String>,
},
/// Task reference: `enter = { task = "setup" }`
TaskRef { task: String },
}
#[derive(Debug, Clone)]
pub struct HookRunTable {
pub run: Option<String>,
pub run_windows: Option<String>,
pub shell: Option<String>,
}
impl<'de> Deserialize<'de> for HookRunTable {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Helper {
run: Option<String>,
run_windows: Option<String>,
shell: Option<String>,
}
let helper = <Helper as Deserialize>::deserialize(deserializer)?;
if helper.run.is_none() && helper.run_windows.is_none() {
return Err(serde::de::Error::custom(
"hook run table must define `run` or `run_windows`",
));
}
Ok(Self {
run: helper.run,
run_windows: helper.run_windows,
shell: helper.shell,
})
}
}
+256
View File
@@ -0,0 +1,256 @@
//! The JSON schema for `[hooks]`, written by hand next to its declaration types.
//!
//! What an untagged [`HookDefItem`](super::HookDefItem) means depends on the event: a `script`
//! with a `shell` is evaluated in the active shell for `enter`, `leave` and `cd`, while the install
//! hooks ignore the `shell`. A derive cannot tell those apart. Change these together with the types
//! in `super` and mise's `hooks` module, then run `mise run render:schema`.
use schemars::{Schema, json_schema};
/// Each definition, keyed by its name under `$defs` in `schema/mise.json`.
pub(crate) fn defs() -> Vec<(&'static str, Schema)> {
vec![
("hook_run_string", hook_run_string()),
("hook_run_table", hook_run_table()),
("hook_script_value", hook_script_value()),
("hook_scripts_value", hook_scripts_value()),
(
"hook_current_shell_script_table",
hook_current_shell_script_table(),
),
(
"hook_current_shell_scripts_table",
hook_current_shell_scripts_table(),
),
("hook_spawned_script_table", hook_spawned_script_table()),
("hook_spawned_scripts_table", hook_spawned_scripts_table()),
("hook_install_script_table", hook_install_script_table()),
("hook_install_scripts_table", hook_install_scripts_table()),
("hook_task_ref", hook_task_ref()),
("hook_definition_item", hook_definition_item()),
("hook_definition", hook_definition()),
(
"hook_install_definition_item",
hook_install_definition_item(),
),
("hook_install_definition", hook_install_definition()),
("hooks", hooks()),
]
}
fn hook_run_string() -> Schema {
json_schema!({"description": "inline hook command to run", "type": "string"})
}
fn hook_run_table() -> Schema {
json_schema!({
"unevaluatedProperties": false,
"properties": {
"run": {"description": "inline hook command to run", "type": "string"},
"run_windows": {
"description": "inline hook command to run on Windows",
"type": "string"
},
"shell": {
"description": "inline shell command to use for run hooks, such as \"bash -c\"",
"type": "string"
}
},
"anyOf": [{"required": ["run"]}, {"required": ["run_windows"]}],
"type": "object"
})
}
fn hook_script_value() -> Schema {
json_schema!({"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}}]})
}
fn hook_scripts_value() -> Schema {
json_schema!({"type": "array", "items": {"type": "string"}})
}
fn hook_current_shell_script_table() -> Schema {
json_schema!({
"unevaluatedProperties": false,
"properties": {
"script": {
"description": "script or scripts to evaluate in the active shell for enter/leave/cd hooks",
"$ref": "#/$defs/hook_script_value"
},
"shell": {
"description": "active shell name for current-shell hooks, such as \"bash\"",
"type": "string"
}
},
"required": ["script", "shell"],
"type": "object"
})
}
fn hook_current_shell_scripts_table() -> Schema {
json_schema!({
"unevaluatedProperties": false,
"properties": {
"scripts": {
"description": "scripts to evaluate in the active shell for enter/leave/cd hooks",
"$ref": "#/$defs/hook_scripts_value"
},
"shell": {
"description": "active shell name for current-shell hooks, such as \"bash\"",
"type": "string"
}
},
"required": ["scripts", "shell"],
"type": "object"
})
}
fn hook_spawned_script_table() -> Schema {
json_schema!({
"deprecated": true,
"unevaluatedProperties": false,
"properties": {
"script": {
"deprecated": true,
"description": "deprecated alias for run when an enter/leave/cd hook has no shell",
"$ref": "#/$defs/hook_script_value"
}
},
"required": ["script"],
"type": "object"
})
}
fn hook_spawned_scripts_table() -> Schema {
json_schema!({
"deprecated": true,
"unevaluatedProperties": false,
"properties": {
"scripts": {
"deprecated": true,
"description": "deprecated alias for run when an enter/leave/cd hook has no shell",
"$ref": "#/$defs/hook_scripts_value"
}
},
"required": ["scripts"],
"type": "object"
})
}
fn hook_install_script_table() -> Schema {
json_schema!({
"deprecated": true,
"unevaluatedProperties": false,
"properties": {
"script": {
"deprecated": true,
"description": "deprecated alias for run in preinstall/postinstall hooks; executes in a subprocess",
"$ref": "#/$defs/hook_script_value"
},
"shell": {
"deprecated": true,
"description": "ignored for preinstall/postinstall hooks that use script; use a run hook to select an inline shell",
"type": "string"
}
},
"required": ["script"],
"type": "object"
})
}
fn hook_install_scripts_table() -> Schema {
json_schema!({
"deprecated": true,
"unevaluatedProperties": false,
"properties": {
"scripts": {
"deprecated": true,
"description": "deprecated alias for run in preinstall/postinstall hooks; joins the scripts and executes them in a subprocess",
"$ref": "#/$defs/hook_scripts_value"
},
"shell": {
"deprecated": true,
"description": "ignored for preinstall/postinstall hooks that use scripts; use a run hook to select an inline shell",
"type": "string"
}
},
"required": ["scripts"],
"type": "object"
})
}
fn hook_task_ref() -> Schema {
json_schema!({
"unevaluatedProperties": false,
"properties": {"task": {"description": "mise task to run", "type": "string"}},
"required": ["task"],
"type": "object"
})
}
fn hook_definition_item() -> Schema {
json_schema!({
"oneOf": [
{"$ref": "#/$defs/hook_run_string"},
{"$ref": "#/$defs/hook_run_table"},
{"$ref": "#/$defs/hook_current_shell_script_table"},
{"$ref": "#/$defs/hook_current_shell_scripts_table"},
{"$ref": "#/$defs/hook_spawned_script_table"},
{"$ref": "#/$defs/hook_spawned_scripts_table"},
{"$ref": "#/$defs/hook_task_ref"}
]
})
}
fn hook_definition() -> Schema {
json_schema!({
"oneOf": [
{"$ref": "#/$defs/hook_definition_item"},
{
"description": "scripts/tasks to run",
"items": {"$ref": "#/$defs/hook_definition_item"},
"type": "array"
}
]
})
}
fn hook_install_definition_item() -> Schema {
json_schema!({
"oneOf": [
{"$ref": "#/$defs/hook_run_string"},
{"$ref": "#/$defs/hook_run_table"},
{"$ref": "#/$defs/hook_install_script_table"},
{"$ref": "#/$defs/hook_install_scripts_table"},
{"$ref": "#/$defs/hook_task_ref"}
]
})
}
fn hook_install_definition() -> Schema {
json_schema!({
"oneOf": [
{"$ref": "#/$defs/hook_install_definition_item"},
{
"description": "install hooks to run",
"items": {"$ref": "#/$defs/hook_install_definition_item"},
"type": "array"
}
]
})
}
fn hooks() -> Schema {
json_schema!({
"description": "hooks to run",
"type": "object",
"properties": {
"enter": {"$ref": "#/$defs/hook_definition"},
"leave": {"$ref": "#/$defs/hook_definition"},
"cd": {"$ref": "#/$defs/hook_definition"},
"preinstall": {"$ref": "#/$defs/hook_install_definition"},
"postinstall": {"$ref": "#/$defs/hook_install_definition"}
},
"additionalProperties": false
})
}
+7 -3
View File
@@ -1,8 +1,10 @@
//! The `mise.toml` declaration types: plain data parsed from the file, with no templating,
//! settings, or filesystem access. So far this covers `[bootstrap]`, the `[daemons]` sections,
//! `[doctor]`, `[dotfiles]`, `[env]`, `[dotfile_groups]`, `[history]`, `[oci]`, `[tool_config]`,
//! `[vars]`, `[watch_files]`, `[wrappers]`, `[deps]`, `[task_config]`, `[tool_alias]`,
//! `[monorepo]`, `min_version`, `[tools]`, and the `[tasks]` and `[task_templates]` declarations.
//! `[doctor]`, `[dotfiles]`, `[env]`, `[dotfile_groups]`, `[history]`, `[hooks]`, `[oci]`,
//! `[tool_config]`, `[vars]`, `[watch_files]`, `[wrappers]`, `[deps]`, `[task_config]`,
//! `[tool_alias]`, `[monorepo]`, `min_version`, `[tools]`, and the `[tasks]` and
//! `[task_templates]` declarations. [`mise_toml::MiseTomlDecl`] names every top-level key, and
//! [`schema`] generates mise's JSON schemas from these types.
//!
//! mise's config layer stores these as parsed, and the system modules interpret them. Keeping
//! the plain data here, below both, is what lets those two layers be separate crates.
@@ -22,10 +24,12 @@ pub mod env;
pub mod env_value;
pub mod firewall;
pub mod history;
pub mod hooks;
pub mod launchd;
pub mod login_items;
pub mod managed_files;
pub mod min_version;
pub mod mise_toml;
pub mod monorepo;
pub mod oci;
pub mod packages;
+2 -3
View File
@@ -138,8 +138,7 @@ impl JsonSchema for MinVersionSpec {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({
"description": "minimum version of mise required to use this config",
crate::schema::hand_written(json_schema!({
"oneOf": [
{"pattern": "^\\d+\\.\\d+\\.\\d+$", "type": "string"},
{
@@ -159,6 +158,6 @@ impl JsonSchema for MinVersionSpec {
}
}
]
})
}))
}
}
+245
View File
@@ -0,0 +1,245 @@
//! The top level of a `mise.toml`: every key mise reads from it.
//!
//! `schema/mise.json` is generated from [`MiseTomlDecl`]. mise still parses a file into its own
//! `MiseToml`, and a test in mise checks that the two accept the same keys; parsing through this
//! type is the next step.
use indexmap::IndexMap;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use crate::alias::Alias;
use crate::bootstrap::BootstrapTomlConfig;
use crate::daemons::{DaemonSettings, GroupDeclaration};
use crate::history::HistoryTomlConfig;
use crate::min_version::MinVersionSpec;
use crate::oci::OciConfig;
use crate::schema::{hand_written, refs};
use crate::wrappers::CommandWrapper;
// The doc comments on these fields are their descriptions in schema/mise.json, which editors show
// to users. Notes for Rust readers go in plain comments.
//
// A field whose schema is a `$ref`, or written by hand, takes it from `schema_with`; until mise
// parses through this struct, such a field's type is just the value it holds. Every field is
// optional, and the struct-level `default` keeps any of them from being required.
/// config file for mise version manager (mise.toml)
#[derive(Debug, Default, JsonSchema)]
#[schemars(default)]
pub struct MiseTomlDecl {
/// custom shorthands
#[schemars(schema_with = "alias_map", extend("deprecated" = true))]
pub alias: Option<IndexMap<String, Alias>>,
/// Tool version aliases
#[schemars(schema_with = "alias_map")]
pub tool_alias: Option<IndexMap<String, Alias>>,
/// shell aliases
#[schemars(schema_with = "shell_alias")]
pub shell_alias: Option<IndexMap<String, String>>,
/// commands that intercept a binary name before delegating to the active toolset
pub wrappers: Option<IndexMap<String, CommandWrapper>>,
/// environment variables to set
#[schemars(schema_with = "env")]
pub env: Option<toml::Value>,
/// config files merged into this one, ranking just below it: a local path (relative to this file), git::<url>//<path>.toml?ref=<ref> or oci::<registry>/<repo>[:tag|@sha256:<digest>] (the artifact holds a mise.toml). Paranoid mode requires a commit sha or digest for remote includes and rejects local ones; otherwise a branch or tag is refreshed after fetch_remote_versions_cache
#[schemars(schema_with = "include")]
pub include: Option<Vec<String>>,
/// dotenv file(s) to load. Deprecated; use env._.file instead. This will be removed in mise 2027.4.0.
#[schemars(schema_with = "dotenv_files", extend("deprecated" = true))]
pub env_file: Option<Vec<String>>,
/// dotenv file(s) to load. Deprecated; use env._.file instead. This will be removed in mise 2027.4.0.
#[schemars(schema_with = "dotenv_files", extend("deprecated" = true))]
pub dotenv: Option<Vec<String>>,
/// PATH entries to add. Deprecated; use env._.path instead. This will be removed in mise 2027.4.0.
#[schemars(schema_with = "path_entries", extend("deprecated" = true))]
pub env_path: Option<Vec<String>>,
/// marks this config as a monorepo root for task path syntax
#[schemars(schema_with = "refs::monorepo_root")]
pub monorepo_root: Option<bool>,
/// deprecated alias of monorepo_root
#[schemars(extend("deprecated" = true))]
pub experimental_monorepo_root: Option<bool>,
/// configuration for monorepo task discovery
#[schemars(schema_with = "refs::monorepo")]
pub monorepo: Option<toml::Value>,
/// minimum version of mise required to use this config
pub min_version: Option<MinVersionSpec>,
/// [experimental] Sources for mise secrets: values resolved only when mise starts a task or `mise x` command that was granted them. Project config only.
#[schemars(schema_with = "secrets")]
pub secrets: Option<toml::Value>,
/// env or vars keys to redact from logs
pub redactions: Option<Vec<String>>,
/// plugins to use
#[schemars(schema_with = "plugins")]
pub plugins: Option<IndexMap<String, String>>,
/// configure deps providers
#[schemars(schema_with = "deps")]
pub deps: Option<toml::Value>,
pub oci: Option<OciConfig>,
/// mise settings
#[schemars(schema_with = "settings")]
pub settings: Option<toml::Table>,
/// dotfiles applied with `mise dotfiles apply` or `mise bootstrap`, keyed by target path or a logical name when every variant overrides the target; an omitted source for the latter resolves the relative key under dotfiles.root
#[schemars(schema_with = "dotfiles")]
pub dotfiles: Option<toml::Value>,
// The schema has no description for `[dotfile_groups]` yet.
#[schemars(schema_with = "dotfile_groups")]
pub dotfile_groups: Option<toml::Value>,
pub bootstrap: Option<BootstrapTomlConfig>,
pub history: Option<HistoryTomlConfig>,
/// configuration for task execution and management
#[schemars(schema_with = "refs::task_config")]
pub task_config: Option<toml::Value>,
/// policy for tools declared by this config root
#[schemars(schema_with = "refs::tool_config")]
pub tool_config: Option<toml::Value>,
/// task runner tasks
#[schemars(schema_with = "task_map")]
pub tasks: Option<toml::Table>,
/// task templates that can be extended by tasks via extends
#[schemars(schema_with = "task_template_map")]
pub task_templates: Option<toml::Table>,
/// dev tools to use
#[schemars(schema_with = "tools")]
pub tools: Option<crate::tools::ToolsDecl>,
/// hooks to run on events like cd, enter, leave
#[schemars(schema_with = "refs::hooks")]
pub hooks: Option<toml::Value>,
/// variables to set for use in config templates
#[schemars(schema_with = "refs::vars")]
pub vars: Option<toml::Value>,
/// files to watch for changes and run scripts when modified
#[schemars(schema_with = "refs::watch_files")]
pub watch_files: Option<toml::Value>,
// `[_]` holds the user's own data, which mise never reads.
#[schemars(rename = "_", schema_with = "custom")]
pub custom: Option<toml::Table>,
#[schemars(schema_with = "refs::doctor")]
pub doctor: Option<toml::Value>,
/// Experimental project daemons: custom pitchfork commands or managed presets.
#[schemars(schema_with = "daemons")]
pub daemons: Option<toml::Table>,
/// Experimental shared servers owned by the user. Only allowed in global configuration.
#[schemars(schema_with = "daemon_providers")]
pub daemon_providers: Option<toml::Table>,
pub daemons_settings: Option<DaemonSettings>,
/// Experimental named groups of project daemons. Members are daemon names or other group names declared in the same project.
pub daemon_groups: Option<IndexMap<String, GroupDeclaration>>,
}
/// The `$defs` entries for top-level keys that other sections do not share.
pub(crate) fn schema_defs() -> Vec<(&'static str, Schema)> {
vec![(
"monorepo_root",
json_schema!({
"description": "Marks this config as a monorepo root, enabling target path syntax for tasks. When enabled, tasks can be referenced with paths like //projects/frontend:build",
"type": "boolean"
}),
)]
}
fn alias_map(_: &mut SchemaGenerator) -> Schema {
json_schema!({"type": "object", "additionalProperties": {"$ref": "#/$defs/tool_alias_entry"}})
}
fn task_map(_: &mut SchemaGenerator) -> Schema {
json_schema!({"type": "object", "additionalProperties": {"$ref": "#/$defs/task"}})
}
fn task_template_map(_: &mut SchemaGenerator) -> Schema {
json_schema!({"type": "object", "additionalProperties": {"$ref": "#/$defs/task_template"}})
}
fn settings(_: &mut SchemaGenerator) -> Schema {
json_schema!({"type": "object", "$ref": "#/$defs/settings"})
}
fn env(_: &mut SchemaGenerator) -> Schema {
json_schema!({
"oneOf": [{"$ref": "#/$defs/env"}, {"type": "array", "items": {"$ref": "#/$defs/env"}}]
})
}
fn shell_alias(_: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": "object",
"additionalProperties": {"description": "command to run", "type": "string"}
})
}
fn plugins(_: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": "object",
"additionalProperties": {"description": "url to plugin repository", "type": "string"}
})
}
fn custom(_: &mut SchemaGenerator) -> Schema {
json_schema!({"type": "object", "additionalProperties": true})
}
fn include(_: &mut SchemaGenerator) -> Schema {
one_or_many("a config fragment", "config fragments")
}
fn dotenv_files(_: &mut SchemaGenerator) -> Schema {
one_or_many("dotenv file to load", "dotenv files to load")
}
fn path_entries(_: &mut SchemaGenerator) -> Schema {
one_or_many("PATH entry to add", "PATH entries to add")
}
/// A string, or a list of them, as `mise_config::de::deserialize_arr` accepts.
fn one_or_many(one: &str, many: &str) -> Schema {
json_schema!({
"oneOf": [
{"description": one, "type": "string"},
{"description": many, "type": "array", "items": {"type": "string"}}
]
})
}
// Raw on purpose in mise: a malformed `[secrets]` must not fail the whole file.
fn secrets(_: &mut SchemaGenerator) -> Schema {
hand_written(json_schema!({
"type": "object",
"properties": {
"fnox": {
"description": "Use fnox (fnox.toml discovered from this config's directory). https://mise.jdx.dev/environments/secrets/fnox.html",
"type": "object",
"additionalProperties": false,
"properties": {
"profile": {
"type": "string",
"description": "fnox profile (fnox -P); defaults to FNOX_PROFILE, then \"default\""
}
}
}
}
}))
}
fn tools(_: &mut SchemaGenerator) -> Schema {
hand_written(crate::tools::schema::tools())
}
fn deps(_: &mut SchemaGenerator) -> Schema {
hand_written(crate::deps::schema::deps())
}
fn dotfiles(_: &mut SchemaGenerator) -> Schema {
hand_written(crate::dotfiles::schema::dotfiles())
}
fn dotfile_groups(_: &mut SchemaGenerator) -> Schema {
hand_written(crate::dotfiles::schema::dotfile_groups())
}
fn daemons(_: &mut SchemaGenerator) -> Schema {
hand_written(crate::daemons::schema::daemons())
}
fn daemon_providers(_: &mut SchemaGenerator) -> Schema {
hand_written(crate::daemons::schema::daemon_providers())
}
+255 -109
View File
@@ -1,121 +1,184 @@
//! The JSON schema for the `mise.toml` sections whose types live in this crate.
//! The JSON schemas mise publishes: `schema/mise.json`, `schema/mise-task.json` and
//! `schema/miserc.json`, all generated by `mise run render:schema` through the `render-schema`
//! example, and checked by the test below.
//!
//! `schema/mise.json` is still partly written by hand. `mise run render:schema` replaces each
//! fragment listed here at its JSON pointer, the way it already fills in `$defs.settings` from
//! `settings.toml`. A section moves out of the hand-written part when its types gain a
//! `JsonSchema` derive here, and the test below fails while the committed file is stale.
//!
//! A field whose value stays raw TOML (so newer shapes warn instead of failing) has no Rust type
//! to describe it. Its schema stays hand-written under `$defs`, and the generated tree points at
//! it through [`refs`].
//! `mise.json`'s top-level keys come from [`MiseTomlDecl`] and the `[settings]` from mise's
//! `settings.toml`. A section whose types derive `JsonSchema` is generated from them; one whose
//! parser accepts shapes a derive cannot describe has its schema written by hand next to that
//! parser, as does a field whose value stays raw TOML (so newer shapes warn instead of failing).
//! The generated tree points at shared definitions through [`refs`].
mod settings;
mod shared;
use std::collections::{BTreeMap, VecDeque};
use schemars::generate::SchemaSettings;
use schemars::transform::RecursiveTransform;
use schemars::transform::{Transform, transform_subschemas};
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde_json::Value;
use serde_json::{Map, Value, json};
use crate::alias::Alias;
use crate::bootstrap::BootstrapTomlConfig;
use crate::daemons::{DaemonSettings, GroupDeclaration};
use crate::doctor::DoctorConfig;
use crate::history::HistoryTomlConfig;
use crate::launchd::LaunchdCalendarInterval;
use crate::min_version::MinVersionSpec;
use crate::mise_toml::MiseTomlDecl;
use crate::monorepo::MonorepoConfig;
use crate::oci::OciConfig;
use crate::remote::InstallMiseTomlConfig;
use crate::task::{TaskConfig, TaskDecl};
use crate::tool_config::ToolConfig;
use crate::watch_files::WatchFile;
use crate::wrappers::CommandWrapper;
/// Each generated fragment, keyed by the JSON pointer it replaces in `schema/mise.json`.
pub fn fragments() -> Vec<(&'static str, Value)> {
pub(crate) const DRAFT: &str = "https://json-schema.org/draft/2020-12/schema";
/// Each schema file, keyed by its path from the repository root, given the text of mise's
/// `settings.toml`.
pub fn render(settings_toml: &str) -> Vec<(&'static str, Value)> {
let settings: toml::Table = toml::from_str(settings_toml).expect("settings.toml parses");
let mise = mise_json(&settings);
let task = task_json(&mise);
vec![
("schema/mise.json", mise),
("schema/mise-task.json", task),
("schema/miserc.json", settings::miserc(&settings)),
]
}
fn mise_json(settings: &toml::Table) -> Value {
let mut generator = generator();
let derived = vec![
(
"/properties/bootstrap",
fragment::<BootstrapTomlConfig>(&mut generator),
),
(
"/properties/history",
fragment::<HistoryTomlConfig>(&mut generator),
),
(
"/properties/wrappers/additionalProperties",
fragment::<CommandWrapper>(&mut generator),
),
(
"/properties/daemons_settings",
fragment::<DaemonSettings>(&mut generator),
),
(
"/properties/daemon_groups/additionalProperties",
fragment::<GroupDeclaration>(&mut generator),
),
("/properties/oci", fragment::<OciConfig>(&mut generator)),
("/$defs/doctor", fragment::<DoctorConfig>(&mut generator)),
(
"/$defs/monorepo",
fragment::<MonorepoConfig>(&mut generator),
),
("/$defs/tool_config", fragment::<ToolConfig>(&mut generator)),
(
"/$defs/watch_files/items",
fragment::<WatchFile>(&mut generator),
),
(
"/$defs/bootstrap_remote_install_mise",
fragment::<InstallMiseTomlConfig>(&mut generator),
),
(
"/$defs/launchd_calendar_interval",
fragment::<LaunchdCalendarInterval>(&mut generator),
),
];
let decl = fragment::<MiseTomlDecl>(&mut generator);
let settings_def = json!({
"type": "object",
"unevaluatedProperties": false,
"properties": settings::properties(settings)
});
// Sorted by name, so a section's definitions sit together whichever module writes them.
let mut defs = BTreeMap::new();
for (name, schema) in derived_defs(&mut generator)
.into_iter()
.chain(hand_written_defs(&mut generator))
.chain([("settings", settings_def)])
{
assert!(
defs.insert(name, schema).is_none(),
"$defs.{name} is defined twice"
);
}
let mut mise = json!({
"$id": "https://mise.jdx.dev/schema/mise.json",
"$schema": DRAFT,
"title": "mise",
"type": "object",
"$defs": Map::from_iter(defs.into_iter().map(|(name, schema)| (name.to_string(), schema))),
"unevaluatedProperties": false,
"description": decl["description"],
"properties": decl["properties"]
});
strip_hand_written(&mut mise);
mise
}
/// The `$defs` entries generated from this crate's types.
fn derived_defs(generator: &mut SchemaGenerator) -> Vec<(&'static str, Value)> {
// `task_props` is composed with `allOf` in `task` and `task_template`, which close it with
// `unevaluatedProperties`; closing it here as well would reject every key they add.
let mut task_props = fragment::<TaskDecl>(&mut generator);
let mut task_props = fragment::<TaskDecl>(generator);
if let Some(object) = task_props.as_object_mut() {
object.remove("additionalProperties");
}
let task_template = crate::task::template_schema(&task_props);
let tasks = vec![
("/$defs/task_props", task_props),
("/$defs/task_template", task_template),
("/$defs/task_config", fragment::<TaskConfig>(&mut generator)),
];
// Written by hand next to their parser, so they skip the generator's transforms and are
// emitted exactly as written.
let hand_written = crate::env::schema_fragments()
let mut watch_files = fragment::<Vec<WatchFile>>(generator);
watch_files["description"] = "files to watch for changes".into();
vec![
("task_props", task_props),
("task_template", task_template),
("task_config", fragment::<TaskConfig>(generator)),
("tool_config", fragment::<ToolConfig>(generator)),
("monorepo", fragment::<MonorepoConfig>(generator)),
("watch_files", watch_files),
("doctor", fragment::<DoctorConfig>(generator)),
(
"launchd_calendar_interval",
fragment::<LaunchdCalendarInterval>(generator),
),
(
"bootstrap_remote_install_mise",
fragment::<InstallMiseTomlConfig>(generator),
),
]
}
/// The `$defs` entries written by hand next to their parsers. They skip the generator's
/// transforms and are emitted exactly as written.
fn hand_written_defs(generator: &mut SchemaGenerator) -> Vec<(&'static str, Value)> {
shared::defs()
.into_iter()
.chain(crate::task::schema_fragments())
.chain(crate::tools::schema_fragments())
.chain(crate::env::schema_defs())
.chain(crate::task::schema_defs())
.chain(crate::tools::schema_defs())
.chain([
(
"/$defs/tool_alias_entry",
Alias::json_schema(&mut generator),
),
(
"/$defs/tool_alias_versions",
crate::alias::versions_schema(),
),
(
"/properties/min_version",
MinVersionSpec::json_schema(&mut generator),
),
("tool_alias_entry", Alias::json_schema(generator)),
("tool_alias_versions", crate::alias::versions_schema()),
])
.map(|(pointer, schema)| (pointer, schema.to_value()));
derived
.into_iter()
.chain(tasks)
.chain(hand_written)
.chain(crate::hooks::schema_defs())
.chain(crate::mise_toml::schema_defs())
.chain(crate::deps::schema::defs())
.chain(crate::daemons::schema::defs())
.chain(crate::bootstrap::schema::defs())
.map(|(name, schema)| (name, schema.to_value()))
.collect()
}
/// `schema/mise-task.json`, for the task files `task_config.includes` loads: a table of tasks,
/// with the `$defs` of `mise.json` that a task refers to.
fn task_json(mise: &Value) -> Value {
let mut task = json!({
"$id": "https://mise.jdx.dev/schema/mise-task.json",
"$schema": DRAFT,
"title": "mise-task-schema",
"type": "object",
"description": "Config file for included mise tasks (https://mise.jdx.dev/tasks/task-configuration.html)",
"additionalProperties": {"$ref": "#/$defs/task"}
});
task["$defs"] = referenced_defs(&mise["$defs"], &task).into();
task
}
/// Every entry of `defs` that `root` refers to, directly or through another entry, in the order
/// a breadth-first walk reaches them.
fn referenced_defs(defs: &Value, root: &Value) -> Map<String, Value> {
let mut picked = Map::new();
let mut queue = VecDeque::from([root]);
while let Some(value) = queue.pop_front() {
match value {
Value::Array(items) => queue.extend(items),
Value::Object(object) => {
if let Some(Value::String(reference)) = object.get("$ref") {
let Some(path) = reference.strip_prefix("#/$defs/") else {
assert!(
!reference.starts_with("#/"),
"unsupported local JSON schema ref: {reference}"
);
queue.extend(object.values());
continue;
};
let name = path.split('/').next().unwrap_or(path);
let def = defs
.get(name)
.unwrap_or_else(|| panic!("schema/mise.json is missing $defs.{name}"));
if !picked.contains_key(name) {
picked.insert(name.to_string(), def.clone());
queue.push_back(def);
}
}
queue.extend(object.values());
}
_ => {}
}
}
picked
}
/// `schema_with` functions that point at a `$defs` entry, for fields that share one definition.
///
/// Some entries are generated by [`fragments`]; the rest are still written by hand.
pub(crate) mod refs {
use schemars::{Schema, SchemaGenerator, json_schema};
@@ -139,16 +202,23 @@ pub(crate) mod refs {
bootstrap_macos_trackpad,
bootstrap_mise_shell_activate,
bootstrap_remote_install_mise,
doctor,
env,
hooks,
launchd_calendar_interval,
monorepo,
monorepo_root,
os_filter,
task_config,
task_watch,
tool_config,
vars,
watch_files,
);
}
/// Closes a `deny_unknown_fields` struct with `unevaluatedProperties`, the form the hand-written
/// file uses, in place of the `additionalProperties: false` schemars writes. The two accept the
/// schemas use, in place of the `additionalProperties: false` schemars writes. The two accept the
/// same tables when the schema has no subschemas.
pub(crate) fn unevaluated(schema: &mut Schema) {
if schema.get("additionalProperties") == Some(&Value::Bool(false)) {
@@ -157,7 +227,7 @@ pub(crate) fn unevaluated(schema: &mut Schema) {
}
}
/// Writes an untagged enum's alternatives as `oneOf`, the form the hand-written file uses.
/// Writes an untagged enum's alternatives as `oneOf`, the form the hand-written schemas use.
///
/// schemars writes `anyOf`. The alternatives of these enums never overlap, so the two accept
/// the same values.
@@ -170,18 +240,67 @@ pub(crate) fn one_of(schema: &mut Schema) {
fn generator() -> SchemaGenerator {
SchemaSettings::draft2020_12()
.with(|settings| {
// Every fragment is written in place, so a nested type cannot point at a `$defs`
// entry that the hand-written file does not have.
// Every section is written in place, so a nested type cannot point at a `$defs`
// entry that is not listed in `mise_json`.
settings.inline_subschemas = true;
})
.with_transform(RecursiveTransform(drop_null))
.with_transform(RecursiveTransform(drop_integer_format))
.with_transform(RecursiveTransform(drop_empty_default))
.with_transform(RecursiveTransform(reject_unknown_keys))
.with_transform(RecursiveTransform(describe_first))
.with_transform(Generated(drop_null))
.with_transform(Generated(drop_integer_format))
.with_transform(Generated(drop_empty_default))
.with_transform(Generated(reject_unknown_keys))
.with_transform(Generated(describe_first))
.into_generator()
}
const HAND_WRITTEN: &str = "x-mise-hand-written";
/// Marks a schema written by hand, so the generator's transforms leave it as written when a
/// derived type embeds it.
pub(crate) fn hand_written(mut schema: Schema) -> Schema {
schema.insert(HAND_WRITTEN.into(), true.into());
schema
}
/// Applies a transform to a schema and its subschemas, stopping at a [`hand_written`] one.
#[derive(Clone)]
struct Generated<T>(T);
impl<T: Transform> Transform for Generated<T> {
fn transform(&mut self, schema: &mut Schema) {
if schema.get(HAND_WRITTEN).is_some() {
return;
}
self.0.transform(schema);
// `drop_null` can merge a hand-written schema into its `Option` wrapper.
if schema.get(HAND_WRITTEN).is_none() {
transform_subschemas(self, schema);
}
}
}
/// Removes the [`hand_written`] markers, with the empty `default` the derive gives an optional
/// field: TOML has no null, and `{}` or `[]` says nothing an editor can use.
fn strip_hand_written(value: &mut Value) {
match value {
Value::Object(object) => {
if object.remove(HAND_WRITTEN).is_some() {
let empty = match object.get("default") {
Some(Value::Null) => true,
Some(Value::Array(values)) => values.is_empty(),
Some(Value::Object(values)) => values.is_empty(),
_ => false,
};
if empty {
object.remove("default");
}
}
object.values_mut().for_each(strip_hand_written);
}
Value::Array(items) => items.iter_mut().for_each(strip_hand_written),
_ => {}
}
}
fn fragment<T: JsonSchema>(generator: &mut SchemaGenerator) -> Value {
let mut schema = generator.root_schema_for::<T>();
let object = schema.ensure_object();
@@ -280,17 +399,44 @@ fn describe_first(schema: &mut Schema) {
mod tests {
use super::*;
const ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../..");
#[test]
fn schema_json_is_up_to_date() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../schema/mise.json");
let committed: Value =
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
for (pointer, generated) in fragments() {
assert_eq!(
committed.pointer(pointer),
Some(&generated),
"schema/mise.json{pointer} differs from the mise-config types; run `mise run render:schema`"
);
fn schema_files_are_up_to_date() {
let settings = std::fs::read_to_string(format!("{ROOT}/settings.toml")).unwrap();
for (path, generated) in render(&settings) {
let committed: Value =
serde_json::from_str(&std::fs::read_to_string(format!("{ROOT}/{path}")).unwrap())
.unwrap();
if let Some(pointer) = first_difference(&committed, &generated, String::new()) {
panic!(
"{path} differs from mise-config at {pointer}; run `mise run render:schema`"
);
}
}
}
/// The JSON pointer of the first value that differs, ignoring key order.
fn first_difference(a: &Value, b: &Value, pointer: String) -> Option<String> {
match (a, b) {
(Value::Object(a), Value::Object(b)) => {
// Each key once: comparing a shared key from both sides would double the work
// at every level of nesting.
let only_b = b.keys().filter(|key| !a.contains_key(*key));
a.keys().chain(only_b).find_map(|key| {
let at = format!("{pointer}/{}", key.replace('~', "~0").replace('/', "~1"));
match (a.get(key), b.get(key)) {
(Some(a), Some(b)) => first_difference(a, b, at),
_ => Some(at),
}
})
}
(Value::Array(a), Value::Array(b)) if a.len() == b.len() => a
.iter()
.zip(b)
.enumerate()
.find_map(|(i, (a, b))| first_difference(a, b, format!("{pointer}/{i}"))),
_ => (a != b).then_some(pointer),
}
}
}
+154
View File
@@ -0,0 +1,154 @@
//! The `[settings]` schema and `schema/miserc.json`, built from mise's `settings.toml`.
//!
//! Each leaf table of `settings.toml` (one with a `type`) describes a setting; every other table
//! is a group of settings. mise-settings generates the Rust types from the same file, so a setting
//! and its schema cannot drift apart.
use serde_json::{Map, Value, json};
/// The `properties` of `$defs.settings`, one per top-level setting or group.
pub(crate) fn properties(settings: &toml::Table) -> Map<String, Value> {
group(settings, "")
}
/// `schema/miserc.json`: the top-level settings marked `rc`, which mise reads from
/// `.miserc.toml` before it loads any config file.
pub(crate) fn miserc(settings: &toml::Table) -> Value {
let properties: Map<String, Value> = settings
.iter()
.filter_map(|(key, node)| {
let props = leaf(node)?;
(props.get("rc").and_then(toml::Value::as_bool) == Some(true))
.then(|| (key.clone(), element(key, props)))
})
.collect();
json!({
"$schema": super::DRAFT,
"title": "mise rc config",
"description": "Early initialization settings for mise. These settings are loaded before the main config files.",
"type": "object",
"unevaluatedProperties": false,
"properties": properties
})
}
fn leaf(node: &toml::Value) -> Option<&toml::Table> {
node.as_table().filter(|table| table.contains_key("type"))
}
fn group(table: &toml::Table, prefix: &str) -> Map<String, Value> {
table
.iter()
.map(|(key, node)| {
let name = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
let schema = match leaf(node) {
Some(props) => element(&name, props),
None => {
let table = node
.as_table()
.unwrap_or_else(|| panic!("settings.toml: {name} is not a table"));
json!({
"type": "object",
"unevaluatedProperties": false,
"properties": group(table, &name)
})
}
};
(key.clone(), schema)
})
.collect()
}
/// One setting's schema. `name` is its dotted path, for error messages.
fn element(name: &str, props: &toml::Table) -> Value {
let str_prop = |key: &str| props.get(key).and_then(toml::Value::as_str);
let setting_type = str_prop("type").unwrap_or_default();
let json_type = match setting_type {
"String" | "Path" | "Url" | "Duration" => "string",
"Bool" => "boolean",
"Integer" => "integer",
"ListString" | "ListPath" | "SetString" => "array",
"IndexMap<String, String>" => "object",
// Written as `oneOf` below, which AJV's strictTypes accepts where a list of types fails.
"BoolOrString" | "BoolOrList" => "",
other => panic!("settings.toml: unknown type {other:?} for {name}"),
};
let description = str_prop("description")
.unwrap_or_else(|| panic!("settings.toml: missing description for {name}"));
let choices: Option<Value> = props
.get("enum")
.and_then(toml::Value::as_array)
.map(|items| {
items
.iter()
// An entry is a bare value or `{ value, description }`.
.map(|item| to_json(item.as_table().and_then(|t| t.get("value")).unwrap_or(item)))
.collect()
});
let strings = || {
let mut items = json!({"type": "string"});
if let Some(choices) = &choices {
items["enum"] = choices.clone();
}
items
};
let mut element = Map::new();
if let Some(default) = props.get("default") {
element.insert("default".into(), to_json(default));
}
element.insert("description".into(), description.into());
if !json_type.is_empty() {
element.insert("type".into(), json_type.into());
}
if props.contains_key("deprecated") {
element.insert("deprecated".into(), true.into());
}
if json_type == "integer" && str_prop("rust_type").is_some_and(|t| t.starts_with('u')) {
element.insert("minimum".into(), 0.into());
}
if json_type == "array" {
element.insert("items".into(), strings());
} else if setting_type != "BoolOrList"
&& let Some(choices) = &choices
{
element.insert("enum".into(), choices.clone());
}
if json_type == "object" {
element.insert("additionalProperties".into(), json!({"type": "string"}));
}
match setting_type {
// A list setting that also takes a single value.
_ if json_type == "array" && str_prop("deserialize_with") == Some("path_or_paths") => {
element.remove("type");
let items = element.remove("items");
element.insert(
"oneOf".into(),
json!([{"type": "string"}, {"type": "array", "items": items}]),
);
}
"BoolOrString" => {
element.insert(
"oneOf".into(),
json!([{"type": "boolean"}, {"type": "string"}]),
);
}
// A boolean, or a list of the strings the setting's `enum` allows.
"BoolOrList" => {
element.insert(
"oneOf".into(),
json!([{"type": "boolean"}, {"type": "array", "items": strings()}]),
);
}
_ => {}
}
element.into()
}
fn to_json(value: &toml::Value) -> Value {
serde_json::to_value(value).expect("a settings.toml value converts to JSON")
}
+30
View File
@@ -0,0 +1,30 @@
//! Definitions written by hand that several sections share.
use schemars::{Schema, json_schema};
/// Each definition, keyed by its name under `$defs` in `schema/mise.json`.
pub(crate) fn defs() -> Vec<(&'static str, Schema)> {
vec![
("os_filter", os_filter()),
("os_filter_item", os_filter_item()),
]
}
fn os_filter() -> Schema {
json_schema!({
"description": "operating system filters to install on",
"oneOf": [
{"$ref": "#/$defs/os_filter_item"},
{"type": "array", "items": {"$ref": "#/$defs/os_filter_item"}, "minItems": 1}
]
})
}
fn os_filter_item() -> Schema {
json_schema!({
"description": "operating system or os/arch pair to install on",
"type": "string",
"pattern": "^[A-Za-z0-9_.+-]+(/[A-Za-z0-9_.+-]+)?$",
"examples": ["linux", "macos", "windows", "unix", "macos/arm64", "linux/x64"]
})
}
+1 -1
View File
@@ -19,7 +19,7 @@ pub use confirm::TaskConfirm;
pub use decl::{TaskDecl, TaskTemplate};
pub use dep::TaskDep;
pub use outputs::TaskOutputs;
pub(crate) use schema::{fragments as schema_fragments, task_template as template_schema};
pub(crate) use schema::{defs as schema_defs, task_template as template_schema};
pub use secrets::{G7_TEXT, TaskSecrets};
// A `{ version = "..." }`-style tool table, shared by `[tools]` and a task's `tools`: exactly one
+7 -7
View File
@@ -15,14 +15,14 @@ use serde_json::{Map, Value};
use super::TaskTemplate;
/// The hand-written definitions, keyed by the JSON pointer each replaces in `schema/mise.json`.
pub(crate) fn fragments() -> Vec<(&'static str, Schema)> {
/// The hand-written definitions, keyed by their names under `$defs` in `schema/mise.json`.
pub(crate) fn defs() -> Vec<(&'static str, Schema)> {
vec![
("/$defs/task", task()),
("/$defs/task_dependency_item", task_dependency_item()),
("/$defs/task_dependency_table", task_dependency_table()),
("/$defs/task_run_entry", task_run_entry()),
("/$defs/task_watch", task_watch()),
("task", task()),
("task_dependency_item", task_dependency_item()),
("task_dependency_table", task_dependency_table()),
("task_run_entry", task_run_entry()),
("task_watch", task_watch()),
]
}
+2 -2
View File
@@ -12,8 +12,8 @@ use serde::{Deserialize, Deserializer};
use crate::env_value::EnvValue;
use crate::task::ParsedToolMap;
mod schema;
pub(crate) use schema::fragments as schema_fragments;
pub(crate) mod schema;
pub(crate) use schema::defs as schema_defs;
/// `[tools]` as written: tool key (`node`, `"cargo:ripgrep"`, `"node[os=linux]"`) to its versions.
pub type ToolsDecl = IndexMap<String, ToolSpecList>;
+55 -63
View File
@@ -9,72 +9,65 @@
use schemars::{Schema, json_schema};
/// Each definition, keyed by the JSON pointer it replaces in `schema/mise.json`.
pub(crate) fn fragments() -> Vec<(&'static str, Schema)> {
/// Each definition, keyed by its name under `$defs` in `schema/mise.json`.
pub(crate) fn defs() -> Vec<(&'static str, Schema)> {
vec![
("/properties/tools", tools()),
("/$defs/tool", tool()),
("/$defs/tool_bool", tool_bool()),
("/$defs/tool_bool_text", tool_bool_text()),
("/$defs/tool_platforms", tool_platforms()),
("/$defs/tool_forge_options", tool_forge_options()),
("/$defs/tool_github_options", tool_github_options()),
("/$defs/tool_forge_platforms", tool_forge_platforms()),
("/$defs/tool_github", tool_github()),
("/$defs/tool_gitlab_options", tool_gitlab_options()),
("/$defs/tool_gitlab", tool_gitlab()),
("/$defs/tool_forgejo_options", tool_forgejo_options()),
("/$defs/tool_forgejo", tool_forgejo()),
("/$defs/tool_ubi_options", tool_ubi_options()),
("/$defs/tool_ubi_platforms", tool_ubi_platforms()),
("/$defs/tool_ubi", tool_ubi()),
("/$defs/tool_http_options", tool_http_options()),
("/$defs/tool_http_platforms", tool_http_platforms()),
("/$defs/tool_http", tool_http()),
("/$defs/tool_s3_options", tool_s3_options()),
("/$defs/tool_s3_platforms", tool_s3_platforms()),
("/$defs/tool_s3", tool_s3()),
("/$defs/tool_aqua_options", tool_aqua_options()),
("/$defs/tool_aqua", tool_aqua()),
("/$defs/tool_cargo_options", tool_cargo_options()),
("/$defs/tool_cargo_platforms", tool_cargo_platforms()),
("/$defs/tool_cargo", tool_cargo()),
("/$defs/tool_npm_options", tool_npm_options()),
("/$defs/tool_npm", tool_npm()),
("/$defs/tool_pypi_options", tool_pypi_options()),
("/$defs/tool_pypi", tool_pypi()),
("/$defs/tool_gem_options", tool_gem_options()),
("/$defs/tool_gem", tool_gem()),
("/$defs/tool_go_options", tool_go_options()),
("/$defs/tool_go", tool_go()),
("/$defs/tool_conda_options", tool_conda_options()),
("/$defs/tool_conda", tool_conda()),
("/$defs/tool_spm_options", tool_spm_options()),
("/$defs/tool_spm_platforms", tool_spm_platforms()),
("/$defs/tool_spm", tool_spm()),
("/$defs/tool_packslip_options", tool_packslip_options()),
("/$defs/tool_packslip", tool_packslip()),
("/$defs/tool_spinel_options", tool_spinel_options()),
("/$defs/tool_spinel", tool_spinel()),
(
"/$defs/tool_core_python_options",
tool_core_python_options(),
),
("/$defs/tool_core_python", tool_core_python()),
("/$defs/tool_core_java_options", tool_core_java_options()),
("/$defs/tool_core_java", tool_core_java()),
("/$defs/tool_core_rust_options", tool_core_rust_options()),
("/$defs/tool_core_rust", tool_core_rust()),
(
"/$defs/tool_core_dotnet_options",
tool_core_dotnet_options(),
),
("/$defs/tool_core_dotnet", tool_core_dotnet()),
("/$defs/tool_options", tool_options()),
("tool", tool()),
("tool_bool", tool_bool()),
("tool_bool_text", tool_bool_text()),
("tool_platforms", tool_platforms()),
("tool_forge_options", tool_forge_options()),
("tool_github_options", tool_github_options()),
("tool_forge_platforms", tool_forge_platforms()),
("tool_github", tool_github()),
("tool_gitlab_options", tool_gitlab_options()),
("tool_gitlab", tool_gitlab()),
("tool_forgejo_options", tool_forgejo_options()),
("tool_forgejo", tool_forgejo()),
("tool_ubi_options", tool_ubi_options()),
("tool_ubi_platforms", tool_ubi_platforms()),
("tool_ubi", tool_ubi()),
("tool_http_options", tool_http_options()),
("tool_http_platforms", tool_http_platforms()),
("tool_http", tool_http()),
("tool_s3_options", tool_s3_options()),
("tool_s3_platforms", tool_s3_platforms()),
("tool_s3", tool_s3()),
("tool_aqua_options", tool_aqua_options()),
("tool_aqua", tool_aqua()),
("tool_cargo_options", tool_cargo_options()),
("tool_cargo_platforms", tool_cargo_platforms()),
("tool_cargo", tool_cargo()),
("tool_npm_options", tool_npm_options()),
("tool_npm", tool_npm()),
("tool_pypi_options", tool_pypi_options()),
("tool_pypi", tool_pypi()),
("tool_gem_options", tool_gem_options()),
("tool_gem", tool_gem()),
("tool_go_options", tool_go_options()),
("tool_go", tool_go()),
("tool_conda_options", tool_conda_options()),
("tool_conda", tool_conda()),
("tool_spm_options", tool_spm_options()),
("tool_spm_platforms", tool_spm_platforms()),
("tool_spm", tool_spm()),
("tool_packslip_options", tool_packslip_options()),
("tool_packslip", tool_packslip()),
("tool_spinel_options", tool_spinel_options()),
("tool_spinel", tool_spinel()),
("tool_core_python_options", tool_core_python_options()),
("tool_core_python", tool_core_python()),
("tool_core_java_options", tool_core_java_options()),
("tool_core_java", tool_core_java()),
("tool_core_rust_options", tool_core_rust_options()),
("tool_core_rust", tool_core_rust()),
("tool_core_dotnet_options", tool_core_dotnet_options()),
("tool_core_dotnet", tool_core_dotnet()),
("tool_options", tool_options()),
]
}
fn tools() -> Schema {
pub(crate) fn tools() -> Schema {
json_schema!({
"patternProperties": {
"^github:": {
@@ -201,7 +194,6 @@ fn tools() -> Schema {
"additionalProperties": {
"oneOf": [{"items": {"$ref": "#/$defs/tool"}, "type": "array"}, {"$ref": "#/$defs/tool"}]
},
"description": "dev tools to use",
"type": "object"
})
}
+15 -18
View File
@@ -213,25 +213,22 @@ remove it.
Edit the source, then regenerate. Commit the output with the change that
produced it.
| Output | Source | Regenerate with |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| `mise.usage.kdl`, `docs/cli/` | Command definitions in `src/cli/`, `src/assets/mise-extra.usage.kdl`, and `docs/.vitepress/cli-reference.ts` | `mise run render:usage` |
| `tasks.md` | The project's tasks in `tasks.toml`, `xtasks/`, and `mise.toml`, through `mise generate task-docs` | `mise run render:usage` |
| `man/man1/mise.1` | `mise.usage.kdl` | `mise run render:mangen` |
| `completions/` | The usage spec, through `mise completion <shell>` | `mise run render:completions` |
| `docs/.vitepress/cli_commands.ts` and the idiomatic version file table | `src/cli/` and the `idiomatic_files` keys in `registry/*.toml` | `mise run render:help` |
| The settings part of `schema/mise.json`, `schema/miserc.json`, `schema/mise-task.json` | `settings.toml`; `mise-task.json` also copies the task definitions from `schema/mise.json` | `mise run render:schema` |
| The parts of `schema/mise.json` for sections declared in `crates/mise-config` | The types and doc comments in `crates/mise-config`, through its `render-schema` example | `mise run render:schema` |
| `docs/public/llms.txt` | Page titles and leads, and `docs/.vitepress/sidebar.ts` | `mise run render:llms` |
| `Settings` types | `settings.toml` | Every build (`crates/mise-settings/build.rs`) |
| The registry compiled into mise | `registry/*.toml` | Every build (`build.rs`) |
| The [settings reference](/configuration/settings.html) | `settings.toml` | Every docs build |
| Output | Source | Regenerate with |
| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `mise.usage.kdl`, `docs/cli/` | Command definitions in `src/cli/`, `src/assets/mise-extra.usage.kdl`, and `docs/.vitepress/cli-reference.ts` | `mise run render:usage` |
| `tasks.md` | The project's tasks in `tasks.toml`, `xtasks/`, and `mise.toml`, through `mise generate task-docs` | `mise run render:usage` |
| `man/man1/mise.1` | `mise.usage.kdl` | `mise run render:mangen` |
| `completions/` | The usage spec, through `mise completion <shell>` | `mise run render:completions` |
| `docs/.vitepress/cli_commands.ts` and the idiomatic version file table | `src/cli/` and the `idiomatic_files` keys in `registry/*.toml` | `mise run render:help` |
| `schema/mise.json`, `schema/mise-task.json`, `schema/miserc.json` | The types and doc comments in `crates/mise-config` (`MiseTomlDecl` for the top level) and `settings.toml`, through mise-config's `render-schema` example | `mise run render:schema` |
| `docs/public/llms.txt` | Page titles and leads, and `docs/.vitepress/sidebar.ts` | `mise run render:llms` |
| `Settings` types | `settings.toml` | Every build (`crates/mise-settings/build.rs`) |
| The registry compiled into mise | `registry/*.toml` | Every build (`build.rs`) |
| The [settings reference](/configuration/settings.html) | `settings.toml` | Every docs build |
`mise run render` runs every `render:*` task. The rest of `schema/mise.json` is
written by hand: change it when you change config syntax, then run
`mise run render:schema`. Sections move to `crates/mise-config` one at a time.
For those, change the types and doc comments, not the JSON; a `mise-config`
test fails while the committed file is stale.
`mise run render` runs every `render:*` task. For the schemas, change the types
and doc comments in `crates/mise-config`, not the JSON; a `mise-config` test
fails while the committed files are stale.
## Tests {#tests}
+2 -1
View File
@@ -252,7 +252,8 @@ not have to change your default toolchain.
### Schema conformance {#schema-conformance}
`schema/mise.json` is written by hand, so a unit test checks it against the
`schema/mise.json` is generated from the declaration types in
`crates/mise-config`, not from the parser, so a unit test checks it against the
parser: `cargo test schema_matches_parser` parses every `toml` block in `docs/`
and every mise config an E2E test writes with a heredoc, validates each against
the schema, and fails when the two disagree. Fix the schema, then run
+3368 -3368
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1973,7 +1973,7 @@ impl ConfigFile for MiseToml {
.hooks
.iter()
.map(|(hook_type, def)| {
let mut hooks = def.clone().into_hooks(*hook_type);
let mut hooks = crate::hooks::hooks_from_def(def.clone(), *hook_type);
for hook in hooks.iter_mut() {
hook.render_templates(|s| self.parse_template(s))?;
}
@@ -566,3 +566,60 @@ fn redirect_targets() {
assert_eq!(redirect_target(line), target, "{line}");
}
}
/// `schema/mise.json` lists its top-level keys from `mise_config::mise_toml::MiseTomlDecl`, and
/// [`MiseToml`] must read exactly those keys: one it reads that the schema lacks is flagged in
/// every editor, and one the schema lists that it skips is ignored with a warning.
#[test]
fn schema_keys_match_parser() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let schema = std::fs::read_to_string(root.join("schema/mise.json")).unwrap();
let schema: serde_json::Value = serde_json::from_str(&schema).unwrap();
let declared: BTreeSet<&str> = schema["properties"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
let parsed: BTreeSet<&str> = serde_fields::<MiseToml>().iter().copied().collect();
assert_eq!(
declared, parsed,
"MiseTomlDecl in crates/mise-config and MiseToml read different top-level keys"
);
}
/// The fields a derived `Deserialize` reads, as serde hands them to `deserialize_struct`.
fn serde_fields<T: serde::de::DeserializeOwned>() -> &'static [&'static str] {
use serde::de::{self, Visitor};
struct Fields<'a>(&'a mut &'static [&'static str]);
impl<'de> de::Deserializer<'de> for Fields<'_> {
type Error = de::value::Error;
fn deserialize_any<V: Visitor<'de>>(self, _: V) -> Result<V::Value, Self::Error> {
Err(de::Error::custom("not a struct"))
}
fn deserialize_struct<V: Visitor<'de>>(
self,
_: &'static str,
fields: &'static [&'static str],
_: V,
) -> Result<V::Value, Self::Error> {
*self.0 = fields;
Err(de::Error::custom("only the field names were wanted"))
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf
option unit unit_struct newtype_struct seq tuple tuple_struct map enum identifier
ignored_any
}
}
let mut fields: &'static [&'static str] = &[];
let _ = T::deserialize(Fields(&mut fields));
assert!(!fields.is_empty(), "not a derived struct");
fields
}
+62 -176
View File
@@ -60,174 +60,56 @@ fn hook_backend_identifier(full: &str) -> String {
format!("{backend}:{url}")
}
#[derive(
Debug,
Clone,
Copy,
serde::Serialize,
serde::Deserialize,
strum::Display,
Ord,
PartialOrd,
Eq,
PartialEq,
Hash,
)]
#[serde(rename_all = "lowercase")]
pub enum Hooks {
Enter,
Leave,
Cd,
Preinstall,
Postinstall,
pub use mise_config::hooks::Hooks;
pub(crate) use mise_config::hooks::{HookDef, HookDefItem};
/// The hooks a `[hooks]` entry declares for `hook_type`.
pub(crate) fn hooks_from_def(def: HookDef, hook_type: Hooks) -> Vec<Hook> {
def.into_items()
.into_iter()
.map(|item| hook_from_item(item, hook_type))
.collect()
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(untagged)]
pub(crate) enum HookScripts {
One(String),
Many(Vec<String>),
}
impl HookScripts {
fn into_script(self) -> String {
match self {
Self::One(script) => script,
Self::Many(scripts) => scripts.join("\n"),
}
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(untagged)]
pub(crate) enum HookDef {
/// Array of hook definitions: `enter = ["echo hello", { task = "setup" }]`
Array(Vec<HookDefItem>),
One(HookDefItem),
}
impl HookDef {
/// This definition followed by the hooks of `later`, so both run.
pub(crate) fn then(self, later: HookDef) -> HookDef {
let items = |def: HookDef| match def {
HookDef::Array(items) => items,
HookDef::One(item) => vec![item],
};
let mut all = items(self);
all.extend(items(later));
HookDef::Array(all)
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub(crate) enum HookDefItem {
/// Simple run string: `enter = "echo hello"`
RunString(String),
/// Table with run: `enter = { run = "echo hello" }`
Run(HookRunTable),
/// Table with script and optional shell: `enter = { script = "echo hello", shell = "bash" }`
ScriptTable {
script: HookScripts,
shell: Option<String>,
},
/// Table with scripts and optional shell: `enter = { scripts = ["echo hello"], shell = "bash" }`
ScriptsTable {
scripts: Vec<String>,
shell: Option<String>,
},
/// Task reference: `enter = { task = "setup" }`
TaskRef { task: String },
}
#[derive(Debug, Clone)]
pub(crate) struct HookRunTable {
run: Option<String>,
run_windows: Option<String>,
shell: Option<String>,
}
impl<'de> serde::Deserialize<'de> for HookRunTable {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Helper {
run: Option<String>,
run_windows: Option<String>,
shell: Option<String>,
}
let helper = <Helper as serde::Deserialize>::deserialize(deserializer)?;
if helper.run.is_none() && helper.run_windows.is_none() {
return Err(serde::de::Error::custom(
"hook run table must define `run` or `run_windows`",
));
}
Ok(Self {
run: helper.run,
run_windows: helper.run_windows,
shell: helper.shell,
})
}
}
impl HookDef {
/// Convert to a list of Hook structs with the given hook type
pub(crate) fn into_hooks(self, hook_type: Hooks) -> Vec<Hook> {
match self {
HookDef::One(def) => vec![def.into_hook(hook_type)],
HookDef::Array(arr) => arr
.into_iter()
.map(|def| def.into_hook(hook_type))
.collect(),
}
}
}
impl HookDefItem {
fn into_hook(self, hook_type: Hooks) -> Hook {
match self {
HookDefItem::RunString(script) => Hook {
hook: hook_type,
action: HookAction::Run {
run: Some(script),
run_windows: None,
shell: None,
legacy_script: false,
ignored_shell: None,
},
global: false,
fn hook_from_item(item: HookDefItem, hook_type: Hooks) -> Hook {
match item {
HookDefItem::RunString(script) => Hook {
hook: hook_type,
action: HookAction::Run {
run: Some(script),
run_windows: None,
shell: None,
legacy_script: false,
ignored_shell: None,
},
HookDefItem::Run(table) => Hook {
hook: hook_type,
action: HookAction::Run {
run: table.run,
run_windows: table.run_windows,
shell: table.shell,
legacy_script: false,
ignored_shell: None,
},
global: false,
global: false,
},
HookDefItem::Run(table) => Hook {
hook: hook_type,
action: HookAction::Run {
run: table.run,
run_windows: table.run_windows,
shell: table.shell,
legacy_script: false,
ignored_shell: None,
},
HookDefItem::ScriptTable { script, shell } => Hook {
hook: hook_type,
action: script_hook_action(hook_type, script.into_script(), shell, true),
global: false,
},
HookDefItem::ScriptsTable { scripts, shell } => Hook {
hook: hook_type,
action: script_hook_action(hook_type, scripts.join("\n"), shell, true),
global: false,
},
HookDefItem::TaskRef { task } => Hook {
hook: hook_type,
action: HookAction::Task { task_name: task },
global: false,
},
}
global: false,
},
HookDefItem::ScriptTable { script, shell } => Hook {
hook: hook_type,
action: script_hook_action(hook_type, script.into_script(), shell, true),
global: false,
},
HookDefItem::ScriptsTable { scripts, shell } => Hook {
hook: hook_type,
action: script_hook_action(hook_type, scripts.join("\n"), shell, true),
global: false,
},
HookDefItem::TaskRef { task } => Hook {
hook: hook_type,
action: HookAction::Task { task_name: task },
global: false,
},
}
}
@@ -1066,7 +948,7 @@ mod tests {
"#,
)
.unwrap();
let hooks = parsed.hook.into_hooks(Hooks::Postinstall);
let hooks = hooks_from_def(parsed.hook, Hooks::Postinstall);
assert_eq!(hooks.len(), 1);
match &hooks[0].action {
@@ -1092,7 +974,7 @@ mod tests {
"#,
)
.unwrap();
let hooks = parsed.hook.into_hooks(Hooks::Postinstall);
let hooks = hooks_from_def(parsed.hook, Hooks::Postinstall);
assert_eq!(hooks.len(), 3);
let runs = hooks
@@ -1165,11 +1047,13 @@ mod tests {
#[test]
fn scripts_tables_are_legacy_only_when_spawned() {
let spawned = HookDef::One(HookDefItem::ScriptsTable {
scripts: vec!["echo one".into(), "echo two".into()],
shell: None,
})
.into_hooks(Hooks::Postinstall);
let spawned = hooks_from_def(
HookDef::One(HookDefItem::ScriptsTable {
scripts: vec!["echo one".into(), "echo two".into()],
shell: None,
}),
Hooks::Postinstall,
);
assert!(matches!(
&spawned[0].action,
HookAction::Run {
@@ -1178,11 +1062,13 @@ mod tests {
}
));
let current_shell = HookDef::One(HookDefItem::ScriptsTable {
scripts: vec!["echo one".into(), "echo two".into()],
shell: Some("bash".into()),
})
.into_hooks(Hooks::Enter);
let current_shell = hooks_from_def(
HookDef::One(HookDefItem::ScriptsTable {
scripts: vec!["echo one".into(), "echo two".into()],
shell: Some("bash".into()),
}),
Hooks::Enter,
);
assert!(matches!(
&current_shell[0].action,
HookAction::CurrentShell { .. }
-2
View File
@@ -196,8 +196,6 @@ Generate man pages
## `render:schema`
- Depends: docs:setup
- **Usage:** `render:schema`
Render JSON schema
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
#MISE description="Render JSON schema"
set -euo pipefail
# mise-config generates schema/mise.json, schema/mise-task.json and schema/miserc.json from its
# types and settings.toml. Like CI's other cargo commands, this skips the MSRV check: the lint
# runner's rustc can be older than the MSRV.
cargo run --quiet --ignore-rust-version --package mise-config --example render-schema
oxfmt --write schema/mise.json schema/mise-task.json schema/miserc.json
-336
View File
@@ -1,336 +0,0 @@
#!/usr/bin/env node
//MISE description="Render JSON schema"
//MISE depends=["docs:setup"]
import { spawnSync } from "node:child_process";
import * as fs from "node:fs";
import * as toml from "toml";
type EnumValue = string | boolean | number;
type EnumItem = EnumValue | { value: EnumValue; description?: string };
type Props = {
type: string;
description: string;
default?: unknown;
rust_type?: string;
deprecated?: string;
enum?: EnumItem[];
rc?: boolean;
deserialize_with?: string;
};
type SettingsNode = Props | { [key: string]: SettingsNode };
type SettingsToml = Record<string, SettingsNode>;
type JsonObject = Record<string, unknown>;
type Element = {
type: string | string[];
default: unknown;
description: string;
deprecated?: true;
minimum?: number;
enum?: EnumValue[];
items?: {
type: string | string[];
enum?: EnumValue[];
};
additionalProperties?: {
type: string;
};
};
type NestedElement = {
type: "object";
unevaluatedProperties: false;
deprecated?: true;
properties: Record<string, Element | NestedElement>;
};
const writtenPaths: string[] = [];
function writeFormattedJson(path: string, value: unknown) {
fs.writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
writtenPaths.push(path);
}
function formatWithOxfmt(paths: string[]) {
if (paths.length === 0) return;
const result = spawnSync("oxfmt", ["--write", ...paths], {
stdio: "inherit",
});
if (result.error) {
throw new Error(`oxfmt failed to spawn: ${result.error.message}`);
}
if (result.status !== 0) {
throw new Error(`oxfmt exited with status ${result.status}`);
}
}
function crawlReferencedDefs(schema: JsonObject, root: unknown) {
const defs = schema["$defs"] as JsonObject | undefined;
if (!defs) {
throw new Error("schema/mise.json is missing $defs");
}
const queued = [root];
const seenDefs = new Set<string>();
const picked: JsonObject = {};
for (let i = 0; i < queued.length; i++) {
const value = queued[i];
if (Array.isArray(value)) {
queued.push(...value);
continue;
}
if (!value || typeof value !== "object") {
continue;
}
const obj = value as JsonObject;
const ref = obj["$ref"];
if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
const key = ref.slice("#/$defs/".length).split("/")[0];
if (defs[key] === undefined) {
throw new Error(`schema/mise.json is missing $defs.${key}`);
}
if (!seenDefs.has(key)) {
seenDefs.add(key);
picked[key] = defs[key];
queued.push(defs[key]);
}
} else if (typeof ref === "string" && ref.startsWith("#/")) {
throw new Error(`unsupported local JSON schema ref: ${ref}`);
}
queued.push(...Object.values(obj));
}
return picked;
}
function buildTaskSchema(schema: JsonObject) {
const taskSchema: JsonObject = {
$id: "https://mise.jdx.dev/schema/mise-task.json",
$schema: schema["$schema"],
title: "mise-task-schema",
type: "object",
description:
"Config file for included mise tasks (https://mise.jdx.dev/tasks/task-configuration.html)",
additionalProperties: {
$ref: "#/$defs/task",
},
};
taskSchema["$defs"] = crawlReferencedDefs(schema, taskSchema);
return taskSchema;
}
function buildElement(key: string, props: Props): Element {
const typeMap: Record<string, string | string[]> = {
String: "string",
Path: "string",
Url: "string",
Duration: "string",
Bool: "boolean",
Integer: "integer",
ListString: "string[]",
ListPath: "string[]",
SetString: "string[]",
"IndexMap<String, String>": "object",
BoolOrString: "__bool_or_string__",
BoolOrList: "__bool_or_list__",
};
const type = props.type ? typeMap[props.type] : undefined;
if (!type) {
throw new Error(`Unknown type: ${props.type}`);
}
if (!props.description) {
throw new Error(`Missing description for ${key}`);
}
const element: Element = {
default: props.default,
description: props.description,
type,
};
if (props.deprecated) {
element.deprecated = true;
}
if (type === "integer" && props.rust_type?.startsWith("u")) {
element.minimum = 0;
}
const enumValues = props.enum?.map((e) =>
typeof e === "object" && e !== null && "value" in e ? e.value : e,
);
if (type === "string[]") {
element.type = "array";
element.items = {
type: "string",
...(enumValues ? { enum: enumValues } : {}),
};
} else if (enumValues) {
element.enum = enumValues;
}
if (type === "object") {
element.additionalProperties = {
type: "string",
};
}
// A list setting that also takes a single value: use oneOf for AJV strictTypes
if (type === "string[]" && props.deserialize_with === "path_or_paths") {
const items = element.items;
delete (element as Record<string, unknown>).type;
delete (element as Record<string, unknown>).items;
(element as Record<string, unknown>).oneOf = [
{ type: "string" },
{ type: "array", items },
];
}
// BoolOrString: use oneOf instead of union type array for AJV strictTypes
if (type === "__bool_or_string__") {
delete (element as Record<string, unknown>).type;
const oneOfTypes: Array<{ type: string }> = [
{ type: "boolean" },
{ type: "string" },
];
(element as Record<string, unknown>).oneOf = oneOfTypes;
}
// BoolOrList: a boolean, or a list of the strings the setting's enum allows.
if (type === "__bool_or_list__") {
delete (element as Record<string, unknown>).type;
delete element.enum;
(element as Record<string, unknown>).oneOf = [
{ type: "boolean" },
{
type: "array",
items: { type: "string", ...(enumValues ? { enum: enumValues } : {}) },
},
];
}
return element;
}
const doc = toml.parse(
fs.readFileSync("settings.toml", "utf-8"),
) as SettingsToml;
const isLeaf = (node: SettingsNode): node is Props => {
return "type" in node;
};
function buildSettings(
table: Record<string, SettingsNode>,
prefix = "",
): Record<string, Element | NestedElement> {
const settings: Record<string, Element | NestedElement> = {};
for (const [key, node] of Object.entries(table)) {
const name = prefix ? `${prefix}.${key}` : key;
if (isLeaf(node)) {
settings[key] = buildElement(name, node);
} else {
settings[key] = {
type: "object",
unevaluatedProperties: false,
properties: buildSettings(node, name),
};
}
}
return settings;
}
const settings = buildSettings(doc);
// The sections whose types live in crates/mise-config, generated from those types.
function generatedFragments(): Record<string, unknown> {
const result = spawnSync(
"cargo",
[
"run",
"--quiet",
// Like CI's other cargo commands: the lint runner's rustc can be older than the MSRV.
"--ignore-rust-version",
"--package",
"mise-config",
"--example",
"render-schema",
],
{
encoding: "utf-8",
stdio: ["ignore", "pipe", "inherit"],
maxBuffer: 64 * 1024 * 1024,
},
);
if (result.error) {
throw new Error(`cargo failed to spawn: ${result.error.message}`);
}
if (result.status !== 0) {
throw new Error(
`mise-config's render-schema example exited with status ${result.status}`,
);
}
return JSON.parse(result.stdout);
}
// Replaces the value at a JSON pointer. The key must already exist, so a mistyped pointer
// fails instead of adding a section nothing references.
function replaceAtPointer(root: JsonObject, pointer: string, value: unknown) {
const keys = pointer
.split("/")
.slice(1)
.map((key) => key.replaceAll("~1", "/").replaceAll("~0", "~"));
const last = keys.pop();
let node: unknown = root;
for (const key of keys) {
node = (node as JsonObject | undefined)?.[key];
}
if (
last === undefined ||
typeof node !== "object" ||
node === null ||
!(last in node)
) {
throw new Error(`schema/mise.json has no ${pointer} to replace`);
}
(node as JsonObject)[last] = value;
}
const schema = JSON.parse(fs.readFileSync("schema/mise.json", "utf-8"));
schema["$defs"].settings.properties = settings;
for (const [pointer, fragment] of Object.entries(generatedFragments())) {
replaceAtPointer(schema, pointer, fragment);
}
writeFormattedJson("schema/mise.json", schema);
writeFormattedJson("schema/mise-task.json", buildTaskSchema(schema));
// Generate .miserc.toml schema with only rc=true settings
const misercSettings: Record<string, Element> = {};
for (const key in doc) {
const props = doc[key];
if (isLeaf(props) && props.rc === true) {
misercSettings[key] = buildElement(key, props);
}
}
const misercSchema = {
$schema: "https://json-schema.org/draft/2020-12/schema",
title: "mise rc config",
description:
"Early initialization settings for mise. These settings are loaded before the main config files.",
type: "object",
unevaluatedProperties: false,
properties: misercSettings,
};
writeFormattedJson("schema/miserc.json", misercSchema);
formatWithOxfmt(writtenPaths);