@ModuleInfo { minPklVersion = "0.27.2" }
module hk.Config
import "pkl:base" as base
min_hk_version = "{{version | truncate(length=1)}}.0.0"
typealias StashMethod = Boolean | "git" | "patch-file" | "none"
// NB: Re-export Regex in case users use Config.Regex
@Deprecated {
since = "1.27.1"
message = "Replace `Types.Regex` with `Regex` (pkl built-in)"
replaceWith = "Regex"
}
const function Regex(s: String): Regex = base.Regex(s)
class Script {
linux: String?
macos: String?
windows: String?
other: String?
}
/// Default: auto-detected
///
/// Specifies the preferred default branch to compare against when hk needs a reference
/// (e.g., suggestions in pre-commit warnings).
/// If unset or empty, hk attempts to detect it via `origin/HEAD`, the current branch's remote,
/// or falls back to `main`/`master` if they exist on the remote.
///
/// Examples:
///
/// ```pkl
/// // Use a local branch name
/// default_branch = "main"
///
/// // Or a remote-qualified ref
/// default_branch = "origin/main"
/// ```
///
/// Notes:
///
/// - Both local branch names (e.g., `main`) and remote-qualified refs (e.g., `origin/main`) are supported.
/// - If omitted, hk will detect the default branch based on your repository's remotes and branches.
default_branch: String?
/// Environment variables can be set in hk.pkl for configuring the linters.
///
/// ```pkl
/// env {
/// ["NODE_ENV"] = "production"
/// }
/// ```
env: Mapping<String, String> = new Mapping<String, String> {}
/// Default: `(empty)`
///
/// Global exclude patterns that apply to all hooks and steps.
/// Files matching these patterns will be skipped from processing.
/// Supports directory names, glob patterns, and regex patterns.
///
/// ```pkl
/// // Exclude specific directories
/// exclude = List("node_modules", "dist", "build")
///
/// // Exclude using glob patterns
/// exclude = List("**/*.min.js", "**/*.map", "**/vendor/**")
///
/// // Single pattern
/// exclude = "node_modules"
///
/// // Exclude using regex pattern (for complex matching)
/// exclude = Regex(#".*\.(test|spec)\.(js|ts)$"#)
/// ```
///
/// Notes:
///
/// - Patterns from all configuration sources are unioned together
/// - Simple directory names automatically match their contents
/// (e.g., `"excluded"` matches `excluded/*` and `excluded/**`)
/// - Can be overridden per-step with `<STEP>.exclude`
/// - Regex patterns use Rust regex syntax and match against full file paths
exclude: (String | List<String> | Regex)?
/// Default: `true`
///
/// Controls whether hk aborts remaining steps/groups after the first failure.
///
/// - When `true`, as soon as a step fails, hk cancels pending steps in the same hook and returns the error.
/// - When `false`, hk continues running other steps and reports all failures at the end.
fail_fast: Boolean?
/// Controls which skip reasons are displayed in the output
/// By default, only profile-not-enabled messages are displayed
/// Possible values:
/// - "profile-not-enabled"
/// - "profile-explicitly-disabled"
/// - "no-command-for-run-type"
/// - "no-files-to-process"
/// - "condition-false"
/// - "disabled-by-env"
/// - "disabled-by-cli"
/// - "missing-required-env"
display_skip_reasons: List<String> = List("profile-not-enabled")
/// Which warning categories to show. Empty by default (no warnings shown).
/// Available tags include: "missing-profiles"
warnings: List<String> = List()
/// Warning tags to suppress. Allows hiding specific warning messages that you don't want to see.
///
/// Available warning tags:
/// - `missing-profiles`: Suppresses warnings about steps being skipped due to missing profiles
///
/// Example: `hide_warnings = List("missing-profiles")`
///
/// All hide configurations from different sources are **unioned** together.
hide_warnings: List<String> = List()
/// The number of parallel processes that hk will use to execute steps concurrently.
/// This affects performance by controlling how many linting/formatting tasks can run simultaneously.
///
/// Set to `0` (default) to auto-detect based on CPU cores.
jobs: UInt?
/// Profiles to enable or disable. Profiles allow you to group steps that should run only in certain contexts (e.g., CI, slow tests).
///
/// Prefix with `!` to explicitly disable a profile.
///
/// Example usage:
/// ```pkl
/// profiles = List("ci", "slow")
/// profiles = List("!slow") // explicitly disable
/// ```
profiles: List<String> = List()
/// A list of hook names to skip entirely. This allows you to disable specific git hooks from running.
///
/// For example: `skip_hooks = List("pre-commit", "pre-push")` would skip running those hooks completely.
///
/// This is useful when you want to temporarily disable certain hooks while still keeping them configured in your `hk.pkl` file.
/// Unlike `skip_steps` which skips individual steps, this skips the entire hook and all its steps.
///
/// **All skip configurations from different sources are unioned together.**
skip_hooks: List<String> = List()
/// A list of step names to skip when running hooks. This allows you to bypass specific linting or formatting tasks.
///
/// For example: `skip_steps = List("lint", "test")` would skip any steps named "lint" or "test".
///
/// **All skip configurations from different sources are unioned together.**
skip_steps: List<String> = List()
/// When specified, overrides the [hook's `stage` key](https://hk.jdx.dev/configuration.html#hooks-hook-stage-boolean).
///
/// This is useful when you want to manually review changes made by auto-fixers before including them in your commit.
stage: Boolean?
/// Number of backup patch files to keep per repository when using git stash.
///
/// Each time git stash is used, hk creates a backup patch file in
/// `$HK_STATE_DIR/patches/`. This setting controls how many of these
/// backups are retained per repository (oldest are automatically deleted).
///
/// Set to 0 to disable patch backup creation entirely.
///
/// Default: 20
stash_backup_count: UInt?
/// Enables or disables reporting progress via OSC sequences to compatible terminals.
terminal_progress: Boolean?
/// Controls whether hk respects .gitignore and other ignore files when walking directories.
///
/// Default: true
walk_ignore: Boolean?
/// Hooks define when and how linters are run. See [hooks](https://hk.jdx.dev/hooks) for more information.
hooks: Mapping<String, Hook> = new Mapping<String, Hook> {}
class Hook {
/// Default: `false` (`true` for `fix` hook)
///
/// If true, hk will run the fix command for each step (if it exists) to make modifications.
fix: Boolean?
/// Default: `true`
///
/// If true, hk will automatically stage fixed files after `fix` commands run.
///
/// This can be overridden via the `stage` [configuration setting](https://hk.jdx.dev/configuration.html#stage).
/// Note that this means the value of `stage` in your `hk.pkl` takes precendence.
stage: Boolean?
/// Default: `"none"`
///
/// - `"git"`: Use `git stash` to stash unstaged changes before running fix steps.
/// - `"patch-file"`: Alias of `git` behavior for now.
/// - `"none"`: Do not stash unstaged changes before running fix steps.
/// - `true` (boolean): Alias of `"git"`.
/// - `false` (boolean): Alias of `"none"`.
///
/// Examples:
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// fix = true
/// stash = true // boolean shorthand for git
/// steps = linters
/// }
///
/// ["fix"] {
/// fix = true
/// stash = "none" // disable stashing
/// steps = linters
/// }
/// }
/// ```
stash: StashMethod?
/// Environment variables specific to this hook.
/// These are merged into each step's environment variables, with step-level env taking precedence.
///
/// ```pkl
/// hooks {
/// ["pre-push"] {
/// env {
/// ["HK_PROFILES"] = "types"
/// }
/// steps = linters
/// }
/// }
/// ```
env: Mapping<String, String> = new Mapping<String, String> {}
/// Default: `false`
///
/// If true, the hook will fail when fix commands modify files.
/// This is useful with `stage = false` in pre-commit hooks to apply fixes
/// but block the commit so you can review the changes before staging manually.
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// fix = true
/// stage = false
/// fail_on_fix = true
/// steps = linters
/// }
/// }
/// ```
fail_on_fix: Boolean = false
/// Command to run after the hook completes. Receives timing JSON in `HK_REPORT_JSON`.
report: (String | Script)?
/// Steps are the individual linters that make up a hook.
/// They are executed in the order they are defined in parallel up to
/// [`HK_JOBS`](https://hk.jdx.dev//configuration#hk-jobs) at a time.
steps: Mapping<String, Step | Group> = new Mapping<String, Step> {}
}
class Step {
/// List of environment variables that must be set for this step to run.
/// A variable is considered satisfied if it is present in the process environment,
/// the global `env` block in hk.pkl, or the step's own `env` block.
/// If any are missing, the step will be skipped with a clear message.
required: List<String> = List()
/// Files the step should run on. By default this will only run this step if
/// at least 1 staged file matches the glob or regex patterns.
/// If no patterns are provided, the step will always run.
///
/// ```pkl
/// // Glob patterns
/// ["prettier"] {
/// glob = List("*.js", "*.ts")
/// check = "prettier --check {{files}}"
/// }
///
/// // Single glob pattern
/// ["eslint"] {
/// glob = "*.js"
/// check = "eslint {{files}}"
/// }
///
/// // Regex pattern for complex matching
/// ["config-lint"] {
/// glob = Types.Regex(#"^(config|settings).*\.(json|yaml|yml)$"#)
/// check = "config-lint {{files}}"
/// }
/// ```
glob: (String | List<String> | Regex)?
/// Default: `(none)`
///
/// Filter files by their type rather than just glob patterns.
/// Matches files by extension, shebang, or content detection
/// (OR logic - file must match ANY of the specified types).
/// This is particularly useful for matching scripts without file extensions.
///
/// ```pkl
/// // Match Python files by extension AND shebang (including extensionless scripts)
/// ["black"] {
/// types = List("python")
/// fix = "black {{files}}"
/// }
///
/// // Match shell scripts by extension or shebang
/// ["shellcheck"] {
/// types = List("shell")
/// check = "shellcheck {{files}}"
/// }
///
/// // Match multiple types (OR logic)
/// ["format-scripts"] {
/// types = List("python", "shell", "ruby")
/// fix = "format-script {{files}}"
/// }
///
/// // Combine types with glob patterns for more precise filtering
/// ["format-src-python"] {
/// glob = "src/**/*" // Only files in src/
/// types = List("python") // That are Python files
/// fix = "black {{files}}"
/// }
/// ```
///
/// **Supported types include:**
///
/// - **Languages:** `python`, `javascript`, `typescript`, `ruby`, `go`, `rust`, `java`, `kotlin`, `swift`, `c`, `c++`, `csharp`, `php`, "lua"
/// - **Shells:** `shell`, `bash`, `zsh`, `fish`, `sh`
/// - **Data formats:** `json`, `yaml`, `toml`, `xml`, `csv`, 'pkl'
/// - **Markup:** `html`, `markdown`, `css`, 'asciidoc'
/// - **Special:** `text`, `binary`, `executable`, `symlink`, `dockerfile`
/// - **Images:** `image`, `png`, `jpeg`, `gif`, `svg`, `webp`
/// - **Archives:** `archive`, `zip`, `tar`, `gzip`
///
/// Types are detected using:
/// 1. File extension (e.g., `.py` → `python`)
/// 2. Shebang line (e.g., `#!/usr/bin/env python3` → `python`)
/// 3. Special filenames (e.g., `Dockerfile` → `dockerfile`)
/// 4. Content/magic number detection for binary files
types: List<String>?
/// Whether to include symbolic links (default: false)
allow_symlinks: Boolean = false
/// A command to run that does not modify files.
/// This typically is a "check" command like `eslint` or `prettier --check`
/// that returns a non-zero exit code if there are errors.
/// Parallelization works better with check commands than fix commands as no files are being modified.
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// ["prettier"] {
/// check = "prettier --check {{files}}"
/// }
/// }
/// }
/// ```
///
/// If you want to use a different check command for different operating systems,
/// you can define a Script instead of a String:
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// ["prettier"] {
/// check = new Script {
/// linux = "prettier --check {{files}}"
/// macos = "prettier --check {{files}}"
/// windows = "prettier --check {{files}}"
/// other = "prettier --check {{files}}"
/// }
/// }
/// }
/// }
/// ```
///
/// Template variables:
///
/// - `{{files}}`: A list of files to run the linter on.
/// - `{{workspace}}`: When `workspace_indicator` is set and matched, this is the workspace directory path
/// (e.g., `.` for the repo root or `packages/app`).
/// - `{{workspace_indicator}}`: Full path to the matched workspace indicator file
/// (e.g., `packages/app/package.json`).
/// - `{{workspace_files}}`: A list of files relative to `{{workspace}}`.
check: (String | Script)?
/// A command that returns a list of files that need fixing.
/// This is used to optimize the fix step when `check_first` is enabled.
/// Instead of running the fix command on all files, it will only run on files that need fixing.
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// ["prettier"] {
/// check_list_files = "prettier --list-different {{files}}"
/// }
/// }
/// }
/// ```
check_list_files: (String | Script)?
/// A command that shows the diff of what would be changed.
/// This is an alternative to `check` that can provide more detailed information about what would be changed.
///
/// When defined, hk will attempt to apply the diff output directly using `git apply`
/// instead of running the fix command. This is more efficient for tools that
/// produce standard unified diffs (black, ruff, shfmt, etc.).
///
/// Falls back to running the fix command if patch application fails.
check_diff: (String | Script)?
/// A command to run that modifies files.
/// This typically is a "fix" command like `eslint --fix` or `prettier --write`.
/// Templates variables are the same as for `check`.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["prettier"] {
/// fix = "prettier --write {{files}}"
/// }
/// }
/// ```
///
/// By default, hk will use `fix` commands but this can be overridden by setting
/// [`HK_FIX=0`](https://hk.jdx.dev/configuration#hk-fix) or running `hk run <HOOK> --check`.
fix: (String | Script)?
/// If true, hk will run the check step first and only run the fix step if the check step fails.
check_first: Boolean = true
/// Default: `false`
///
/// If true, hk will run the linter on batches of files instead of all files at once.
/// This takes advantage of parallel processing for otherwise single-threaded linters like eslint and prettier.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["eslint"] {
/// batch = true
/// }
/// }
/// ```
batch: Boolean = false
/// Default: `false`
///
/// If true, hk will get a write lock instead of a read lock when running fix/fix_all.
/// Use this if the tool has its own locking mechanism or you simply don't care
/// if files may be written to by multiple linters simultaneously.
stomp: Boolean = false
/// If set, run the linter on workspaces only which are parent directories containing this filename.
/// This is useful for tools that need to be run from a specific directory, like a project root.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["cargo-clippy"] {
/// glob = "*.rs"
/// workspace_indicator = "Cargo.toml"
/// check = "cargo clippy --manifest-path {{workspace_indicator}}"
/// }
/// }
/// ```
///
/// In this example, given a file list like the following:
///
/// ```text
/// └── workspaces/
/// ├── proj1/
/// │ ├── Cargo.toml
/// │ └── src/
/// │ ├── lib.rs
/// │ └── main.rs
/// └── proj2/
/// ├── Cargo.toml
/// └── src/
/// ├── lib.rs
/// └── main.rs
/// ```
///
/// hk will run 1 step for each workspace even though multiple rs files are in each workspace:
///
/// - `cargo clippy --manifest-path workspaces/proj1/Cargo.toml`
/// - `cargo clippy --manifest-path workspaces/proj2/Cargo.toml`
///
/// When `workspace_indicator` is used, the following template variables become available in commands and env:
///
/// - `{{workspace}}`: the workspace directory path
/// - `{{workspace_indicator}}`: the matched indicator file path
/// - `{{workspace_files}}`: files relative to `{{workspace}}`
///
/// For example, in a monorepo with Node packages:
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["npm-lint"] {
/// glob = List("*.js", "*.jsx", "*.ts", "*.tsx")
/// workspace_indicator = "package.json"
/// check = "echo cd {{workspace}} && npm run lint -- {{workspace_files}}"
/// fix = "echo cd {{workspace}} && npm run fix -- {{workspace_files}}"
/// }
/// }
/// ```
workspace_indicator: String?
/// If set, run the linter scripts with this prefix, e.g.: "mise exec --" or "npm run".
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["eslint"] {
/// prefix = "npm run"
/// }
/// }
/// ```
prefix: String?
/// If set, run the linter scripts in this directory.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["eslint"] = (Builtins.eslint) {
/// dir = "frontend"
/// }
/// }
/// ```
dir: String?
/// Profiles are a way to enable/disable linters based on the current profile.
/// The linter will only run if its profile is in [`HK_PROFILE`](https://hk.jdx.dev/configuration#hk-profile).
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["prettier"] = (Builtins.prettier) {
/// profiles = List("slow")
/// }
/// }
/// ```
///
/// Profiles can be prefixed with `!` to disable them.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["prettier"] = (Builtins.prettier) {
/// profiles = List("!slow")
/// }
/// }
/// ```
profiles: List<String>?
/// A list of steps that must finish before this step can run.
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// steps {
/// ["prettier"] {
/// depends = List("eslint")
/// }
/// }
/// }
/// }
/// ```
depends: (String | List<String>) = List()
/// If set, use this shell instead of the default `sh -o errexit -c`.
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// steps {
/// ["prettier"] {
/// shell = "bash -o errexit -c"
/// }
/// }
/// }
/// }
/// ```
shell: (String | Script)?
/// A list of globs of files to add to the git index after running a fix step.
///
/// When staging is enabled and the step has a `fix` command, this defaults to the step's
/// glob. Set explicitly if your fix modifies different files than those it reads.
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// steps {
/// ["prettier"] {
/// stage = List("*.js", "*.ts")
/// }
/// }
/// }
/// }
/// ```
stage: (String | List<String>)?
/// Default: `false`
///
/// If true, this step will wait for any previous steps to finish before running.
/// No other steps will start until this one finishes.
/// Under the hood this groups the previous steps into a group.
///
/// ```pkl
/// hooks {
/// ["pre-commit"] {
/// steps {
/// ["prelint"] {
/// exclusive = true // blocks other steps from starting until this one finishes
/// check = "mise run prelint"
/// }
/// // ... other steps will run in parallel ...
/// ["postlint"] {
/// exclusive = true // wait for all previous steps to finish before starting
/// check = "mise run postlint"
/// }
/// }
/// }
/// }
/// ```
exclusive: Boolean = false
/// Files to exclude from the step. Supports glob patterns and regex patterns.
/// Files matching these patterns will be skipped.
///
/// ```pkl
/// // Exclude with glob patterns
/// ["prettier"] {
/// glob = List("**/*.yaml")
/// exclude = List("*.test.yaml", "*.fixture.yaml")
/// check = "prettier --check {{files}}"
/// }
///
/// // Exclude with regex pattern for complex matching
/// ["linter"] {
/// glob = List("**/*")
/// exclude = Types.Regex(#"""
/// (?x)
/// ^(vendor|dist|build)/.*$|
/// .*\.(min|bundle)\.(js|css)$|
/// .*\.generated\.(ts|js)$
/// """#)
/// check = "custom-lint {{files}}"
/// }
/// ```
///
/// Notes:
/// - Regex patterns use Rust regex syntax
/// - The `(?x)` flag enables verbose mode for multi-line patterns with comments
/// - Use raw strings (`#"..."#` or `#"""..."""#`) to avoid escaping backslashes
exclude: (String | List<String> | Regex) = List()
/// Default: `false`
///
/// If true, connects stdin/stdout/stderr to hk's execution.
/// This implies `exclusive = true`.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["show-warning"] {
/// interactive = true
/// check = "echo warning && read -p 'Press Enter to continue'"
/// }
/// }
/// ```
interactive: Boolean = false
/// If set, sends the template-expanded string to the command's stdin (mutually exclusive with `interactive`).
/// Supports variable `file_list` (`list[str]`) and Tera builtins: https://keats.github.io/tera/docs/#built-ins.
///
/// This is useful for tools which allow passing filenames via stdin (like xargs), to avoid hk's automatic batching.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["hungry-command"] {
/// stdin = " {{ files_list | join(sep='\n') }}"
/// check = "xargs my-command"
/// }
/// }
/// ```
stdin: String?
/// If set, the jobs in this step will only run if this condition evaluates to true.
/// This is evaluated per step-job (e.g. multiple times if hk batches, or if there are multiple workspaces)
/// Evaluated with [`expr`](https://github.com/jdx/expr-rs).
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["prettier"] {
/// condition = "exec('test -f check.js')"
/// }
/// }
/// ```
condition: String?
/// If set, the step will only run if this condition evaluates to true.
/// Evaluated once (regardless of batching or workspaces).
/// Evaluated with [`expr`](https://github.com/jdx/expr-rs).
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["prettier"] {
/// condition = "exec('test -f check.js')"
/// }
/// }
/// ```
step_condition: String?
/// Default: `false`
///
/// If true, the step will be hidden from output.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["prettier"] {
/// hide = true
/// }
/// }
/// ```
hide: Boolean = false
/// Default: `"stderr"`
///
/// Controls which stream(s) from the step’s command are captured and printed
/// at the end of the hook run. This prints a single consolidated block per step
/// that produced any output, with a header like `STEP_NAME stderr:`.
///
/// - `"stderr"` (default): capture only standard error
/// - `"stdout"`: capture only standard output
/// - `"combined"`: capture both stdout and stderr interleaved (line-by-line as produced)
/// - `"hide"`: capture nothing and print nothing for this step
///
/// Examples:
///
/// ```pkl
/// hooks {
/// ["check"] {
/// steps {
/// ["lint"] {
/// check = "eslint {{files}}"
/// output_summary = "combined"
/// }
/// ["format"] {
/// check = "prettier --check {{files}}"
/// output_summary = "stdout"
/// }
/// ["quiet-step"] {
/// check = "echo noisy && echo warn 1>&2"
/// output_summary = "hide"
/// }
/// }
/// }
/// }
/// ```
output_summary: "stdout" | "stderr" | "combined" | "hide" = "stderr"
/// Environment variables specific to this step.
/// These are merged with the global environment variables.
///
/// ```pkl
/// local linters = new Mapping<String, Step> {
/// ["prettier"] {
/// env {
/// ["NODE_ENV"] = "production"
/// }
/// }
/// }
/// ```
env: Mapping<String, String> = new Mapping<String, String> {}
/// Define self-contained tests for a step, runnable via `hk test`.
///
/// Key points:
///
/// - Mapping is keyed by test name.
/// - Supported run modes: `check` or `fix` (defaults to `check`).
/// - `files` is optional; if omitted, it defaults to the keys of `write`.
/// - `write` lets you create files before the test runs (paths can be relative to the sandbox or absolute).
/// - `fixture` copies a directory into a temporary sandbox before the test runs.
/// - `env` merges with the step’s `env` (test env wins on conflicts).
/// - `before` is an optional shell command to run before the test's main command.
/// If it fails (non-zero exit), the test fails immediately.
/// - `after` is an optional shell command to run after the main command, before evaluating expectations.
/// If it fails, the test fails and reports that failure.
/// - `expect` supports:
/// - `code` (default 0)
/// - `stdout`, `stderr` substring checks
/// - `files` full-file content assertions
///
/// Template variables available in tests are the same as for steps, plus:
///
/// - `{{files}}`, `{{globs}}`, `{{workspace}}`, `{{workspace_indicator}}`
/// - `{{root}}`: project root
/// - `{{tmp}}`: sandbox path used to execute the test
///
/// Example:
///
/// ```pkl
/// hooks {
/// ["check"] {
/// steps {
/// ["prettier"] {
/// check = "prettier --check {{ files }}"
/// fix = "prettier --write {{ files }}"
/// tests {
/// ["formats json via fix"] {
/// run = "fix"
/// write { ["{{tmp}}/a.json"] = "{\"b\":1}" }
/// // files omitted -> defaults to write keys
/// expect { files { ["{{tmp}}/a.json"] = "{\n \"b\": 1\n}\n" } }
/// }
/// ["check shows output"] {
/// run = "check"
/// files = List("{{tmp}}/a.json")
/// env { ["FOO"] = "bar" }
/// expect { stdout = "prettier" }
/// }
/// ["before generates file, after verifies contents"] {
/// run = "fix"
/// // before: generate an input file the step will process
/// before = #"printf '{\"b\":1}' > {{tmp}}/raw.json"#
/// // files: tell hk which file the step should operate on
/// files = List("{{tmp}}/raw.json")
/// // after: verify the contents using a shell assertion
/// after = #"grep -q '\"b\": 1' {{tmp}}/raw.json"#
/// // expect: full-file match after formatting
/// expect { files { ["{{tmp}}/raw.json"] = "{\n \"b\": 1\n}\n" } }
/// }
/// }
/// }
/// }
/// }
/// }
/// ```
///
/// Run tests with:
///
/// ```bash
/// hk test # all tests
/// hk test --step prettier # only prettier's tests
/// hk test --name formats json via fix
/// hk test --list # list without running
/// ```
tests: Mapping<String, StepTest> = new Mapping<String, StepTest> {}
}
class StepTest {
/// What to run: "check", "fix", or "command"
run: "check" | "fix" = "check"
/// Files to pass to the command (used to render {{ files }})
files: (String | List<String>)?
/// Optional path to copy into a temp sandbox before running
fixture: String?
/// Inline files to write into the sandbox
write: Mapping<String, String> = new Mapping<String, String> {}
/// Command to run before executing the test command
before: String?
/// Extra environment variables for this test
env: Mapping<String, String> = new Mapping<String, String> {}
/// Command to run after the main command, before expectations (cleanup or extra verification)
after: String?
/// Whether to run in a temporary directory sandbox
/// If true, always use a sandbox; if false, always use repo root
/// If not specified, auto-detects based on whether files reference {{tmp}}
tmpdir: Boolean?
/// Expectations for the test result
expect: StepTestExpect = new StepTestExpect {}
}
class StepTestExpect {
/// Expected exit code (default 0)
code = 0
/// Substring that must appear in stdout
stdout: String?
/// Substring that must appear in stderr
stderr: String?
/// Expected files after run: path => full contents (exact match)
files: Mapping<String, String> = new Mapping<String, String> {}
}
class Group {
steps: Mapping<String, Step> = new Mapping<String, Step> {}
}
output {
renderer {
converters {
[Regex] = (r) -> new Mapping {
["_type"] = "regex"
["pattern"] = r.pattern
}
[Group] = (g) -> new Dynamic {
_type = "group"
...g.toDynamic()
}
[Step] = (s) -> new Dynamic {
_type = "step"
...s
.toMap()
.mapValues((k, v) ->
if ((k == "depends" || k == "stage") && v is String)
List(v) // permits "s" instead of List("s")
else if (k == "stash" && v is Boolean)
if (v) "git" else "none"
else
v
)
.toDynamic()
}
}
}
}