Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Repository Guidelines

## Project Structure & Module Organization
- `src/` contains the Rust CLI/TUI implementation; entry points live in `src/main.rs` and shared logic in `src/lib.rs`.
- `src/tests.rs` and `src/tests/` hold unit tests (currently `src/tests/config_test.rs`).
- `main.ts` is the Deno entry point for the published package.
- `deno.json` and `.huk.json` define tasks and hook configuration; `schema.json` documents the config schema.
- Build outputs land in `bin/` (custom artifacts) and `target/` (Cargo defaults).

## Build, Test, and Development Commands
- `deno task build` builds the release binary into `bin/` (uses Cargo under the hood).
- `deno task build:debug` builds a debug binary into `bin/`.
- `deno task test` runs `cargo test --all`; use `deno task test:verbose` for full logs.
- `deno task fmt` / `deno task fmt:check` format or verify formatting.
- `deno task lint` runs `cargo clippy --all --all-targets -- -D warnings`.
- `cargo run --bin huk -- <subcommand>` runs the CLI locally (e.g., `cargo run --bin huk -- dashboard`).

## Coding Style & Naming Conventions
- Rust edition is 2024; formatting is enforced by `.rustfmt.toml` (80 column max, 2-space tabs, item-level imports).
- Use `cargo fmt` before commits and keep Clippy clean (`deno task lint`).
- Follow Rust conventions: `snake_case` for modules/functions/tests (e.g., `parse_task_spec_string`), `PascalCase` for types, and `SCREAMING_SNAKE_CASE` for constants.

## Testing Guidelines
- Use `cargo test --all` or `deno task test`; tests live in `src/tests.rs` and `src/tests/*.rs`.
- Add targeted tests for config parsing, hook resolution, and task execution paths.

## Commit & Pull Request Guidelines
- Commit messages follow Conventional Commits (`feat(tui): add tasks view`, `docs: update README`), with optional `[WIP]` suffix when needed.
- PRs should include a concise summary, tests run, and note any config schema or hook changes.
- Include screenshots or short clips for TUI-facing changes.

## Configuration & Hook Definitions
- Define hooks in `deno.json` or `.huk.json` under the `hooks` field; tasks live under `tasks`.
- When changing config formats or validation, update `schema.json` and add/adjust tests.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ derive_more = { version = "2.1", features = [
"index",
"index_mut",
] }
moos = "0.1.0"
moos = "0.3.0"
toml = { version = "0.9.8", features = ["preserve_order"] }
paste = { version = "0.2.0", package = "pastey" }
chrono = { version = "0.4.42", optional = true }
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ just before committing (`pre‑commit`), when preparing a commit message
(`prepare‑commit‑msg`) or before pushing (`pre‑push`). Git’s documentation
describes a rich set of client‑side hooks, including `pre‑commit`,
`prepare‑commit‑msg`, `commit‑msg`, `post‑commit`, `pre‑rebase` and
`pre‑push`【23307213681274†L240-L330】. Setting up and distributing these
`pre‑push`. Setting up and distributing these
scripts across multiple environments can be cumbersome. hük centralizes hook
definitions alongside your project’s existing task configuration, making it
simple to install and manage them.
Expand All @@ -24,7 +24,7 @@ If your project targets Node.js you can also specify a `packageManager` field in
`npm@x.y.z`, `pnpm@x.y.z` and `yarn@x.y.z`. The
[Corepack](https://nodejs.org/docs/latest/api/cli.html#corepack) tool uses this
field to download and select the appropriate package manager; hük respects it
and falls back to `npm` when unspecified【349948098167533†L48-L59】.
and falls back to `npm` when unspecified.

## Installation

Expand Down Expand Up @@ -63,7 +63,7 @@ Tasks can refer to:

If both a `deno.json` and a `package.json` are present, hük prefers the
`deno.json` and falls back to `package.json`. When executing Node scripts hük
honours the `packageManager` field if present【349948098167533†L48-L59】.
honours the `packageManager` field if present.

### Example (Deno)

Expand Down Expand Up @@ -100,7 +100,7 @@ honours the `packageManager` field if present【349948098167533†L48-L59】.
"lint",
{ "command": "npm run test", "description": "Run tests" }
],
"commit-msg": { "command": "echo Validate commit message" }
"commit-msg": "npx git-cz",
}
}
```
Expand Down
12 changes: 11 additions & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy"]
components = ["rustfmt", "clippy", "cargo"]
targets = [
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"aarch64-apple-darwin",
"aarch64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
]
82 changes: 42 additions & 40 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
//! the `huk` executable exposes. It uses the [`clap`](https://crates.io/crates/clap)
//! crate for ergonomic argument parsing.

use std::path::PathBuf;

use clap::Args;
use clap::Parser;
use clap::Subcommand;
use derive_more::with_trait::IsVariant;
use derive_more::with_trait::TryInto;
use lazy_static::lazy_static;
use paste::paste;
use thiserror::Error;

Expand Down Expand Up @@ -46,6 +49,10 @@ pub struct Cli {
pub command: Commands,
}

lazy_static! {
static ref LAZY_CWD: PathBuf = std::env::current_dir().unwrap_or_default();
}

macro_rules! cli {
(
$(
Expand Down Expand Up @@ -113,6 +120,21 @@ macro_rules! cli {
};
}

const TASK_SPEC_LONG_HELP: &str = "\
Task specification to associate with the hook.\n\n\
Accepted task specification forms:\n \
1. a raw shell command string (e.g. `\"git add -A\"`)\n \
2. a task name from the configuration file, which must either be:\n \
- defined in the `tasks` section of a deno.json file, or ...\n \
- defined in the `scripts` section of a package.json file\n \
3. an object with `command`, `dependencies`, and/or `description` fields, where:\n \
- `command` is a shell command string to execute,\n \
- `dependencies` is an array of tasks to run before the command,\n \
Note: this field is required if `command` is not provided.\n \
- `description` is a human-readable summary of the task (optional)\n \
4. a sequence where value satisfies either type 1, 2, or 3 a `bove.\n \

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help text has a typo: "a `bove" should be "above" (and remove the stray backtick) so the long help renders correctly.

