├── CRUSH.md
├── src
├── hk-extras.usage.kdl
├── ui
│ ├── mod.rs
│ └── style.rs
├── cli
│ ├── fix.rs
│ ├── check.rs
│ ├── version.rs
│ ├── cache
│ │ ├── clear.rs
│ │ └── mod.rs
│ ├── run
│ │ ├── pre_commit.rs
│ │ ├── commit_msg.rs
│ │ ├── prepare_commit_msg.rs
│ │ └── mod.rs
│ ├── builtins.rs
│ ├── validate.rs
│ ├── usage.rs
│ ├── completion.rs
│ ├── uninstall.rs
│ └── util
│ │ └── no_commit_to_branch.rs
├── error.rs
├── step_locks.rs
├── hash.rs
├── version.rs
├── step_depends.rs
├── tests
│ └── config_unification.rs
└── step_test.rs
├── .cargo
└── config.toml
├── docs
├── index.md
├── public
│ ├── logo.png
│ ├── favicon.ico
│ ├── hk-demo.gif
│ ├── benchmark.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── about.txt
│ ├── site.webmanifest
│ ├── javascript-project.pkl
│ └── python-project.pkl
├── .gitignore
├── cli
│ ├── validate.md
│ ├── version.md
│ ├── cache
│ │ └── clear.md
│ ├── builtins.md
│ ├── uninstall.md
│ ├── completion.md
│ ├── util
│ │ ├── check-symlinks.md
│ │ ├── detect-private-key.md
│ │ ├── python-check-ast.md
│ │ ├── fix-smart-quotes.md
│ │ ├── python-debug-statements.md
│ │ ├── check-byte-order-marker.md
│ │ ├── fix-byte-order-marker.md
│ │ ├── check-case-conflict.md
│ │ ├── check-executables-have-shebangs.md
│ │ ├── no-commit-to-branch.md
│ │ ├── mixed-line-ending.md
│ │ ├── end-of-file-fixer.md
│ │ ├── check-merge-conflict.md
│ │ ├── trailing-whitespace.md
│ │ ├── check-added-large-files.md
│ │ └── check-conventional-commit.md
│ ├── migrate.md
│ ├── config
│ │ ├── sources.md
│ │ ├── explain.md
│ │ ├── dump.md
│ │ └── get.md
│ ├── test.md
│ ├── init.md
│ ├── install.md
│ ├── migrate
│ │ └── pre-commit.md
│ ├── config.md
│ ├── util.md
│ ├── fix.md
│ ├── check.md
│ ├── run
│ │ ├── pre-commit.md
│ │ ├── pre-push.md
│ │ ├── commit-msg.md
│ │ └── prepare-commit-msg.md
│ └── run.md
├── eslint.config.mjs
├── reference
│ └── examples
│ │ ├── index.md
│ │ └── javascript-project.md
├── package.json
├── .vitepress
│ └── theme
│ │ └── index.ts
└── about.md
├── .prettierignore
├── pkl
├── PklProject.deps.json
├── builtins
│ ├── go_sec.pkl
│ ├── go_vet.pkl
│ ├── reek.pkl
│ ├── revive.pkl
│ ├── pylint.pkl
│ ├── sorbet.pkl
│ ├── err_check.pkl
│ ├── luacheck.pkl
│ ├── erb.pkl
│ ├── fasterer.pkl
│ ├── flake8.pkl
│ ├── staticcheck.pkl
│ ├── xmllint.pkl
│ ├── go_vuln_check.pkl
│ ├── astro.pkl
│ ├── brakeman.pkl
│ ├── mypy.pkl
│ ├── tf_lint.pkl
│ ├── jq.pkl
│ ├── php_cs.pkl
│ ├── deno_check.pkl
│ ├── nix_fmt.pkl
│ ├── go_fumpt.pkl
│ ├── gomod_tidy.pkl
│ ├── rubocop.pkl
│ ├── tsserver.pkl
│ ├── go_lines.pkl
│ ├── isort.pkl
│ ├── stylua.pkl
│ ├── go_imports.pkl
│ ├── mix_test.pkl
│ ├── alejandra.pkl
│ ├── standard_rb.pkl
│ ├── mix_compile.pkl
│ ├── nixpkgs_format.pkl
│ ├── sql_fluff.pkl
│ ├── bundle_audit.pkl
│ ├── check_conventional_commit.pkl
│ ├── actionlint.pkl
│ ├── cpp_lint.pkl
│ ├── cargo_check.pkl
│ ├── golangci_lint.pkl
│ ├── markdown_lint.pkl
│ ├── deno.pkl
│ ├── tofu.pkl
│ ├── ox_lint.pkl
│ ├── sort_package_json.pkl
│ ├── xo.pkl
│ ├── mix_fmt.pkl
│ ├── standard_js.pkl
│ ├── terraform.pkl
│ ├── biome.pkl
│ ├── eslint.pkl
│ ├── lychee.pkl
│ ├── cargo_fmt.pkl
│ ├── go_fmt.pkl
│ ├── clang_format.pkl
│ ├── dprint.pkl
│ ├── rustfmt.pkl
│ ├── typos.pkl
│ ├── vacuum.pkl
│ ├── pkl.pkl
│ ├── yamllint.pkl
│ ├── hadolint.pkl
│ ├── shfmt.pkl
│ ├── cargo_clippy.pkl
│ ├── check_merge_conflict.pkl
│ ├── taplo.pkl
│ ├── shellcheck.pkl
│ ├── fix_byte_order_marker.pkl
│ ├── fix_smart_quotes.pkl
│ ├── no_commit_to_branch.pkl
│ ├── newlines.pkl
│ ├── tombi.pkl
│ ├── check_added_large_files.pkl
│ ├── tsc.pkl
│ ├── check_case_conflict.pkl
│ ├── trailing_whitespace.pkl
│ ├── black.pkl
│ ├── python_check_ast.pkl
│ ├── check_byte_order_marker.pkl
│ ├── yq.pkl
│ ├── swiftlint.pkl
│ ├── taplo_format.pkl
│ ├── yamlfmt.pkl
│ ├── mixed_line_ending.pkl
│ ├── check_symlinks.pkl
│ ├── ktlint.pkl
│ ├── tombi_format.pkl
│ ├── detect_private_key.pkl
│ ├── check_executables_have_shebangs.pkl
│ ├── python_debug_statements.pkl
│ ├── mise.pkl
│ ├── ruff_format.pkl
│ ├── ruff.pkl
│ ├── pkl_format.pkl
│ ├── prettier.pkl
│ └── stylelint.pkl
├── Types.pkl
├── PklProject
└── UserConfig.pkl
├── mise-tasks
├── package-pkl.sh
└── update-version.sh
├── hk.code-workspace
├── test
├── builtin_tool_stubs
│ ├── tombi
│ ├── ktlint
│ ├── black
│ ├── yq
│ ├── ruff
│ ├── stylelint
│ ├── swiftlint
│ ├── yamllint
│ ├── yamlfmt
│ ├── hadolint
│ └── shellcheck
├── data
│ ├── unpretty.js
│ ├── eslint.config.mjs
│ └── package.json
├── init_creates_hk_pkl.bats
├── version.bats
├── install_creates_git_hooks.bats
├── validate.bats
├── arg_escape.bats
├── builtins_tests.bats
├── prepare_commit_msg.bats
├── hk_test_failure.bats
├── skip_step_flag.bats
├── builtin_json.bats
├── hk_pkl_http_proxy.bats
├── builtin_json_format.bats
├── hk_test_env.bats
├── run_pre_commit_all.bats
├── commit_msg.bats
├── condition.bats
├── check_fix_suggestion_fix_mode.bats
├── git_runs_pre_commit_on_staged_files.bats
├── skip_hook.bats
├── uninstall.bats
├── hk_test_project_paths.bats
├── skip_steps.bats
├── hk_test_files_default.bats
├── builtins.bats
├── skip_missing_run_cmd.bats
├── config_pkl_imports.bats
├── pre_push.bats
├── depends_condition_false.bats
├── dir.bats
├── localconfig.bats
├── untracked_all.bats
├── hook_fix_default.bats
├── check_first_waits.bats
├── hk_test_sandboxing.bats
├── stage_generated_files.bats
├── fail_fast_config.bats
├── stash_default.bats
├── test_helper
│ ├── cache_setup.bash
│ └── common_setup.bash
├── workspace_indicator.bats
├── git_status_ad_deleted.bats
├── depends.bats
├── fix_from_ref_to_ref.bats
├── skip_reason_precedence.bats
├── stash_prefers_unstaged_over_fixes.bats
├── pre_commit_does_not_stash_staged_only_files.bats
├── newline_stripping_bug.bats
└── skip_output.bats
├── .github
├── renovate.json
└── workflows
│ ├── semantic-pr-lint.yml
│ └── release-plz.yml
├── .gitignore
├── .vscode
└── settings.json
├── bin
└── hk
├── eslint.config.mjs
├── README.md
├── benchmark
├── lefthook.yml
├── hk.pkl
└── .pre-commit-config.yaml
├── tapes
└── hk-demo.tape
├── .taplo.toml
├── .gitmodules
├── package.json
├── CONTRIBUTING.md
├── flake.nix
├── default.nix
├── LICENSE
├── settings-schema.json
├── scripts
├── reflect.pkl
└── generate-examples.sh
└── flake.lock
/CRUSH.md:
--------------------------------------------------------------------------------
1 | ./CLAUDE.md
--------------------------------------------------------------------------------
/src/hk-extras.usage.kdl:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod style;
2 |
--------------------------------------------------------------------------------
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [patch.crates-io]
2 | clx = { path = "clx" }
3 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | ---
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/logo.png
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | docs/.vitepress
2 | test/bats
3 | test/data
4 | test/test_helper
5 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | .vitepress/.temp
2 | .vitepress/cache
3 | .vitepress/dist
4 | gen
5 |
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/favicon.ico
--------------------------------------------------------------------------------
/docs/public/hk-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/hk-demo.gif
--------------------------------------------------------------------------------
/docs/public/benchmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/benchmark.png
--------------------------------------------------------------------------------
/pkl/PklProject.deps.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": 1,
3 | "resolvedDependencies": {}
4 | }
5 |
--------------------------------------------------------------------------------
/docs/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/mise-tasks/package-pkl.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euxo pipefail
3 |
4 | pkl project package pkl
5 |
--------------------------------------------------------------------------------
/hk.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {}
8 | }
9 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/tombi:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "0.7.4"
4 | tool = "tombi"
5 |
--------------------------------------------------------------------------------
/docs/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jdx/hk/HEAD/docs/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/ktlint:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "1.7.1"
4 | tool = "ktlint"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/black:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "25.11.0"
4 | tool = "pipx:black"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/yq:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "4.49.2"
4 | tool = "aqua:mikefarah/yq"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/ruff:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "0.13.3"
4 | tool = "aqua:astral-sh/ruff"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/stylelint:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "16.23.1"
4 | tool = "npm:stylelint"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/swiftlint:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "0.59.1"
4 | tool = "asdf:swiftlint"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/yamllint:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "1.37.1"
4 | tool = "pipx:yamllint"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/yamlfmt:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "0.20.0"
4 | tool = "aqua:google/yamlfmt"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/hadolint:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "2.12.1-beta"
4 | tool = "aqua:hadolint/hadolint"
5 |
--------------------------------------------------------------------------------
/test/builtin_tool_stubs/shellcheck:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S mise tool-stub
2 |
3 | version = "0.11.0"
4 | tool = "ubi:koalaman/shellcheck"
5 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["local>jdx/renovate-config"]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /tmp
3 | node_modules
4 | .aider*
5 | .pkl-lsp
6 | /.pre-commit-config.yaml
7 | /lefthook.yml
8 | .hkrc.pkl
9 | *.log
10 | !/build
11 |
--------------------------------------------------------------------------------
/docs/cli/validate.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk validate`
4 |
5 | - **Usage**: `hk validate`
6 |
7 | Validate the config file
8 |
--------------------------------------------------------------------------------
/docs/cli/version.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk version`
4 |
5 | - **Usage**: `hk version`
6 |
7 | Print the version of hk
8 |
--------------------------------------------------------------------------------
/docs/cli/cache/clear.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk cache clear`
4 |
5 | - **Usage**: `hk cache clear`
6 |
7 | Clear the cache directory
8 |
--------------------------------------------------------------------------------
/docs/cli/builtins.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk builtins`
4 |
5 | - **Usage**: `hk builtins`
6 |
7 | Lists all available builtin linters
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "node_modules": true,
4 | "target": true,
5 | "test/bats": true,
6 | "test/test_helper/bats-*": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/bin/hk:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | script_dir=$(dirname "$0")
5 |
6 | exec cargo run --all-features --manifest-path "$script_dir/../Cargo.toml" --bin hk -- "$@"
7 |
--------------------------------------------------------------------------------
/docs/cli/uninstall.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk uninstall`
4 |
5 | - **Usage**: `hk uninstall`
6 |
7 | Removes hk hooks from the current git repository
8 |
--------------------------------------------------------------------------------
/src/cli/fix.rs:
--------------------------------------------------------------------------------
1 | use crate::hook_options::HookOptions;
2 |
3 | /// Fixes code
4 | #[derive(clap::Args)]
5 | #[clap(visible_alias = "f")]
6 | pub struct Fix {
7 | #[clap(flatten)]
8 | pub(crate) hook: HookOptions,
9 | }
10 |
--------------------------------------------------------------------------------
/src/cli/check.rs:
--------------------------------------------------------------------------------
1 | use crate::hook_options::HookOptions;
2 |
3 | /// Checks code
4 | #[derive(clap::Args)]
5 | #[clap(visible_alias = "c")]
6 | pub struct Check {
7 | #[clap(flatten)]
8 | pub(crate) hook: HookOptions,
9 | }
10 |
--------------------------------------------------------------------------------
/docs/public/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following font:
2 |
3 | - Font Title: Lato
4 | - Font Author: undefined
5 | - Font Source: https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHvxk6XweuBCY.ttf
6 | - Font License: undefined)
7 |
--------------------------------------------------------------------------------
/pkl/builtins/go_sec.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Security scanner"
7 | }
8 | go_sec = new Config.Step {
9 | glob = "**/*.go"
10 | check = "gosec {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/go_vet.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Go code vetting"
7 | }
8 | go_vet = new Config.Step {
9 | glob = "**/*.go"
10 | check = "go vet {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/reek.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Ruby"
6 | description = "Code smell detector"
7 | }
8 | reek = new Config.Step {
9 | glob = "**/*.rb"
10 | check = "reek {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/revive.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Fast Go linter"
7 | }
8 | revive = new Config.Step {
9 | glob = "**/*.go"
10 | check = "revive {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/pylint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Python"
6 | description = "Python code analysis"
7 | }
8 | pylint = new Config.Step {
9 | glob = "**/*.py"
10 | check = "pylint {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/sorbet.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Ruby"
6 | description = "Type checker for Ruby"
7 | }
8 | sorbet = new Config.Step {
9 | glob = "**/*.rb"
10 | check = "srb tc {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/err_check.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Error handling checker"
7 | }
8 | err_check = new Config.Step {
9 | glob = "**/*.go"
10 | check = "errcheck {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/luacheck.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Other Languages"
6 | description = "Lua linter"
7 | }
8 | luacheck = new Config.Step {
9 | glob = "**/*.lua"
10 | check = "luacheck {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/docs/cli/completion.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk completion`
4 |
5 | - **Usage**: `hk completion `
6 |
7 | Generates shell completion scripts
8 |
9 | ## Arguments
10 |
11 | ### ``
12 |
13 | The shell to generate completion for
14 |
--------------------------------------------------------------------------------
/docs/cli/util/check-symlinks.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util check-symlinks`
4 |
5 | - **Usage**: `hk util check-symlinks …`
6 |
7 | Check for broken symlinks
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check
14 |
--------------------------------------------------------------------------------
/docs/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
2 |
--------------------------------------------------------------------------------
/pkl/builtins/erb.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Ruby"
6 | description = "ERB template linter"
7 | }
8 | erb = new Config.Step {
9 | glob = "**/*.erb"
10 | check = "erb -P -x -T - {{ files }} | ruby -c"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/fasterer.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Ruby"
6 | description = "Performance suggestions"
7 | }
8 | fasterer = new Config.Step {
9 | glob = "**/*.rb"
10 | check = "fasterer {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/flake8.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Python"
6 | description = "Python style guide enforcement"
7 | }
8 | flake8 = new Config.Step {
9 | glob = "**/*.py"
10 | check = "flake8 {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/staticcheck.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Go static analysis"
7 | }
8 | staticcheck = new Config.Step {
9 | glob = "**/*.go"
10 | check = "staticcheck {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/xmllint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Data Formats"
6 | description = "XML validator"
7 | }
8 | xmllint = new Config.Step {
9 | glob = "**/*.xml"
10 | check = "xmllint --noout {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/docs/cli/migrate.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk migrate`
4 |
5 | - **Usage**: `hk migrate `
6 |
7 | Migrate from other hook managers to hk
8 |
9 | ## Subcommands
10 |
11 | - [`hk migrate pre-commit [FLAGS]`](/cli/migrate/pre-commit.md)
12 |
--------------------------------------------------------------------------------
/pkl/builtins/go_vuln_check.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Vulnerability scanner"
7 | }
8 | go_vuln_check = new Config.Step {
9 | glob = "**/*.go"
10 | check = "govulncheck {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/astro.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Other Languages"
6 | description = "Astro component checker"
7 | }
8 | astro = new Config.Step {
9 | glob = "**/*.astro"
10 | check = "astro check {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/docs/cli/util/detect-private-key.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util detect-private-key`
4 |
5 | - **Usage**: `hk util detect-private-key …`
6 |
7 | Detect private keys in files
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check
14 |
--------------------------------------------------------------------------------
/docs/cli/util/python-check-ast.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util python-check-ast`
4 |
5 | - **Usage**: `hk util python-check-ast …`
6 |
7 | Check Python files for valid syntax
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check
14 |
--------------------------------------------------------------------------------
/pkl/builtins/brakeman.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Ruby"
6 | description = "Security scanner for Rails"
7 | }
8 | brakeman = new Config.Step {
9 | glob = List("**/*.rb")
10 | check = "brakeman -q -w2 {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/mypy.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Python"
6 | description = "Static type checker for Python"
7 | }
8 | mypy = new Config.Step {
9 | glob = List("**/*.py", "**/*.pyi")
10 | check = "mypy {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/test/data/unpretty.js:
--------------------------------------------------------------------------------
1 | class Base {
2 | constructor() {}
3 |
4 | }
5 |
6 | class MyClass extends Base {
7 | constructor() {
8 | super();
9 | }
10 |
11 | method() {
12 | return 1;
13 | }
14 | }
15 |
16 | let mc = new MyClass()
17 | console . log ( `Hello World! ${mc.method()}` );
18 |
--------------------------------------------------------------------------------
/docs/cli/util/fix-smart-quotes.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util fix-smart-quotes`
4 |
5 | - **Usage**: `hk util fix-smart-quotes …`
6 |
7 | Replace UTF-8 smart quotes
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to replace smart quotes in
14 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | //pub use std::error::*;
2 |
3 | #[derive(Debug, thiserror::Error)]
4 | pub enum Error {
5 | #[error("check list failed: {source}")]
6 | CheckListFailed {
7 | #[source]
8 | source: eyre::Error,
9 | stdout: String,
10 | stderr: String,
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/test/init_creates_hk_pkl.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | setup() {
4 | load 'test_helper/common_setup'
5 | _common_setup
6 | }
7 |
8 | teardown() {
9 | _common_teardown
10 | }
11 |
12 | @test "hk init creates hk.pkl" {
13 | hk init
14 | assert_file_contains hk.pkl "linters ="
15 | }
16 |
--------------------------------------------------------------------------------
/docs/cli/util/python-debug-statements.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util python-debug-statements`
4 |
5 | - **Usage**: `hk util python-debug-statements …`
6 |
7 | Detect Python debug statements
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check
14 |
--------------------------------------------------------------------------------
/src/cli/version.rs:
--------------------------------------------------------------------------------
1 | use crate::Result;
2 | use crate::version;
3 |
4 | /// Print the version of hk
5 | #[derive(Debug, clap::Args)]
6 | pub struct Version {}
7 |
8 | impl Version {
9 | pub async fn run(&self) -> Result<()> {
10 | println!("{}", version::version());
11 | Ok(())
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/docs/cli/util/check-byte-order-marker.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util check-byte-order-marker`
4 |
5 | - **Usage**: `hk util check-byte-order-marker …`
6 |
7 | Check for UTF-8 byte order marker (BOM)
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check
14 |
--------------------------------------------------------------------------------
/docs/cli/util/fix-byte-order-marker.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util fix-byte-order-marker`
4 |
5 | - **Usage**: `hk util fix-byte-order-marker …`
6 |
7 | Remove UTF-8 byte order marker (BOM)
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to remove BOM from
14 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 |
3 |
4 | /** @type {import('eslint').Linter.Config[]} */
5 | export default [
6 | {ignores: [
7 | "docs/.vitepress/cache/**/*",
8 | "test/{bats,test_helper}/**/*",
9 | "target/**/*",
10 | ]},
11 | {languageOptions: { globals: globals.node }},
12 | ];
13 |
--------------------------------------------------------------------------------
/pkl/builtins/tf_lint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Infrastructure"
6 | description = "Terraform linter"
7 | }
8 | tf_lint = new Config.Step {
9 | glob = "**/*.tf"
10 | stage = ""
11 | check = "tflint"
12 | fix = "tflint --fix"
13 | }
14 |
--------------------------------------------------------------------------------
/test/version.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | setup() {
4 | load 'test_helper/common_setup'
5 | _common_setup
6 | }
7 |
8 | teardown() {
9 | _common_teardown
10 | }
11 |
12 | @test "hk --version prints version" {
13 | run hk --version
14 | assert_output --regexp "^hk\ [0-9]+\.[0-9]+\.[0-9]+$"
15 | }
16 |
--------------------------------------------------------------------------------
/docs/cli/config/sources.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk config sources`
4 |
5 | - **Usage**: `hk config sources`
6 |
7 | Show the configuration source precedence order
8 |
9 | Lists all configuration sources in order of precedence to help understand where configuration values come from.
10 |
--------------------------------------------------------------------------------
/pkl/builtins/jq.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Data Formats"
6 | description = "JSON processor"
7 | }
8 | jq = new Config.Step {
9 | glob = "**/*.json"
10 | stage = ""
11 | check = "jq . {{ files }}"
12 | fix = "jq -S . {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/test/data/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import js from "@eslint/js";
3 |
4 |
5 | /** @type {import('eslint').Linter.Config[]} */
6 | export default [
7 | {languageOptions: { globals: globals.node }},
8 | js.configs.recommended,
9 | {rules:{
10 | semi: ["error", "always"],
11 | }}
12 | ];
13 |
--------------------------------------------------------------------------------
/pkl/builtins/php_cs.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "PHP"
6 | description = "PHP coding standards"
7 | }
8 | php_cs = new Config.Step {
9 | glob = "**/*.php"
10 | stage = ""
11 | check = "phpcs {{ files }}"
12 | fix = "phpcbf {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/docs/cli/util/check-case-conflict.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util check-case-conflict`
4 |
5 | - **Usage**: `hk util check-case-conflict …`
6 |
7 | Check for case-insensitive filename conflicts
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check for case conflicts
14 |
--------------------------------------------------------------------------------
/pkl/builtins/deno_check.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "JavaScript/TypeScript"
6 | description = "Deno type checker"
7 | }
8 | deno_check = new Config.Step {
9 | glob = List("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx")
10 | check = "deno check {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/pkl/builtins/nix_fmt.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Nix"
6 | description = "Nix formatter"
7 | }
8 | nix_fmt = new Config.Step {
9 | glob = "**/*.nix"
10 | stage = ""
11 | check = "nixfmt --check {{ files }}"
12 | fix = "nixfmt {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/go_fumpt.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Strict Go formatter"
7 | }
8 | go_fumpt = new Config.Step {
9 | glob = "**/*.go"
10 | stage = ""
11 | check = "gofumpt -l {{ files }}"
12 | fix = "gofumpt -w {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/gomod_tidy.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Go module maintenance"
7 | }
8 | gomod_tidy = new Config.Step {
9 | glob = "**/go.mod"
10 | stage = ""
11 | check_diff = "go mod tidy -diff"
12 | fix = "go mod tidy"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/rubocop.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Ruby"
6 | description = "Ruby style guide"
7 | }
8 | rubocop = new Config.Step {
9 | glob = "**/*.rb"
10 | stage = ""
11 | check = "rubocop {{ files }}"
12 | fix = "rubocop --fix {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/tsserver.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "JavaScript/TypeScript"
6 | description = "TypeScript language server diagnostics"
7 | }
8 | tsserver = new Config.Step {
9 | glob = List("**/*.ts", "**/*.tsx")
10 | check = "tsc-files --noEmit {{ files }}"
11 | }
12 |
--------------------------------------------------------------------------------
/docs/cli/util/check-executables-have-shebangs.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util check-executables-have-shebangs`
4 |
5 | - **Usage**: `hk util check-executables-have-shebangs …`
6 |
7 | Check that executable files have shebangs
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check
14 |
--------------------------------------------------------------------------------
/pkl/builtins/go_lines.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Long line fixer"
7 | }
8 | go_lines = new Config.Step {
9 | glob = "**/*.go"
10 | stage = ""
11 | check = "golines --dry-run {{ files }}"
12 | fix = "golines -w {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/isort.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Python"
6 | description = "Python import sorter"
7 | }
8 | isort = new Config.Step {
9 | glob = "**/*.py"
10 | stage = ""
11 | check = "isort --check-only {{ files }}"
12 | fix = "isort {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/stylua.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Other Languages"
6 | description = "Lua formatter"
7 | }
8 | stylua = new Config.Step {
9 | glob = "**/*.lua"
10 | stage = ""
11 | check = "stylua --check {{ files }}"
12 | fix = "stylua {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/go_imports.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Go import management"
7 | }
8 | go_imports = new Config.Step {
9 | glob = "**/*.go"
10 | stage = ""
11 | check = "goimports -l {{ files }}"
12 | fix = "goimports -w {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/mix_test.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Elixir"
6 | description = "Test Elixir with Mix"
7 | }
8 | mix_test = new Config.Step {
9 | glob = List("**/*.ex", "**/*.exs")
10 | exclude = List("deps/**/*")
11 | check = "mix test --warnings-as-errors {{files}}"
12 | }
13 |
--------------------------------------------------------------------------------
/docs/cli/util/no-commit-to-branch.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util no-commit-to-branch`
4 |
5 | - **Usage**: `hk util no-commit-to-branch [--branch… ]`
6 |
7 | Prevent commits to specific branches
8 |
9 | ## Flags
10 |
11 | ### `--branch… `
12 |
13 | Branch names to protect (default: main, master)
14 |
--------------------------------------------------------------------------------
/pkl/builtins/alejandra.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Nix"
6 | description = "Alternative Nix formatter"
7 | }
8 | alejandra = new Config.Step {
9 | glob = "**/*.nix"
10 | stage = ""
11 | check = "alejandra --check {{ files }}"
12 | fix = "alejandra {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/standard_rb.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Ruby"
6 | description = "Ruby Standard Style"
7 | }
8 | standard_rb = new Config.Step {
9 | glob = "**/*.rb"
10 | stage = ""
11 | check = "standardrb {{ files }}"
12 | fix = "standardrb --fix {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/docs/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import tseslint from "typescript-eslint";
3 |
4 |
5 | /** @type {import('eslint').Linter.Config[]} */
6 | export default [
7 | {files: ["**/*.{js,mjs,cjs,ts}"]},
8 | {ignores: [".vitepress/**/*"]},
9 | {languageOptions: { globals: globals.browser }},
10 | ...tseslint.configs.recommended,
11 | ];
12 |
--------------------------------------------------------------------------------
/docs/reference/examples/index.md:
--------------------------------------------------------------------------------
1 | # Configuration Examples
2 |
3 | This directory contains runnable examples extracted from the public Pkl configurations.
4 |
5 | ## Available Examples
6 |
7 | - [custom-linters](./custom-linters.md)
8 | - [javascript-project](./javascript-project.md)
9 | - [monorepo](./monorepo.md)
10 | - [python-project](./python-project.md)
11 |
--------------------------------------------------------------------------------
/pkl/builtins/mix_compile.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Elixir"
6 | description = "Compile Elixir with Mix"
7 | }
8 | mix_compile = new Config.Step {
9 | glob = "**/*.ex"
10 | exclude = List("deps/**/*")
11 | check = "mix compile --warnings-as-errors --strict-errors {{files}}"
12 | }
13 |
--------------------------------------------------------------------------------
/pkl/builtins/nixpkgs_format.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Nix"
6 | description = "Nixpkgs formatter"
7 | }
8 | nixpkgs_format = new Config.Step {
9 | glob = "**/*.nix"
10 | stage = ""
11 | check = "nixpkgs-fmt --check {{ files }}"
12 | fix = "nixpkgs-fmt {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/sql_fluff.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Data Formats"
6 | description = "SQL linter and formatter"
7 | }
8 | sql_fluff = new Config.Step {
9 | glob = "**/*.sql"
10 | stage = ""
11 | check = "sqlfluff lint {{ files }}"
12 | fix = "sqlfluff fix {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/bundle_audit.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Ruby"
6 | description = "Dependency security audit"
7 | }
8 | bundle_audit = new Config.Step {
9 | glob = "**/Gemfile.lock"
10 | stage = ""
11 | check = "bundle-audit check {{ files }}"
12 | fix = "bundle-audit update"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/check_conventional_commit.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Special Purpose"
6 | description = "Verify commit message matches conventional commits formatting"
7 | }
8 | check_conventional_commit = new Config.Step {
9 | check = "hk util check-conventional-commit {{commit_msg_file}}"
10 | }
11 |
--------------------------------------------------------------------------------
/pkl/Types.pkl:
--------------------------------------------------------------------------------
1 | @ModuleInfo { minPklVersion = "0.27.2" }
2 | module hk.Types
3 | import "pkl:base"
4 |
5 | /// Helper function to create regex patterns with clean syntax
6 | @Deprecated {
7 | since = "1.27.1"
8 | message = "Replace `Types.Regex` with `Regex` (pkl built-in)"
9 | replaceWith = "Regex"
10 | }
11 | function Regex(pattern: String): Regex = base.Regex(pattern)
12 |
--------------------------------------------------------------------------------
/pkl/builtins/actionlint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Infrastructure"
6 | description = "GitHub Actions workflow linter"
7 | }
8 | const actionlint = new Config.Step {
9 | glob = List(".github/workflows/*.yml", ".github/workflows/*.yaml")
10 | batch = true
11 | check = "actionlint {{ files }}"
12 | }
13 |
--------------------------------------------------------------------------------
/pkl/builtins/cpp_lint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Other Languages"
6 | description = "C++ style checker"
7 | }
8 | cpp_lint = new Config.Step {
9 | glob =
10 | List("**/*.c", "**/*.h", "**/*.cpp", "**/*.hpp", "**/*.cc", "**/*.hh", "**/*.cxx", "**/*.hxx")
11 | check = "cpplint {{ files }}"
12 | }
13 |
--------------------------------------------------------------------------------
/pkl/builtins/cargo_check.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Rust"
6 | description = "Fast Rust type checking"
7 | }
8 | cargo_check = new Config.Step {
9 | glob = "**/*.rs"
10 | check = "cargo check -q"
11 | env {
12 | ["CARGO_TERM_COLOR"] = "{% if color %}always{% else %}never{% endif %}"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/pkl/builtins/golangci_lint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Go meta-linter"
7 | }
8 | golangci_lint = new Config.Step {
9 | glob = "**/*.go"
10 | stage = ""
11 | check = "golangci-lint run --fix=false {{ files }}"
12 | fix = "golangci-lint run --fix {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/markdown_lint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Markdown"
6 | description = "Markdown linter"
7 | }
8 | markdown_lint = new Config.Step {
9 | glob = List("**/*.md", "**/*.markdown")
10 | stage = ""
11 | check = "markdownlint {{ files }}"
12 | fix = "markdownlint --fix {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/docs/cli/test.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk test`
4 |
5 | - **Usage**: `hk test [FLAGS]`
6 |
7 | Run step-defined tests
8 |
9 | ## Flags
10 |
11 | ### `--list`
12 |
13 | List tests without running
14 |
15 | ### `--name… `
16 |
17 | Filter by test name (repeatable)
18 |
19 | ### `--step… `
20 |
21 | Filter by step name (repeatable)
22 |
--------------------------------------------------------------------------------
/pkl/builtins/deno.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "JavaScript/TypeScript"
6 | description = "Deno formatter"
7 | }
8 | deno = new Config.Step {
9 | glob = List("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx")
10 | stage = ""
11 | check = "deno fmt --check {{ files }}"
12 | fix = "deno fmt {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/src/cli/cache/clear.rs:
--------------------------------------------------------------------------------
1 | use crate::{Result, env};
2 |
3 | #[derive(Debug, clap::Args)]
4 | pub struct Clear {}
5 |
6 | impl Clear {
7 | pub async fn run(&self) -> Result<()> {
8 | if env::HK_CACHE_DIR.exists() {
9 | xx::file::remove_dir_all(&*env::HK_CACHE_DIR)?;
10 | xx::file::mkdirp(&*env::HK_CACHE_DIR)?;
11 | }
12 | Ok(())
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/pkl/builtins/tofu.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Infrastructure"
6 | description = "OpenTofu formatter"
7 | }
8 | tofu = new Config.Step {
9 | glob = List("**/*.tf", "**/*.tfvars", "**/*.tftest.hcl")
10 | stage = ""
11 | check_list_files = "tofu fmt -check {{ files }}"
12 | fix = "tofu fmt {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/src/cli/run/pre_commit.rs:
--------------------------------------------------------------------------------
1 | use crate::{Result, hook_options::HookOptions};
2 |
3 | /// Sets up git hooks to run hk
4 | #[derive(clap::Args)]
5 | #[clap(visible_alias = "pc")]
6 | pub struct PreCommit {
7 | #[clap(flatten)]
8 | hook: HookOptions,
9 | }
10 |
11 | impl PreCommit {
12 | pub async fn run(self) -> Result<()> {
13 | self.hook.run("pre-commit").await
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/step_locks.rs:
--------------------------------------------------------------------------------
1 | use crate::file_rw_locks::Flocks;
2 | use tokio::sync::OwnedSemaphorePermit;
3 |
4 | #[allow(unused)]
5 | #[derive(Debug)]
6 | pub struct StepLocks {
7 | flocks: Flocks,
8 | semaphore: OwnedSemaphorePermit,
9 | }
10 |
11 | impl StepLocks {
12 | pub fn new(flocks: Flocks, semaphore: OwnedSemaphorePermit) -> Self {
13 | Self { flocks, semaphore }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/test/data/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "data",
3 | "version": "1.0.0",
4 | "main": "unpretty.js",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1"
7 | },
8 | "author": "",
9 | "license": "ISC",
10 | "description": "",
11 | "devDependencies": {
12 | "@eslint/js": "^9.19.0",
13 | "eslint": "^9.19.0",
14 | "globals": "^15.14.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hk
2 |
3 | A git hook manager and project linting tool with an emphasis on performance. Compared to other
4 | git hook managers, hk has tighter integration with linters and is able to make use of read/write
5 | file locks in order to maximize concurrency while also preventing race conditions.
6 |
7 | See docs: https://hk.jdx.dev/
8 |
9 | ## Demo
10 |
11 | 
12 |
--------------------------------------------------------------------------------
/pkl/builtins/ox_lint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "JavaScript/TypeScript"
6 | description = "Oxidation compiler linter"
7 | }
8 | ox_lint = new Config.Step {
9 | glob = List("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx")
10 | stage = ""
11 | check = "oxlint {{ files }}"
12 | fix = "oxlint --fix {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/sort_package_json.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Configuration"
6 | description = "Sort package.json keys"
7 | }
8 | sort_package_json = new Config.Step {
9 | glob = "**/package.json"
10 | stage = ""
11 | check = "sort-package-json --check {{ files }}"
12 | fix = "sort-package-json {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/xo.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "JavaScript/TypeScript"
6 | description = "JavaScript/TypeScript linter with great defaults"
7 | }
8 | xo = new Config.Step {
9 | glob = List("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx")
10 | stage = ""
11 | check = "xo {{ files }}"
12 | fix = "xo --fix {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/src/cli/builtins.rs:
--------------------------------------------------------------------------------
1 | use crate::Result;
2 |
3 | /// Lists all available builtin linters
4 | #[derive(Debug, clap::Args)]
5 | pub struct Builtins;
6 | include!(concat!(env!("OUT_DIR"), "/builtins.rs"));
7 |
8 | impl Builtins {
9 | pub async fn run(&self) -> Result<()> {
10 | for builtin in BUILTINS {
11 | println!("{builtin}");
12 | }
13 |
14 | Ok(())
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docs/cli/init.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk init`
4 |
5 | - **Usage**: `hk init [-f --force] [--mise]`
6 |
7 | Generates a new hk.pkl file for a project
8 |
9 | ## Flags
10 |
11 | ### `-f --force`
12 |
13 | Overwrite existing hk.pkl file
14 |
15 | ### `--mise`
16 |
17 | Generate a mise.toml file with hk configured
18 |
19 | Set HK_MISE=1 to make this default behavior.
20 |
--------------------------------------------------------------------------------
/docs/cli/util/mixed-line-ending.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util mixed-line-ending`
4 |
5 | - **Usage**: `hk util mixed-line-ending [-f --fix] …`
6 |
7 | Detect and fix mixed line endings
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check or fix
14 |
15 | ## Flags
16 |
17 | ### `-f --fix`
18 |
19 | Fix mixed line endings by normalizing to LF
20 |
--------------------------------------------------------------------------------
/pkl/builtins/mix_fmt.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Elixir"
6 | description = "Format Elixir with Mix"
7 | }
8 | mix_fmt = new Config.Step {
9 | glob = List("**/*.ex", "**/*.exs")
10 | stage = ""
11 | exclude = List("deps/**/*")
12 | check = "mix format --check-formatted {{files}}"
13 | fix = "mix format {{files}}"
14 | }
15 |
--------------------------------------------------------------------------------
/pkl/builtins/standard_js.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "JavaScript/TypeScript"
6 | description = "JavaScript Standard Style"
7 | }
8 | standard_js = new Config.Step {
9 | glob = List("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx")
10 | stage = ""
11 | check = "standard {{ files }}"
12 | fix = "standard --fix {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/docs/cli/util/end-of-file-fixer.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util end-of-file-fixer`
4 |
5 | - **Usage**: `hk util end-of-file-fixer [-f --fix] …`
6 |
7 | Check for and optionally fix missing final newlines
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check/fix
14 |
15 | ## Flags
16 |
17 | ### `-f --fix`
18 |
19 | Fix files by adding final newline
20 |
--------------------------------------------------------------------------------
/pkl/builtins/terraform.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Infrastructure"
6 | description = "Terraform formatter"
7 | }
8 | terraform = new Config.Step {
9 | glob = List("**/*.tf", "**/*.tfvars", "**/*.tftest.hcl")
10 | stage = ""
11 | check_list_files = "terraform fmt -check {{ files }}"
12 | fix = "terraform fmt {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "scripts": {
4 | "docs:dev": "vitepress dev",
5 | "docs:build": "vitepress build",
6 | "docs:preview": "vitepress preview",
7 | "eslint": "eslint"
8 | },
9 | "dependencies": {
10 | "eslint": "^9.21.0",
11 | "vitepress": "^1.6.3"
12 | },
13 | "devDependencies": {
14 | "globals": "^16.0.0",
15 | "typescript-eslint": "^8.24.1"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/pkl/builtins/biome.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "JavaScript/TypeScript"
6 | description = "Fast formatter and linter"
7 | }
8 | biome = new Config.Step {
9 | glob = List("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.json")
10 | stage = ""
11 | check = "biome check {{ files }}"
12 | fix = "biome check --write {{ files }}"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/eslint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "JavaScript/TypeScript"
6 | description = "Pluggable JavaScript linter"
7 | }
8 | eslint = new Config.Step {
9 | glob = List("**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx")
10 | stage = ""
11 | batch = true
12 | check = "eslint {{ files }}"
13 | fix = "eslint --fix {{ files }}"
14 | }
15 |
--------------------------------------------------------------------------------
/docs/cli/util/check-merge-conflict.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util check-merge-conflict`
4 |
5 | - **Usage**: `hk util check-merge-conflict [--assume-in-merge] …`
6 |
7 | Check for merge conflict markers
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check
14 |
15 | ## Flags
16 |
17 | ### `--assume-in-merge`
18 |
19 | Run the check even when not in a merge
20 |
--------------------------------------------------------------------------------
/docs/cli/util/trailing-whitespace.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util trailing-whitespace`
4 |
5 | - **Usage**: `hk util trailing-whitespace [-f --fix] …`
6 |
7 | Check for and optionally fix trailing whitespace
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check/fix
14 |
15 | ## Flags
16 |
17 | ### `-f --fix`
18 |
19 | Fix trailing whitespace by removing it
20 |
--------------------------------------------------------------------------------
/docs/cli/config/explain.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk config explain`
4 |
5 | - **Usage**: `hk config explain `
6 |
7 | Explain where a configuration value comes from
8 |
9 | Shows the resolved value, its source (env/git/cli/default), and the full precedence chain showing all layers that could affect it.
10 |
11 | ## Arguments
12 |
13 | ### ``
14 |
15 | Configuration key to explain
16 |
--------------------------------------------------------------------------------
/docs/cli/install.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk install`
4 |
5 | - **Usage**: `hk install [--mise]`
6 | - **Aliases**: `i`
7 |
8 | Sets up git hooks to run hk
9 |
10 | ## Flags
11 |
12 | ### `--mise`
13 |
14 | Use `mise x` to execute hooks. With this, it won't
15 | be necessary to activate mise in order to run hooks
16 | with mise tools.
17 |
18 | Set HK_MISE=1 to make this default behavior.
19 |
--------------------------------------------------------------------------------
/pkl/builtins/lychee.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Special Purpose"
6 | description = "Fast, async, stream-based link checker"
7 | }
8 | lychee = new Config.Step {
9 | // https://github.com/lycheeverse/lychee/blob/db0f8a842f594e0a879563caf7d183266c02ca95/.pre-commit-hooks.yaml#L7
10 | types = List("text")
11 | check = "lychee --no-progress {{ files }}"
12 | }
13 |
--------------------------------------------------------------------------------
/src/hash.rs:
--------------------------------------------------------------------------------
1 | use std::hash::{Hash, Hasher};
2 |
3 | use siphasher::sip::SipHasher;
4 |
5 | pub fn hash_to_str(t: &T) -> String {
6 | let mut s = SipHasher::new();
7 | t.hash(&mut s);
8 | format!("{:x}", s.finish())
9 | }
10 |
11 | #[cfg(test)]
12 | mod tests {
13 | use super::*;
14 |
15 | #[test]
16 | fn test_hash_to_str() {
17 | assert_eq!(hash_to_str(&"foo"), "e1b19adfb2e348a2");
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pkl/builtins/cargo_fmt.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Rust"
6 | description = "Rust code formatter"
7 | }
8 | cargo_fmt = new Config.Step {
9 | glob = "**/*.rs"
10 | stage = ""
11 | workspace_indicator = "Cargo.toml"
12 | check = "cargo fmt --check --manifest-path {{workspace_indicator}}"
13 | fix = "cargo fmt --manifest-path {{workspace_indicator}}"
14 | }
15 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { h } from 'vue'
2 | import type { Theme } from 'vitepress'
3 | import DefaultTheme from 'vitepress/theme-without-fonts'
4 | import Layout from './Layout.vue'
5 | import HomePage from './HomePage.vue'
6 | import './style.css'
7 |
8 | export default {
9 | extends: DefaultTheme,
10 | Layout,
11 | enhanceApp({ app, router, siteData }) {
12 | app.component('HomePage', HomePage)
13 | },
14 | } satisfies Theme
15 |
--------------------------------------------------------------------------------
/benchmark/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | parallel: true
3 | jobs:
4 | - run: actionlint {staged_files}
5 | glob: ".github/workflows/*.{yml,yaml}"
6 | - run: cargo fmt
7 | glob: "*.rs"
8 | - run: '! rg -e "dbg!" {staged_files}'
9 | glob: "*.rs"
10 | - run: prettier --write {staged_files}
11 | glob: "*.{js,jsx,ts,tsx,css,scss,less,html,json,jsonc,yaml,markdown,markdown.mdx,graphql,handlebars,svelte,astro,htmlangular}"
12 |
--------------------------------------------------------------------------------
/pkl/builtins/go_fmt.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Go"
6 | description = "Go formatter"
7 | }
8 | go_fmt = new Config.Step {
9 | glob = "**/*.go"
10 | stage = ""
11 | check_list_files =
12 | """
13 | FILES=$(gofmt -s -l {{files}})
14 | if [ -n "$FILES" ]; then
15 | echo "$FILES"
16 | exit 1
17 | fi
18 | """
19 | fix = "gofmt -s -w {{files}}"
20 | }
21 |
--------------------------------------------------------------------------------
/pkl/builtins/clang_format.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Other Languages"
6 | description = "C/C++ formatter"
7 | }
8 | clang_format = new Config.Step {
9 | glob =
10 | List("**/*.c", "**/*.h", "**/*.cpp", "**/*.hpp", "**/*.cc", "**/*.hh", "**/*.cxx", "**/*.hxx")
11 | stage = ""
12 | check = "clang-format --dry-run -Werror {{ files }}"
13 | fix = "clang-format -i {{ files }}"
14 | }
15 |
--------------------------------------------------------------------------------
/pkl/builtins/dprint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Other Languages"
6 | description = "Pluggable code formatter"
7 | }
8 | dprint = new Config.Step {
9 | glob = "**/*"
10 | stage = ""
11 | check = "dprint check --allow-no-files {{ files }}"
12 | check_list_files = "dprint check --allow-no-files --list-different {{ files }}"
13 | fix = "dprint fmt --allow-no-files {{ files }}"
14 | }
15 |
--------------------------------------------------------------------------------
/pkl/builtins/rustfmt.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Rust"
6 | description = "Rust code formatter (standalone)"
7 | }
8 | rustfmt = new Config.Step {
9 | glob = "**/*.rs"
10 | stage = ""
11 | check = "rustfmt --check --edition 2024 {{ files }}"
12 | check_list_files = "rustfmt --check --edition 2024 --files-with-diff {{ files }}"
13 | fix = "rustfmt --edition 2024 {{ files }}"
14 | }
15 |
--------------------------------------------------------------------------------
/docs/cli/util/check-added-large-files.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util check-added-large-files`
4 |
5 | - **Usage**: `hk util check-added-large-files [--maxkb ] …`
6 |
7 | Check for large files being added to repository
8 |
9 | ## Arguments
10 |
11 | ### `…`
12 |
13 | Files to check
14 |
15 | ## Flags
16 |
17 | ### `--maxkb `
18 |
19 | Maximum file size in kilobytes (default: 500)
20 |
21 | **Default:** `500`
22 |
--------------------------------------------------------------------------------
/pkl/builtins/typos.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Special Purpose"
6 | description = "Source code spell checker"
7 | }
8 | typos = new Config.Step {
9 | glob = "**/*"
10 | stage = ""
11 | check_diff =
12 | """
13 | output=$(typos --diff {{files}})
14 | [ -z "$output" ] && exit 0
15 | printf "%s" "$output"
16 | exit 1
17 | """
18 | fix = "typos --write-changes {{ files }}"
19 | }
20 |
--------------------------------------------------------------------------------
/tapes/hk-demo.tape:
--------------------------------------------------------------------------------
1 | Output docs/public/hk-demo.gif
2 |
3 | Set WindowBar Colorful
4 | Set FontSize 16
5 | Set Theme "Catppuccin Frappe"
6 | Set Padding 10
7 | Set Margin 10
8 | Set Framerate 30
9 | Set Width 900
10 | Set Height 1000
11 | Set PlaybackSpeed 0.8
12 | Set TypingSpeed 100ms
13 | Set CursorBlink false
14 |
15 | Hide
16 | Type "clear"
17 | Enter
18 | Show
19 |
20 | # Run the demo (hk is on PATH via mise env)
21 | Type "hk check --all"
22 | Enter
23 | # Wait@10s /VHS_DONE/
24 | Sleep 10s
25 |
--------------------------------------------------------------------------------
/.github/workflows/semantic-pr-lint.yml:
--------------------------------------------------------------------------------
1 | name: semantic-pr-lint
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - edited
8 | - reopened
9 |
10 | jobs:
11 | main:
12 | name: Validate PR title
13 | runs-on: ubuntu-latest
14 | permissions:
15 | pull-requests: read
16 | steps:
17 | - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 |
--------------------------------------------------------------------------------
/test/install_creates_git_hooks.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | setup() {
4 | load 'test_helper/common_setup'
5 | _common_setup
6 | }
7 |
8 | teardown() {
9 | _common_teardown
10 | }
11 |
12 | @test "hk install creates git hooks" {
13 | cat < hk.pkl
14 | amends "$PKL_PATH/Config.pkl"
15 | import "$PKL_PATH/Builtins.pkl"
16 | hooks { ["pre-commit"] { steps { ["prettier"] = Builtins.prettier } } }
17 | EOF
18 | hk install
19 | assert_file_exists ".git/hooks/pre-commit"
20 | }
21 |
--------------------------------------------------------------------------------
/pkl/builtins/vacuum.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Other Languages"
6 | description = "Fast OpenAPI linter"
7 | }
8 | vacuum = new Config.Step {
9 | glob =
10 | List(
11 | "**/*openapi*.yaml",
12 | "**/*openapi*.yml",
13 | "**/*openapi*.json",
14 | "**/*swagger*.yaml",
15 | "**/*swagger*.yml",
16 | "**/*swagger*.json",
17 | )
18 | check = "vacuum lint {{files}}"
19 | fix = "vacuum lint --fix {{files}}"
20 | }
21 |
--------------------------------------------------------------------------------
/docs/cli/config/dump.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk config dump`
4 |
5 | - **Usage**: `hk config dump [--format ]`
6 |
7 | Print effective runtime settings (JSON format)
8 |
9 | Shows the merged configuration from all sources including CLI flags, environment variables, git config, user config, and project config.
10 |
11 | ## Flags
12 |
13 | ### `--format `
14 |
15 | Output format (json or toml)
16 |
17 | **Choices:**
18 |
19 | - `json`
20 | - `toml`
21 |
22 | **Default:** `json`
23 |
--------------------------------------------------------------------------------
/docs/cli/util/check-conventional-commit.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk util check-conventional-commit`
4 |
5 | - **Usage**: `hk util check-conventional-commit [--allowed-types… ] `
6 |
7 | Check for conventional commit message
8 |
9 | ## Arguments
10 |
11 | ### ``
12 |
13 | Commit message file to check
14 |
15 | ## Flags
16 |
17 | ### `--allowed-types… `
18 |
19 | **Default:** `build,chore,ci,docs,feat,fix,perf,refactor,revert,style,test`
20 |
--------------------------------------------------------------------------------
/pkl/builtins/pkl.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 | import "./test/helpers.pkl"
4 |
5 | @Builtins.meta {
6 | category = "Configuration"
7 | description = "Pkl configuration language"
8 | }
9 | pkl = new Config.Step {
10 | glob = "**/*.pkl"
11 | check = "pkl eval {{files}} >/dev/null"
12 | tests {
13 | local const testMaker = new helpers.TestMaker { filename = "test.pkl" }
14 | ["check bad file"] = testMaker.checkFail("x == 1", 1)
15 | ["check good file"] = testMaker.checkPass("x = 1")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/pkl/builtins/yamllint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 | import "./test/helpers.pkl"
4 |
5 | @Builtins.meta {
6 | category = "Data Formats"
7 | description = "YAML linter"
8 | }
9 | yamllint = new Config.Step {
10 | glob = List("**/*.yml", "**/*.yaml")
11 | check = "yamllint {{ files }}"
12 | tests {
13 | local const testMaker = new helpers.TestMaker { filename = "test.yaml" }
14 | ["check bad file"] = testMaker.checkFail("x: 1\nx: 2\n", 1)
15 | ["check good file"] = testMaker.checkPass("x: 1\n")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/docs/cli/config/get.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk config get`
4 |
5 | - **Usage**: `hk config get `
6 |
7 | Get a specific configuration value
8 |
9 | Available keys: jobs, enabled_profiles, disabled_profiles, fail_fast, display_skip_reasons, warnings, exclude, skip_steps, skip_hooks, stage
10 |
11 | ## Arguments
12 |
13 | ### ``
14 |
15 | Configuration key to retrieve
16 |
17 | Available keys: jobs, enabled_profiles, disabled_profiles, fail_fast, display_skip_reasons, warnings, exclude, skip_steps, skip_hooks, stage
18 |
--------------------------------------------------------------------------------
/pkl/builtins/hadolint.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 | import "./test/helpers.pkl"
4 |
5 | @Builtins.meta {
6 | category = "Infrastructure"
7 | description = "Dockerfile linter"
8 | }
9 | hadolint = new Config.Step {
10 | glob = "**/Dockerfile*"
11 | check = "hadolint {{ files }}"
12 | tests {
13 | local const testMaker = new helpers.TestMaker { filename = "Dockerfile" }
14 | ["check bad file"] = testMaker.checkFail("FROM debian\n", 1)
15 | ["check good file"] = testMaker.checkPass("FROM debian:jessie\n")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/pkl/builtins/shfmt.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Shell"
6 | description = "Shell formatter"
7 | }
8 | shfmt = new Config.Step {
9 | batch = true
10 | glob = List("**/*.sh", "**/*.bash", "**/*.mksh", "**/*.bats", "**/*.zsh")
11 | stage = ""
12 | check_diff = "shfmt -d {{ files }}"
13 | check_list_files =
14 | """
15 | files=$(shfmt -l {{ files }})
16 | if [ -n "$files" ]; then
17 | echo "$files"
18 | exit 1
19 | fi
20 | """
21 | fix = "shfmt -w {{ files }}"
22 | }
23 |
--------------------------------------------------------------------------------
/test/validate.bats:
--------------------------------------------------------------------------------
1 | setup() {
2 | load 'test_helper/common_setup'
3 | _common_setup
4 | }
5 | teardown() {
6 | _common_teardown
7 | }
8 |
9 | @test "validate" {
10 | cat < hk.pkl
11 | amends "$PKL_PATH/Config.pkl"
12 | import "$PKL_PATH/Builtins.pkl"
13 | hooks {
14 | ["pre-commit"] { steps { ["newlines"] = Builtins.newlines } }
15 | ["pre-push"] { steps { ["newlines"] = Builtins.newlines } }
16 | ["fix"] { steps { ["newlines"] = Builtins.newlines } }
17 | ["check"] { steps { ["newlines"] = Builtins.newlines } }
18 | }
19 | EOF
20 | hk validate
21 | }
22 |
--------------------------------------------------------------------------------
/pkl/builtins/cargo_clippy.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 |
4 | @Builtins.meta {
5 | category = "Rust"
6 | description = "Rust linter"
7 | }
8 | cargo_clippy = new Config.Step {
9 | glob = "**/*.rs"
10 | stage = ""
11 | workspace_indicator = "Cargo.toml"
12 | check = "cargo clippy --manifest-path {{workspace_indicator}} --quiet"
13 | fix =
14 | "cargo clippy --manifest-path {{workspace_indicator}} --fix --allow-dirty --allow-staged --quiet"
15 | check_first = false
16 | env {
17 | ["CARGO_TERM_PROGRESS_WHEN"] = "never"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pkl/PklProject:
--------------------------------------------------------------------------------
1 | amends "pkl:Project"
2 |
3 | package {
4 | name = "hk"
5 | authors { "jdx <216188+jdx@users.noreply.github.com>" }
6 | version = read?("env:VERSION")?.replaceFirst("\(name)@", "") ?? "0.0.1-SNAPSHOT"
7 | baseUri = "package://pkg.pkl-lang.org/github.com/jdx/hk"
8 | packageZipUrl = "https://github.com/jdx/hk/releases/download/v\(version)/\(name)@\(version).zip"
9 | sourceCode = "https://github.com/jdx/hk"
10 | sourceCodeUrlScheme = "https://github.com/jdx/hk/blob/\(version)/pkl%{path}#%{line}-%{endLine}"
11 | license = "MIT"
12 | description = "pkl code for hk"
13 | }
14 |
--------------------------------------------------------------------------------
/pkl/builtins/check_merge_conflict.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 | import "./test/helpers.pkl"
4 |
5 | @Builtins.meta {
6 | category = "Special Purpose"
7 | description = "Detect merge conflict markers"
8 | }
9 | check_merge_conflict = new Config.Step {
10 | glob = "**/*"
11 | check = "hk util check-merge-conflict --assume-in-merge {{files}}"
12 | tests {
13 | local const testMaker = new helpers.TestMaker {}
14 | ["check bad file"] = testMaker.checkFail("<<<<<<< HEAD\nconflict\n", 1)
15 | ["check good file"] = testMaker.checkPass("normal line\n")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/pkl/builtins/taplo.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 | import "./test/helpers.pkl"
4 |
5 | @Builtins.meta {
6 | category = "Data Formats"
7 | description = "TOML linter"
8 | }
9 | taplo = new Config.Step {
10 | glob = "**/*.toml"
11 | check = "taplo lint {{ files }}"
12 | tests {
13 | local const testMaker = new helpers.TestMaker { filename = "test.toml" }
14 | ["check bad file"] = testMaker.checkFail("[table]\nkey = 0\nkey = 1\n", 1)
15 | // NB: Formatting is bad, but `lint` doesn't check formatting
16 | ["check good file"] = testMaker.checkPass("[table]\nkey = 0\n")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/cli/cache/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::Result;
2 |
3 | mod clear;
4 |
5 | /// Manage hk internal cache
6 | #[derive(Debug, clap::Args)]
7 | #[clap(hide = true)] // TODO: unhide if we actually use cache (which we probably will)
8 | pub struct Cache {
9 | #[clap(subcommand)]
10 | command: Commands,
11 | }
12 |
13 | #[derive(Debug, clap::Subcommand)]
14 | enum Commands {
15 | /// Clear the cache directory
16 | Clear(clear::Clear),
17 | }
18 |
19 | impl Cache {
20 | pub async fn run(self) -> Result<()> {
21 | match self.command {
22 | Commands::Clear(cmd) => cmd.run().await,
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/cli/run/commit_msg.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use crate::Result;
4 | use crate::hook_options::HookOptions;
5 |
6 | #[derive(clap::Args)]
7 | #[clap(visible_alias = "cm")]
8 | pub struct CommitMsg {
9 | /// The path to the file that contains the commit message
10 | commit_msg_file: PathBuf,
11 | #[clap(flatten)]
12 | hook: HookOptions,
13 | }
14 |
15 | impl CommitMsg {
16 | pub async fn run(mut self) -> Result<()> {
17 | self.hook
18 | .tctx
19 | .insert("commit_msg_file", &self.commit_msg_file.to_string_lossy());
20 | self.hook.run("commit-msg").await
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/cli/validate.rs:
--------------------------------------------------------------------------------
1 | use eyre::bail;
2 |
3 | use crate::{Result, config::Config};
4 |
5 | /// Validate the config file
6 | #[derive(Debug, clap::Args)]
7 | pub struct Validate {}
8 |
9 | impl Validate {
10 | pub async fn run(&self) -> Result<()> {
11 | let config = Config::get()?;
12 | config.validate()?;
13 | if !config.path.exists() {
14 | bail!(
15 | "config file {} does not exist",
16 | xx::file::display_path(&config.path)
17 | );
18 | }
19 | info!("{} is valid", xx::file::display_path(&config.path));
20 | Ok(())
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/pkl/builtins/shellcheck.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 | import "./test/helpers.pkl"
4 |
5 | @Builtins.meta {
6 | category = "Shell"
7 | description = "Shell script analyzer"
8 | }
9 | shellcheck = new Config.Step {
10 | batch = true
11 | glob = List("**/*.sh", "**/*.bash")
12 | check = "shellcheck {{ files }}"
13 | tests {
14 | local const testMaker = new helpers.TestMaker {
15 | filename = "test.sh"
16 | }
17 | ["check bad file"] = testMaker.checkFail("#!/usr/bin/bash\necho 'oops I'm escaped'", 1)
18 | ["check good file"] = testMaker.checkPass("#!/usr/bin/bash\necho 'oops I'\\''m escaped'\n")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/cli/usage.rs:
--------------------------------------------------------------------------------
1 | use crate::Result;
2 | use crate::cli::Cli;
3 | use clap::CommandFactory;
4 |
5 | /// Generates a usage spec for the CLI
6 | ///
7 | /// https://usage.jdx.dev
8 | #[derive(Debug, clap::Args)]
9 | #[clap(hide = true, verbatim_doc_comment)]
10 | pub struct Usage {}
11 |
12 | impl Usage {
13 | pub async fn run(&self) -> Result<()> {
14 | let mut cmd = Cli::command();
15 | let mut buf = vec![];
16 | clap_usage::generate(&mut cmd, "hk", &mut buf);
17 | let usage = String::from_utf8(buf).unwrap() + "\n" + include_str!("../hk-extras.usage.kdl");
18 | println!("{}", usage.trim());
19 | Ok(())
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.taplo.toml:
--------------------------------------------------------------------------------
1 | [[rule]]
2 | formatting.align_entries = false
3 | formatting.reorder_keys = false
4 | include = ["settings.toml"]
5 | schema.enabled = true
6 | schema.path = "settings-schema.json"
7 | [formatting]
8 | align_entries = true
9 | allowed_blank_lines = 1
10 | array_auto_collapse = false
11 | array_auto_expand = false
12 | array_trailing_comma = true
13 | column_width = 100
14 | compact_arrays = true
15 | compact_inline_tables = false
16 | crlf = false
17 | indent_entries = false
18 | indent_tables = false
19 | reorder_keys = true
20 | trailing_newline = true
21 |
--------------------------------------------------------------------------------
/test/arg_escape.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | setup() {
4 | load 'test_helper/common_setup'
5 | _common_setup
6 | }
7 | teardown() {
8 | _common_teardown
9 | }
10 |
11 | @test "arg escape" {
12 | export NO_COLOR=1
13 | cat < hk.pkl
14 | amends "$PKL_PATH/Config.pkl"
15 | import "$PKL_PATH/Builtins.pkl"
16 | hooks { ["pre-commit"] { steps { ["prettier"] = Builtins.prettier } } }
17 | EOF
18 | git add hk.pkl
19 | git commit -m "install hk"
20 | hk install
21 | echo 'console.log("test")' > '$test.js'
22 | git add '$test.js'
23 | run git commit -m "test"
24 | assert_failure
25 | assert_output --partial '[warn] $test.js'
26 | }
27 |
--------------------------------------------------------------------------------
/src/cli/completion.rs:
--------------------------------------------------------------------------------
1 | use crate::Result;
2 |
3 | /// Generates shell completion scripts
4 | #[derive(Debug, clap::Args)]
5 | #[clap()]
6 | pub struct Completion {
7 | /// The shell to generate completion for
8 | #[clap()]
9 | shell: String,
10 | }
11 |
12 | impl Completion {
13 | pub async fn run(&self) -> Result<()> {
14 | xx::process::cmd(
15 | "usage",
16 | [
17 | "g",
18 | "completion",
19 | &self.shell,
20 | "hk",
21 | "--usage-cmd",
22 | "hk usage",
23 | ],
24 | )
25 | .run()?;
26 | Ok(())
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/benchmark/hk.pkl:
--------------------------------------------------------------------------------
1 | amends "../pkl/Config.pkl"
2 | import "../pkl/Builtins.pkl"
3 |
4 | // defines what happens during git pre-commit hook
5 | local linters = new Mapping {}
6 |
7 | hooks = new {
8 | ["pre-commit"] {
9 | stash = "patch-file"
10 | steps {
11 | ["actionlint"] = Builtins.actionlint
12 | ["cargo-fmt"] = Builtins.cargo_fmt
13 | ["dbg"] {
14 | // ensure no dbg! macros are used
15 | glob = "**/*.rs"
16 | check = "! rg -e 'dbg!' {{files}}"
17 | }
18 | ["prettier"] = (Builtins.prettier) {
19 | glob = List("*.js", "*.ts", "*.yml", "*.yaml") // override the default globs
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/pkl/builtins/fix_byte_order_marker.pkl:
--------------------------------------------------------------------------------
1 | import "../Builtins.pkl"
2 | import "../Config.pkl"
3 | import "./test/helpers.pkl"
4 |
5 | @Builtins.meta {
6 | category = "Special Purpose"
7 | description = "Remove UTF-8 BOM"
8 | }
9 | fix_byte_order_marker = new Config.Step {
10 | glob = "**/*"
11 | stage = ""
12 | fix = "hk util fix-byte-order-marker {{files}}"
13 | tests {
14 | local const testMaker = new helpers.TestMaker {}
15 | ["fix file with BOM"] = testMaker.fixPass("\u{FEFF}Hello, world!", "Hello, world!")
16 | ["fix file without BOM"] = testMaker.fixPass("Hello, world!", "Hello, world!")
17 | ["fix empty file"] = testMaker.fixPass("", "")
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/builtins_tests.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | setup() {
4 | load 'test_helper/common_setup'
5 | _common_setup
6 | }
7 |
8 | teardown() {
9 | _common_teardown
10 | }
11 |
12 | @test "builtins tests run" {
13 | cat < hk.pkl
14 | amends "$PKL_PATH/Config.pkl"
15 | import "$PKL_PATH/Builtins.pkl" as Builtins
16 | hooks {
17 | ["check"] {
18 | // Include all Builtins.* steps
19 | steps = Builtins.toMap().toMapping()
20 | }
21 | }
22 | PKL
23 |
24 | PATH="$PATH":"$PROJECT_ROOT"/test/builtin_tool_stubs
25 | run hk test
26 | assert_success
27 | # At least the newlines builtin has a test
28 | assert_output --partial "ok - newlines :: fix bad file"
29 | }
30 |
--------------------------------------------------------------------------------
/test/prepare_commit_msg.bats:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bats
2 |
3 | setup() {
4 | load 'test_helper/common_setup'
5 | _common_setup
6 | }
7 | teardown() {
8 | _common_teardown
9 | }
10 |
11 | @test "prepare-commit-msg hook" {
12 | cat < hk.pkl
13 | amends "$PKL_PATH/Config.pkl"
14 | hooks = new {
15 | ["prepare-commit-msg"] {
16 | steps {
17 | ["render-commit-msg"] {
18 | check = "echo default_commit_msg > {{commit_msg_file}}"
19 | }
20 | }
21 | }
22 | }
23 | EOF
24 | hk install
25 | echo "test" > test.txt
26 | git add test.txt
27 | run git commit --no-edit
28 | assert_output --partial "default_commit_msg"
29 | }
30 |
--------------------------------------------------------------------------------
/docs/cli/migrate/pre-commit.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # `hk migrate pre-commit`
4 |
5 | - **Usage**: `hk migrate pre-commit [FLAGS]`
6 |
7 | Migrate from pre-commit to hk
8 |
9 | ## Flags
10 |
11 | ### `-c --config `
12 |
13 | Path to .pre-commit-config.yaml
14 |
15 | **Default:** `.pre-commit-config.yaml`
16 |
17 | ### `-f --force`
18 |
19 | Overwrite existing hk.pkl file
20 |
21 | ### `-o --output