Suggested change
4. a sequence where value satisfies either type 1, 2, or 3 a `bove.\n \
4. a sequence where value satisfies either type 1, 2, or 3 above.\n \

Copilot uses AI. Check for mistakes.
Multiple specifications can be provided to build a sequence.";

cli! {
/// Launch an interactive dashboard for managing hooks and tasks.
#[command(
Expand Down Expand Up @@ -140,7 +162,12 @@ cli! {
←|→ (left / right)\n \
Reposition the cursor in text fields.\n")]
#[cfg(feature = "tui")]
Dashboard(Default),
Dashboard(Default) {
/// Set the working directory to run the huk dashboard in.
///
/// Defaults to the current working directory.
cwd(long, short = 'C', default_value = LAZY_CWD.to_str()): Option<PathBuf>,

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default_value = LAZY_CWD.to_str() is not a valid clap default_value (it expects a string literal / &str, but this expression yields an Option<&str> and is not 'static). Also, since the field type is Option<PathBuf>, supplying a default value makes the option always Some.

Consider removing the default entirely and treating None as std::env::current_dir() in the dashboard handler, or switch to a non-optional PathBuf with an appropriate default_value (e.g. ".") / default_value_os_t if you want clap to populate it.

Suggested change
cwd(long, short = 'C', default_value = LAZY_CWD.to_str()): Option<PathBuf>,
cwd(long, short = 'C'): Option<PathBuf>,

Copilot uses AI. Check for mistakes.
},
Comment on lines +165 to +170

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a --cwd/-C option to the Dashboard command, but the dashboard handler currently always uses std::env::current_dir() (it takes _opts and ignores it). Either wire this flag through to the handler (use it for HookConfig::discover and for state.run(...)) or drop the option for now to avoid a misleading CLI.

Suggested change
Dashboard(Default) {
/// Set the working directory to run the huk dashboard in.
///
/// Defaults to the current working directory.
cwd(long, short = 'C', default_value = LAZY_CWD.to_str()): Option<PathBuf>,
},
Dashboard(Default) {},

Copilot uses AI. Check for mistakes.
/// List configured Git hooks and associated tasks.
#[command(
aliases = ["ls", "l", "hooks"],
Expand All @@ -157,21 +184,21 @@ cli! {
): bool,
/// Only output hook names without associated tasks.
name_only(long, short = 'n'): bool,
/// Format the results as standard JSON (JavaScript Object Notation).
/// Format results as JSON (JavaScript Object Notation).
json(long, short = 'j'): bool,
/// Format the results as YAML (YAML Ain't Markup Language).
yaml(long, short = 'y', long_help = "Format the results as YAML (YAML \
/// Format results as YAML (YAML Ain't Markup Language).
yaml(long, short = 'y', long_help = "Format results as YAML (YAML \
Ain't Markup Language).\n\nNote: this currently ignores the --compact flag."): bool,
/// Format the results as TOML (Tom's Obvious, Minimal Language).
/// Format results as TOML (Tom's Obvious, Minimal Language).
toml(long, short = 't'): bool,
/// Outputs a static list of names of all Git hooks that `huk` supports.
/// Output a static list of names of all Git hooks that `huk` supports.
all(
long,
short = 'a',
long_help = "Output a list of names of all the Git hooks supported by \
`huk`.\n\nUnlike other list options, this is unrelated to configuration.\n\
It returns an immutable list of Git hook names (like 'pre-commit'),\n\
indicating all of the hooks supported and understood by `huk`."
long_help = "Outputs the names of all Git hooks supported by `huk`.\n\n\
Unlike other list options, this is unrelated to configuration,\n\
and returns an immutable list of hook names (like 'pre-commit',\n\
'post-checkout', etc.) supported as keys in the 'hooks' object."
): bool,
},
/// Run the tasks for the specified hook name.
Expand All @@ -186,11 +213,10 @@ cli! {
hook(): String,
/// Additional arguments to forward to the hook runner.
args(
last = true,
long_help = "Additional arguments to forward to the hook runner.\n\n\
Depending on the hook being executed, Git may provide additional \
arguments, such as the commit message file for `commit-msg` hook. \
These will be passed along in order."
Depending on the hook being executed, Git might provide\n\
additional arguments at runtime (e.g., a commit message\n\
file to `commit-msg`). These are passed as-is, in order."
): Vec<String>,
/// Enable verbose output during task execution.
verbose(long, short = 'v'): bool,
Expand Down Expand Up @@ -233,19 +259,7 @@ cli! {
spec(
required = true,
last = true,
long_help = "Task specification to associate with the hook.\n\n\
Task specifications can take on several different forms:\n \
1. a raw shell command string (e.g. `\"git add -A\"`)\n \
2. a task name from the configuration file, which must either be:\n \
- defined in the `tasks` section of a deno.json file, or ...\n \
- defined in the `scripts` section of a package.json file\n \
3. an object with `command`, `dependencies`, and/or `description` fields, where:\n \
- `command` is a shell command string to execute,\n \
- `dependencies` is an array of tasks to run before the command,\n \
Note: this field is required if `command` is not provided.\n \
- `description` is a human-readable summary of the task (optional)\n \
4. a sequence where value satisfies either type 1, 2, or 3 above.\n \
Multiple specifications can be provided to build a sequence."
long_help = TASK_SPEC_LONG_HELP
): Vec<String>,
/// Replace any existing hook definition instead of appending to it.
replace(long, short = 'r'): bool,
Expand Down Expand Up @@ -275,19 +289,7 @@ cli! {
spec(
required = true,
last = true,
long_help = "New task specification to associate with the hook.\n\n\
Task specifications can take on several different forms:\n \
1. a raw shell command string (e.g. `\"git add -A\"`)\n \
2. a task name from the configuration file, which must either be:\n \
- defined in the `tasks` section of a deno.json file, or ...\n \
- defined in the `scripts` section of a package.json file\n \
3. an object with `command`, `dependencies`, and/or `description` fields, where:\n \
- `command` is a shell command string to execute,\n \
- `dependencies` is an array of tasks to run before the command,\n \
Note: this field is required if `command` is not provided.\n \
- `description` is a human-readable summary of the task (optional)\n \
4. a sequence where value satisfies either type 1, 2, or 3 above.\n \
Multiple specifications can be provided to build a sequence."
long_help = TASK_SPEC_LONG_HELP
): Vec<String>,
/// Replace the existing hook definition instead of appending to it.
replace(long, short = 'r'): bool,
Expand Down
Loading
Loading