diff --git a/packages/docs/plans/2026-08-03_agent-skills-release-refresh.md b/packages/docs/plans/2026-08-03_agent-skills-release-refresh.md index 635992c24e..48a5a8480f 100644 --- a/packages/docs/plans/2026-08-03_agent-skills-release-refresh.md +++ b/packages/docs/plans/2026-08-03_agent-skills-release-refresh.md @@ -138,6 +138,10 @@ skill's 30-source set rather than treated as independent 30-source units. date-version ledger originally described); the skill validator passes for all five. See the 2026-08-04 entry below for independent liveness verification of every listed source. +- 2026-08-03: Completed the next language/tooling wave for Go, Lua, JVM, and + Fish: 238 primary pages fetched and inspected. Nine skills now have 424 + primary plus 15 supplemental sources, 439 total. The new four rewrites pass + formatting, skill validation, and batched live-link checks. - 2026-08-04: Re-verified the five shipped Research ledgers (`git-helper`, `bun-runtime-best-practices`, `typescript-helper`, `rust-helper`, `python-helper`) by extracting all linked URLs and live-checking each with @@ -150,6 +154,13 @@ skill's 30-source set rather than treated as independent 30-source units. reconciling the branch's actual tracking with the corrected Execution Model. The branch is git-spice-tracked going forward; no `gh stack` state remains authoritative for this work. +- 2026-08-04: Found the jvm-helper and lua-helper Research ledgers linked 28 + `.../releases/latest` URLs, which redirect (HTTP 302) and fail the + `curl -sI` liveness check the Quality Gates require even though the target + content is live. Resolved each to its actual release-tag URL and + re-verified all 28 return HTTP 200 with a plain `curl -sI` (no + redirect-follow), closing the same class of reproducibility gap as the + entry above. ## Session Log — 2026-08-03 diff --git a/packages/dotfiles/dot_agents/skills/fish-helper/SKILL.md b/packages/dotfiles/dot_agents/skills/fish-helper/SKILL.md index 9810cd187c..35b2a59948 100644 --- a/packages/dotfiles/dot_agents/skills/fish-helper/SKILL.md +++ b/packages/dotfiles/dot_agents/skills/fish-helper/SKILL.md @@ -1,497 +1,184 @@ --- name: fish-helper -description: | - Fish shell scripting - functions, abbreviations, completions, and configuration - When user works with .fish files, mentions Fish shell, fish config, Fisher plugins, or Fish scripting patterns +description: Current Fish shell scripting, functions, abbreviations, completions, variables, events, configuration, plugins, testing, and safety guidance. Use when writing or reviewing Fish config, `.fish` scripts, completions, functions, prompts, or plugin setup. --- -# Fish Shell Helper Agent +# Fish Helper -## What's New in Fish 4.x (2025-2026) +Write Fish as Fish rather than translated POSIX shell. Preserve argv boundaries, propagate statuses, make configuration idempotent, and distinguish optional integrations from required tools. -### Fish 4.0 (February 2025) +## Current baseline -- **Rust rewrite**: Entire codebase ported from C++ to Rust (2,731 commits, 200+ contributors) -- **New keyboard protocol**: Human-readable bind notation (`bind ctrl-right` instead of escape sequences), xterm modifyOtherKeys and kitty keyboard protocol support -- **OSC 133 prompt marking**: Prompts and command output marked for terminal integration -- **Command-specific abbreviations**: `abbr --command git co checkout` -- **Self-installable builds**: Static binaries embed functions, man pages, and webconfig -- **History filtering**: `fish_should_add_to_history` function for selective exclusion -- **`string match --max-matches`** and **`set --no-event`** flags - -### Fish 4.1 (September 2025) - -- **Brace compound commands**: `{ echo 1; echo 2 }` syntax -- **Transient prompts**: `fish_transient_prompt` function for simplified prompt after execution -- **Mouse support**: OSC 133 prompt marking with kitty click events -- **`string pad --center`** option -- **Vi mode**: ctrl+a (increment) and ctrl+x (decrement) - -### Fish 4.2 (November 2025) - -- Multi-line autosuggestions from history -- `fish_tab_title` function for separate tab titles -- Fish assumes UTF-8 regardless of system locale - -### Fish 4.3 (December 2025) - -- **Universal variables replaced with global defaults** for cleaner configuration -- **Adaptive themes**: `[light]` and `[dark]` sections in theme files -- Terminal working directory reported via OSC 7 - -### Fish 4.4 (February 2026) - -- Vi mode word motions aligned with Vim behavior (counts supported: `d3w`) -- New `catppuccin-*` color themes -- `set_color` strikethrough modifier - -## Overview - -Fish (Friendly Interactive SHell) is a modern interactive shell focused on user experience. It provides syntax highlighting, autosuggestions, and tab completions out of the box. Fish intentionally breaks POSIX compatibility in favor of cleaner, more discoverable syntax. - -Key design principles: - -- Discoverability over tradition (no hidden configuration) -- User-friendliness over backward compatibility -- Correctness (no word splitting on variables) - -## Syntax Differences from Bash - -### Variable Assignment +Verified against Fish 4.8.1 on 2026-08-03: ```fish -# Fish uses set, not VAR=value -set name "world" -set -gx PATH /usr/local/bin $PATH # global + exported -set -l local_var "temporary" # local scope -set -U EDITOR vim # universal (persists across sessions) -set -e var_name # erase variable +fish --version ``` -### Variable Scopes - -- **Universal** (`-U`): Shared across all sessions, persisted to disk -- **Global** (`-g`): Current session only -- **Function** (`-f`): Current function -- **Local** (`-l`): Current block +Fish 4.8 changed installed/embedded completion and function layout, added `cd -L/-P`, and removed automatic `__fish_initialized` universal creation. Fish 4.7 changed noninteractive theme initialization; Fish 4.6 changed emoji width and added prompt environment controls; Fish 4.5 mainly fixed Vi-mode regressions. -### Command Substitution +Read [references/releases.md](references/releases.md) for the 51-page research ledger. Read [references/syntax-and-safety.md](references/syntax-and-safety.md) for variables, argv, statuses, reading, tracing, temp directories, and shell boundaries. Read [references/functions-completions-config.md](references/functions-completions-config.md) for functions, events, abbreviations, completions, startup, prompts, and themes. Read [references/plugins-and-testing.md](references/plugins-and-testing.md) for Fisher, popular plugins, testing, and installation security. -```fish -# Fish uses (command) or $(command), NOT backticks -set files (ls) -echo "Current dir: $(pwd)" +## Variables and environment -# Output splits on newlines only (not whitespace like bash) -# Use quotes to prevent splitting -set content "$(cat file.txt)" -``` - -### No Process Substitution +Use exported global variables in version-controlled config for child-process environment: ```fish -# Bash: diff <(cmd1) <(cmd2) -# Fish: use psub -diff (cmd1 | psub) (cmd2 | psub) +set -gx EDITOR nvim +fish_add_path $HOME/.local/bin ``` -### Conditionals and Loops +Universal variables remain supported, but Fish 4.3 stopped creating several user-facing defaults as universal values. Use universal state only when cross-session persistence is intentional. `set -U EDITOR vim` is not exported; `set -Ux` is persistent and exported but can create hidden machine state. -```fish -# if/else if/else/end (no then/fi) -if test -f /etc/os-release - cat /etc/os-release -else if test -f /etc/issue - cat /etc/issue -else - echo "Unknown OS" -end +Use `fish_add_path` for idempotent path changes. Do not replace `PATH` with a short hard-coded list or prepend the same directory every time config is sourced. -# switch/case/end (no esac, no fallthrough) -switch (uname) -case Linux - echo "Linux" -case Darwin - echo "macOS" -case '*' - echo "Other" -end - -# for/end (no do/done) -for file in *.txt - echo $file -end - -# while/end -while read -l line - echo "Line: $line" -end < input.txt -``` - -### Lists (Arrays) +Fish supports command-scoped environment overrides: ```fish -# All variables are lists. 1-indexed, negative indexing supported -set colors red green blue -echo $colors[1] # red -echo $colors[-1] # blue -echo $colors[2..3] # green blue -echo (count $colors) # 3 - -# PATH variables auto-split on colons -set -gx PATH /usr/local/bin /usr/bin /bin +MODE=test command --flag ``` -### String Manipulation +Standalone assignment still uses `set`. -```fish -# Use the string builtin (no ${var%pattern} parameter expansion) -string length "hello" # 5 -string upper "hello" # HELLO -string replace "old" "new" "old text" # new text -string split "," "a,b,c" # a\nb\nc -string match -r '(\d+)' "file42.txt" # 42 -string trim " hello " # hello -string sub -s 2 -l 3 "hello" # ell -``` +## Preserve argv -### Arithmetic +Accept commands as the remaining arguments and invoke them directly: ```fish -# Use math builtin (no $(( )) or let) -math 2 + 2 # 4 -math "10 / 3" # 3.333333 (floating point by default) -math "sqrt(16)" # 4 -set result (math "$x * 2") -``` - -### Special Variables - -| Bash | Fish | -| ---------- | ------------------- | -| `$?` | `$status` | -| `$@`, `$*` | `$argv` | -| `$$` | `$fish_pid` | -| `$#` | `(count $argv)` | -| `$!` | `$last_pid` | -| `$0` | `(status filename)` | - -### Other Key Differences +function retry --description 'Retry a command with exact arguments' + argparse 'n/max-attempts=' -- $argv + or return -- No heredocs: use `printf '%s\n' "line1" "line2"` or multi-line strings -- No `[[`: use `test` or `[` only -- No subshells: use `begin; end` for grouping, `set -l` for scoping -- No `export`: use `set -gx` -- No `source ~/.bashrc`: use `source ~/.config/fish/config.fish` -- Globs that match nothing cause command failure (not literal pass-through) -- `?` glob deprecated; use `*` or disable with `qmark-noglob` -- No word splitting on variable expansion (a feature, not a bug) - -## Functions Quick Reference - -```fish -# Define a function -function greet -d "Greet someone" - echo "Hello, $argv[1]!" -end - -# Function with argument names -function mkcd -a dir -d "Create and enter directory" - mkdir -p $dir && cd $dir -end - -# Function wrapping a command (inherit completions) -function ls --wraps ls -d "ls with color" - command ls --color=auto $argv -end - -# Event handlers -function on_pwd_change --on-variable PWD - echo "Changed to $PWD" -end + set -l max_attempts 3 + if set -q _flag_max_attempts + set max_attempts $_flag_max_attempts + end + if test (count $argv) -eq 0 + echo 'retry: missing command' >&2 + return 2 + end -function on_exit --on-event fish_exit - echo "Goodbye!" + for attempt in (seq $max_attempts) + $argv + set -l command_status $status + if test $command_status -eq 0 + return 0 + end + if test $attempt -eq $max_attempts + return $command_status + end + end end - -# Save function to autoload file -funcsave greet # saves to ~/.config/fish/functions/greet.fish ``` -## Abbreviations Quick Reference - -```fish -# Simple abbreviation -abbr -a gco git checkout -abbr -a gst git status - -# Position: expand anywhere (not just as command) -abbr -a --position anywhere -- -C --color - -# Command-specific (Fish 4.0+) -abbr --command git co checkout -abbr --command git br branch +Do not turn command arguments into source text with `eval`. Invoke the argv list directly or call an exact function. -# With cursor positioning -abbr -a L --position anywhere --set-cursor "| less" +## Error propagation -# Function-based expansion -abbr -a !! --position anywhere --function last_history_item - -# Regex-based -abbr -a dotenv --regex '\.env.*' --function edit_with_caution -``` - -## Completions Quick Reference +Fish functions return the status of their last command unless overridden. A helper named `die` that only executes `return 1` returns from itself; its caller continues unless it propagates the status. ```fish -# Basic completion for a command -complete -c mycommand -s h -l help -d "Show help" -complete -c mycommand -s v -l verbose -d "Verbose output" - -# Require a parameter -complete -c mycommand -s o -l output -r -d "Output file" -F - -# Exclusive (require param, no files) -complete -c mycommand -s f -l format -x -a "json yaml toml" -d "Output format" - -# Conditional completions -complete -c git -n "__fish_use_subcommand" -a checkout -d "Switch branches" -complete -c git -n "__fish_seen_subcommand_from checkout" -a "(git branch --format='%(refname:short)')" -d "Branch" - -# Disable file completions globally -complete -c mycommand -f - -# Wrap another command's completions -complete -c hub -w git -``` - -## Configuration Structure - -``` -~/.config/fish/ - config.fish # Main config (runs on every shell start) - conf.d/ # Modular config snippets (sourced alphabetically) - abbr.fish - path.fish - env.fish - functions/ # Autoloaded functions (one per file) - fish_prompt.fish - fish_right_prompt.fish - mkcd.fish - completions/ # Custom completions (one per command) - mycommand.fish - themes/ # Color themes (.theme files) - fish_plugins # Fisher plugin list - fish_variables # Universal variables (auto-managed, do not edit) +require_tool rg +or return ``` -### Startup Order - -1. Files in `conf.d/` directories (system, then user) in alphabetical order -2. `config.fish` -3. Functions autoloaded on first call +Check `mktemp`, `pushd`, reads, generated init commands, and cleanup explicitly. Required tools should fail fast. Only optional integrations may be conditionally absent, and the config should label them optional. -### Prompt Functions +## Reading input -- `fish_prompt` -- left prompt -- `fish_right_prompt` -- right prompt -- `fish_mode_prompt` -- vi mode indicator -- `fish_transient_prompt` -- simplified prompt shown after command execution (Fish 4.1+) -- `fish_greeting` -- message shown on shell start (set to empty to disable) - -## Key Bindings +`read` normally reads one line. It does not turn a whole file into an array by adding list flags: ```fish -# Emacs mode (default) -fish_default_key_bindings - -# Vi mode -fish_vi_key_bindings - -# Custom bindings -bind ctrl-r 'commandline -f history-pager' -bind \t complete -bind ctrl-e 'edit_command_buffer' - -# Vi mode insert-mode binding -bind --mode insert ctrl-c 'commandline -r ""' +while read -l line + process_line $line +end < file.txt ``` -### Default Key Bindings (Emacs Mode) - -- Tab: complete, Shift+Tab: search completions -- Ctrl+R: history pager -- Ctrl+C: cancel/interrupt -- Ctrl+L: clear screen -- Ctrl+U: delete to beginning of line -- Ctrl+K: delete to end of line -- Ctrl+W: delete previous path component -- Ctrl+Z: undo, Alt+/: redo -- Alt+E: edit in $EDITOR -- Alt+H: show man page for current command -- Right arrow / Ctrl+F: accept autosuggestion -- Alt+Right / Alt+F: accept next word of autosuggestion +Use `string split` when delimiter-based parsing is deliberate. Use `read --silent` for interactive secrets, or a tool-specific credential file/secret manager; do not write credential-shaped literals into shell config. -## Common Builtins +## Abbreviations -### read -- User Input +Use abbreviations for interactive expansion and functions for reusable logic. Cursor markers default to `%`: ```fish -read -l -P "Name: " name -read -l -s -P "Password: " password # silent input -read -l -P "Continue? [Y/n] " -c "Y" answer -read -l -n 1 char # single character -read -la lines < file.txt # read file into list - -# Read from pipe -echo "hello world" | read -l first rest -echo $first # hello -echo $rest # world +abbr --add L --position anywhere --set-cursor '% | less' ``` -### status -- Shell State +The expansion needs the marker for cursor movement. -```fish -status is-interactive # true in interactive shell -status is-login # true in login shell -status is-command-substitution # true inside $(...) -status filename # current script path -status function # current function name -status line-number # current line number -status current-command # name of currently running command -status features # list enabled features -status test-feature qmark-noglob # check feature flag -``` +## Transient prompts and themes -### contains -- List Membership +Enable transient prompts with a variable, not a function: ```fish -if contains "blue" $colors - echo "Found blue" -end - -set idx (contains -i "green" $colors) # get index +set -g fish_transient_prompt 1 ``` -### type / command -- Command Resolution +Fish reruns `fish_prompt`, `fish_right_prompt`, and `fish_mode_prompt` with `--final-rendering`. -```fish -type --short ls # alias, builtin, function, or file -type --path ls # file path of command -command -sq docker # check if command exists (silent, quiet) -builtin -n # list all builtins -functions # list all defined functions -functions --names # names only -functions myfunction # print source of function -``` +Prefer `fish_config theme choose THEME` for adaptive theme behavior. `fish_config theme save` stores universal colors and disables dynamic light/dark switching. Theme files use `fish_color_command blue`, not assignment syntax. -### source -- Execute Fish Scripts +## Startup and configuration -```fish -source file.fish -source (command which env_setup.fish) +Fish searches user `conf.d`, system configuration, and user/vendor data directories according to documented priority; snippets are naturally sorted, and only the first same-named file is run. Do not describe a simple system-then-user order. -# Source with arguments -source script.fish arg1 arg2 # $argv available in script -``` - -### emit -- Custom Events +Put environment and path setup needed by noninteractive Fish before an interactive-only guard: ```fish -emit my_custom_event "arg1" "arg2" +fish_add_path $HOME/.local/bin +set -gx EDITOR nvim -function handle_event --on-event my_custom_event - echo "Event received: $argv" -end +status is-interactive +or return ``` -## Common Patterns +Use `fish --profile-startup -ic exit` to profile startup. Plain `--profile` excludes startup/config loading. -### Guard for Interactive Shell +## Completions and events -```fish -# At top of config.fish -if not status is-interactive - return -end -``` +Fish 4.8 embeds bundled completions/functions; use `status list-files` to inspect embedded files rather than assuming `/usr/share/fish/completions`. -### Conditional PATH Setup +Register multiple commands with repeated `--command` or brace expansion: ```fish -# Only add to PATH if directory exists -for dir in ~/.local/bin ~/.cargo/bin ~/go/bin - test -d $dir; and fish_add_path $dir -end +complete --command={docker,podman} --long-option help --description 'Show help' ``` -### Wrapper Function Pattern +Dynamic completion generators must be fast, bounded, side-effect-free, and must not interpret untrusted source. Avoid network calls on each Tab press. -```fish -function git --wraps git -d "Git with default options" - command git -c color.ui=always $argv -end -``` +Variable event handlers can coalesce updates, can run on same-value sets, and have unspecified ordering across handlers. Universal updates from another shell have distinct delivery behavior. Use events for notifications, not ordering-critical state machines. -### Retry Pattern +## Tracing and introspection -```fish -function retry -a max_attempts cmd - set -l attempt 1 - while test $attempt -le $max_attempts - eval $cmd; and return 0 - set attempt (math $attempt + 1) - sleep 1 - end - return 1 -end -``` +`type --type name` prints classifications such as function, builtin, or file. `type --short` only suppresses full function definitions. -### Temporary Environment Variables +Tracing is enabled when `fish_trace` is set and non-empty. Disable it by erasing the variable: ```fish -# Fish has no VAR=value command syntax. Use env or begin/end block: -env PGPASSWORD=secret psql -U user db - -# Or scope with begin/end -begin - set -lx NODE_ENV production - npm start -end +set -e fish_trace ``` -## Debugging - -```fish -# Trace execution -set fish_trace 1 -some_command -set fish_trace 0 - -# Profile script performance -fish --profile profile.log -c 'source script.fish' - -# Check if interactive/login -status is-interactive -status is-login - -# Print function source -functions myfunction -type myfunction - -# Debug completions -complete -C "mycommand " # show what would complete +Use `$version` or compatibility variable `$FISH_VERSION`; `$fish_version` does not exist in Fish 4.8.1. -# List all key bindings -bind # show all active bindings -bind --mode insert # vi insert mode bindings -``` +## Security -## Reference Files +- Avoid `eval` and remote `curl | source` installation patterns. +- Pin and inspect a downloaded plugin installer before sourcing it. +- Resolve source paths with `type --path`, verify exactly one intended file, then source it. +- Keep dynamic completions and event handlers free of destructive side effects. +- Use secret managers, protected credential files, or `read --silent`; environment variables can still leak through child processes and diagnostics. +- Preserve exact arguments rather than re-parsing command text. +- Create and clean temporary directories only after each operation succeeds, preserving the wrapped command status. -For detailed reference material, see: +## Review checklist -- `references/fish-syntax.md` -- Variables, control flow, strings, lists, pipes, math -- `references/completions-functions.md` -- Writing completions, functions, abbreviations, event handlers -- `references/plugins-config.md` -- Fisher, popular plugins, config patterns, prompt customization +- Verify Fish 4.8.1 behavior and the project's minimum version. +- Use exported globals and `fish_add_path` for version-controlled environment setup. +- Preserve argv and avoid `eval`. +- Propagate failures through functions and cleanup. +- Read complete files with a loop or explicit splitting. +- Use the current transient-prompt variable and adaptive theme workflow. +- Put noninteractive environment setup before the interactive guard. +- Treat completion subprocesses and event handlers as bounded, side-effect-free hooks. +- Inspect embedded completion paths with `status list-files`. +- Pin and review third-party plugin installation. diff --git a/packages/dotfiles/dot_agents/skills/fish-helper/references/completions-functions.md b/packages/dotfiles/dot_agents/skills/fish-helper/references/completions-functions.md deleted file mode 100644 index d4ec1de26b..0000000000 --- a/packages/dotfiles/dot_agents/skills/fish-helper/references/completions-functions.md +++ /dev/null @@ -1,750 +0,0 @@ -# Fish Completions, Functions, and Event Handlers - -## Writing Completions - -### The complete Command - -Register completions for commands using `complete`. Fish evaluates completions dynamically each time Tab is pressed. - -```fish -complete -c COMMAND [options] -``` - -### Core Options - -| Flag | Long Form | Description | -| ---- | --------------------- | --------------------------------------------------- | -| `-c` | `--command` | Command to complete for | -| `-s` | `--short-option` | Single-character option (e.g., `-v`) | -| `-l` | `--long-option` | GNU long option (e.g., `--verbose`) | -| `-o` | `--old-option` | Old-style long option (single dash, e.g., `-Wall`) | -| `-a` | `--arguments` | Space-separated list of completions | -| `-f` | `--no-files` | Disable file completions | -| `-F` | `--force-files` | Force file completions (override `-f`) | -| `-r` | `--require-parameter` | Option requires an argument | -| `-x` | `--exclusive` | Shorthand for `-r` + `-f` | -| `-n` | `--condition` | Shell command; offer completion only if returns 0 | -| `-d` | `--description` | Description shown in completion menu | -| `-w` | `--wraps` | Inherit completions from another command | -| `-k` | `--keep-order` | Preserve argument order (don't sort) | -| `-e` | `--erase` | Remove completions | -| `-p` | `--path` | Match command by absolute path (supports wildcards) | - -### Basic Examples - -```fish -# Simple command with options -complete -c myapp -s h -l help -d "Show help" -complete -c myapp -s v -l version -d "Show version" -complete -c myapp -s V -l verbose -d "Enable verbose output" - -# Option that requires a parameter -complete -c myapp -s o -l output -r -d "Output file" - -# Option with specific choices (exclusive: requires param, no file completion) -complete -c myapp -s f -l format -x -a "json yaml toml csv" -d "Output format" - -# Disable all file completions for command -complete -c myapp -f - -# Force file completions for specific option -complete -c myapp -s i -l input -r -F -d "Input file" -``` - -### Dynamic Completions - -Generate completions from commands: - -```fish -# Complete git branches -complete -c git-checkout -a "(git branch --format='%(refname:short)')" - -# Complete running process names -complete -c kill -a "(ps -eo comm= | sort -u)" - -# Complete usernames -complete -c chown -a "(__fish_complete_users)" - -# Complete from a file -complete -c myapp -a "(cat ~/.myapp/commands.txt)" -``` - -### Conditional Completions - -Use `-n` (condition) to control when completions appear: - -```fish -# Only complete subcommands when no subcommand given yet -complete -c git -n "__fish_use_subcommand" -a "add" -d "Add files" -complete -c git -n "__fish_use_subcommand" -a "commit" -d "Record changes" -complete -c git -n "__fish_use_subcommand" -a "push" -d "Update remote" - -# Complete branch names only after "checkout" -complete -c git -n "__fish_seen_subcommand_from checkout switch" \ - -a "(git branch --format='%(refname:short)')" -d "Branch" - -# Complete files only after "add" -complete -c git -n "__fish_seen_subcommand_from add" -F -``` - -### Helper Functions for Conditions - -Fish provides built-in helpers: - -| Function | Purpose | -| ------------------------------------ | ----------------------------------------------- | -| `__fish_use_subcommand` | True if no subcommand given yet | -| `__fish_seen_subcommand_from CMD...` | True if one of the listed subcommands appears | -| `__fish_contains_opt -s X long` | True if the given option has been typed | -| `__fish_complete_directories` | Complete directory names with descriptions | -| `__fish_complete_path` | Complete file/directory paths with descriptions | -| `__fish_complete_suffix .EXT` | Complete files with given extension | -| `__fish_complete_users` | Complete system usernames | -| `__fish_complete_groups` | Complete system groups | -| `__fish_complete_pids` | Complete process IDs | -| `__fish_print_hostnames` | Print known hostnames | -| `__fish_print_interfaces` | Print network interfaces | - -### Complete Example: Custom Command - -```fish -# completions/deploy.fish - -# Disable default file completions -complete -c deploy -f - -# Subcommands (only when no subcommand given) -complete -c deploy -n "__fish_use_subcommand" -a "start" -d "Start deployment" -complete -c deploy -n "__fish_use_subcommand" -a "stop" -d "Stop deployment" -complete -c deploy -n "__fish_use_subcommand" -a "status" -d "Show deployment status" -complete -c deploy -n "__fish_use_subcommand" -a "rollback" -d "Rollback to previous" - -# Global options -complete -c deploy -s h -l help -d "Show help" -complete -c deploy -s v -l verbose -d "Verbose output" -complete -c deploy -l env -x -a "staging production" -d "Target environment" - -# Options specific to "start" subcommand -complete -c deploy -n "__fish_seen_subcommand_from start" \ - -l tag -x -a "(git tag -l 'v*' | sort -rV | head -10)" -d "Version tag" -complete -c deploy -n "__fish_seen_subcommand_from start" \ - -l dry-run -d "Preview changes without deploying" - -# Options specific to "rollback" subcommand -complete -c deploy -n "__fish_seen_subcommand_from rollback" \ - -l steps -x -a "1 2 3 5" -d "Number of versions to rollback" -``` - -### Wrapping Commands - -Inherit completions from existing commands: - -```fish -# hub inherits all git completions -complete -c hub -w git - -# myls inherits ls completions -complete -c myls -w ls - -# Can also use --wraps in function definition -function myls --wraps ls - command ls --color=auto $argv -end -``` - -### Completion Autoloading - -Place completion files in `~/.config/fish/completions/COMMAND.fish`. Fish loads them automatically when Tab is pressed for that command. - -Search order for completion files: - -1. `~/.config/fish/completions/` (user) -2. `/etc/fish/completions/` (system admin) -3. `~/.local/share/fish/vendor_completions.d/` (third-party) -4. `/usr/share/fish/vendor_completions.d/` (vendor) -5. `/usr/share/fish/completions/` (bundled) -6. `~/.cache/fish/generated_completions/` (auto-generated from man pages) - -### Erasing Completions - -```fish -# Erase all completions for a command -complete -c myapp -e - -# Erase specific completion -complete -c myapp -l verbose -e - -# Prevent autoloading completions (Fish 4.0+) -complete -c myapp -e -``` - -## Defining Functions - -### Basic Function Definition - -```fish -function name - # commands -end - -function greet -d "Greet a person by name" - echo "Hello, $argv[1]!" -end -``` - -### Function Options - -| Flag | Long Form | Description | -| ---- | ---------------------- | ---------------------------------------- | -| `-d` | `--description` | Short description (shown in completions) | -| `-a` | `--argument-names` | Name positional arguments | -| `-w` | `--wraps` | Inherit completions from another command | -| `-S` | `--no-scope-shadowing` | Access caller's local variables | -| `-V` | `--inherit-variable` | Snapshot a variable at definition time | -| `-e` | `--on-event` | Register as event handler | -| `-v` | `--on-variable` | Trigger on variable change | -| `-j` | `--on-job-exit` | Trigger when job exits | -| `-p` | `--on-process-exit` | Trigger when process exits | -| `-s` | `--on-signal` | Trigger on signal | - -### Argument Handling - -All arguments arrive in `$argv`. Name them for clarity: - -```fish -function mkcd -a directory -d "Create and enter directory" - mkdir -p $directory - cd $directory -end - -function copy_to -a source destination -d "Copy file to destination" - cp $source $destination -end -``` - -Extra arguments beyond named ones remain in `$argv`: - -```fish -function mycommand -a first second - echo "First: $first" - echo "Second: $second" - echo "Rest: $argv[3..]" -end -``` - -### argparse for Robust Options - -```fish -function serve -d "Start a dev server" - argparse h/help 'p/port=!_validate_int' v/verbose -- $argv - or return - - if set -ql _flag_help - echo "Usage: serve [-p PORT] [-v] [DIR]" - return 0 - end - - set -l port 8080 - if set -ql _flag_port - set port $_flag_port - end - - set -l dir "." - if test (count $argv) -gt 0 - set dir $argv[1] - end - - if set -ql _flag_verbose - echo "Serving $dir on port $port" - end -end -``` - -`argparse` sets `_flag_NAME` variables for each matched flag. Use `set -ql` (query local) to check presence. - -### Function Scope - -Functions create a new scope. Variables from the calling scope are NOT accessible unless: - -```fish -# Method 1: --no-scope-shadowing (access ALL caller variables) -function modify_caller -S - set local_in_caller "modified" -end - -# Method 2: --inherit-variable (snapshot specific variable at definition time) -function make_closure - set -l captured "snapshot" - function inner -V captured - echo $captured # always "snapshot", even if original changes - end -end - -# Method 3: use global/universal scope explicitly -function set_global - set -g result "from function" -end -``` - -### Autoloading Functions - -Place each function in its own file at `~/.config/fish/functions/FUNCNAME.fish`. Fish loads it on first invocation: - -```fish -# ~/.config/fish/functions/mkcd.fish -function mkcd -d "Create and enter directory" - mkdir -p $argv[1] - cd $argv[1] -end -``` - -Autoloaded functions: - -- Load lazily (only when called) -- Override built-in functions of the same name -- Are the recommended way to define persistent functions - -Save an interactively-defined function: - -```fish -funcsave myfunction # writes to ~/.config/fish/functions/myfunction.fish -``` - -Edit a function interactively: - -```fish -funced myfunction # opens in editor, reloads on save -``` - -### Wrapping Commands - -Create aliases that inherit completions: - -```fish -function ls --wraps ls -d "ls with color" - command ls --color=auto $argv -end - -# The alias command is shorthand: -alias ll "ls -la" -# equivalent to: -function ll --wraps 'ls -la' -d 'alias ll=ls -la' - ls -la $argv -end -``` - -Always use `command` to call the original when wrapping, to prevent infinite recursion. Always pass `$argv` to forward arguments. - -## Abbreviations - -Abbreviations expand in the command line when Space or Enter is pressed. They differ from aliases: the expansion is visible and editable before execution. - -### Creating Abbreviations - -```fish -abbr -a gco git checkout -abbr -a gst git status -abbr -a gp git push -abbr -a gl git log --oneline --graph -``` - -### Position Control - -```fish -# command position only (default) -- expands only as first word -abbr -a gco git checkout - -# anywhere position -- expands anywhere in command line -abbr -a --position anywhere -- -C --color -abbr -a --position anywhere -- -H 'Accept: application/json' -``` - -### Command-Specific Abbreviations (Fish 4.0+) - -Expand only when typing arguments to a specific command: - -```fish -abbr --command git co checkout -abbr --command git br branch -abbr --command git ci commit -abbr --command git st status -abbr --command kubectl gp "get pods" -abbr --command=docker,podman ps "ps --format 'table {{.Names}}\t{{.Status}}'" -``` - -### Cursor Positioning - -Place the cursor at a specific position after expansion: - -```fish -# % is the default cursor marker -abbr -a L --position anywhere --set-cursor "| less" -# Typing: cat file L becomes: cat file | less (cursor after |) - -# Custom marker -abbr -a todo --set-cursor=CURSOR "# TODO: CURSOR" -``` - -### Function-Based Abbreviations - -Call a function to generate the expansion dynamically: - -```fish -function last_history_item - echo $history[1] -end -abbr -a !! --position anywhere --function last_history_item - -function multiline_git_commit - echo "git commit -m '"(commandline -t)"'" -end -abbr -a gc --function multiline_git_commit -``` - -### Regex Abbreviations - -Match patterns instead of literal text: - -```fish -abbr -a vim_texts --regex '.+\.txt' --function edit_with_vim -# Any .txt filename typed as a command expands via the function -``` - -### Managing Abbreviations - -```fish -abbr --list # list abbreviation names -abbr --show # show all with expansions (importable format) -abbr --erase gco # remove specific abbreviation -abbr --query gco # check if exists (exit status) -abbr --rename gco gch # rename -``` - -### Where to Define Abbreviations - -Define abbreviations in `config.fish` or a file in `conf.d/`: - -```fish -# ~/.config/fish/conf.d/abbr.fish -abbr -a gco git checkout -abbr -a gst git status -abbr -a gp git push -``` - -Do NOT use universal variables for abbreviations (the old `abbr --universal` is non-functional). - -## Event Handlers - -Functions can register as event handlers that fire automatically. - -### Named Events (--on-event) - -```fish -function on_fish_start --on-event fish_prompt - # Fires every time a prompt is about to be shown -end - -function on_exit --on-event fish_exit - echo "Goodbye!" -end - -function on_postexec --on-event fish_postexec - # Fires after every command execution - # $argv[1] contains the command that was run -end - -function on_preexec --on-event fish_preexec - # Fires before command execution -end -``` - -Built-in events: - -- `fish_prompt` -- before displaying prompt -- `fish_preexec` -- before executing a command -- `fish_postexec` -- after executing a command -- `fish_exit` -- when the shell exits -- `fish_cancel` -- when command line is cancelled (Ctrl+C) - -Custom events via `emit`: - -```fish -function handle_deploy --on-event deploy_complete - echo "Deployment finished: $argv" -end - -# Trigger the event -emit deploy_complete "v1.2.3" -``` - -### Variable Change Events (--on-variable) - -```fish -function on_pwd_change --on-variable PWD - echo "Changed to: $PWD" -end - -function on_path_change --on-variable PATH - echo "PATH updated" -end -``` - -### Signal Handlers (--on-signal) - -```fish -function on_winch --on-signal WINCH - echo "Terminal resized" -end - -function on_int --on-signal INT - echo "Caught interrupt" -end -``` - -### Job/Process Exit Events - -```fish -# When a specific background job exits -function notify_done --on-job-exit $job_pid - echo "Job $job_pid finished" -end - -# When any job started by current command exits -function on_caller_job --on-job-exit caller - echo "Background job completed" -end - -# When a specific child process exits -function on_proc_exit --on-process-exit $pid - echo "Process $pid exited with $argv[3]" -end -``` - -### Event Handler Placement - -Place event handlers in `conf.d/` files so they load before events fire: - -```fish -# ~/.config/fish/conf.d/events.fish -function __my_on_exit --on-event fish_exit - # cleanup logic -end -``` - -Prefix internal event handler names with `__` to indicate they are implementation details. - -### Fisher Plugin Events - -Fisher emits lifecycle events for plugins: - -```fish -function _myplugin_install --on-event myplugin_install - echo "Plugin installed" -end - -function _myplugin_update --on-event myplugin_update - echo "Plugin updated" -end - -function _myplugin_uninstall --on-event myplugin_uninstall - echo "Plugin removed, cleaning up" -end -``` - -## Useful Patterns - -### Checking Command Availability - -```fish -if command -sq docker - echo "Docker is installed" -end - -# -s: don't print path, -q: quiet -``` - -### Default Variable Values - -```fish -set -q MY_VAR; or set -g MY_VAR "default_value" -``` - -### Guard Against Empty Arguments - -```fish -function mycommand - if test (count $argv) -eq 0 - echo "Usage: mycommand FILE..." - return 1 - end - # proceed -end -``` - -### Temporary Directory Pattern - -```fish -function with_tmpdir - set -l tmpdir (mktemp -d) - # do work in $tmpdir - rm -rf $tmpdir -end -``` - -### Reading User Input - -```fish -read -l -P "Enter your name: " name -echo "Hello, $name" - -# With default -read -l -P "Continue? [Y/n] " -c "Y" answer - -# Silent (for passwords) -read -l -s -P "Password: " password -``` - -### Status Code Patterns - -```fish -# Chain with short-circuit -test -f file.txt; and cat file.txt; or echo "File not found" - -# Capture and check -some_command -set -l cmd_status $status -if test $cmd_status -ne 0 - echo "Failed with status $cmd_status" - return $cmd_status -end -``` - -### Complete Completion Script Example - -A production-quality completion script for a multi-subcommand CLI tool: - -```fish -# ~/.config/fish/completions/mytool.fish - -# Subcommands -set -l subcommands init build deploy status config - -# Disable file completions by default -complete -c mytool -f - -# Global options (available with any subcommand) -complete -c mytool -s h -l help -d "Show help" -complete -c mytool -s V -l version -d "Show version" -complete -c mytool -l config -r -F -d "Path to config file" -complete -c mytool -l verbose -d "Enable verbose logging" - -# Subcommand completions (only when no subcommand yet) -complete -c mytool -n "not __fish_seen_subcommand_from $subcommands" \ - -a init -d "Initialize new project" -complete -c mytool -n "not __fish_seen_subcommand_from $subcommands" \ - -a build -d "Build the project" -complete -c mytool -n "not __fish_seen_subcommand_from $subcommands" \ - -a deploy -d "Deploy to environment" -complete -c mytool -n "not __fish_seen_subcommand_from $subcommands" \ - -a status -d "Show current status" -complete -c mytool -n "not __fish_seen_subcommand_from $subcommands" \ - -a config -d "Manage configuration" - -# build subcommand options -complete -c mytool -n "__fish_seen_subcommand_from build" \ - -l target -x -a "debug release" -d "Build target" -complete -c mytool -n "__fish_seen_subcommand_from build" \ - -l jobs -x -a "(seq 1 (nproc 2>/dev/null; or echo 8))" -d "Parallel jobs" - -# deploy subcommand options -complete -c mytool -n "__fish_seen_subcommand_from deploy" \ - -l env -x -a "dev staging production" -d "Target environment" -complete -c mytool -n "__fish_seen_subcommand_from deploy" \ - -l dry-run -d "Preview without deploying" -complete -c mytool -n "__fish_seen_subcommand_from deploy" \ - -l tag -x -a "(git tag -l 'v*' 2>/dev/null | sort -rV)" -d "Version tag" - -# config subcommand has sub-subcommands -complete -c mytool -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set list" \ - -a "get" -d "Get config value" -complete -c mytool -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set list" \ - -a "set" -d "Set config value" -complete -c mytool -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set list" \ - -a "list" -d "List all config" -``` - -### Function Best Practices - -```fish -# Always use -d for discoverability -function serve -d "Start development server" - # ... -end - -# Always validate arguments -function deploy -a environment -d "Deploy to environment" - if not contains $environment staging production - echo "Error: environment must be 'staging' or 'production'" >&2 - return 1 - end - # ... -end - -# Use argparse for complex options rather than manual argv parsing -function build -d "Build project" - argparse h/help t/target= j/jobs= clean -- $argv - or return - # ... -end - -# Prefer returning status codes over printing errors for composability -function check_prereqs - command -sq docker; or return 1 - command -sq kubectl; or return 1 - return 0 -end - -if not check_prereqs - echo "Missing prerequisites" >&2 - return 1 -end -``` - -### Abbreviation Organization - -Keep abbreviations organized by category: - -```fish -# conf.d/abbr.fish - -# Git -abbr -a g git -abbr -a ga "git add" -abbr -a gc "git commit -v" -abbr -a gco "git checkout" -abbr -a gd "git diff" -abbr -a gl "git log --oneline --graph" -abbr -a gp "git push" -abbr -a gpl "git pull" -abbr -a gst "git status" - -# Docker -abbr -a d docker -abbr -a dc "docker compose" -abbr -a dps "docker ps" -abbr -a drm "docker rm" -abbr -a drmi "docker rmi" - -# Kubernetes -abbr -a k kubectl -abbr -a kgp "kubectl get pods" -abbr -a kgs "kubectl get svc" -abbr -a kgd "kubectl get deployments" -abbr -a kl "kubectl logs" -abbr -a ke "kubectl exec -it" - -# Navigation -abbr -a .. "cd .." -abbr -a ... "cd ../.." -abbr -a .... "cd ../../.." -``` diff --git a/packages/dotfiles/dot_agents/skills/fish-helper/references/fish-syntax.md b/packages/dotfiles/dot_agents/skills/fish-helper/references/fish-syntax.md deleted file mode 100644 index c400b9e5db..0000000000 --- a/packages/dotfiles/dot_agents/skills/fish-helper/references/fish-syntax.md +++ /dev/null @@ -1,815 +0,0 @@ -# Fish Syntax Reference - -## Variables - -### Setting Variables - -Use `set` to create and modify variables. Fish has no `VAR=value` assignment syntax. - -```fish -set name "Alice" -set count 42 -set empty_list -``` - -### Variable Expansion - -Access variables with the `$` prefix. Separate variable names from adjacent text with quotes or braces: - -```fish -echo $name -echo "Hello, $name!" -echo "The {$name}s" # brace separation -echo "The "$name"s" # quote separation -``` - -### Variable Scopes - -Fish provides four scoping levels, specified as flags to `set`: - -| Flag | Scope | Lifetime | -| ---- | --------- | ------------------------------- | -| `-l` | Local | Current block only | -| `-f` | Function | Current function | -| `-g` | Global | Current shell session | -| `-U` | Universal | All sessions, persisted to disk | - -```fish -set -l temp "block-scoped" -set -g session_var "session-scoped" -set -U EDITOR vim # persists across all fish instances and reboots -``` - -When no scope flag is given, `set` uses the narrowest existing scope for that variable name. If the variable does not exist, it creates it in function scope (inside a function) or global scope (outside). - -### Exporting Variables - -Export variables to child processes with `-x` (or `--export`). Combine with scope flags: - -```fish -set -gx PATH /usr/local/bin $PATH # global + exported -set -lx TEMP_VAR "for child only" # local + exported -set -gxu MY_VAR # unexport with -u -``` - -By convention, exported variables use UPPERCASE names. - -### Erasing Variables - -```fish -set -e variable_name -set -eg GLOBAL_VAR # erase global specifically -set -eU UNIVERSAL_VAR # erase universal specifically -``` - -### Querying Variables - -```fish -set -q variable_name # returns 0 if set, 1 otherwise -set -q variable_name[2] # check if second element exists - -if set -q MY_VAR - echo "MY_VAR is defined" -end -``` - -### Special Variables - -| Variable | Description | -| --------------- | ------------------------------------------------- | -| `$status` | Exit status of last command (like bash `$?`) | -| `$pipestatus` | List of exit statuses from last pipeline | -| `$argv` | Arguments to current function/script | -| `$fish_pid` | PID of the fish process (like bash `$$`) | -| `$last_pid` | PID of last backgrounded process (like bash `$!`) | -| `$PATH` | Command search path | -| `$PWD` | Current working directory | -| `$HOME` | User home directory | -| `$USER` | Current username | -| `$HOSTNAME` | System hostname | -| `$fish_version` | Fish version string | -| `$fish_trace` | Set to 1 to enable execution tracing | -| `$SHLVL` | Shell nesting level | - -### PATH Variables - -Variables with names ending in `PATH` receive special treatment -- they split and join on colons automatically: - -```fish -set -gx MANPATH /usr/local/share/man /usr/share/man -echo "$MANPATH" # /usr/local/share/man:/usr/share/man - -set MYPATH "1:2:3" -echo $MYPATH # 1 2 3 (split into list) -``` - -Use `fish_add_path` to prepend directories to `$PATH` persistently: - -```fish -fish_add_path /usr/local/bin -fish_add_path ~/.cargo/bin -``` - -## Lists - -All fish variables are lists. A variable holding a single value is a list of length 1. - -### Creating Lists - -```fish -set colors red green blue -set empty_list # empty list (0 elements) -set single "just one" # list of 1 element -``` - -### Indexing (1-based) - -```fish -set list a b c d e -echo $list[1] # a -echo $list[3] # c -echo $list[-1] # e (last element) -echo $list[-2] # d (second-to-last) -``` - -### Slicing - -```fish -echo $list[2..4] # b c d -echo $list[3..] # c d e (from 3 to end) -echo $list[..2] # a b (from start to 2) -echo $list[-1..1] # e d c b a (reversed) -``` - -### List Operations - -```fish -# Count elements -count $list # 5 - -# Append -set -a list f g # list is now a b c d e f g -set list $list "new item" # equivalent - -# Prepend -set -p list z # z a b c d e f g - -# Check if empty -if test (count $list) -eq 0 - echo "Empty list" -end - -# Contains check -if contains blue $colors - echo "Found blue" -end - -# Index of element -set idx (contains -i green $colors) # returns index or fails -``` - -### Cartesian Product - -When combining a list variable with text, fish produces the Cartesian product: - -```fish -set ext c h -echo file.$ext # file.c file.h -echo {a,b}$ext # ac ah bc bh -``` - -### Empty List Behavior - -An empty variable expands to nothing (not an empty string): - -```fish -set empty -echo prefix$empty suffix # prints: prefixsuffix (one argument lost) -echo "prefix${empty}suffix" # prints: prefixsuffix -``` - -## Command Substitution - -Capture command output with parentheses: - -```fish -set files (ls) -set today (date +%Y-%m-%d) -echo "You are in $(pwd)" - -# Inside double quotes, use $() form -set content "$(cat file.txt)" # single string, preserves newlines -set lines (cat file.txt) # list, split on newlines -``` - -Fish splits command substitution output on newlines by default (not whitespace like bash). To prevent splitting, use double quotes. - -### Piping to String Split - -For splitting on other delimiters: - -```fish -set words (echo "a,b,c" | string split ",") # a b c as list -``` - -## Piping and Redirections - -### Pipes - -```fish -command1 | command2 # stdout of cmd1 to stdin of cmd2 -command1 2>| command2 # stderr of cmd1 to stdin of cmd2 -command1 &| command2 # stdout+stderr of cmd1 to stdin of cmd2 -``` - -### Output Redirections - -```fish -command > file.txt # write stdout to file (truncate) -command >> file.txt # append stdout to file -command 2> errors.txt # write stderr to file -command 2>> errors.txt # append stderr to file -command &> all.txt # stdout+stderr to file -command > /dev/null # discard stdout -command &> /dev/null # discard all output -``` - -### Input Redirection - -```fish -command < input.txt -``` - -### File Descriptor Redirection - -```fish -command 2>&1 # stderr to stdout -command 1>&2 # stdout to stderr -``` - -### No Heredocs - -Fish does not support heredocs. Alternatives: - -```fish -# Multi-line echo -echo "line 1 -line 2 -line 3" | command - -# printf for precise control -printf '%s\n' "line 1" "line 2" "line 3" | command - -# Or read from a temporary file -``` - -## Control Flow - -### if / else if / else - -Conditional execution based on command exit status (0 = true): - -```fish -if test -f config.toml - echo "Config found" -else if test -f config.yaml - echo "YAML config found" -else - echo "No config" -end -``` - -Combine conditions: - -```fish -if test -f file.txt; and test -r file.txt - echo "File exists and is readable" -end - -# Or with && and || -if test -d src && test -f src/main.rs - echo "Rust project detected" -end -``` - -### test / [ ] Conditions - -Fish supports `test` and `[` but NOT `[[`: - -```fish -# File tests -test -e path # exists -test -f path # regular file -test -d path # directory -test -L path # symlink -test -r path # readable -test -w path # writable -test -x path # executable -test -s path # non-empty file - -# String tests -test -n "$var" # non-empty string -test -z "$var" # empty string -test "$a" = "$b" # string equality -test "$a" != "$b" # string inequality - -# Numeric tests -test "$n" -eq 5 # equal -test "$n" -ne 5 # not equal -test "$n" -gt 5 # greater than -test "$n" -ge 5 # greater or equal -test "$n" -lt 5 # less than -test "$n" -le 5 # less or equal - -# Logical operators within test -test -f a -a -f b # AND -test -f a -o -f b # OR -test ! -f a # NOT -``` - -Always quote variable expansions in test expressions to handle empty values: - -```fish -# WRONG: breaks if $var is empty -if test $var = "value" - -# CORRECT: -if test "$var" = "value" -``` - -### switch / case - -Pattern matching with glob support: - -```fish -switch $animal -case cat - echo "Meow" -case 'dog' 'wolf' - echo "Woof" -case '*.fish' - echo "A fish file" -case '*' - echo "Unknown" -end -``` - -No fallthrough between cases. The first matching case executes and control leaves the switch. - -### for Loops - -```fish -for file in *.txt - echo "Processing $file" -end - -for i in (seq 1 10) - echo $i -end - -for color in red green blue - echo $color -end - -for arg in $argv - echo "Argument: $arg" -end -``` - -### while Loops - -```fish -while test $count -gt 0 - echo $count - set count (math $count - 1) -end - -# Read lines from file -while read -l line - echo ">> $line" -end < input.txt - -# Read lines from command output -command | while read -l line - echo $line -end -``` - -### Loop Control - -```fish -for i in (seq 100) - if test $i -eq 50 - break # exit loop - end - if math "$i % 2" > /dev/null - continue # skip to next iteration - end - echo $i -end -``` - -### Logical Combiners - -```fish -command1 && command2 # run cmd2 only if cmd1 succeeds -command1 || command2 # run cmd2 only if cmd1 fails -not command # invert exit status - -# Keyword forms (equivalent, lower precedence) -command1; and command2 -command1; or command2 -``` - -### begin / end Blocks - -Group commands for redirection or scoping: - -```fish -begin - set -l temp_var "scoped" - echo "inside: $temp_var" -end > output.txt -# temp_var not available here - -# Brace syntax (Fish 4.1+) -{ echo line1; echo line2 } > output.txt -``` - -## String Builtin - -The `string` command handles all string manipulation: - -### string length - -```fish -string length "hello" # 5 -string length -V "emoji: 🐟" # visible width: 9 -``` - -### string sub - -```fish -string sub -s 2 -l 3 "hello" # ell -string sub -s -3 "hello" # llo -string sub -e 3 "hello" # hel -``` - -### string split / split0 - -```fish -string split "," "a,b,c" # a\nb\nc (list) -string split -m 1 "," "a,b,c" # a\nb,c (max 1 split) -string split -r -m 1 "/" "/a/b/c" # /a/b\nc (right-to-left) -string split0 < null-delimited-input -``` - -### string join / join0 - -```fish -string join "," a b c # a,b,c -string join \n "line1" "line2" # line1\nline2 -string join0 a b c # null-byte separated -``` - -### string match - -```fish -# Glob matching -string match "*.txt" "readme.txt" # readme.txt -string match -v "*.log" $files # exclude .log files - -# Regex matching -string match -r '(\d+)\.(\d+)' "v1.23" -# Match: v1.23, Group 1: 1, Group 2: 23 - -string match -rg '(\w+)=(\w+)' "key=val" -# Groups only: key\nval - -# Case-insensitive -string match -ri 'hello' "HELLO" - -# All matches -string match -ra '\d+' "a1b2c3" # 1\n2\n3 -``` - -### string replace - -```fish -string replace "old" "new" "the old text" # the new text -string replace -a "o" "0" "foo boo" # f00 b00 -string replace -r '(\w+)' 'word:$1' "hello" # word:hello -string replace -r '\s+' ' ' "too many spaces" # too many spaces -string replace -f "pattern" "new" $strings # filter: only output changed -``` - -### string trim - -```fish -string trim " hello " # hello -string trim -l " hello " # "hello " -string trim -r " hello " # " hello" -string trim -c "x" "xxxhelloxxx" # hello -``` - -### string upper / lower - -```fish -string upper "hello" # HELLO -string lower "HELLO" # hello -``` - -### string pad - -```fish -string pad -w 10 "hello" # " hello" -string pad -w 10 -r "hello" # "hello " -string pad -w 10 -C "hello" # " hello " (center, Fish 4.1+) -string pad -c 0 -w 5 42 # 00042 -``` - -### string repeat - -```fish -string repeat -n 3 "ab" # ababab -string repeat -n 3 -m 5 "abc" # abcab (max 5 chars) -``` - -### string escape / unescape - -```fish -string escape "hello world" # hello\ world -string escape --style=url "hello world" # hello%20world -string escape --style=var "my-var" # my_2Dvar -string unescape "hello\\ world" # hello world -``` - -### string shorten - -```fish -string shorten -m 10 "a long string" # a long st… -string shorten -m 10 -c "..." "long" # long st... -``` - -### string collect - -```fish -# Collapse multi-line output into single argument -echo -e "a\nb\nc" | string collect # "a\nb\nc" as one argument -``` - -## Math Builtin - -Evaluate arithmetic expressions: - -```fish -math 2 + 2 # 4 -math "10 / 3" # 3.333333 -math "10 % 3" # 1 -math "2 ^ 10" # 1024 -math "floor(3.7)" # 3 -math "ceil(3.2)" # 4 -math "round(3.5)" # 4 -math "abs(-5)" # 5 -math "sqrt(144)" # 12 -math "sin(pi)" # ~0 -math "log2(1024)" # 10 -math "max(3, 7)" # 7 -math "min(3, 7)" # 3 -``` - -Use in variable assignment: - -```fish -set result (math "$x + $y") -set hex (math --base=hex 255) # 0xff -``` - -### Operators - -| Operator | Description | -| --------------------------- | -------------------------- | -| `+` `-` `*` `/` | Basic arithmetic | -| `%` | Modulo | -| `^` | Exponentiation | -| `( )` | Grouping | -| `>` `<` `>=` `<=` `==` `!=` | Comparison (return 0 or 1) | - -### Functions Available in math - -`abs`, `acos`, `asin`, `atan`, `atan2`, `bitand`, `bitor`, `bitxor`, `ceil`, `cos`, `exp`, `fac`, `floor`, `ln`, `log`, `log2`, `log10`, `max`, `min`, `ncr`, `npr`, `pow`, `round`, `sin`, `sqrt`, `tan` - -Constants: `pi`, `e`, `tau`, `inf` - -## Exit Status - -Every command returns an integer exit status (0 = success): - -```fish -command_that_succeeds -echo $status # 0 - -command_that_fails -echo $status # non-zero - -# Pipeline status -cat file | grep pattern | wc -l -echo $pipestatus # list of all exit codes in pipeline -echo $pipestatus[2] # grep's exit code specifically -``` - -### Return from Functions - -```fish -function check_file - if not test -f $argv[1] - return 1 - end - return 0 -end -``` - -## Quoting Rules - -### Single Quotes - -Prevent all expansions. Only `\'` and `\\` are special inside single quotes: - -```fish -echo 'No $expansion here' -echo 'Literal (parentheses)' -echo 'Use \'single quotes\' inside' -``` - -### Double Quotes - -Allow variable expansion and command substitution. Prevent globbing and splitting: - -```fish -echo "Hello $name" -echo "Current dir: $(pwd)" -echo "Literal \$dollar" -echo "List as one arg: $mylist" # elements joined by space -``` - -### No Quotes - -All expansions apply. Glob patterns match files. Variables expand to multiple arguments: - -```fish -set list a b c -echo $list # three arguments: a b c -echo *.txt # matches files -echo \$literal # escape special chars with backslash -``` - -### Escape Sequences (Outside Quotes) - -| Sequence | Result | -| ------------ | ----------------- | -| `\\` | Literal backslash | -| `\n` | Newline | -| `\t` | Tab | -| `\r` | Carriage return | -| `\xHH` | Hex byte | -| `\uXXXX` | Unicode codepoint | -| `\UXXXXXXXX` | Extended Unicode | -| `\a` | Alert (bell) | -| `\e` | Escape character | - -## Wildcards and Globbing - -```fish -ls *.txt # all .txt files in current dir -ls **/*.rs # recursive: all .rs files in any subdirectory -ls file?.txt # ? matches single char (deprecated, use qmark-noglob) -ls {src,test}/*.fish # brace expansion + glob -``` - -Hidden files (starting with `.`) are not matched unless the pattern explicitly starts with `.`: - -```fish -ls .* # matches hidden files -ls * # does NOT match hidden files -``` - -If a glob matches nothing, the command fails with status 124 (unless used with `for`, `set`, or `count`). - -## Brace Expansion - -```fish -echo {a,b,c} # a b c -echo file.{txt,md} # file.txt file.md -cp file{,.bak} # cp file file.bak -echo {1..5} # NOT supported (use seq instead) -``` - -Braces require commas or variable expansion to trigger expansion. Literal braces without commas pass through unchanged. - -## Practical Patterns - -### String Processing Pipelines - -```fish -# Extract field from colon-delimited line -echo "user:1000:group" | string split ":" | head -1 - -# Process CSV-like data -for line in (cat data.csv) - set fields (string split "," $line) - echo "Name: $fields[1], Age: $fields[2]" -end - -# Remove file extensions -for f in *.tar.gz - set base (string replace -r '\.tar\.gz$' '' $f) - echo $base -end - -# Validate email format -function is_email - string match -rq '^[^@]+@[^@]+\.[^@]+$' $argv[1] -end -``` - -### Safe Variable Patterns - -```fish -# Default values -set -q MY_PORT; or set MY_PORT 8080 - -# Coalesce: use first non-empty value -set -l result -for var in $PREFERRED $FALLBACK $DEFAULT - if test -n "$var" - set result $var - break - end -end - -# Check variable is set and non-empty -if set -q var; and test -n "$var" - echo "var is set and non-empty: $var" -end -``` - -### Error Handling - -```fish -# Check command existence before use -if not command -sq jq - echo "Error: jq is required but not installed" >&2 - return 1 -end - -# Capture stderr -set -l output (command 2>&1) -set -l code $status - -# Die pattern -function die - echo "Error: $argv" >&2 - return 1 -end - -test -f config.toml; or die "config.toml not found" -``` - -### Temporary File Patterns - -```fish -# Create and clean up temp files -set -l tmpfile (mktemp) -echo "data" > $tmpfile -# ... use $tmpfile ... -rm -f $tmpfile - -# Temp directory with cleanup -function with_temp - set -l tmpdir (mktemp -d) - pushd $tmpdir - eval $argv - popd - rm -rf $tmpdir -end -``` - -### Parallel Execution - -```fish -# Background jobs -for host in server1 server2 server3 - ssh $host "uptime" & -end -wait # wait for all background jobs - -# With status collection -set -l pids -for task in $tasks - process_task $task & - set -a pids $last_pid -end -for pid in $pids - wait $pid -end -``` diff --git a/packages/dotfiles/dot_agents/skills/fish-helper/references/functions-completions-config.md b/packages/dotfiles/dot_agents/skills/fish-helper/references/functions-completions-config.md new file mode 100644 index 0000000000..4439a87220 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/fish-helper/references/functions-completions-config.md @@ -0,0 +1,61 @@ +# Fish functions, completions, and configuration + +Read this when defining functions, abbreviations, completions, bindings, events, prompts, themes, or startup configuration. + +## Functions + +Use `function` for logic and `funcsave` only when intentional user state should be written to the function path. Version-controlled functions should remain source-managed. + +## Abbreviations + +Abbreviations expand interactive input. `--set-cursor` uses `%` unless another marker is configured; the expansion must contain it. + +## Completions + +Use repeated `--command` or brace expansion for multiple command names. `--command=docker,podman` registers the literal comma-containing name. + +Bundled completions are embedded in Fish 4.8. `status list-files` lists embedded files. Custom completions still use the documented completion path and should be fast and side-effect-free. + +## Events + +Functions can observe variables, signals, process exits, generic events, and newer events such as `fish_posterror`, `fish_focus_in`, and `fish_focus_out`. + +Variable updates can be coalesced and same-value sets can still produce events. Handler order is unspecified. Use an explicit coordinator when ordering matters. + +## Startup + +Config search includes user `conf.d`, system config, and user/vendor data directories with documented priority and filename shadowing. Environment required in noninteractive shells belongs before `status is-interactive; or return`. + +## Prompt + +Transient prompt is enabled with: + +```fish +set -g fish_transient_prompt 1 +``` + +Prompt functions receive `--final-rendering` on the final repaint. They should avoid slow network/process calls and remain deterministic. + +## Theme + +Use `fish_config theme choose`. Theme files contain name and value separated by whitespace. Universal color variables can disable adaptive light/dark behavior. + +## Primary documentation + +- [Interactive use](https://fishshell.com/docs/current/interactive.html) +- [Completions](https://fishshell.com/docs/current/completions.html) +- [Prompt](https://fishshell.com/docs/current/prompt.html) +- [Design](https://fishshell.com/docs/current/design.html) +- [Terminal compatibility](https://fishshell.com/docs/current/terminal-compatibility.html) +- [abbr](https://fishshell.com/docs/current/cmds/abbr.html) +- [complete](https://fishshell.com/docs/current/cmds/complete.html) +- [function](https://fishshell.com/docs/current/cmds/function.html) +- [functions](https://fishshell.com/docs/current/cmds/functions.html) +- [funcsave](https://fishshell.com/docs/current/cmds/funcsave.html) +- [funced](https://fishshell.com/docs/current/cmds/funced.html) +- [bind](https://fishshell.com/docs/current/cmds/bind.html) +- [emit](https://fishshell.com/docs/current/cmds/emit.html) +- [fish_add_path](https://fishshell.com/docs/current/cmds/fish_add_path.html) +- [fish_config](https://fishshell.com/docs/current/cmds/fish_config.html) +- [set_color](https://fishshell.com/docs/current/cmds/set_color.html) +- [fish_key_reader](https://fishshell.com/docs/current/cmds/fish_key_reader.html) diff --git a/packages/dotfiles/dot_agents/skills/fish-helper/references/plugins-and-testing.md b/packages/dotfiles/dot_agents/skills/fish-helper/references/plugins-and-testing.md new file mode 100644 index 0000000000..42d73d3891 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/fish-helper/references/plugins-and-testing.md @@ -0,0 +1,36 @@ +# Fish plugins and testing + +Read this when installing a plugin manager, selecting Fish plugins, or testing functions/configuration. + +## Installation safety + +Upstream quick installs sometimes pipe a mutable remote script into Fish. For automation, download a reviewed release or commit, verify it, and source the local file. Plugin installation runs third-party code and mutates Fish configuration/state. + +Required plugin managers and tools should fail fast. Optional integrations may be conditional, but label them optional and keep their absence from changing required environment setup. + +## Plugin scope + +- Fisher is a small Fish plugin manager. +- nvm.fish supports current aliases such as `lts`, `latest`, `.nvmrc`, and `.node-version`; avoid hard-coding a stale Node major in generic guidance. +- fzf.fish requires current Fish/fzf and platform helpers and can conflict with other fzf plugins. +- Tide supplies a prompt; verify its current Fish compatibility rather than repeating stale README version prose. +- done, autopair.fish, and Oh My Fish have distinct platform and startup behavior; use them only when the project/user wants that functionality. + +Avoid popularity and “zero overhead” comparisons without current measurements. + +## Tests + +Fishtape is one Fish-native test option. Test exact stdout, stderr, status, environment changes, and file effects. Keep required commands available; do not turn missing tools into silent skips. + +Format/check scripts with `fish_indent --check` where supported by the installed Fish release, and execute representative interactive/noninteractive startup paths. + +## Primary projects + +- [Fisher](https://github.com/jorgebucaran/fisher) +- [nvm.fish](https://github.com/jorgebucaran/nvm.fish) +- [fzf.fish](https://github.com/PatrickF1/fzf.fish) +- [Tide](https://github.com/IlanCosman/tide) +- [done](https://github.com/franciscolourenco/done) +- [autopair.fish](https://github.com/jorgebucaran/autopair.fish) +- [Fishtape](https://github.com/jorgebucaran/fishtape) +- [Oh My Fish](https://github.com/oh-my-fish/oh-my-fish) diff --git a/packages/dotfiles/dot_agents/skills/fish-helper/references/plugins-config.md b/packages/dotfiles/dot_agents/skills/fish-helper/references/plugins-config.md deleted file mode 100644 index 97213f56d8..0000000000 --- a/packages/dotfiles/dot_agents/skills/fish-helper/references/plugins-config.md +++ /dev/null @@ -1,573 +0,0 @@ -# Fish Plugins and Configuration - -## Fisher Plugin Manager - -Fisher is the most popular plugin manager for Fish. It is pure-Fish, has zero startup overhead, and requires no configuration. - -### Installation - -```fish -curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher -``` - -### Commands - -```fish -# Install a plugin from GitHub -fisher install jorgebucaran/nvm.fish - -# Install specific version/branch/tag -fisher install IlanCosman/tide@v6 - -# Install from local directory -fisher install ~/my-plugin - -# Install from GitLab -fisher install gitlab.com/user/repo - -# List installed plugins -fisher list - -# Update all plugins -fisher update - -# Update specific plugin -fisher update jorgebucaran/fisher - -# Remove a plugin -fisher remove jorgebucaran/nvm.fish - -# Remove all plugins -fisher list | fisher remove -``` - -### fish_plugins File - -Fisher records installed plugins in `$__fish_config_dir/fish_plugins` (typically `~/.config/fish/fish_plugins`). This file enables declarative plugin management: - -``` -# ~/.config/fish/fish_plugins -jorgebucaran/fisher -jorgebucaran/nvm.fish -IlanCosman/tide@v6 -PatrickF1/fzf.fish -jethrokuan/z -``` - -Manually edit this file and run `fisher update` to sync -- Fisher installs new entries, removes deleted lines, and updates existing plugins. - -Add `fish_plugins` to version control for reproducible setups across machines. - -### Plugin Directory Structure - -A Fisher plugin is a Git repository containing any combination of: - -``` -plugin-name/ - functions/ # Autoloaded functions - my_function.fish - completions/ # Command completions - my_command.fish - conf.d/ # Startup configuration scripts - plugin_init.fish - themes/ # Color themes (.theme files, Fish 3.4+) - my_theme.theme -``` - -### Plugin Lifecycle Events - -Plugins receive events during Fisher operations. Place handlers in `conf.d/` so they load before events fire: - -```fish -# conf.d/my_plugin.fish -function _my_plugin_install --on-event my_plugin_install - # Run after fisher install -end - -function _my_plugin_update --on-event my_plugin_update - # Run after fisher update -end - -function _my_plugin_uninstall --on-event my_plugin_uninstall - # Cleanup: remove universal variables, temp files, etc. -end -``` - -## Popular Plugins - -### Directory Navigation - -**z** (`jethrokuan/z`) -- Frecency-based directory jumping: - -```fish -fisher install jethrokuan/z - -z project # jump to most frecent directory matching "project" -z -l # list tracked directories -z -c pattern # restrict to subdirectories of $PWD -z --clean # remove non-existent directories from database -``` - -**zoxide** -- Smarter cd alternative (standalone binary with Fish integration): - -```fish -# Install zoxide binary first, then initialize -zoxide init fish | source - -z project # jump to best match -zi project # interactive selection with fzf -``` - -### Fuzzy Finding - -**fzf.fish** (`PatrickF1/fzf.fish`) -- fzf integration with keybindings: - -```fish -fisher install PatrickF1/fzf.fish - -# Default keybindings: -# Ctrl+Alt+F -- search files -# Ctrl+Alt+L -- search git log -# Ctrl+Alt+S -- search git status -# Ctrl+Alt+P -- search processes -# Ctrl+R -- search command history -``` - -**jethrokuan/fzf** -- Alternative fzf integration: - -```fish -fisher install jethrokuan/fzf - -# Ctrl+O -- find file -# Ctrl+R -- search history -# Alt+C -- cd to directory -# Alt+O -- open file in editor -# Alt+Shift+O -- open file in editor (git-tracked) -``` - -### Notifications - -**done** (`franciscolourenco/done`) -- Notify when long commands finish: - -```fish -fisher install franciscolourenco/done - -# Automatically sends a notification when a command takes longer than -# $__done_min_cmd_duration (default: 5 seconds) and terminal is not focused -# Supports macOS, Linux (notify-send), and Windows (BurntToast) - -set -U __done_min_cmd_duration 10000 # 10 seconds (in ms) -set -U __done_notify_sound 1 # enable sound -``` - -### Auto-pairing - -**autopair** (`jorgebucaran/autopair.fish`) -- Auto-close brackets, quotes, etc: - -```fish -fisher install jorgebucaran/autopair.fish - -# Automatically pairs: () [] {} "" '' -# Skips closing char if already present -# Backspace removes both characters of an empty pair -``` - -**pisces** (`laughedelic/pisces`) -- Alternative auto-pairing: - -```fish -fisher install laughedelic/pisces -``` - -### Bash Compatibility - -**bax** (`jorgebucaran/bax.fish`) -- Run bash commands/scripts from Fish: - -```fish -fisher install jorgebucaran/bax.fish - -bax 'export FOO=bar && echo $FOO' -bax source script.sh -``` - -**bass** -- Alternative bash-to-fish bridge (older, less maintained): - -```fish -fisher install edc/bass -bass source script.sh -``` - -### Node.js Version Management - -**nvm.fish** (`jorgebucaran/nvm.fish`) -- Pure-Fish Node.js version manager: - -```fish -fisher install jorgebucaran/nvm.fish - -nvm install 20 # install Node 20 -nvm use 20 # switch to Node 20 -nvm list # list installed versions -nvm list-remote # list available versions -set -U nvm_default_version 20 # set default -``` - -### Testing - -**fishtape** (`jorgebucaran/fishtape`) -- TAP-based test runner: - -```fish -fisher install jorgebucaran/fishtape - -# test.fish -@test "math works" (math 2 + 2) = 4 -@test "string works" (string upper hello) = HELLO - -fishtape test.fish -``` - -### Other Useful Plugins - -- **abbreviation-tips** (`Gazorby/fish-abbreviation-tips`) -- Reminds you of abbreviations -- **spark** (`jorgebucaran/spark.fish`) -- Sparkline generator -- **gitnow** (`joseluisq/gitnow`) -- Git workflow shortcuts -- **virtualfish** (`adambrenecki/virtualfish`) -- Python virtualenv wrapper -- **colored-man-pages** -- Colorize man pages -- **fish-async-prompt** (`acomagu/fish-async-prompt`) -- Async prompt rendering - -## Configuration Patterns - -### config.fish - -The main configuration file at `~/.config/fish/config.fish`. Keep it lean -- use `conf.d/` for modular organization: - -```fish -# ~/.config/fish/config.fish - -# Only run in interactive shells -if not status is-interactive - return -end - -# Environment variables -set -gx EDITOR nvim -set -gx VISUAL nvim -set -gx PAGER less -set -gx LANG en_US.UTF-8 - -# PATH additions -fish_add_path ~/.local/bin -fish_add_path ~/.cargo/bin -fish_add_path ~/go/bin - -# Disable greeting -set -g fish_greeting - -# Abbreviations (or put in conf.d/abbr.fish) -abbr -a g git -abbr -a gco git checkout -abbr -a gst git status -abbr -a gp git push -``` - -### conf.d/ Directory - -Files in `~/.config/fish/conf.d/` execute before `config.fish`, in alphabetical order. Use this for modular configuration: - -``` -conf.d/ - 00-env.fish # environment variables - 10-path.fish # PATH configuration - 20-abbr.fish # abbreviations - 30-aliases.fish # function aliases - 50-tools.fish # tool initialization (starship, zoxide, etc.) - 99-local.fish # machine-specific overrides -``` - -Prefix with numbers to control execution order. - -### Example conf.d Files - -```fish -# conf.d/00-env.fish -set -gx EDITOR nvim -set -gx GOPATH ~/go -set -gx DOCKER_BUILDKIT 1 - -# conf.d/10-path.fish -fish_add_path ~/.local/bin -fish_add_path ~/.cargo/bin -fish_add_path $GOPATH/bin - -# conf.d/20-abbr.fish -abbr -a g git -abbr -a k kubectl -abbr -a d docker -abbr -a dc docker compose -abbr --command git co checkout -abbr --command git br branch -abbr --command git ci "commit -v" -abbr --command kubectl gp "get pods" -abbr --command kubectl gs "get svc" - -# conf.d/50-tools.fish -# Initialize Starship prompt -if command -sq starship - starship init fish | source -end - -# Initialize zoxide -if command -sq zoxide - zoxide init fish | source -end - -# Initialize direnv -if command -sq direnv - direnv hook fish | source -end -``` - -### functions/ Directory - -Each function lives in its own file at `~/.config/fish/functions/NAME.fish`: - -```fish -# functions/mkcd.fish -function mkcd -d "Create and enter directory" - mkdir -p $argv[1]; and cd $argv[1] -end - -# functions/fish_greeting.fish -function fish_greeting - # Empty to disable, or customize: - echo "Welcome to "(set_color cyan)(prompt_hostname)(set_color normal) -end -``` - -### Universal Variables vs Config Files - -Universal variables (`set -U`) persist across sessions without config files. Use them for: - -- User preferences that rarely change -- Plugin configuration -- Theme settings - -Prefer `config.fish` or `conf.d/` for: - -- PATH modifications (use `fish_add_path` instead of raw `set -U`) -- Abbreviations (the old universal storage is deprecated) -- Configuration that should be version-controlled - -Fish 4.3+ moved away from universal variables toward global defaults for cleaner configuration. - -## Prompt Customization - -### Built-in Prompt Functions - -```fish -# Left prompt (required) -function fish_prompt - set -l last_status $status - set -l cwd (prompt_pwd) - - if test $last_status -ne 0 - set_color red - else - set_color green - end - echo -n "$ " - set_color normal - echo -n "$cwd> " -end - -# Right prompt (optional) -function fish_right_prompt - set_color brblack - echo (date +%H:%M) - set_color normal -end - -# Vi mode indicator (optional, only with vi keybindings) -function fish_mode_prompt - switch $fish_bind_mode - case default - set_color red - echo "[N] " - case insert - set_color green - echo "[I] " - case replace_one replace - set_color yellow - echo "[R] " - case visual - set_color magenta - echo "[V] " - end - set_color normal -end - -# Transient prompt (Fish 4.1+) -# Shown in place of fish_prompt after command execution -function fish_transient_prompt - echo -n "$ " -end -``` - -### set_color - -```fish -set_color red # named color -set_color brgreen # bright green -set_color 0F0 # hex color -set_color --bold red # bold -set_color --underline # underline -set_color --italics # italic -set_color --dim # dim -set_color --reverse # reverse video -set_color -b blue # background color -set_color normal # reset all -``` - -### Useful Prompt Helpers - -```fish -prompt_pwd # shortened $PWD (~/P/fish-helper) -prompt_pwd --full-length-dirs 2 # keep last 2 dirs full -prompt_hostname # short hostname -fish_vcs_prompt # git/hg/svn status -fish_git_prompt # git-specific prompt info -``` - -### Git Prompt Variables - -Configure `fish_git_prompt` output: - -```fish -set -g __fish_git_prompt_show_informative_status 1 -set -g __fish_git_prompt_showcolorhints 1 -set -g __fish_git_prompt_showuntrackedfiles 1 -set -g __fish_git_prompt_showdirtystate 1 -set -g __fish_git_prompt_showstashstate 1 -set -g __fish_git_prompt_showupstream informative -``` - -### Starship Integration - -Starship is a popular cross-shell prompt. Initialize in Fish: - -```fish -# conf.d/starship.fish -if command -sq starship - starship init fish | source -end -``` - -Starship replaces `fish_prompt` and `fish_right_prompt` with its own. Configure via `~/.config/starship.toml`. - -### Tide Prompt - -Tide is a Fish-specific prompt framework with async rendering: - -```fish -fisher install IlanCosman/tide@v6 -tide configure # interactive setup wizard -``` - -Features: async git info, multi-line prompt, vi mode indicator, transient prompt, and configurable segments. - -## Theme Management - -### Built-in Themes - -```fish -fish_config theme show # list available themes -fish_config theme choose monokai # preview and apply a theme -fish_config theme save # save current colors -``` - -### Theme Variables - -Key color variables: - -```fish -set -U fish_color_command blue # commands -set -U fish_color_error red # errors -set -U fish_color_param normal # parameters -set -U fish_color_comment brblack # comments -set -U fish_color_autosuggestion brblack # autosuggestions -set -U fish_color_valid_path --underline # valid file paths -set -U fish_color_operator cyan # operators -set -U fish_color_escape cyan # escape sequences -set -U fish_color_quote yellow # quoted strings -set -U fish_color_redirection cyan # redirections -``` - -### Adaptive Themes (Fish 4.3+) - -Theme files can include both light and dark sections: - -``` -# mytheme.theme -[light] -fish_color_command = blue -fish_color_error = red - -[dark] -fish_color_command = brblue -fish_color_error = brred -``` - -Fish selects the appropriate section based on terminal background detection. - -## Other Plugin Managers - -### Oh My Fish (OMF) - -Heavier framework with its own package ecosystem: - -```fish -curl https://raw.githubusercontent.com/oh-my-fish/oh-my-fish/master/bin/install | fish -omf install z -omf theme bobthefish -``` - -### Fundle - -Config-file-based manager inspired by Vim's Vundle: - -```fish -# config.fish -fundle plugin 'jethrokuan/z' -fundle plugin 'edc/bass' -fundle init -``` - -Fisher is generally preferred for its simplicity and zero-overhead approach. - -## Tips - -### Conditional Tool Initialization - -Only initialize tools when they are installed: - -```fish -command -sq tool; and tool init fish | source -``` - -### Performance - -- Prefer autoloaded functions over defining everything in `config.fish` -- Use `fish_add_path` instead of manually prepending to `$PATH` in config -- Fisher has zero startup overhead; OMF adds measurable startup time -- Use `fish --profile /tmp/profile.log` to identify slow startup scripts -- Lazy-load heavy initializations using autoloaded functions - -### Migrating from Bash - -1. Convert `export VAR=val` to `set -gx VAR val` -2. Convert `$(cmd)` to `(cmd)` (both forms work in Fish, but `(cmd)` is idiomatic) -3. Replace `[[` with `test` or `[` -4. Replace `${var:-default}` with `set -q var; or set var default` -5. Replace `${var%pattern}` with `string replace` or `string match` -6. Replace functions: `foo() { ... }` with `function foo ... end` -7. Replace `if/then/fi` with `if/end` -8. Replace `for x in ...; do ... done` with `for x in ...; ... end` -9. Source bash scripts using `bax` or `bass` plugins when conversion is impractical diff --git a/packages/dotfiles/dot_agents/skills/fish-helper/references/releases.md b/packages/dotfiles/dot_agents/skills/fish-helper/references/releases.md new file mode 100644 index 0000000000..9f0b6ab41d --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/fish-helper/references/releases.md @@ -0,0 +1,68 @@ +# Fish release lifecycle + +Read this when upgrading Fish, adopting an API introduced after Fish 4.3, or checking a plugin compatibility claim. + +## Current version + +Fish 4.8.1 is current as of 2026-08-03. Important recent changes: + +- 4.5: Vi-mode regressions and permanent removal of old terminfo behavior. +- 4.6: emoji width default 2, prompt environment controls, `set_color` additions, and `|&` support. +- 4.7/4.7.1: noninteractive theme initialization changes, sanitized prompt paths, and fish_config fixes. +- 4.8/4.8.1: `cd -L/-P`, binding source reporting, embedded install-layout changes, and input/completion fixes. + +## Research ledger + +The following 51 primary pages were fetched and inspected: + +1. [Fish documentation](https://fishshell.com/docs/current/index.html) +2. [FAQ](https://fishshell.com/docs/current/faq.html) +3. [Interactive use](https://fishshell.com/docs/current/interactive.html) +4. [Language](https://fishshell.com/docs/current/language.html) +5. [Commands](https://fishshell.com/docs/current/commands.html) +6. [Fish for Bash users](https://fishshell.com/docs/current/fish_for_bash_users.html) +7. [Tutorial](https://fishshell.com/docs/current/tutorial.html) +8. [Completions](https://fishshell.com/docs/current/completions.html) +9. [Prompt](https://fishshell.com/docs/current/prompt.html) +10. [Design](https://fishshell.com/docs/current/design.html) +11. [Release notes](https://fishshell.com/docs/current/relnotes.html) +12. [Terminal compatibility](https://fishshell.com/docs/current/terminal-compatibility.html) +13. [set](https://fishshell.com/docs/current/cmds/set.html) +14. [abbr](https://fishshell.com/docs/current/cmds/abbr.html) +15. [complete](https://fishshell.com/docs/current/cmds/complete.html) +16. [function](https://fishshell.com/docs/current/cmds/function.html) +17. [functions](https://fishshell.com/docs/current/cmds/functions.html) +18. [funcsave](https://fishshell.com/docs/current/cmds/funcsave.html) +19. [funced](https://fishshell.com/docs/current/cmds/funced.html) +20. [bind](https://fishshell.com/docs/current/cmds/bind.html) +21. [read](https://fishshell.com/docs/current/cmds/read.html) +22. [status](https://fishshell.com/docs/current/cmds/status.html) +23. [source](https://fishshell.com/docs/current/cmds/source.html) +24. [emit](https://fishshell.com/docs/current/cmds/emit.html) +25. [fish_add_path](https://fishshell.com/docs/current/cmds/fish_add_path.html) +26. [string](https://fishshell.com/docs/current/cmds/string.html) +27. [string pad](https://fishshell.com/docs/current/cmds/string-pad.html) +28. [math](https://fishshell.com/docs/current/cmds/math.html) +29. [psub](https://fishshell.com/docs/current/cmds/psub.html) +30. [type](https://fishshell.com/docs/current/cmds/type.html) +31. [command](https://fishshell.com/docs/current/cmds/command.html) +32. [eval](https://fishshell.com/docs/current/cmds/eval.html) +33. [argparse](https://fishshell.com/docs/current/cmds/argparse.html) +34. [fish_config](https://fishshell.com/docs/current/cmds/fish_config.html) +35. [set_color](https://fishshell.com/docs/current/cmds/set_color.html) +36. [wait](https://fishshell.com/docs/current/cmds/wait.html) +37. [contains](https://fishshell.com/docs/current/cmds/contains.html) +38. [alias](https://fishshell.com/docs/current/cmds/alias.html) +39. [fish](https://fishshell.com/docs/current/cmds/fish.html) +40. [fish_indent](https://fishshell.com/docs/current/cmds/fish_indent.html) +41. [fish_key_reader](https://fishshell.com/docs/current/cmds/fish_key_reader.html) +42. [Fish website](https://fishshell.com/) +43. [Fish 4.8.1 release](https://github.com/fish-shell/fish-shell/releases/tag/4.8.1) +44. [Fisher](https://github.com/jorgebucaran/fisher) +45. [nvm.fish](https://github.com/jorgebucaran/nvm.fish) +46. [fzf.fish](https://github.com/PatrickF1/fzf.fish) +47. [Tide](https://github.com/IlanCosman/tide) +48. [done](https://github.com/franciscolourenco/done) +49. [autopair.fish](https://github.com/jorgebucaran/autopair.fish) +50. [Fishtape](https://github.com/jorgebucaran/fishtape) +51. [Oh My Fish](https://github.com/oh-my-fish/oh-my-fish) diff --git a/packages/dotfiles/dot_agents/skills/fish-helper/references/syntax-and-safety.md b/packages/dotfiles/dot_agents/skills/fish-helper/references/syntax-and-safety.md new file mode 100644 index 0000000000..af826b07bc --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/fish-helper/references/syntax-and-safety.md @@ -0,0 +1,85 @@ +# Fish syntax and safety + +Read this when handling variables, lists, argv, command status, input, tracing, temp directories, or shell evaluation. + +## Variable scopes + +`set` combines scope (`-l`, `-g`, `-U`) and export state (`-x`, `-u`). Do not request export and unexport simultaneously. Use a project-managed global for deterministic config and universal state only for intentional cross-session preferences. + +## Lists and expansion + +Fish variables are lists. A zero-element list can remove an unquoted expansion, including surrounding unquoted concatenation. `${name}` is not Fish syntax; combine quoted and unquoted segments deliberately. + +The empty-list behavior makes “stringly” command construction fragile. Keep executable and arguments as list elements. + +## Command-scoped variables + +Fish supports `NAME=value command` for a command-scoped environment override. Use `set` for persistent or standalone assignment. + +## Status + +Capture `$status` immediately after the command it describes. Pipelines expose `$pipestatus`. A later `echo`, `set`, or cleanup command replaces `$status`. + +## Safe temp wrapper + +```fish +function with_temp + if test (count $argv) -eq 0 + echo 'with_temp: missing command' >&2 + return 2 + end + + set -l temp_dir (mktemp -d) + or return + + pushd $temp_dir + or begin + command rm -rf -- $temp_dir + return 1 + end + + $argv + set -l command_status $status + + popd + set -l popd_status $status + command rm -rf -- $temp_dir + or return + + if test $popd_status -ne 0 + return $popd_status + end + return $command_status +end +``` + +Resolve and validate the exact temp path before cleanup. Run the `rm -rf` unconditionally after `popd`, not gated behind its success: a wrapped Fish function that clears the directory stack, or a since-removed original directory, makes `popd` fail, and an early `return` there would skip cleanup and leak the temp directory. This wrapper handles ordinary completion; interruption-safe cleanup may need a job/process lifecycle outside a simple function. + +## Source + +Resolve an intended function or file with `type --path`. Do not use external `which` output as trusted source code. Generated init output is code; run it only from a required, versioned tool and propagate failure. + +## Tracing + +Set `fish_trace` to a non-empty value to trace. Erase it to disable. Profiling startup uses `--profile-startup` rather than `--profile`. + +## Primary documentation + +- [Fish language](https://fishshell.com/docs/current/language.html) +- [Fish for Bash users](https://fishshell.com/docs/current/fish_for_bash_users.html) +- [Tutorial](https://fishshell.com/docs/current/tutorial.html) +- [set](https://fishshell.com/docs/current/cmds/set.html) +- [read](https://fishshell.com/docs/current/cmds/read.html) +- [status](https://fishshell.com/docs/current/cmds/status.html) +- [source](https://fishshell.com/docs/current/cmds/source.html) +- [string](https://fishshell.com/docs/current/cmds/string.html) +- [math](https://fishshell.com/docs/current/cmds/math.html) +- [psub](https://fishshell.com/docs/current/cmds/psub.html) +- [type](https://fishshell.com/docs/current/cmds/type.html) +- [command](https://fishshell.com/docs/current/cmds/command.html) +- [eval](https://fishshell.com/docs/current/cmds/eval.html) +- [argparse](https://fishshell.com/docs/current/cmds/argparse.html) +- [wait](https://fishshell.com/docs/current/cmds/wait.html) +- [contains](https://fishshell.com/docs/current/cmds/contains.html) +- [fish](https://fishshell.com/docs/current/cmds/fish.html) +- [fish_indent](https://fishshell.com/docs/current/cmds/fish_indent.html) diff --git a/packages/dotfiles/dot_agents/skills/go-helper/SKILL.md b/packages/dotfiles/dot_agents/skills/go-helper/SKILL.md index 9561b142d6..f60c2a95e5 100644 --- a/packages/dotfiles/dot_agents/skills/go-helper/SKILL.md +++ b/packages/dotfiles/dot_agents/skills/go-helper/SKILL.md @@ -1,439 +1,142 @@ --- name: go-helper -description: | - Go development with modules, testing, linting, and common patterns - When user works with .go files, mentions Go, golang, go modules, go test, or encounters Go compiler errors +description: Current Go development guidance for modules, toolchains, workspaces, testing, fuzzing, concurrency, profiling, security, and Go tooling. Use when writing or reviewing Go, go.mod, go.work, Go CI, tests, performance work, or Go upgrades. --- -# Go Helper Agent +# Go Helper -## What's New in Go (2023-2026) +Use the module's declared Go language version, propagate errors, keep goroutine ownership explicit, and run the repository's real verification commands. Distinguish read-only module checks from operations that edit files or fetch dependencies. -- **Go 1.26** (Feb 2026): `new()` accepts any expression (not just type names), Green Tea GC enabled by default (10-40% lower GC overhead), `crypto/hpke` package (HPKE RFC 9180), experimental `simd/archsimd` package (`GOEXPERIMENT=simd`), ~30% lower cgo call overhead, `cmd/doc` removed (use `go doc`), pprof opens flame graph by default -- **Go 1.25** (Aug 2025): Experimental Green Tea GC (10-40% lower GC overhead in heavy workloads), `encoding/json/v2` package with custom marshalers/unmarshalers, `testing/synctest` now stable, `runtime/trace.FlightRecorder` ring buffer API, DWARF v5 debug info (smaller binaries), cgroup CPU bandwidth-aware GOMAXPROCS on Linux -- **Go 1.24** (Feb 2025): Generic type aliases fully supported, `tool` directives in go.mod for executable dependencies, SwissTable map implementation (~30% faster large map access), `runtime.AddCleanup` replaces `SetFinalizer`, `os.Root` for directory-scoped filesystem ops, FIPS 140-3 compliance mechanisms, `go:wasmexport` directive, experimental `testing/synctest` package -- **Go 1.23** (Aug 2024): Range-over-function iterators (`range` accepts iterator functions), new `iter` package, new `unique` package for value interning, `slices`/`maps` iterator functions (`All`, `Values`, `Collect`), unbuffered timer channels (no stale values after Stop/Reset), `go vet` checks for too-new symbols, `go env -changed`, `go mod tidy -diff` -- **Go 1.22** (Feb 2024): Per-iteration for-loop variables (no more accidental sharing), `range` over integers, `net/http.ServeMux` supports methods and wildcards (`GET /task/{id}/`), `math/rand/v2`, `slices.Concat`, PGO devirtualization (2-14% improvement) -- **Go 1.21** (Aug 2023): Built-in `min`, `max`, `clear` functions, `log/slog` structured logging, `slices`/`maps`/`cmp` packages, `panic(nil)` now causes `*runtime.PanicNilError`, WASI Preview 1 support, PGO 2-7% improvements, GC tail latency up to 40% lower -- **Current stable**: 1.26.x (Feb 2026) +## Current baseline -## Overview - -This skill covers Go development using the go toolchain, testing (go test, table-driven tests, fuzzing, benchmarks), linting (go vet, golangci-lint), formatting (gofmt, goimports), debugging (delve, pprof), and the module system. It includes error handling, interfaces, generics, concurrency, context, iterators, and struct embedding patterns. - -## CLI Commands - -### Auto-Approved Safe Commands +Verified 2026-08-03: current stable is Go 1.26.5. ```bash -# Check for issues -go vet ./... - -# Format code -gofmt -l . -goimports -l . - -# Build -go build ./... - -# Run tests -go test ./... - -# Show module dependencies -go list -m all - -# Tidy module dependencies -go mod tidy - -# Download dependencies -go mod download - -# Show documentation -go doc fmt.Println - -# Show environment -go env - -# List available tools -go tool +go version +go env GOTOOLCHAIN GOVERSION ``` -### Build and Run - -```bash -# Build current package -go build ./... - -# Build specific package -go build ./cmd/myapp - -# Build with output name -go build -o myapp ./cmd/myapp +Go 1.26 added expression-valued `new`, the default Green Tea garbage collector, lower cgo overhead, `crypto/hpke`, and other library/tool changes. JSON v2 remains experimental behind `GOEXPERIMENT=jsonv2`; SIMD work is experimental and platform-specific. -# Build with race detector -go build -race ./cmd/myapp +The `go` directive is a strict minimum language/toolchain version. `toolchain` suggests a toolchain for the main module. Module-version boundaries also control behavior such as Go 1.22 loop variables and Go 1.23 timer channels. -# Build with build tags -go build -tags "integration,debug" ./... +Read [references/releases.md](references/releases.md) for the 44-page research ledger. Read [references/modules-and-tooling.md](references/modules-and-tooling.md) for module, workspace, toolchain, lint, and dependency operations. Read [references/testing-and-performance.md](references/testing-and-performance.md) for tests, fuzzing, races, `synctest`, traces, profiles, and PGO. Read [references/patterns-and-security.md](references/patterns-and-security.md) for errors, goroutines, HTTP, paths, logging, randomness, and vulnerability checks. -# Build with linker flags (embed version info) -go build -ldflags "-X main.version=1.0.0 -X main.commit=$(git rev-parse HEAD)" ./cmd/myapp - -# Build for production (strip debug info, smaller binary) -go build -ldflags "-s -w" -trimpath ./cmd/myapp - -# Run directly -go run ./cmd/myapp -go run ./cmd/myapp -- --flag value - -# Install binary to $GOPATH/bin -go install ./cmd/myapp - -# Cross-compile -GOOS=linux GOARCH=amd64 go build -o myapp-linux ./cmd/myapp -GOOS=darwin GOARCH=arm64 go build -o myapp-darwin ./cmd/myapp -GOOS=windows GOARCH=amd64 go build -o myapp.exe ./cmd/myapp - -# List supported platforms -go tool dist list -``` +## Command authority -### Testing +Read-only or check-oriented commands: ```bash -# Run all tests +go version +go env +go list ./... go test ./... - -# Run with verbose output -go test -v ./... - -# Run specific test function -go test -run TestMyFunction ./pkg/mypackage - -# Run with race detector go test -race ./... - -# Run with coverage -go test -cover ./... -go test -coverprofile=coverage.out ./... -go tool cover -html=coverage.out - -# Run benchmarks -go test -bench=. ./... -go test -bench=BenchmarkMyFunc -benchmem ./... - -# Run fuzz tests -go test -fuzz=FuzzMyFunc -fuzztime=30s ./... - -# Run with timeout -go test -timeout 60s ./... - -# Run short tests only -go test -short ./... - -# Show test binary output -go test -v -count=1 ./... - -# List tests without running -go test -list '.*' ./... - -# Run tests single-threaded -go test -parallel 1 ./... -``` - -### Linting and Formatting - -```bash -# Format code (write changes) -gofmt -w . -goimports -w . - -# Check formatting without writing -gofmt -l . -goimports -l . - -# Vet (built-in static analysis) go vet ./... - -# golangci-lint (meta-linter, 50+ linters) -golangci-lint run -golangci-lint run ./... -golangci-lint run --fix -golangci-lint run --enable errcheck,staticcheck,gosec - -# golangci-lint v2 configuration (.golangci.yml) -# linters: -# default: standard -# enable: -# - errcheck -# - staticcheck -# - gosec -# - gocritic -# - revive -``` - -### Modules - -```bash -# Initialize new module -go mod init github.com/user/project - -# Add dependency -go get github.com/pkg/errors -go get github.com/pkg/errors@v0.9.1 -go get github.com/pkg/errors@latest - -# Update all dependencies -go get -u ./... - -# Update specific dependency -go get -u github.com/pkg/errors - -# Remove unused dependencies -go mod tidy - -# Vendor dependencies -go mod vendor - -# Show dependency graph -go mod graph - -# Verify dependencies go mod verify - -# Show why a module is needed -go mod why github.com/pkg/errors - -# Edit go.mod -go mod edit -require github.com/pkg/errors@v0.9.1 -go mod edit -replace github.com/old/pkg=github.com/new/pkg@v1.0.0 -go mod edit -dropreplace github.com/old/pkg - -# Workspaces (multi-module development) -go work init ./module-a ./module-b -go work use ./module-c -go work sync +go mod tidy -diff +go work edit -json ``` -### Tool Dependencies (Go 1.24+) - -```bash -# Add tool dependency to go.mod -go get -tool golang.org/x/tools/cmd/stringer -go get -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint - -# Run tool from go.mod -go tool stringer -type=MyType -go tool golangci-lint run - -# List tool dependencies -go mod edit -json | jq '.Tool' -``` +`go mod tidy` edits `go.mod` and `go.sum`. Dependency and download commands can mutate caches and involve network state. Review their effect instead of labeling them read-only. -## Essential Patterns Quick Reference +## Focused verification -### Error Handling +Use the package's actual task when one exists. A generic Go project baseline is: -```go -// Return errors, don't panic -func readConfig(path string) (*Config, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("reading config %s: %w", path, err) - } - var cfg Config - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, fmt.Errorf("parsing config: %w", err) - } - return &cfg, nil -} - -// Sentinel errors -var ErrNotFound = errors.New("not found") -var ErrPermission = errors.New("permission denied") - -// Check with errors.Is (works through wrapping) -if errors.Is(err, ErrNotFound) { /* handle */ } - -// Extract with errors.As -var pathErr *os.PathError -if errors.As(err, &pathErr) { /* use pathErr.Path */ } +```bash +go test ./... +go test -race ./... +go vet ./... +go mod tidy -diff ``` -### Interfaces - -```go -// Small, focused interfaces -type Reader interface { - Read(p []byte) (n int, err error) -} +Run the repository-pinned golangci-lint command if configured. Do not prescribe a fixed number of linters; its catalog and defaults are versioned. -type Writer interface { - Write(p []byte) (n int, err error) -} +## Modules and toolchains -// Compose interfaces -type ReadWriter interface { - Reader - Writer -} +```go.mod +module example.com/project -// Accept interfaces, return structs -func Process(r io.Reader) (*Result, error) { - data, err := io.ReadAll(r) - if err != nil { - return nil, err - } - return &Result{Data: data}, nil -} +go 1.26 ``` -### Generics (Go 1.18+) +The `go` line is the minimum required version and selects language behavior. `GOTOOLCHAIN` controls whether the bundled, PATH, or downloadable toolchain is selected. -```go -// Generic function -func Map[T, U any](s []T, f func(T) U) []U { - result := make([]U, len(s)) - for i, v := range s { - result[i] = f(v) - } - return result -} +Use `go get` for dependency changes and `go mod tidy` to reconcile the module graph. Review both `go.mod` and `go.sum`. Avoid a broad `go get -u ./...`; upgrade an intentional set, read release notes, and verify the resulting graph. -// Generic type with constraint -type Number interface { - ~int | ~int32 | ~int64 | ~float32 | ~float64 -} +For a non-mutating tidy check: -func Sum[T Number](nums []T) T { - var total T - for _, n := range nums { - total += n - } - return total -} +```bash +go mod tidy -diff ``` -### Iterators (Go 1.23+) - -```go -// Push iterator (standard) -func All[T any](s []T) iter.Seq[T] { - return func(yield func(T) bool) { - for _, v := range s { - if !yield(v) { - return - } - } - } -} +## Errors -// Key-value iterator -func Entries[K comparable, V any](m map[K]V) iter.Seq2[K, V] { - return func(yield func(K, V) bool) { - for k, v := range m { - if !yield(k, v) { - return - } - } - } -} - -// Use in range loop -for v := range All(mySlice) { - fmt.Println(v) -} -``` - -### Concurrency +Return errors with context and preserve identity with `%w` when callers may inspect them: ```go -// Goroutines with WaitGroup -var wg sync.WaitGroup -for _, url := range urls { - wg.Add(1) - go func() { - defer wg.Done() - fetch(url) - }() -} -wg.Wait() - -// errgroup for concurrent tasks with error handling -g, ctx := errgroup.WithContext(ctx) -for _, url := range urls { - g.Go(func() error { - return fetch(ctx, url) - }) -} -if err := g.Wait(); err != nil { - return err +func load(path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read %q: %w", path, err) + } + return data, nil } ``` -### Struct Embedding +Do not discard `Close`, `ReadAll`, `Write`, trace, or server errors when failure can affect correctness. For cleanup whose error matters, call and return it explicitly rather than relying only on `defer`. -```go -// Embedding promotes methods and fields -type Base struct { - ID string -} +## Goroutine ownership -func (b *Base) Identify() string { return b.ID } +Every goroutine needs a lifecycle: completion, cancellation, or a process-long owner. Thread `context.Context` through request-scoped operations; never store it in a long-lived struct solely for convenience. -type Server struct { - Base // Embedded, not named - Host string - Port int -} +Prefer `errgroup` or an equivalent structured owner when sibling failure should cancel work. Bound concurrency and preserve output ordering when the contract requires it. -s := Server{Base: Base{ID: "srv-1"}, Host: "localhost", Port: 8080} -s.Identify() // Promoted from Base -``` +## Tests and benchmarks -## go.mod Quick Reference +Go 1.25 stabilized `testing/synctest`; the stable API is `synctest.Test(t, func(t *testing.T) { ... })` plus `synctest.Wait()`. Fake time advances only when goroutines in the bubble are durably blocked. -```go -module github.com/user/project +Use `B.Loop()` for current benchmarks. Avoid exact universal race-detector or benchmark overhead claims. The race detector only finds races executed on a supported platform. -go 1.24 +Fuzz failures are stored at `testdata/fuzz/FuzzName/`. Keep them as regression corpus entries. -// Tool dependencies (Go 1.24+) -tool ( - golang.org/x/tools/cmd/stringer - github.com/golangci/golangci-lint/v2/cmd/golangci-lint -) +## Modern APIs -require ( - github.com/go-chi/chi/v5 v5.1.0 - github.com/jackc/pgx/v5 v5.7.0 - go.uber.org/zap v1.27.0 -) +- Iterator functions and `iter.Seq` / `Seq2` support range-over-function patterns. Do not yield after `yield` returns false. +- `maps` and `slices` expose iterator-producing and consuming helpers. +- `unique.Make` canonicalizes comparable values into handles. +- `math/rand/v2` is non-cryptographic randomness. +- `os.Root` confines supported file operations against symlink path escapes. +- `runtime/trace.FlightRecorder` provides bounded recent trace data; check `Start` and `WriteTo` errors and call `Stop`. +- `crypto/hpke` implements RFC 9180, including hybrid post-quantum KEMs; use protocol-specific expertise before designing cryptography. -require ( - // indirect dependencies managed by go mod tidy - golang.org/x/sys v0.25.0 // indirect -) +## HTTP and profiling -// Local replacement (development) -replace github.com/my/lib => ../my-lib -``` +Check every server error. Bind diagnostic endpoints to an operator-only interface and never expose `net/http/pprof` unauthenticated to the public network. -## When to Ask for Help +Block and mutex profiles require nonzero profiling rates. CPU, heap, goroutine, block, and mutex profiles answer different questions. -Ask the user for clarification when: +Representative production CPU profiles can drive PGO through `default.pgo`. A profile must match the workload; do not treat PGO as a universal speed switch. -- Error handling strategy needs deciding (sentinel vs custom types vs wrapping) -- Concurrency pattern choice is unclear (channels vs mutex vs errgroup) -- Interface design decisions are needed -- Module structure or workspace layout is unclear -- Performance vs readability tradeoffs exist -- Context propagation or cancellation patterns are complex +## Security ---- +- Use `crypto/rand` for secrets; `math/rand/v2` is for non-security randomness. +- Use `os.Root` or equivalent confinement for untrusted relative paths. +- Keep private module configuration in `GOPRIVATE`, `GONOPROXY`, and `GONOSUMDB` as appropriate. `GONOSUMCHECK` does not exist. +- Run `govulncheck` for known vulnerabilities reachable from application call paths. +- Treat imported pprof handlers, cgo, templates, archive extraction, and subprocess arguments as security boundaries. +- Use structured `log/slog` fields and never log secrets. -See `references/` for detailed guides: +## Review checklist -- `patterns.md` - Error handling, interfaces, generics, concurrency, context, iterators, struct embedding, testing patterns -- `modules-tooling.md` - Go modules, workspaces, dependency management, golangci-lint, go vet, gopls, build tags, cross-compilation, popular packages -- `testing-debugging.md` - go test, table-driven tests, benchmarks, fuzzing, testify, delve debugger, profiling with pprof, race detector +- Verify Go stable, the module `go` line, and selected toolchain. +- Use `go mod tidy -diff` for a non-writing module check. +- Review intentional dependency changes and both module files. +- Propagate meaningful errors, including cleanup and server failures. +- Give every goroutine an owner and cancellation path. +- Use exact tests, current `synctest`, correct fuzz corpus paths, and workload-qualified performance claims. +- Keep profiling endpoints private and check trace/profile errors. +- Use current golangci-lint v2 configuration and canonical gopls setting names. +- Separate experimental JSON v2 and SIMD behavior from stable defaults. +- Run `govulncheck` and use cryptographic randomness for secrets. diff --git a/packages/dotfiles/dot_agents/skills/go-helper/references/modules-and-tooling.md b/packages/dotfiles/dot_agents/skills/go-helper/references/modules-and-tooling.md new file mode 100644 index 0000000000..9a83b3ad5a --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/go-helper/references/modules-and-tooling.md @@ -0,0 +1,64 @@ +# Go modules and tooling + +Read this when changing `go.mod`, `go.work`, toolchains, dependencies, golangci-lint, gopls, or project layout. + +## Language and toolchain versions + +The `go` directive is a strict minimum and controls language semantics. The `toolchain` directive suggests the toolchain for the main module. `GOTOOLCHAIN` selects bundled, PATH, or downloadable toolchain behavior. + +Go 1.22 loop-variable semantics and Go 1.23 timer-channel semantics depend on the package or main module language version. Do not infer behavior only from the installed binary. + +## Modules + +Use `go mod tidy -diff` for a read-only consistency check. `go mod tidy` edits module files. `go mod download` can fetch and populate caches. + +For major version 2 or later, the module path generally needs the matching `/vN` suffix. + +`GOPRIVATE` sets private module patterns and commonly supplies defaults for `GONOPROXY` and `GONOSUMDB`. Configure the narrower variables when proxy and checksum policy differ. There is no `GONOSUMCHECK` variable. + +## Workspaces + +`go.work`, `go work use`, and `go work sync` coordinate local modules. Committing `go.work` is repository policy, not a universal rule. Use a workspace when modules are developed together; avoid it when it hides version requirements that downstream users need to resolve independently. + +## Tool dependencies + +Current Go supports tool directives in `go.mod`. Prefer a versioned tool dependency or official versioned installation guidance over executing a mutable script from a repository's default branch. + +## golangci-lint v2 + +Use the repository-pinned release. Current v2 configuration nests linter settings under `linters.settings` and uses current exclusion paths. Do not copy v1 `linters-settings` or `issues.exclude-dirs` examples. + +Available and default linter sets change by release. Link the current catalog instead of claiming a fixed count. + +## gopls + +Canonical setting names are short keys such as `gofumpt`, `analyses`, `semanticTokens`, `usePlaceholders`, and `directoryFilters`. Dotted names can be editor-specific aliases; do not describe an unsupported repository-level `.gopls.json` as canonical. + +## Project layout + +Go defines no mandatory `pkg/`, `internal/`, or `cmd/` tree. `internal` has enforced import visibility semantics; the others are conventions. Describe an example as one common layout, not the standard layout. + +## Environment facts + +Query target-specific values: + +```bash +go env CGO_ENABLED GOOS GOARCH +``` + +Do not hard-code CGO or CPU defaults. Current `GOMAXPROCS` considers CPU count, affinity, and Linux cgroup quota and can update as limits change. + +## Primary documentation + +- [Go module reference](https://go.dev/doc/modules/gomod-ref) +- [Go module reference specification](https://go.dev/ref/mod) +- [Go toolchains](https://go.dev/doc/toolchain) +- [cmd/go](https://pkg.go.dev/cmd/go) +- [Go workspaces](https://go.dev/doc/tutorial/workspaces) +- [Managing dependencies](https://go.dev/doc/modules/managing-dependencies) +- [Module release workflow](https://go.dev/doc/modules/release-workflow) +- [golangci-lint changelog](https://golangci-lint.run/docs/product/changelog/) +- [golangci-lint configuration](https://golangci-lint.run/docs/configuration/file/) +- [golangci-lint linters](https://golangci-lint.run/docs/linters/) +- [gopls settings](https://go.dev/gopls/settings) +- [Delve usage](https://github.com/go-delve/delve/blob/master/Documentation/usage/dlv.md) diff --git a/packages/dotfiles/dot_agents/skills/go-helper/references/modules-tooling.md b/packages/dotfiles/dot_agents/skills/go-helper/references/modules-tooling.md deleted file mode 100644 index e841a8b989..0000000000 --- a/packages/dotfiles/dot_agents/skills/go-helper/references/modules-tooling.md +++ /dev/null @@ -1,658 +0,0 @@ -# Go Modules and Tooling - -Comprehensive reference for Go modules, workspaces, dependency management, golangci-lint, go vet, gopls, build tags, cross-compilation, and popular packages. - -## Go Modules - -### Module Initialization - -```bash -# Create new module -go mod init github.com/user/project - -# Creates go.mod: -# module github.com/user/project -# go 1.24 -``` - -### go.mod File Structure - -```go -module github.com/user/project - -go 1.24 - -// Tool dependencies (Go 1.24+) -tool ( - golang.org/x/tools/cmd/stringer - github.com/golangci/golangci-lint/v2/cmd/golangci-lint -) - -require ( - github.com/go-chi/chi/v5 v5.1.0 - github.com/jackc/pgx/v5 v5.7.0 - go.uber.org/zap v1.27.0 - golang.org/x/sync v0.8.0 -) - -require ( - // Indirect dependencies (managed by go mod tidy) - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect -) - -// Replace directives (local development or forks) -replace github.com/original/pkg => ../local-pkg -replace github.com/original/pkg => github.com/fork/pkg v1.0.0 - -// Exclude a specific version -exclude github.com/broken/pkg v1.2.3 - -// Retract versions (used by module authors) -retract ( - v1.0.0 // Published accidentally - [v1.1.0, v1.2.0] // Contains critical bug -) -``` - -### Dependency Management Commands - -```bash -# Add dependency -go get github.com/pkg/errors # Latest -go get github.com/pkg/errors@v0.9.1 # Specific version -go get github.com/pkg/errors@latest # Latest tagged -go get github.com/pkg/errors@main # Branch tip -go get github.com/pkg/errors@abc1234 # Specific commit - -# Update dependency -go get -u github.com/pkg/errors # Latest minor/patch -go get -u=patch github.com/pkg/errors # Latest patch only -go get -u ./... # Update all direct deps - -# Remove unused dependencies -go mod tidy - -# Show diff without modifying (Go 1.23+) -go mod tidy -diff - -# Download dependencies to local cache -go mod download - -# Vendor dependencies -go mod vendor - -# Verify checksums -go mod verify - -# Show dependency graph -go mod graph - -# Show why a dependency is needed -go mod why github.com/pkg/errors -go mod why -m golang.org/x/sys - -# Edit go.mod programmatically -go mod edit -require github.com/pkg/errors@v0.9.1 -go mod edit -droprequire github.com/pkg/errors -go mod edit -replace old=new@v1.0.0 -go mod edit -dropreplace old -go mod edit -go 1.24 -go mod edit -json # Output as JSON -``` - -### Tool Dependencies (Go 1.24+) - -Before Go 1.24, tool dependencies required a `tools.go` file with blank imports. Now use `tool` directives: - -```bash -# Add tool dependency -go get -tool golang.org/x/tools/cmd/stringer -go get -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint -go get -tool google.golang.org/protobuf/cmd/protoc-gen-go - -# Run tool -go tool stringer -type=Color -go tool golangci-lint run -go tool protoc-gen-go - -# In go.mod -tool ( - golang.org/x/tools/cmd/stringer - github.com/golangci/golangci-lint/v2/cmd/golangci-lint -) -``` - -### Semantic Versioning - -Go modules follow semantic versioning strictly: - -``` -v1.2.3 - │ │ └── Patch: bug fixes, no API changes - │ └──── Minor: new features, backward-compatible - └────── Major: breaking changes -``` - -Major version 2+ requires path suffix: - -```go -import "github.com/user/repo/v2" -import "github.com/user/repo/v3/pkg" -``` - -### Module Proxies and Checksums - -```bash -# Default proxy -GOPROXY=https://proxy.golang.org,direct - -# Private modules (bypass proxy) -GONOSUMCHECK=github.com/private/* -GONOSUMDB=github.com/private/* -GOPRIVATE=github.com/private/* - -# Or set in go env -go env -w GOPRIVATE=github.com/mycompany/* -``` - -## Workspaces - -### When to Use Workspaces - -Use `go.work` when developing multiple modules that depend on each other locally. Common scenarios: - -- Monorepo with multiple services sharing internal packages -- Developing a library and testing it in a consumer app -- Working on a dependency fork alongside your project - -### Creating a Workspace - -```bash -# Initialize workspace -go work init ./service-a ./service-b ./shared-lib - -# Creates go.work: -# go 1.24 -# use ( -# ./service-a -# ./service-b -# ./shared-lib -# ) - -# Add another module -go work use ./service-c - -# Sync dependencies across workspace modules -go work sync - -# Build across workspace -go build ./... -go test ./... -``` - -### go.work File - -```go -go 1.24 - -use ( - ./service-a - ./service-b - ./shared-lib -) - -// Replace applies workspace-wide -replace github.com/external/dep => ../local-dep -``` - -### Workspace Best Practices - -- Add `go.work` and `go.work.sum` to `.gitignore` for personal development -- Commit `go.work` only if the repo is a true monorepo where all modules are always built together -- Each module should still have its own `go.mod` and work independently -- Use `GOWORK=off` to disable workspace mode temporarily: `GOWORK=off go build ./...` - -## golangci-lint - -### Installation - -```bash -# Binary install (recommended) -curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin - -# Or as tool dependency (Go 1.24+) -go get -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint - -# Or brew -brew install golangci-lint -``` - -### Usage - -```bash -# Run all enabled linters -golangci-lint run - -# Run on specific packages -golangci-lint run ./pkg/... - -# Auto-fix issues -golangci-lint run --fix - -# Show all available linters -golangci-lint linters - -# Run specific linters -golangci-lint run --enable errcheck,gosec,gocritic -``` - -### Configuration (v2 - .golangci.yml) - -```yaml -version: "2" - -linters: - default: standard - enable: - - errcheck # Check for unchecked errors - - gocritic # Opinionated Go linter - - gosec # Security checks - - revive # Fast, configurable Go linter - - unconvert # Unnecessary type conversions - - unparam # Unused function parameters - - goconst # Repeated strings that could be constants - - prealloc # Slice pre-allocation suggestions - - misspell # Spelling corrections - -formatters: - enable: - - gofmt - - goimports - -linters-settings: - gocritic: - enabled-tags: - - diagnostic - - style - - performance - revive: - rules: - - name: exported - arguments: - - "checkPrivateReceivers" - gosec: - excludes: - - G104 # Unhandled errors (covered by errcheck) - -issues: - exclude-dirs: - - vendor - - generated - max-issues-per-linter: 50 - max-same-issues: 5 -``` - -### Key Linters Explained - -| Linter | Purpose | -| ------------- | ----------------------------------------------------- | -| `staticcheck` | Comprehensive static analysis (included in standard) | -| `errcheck` | Detect unchecked error return values | -| `gosimple` | Simplify code (included in standard) | -| `govet` | Report suspicious constructs (included in standard) | -| `gosec` | Security-focused analysis | -| `gocritic` | Opinionated lints for style, performance, diagnostics | -| `revive` | Fast, configurable alternative to golint | -| `ineffassign` | Detect ineffectual assignments | -| `misspell` | Fix common misspellings | -| `unconvert` | Remove unnecessary type conversions | -| `prealloc` | Suggest slice pre-allocation | - -## go vet - -Built-in static analysis tool that catches common mistakes. - -```bash -# Run all analyzers -go vet ./... - -# What go vet catches: -# - printf format string mismatches -# - unreachable code -# - suspicious mutex usage (copying) -# - nil function comparisons -# - struct tag validation -# - too-new symbols for target Go version (Go 1.23+) -# - errors passed to log.Fatal instead of log.Println -``` - -## gopls (Go Language Server) - -gopls is the official Go language server, providing IDE features. - -### Configuration (.gopls.json or in editor settings) - -```json -{ - "formatting.gofumpt": true, - "ui.semanticTokens": true, - "ui.diagnostic.analyses": { - "unusedvariable": true, - "shadow": true, - "useany": true - }, - "ui.completion.usePlaceholders": true, - "build.directoryFilters": ["-vendor", "-node_modules"] -} -``` - -### gopls Features - -- Auto-completion with type-aware suggestions -- Go to definition, find references, find implementations -- Rename refactoring across packages -- Code actions (organize imports, extract function, fill struct) -- Inline diagnostics from go vet and staticcheck -- Signature help and hover documentation -- Workspace symbol search - -## Build Tags - -### Syntax - -```go -// Modern syntax (Go 1.17+): //go:build -//go:build linux && amd64 - -// Multiple constraints -//go:build (linux || darwin) && amd64 - -// Negation -//go:build !windows - -// Custom build tags -//go:build integration - -package mypackage -``` - -### File Naming Conventions - -``` -// Automatically applied build constraints based on filename: -file_linux.go // Only compiled on linux -file_windows.go // Only compiled on windows -file_amd64.go // Only compiled for amd64 -file_linux_amd64.go // Only compiled on linux/amd64 -file_test.go // Only compiled during testing -``` - -### Using Build Tags - -```bash -# Build with custom tag -go build -tags integration ./... -go test -tags "integration,e2e" ./... - -# Multiple tags -go build -tags "debug,verbose" ./... -``` - -### Common Build Tag Patterns - -```go -// Separate integration tests -//go:build integration - -package mypackage - -func TestIntegration(t *testing.T) { - // Only runs with: go test -tags integration -} -``` - -```go -// Platform-specific code -//go:build darwin - -package platform - -func openBrowser(url string) error { - return exec.Command("open", url).Start() -} -``` - -## Cross-Compilation - -### Basic Cross-Compilation - -```bash -# Linux AMD64 -GOOS=linux GOARCH=amd64 go build -o app-linux-amd64 ./cmd/app - -# Linux ARM64 -GOOS=linux GOARCH=arm64 go build -o app-linux-arm64 ./cmd/app - -# macOS AMD64 (Intel) -GOOS=darwin GOARCH=amd64 go build -o app-darwin-amd64 ./cmd/app - -# macOS ARM64 (Apple Silicon) -GOOS=darwin GOARCH=arm64 go build -o app-darwin-arm64 ./cmd/app - -# Windows AMD64 -GOOS=windows GOARCH=amd64 go build -o app.exe ./cmd/app - -# WebAssembly -GOOS=js GOARCH=wasm go build -o app.wasm ./cmd/app -GOOS=wasip1 GOARCH=wasm go build -o app.wasm ./cmd/app - -# List all supported platforms -go tool dist list -``` - -### CGO and Cross-Compilation - -CGO is disabled by default during cross-compilation. If you need CGO: - -```bash -# Disable CGO explicitly (pure Go, most portable) -CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app ./cmd/app - -# Enable CGO with cross-compiler -CGO_ENABLED=1 CC=x86_64-linux-gnu-gcc GOOS=linux GOARCH=amd64 go build -o app ./cmd/app - -# Static linking (for containers) -CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-s -w -extldflags "-static"' -o app ./cmd/app -``` - -### Multi-Platform Build Script - -```bash -#!/bin/bash -APP=myapp -VERSION=$(git describe --tags --always) -LDFLAGS="-s -w -X main.version=${VERSION}" - -platforms=( - "linux/amd64" - "linux/arm64" - "darwin/amd64" - "darwin/arm64" - "windows/amd64" -) - -for platform in "${platforms[@]}"; do - GOOS="${platform%/*}" - GOARCH="${platform#*/}" - output="${APP}-${GOOS}-${GOARCH}" - [[ "$GOOS" == "windows" ]] && output="${output}.exe" - - echo "Building ${output}..." - CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build \ - -ldflags "$LDFLAGS" -trimpath -o "dist/${output}" ./cmd/app -done -``` - -## Popular Packages - -### Web Frameworks and Routers - -| Package | Description | -| ----------------------------- | -------------------------------------------------------- | -| `net/http` (stdlib) | Standard HTTP server, enhanced routing in Go 1.22+ | -| `github.com/go-chi/chi/v5` | Lightweight, idiomatic router, fully net/http compatible | -| `github.com/gin-gonic/gin` | High-performance web framework (48% usage in 2025) | -| `github.com/labstack/echo/v4` | Minimalist, extensible web framework | -| `github.com/gofiber/fiber/v2` | Express-inspired, built on fasthttp | -| `connectrpc.com/connect` | gRPC-compatible HTTP APIs | - -### Database - -| Package | Description | -| ----------------------------- | ----------------------------------------------------- | -| `database/sql` (stdlib) | Standard database interface | -| `github.com/jackc/pgx/v5` | PostgreSQL driver and toolkit (preferred over lib/pq) | -| `github.com/jmoiron/sqlx` | Extensions to database/sql (StructScan, NamedExec) | -| `github.com/sqlc-dev/sqlc` | Generate type-safe Go from SQL | -| `gorm.io/gorm` | ORM with auto-migration, associations | -| `entgo.io/ent` | Entity framework with code generation | -| `github.com/mattn/go-sqlite3` | SQLite3 driver (CGO) | -| `modernc.org/sqlite` | SQLite3 driver (pure Go, no CGO) | - -### Configuration and CLI - -| Package | Description | -| ----------------------------- | ------------------------------------------------ | -| `github.com/spf13/cobra` | CLI application framework | -| `github.com/spf13/viper` | Configuration management (JSON, YAML, TOML, env) | -| `github.com/urfave/cli/v2` | Simple CLI framework | -| `github.com/caarlos0/env/v11` | Parse environment variables into structs | -| `github.com/joho/godotenv` | Load .env files | -| `github.com/knadh/koanf/v2` | Lighter alternative to viper | - -### Logging and Observability - -| Package | Description | -| ------------------------------------- | ----------------------------------- | -| `log/slog` (stdlib, Go 1.21+) | Structured logging | -| `go.uber.org/zap` | High-performance structured logging | -| `github.com/rs/zerolog` | Zero-allocation JSON logger | -| `go.opentelemetry.io/otel` | OpenTelemetry tracing and metrics | -| `github.com/prometheus/client_golang` | Prometheus metrics | - -### HTTP and Networking - -| Package | Description | -| --------------------------------------- | ---------------------------------- | -| `net/http` (stdlib) | HTTP client and server | -| `github.com/go-resty/resty/v2` | HTTP client with retry, middleware | -| `github.com/hashicorp/go-retryablehttp` | Retryable HTTP client | -| `google.golang.org/grpc` | gRPC framework | -| `nhooyr.io/websocket` | WebSocket library | - -### Testing - -| Package | Description | -| -------------------------------- | --------------------------- | -| `testing` (stdlib) | Standard testing framework | -| `github.com/stretchr/testify` | Assertions, mocks, suites | -| `github.com/google/go-cmp` | Deep comparison for tests | -| `go.uber.org/mock` | Interface mocking (mockgen) | -| `github.com/DATA-DOG/go-sqlmock` | SQL mock for database tests | -| `github.com/jarcoal/httpmock` | HTTP request mocking | - -### Serialization - -| Package | Description | -| --------------------------------- | ------------------------------- | -| `encoding/json` (stdlib) | JSON (v2 in Go 1.25+) | -| `github.com/goccy/go-json` | Fast JSON (drop-in replacement) | -| `google.golang.org/protobuf` | Protocol Buffers | -| `gopkg.in/yaml.v3` | YAML parsing | -| `github.com/pelletier/go-toml/v2` | TOML parsing | - -### Concurrency and Sync - -| Package | Description | -| -------------------------------- | ------------------------------------ | -| `sync` (stdlib) | Mutex, WaitGroup, Once, Map | -| `golang.org/x/sync/errgroup` | Goroutine groups with error handling | -| `golang.org/x/sync/semaphore` | Weighted semaphore | -| `golang.org/x/sync/singleflight` | Deduplicate concurrent calls | - -### Utilities - -| Package | Description | -| -------------------------------- | ------------------------------ | -| `github.com/google/uuid` | UUID generation | -| `github.com/samber/lo` | Lodash-style generic utilities | -| `golang.org/x/exp` | Experimental stdlib extensions | -| `github.com/cenkalti/backoff/v4` | Exponential backoff | -| `github.com/robfig/cron/v3` | Cron job scheduler | - -## Project Layout - -Standard Go project structure: - -``` -project/ - cmd/ - myapp/ - main.go # Entry point - internal/ # Private packages (not importable by others) - server/ - server.go - database/ - database.go - pkg/ # Public packages (importable by others) - api/ - api.go - go.mod - go.sum - Makefile - .golangci.yml -``` - -For libraries: - -``` -library/ - library.go # Package root - library_test.go - internal/ # Private helpers - helper.go - go.mod - go.sum -``` - -## Environment Variables - -| Variable | Purpose | Default | -| -------------- | ----------------------------- | --------------------------------- | -| `GOPATH` | Workspace directory | `~/go` | -| `GOBIN` | Binary install directory | `$GOPATH/bin` | -| `GOPROXY` | Module proxy URL | `https://proxy.golang.org,direct` | -| `GOPRIVATE` | Private module patterns | (none) | -| `GONOSUMCHECK` | Skip checksum verification | (none) | -| `CGO_ENABLED` | Enable/disable CGO | `1` on native, `0` cross-compile | -| `GOOS` | Target operating system | Host OS | -| `GOARCH` | Target architecture | Host architecture | -| `GOFLAGS` | Default go command flags | (none) | -| `GOEXPERIMENT` | Experimental features | (none) | -| `GOMAXPROCS` | Max OS threads for goroutines | Number of CPUs | - -```bash -# View all settings -go env - -# View only changed settings (Go 1.23+) -go env -changed - -# Set persistent env -go env -w GOPRIVATE=github.com/mycompany/* -go env -w GOPROXY=https://proxy.golang.org,direct - -# Unset -go env -u GOPRIVATE -``` diff --git a/packages/dotfiles/dot_agents/skills/go-helper/references/patterns-and-security.md b/packages/dotfiles/dot_agents/skills/go-helper/references/patterns-and-security.md new file mode 100644 index 0000000000..e58d0c190b --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/go-helper/references/patterns-and-security.md @@ -0,0 +1,55 @@ +# Go patterns and security + +Read this when handling errors, goroutines, HTTP, iterators, file paths, logging, cryptography, or vulnerability scanning. + +## Error ownership + +Wrap with `%w` only when callers should inspect the cause. Use `errors.Is` and `errors.As` rather than matching text. Do not ignore errors from I/O, cleanup, database closure, tracing, or serving. + +## Goroutines and channels + +The creator owns goroutine shutdown. Close a channel from the sending side when ownership is unambiguous. Use context cancellation for request-scoped work and bound worker pools. + +## HTTP + +Servers must check the returned error and distinguish expected shutdown from failure. Clients must close response bodies and check status before decoding. Apply timeouts at the correct request, transport, and server layers. + +## Iterators + +Iterator functions use `iter.Seq` or `Seq2`. Stop immediately when `yield` returns false. `maps` and `slices` expose helpers such as `All`, `Keys`, `Values`, `Collect`, and `Sorted`. + +## Safe paths + +`os.Root` confines supported operations beneath a root and prevents symlink-based escape. It is useful for archive extraction and untrusted relative paths, but callers must still bound file types, sizes, counts, and resource use. + +## Randomness and HPKE + +Use `crypto/rand` for secrets. `math/rand/v2` is non-cryptographic. `crypto/hpke` is a protocol implementation, not permission to invent a new encryption scheme; follow RFC and application protocol requirements. + +## Logging + +`log/slog` provides structured records, handlers, levels, groups, and context-aware logging. Use stable field names, keep cardinality bounded, and redact secrets. + +## Vulnerabilities + +`govulncheck` reports known vulnerabilities reachable from application call paths. It can scan source or binaries. Treat results as actionable dependency/code evidence while still reviewing deployment reachability and remediation. + +## Current package corrections + +`github.com/pkg/errors` is in maintenance mode; prefer standard-library error wrapping for new code. The former `nhooyr/websocket` project now lives at `coder/websocket`; verify migration and version requirements before changing a dependency. + +## Primary documentation + +- [Go specification](https://go.dev/ref/spec) +- [crypto/hpke](https://pkg.go.dev/crypto/hpke) +- [os](https://pkg.go.dev/os) +- [runtime](https://pkg.go.dev/runtime) +- [iter](https://pkg.go.dev/iter) +- [maps](https://pkg.go.dev/maps) +- [slices](https://pkg.go.dev/slices) +- [unique](https://pkg.go.dev/unique) +- [math/rand/v2](https://pkg.go.dev/math/rand/v2) +- [log/slog](https://pkg.go.dev/log/slog) +- [Go vulnerability management](https://go.dev/doc/security/vuln/) +- [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) +- [go vet](https://pkg.go.dev/cmd/vet) diff --git a/packages/dotfiles/dot_agents/skills/go-helper/references/patterns.md b/packages/dotfiles/dot_agents/skills/go-helper/references/patterns.md deleted file mode 100644 index 35d626fa58..0000000000 --- a/packages/dotfiles/dot_agents/skills/go-helper/references/patterns.md +++ /dev/null @@ -1,903 +0,0 @@ -# Go Patterns and Idioms - -Common Go patterns covering error handling, interfaces, generics, concurrency, context, iterators, struct embedding, and testing patterns. - -## Error Handling - -### The Basic Pattern - -Go's error handling follows an explicit pattern: check errors immediately after every call that can fail. - -```go -result, err := doSomething() -if err != nil { - return fmt.Errorf("doing something: %w", err) -} -// use result -``` - -### Wrapping Errors with Context - -Use `fmt.Errorf` with `%w` to wrap errors, preserving the original while adding context. This creates an error chain that can be inspected with `errors.Is` and `errors.As`. - -```go -func loadUser(id string) (*User, error) { - data, err := db.Query("SELECT * FROM users WHERE id = $1", id) - if err != nil { - return nil, fmt.Errorf("querying user %s: %w", id, err) - } - user, err := parseUser(data) - if err != nil { - return nil, fmt.Errorf("parsing user %s: %w", id, err) - } - return user, nil -} -``` - -### Sentinel Errors - -Sentinel errors are package-level variables that represent specific error conditions. Callers check for them using `errors.Is`. - -```go -package mypackage - -import "errors" - -var ( - ErrNotFound = errors.New("not found") - ErrConflict = errors.New("conflict") - ErrForbidden = errors.New("forbidden") -) - -func GetItem(id string) (*Item, error) { - item, ok := store[id] - if !ok { - return nil, fmt.Errorf("item %s: %w", id, ErrNotFound) - } - return item, nil -} - -// Caller -item, err := GetItem("abc") -if errors.Is(err, mypackage.ErrNotFound) { - // handle not found specifically -} -``` - -### Custom Error Types - -For errors that carry structured data, define a type implementing the `error` interface. - -```go -type ValidationError struct { - Field string - Message string -} - -func (e *ValidationError) Error() string { - return fmt.Sprintf("validation: %s - %s", e.Field, e.Message) -} - -func Validate(u *User) error { - if u.Name == "" { - return &ValidationError{Field: "name", Message: "required"} - } - if u.Age < 0 { - return &ValidationError{Field: "age", Message: "must be non-negative"} - } - return nil -} - -// Caller extracts the structured error -var valErr *ValidationError -if errors.As(err, &valErr) { - fmt.Printf("field %s: %s\n", valErr.Field, valErr.Message) -} -``` - -### Multi-Error Handling - -Go 1.20+ supports wrapping multiple errors with `errors.Join` and `fmt.Errorf` with multiple `%w` verbs. - -```go -// Join multiple errors -err1 := step1() -err2 := step2() -err3 := step3() -if err := errors.Join(err1, err2, err3); err != nil { - return err // contains all non-nil errors -} - -// Multiple %w in fmt.Errorf -err := fmt.Errorf("failed: %w and %w", err1, err2) -// errors.Is(err, err1) == true -// errors.Is(err, err2) == true -``` - -### Panic and Recover - -Use `panic` only for truly unrecoverable situations (programmer errors, impossible states). Use `recover` in deferred functions to catch panics at API boundaries. - -```go -// Only panic for programmer errors -func MustParse(s string) *Config { - cfg, err := Parse(s) - if err != nil { - panic(fmt.Sprintf("MustParse: %v", err)) - } - return cfg -} - -// Recover at API boundary (HTTP handler, goroutine root) -func safeHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if r := recover(); r != nil { - log.Printf("panic recovered: %v\n%s", r, debug.Stack()) - http.Error(w, "internal error", 500) - } - }() - next.ServeHTTP(w, r) - }) -} -``` - -## Interfaces - -### Design Principles - -Go interfaces are satisfied implicitly - no `implements` keyword. This enables loose coupling. - -```go -// Small, focused interfaces (Go proverb: "The bigger the interface, the weaker the abstraction") -type Storer interface { - Store(ctx context.Context, key string, value []byte) error -} - -type Loader interface { - Load(ctx context.Context, key string) ([]byte, error) -} - -// Compose when needed -type Storage interface { - Storer - Loader -} -``` - -### Accept Interfaces, Return Structs - -Functions should accept interfaces for flexibility and return concrete types for clarity. - -```go -// Good: accepts interface -func ProcessData(r io.Reader) (*Result, error) { - data, err := io.ReadAll(r) - if err != nil { - return nil, err - } - return &Result{Data: data}, nil -} - -// Works with any io.Reader: files, HTTP bodies, buffers, strings -ProcessData(os.Stdin) -ProcessData(resp.Body) -ProcessData(bytes.NewReader(data)) -ProcessData(strings.NewReader("hello")) -``` - -### Interface Assertions and Checks - -```go -// Type assertion -val, ok := i.(ConcreteType) -if ok { - // use val as ConcreteType -} - -// Type switch -switch v := i.(type) { -case string: - fmt.Println("string:", v) -case int: - fmt.Println("int:", v) -case io.Reader: - data, _ := io.ReadAll(v) - fmt.Println("reader:", string(data)) -default: - fmt.Println("unknown type") -} - -// Compile-time interface check -var _ io.ReadCloser = (*MyType)(nil) -``` - -### Common Standard Library Interfaces - -| Interface | Methods | Purpose | -| ------------------------ | ------------------------------------- | --------------------- | -| `io.Reader` | `Read([]byte) (int, error)` | Read bytes | -| `io.Writer` | `Write([]byte) (int, error)` | Write bytes | -| `io.Closer` | `Close() error` | Release resources | -| `io.ReadWriter` | `Read` + `Write` | Bidirectional I/O | -| `io.ReadCloser` | `Read` + `Close` | Readable + closeable | -| `fmt.Stringer` | `String() string` | String representation | -| `error` | `Error() string` | Error value | -| `sort.Interface` | `Len`, `Less`, `Swap` | Sortable collection | -| `http.Handler` | `ServeHTTP(ResponseWriter, *Request)` | HTTP handler | -| `context.Context` | `Deadline`, `Done`, `Err`, `Value` | Request scoping | -| `encoding.TextMarshaler` | `MarshalText() ([]byte, error)` | Text serialization | -| `json.Marshaler` | `MarshalJSON() ([]byte, error)` | JSON serialization | - -### Functional Options Pattern - -Use functional options for configurable constructors without breaking API compatibility. - -```go -type Server struct { - host string - port int - timeout time.Duration - logger *slog.Logger -} - -type Option func(*Server) - -func WithPort(port int) Option { - return func(s *Server) { s.port = port } -} - -func WithTimeout(d time.Duration) Option { - return func(s *Server) { s.timeout = d } -} - -func WithLogger(l *slog.Logger) Option { - return func(s *Server) { s.logger = l } -} - -func NewServer(host string, opts ...Option) *Server { - s := &Server{ - host: host, - port: 8080, - timeout: 30 * time.Second, - logger: slog.Default(), - } - for _, opt := range opts { - opt(s) - } - return s -} - -// Usage -srv := NewServer("localhost", - WithPort(9090), - WithTimeout(60*time.Second), -) -``` - -## Generics (Go 1.18+) - -### Type Parameters - -```go -// Generic function -func Filter[T any](s []T, pred func(T) bool) []T { - var result []T - for _, v := range s { - if pred(v) { - result = append(result, v) - } - } - return result -} - -// Generic struct -type Pair[T, U any] struct { - First T - Second U -} - -func NewPair[T, U any](first T, second U) Pair[T, U] { - return Pair[T, U]{First: first, Second: second} -} -``` - -### Type Constraints - -```go -// Built-in constraints (from constraints package or inline) -type Ordered interface { - ~int | ~int8 | ~int16 | ~int32 | ~int64 | - ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | - ~float32 | ~float64 | ~string -} - -// ~ allows underlying types (type aliases, defined types) -type MyInt int -// MyInt satisfies ~int but not int - -// comparable constraint - supports == and != -func Contains[T comparable](s []T, target T) bool { - for _, v := range s { - if v == target { - return true - } - } - return false -} - -// Method constraint -type Validator interface { - Validate() error -} - -func ValidateAll[T Validator](items []T) error { - for _, item := range items { - if err := item.Validate(); err != nil { - return err - } - } - return nil -} -``` - -### Generic Type Aliases (Go 1.24+) - -```go -// Type alias with parameters -type Set[T comparable] = map[T]struct{} - -// Use it -var s Set[string] -s = make(Set[string]) -s["hello"] = struct{}{} -``` - -### When to Use Generics - -Use generics for: - -- Container types (sets, stacks, queues, trees) -- Utility functions operating on slices/maps of any type -- Reducing boilerplate when the same logic applies to multiple types - -Avoid generics when: - -- Interfaces already solve the problem cleanly -- The code only works with one or two types -- It makes the code harder to read - -## Concurrency - -### Goroutines - -```go -// Start a goroutine -go func() { - // runs concurrently - result := compute() - fmt.Println(result) -}() - -// Goroutines are lightweight (~2KB initial stack) -// You can run millions of them -``` - -### Channels - -```go -// Unbuffered channel (synchronous) -ch := make(chan string) - -go func() { - ch <- "hello" // blocks until receiver is ready -}() -msg := <-ch // blocks until sender sends - -// Buffered channel (async up to capacity) -ch := make(chan int, 100) - -// Directional channels (for function signatures) -func producer(out chan<- int) { out <- 42 } -func consumer(in <-chan int) { v := <-in } - -// Close and range -close(ch) -for v := range ch { - fmt.Println(v) // iterates until channel is closed -} - -// Select for multiplexing -select { -case msg := <-ch1: - fmt.Println("from ch1:", msg) -case msg := <-ch2: - fmt.Println("from ch2:", msg) -case <-time.After(5 * time.Second): - fmt.Println("timeout") -} -``` - -### sync Package - -```go -// Mutex for shared state -type SafeCounter struct { - mu sync.Mutex - v map[string]int -} - -func (c *SafeCounter) Inc(key string) { - c.mu.Lock() - defer c.mu.Unlock() - c.v[key]++ -} - -// RWMutex for read-heavy workloads -type Cache struct { - mu sync.RWMutex - items map[string]string -} - -func (c *Cache) Get(key string) (string, bool) { - c.mu.RLock() - defer c.mu.RUnlock() - v, ok := c.items[key] - return v, ok -} - -func (c *Cache) Set(key, value string) { - c.mu.Lock() - defer c.mu.Unlock() - c.items[key] = value -} - -// Once for one-time initialization -var once sync.Once -var instance *DB - -func GetDB() *DB { - once.Do(func() { - instance = connectDB() - }) - return instance -} - -// WaitGroup for waiting on goroutines -var wg sync.WaitGroup -for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - work() - }() -} -wg.Wait() -``` - -### Worker Pool Pattern - -```go -func workerPool(ctx context.Context, jobs <-chan Job, numWorkers int) <-chan Result { - results := make(chan Result, numWorkers) - var wg sync.WaitGroup - - for i := 0; i < numWorkers; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for job := range jobs { - select { - case <-ctx.Done(): - return - case results <- process(job): - } - } - }() - } - - go func() { - wg.Wait() - close(results) - }() - - return results -} -``` - -### Fan-Out/Fan-In - -```go -func fanOut(ctx context.Context, input <-chan int, workers int) []<-chan int { - channels := make([]<-chan int, workers) - for i := 0; i < workers; i++ { - channels[i] = worker(ctx, input) - } - return channels -} - -func fanIn(ctx context.Context, channels ...<-chan int) <-chan int { - var wg sync.WaitGroup - merged := make(chan int) - - for _, ch := range channels { - wg.Add(1) - go func() { - defer wg.Done() - for v := range ch { - select { - case <-ctx.Done(): - return - case merged <- v: - } - } - }() - } - - go func() { - wg.Wait() - close(merged) - }() - - return merged -} -``` - -### errgroup for Concurrent Error Handling - -```go -import "golang.org/x/sync/errgroup" - -func fetchAll(ctx context.Context, urls []string) ([]string, error) { - g, ctx := errgroup.WithContext(ctx) - results := make([]string, len(urls)) - - for i, url := range urls { - g.Go(func() error { - body, err := fetch(ctx, url) - if err != nil { - return fmt.Errorf("fetching %s: %w", url, err) - } - results[i] = body - return nil - }) - } - - if err := g.Wait(); err != nil { - return nil, err - } - return results, nil -} -``` - -## Context - -### Creating Contexts - -```go -// Background context (top-level, never canceled) -ctx := context.Background() - -// With cancellation -ctx, cancel := context.WithCancel(parentCtx) -defer cancel() - -// With timeout (relative duration) -ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second) -defer cancel() - -// With deadline (absolute time) -ctx, cancel := context.WithDeadline(parentCtx, time.Now().Add(5*time.Second)) -defer cancel() - -// With value (use sparingly, prefer function parameters) -ctx = context.WithValue(parentCtx, requestIDKey, "abc-123") -``` - -### Using Context - -```go -// Pass context as first parameter -func fetchUser(ctx context.Context, id string) (*User, error) { - // Check for cancellation - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - // Pass to downstream calls - row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id) - // ... -} - -// HTTP handler receives context from request -func handler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - user, err := fetchUser(ctx, r.URL.Query().Get("id")) - // ... -} -``` - -### Context Best Practices - -- Always pass context as the first parameter named `ctx` -- Never store context in a struct; pass it explicitly -- Always call the cancel function (defer it immediately) -- Derive from the incoming context, never create `context.Background()` mid-chain -- Use `context.WithValue` only for request-scoped data (request IDs, auth tokens), not for function parameters -- Check `ctx.Err()` or `ctx.Done()` in long-running operations - -## Iterators (Go 1.23+) - -### Iterator Function Types - -The `iter` package defines two function types: - -```go -// Single-value iterator -type Seq[V any] func(yield func(V) bool) - -// Key-value iterator -type Seq2[K, V any] func(yield func(K, V) bool) -``` - -### Creating Iterators - -```go -// Filter iterator -func Filter[T any](seq iter.Seq[T], pred func(T) bool) iter.Seq[T] { - return func(yield func(T) bool) { - for v := range seq { - if pred(v) { - if !yield(v) { - return - } - } - } - } -} - -// Map iterator -func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] { - return func(yield func(U) bool) { - for v := range seq { - if !yield(f(v)) { - return - } - } - } -} - -// Limit iterator -func Take[T any](seq iter.Seq[T], n int) iter.Seq[T] { - return func(yield func(T) bool) { - i := 0 - for v := range seq { - if i >= n { - return - } - if !yield(v) { - return - } - i++ - } - } -} -``` - -### Standard Library Iterator Support - -```go -// slices package -for i, v := range slices.All(mySlice) { } // index, value -for v := range slices.Values(mySlice) { } // values only -for v := range slices.Backward(mySlice) { } // reverse order -collected := slices.Collect(myIterator) // iterator -> slice -sorted := slices.Sorted(myIterator) // sort values - -// maps package -for k, v := range maps.All(myMap) { } // all entries -for k := range maps.Keys(myMap) { } // keys only -for v := range maps.Values(myMap) { } // values only -collected := maps.Collect(mySeq2) // iterator -> map -``` - -### Pull Iterators - -When ranging is not natural, convert to pull-style iteration: - -```go -next, stop := iter.Pull(mySeq) -defer stop() - -v1, ok := next() -if !ok { return } - -v2, ok := next() -if !ok { return } - -// Pull2 for key-value iterators -next2, stop2 := iter.Pull2(mySeq2) -defer stop2() -``` - -## Struct Embedding - -### Basic Embedding - -```go -type Logger struct { - Prefix string -} - -func (l *Logger) Log(msg string) { - fmt.Printf("[%s] %s\n", l.Prefix, msg) -} - -type Service struct { - Logger // Embedded (promoted methods) - Name string -} - -s := Service{ - Logger: Logger{Prefix: "SVC"}, - Name: "auth", -} -s.Log("started") // Promoted from Logger -s.Logger.Log("direct call") // Also works -``` - -### Interface Embedding in Structs - -Useful for partial interface implementation and the decorator pattern. - -```go -// Wrap an interface, override specific methods -type LoggingReader struct { - io.Reader // Embedded interface - logger *slog.Logger -} - -func (lr *LoggingReader) Read(p []byte) (int, error) { - n, err := lr.Reader.Read(p) // Delegate to wrapped reader - lr.logger.Info("read", "bytes", n, "err", err) - return n, err -} - -// Still satisfies io.Reader -var _ io.Reader = (*LoggingReader)(nil) -``` - -### Embedding vs Named Fields - -```go -// Embedding: promotes methods, acts like "is-a" (composition) -type Server struct { - http.Handler // Server IS-A handler -} - -// Named field: explicit access, acts like "has-a" -type Server struct { - handler http.Handler // Server HAS-A handler -} - -// Prefer named fields when: -// - You want to hide the embedded type's methods -// - Multiple embedded types have conflicting method names -// - The relationship is clearly "has-a" -``` - -## Testing Patterns - -### Table-Driven Tests - -```go -func TestAdd(t *testing.T) { - tests := []struct { - name string - a, b int - expected int - }{ - {"positive", 2, 3, 5}, - {"negative", -1, -2, -3}, - {"zero", 0, 0, 0}, - {"mixed", -1, 5, 4}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := Add(tt.a, tt.b) - if got != tt.expected { - t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected) - } - }) - } -} -``` - -### Test Helpers - -```go -// t.Helper() marks a function as a test helper -// so errors report the caller's line, not the helper's -func assertNoError(t *testing.T, err error) { - t.Helper() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func assertEqual[T comparable](t *testing.T, got, want T) { - t.Helper() - if got != want { - t.Errorf("got %v, want %v", got, want) - } -} - -// Cleanup function -func setupTestDB(t *testing.T) *sql.DB { - t.Helper() - db, err := sql.Open("sqlite3", ":memory:") - assertNoError(t, err) - t.Cleanup(func() { db.Close() }) - return db -} -``` - -### Subtests and Parallel Tests - -```go -func TestParallel(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - {"uppercase", "hello", "HELLO"}, - {"empty", "", ""}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() // Run subtests in parallel - got := strings.ToUpper(tt.input) - if got != tt.want { - t.Errorf("got %q, want %q", got, tt.want) - } - }) - } -} -``` - -### Interface Mocking - -```go -// Define interface for dependencies -type UserStore interface { - GetUser(ctx context.Context, id string) (*User, error) -} - -// Mock implementation for tests -type mockUserStore struct { - getUser func(ctx context.Context, id string) (*User, error) -} - -func (m *mockUserStore) GetUser(ctx context.Context, id string) (*User, error) { - return m.getUser(ctx, id) -} - -func TestService(t *testing.T) { - store := &mockUserStore{ - getUser: func(ctx context.Context, id string) (*User, error) { - if id == "123" { - return &User{Name: "Alice"}, nil - } - return nil, ErrNotFound - }, - } - svc := NewService(store) - user, err := svc.GetUser(context.Background(), "123") - // assert... -} -``` diff --git a/packages/dotfiles/dot_agents/skills/go-helper/references/releases.md b/packages/dotfiles/dot_agents/skills/go-helper/references/releases.md new file mode 100644 index 0000000000..5899bd610c --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/go-helper/references/releases.md @@ -0,0 +1,67 @@ +# Go release lifecycle + +Read this when upgrading Go, adopting a newly added standard-library API, or evaluating a release performance claim. + +## Current release + +Go 1.26.5 is current stable as of 2026-08-03. Verify the patch release live before publishing a version claim. Read every release note between the module's current toolchain and the target. + +Important boundaries: + +- Go 1.21 made the `go` directive a strict minimum and added automatic toolchain switching. +- Go 1.22 changed loop-variable semantics according to package language version. +- Go 1.23 timer-channel semantics depend on the main module's language version. +- Go 1.24 added tool directives, generic aliases, `B.Loop`, and `os.Root`. +- Go 1.25 stabilized `testing/synctest`, added flight recording, and made `GOMAXPROCS` cgroup-aware. +- Go 1.26 enabled Green Tea GC by default and added `crypto/hpke`; JSON v2 remains experimental. + +Performance figures in release notes are benchmark-dependent. Preserve attribution and workload rather than turning them into universal guarantees. + +## Research ledger + +The following 44 primary pages were fetched and inspected: + +1. [Go 1.26 release notes](https://go.dev/doc/go1.26) +2. [Go 1.25 release notes](https://go.dev/doc/go1.25) +3. [Go 1.24 release notes](https://go.dev/doc/go1.24) +4. [Go 1.23 release notes](https://go.dev/doc/go1.23) +5. [Go 1.22 release notes](https://go.dev/doc/go1.22) +6. [Go 1.21 release notes](https://go.dev/doc/go1.21) +7. [Go release history](https://go.dev/doc/devel/release) +8. [Go specification](https://go.dev/ref/spec) +9. [Go module reference](https://go.dev/doc/modules/gomod-ref) +10. [Go module specification](https://go.dev/ref/mod) +11. [Go toolchains](https://go.dev/doc/toolchain) +12. [cmd/go](https://pkg.go.dev/cmd/go) +13. [testing](https://pkg.go.dev/testing) +14. [testing/synctest](https://pkg.go.dev/testing/synctest) +15. [runtime/trace](https://pkg.go.dev/runtime/trace) +16. [runtime/pprof](https://pkg.go.dev/runtime/pprof) +17. [net/http/pprof](https://pkg.go.dev/net/http/pprof) +18. [crypto/hpke](https://pkg.go.dev/crypto/hpke) +19. [encoding/json/v2](https://pkg.go.dev/encoding/json/v2) +20. [os](https://pkg.go.dev/os) +21. [runtime](https://pkg.go.dev/runtime) +22. [iter](https://pkg.go.dev/iter) +23. [maps](https://pkg.go.dev/maps) +24. [slices](https://pkg.go.dev/slices) +25. [unique](https://pkg.go.dev/unique) +26. [math/rand/v2](https://pkg.go.dev/math/rand/v2) +27. [log/slog](https://pkg.go.dev/log/slog) +28. [Go fuzzing](https://go.dev/doc/security/fuzz/) +29. [Race detector](https://go.dev/doc/articles/race_detector) +30. [PGO](https://go.dev/doc/pgo) +31. [GC guide](https://go.dev/doc/gc-guide) +32. [Go workspaces](https://go.dev/doc/tutorial/workspaces) +33. [Managing dependencies](https://go.dev/doc/modules/managing-dependencies) +34. [Module release workflow](https://go.dev/doc/modules/release-workflow) +35. [Go vulnerability management](https://go.dev/doc/security/vuln/) +36. [golangci-lint changelog](https://golangci-lint.run/docs/product/changelog/) +37. [golangci-lint configuration](https://golangci-lint.run/docs/configuration/file/) +38. [golangci-lint linters](https://golangci-lint.run/docs/linters/) +39. [gopls settings](https://go.dev/gopls/settings) +40. [Delve usage](https://github.com/go-delve/delve/blob/master/Documentation/usage/dlv.md) +41. [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) +42. [go vet](https://pkg.go.dev/cmd/vet) +43. [pkg/errors README](https://github.com/pkg/errors/blob/master/README.md) +44. [coder/websocket README](https://github.com/coder/websocket/blob/master/README.md) diff --git a/packages/dotfiles/dot_agents/skills/go-helper/references/testing-and-performance.md b/packages/dotfiles/dot_agents/skills/go-helper/references/testing-and-performance.md new file mode 100644 index 0000000000..1d20392071 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/go-helper/references/testing-and-performance.md @@ -0,0 +1,78 @@ +# Go testing and performance + +Read this when writing tests, fuzzing, using the race detector or `synctest`, collecting traces/profiles, tuning GC, or adopting PGO. + +## Tests and cleanup + +Use table tests when cases share a contract. Cleanup runs in last-in-first-out order. Register cleanup only after setup succeeds, and surface cleanup errors that can invalidate the test. + +## Benchmarks + +Current Go prefers `B.Loop()`: + +```go +func BenchmarkEncode(b *testing.B) { + for b.Loop() { + encode(testValue) + } +} +``` + +Benchmark results need stable inputs, environment context, and statistical comparison. Do not quote a speedup without the exact workload. + +## Fuzzing + +Fuzz targets add seeds and exercise generated inputs. A failing input is written directly to: + +```text +testdata/fuzz/FuzzParseJSON/ +``` + +There are no required `corpus/` or `seed/` subdirectories. Preserve useful failures as regression cases. + +## Race detector + +`go test -race` detects only races executed in that run and only on supported platforms. Overhead is workload-dependent. Run it on representative tests rather than claiming no false positives or a fixed cost. + +## synctest + +Stable Go 1.25+ usage: + +```go +func TestTimeout(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + // start goroutines and advance only after durable blocking + synctest.Wait() + }) +} +``` + +Do not use the removed experimental `synctest.Run` API. + +## Profiles and traces + +`runtime/pprof` supports CPU, heap, goroutine, block, and mutex profiles. Block and mutex profiles require enabling nonzero rates. + +Importing `net/http/pprof` registers handlers. Bind them to localhost or a protected operator interface and check `ListenAndServe` errors. + +Flight recorder lifecycle: + +1. Check `Start()`. +2. Trigger and check `WriteTo()`. +3. Call `Stop()`. + +## PGO and GC + +Go automatically uses `default.pgo` when present. Use a representative production CPU profile. GC tuning through `GOGC` and memory limits is workload-specific; current Go 1.26 defaults to Green Tea GC. + +## Primary documentation + +- [testing](https://pkg.go.dev/testing) +- [testing/synctest](https://pkg.go.dev/testing/synctest) +- [runtime/trace](https://pkg.go.dev/runtime/trace) +- [runtime/pprof](https://pkg.go.dev/runtime/pprof) +- [net/http/pprof](https://pkg.go.dev/net/http/pprof) +- [Go fuzzing](https://go.dev/doc/security/fuzz/) +- [Race detector](https://go.dev/doc/articles/race_detector) +- [PGO](https://go.dev/doc/pgo) +- [GC guide](https://go.dev/doc/gc-guide) diff --git a/packages/dotfiles/dot_agents/skills/go-helper/references/testing-debugging.md b/packages/dotfiles/dot_agents/skills/go-helper/references/testing-debugging.md deleted file mode 100644 index a39b16ce9c..0000000000 --- a/packages/dotfiles/dot_agents/skills/go-helper/references/testing-debugging.md +++ /dev/null @@ -1,855 +0,0 @@ -# Testing and Debugging Go - -Guide to go test, table-driven tests, benchmarks, fuzzing, testify, the delve debugger, profiling with pprof, and the race detector. - -## Testing with go test - -### Basic Usage - -```bash -# Run all tests -go test ./... - -# Run tests in specific package -go test ./pkg/mypackage - -# Run specific test function -go test -run TestMyFunction ./... -go test -run TestMyFunction/subtest_name ./... - -# Verbose output -go test -v ./... - -# Short mode (skip long tests) -go test -short ./... - -# Run with count (disable caching) -go test -count=1 ./... - -# Set timeout -go test -timeout 120s ./... - -# Parallel test limit -go test -parallel 4 ./... - -# List tests without running -go test -list '.*' ./... - -# Show test binary output -go test -v -count=1 ./... - -# JSON output -go test -json ./... -``` - -### Test Organization - -```go -// Unit tests: same package, same file or _test.go suffix -// File: calculator.go -package calculator - -func Add(a, b int) int { return a + b } - -// File: calculator_test.go -package calculator - -import "testing" - -func TestAdd(t *testing.T) { - if got := Add(2, 3); got != 5 { - t.Errorf("Add(2, 3) = %d, want 5", got) - } -} -``` - -```go -// Black-box tests: test the public API from outside -// File: calculator_test.go -package calculator_test - -import ( - "testing" - "github.com/user/project/calculator" -) - -func TestAdd(t *testing.T) { - if got := calculator.Add(2, 3); got != 5 { - t.Errorf("Add(2, 3) = %d, want 5", got) - } -} -``` - -### Integration Tests - -```go -// tests/integration_test.go -//go:build integration - -package tests - -import "testing" - -func TestDatabaseIntegration(t *testing.T) { - // Only runs with: go test -tags integration ./tests/ - db := connectTestDB(t) - // ... -} -``` - -### TestMain - -Use `TestMain` for setup/teardown that applies to all tests in a package. - -```go -func TestMain(m *testing.M) { - // Setup - db := setupTestDB() - - // Run tests - code := m.Run() - - // Teardown - db.Close() - os.Exit(code) -} -``` - -### Test Helpers and Cleanup - -```go -func setupServer(t *testing.T) *httptest.Server { - t.Helper() - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"ok"}`)) - })) - t.Cleanup(func() { srv.Close() }) - return srv -} - -// TempDir creates a temp directory cleaned up after test -func TestFileOps(t *testing.T) { - dir := t.TempDir() // auto-cleaned after test - path := filepath.Join(dir, "test.txt") - os.WriteFile(path, []byte("hello"), 0644) - // ... -} -``` - -## Table-Driven Tests - -The idiomatic Go testing pattern: define test cases as data, loop through them. - -### Basic Table-Driven Test - -```go -func TestParseSize(t *testing.T) { - tests := []struct { - name string - input string - want int64 - wantErr bool - }{ - {name: "bytes", input: "100B", want: 100}, - {name: "kilobytes", input: "1KB", want: 1024}, - {name: "megabytes", input: "5MB", want: 5 * 1024 * 1024}, - {name: "empty", input: "", wantErr: true}, - {name: "invalid", input: "abc", wantErr: true}, - {name: "negative", input: "-1KB", wantErr: true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ParseSize(tt.input) - if (err != nil) != tt.wantErr { - t.Fatalf("ParseSize(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) - } - if got != tt.want { - t.Errorf("ParseSize(%q) = %d, want %d", tt.input, got, tt.want) - } - }) - } -} -``` - -### Parallel Table-Driven Tests - -```go -func TestSlugify(t *testing.T) { - tests := map[string]struct { - input string - want string - }{ - "simple": {input: "Hello World", want: "hello-world"}, - "special chars": {input: "Hello, World!", want: "hello-world"}, - "multiple spaces": {input: "hello world", want: "hello-world"}, - "already slug": {input: "hello-world", want: "hello-world"}, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - t.Parallel() - got := Slugify(tt.input) - if got != tt.want { - t.Errorf("Slugify(%q) = %q, want %q", tt.input, got, tt.want) - } - }) - } -} -``` - -### Table Tests with Complex Setup - -```go -func TestHTTPHandler(t *testing.T) { - tests := []struct { - name string - method string - path string - body string - wantStatus int - wantBody string - }{ - { - name: "get existing", - method: http.MethodGet, - path: "/users/1", - wantStatus: http.StatusOK, - wantBody: `{"id":"1","name":"Alice"}`, - }, - { - name: "get missing", - method: http.MethodGet, - path: "/users/999", - wantStatus: http.StatusNotFound, - }, - { - name: "create user", - method: http.MethodPost, - path: "/users", - body: `{"name":"Bob"}`, - wantStatus: http.StatusCreated, - }, - } - - handler := NewRouter() - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var body io.Reader - if tt.body != "" { - body = strings.NewReader(tt.body) - } - req := httptest.NewRequest(tt.method, tt.path, body) - rec := httptest.NewRecorder() - - handler.ServeHTTP(rec, req) - - if rec.Code != tt.wantStatus { - t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) - } - if tt.wantBody != "" && strings.TrimSpace(rec.Body.String()) != tt.wantBody { - t.Errorf("body = %q, want %q", rec.Body.String(), tt.wantBody) - } - }) - } -} -``` - -## Benchmarks - -### Writing Benchmarks - -```go -func BenchmarkFibonacci(b *testing.B) { - for b.Loop() { - Fibonacci(20) - } -} - -// Benchmark with different inputs -func BenchmarkSort(b *testing.B) { - sizes := []int{10, 100, 1000, 10000} - for _, size := range sizes { - b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) { - data := generateRandomSlice(size) - b.ResetTimer() - for b.Loop() { - sorted := make([]int, len(data)) - copy(sorted, data) - sort.Ints(sorted) - } - }) - } -} - -// Report memory allocations -func BenchmarkConcat(b *testing.B) { - b.ReportAllocs() - for b.Loop() { - var s string - for i := 0; i < 100; i++ { - s += "x" - } - } -} - -func BenchmarkBuilder(b *testing.B) { - b.ReportAllocs() - for b.Loop() { - var sb strings.Builder - for i := 0; i < 100; i++ { - sb.WriteString("x") - } - _ = sb.String() - } -} -``` - -### Running Benchmarks - -```bash -# Run all benchmarks -go test -bench=. ./... - -# Run specific benchmark -go test -bench=BenchmarkFibonacci ./... - -# With memory allocation stats -go test -bench=. -benchmem ./... - -# Run N times for stable results -go test -bench=. -count=5 ./... - -# Set benchmark time -go test -bench=. -benchtime=5s ./... -go test -bench=. -benchtime=10000x ./... # Exact iterations - -# Compare benchmarks (using benchstat) -go test -bench=. -count=10 ./... > old.txt -# Make changes... -go test -bench=. -count=10 ./... > new.txt -go install golang.org/x/perf/cmd/benchstat@latest -benchstat old.txt new.txt -``` - -### Benchmark Output - -``` -BenchmarkSort/size_10-8 5000000 230 ns/op 80 B/op 1 allocs/op -BenchmarkSort/size_100-8 500000 3200 ns/op 896 B/op 1 allocs/op -BenchmarkSort/size_1000-8 30000 42000 ns/op 8192 B/op 1 allocs/op -``` - -## Fuzzing (Go 1.18+) - -### Writing Fuzz Tests - -```go -func FuzzParseJSON(f *testing.F) { - // Seed corpus: known-good inputs - f.Add([]byte(`{"name":"alice"}`)) - f.Add([]byte(`{"name":"bob","age":30}`)) - f.Add([]byte(`{}`)) - f.Add([]byte(`[]`)) - - f.Fuzz(func(t *testing.T, data []byte) { - var result map[string]any - err := json.Unmarshal(data, &result) - if err != nil { - return // Invalid input is fine, just skip - } - // If we could unmarshal, we should be able to marshal back - encoded, err := json.Marshal(result) - if err != nil { - t.Errorf("failed to re-marshal: %v", err) - } - // Round-trip should produce valid JSON - var result2 map[string]any - if err := json.Unmarshal(encoded, &result2); err != nil { - t.Errorf("round-trip failed: %v", err) - } - }) -} - -func FuzzReverse(f *testing.F) { - f.Add("hello") - f.Add("") - f.Add("12345") - - f.Fuzz(func(t *testing.T, s string) { - reversed := Reverse(s) - doubleReversed := Reverse(reversed) - if s != doubleReversed { - t.Errorf("Reverse(Reverse(%q)) = %q", s, doubleReversed) - } - if len(s) != len(reversed) { - t.Errorf("len mismatch: %d != %d", len(s), len(reversed)) - } - }) -} -``` - -### Running Fuzz Tests - -```bash -# Run fuzz test for 30 seconds -go test -fuzz=FuzzParseJSON -fuzztime=30s ./... - -# Run until failure -go test -fuzz=FuzzParseJSON ./... - -# Run as regular test (seed corpus only) -go test -run=FuzzParseJSON ./... - -# Fuzz with specific parallelism -go test -fuzz=FuzzReverse -fuzztime=1m -parallel=4 ./... -``` - -### Corpus Management - -Failing inputs are saved to `testdata/fuzz//` and automatically included in future test runs: - -``` -testdata/ - fuzz/ - FuzzParseJSON/ - corpus/ - abc123 # Auto-generated failing input - seed/ # Optional: manually added seeds -``` - -## Testify - -### Assertions - -```go -import ( - "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestWithTestify(t *testing.T) { - // assert: test continues on failure - assert.Equal(t, 5, Add(2, 3)) - assert.NotNil(t, result) - assert.True(t, ok) - assert.Contains(t, "hello world", "hello") - assert.Len(t, items, 3) - assert.Empty(t, emptySlice) - assert.Error(t, err) - assert.ErrorIs(t, err, ErrNotFound) - assert.ErrorAs(t, err, &targetErr) - assert.NoError(t, err) - assert.InDelta(t, 3.14, pi, 0.01) - assert.Eventually(t, func() bool { return ready }, time.Second, 10*time.Millisecond) - - // require: test stops immediately on failure (uses t.FailNow) - require.NoError(t, err) // Stop if error, no point continuing - require.NotNil(t, result) // Stop if nil, would panic below - assert.Equal(t, "Alice", result.Name) // Fine to continue -} -``` - -### Mocks - -```go -import "github.com/stretchr/testify/mock" - -type MockStore struct { - mock.Mock -} - -func (m *MockStore) Get(ctx context.Context, id string) (*Item, error) { - args := m.Called(ctx, id) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*Item), args.Error(1) -} - -func TestService(t *testing.T) { - store := new(MockStore) - store.On("Get", mock.Anything, "123").Return(&Item{Name: "test"}, nil) - store.On("Get", mock.Anything, "999").Return(nil, ErrNotFound) - - svc := NewService(store) - - item, err := svc.GetItem(ctx, "123") - require.NoError(t, err) - assert.Equal(t, "test", item.Name) - - _, err = svc.GetItem(ctx, "999") - assert.ErrorIs(t, err, ErrNotFound) - - store.AssertExpectations(t) -} -``` - -### go-cmp for Deep Comparison - -```go -import "github.com/google/go-cmp/cmp" - -func TestDeepEqual(t *testing.T) { - got := fetchConfig() - want := &Config{Name: "test", Port: 8080} - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("config mismatch (-want +got):\n%s", diff) - } -} -``` - -## Delve Debugger - -### Installation - -```bash -go install github.com/go-delve/delve/cmd/dlv@latest -``` - -### Starting Delve - -```bash -# Debug current package -dlv debug ./cmd/myapp - -# Debug with arguments -dlv debug ./cmd/myapp -- --config config.yaml - -# Debug test -dlv test ./pkg/mypackage -dlv test ./pkg/mypackage -- -run TestMyFunction - -# Attach to running process -dlv attach - -# Debug core dump -dlv core ./myapp core.dump - -# Run in headless mode (for IDE integration) -dlv debug --headless --listen=:2345 --api-version=2 ./cmd/myapp -``` - -### Delve Commands - -``` -# Breakpoints -break main.main # Set by function name -break main.go:42 # Set by file:line -break mypackage.MyFunc # Set in package -condition 1 x > 10 # Conditional breakpoint -breakpoints # List breakpoints -clear 1 # Remove breakpoint -clearall # Remove all breakpoints - -# Execution -continue (c) # Run until breakpoint -next (n) # Step over -step (s) # Step into -stepout # Step out of current function -restart (r) # Restart program - -# Inspection -print (p) variableName # Print variable -locals # Show all local variables -args # Show function arguments -whatis variableName # Show type of variable -set variableName = value # Modify variable - -# Stack -stack (bt) # Print stack trace -frame 2 # Switch to stack frame -up # Move up stack frame -down # Move down stack frame - -# Goroutines -goroutines # List all goroutines -goroutine 5 # Switch to goroutine 5 -goroutines -t # Show goroutine stack traces - -# Threads -threads # List threads -thread 3 # Switch to thread -``` - -### VS Code Integration - -Install the Go extension. It uses delve automatically. Add launch configuration: - -```json -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Package", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/cmd/myapp", - "args": ["--config", "config.yaml"] - }, - { - "name": "Debug Test", - "type": "go", - "request": "launch", - "mode": "test", - "program": "${workspaceFolder}/pkg/mypackage", - "args": ["-test.run", "TestMyFunction"] - } - ] -} -``` - -## Profiling with pprof - -### Adding pprof to Your Application - -```go -import ( - "net/http" - _ "net/http/pprof" // Register pprof handlers -) - -func main() { - // Start pprof server on separate port - go func() { - http.ListenAndServe("localhost:6060", nil) - }() - - // Your application code... -} -``` - -### Collecting Profiles - -```bash -# CPU profile (30 seconds by default) -go tool pprof http://localhost:6060/debug/pprof/profile -go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60 - -# Heap (memory) profile -go tool pprof http://localhost:6060/debug/pprof/heap - -# Goroutine profile -go tool pprof http://localhost:6060/debug/pprof/goroutine - -# Block profile (contention) -go tool pprof http://localhost:6060/debug/pprof/block - -# Mutex profile -go tool pprof http://localhost:6060/debug/pprof/mutex - -# Allocs profile (all past allocations) -go tool pprof http://localhost:6060/debug/pprof/allocs - -# Thread creation profile -go tool pprof http://localhost:6060/debug/pprof/threadcreate -``` - -### Profiling Tests - -```bash -# CPU profile from tests -go test -cpuprofile=cpu.prof -bench=. ./... -go tool pprof cpu.prof - -# Memory profile from tests -go test -memprofile=mem.prof -bench=. ./... -go tool pprof mem.prof - -# Block profile -go test -blockprofile=block.prof ./... - -# Mutex profile -go test -mutexprofile=mutex.prof ./... -``` - -### pprof Interactive Commands - -``` -# Top functions by CPU/memory -top -top 20 -top -cum # Cumulative (including callees) - -# Show specific function -list functionName - -# Show call graph as text -tree - -# Generate visualization -web # Open in browser (requires graphviz) -svg # Generate SVG - -# Filter -top -cum -nodecount=20 -focus=mypackage -``` - -### Web UI - -```bash -# Open interactive web UI with flame graph (Go 1.26: opens flame graph by default) -go tool pprof -http=:8080 cpu.prof -go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap - -# Compare two profiles -go tool pprof -diff_base=old.prof new.prof -``` - -### Execution Tracer - -```bash -# Collect trace -curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5 - -# From tests -go test -trace=trace.out ./... - -# View trace -go tool trace trace.out -``` - -The trace viewer shows: - -- Goroutine scheduling and blocking -- System calls -- GC events -- Network I/O -- Heap allocation - -### Flight Recorder (Go 1.25+) - -```go -import "runtime/trace" - -fr := trace.NewFlightRecorder() -fr.Start() - -// When something interesting happens... -fr.WriteTo(file) // Snapshot the ring buffer -``` - -## Race Detector - -### Using the Race Detector - -```bash -# Build with race detector -go build -race ./... - -# Test with race detector -go test -race ./... - -# Run with race detector -go run -race ./cmd/myapp -``` - -### What It Detects - -The race detector finds data races: concurrent unsynchronized access to shared memory where at least one access is a write. - -```go -// This has a data race: -var count int -go func() { count++ }() -go func() { count++ }() - -// Fixed with mutex: -var mu sync.Mutex -var count int -go func() { mu.Lock(); count++; mu.Unlock() }() -go func() { mu.Lock(); count++; mu.Unlock() }() - -// Or atomic: -var count atomic.Int64 -go func() { count.Add(1) }() -go func() { count.Add(1) }() -``` - -### Race Detector Notes - -- Adds ~5-10x CPU overhead and ~5-10x memory overhead -- Only detects races that actually occur during execution (not all possible races) -- Always run `go test -race` in CI -- No false positives: if it reports a race, there is one -- Set `GORACE` environment variable for options: - -```bash -# Log to file -GORACE="log_path=/tmp/race.log" go test -race ./... - -# Halt on first race -GORACE="halt_on_error=1" go test -race ./... - -# History size (default 1, increase for better stack traces) -GORACE="history_size=5" go test -race ./... -``` - -## testing/synctest (Go 1.25+) - -For testing concurrent code with a fake clock: - -```go -import "testing/synctest" - -func TestTimeout(t *testing.T) { - synctest.Run(func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - done := make(chan struct{}) - go func() { - // Simulate work - time.Sleep(3 * time.Second) // Uses fake clock - close(done) - }() - - select { - case <-done: - // Work completed before timeout - case <-ctx.Done(): - t.Fatal("unexpected timeout") - } - }) -} -``` - -## CI/CD Testing Pattern - -### GitHub Actions - -```yaml -name: CI -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: "1.26" - - - name: Vet - run: go vet ./... - - - name: Lint - uses: golangci/golangci-lint-action@v6 - with: - version: latest - - - name: Test - run: go test -race -coverprofile=coverage.out ./... - - - name: Coverage - run: go tool cover -func=coverage.out - - - name: Build - run: go build ./... -``` diff --git a/packages/dotfiles/dot_agents/skills/jvm-helper/SKILL.md b/packages/dotfiles/dot_agents/skills/jvm-helper/SKILL.md index a2dc2086f8..061bce88ad 100644 --- a/packages/dotfiles/dot_agents/skills/jvm-helper/SKILL.md +++ b/packages/dotfiles/dot_agents/skills/jvm-helper/SKILL.md @@ -1,366 +1,146 @@ --- name: jvm-helper -description: | - Java and Kotlin development with modern patterns, build tools, and JVM tooling - When user works with .java or .kt files, mentions Java, Kotlin, Gradle, Maven, JVM, or JDK features +description: Current Java, Kotlin, Gradle, Maven, JUnit, JVM diagnostics, packaging, and performance guidance. Use when writing or reviewing Java or Kotlin, build files, JVM tests, concurrency, GraalVM Native Image, jlink/jpackage, or JVM tuning. --- -# JVM Helper Agent +# JVM Helper -## What's New +Use the project's wrappers and toolchains, distinguish stable APIs from previews, and measure runtime behavior before changing JVM flags. Current releases are not automatic migration targets for an existing project. -### Java Releases +## Current baseline -- **Java 25 LTS** (Sep 2025): Next LTS after 21. Finalizes: Scoped Values, Module Import Declarations, Compact Source Files / Instance Main Methods, Flexible Constructor Bodies, Compact Object Headers, Generational Shenandoah. Previews: Structured Concurrency (5th), Primitive Types in Patterns (3rd), Stable Values, PEM Encodings -- **Java 24** (Mar 2025): 24 JEPs. Finalizes Stream Gatherers, Class-File API. Previews: Flexible Constructor Bodies (3rd), Primitive Types in Patterns (2nd). Deprecates 32-bit x86 port -- **Java 23** (Sep 2024): Primitive Types in Patterns preview, Module Import Declarations preview, Implicitly Declared Classes (3rd preview). Removes String Templates (design issues). Introduces Oracle GraalVM JIT as JDK option -- **Java 22** (Mar 2024): 12 JEPs. Finalizes Foreign Function & Memory API (JEP 454), Unnamed Variables & Patterns (JEP 456). Previews: Statements before super(), Implicitly Declared Classes (2nd) -- **Java 21 LTS** (Sep 2023): 15 JEPs. Finalizes Virtual Threads (JEP 444), Record Patterns (JEP 440), Pattern Matching for switch (JEP 441), Sequenced Collections (JEP 431). Previews: String Templates, Structured Concurrency, Scoped Values, Unnamed Patterns +Verified 2026-08-03: -### Kotlin Releases +| Component | Current | Boundary | +| --- | --- | --- | +| Java | JDK 26 GA | Java 25 is Oracle-designated LTS; lifecycle and support vary by vendor | +| Kotlin | 2.4.10 | Kotlin 2.4 adds Java 26 support and stable context parameters | +| Gradle | 9.6.1 | Runs on JVM 17–26; use the project wrapper | +| Maven | 3.9.16 stable | 3.10 and Maven 4 remain preview lines | +| JUnit | 6.1.2 | Requires Java 17; major migration from JUnit 5 | +| kotlinx.coroutines | 1.11.0 | Follow structured cancellation and dispatcher lifecycles | -- **Kotlin 2.1** (Nov 2024): Guard conditions in `when` expressions, basic Swift export support, stable Gradle DSL for compiler options, K2 kapt enabled by default (2.1.20), Lombok `@SuperBuilder` support -- **Kotlin 2.0** (May 2024): Stable K2 compiler - 2x faster compilation on average (initialization up to 488% faster, analysis up to 376% faster). Unified pipeline for all backends (JVM, JS, Wasm, Native). Improved smart casts, redesigned multiplatform compilation scheme +Spring Boot 4.1.0, Ktor 3.5.1, and Shadow 9.6.1 are current, but their major upgrades are not drop-in substitutions for an existing build. -## Overview +Read [references/releases.md](references/releases.md) for the 75-page research ledger. Read [references/java-and-kotlin.md](references/java-and-kotlin.md) for current language/concurrency APIs. Read [references/build-and-test.md](references/build-and-test.md) for wrappers, Gradle, Maven, JUnit, and build-cache correctness. Read [references/diagnostics-and-packaging.md](references/diagnostics-and-packaging.md) for jcmd/JFR/JMX, JVM tuning, jlink, jpackage, and Native Image. -This skill covers Java and Kotlin development on the JVM, including modern language features (Java 21+ and Kotlin 2.x), build tools (Gradle and Maven), packaging tools (jlink, jpackage, GraalVM native-image), and JVM tuning. The user manages Java via mise (LTS versions), so focus on Java 21 LTS features with awareness of 25 LTS additions. - -## CLI Commands - -### Auto-Approved Safe Commands +## Establish the toolchain ```bash -# Compile Java source +java --version javac --version -javac -d out src/Main.java +./gradlew --version +./mvnw --version +``` -# Run Java program -java --version -java -cp out Main +Prefer repository wrappers and declared Java toolchains. A globally installed current JDK does not change the project's source, target, runtime, or support contract. -# Interactive Java REPL -jshell +## Command authority -# Kotlin compiler -kotlinc --version -kotlinc hello.kt -include-runtime -d hello.jar +Build and test commands create outputs and can resolve dependencies: -# Gradle (read-only / build) -gradle --version -./gradlew tasks +```bash ./gradlew build -./gradlew test -./gradlew check -./gradlew dependencies -./gradlew dependencyInsight --dependency -./gradlew projects - -# Maven (read-only / build) -mvn --version -mvn compile -mvn test -mvn package -mvn dependency:tree -mvn dependency:resolve -mvn help:effective-pom -mvn help:active-profiles - -# JDK tools -jar --list --file app.jar -javap -c MyClass.class -jps -jstack -jmap -histo -jcmd VM.flags -jfr print recording.jfr +./mvnw verify ``` -### Build and Package +Live-process diagnostics can pause, attach to, or materially affect the target. Inspect the command's documented impact and production authority before running `jcmd`, `jmap`, heap dumps, or JFR operations. `jmap` is experimental and unsupported; prefer supported `jcmd` operations where appropriate. -```bash -# Gradle build -./gradlew clean build -./gradlew build -x test # skip tests -./gradlew :module:build # specific module -./gradlew bootRun # Spring Boot -./gradlew assemble # build without tests -./gradlew jar # build JAR - -# Maven build -mvn clean package -mvn package -DskipTests -mvn -pl module-name package # specific module -mvn spring-boot:run # Spring Boot -mvn verify # run integration tests - -# Create runtime image with jlink -jlink --module-path $JAVA_HOME/jmods:mods \ - --add-modules com.myapp \ - --output custom-runtime \ - --strip-debug --compress zip-6 - -# Create installable package with jpackage -jpackage --input lib/ --main-jar app.jar \ - --main-class com.example.Main \ - --name MyApp --type dmg - -# GraalVM native image -native-image -jar app.jar myapp -native-image --no-fallback -jar app.jar -``` +## Verification without skipped tests -## Modern Java Essentials (21 LTS) +Do not recommend `-x test`, `-DskipTests`, or `maven.test.skip` as a normal workflow. `assemble` only creates artifacts and does not satisfy verification. -### Records +For Maven, `verify` runs integration tests only when Failsafe or another plugin is bound to the `integration-test` and `verify` lifecycle phases. Check the POM before making the claim. -```java -// Immutable data carrier - auto-generates constructor, equals, hashCode, toString, accessors -record Point(int x, int y) {} - -// Records can have custom constructors and methods -record Range(int lo, int hi) { - Range { // compact constructor for validation - if (lo > hi) throw new IllegalArgumentException(); - } - int length() { return hi - lo; } -} +For Gradle, configure `useJUnitPlatform()` and the intended test suites. JUnit parallel execution is opt-in and must preserve test isolation. -// Records can implement interfaces -record NamedPoint(String name, int x, int y) implements Serializable {} -``` +## Java concurrency -### Sealed Classes +Virtual threads are appropriate for large numbers of blocking tasks, not CPU-bound parallelism. Create them through application-owned executors and close executor lifecycles. -```java -// Restrict which classes can extend -sealed interface Shape permits Circle, Rectangle, Triangle {} -record Circle(double radius) implements Shape {} -record Rectangle(double w, double h) implements Shape {} -record Triangle(double a, double b, double c) implements Shape {} - -// Exhaustive switch - compiler verifies all cases covered -double area(Shape s) { - return switch (s) { - case Circle c -> Math.PI * c.radius() * c.radius(); - case Rectangle r -> r.w() * r.h(); - case Triangle t -> { /* Heron's formula */ yield 0; } - }; -} -``` +JDK 24 eliminated nearly all virtual-thread pinning caused by `synchronized`. Do not mechanically replace correct synchronization with `ReentrantLock`; use JFR or `jcmd` to diagnose remaining pinning cases. -### Pattern Matching +Structured Concurrency remains preview. The API changed across previews. On JDK 25, use `StructuredTaskScope.open()` or `open(Joiner...)`, compile with `--enable-preview --release 25`, and run with `--enable-preview`. Verify the exact target JDK docs before copying an example. -```java -// Pattern matching for instanceof -if (obj instanceof String s && s.length() > 5) { - System.out.println(s.toUpperCase()); -} - -// Pattern matching for switch with guards -String format(Object obj) { - return switch (obj) { - case Integer i when i > 0 -> "positive: " + i; - case Integer i -> "non-positive: " + i; - case String s -> "string: " + s; - case null -> "null"; - default -> "other: " + obj; - }; -} - -// Record patterns (destructuring) -record Point(int x, int y) {} -if (obj instanceof Point(int x, int y)) { - System.out.println("x=" + x + " y=" + y); -} - -// Nested record patterns -record Line(Point start, Point end) {} -switch (shape) { - case Line(Point(var x1, var y1), Point(var x2, var y2)) -> - System.out.println("Line from (%d,%d) to (%d,%d)".formatted(x1, y1, x2, y2)); -} -``` - -### Virtual Threads +Scoped Values finalized in JDK 25: ```java -// Create virtual threads directly -Thread.startVirtualThread(() -> { - // lightweight, ideal for I/O-bound tasks - var result = fetchFromDatabase(); -}); - -// With executor -try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { - IntStream.range(0, 10_000).forEach(i -> - executor.submit(() -> handleRequest(i)) - ); -} - -// Virtual thread builder -Thread vt = Thread.ofVirtual() - .name("worker-", 0) - .start(() -> doWork()); +ScopedValue.where(CURRENT_USER, user).run(() -> handle(request)); ``` -### Sequenced Collections +Do not use removed preview-era `runWhere` examples. -```java -// New interfaces: SequencedCollection, SequencedSet, SequencedMap -SequencedCollection list = new ArrayList<>(List.of("a", "b", "c")); -list.getFirst(); // "a" -list.getLast(); // "c" -list.addFirst("z"); -list.reversed(); // reversed view - -SequencedMap map = new LinkedHashMap<>(); -map.putFirst("first", 1); -map.putLast("last", 99); -map.firstEntry(); // first=1 -map.pollLastEntry(); // removes and returns last -map.sequencedKeySet().reversed(); -``` - -### Unnamed Variables (finalized in Java 22, preview in 21) - -```java -// Underscore for unused variables -try { /* ... */ } catch (Exception _) { log("failed"); } +`HttpClient` makes no guarantee that its default executor uses virtual threads. If blocking `send` should run in a virtual thread, create that ownership explicitly. -for (var _ : collection) { count++; } - -map.forEach((_, value) -> process(value)); - -// Unnamed patterns in switch -case Point(var x, _) -> "x=" + x; // ignore y -``` +## Kotlin coroutines -## Kotlin Essentials - -### Null Safety +Preserve cooperative cancellation. A broad `catch (Exception)` can swallow `CancellationException`: ```kotlin -// Non-null by default -var name: String = "hello" -// name = null // compile error - -// Nullable types with ? -var nullable: String? = null - -// Safe call operator -val length = nullable?.length // null if nullable is null - -// Elvis operator -val len = nullable?.length ?: 0 - -// Not-null assertion (use sparingly) -val forced = nullable!!.length // throws if null - -// Smart cast after null check -if (nullable != null) { - println(nullable.length) // compiler knows it's non-null +try { + performWork() +} catch (cancellation: CancellationException) { + throw cancellation +} catch (failure: IOException) { + handleFailure(failure) } ``` -### Data Classes and Sealed Classes +`Dispatchers.IO` defaults to `max(64, availableProcessors)` parallelism and is configurable. `limitedParallelism` views can exceed that nominal bound. Treat these as implementation controls, not an application concurrency budget. + +Close executor-backed dispatchers: ```kotlin -// Auto-generates equals, hashCode, toString, copy, componentN -data class User(val name: String, val age: Int) -val user = User("Alice", 30) -val copy = user.copy(age = 31) - -// Sealed class hierarchy (exhaustive when) -sealed class Result { - data class Success(val data: T) : Result() - data class Error(val message: String) : Result() - data object Loading : Result() -} -when (result) { - is Result.Success -> println(result.data) - is Result.Error -> println(result.message) - Result.Loading -> println("loading...") +Executors.newFixedThreadPool(4).asCoroutineDispatcher().use { dispatcher -> + withContext(dispatcher) { performWork() } } ``` -### Extension Functions and Scope Functions +K2 has been the default compiler since Kotlin 2.0. Remove obsolete `-Pkotlin.experimental.tryK2=true` guidance. Kotlin guard conditions were preview in 2.1 and stable in 2.2. -```kotlin -// Extension function -fun String.isPalindrome(): Boolean = this == this.reversed() -"racecar".isPalindrome() // true - -// Scope functions -// let - transform, null-safe operations -nullable?.let { println(it.length) } - -// apply - configure object, returns receiver -val config = Config().apply { - host = "localhost" - port = 8080 -} +## Nullability -// also - side effects, returns receiver -val list = mutableListOf(1, 2).also { println("Initial: $it") } +Platform types cross an unchecked Java/Kotlin boundary. Use an identified annotation ecosystem and migration mode. Prefer JSpecify for new shared Java APIs where it fits; otherwise name the chosen JetBrains or Eclipse annotations rather than using unqualified `@Nullable` examples. -// run - compute and return result -val result = connection.run { - connect() - query("SELECT ...") -} +## Build correctness -// with - group calls on object -with(builder) { - setName("app") - setVersion("1.0") - build() -} -``` +Gradle cacheable tasks must declare every input and output. A generated version file needs the project version as an input; otherwise a version change can reuse stale output. -### Coroutines +Use `maven.compiler.release` rather than redundant source/target/release settings. A Maven project containing Kotlin sources needs `kotlin-maven-plugin` executions ordered correctly with Java compilation; declaring `kotlin.version` alone does nothing. -```kotlin -// Suspend function -suspend fun fetchUser(id: Int): User { - return httpClient.get("/users/$id").body() -} +Avoid fast-decaying dependency versions in generic snippets. Use version catalogs, BOMs, project properties, or clearly dated examples and consult migration guides for majors. -// Launch coroutine (fire and forget) -scope.launch { - val user = fetchUser(1) - updateUI(user) -} +## Diagnostics and tuning -// Async/await (concurrent) -val deferred1 = async { fetchUser(1) } -val deferred2 = async { fetchUser(2) } -val users = listOf(deferred1.await(), deferred2.await()) +Tune from evidence: -// Structured concurrency with coroutineScope -suspend fun loadDashboard() = coroutineScope { - val profile = async { fetchProfile() } - val feed = async { fetchFeed() } - Dashboard(profile.await(), feed.await()) -} -``` +1. Establish resource limits and latency/throughput SLOs. +2. Collect JFR, GC, native-memory, thread, and allocation evidence. +3. Change one supported option. +4. Load test the real workload. +5. Retain only measured improvement and document rollback. -## When to Ask for Help +Do not prescribe fixed heap ratios, stack sizes, compiler threads, pause targets, or direct-memory limits as universal defaults. Current JDKs use generational ZGC through `-XX:+UseZGC`; `-XX:+ZGenerational` is obsolete. -Ask the user for clarification when: +Heap dumps can contain secrets. Write them only to a private, access-controlled, capacity-checked path. Never enable unauthenticated or unencrypted remote JMX; use local attach, authenticated TLS, or an SSH-protected path. -- Choice between Java and Kotlin for a new module is unclear -- Build tool selection (Gradle vs Maven) needs deciding -- Spring Boot vs Quarkus vs Micronaut framework choice -- GraalVM native-image compatibility concerns exist -- Complex multi-module project structure decisions -- JVM tuning for specific workload characteristics -- Migration strategy between Java versions +## Packaging ---- +- JDK 25 `jlink` uses numeric compression such as `--compress=2`. +- JDK 26 supports `--compress=zip-6` and related named levels. +- `jpackage` does not cross-compile; build each package format on its target platform. +- Native Image uses closed-world analysis. Tracing-agent output covers only exercised behavior; prefer framework plugins and reachability metadata, then test representative paths. +- Remove obsolete canonical `--no-fallback` guidance and measure artifact/runtime size rather than promising fixed megabytes. -See `references/` for detailed guides: +## Review checklist -- `modern-java.md` - Records, sealed classes, pattern matching, virtual threads, structured concurrency, FFM API -- `kotlin-patterns.md` - Null safety, coroutines, sealed classes, extension functions, K2 compiler, KMP -- `build-tools.md` - Gradle Kotlin DSL, Maven, GraalVM native-image, jlink, jpackage, JVM tuning +- Verify wrapper, JDK, Kotlin, build-tool, and test-platform versions. +- Label previews and compile/run them with the exact target release. +- Preserve cancellation and close custom executor/dispatcher lifecycles. +- Do not claim HttpClient internals or stale virtual-thread pinning behavior. +- Run tests without skip flags and verify Maven lifecycle bindings. +- Declare complete Gradle task inputs and configure Kotlin Maven compilation explicitly. +- Treat live-process diagnostics and heap dumps according to operational impact. +- Secure JMX and diagnostic artifacts. +- Tune from JFR/GC/native-memory evidence, not generic flag recipes. +- Version-gate jlink syntax and build jpackage artifacts on each target platform. diff --git a/packages/dotfiles/dot_agents/skills/jvm-helper/references/build-and-test.md b/packages/dotfiles/dot_agents/skills/jvm-helper/references/build-and-test.md new file mode 100644 index 0000000000..78efd5ed7a --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/jvm-helper/references/build-and-test.md @@ -0,0 +1,85 @@ +# JVM build and test + +Read this when configuring Gradle, Maven, JUnit, Kotlin compilation, build caching, integration tests, or dependency versions. + +## Wrappers and toolchains + +Use `./gradlew` and `./mvnw`. Gradle 9.6.1 runs on JVM 17–26 and can provision compilation/test toolchains. Kotlin validates toolchain and JVM-target alignment. + +Kotlin DSL became the default generated DSL for `gradle init` in Gradle 8.2, not 8.0. + +## Gradle task correctness + +Cacheable tasks need complete declared inputs and outputs: + +```kotlin +val generateVersion by tasks.registering { + val versionText = providers.provider { project.version.toString() } + inputs.property("version", versionText) + val output = layout.buildDirectory.file("generated/version.txt") + outputs.file(output) + doLast { + val target = output.get().asFile.toPath() + java.nio.file.Files.createDirectories(target.parent) + java.nio.file.Files.writeString(target, versionText.get()) + } +} +``` + +A typed cacheable task is preferable for reusable production logic. + +## Maven compilation + +Prefer `maven.compiler.release`. If a project has Kotlin sources, configure `kotlin-maven-plugin` compile/test-compile executions and order Java compilation correctly. A version property and stdlib dependency do not compile Kotlin. + +## JUnit 6 + +JUnit 6.1.2 requires Java 17 and is a major upgrade. Use the Jupiter/Platform BOM or build-tool support, exact assertions, parameterized tests/classes, and opt-in parallel execution only with isolated state. + +Gradle: + +```kotlin +tasks.test { + useJUnitPlatform() +} +``` + +## Maven test lifecycle + +Surefire normally runs unit tests in `test`. Failsafe runs integration tests only when bound to both `integration-test` and `verify`. Do not claim `mvn verify` runs integration tests without that binding. + +Do not normalize skip flags. An artifact assembled without tests is not verified. + +## Versions + +Avoid embedding current library/plugin versions in evergreen patterns. Use a version catalog, BOM, or clearly dated snapshot. Major lines such as Spring Boot 4 and JUnit 6 need migration review. + +## Primary documentation + +- [Kotlin Gradle project configuration](https://kotlinlang.org/docs/gradle-configure-project.html) +- [Kotlin JUnit tests](https://kotlinlang.org/docs/jvm-test-using-junit.html) +- [kotlinx.coroutines API](https://kotlinlang.org/api/kotlinx.coroutines/) +- [kotlinx.coroutines 1.11.0](https://github.com/Kotlin/kotlinx.coroutines/releases/tag/1.11.0) +- [Gradle release notes](https://docs.gradle.org/current/release-notes.html) +- [Gradle compatibility](https://docs.gradle.org/current/userguide/compatibility.html) +- [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) +- [Gradle toolchains](https://docs.gradle.org/current/userguide/toolchains.html) +- [Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html) +- [Gradle 8.2](https://docs.gradle.org/8.2/release-notes.html) +- [Java testing](https://docs.gradle.org/current/userguide/java_testing.html) +- [JVM test suites](https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html) +- [Build cache](https://docs.gradle.org/current/userguide/build_cache.html) +- [Gradle performance](https://docs.gradle.org/current/userguide/performance.html) +- [Gradle 9 upgrade](https://docs.gradle.org/current/userguide/upgrading_version_9.html) +- [Maven downloads](https://maven.apache.org/download.cgi) +- [Maven lifecycle](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) +- [Maven Wrapper](https://maven.apache.org/wrapper/) +- [Compiler release](https://maven.apache.org/plugins/maven-compiler-plugin/examples/set-compiler-release.html) +- [Surefire skip behavior](https://maven.apache.org/surefire/maven-surefire-plugin/examples/skipping-tests.html) +- [Failsafe usage](https://maven.apache.org/surefire/maven-failsafe-plugin/usage.html) +- [JUnit Platform with Surefire](https://maven.apache.org/surefire/maven-surefire-plugin/examples/junit-platform.html) +- [JUnit user guide](https://docs.junit.org/current/user-guide/) +- [JUnit assertions](https://docs.junit.org/6.1.2/writing-tests/assertions.html) +- [JUnit parallel execution](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) +- [Parameterized tests](https://docs.junit.org/6.1.2/writing-tests/parameterized-classes-and-tests.html) +- [JUnit build support](https://docs.junit.org/6.1.2/running-tests/build-support.html) diff --git a/packages/dotfiles/dot_agents/skills/jvm-helper/references/build-tools.md b/packages/dotfiles/dot_agents/skills/jvm-helper/references/build-tools.md deleted file mode 100644 index c6448c0e31..0000000000 --- a/packages/dotfiles/dot_agents/skills/jvm-helper/references/build-tools.md +++ /dev/null @@ -1,601 +0,0 @@ -# JVM Build Tools and Packaging - -Gradle (Kotlin DSL), Maven, GraalVM native-image, jlink, jpackage, and JVM tuning. - -## Gradle (Kotlin DSL) - -Kotlin DSL is the default for new Gradle builds since Gradle 8.0. Files use `.gradle.kts` extension. - -### Basic Project Structure - -``` -project/ - build.gradle.kts # Build script - settings.gradle.kts # Project settings - gradle.properties # Build properties - gradle/ - libs.versions.toml # Version catalog - wrapper/ - gradle-wrapper.properties - src/ - main/ - java/ - kotlin/ - resources/ - test/ - java/ - kotlin/ - resources/ -``` - -### settings.gradle.kts - -```kotlin -rootProject.name = "my-project" - -// Multi-module -include("app", "core", "api") - -// Plugin management -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -// Dependency resolution -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - mavenCentral() - } -} -``` - -### build.gradle.kts - -```kotlin -plugins { - kotlin("jvm") version "2.1.0" - application -} - -group = "com.example" -version = "1.0.0" - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) - } -} - -dependencies { - implementation(libs.kotlinx.coroutines) - implementation(libs.ktor.client.core) - testImplementation(kotlin("test")) - testImplementation(libs.junit.jupiter) -} - -tasks.test { - useJUnitPlatform() -} - -application { - mainClass.set("com.example.MainKt") -} -``` - -### Version Catalogs (libs.versions.toml) - -Centralize dependency versions in `gradle/libs.versions.toml`: - -```toml -[versions] -kotlin = "2.1.0" -coroutines = "1.9.0" -ktor = "3.0.0" -junit = "5.11.0" -spring-boot = "3.4.0" - -[libraries] -kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } -ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } -junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } - -[bundles] -ktor-client = ["ktor-client-core", "ktor-client-cio"] - -[plugins] -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } -``` - -Reference in build scripts: - -```kotlin -plugins { - alias(libs.plugins.kotlin.jvm) -} - -dependencies { - implementation(libs.kotlinx.coroutines) - implementation(libs.bundles.ktor.client) - testImplementation(libs.junit.jupiter) -} -``` - -### Common Tasks - -```kotlin -// Custom task -tasks.register("generateVersion") { - val outputFile = layout.buildDirectory.file("version.txt") - outputs.file(outputFile) - doLast { - outputFile.get().asFile.writeText(project.version.toString()) - } -} - -// Task dependencies -tasks.named("processResources") { - dependsOn("generateVersion") -} - -// Configure existing task -tasks.withType().configureEach { - options.encoding = "UTF-8" - options.compilerArgs.add("-Xlint:all") -} - -tasks.withType().configureEach { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) - freeCompilerArgs.add("-Xjsr305=strict") - } -} - -// Fat JAR / Shadow JAR -plugins { - id("com.gradleup.shadow") version "8.3.0" -} -tasks.shadowJar { - archiveClassifier.set("") - manifest { - attributes("Main-Class" to "com.example.MainKt") - } -} -``` - -### Multi-Module Projects - -```kotlin -// root build.gradle.kts -plugins { - kotlin("jvm") version "2.1.0" apply false -} - -subprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") - - repositories { mavenCentral() } - - dependencies { - "testImplementation"(kotlin("test")) - } -} - -// app/build.gradle.kts -plugins { - application -} - -dependencies { - implementation(project(":core")) - implementation(project(":api")) -} -``` - -### Useful Gradle Commands - -```bash -# Build -./gradlew build # full build -./gradlew build -x test # skip tests -./gradlew clean build # clean first -./gradlew assemble # compile + package, no tests - -# Testing -./gradlew test # run all tests -./gradlew test --tests "*.UserTest" # run specific test class -./gradlew test --tests "*UserTest.testCreate" # specific method -./gradlew test --rerun # force re-run -./gradlew test --fail-fast # stop on first failure - -# Dependencies -./gradlew dependencies # full dependency tree -./gradlew dependencies --configuration runtimeClasspath -./gradlew dependencyInsight --dependency guava -./gradlew dependencyUpdates # check for updates (needs plugin) - -# Info -./gradlew tasks # list available tasks -./gradlew tasks --all # all tasks including hidden -./gradlew projects # list sub-projects -./gradlew properties # all project properties - -# Performance -./gradlew build --build-cache # use build cache -./gradlew build --parallel # parallel module builds -./gradlew build --scan # generate build scan -``` - -## Maven - -### pom.xml Structure - -```xml - - - 4.0.0 - - com.example - my-app - 1.0.0 - jar - - - 21 - 2.1.0 - ${java.version} - ${java.version} - UTF-8 - - - - - - - org.springframework.boot - spring-boot-dependencies - 3.4.0 - pom - import - - - - - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - org.junit.jupiter - junit-jupiter - 5.11.0 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - ${java.version} - - - - - -``` - -### Maven Profiles - -```xml - - - - dev - - true - - - dev - - - - - - prod - - prod - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - com.example.Main - - - - - - - - -``` - -### Common Maven Commands - -```bash -# Lifecycle phases -mvn clean # delete target/ -mvn compile # compile main sources -mvn test # compile + run tests -mvn package # compile + test + package JAR/WAR -mvn verify # run integration tests -mvn install # install to local repo (~/.m2) -mvn deploy # deploy to remote repo - -# Skip tests -mvn package -DskipTests # skip test execution -mvn package -Dmaven.test.skip=true # skip compile + execution - -# Profiles -mvn package -Pprod # activate prod profile -mvn package -P!dev # deactivate dev profile - -# Dependencies -mvn dependency:tree # show dependency tree -mvn dependency:resolve # resolve and download -mvn dependency:analyze # find unused/undeclared deps -mvn versions:display-dependency-updates # check for updates - -# Info -mvn help:effective-pom # resolved POM with inheritance -mvn help:active-profiles # show active profiles -mvn help:describe -Dplugin=compiler # plugin documentation - -# Multi-module -mvn -pl module-name package # build specific module -mvn -pl module-name -am package # also build dependencies -mvn -rf :module-name package # resume from module -``` - -## GraalVM Native Image - -Compile Java applications ahead-of-time into standalone native executables. - -### Basic Usage - -```bash -# Install GraalVM (via mise or SDKMAN) -mise install java graalvm-21 - -# Compile to native executable -native-image -jar app.jar myapp - -# With no fallback (fail at build time if reflection not configured) -native-image --no-fallback -jar app.jar - -# From class path -native-image -cp app.jar com.example.Main -o myapp - -# Optimized build -native-image -O3 -jar app.jar myapp - -# With monitoring/debugging -native-image --enable-monitoring=jfr,heapdump -jar app.jar -``` - -### Reflection and Resource Configuration - -Native image requires configuration for reflection, resources, and proxies: - -```bash -# Generate config by running the app with tracing agent -java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \ - -jar app.jar - -# Generated files: -# reflect-config.json - reflection metadata -# resource-config.json - resource files to include -# proxy-config.json - dynamic proxy classes -# serialization-config.json -# jni-config.json -``` - -### Framework Support - -```bash -# Spring Boot (with spring-boot-maven-plugin or Gradle plugin) -./gradlew nativeCompile # Gradle -mvn -Pnative native:compile # Maven - -# Quarkus -./gradlew build -Dquarkus.native.enabled=true -mvn package -Dnative - -# Micronaut -./gradlew nativeCompile -``` - -## jlink - Custom Runtime Images - -Create minimal JRE containing only required modules: - -```bash -# List modules your application needs -jdeps --multi-release 21 --ignore-missing-deps --print-module-deps app.jar - -# Create custom runtime with only needed modules -jlink --module-path $JAVA_HOME/jmods \ - --add-modules java.base,java.sql,java.net.http \ - --output custom-jre \ - --strip-debug \ - --compress zip-6 \ - --no-header-files \ - --no-man-pages - -# Result: minimal JRE in custom-jre/ (can be ~30MB instead of ~300MB) - -# Run with custom runtime -custom-jre/bin/java -jar app.jar -``` - -## jpackage - Native Installers - -Create platform-specific installers (DMG, MSI, DEB, RPM): - -```bash -# macOS DMG -jpackage --input lib/ --main-jar app.jar \ - --main-class com.example.Main \ - --name MyApp \ - --app-version 1.0.0 \ - --type dmg \ - --icon icon.icns \ - --java-options "-Xmx512m" - -# With custom runtime (smaller package) -jpackage --input lib/ --main-jar app.jar \ - --main-class com.example.Main \ - --name MyApp \ - --type dmg \ - --runtime-image custom-jre/ - -# Linux DEB -jpackage --input lib/ --main-jar app.jar \ - --main-class com.example.Main \ - --name myapp \ - --type deb \ - --linux-shortcut \ - --linux-deb-maintainer "dev@example.com" -``` - -## JVM Tuning Flags - -### Memory Settings - -```bash -# Heap size --Xms512m # initial heap size --Xmx4g # maximum heap size --Xss512k # thread stack size - -# Metaspace (replaces PermGen since Java 8) --XX:MetaspaceSize=128m --XX:MaxMetaspaceSize=256m - -# Direct memory --XX:MaxDirectMemorySize=512m -``` - -### Garbage Collection - -```bash -# G1GC (default since Java 9, recommended for most workloads) --XX:+UseG1GC --XX:MaxGCPauseMillis=200 # target pause time --XX:G1HeapRegionSize=4m # region size (1-32MB, power of 2) --XX:InitiatingHeapOccupancyPercent=45 - -# ZGC (ultra-low latency, sub-millisecond pauses) --XX:+UseZGC --XX:+ZGenerational # generational ZGC (Java 21+, default in 24+) - -# Shenandoah (low latency, available in OpenJDK) --XX:+UseShenandoahGC - -# Serial GC (small heaps, single-threaded) --XX:+UseSerialGC - -# Parallel GC (throughput-oriented) --XX:+UseParallelGC -``` - -### Diagnostics - -```bash -# GC logging --Xlog:gc*:file=gc.log:time,level,tags - -# Heap dump on OOM --XX:+HeapDumpOnOutOfMemoryError --XX:HeapDumpPath=/tmp/heapdump.hprof - -# JMX remote monitoring --Dcom.sun.management.jmxremote --Dcom.sun.management.jmxremote.port=9090 --Dcom.sun.management.jmxremote.authenticate=false --Dcom.sun.management.jmxremote.ssl=false - -# Flight Recorder --XX:StartFlightRecording=duration=60s,filename=recording.jfr - -# Print compilation --XX:+PrintCompilation - -# Native memory tracking --XX:NativeMemoryTracking=summary -``` - -### Performance Tuning - -```bash -# Tiered compilation (default, fastest startup + peak performance) --XX:+TieredCompilation - -# Disable tiered for faster startup (less peak performance) --XX:-TieredCompilation -XX:+UseCompressedOops - -# Compiler threads --XX:CICompilerCount=4 - -# String deduplication (with G1) --XX:+UseStringDeduplication - -# Compact object headers (Java 25+, saves ~10% heap) --XX:+UseCompactObjectHeaders - -# Container awareness (default in modern JDK) --XX:+UseContainerSupport --XX:MaxRAMPercentage=75.0 # use 75% of container memory limit -``` - -### Recommended Defaults for Containers - -```bash -java \ - -XX:MaxRAMPercentage=75.0 \ - -XX:+UseG1GC \ - -XX:MaxGCPauseMillis=200 \ - -XX:+HeapDumpOnOutOfMemoryError \ - -XX:HeapDumpPath=/tmp/heapdump.hprof \ - -Xlog:gc*:file=/var/log/gc.log:time,level,tags:filecount=5,filesize=10m \ - -jar app.jar -``` - -### For Low-Latency Applications - -```bash -java \ - -XX:+UseZGC \ - -XX:+ZGenerational \ - -Xmx4g \ - -XX:+HeapDumpOnOutOfMemoryError \ - -jar app.jar -``` diff --git a/packages/dotfiles/dot_agents/skills/jvm-helper/references/diagnostics-and-packaging.md b/packages/dotfiles/dot_agents/skills/jvm-helper/references/diagnostics-and-packaging.md new file mode 100644 index 0000000000..e537355cf6 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/jvm-helper/references/diagnostics-and-packaging.md @@ -0,0 +1,52 @@ +# JVM diagnostics and packaging + +Read this when attaching to a JVM, collecting heap or flight recordings, configuring JMX, tuning memory/GC, using jlink/jpackage, or building Native Image. + +## Attach impact + +`jcmd` operations document impact levels and generally require the same user/machine. `jmap` is experimental and unsupported. Diagnose in a staging or approved production window according to the exact operation's impact. + +## Heap and JMX security + +Heap dumps commonly contain tokens, credentials, personal data, and application payloads. Use a private path with access control and available capacity; define retention and deletion. + +Remote JMX provides control and inspection. Never expose it without authentication and TLS. Prefer local attach or an SSH-protected channel when practical. + +## Measurement-first tuning + +Collect JFR, GC logs, native-memory tracking, thread state, allocation, and system/container evidence. Change one supported flag, load test, and keep only a measured improvement. + +Compact object headers are default-enabled in JDK 25; memory savings are workload-dependent. Current ZGC is generational by default, and `-XX:+ZGenerational` is obsolete. + +## jlink + +JDK 25 uses numeric compression levels such as `--compress=2`. JDK 26 adds `zip-0` through `zip-9`. Measure runtime-image size for the selected modules, platform, compression, and debug-symbol policy. + +## jpackage + +Each native package format must be created on its target platform. There is no cross-platform package build. Test signing, runtime image, upgrade, uninstall, and user-data behavior on the target OS. + +## GraalVM Native Image + +Native Image performs closed-world analysis. Dynamic reflection, resources, proxies, serialization, and JNI need reachable metadata or code-computed configuration. + +Tracing-agent output observes only executed paths. Exercise representative workloads, merge and review metadata, and prefer framework plugins or the reachability metadata repository when available. Do not use `--no-fallback` as a current canonical requirement. + +## Primary documentation + +- [Java support roadmap](https://www.oracle.com/java/technologies/java-se-support-roadmap.html) +- [JDK 25 jlink](https://docs.oracle.com/en/java/javase/25/docs/specs/man/jlink.html) +- [JDK 26 jlink](https://docs.oracle.com/en/java/javase/26/docs/specs/man/jlink.html) +- [jpackage](https://docs.oracle.com/en/java/javase/25/docs/specs/man/jpackage.html) +- [jcmd](https://docs.oracle.com/en/java/javase/25/docs/specs/man/jcmd.html) +- [jmap](https://docs.oracle.com/en/java/javase/25/docs/specs/man/jmap.html) +- [JMX agent security](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html) +- [Native Image](https://www.graalvm.org/latest/reference-manual/native-image/) +- [Tracing agent](https://www.graalvm.org/latest/reference-manual/native-image/guides/configure-with-tracing-agent/) +- [Native Image compatibility](https://www.graalvm.org/latest/reference-manual/native-image/metadata/Compatibility/) +- [Native Image memory management](https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/MemoryManagement/) +- [Ktor releases](https://ktor.io/docs/releases.html) +- [Ktor 3.5.1](https://github.com/ktorio/ktor/releases/tag/3.5.1) +- [Spring Boot v4.1.0](https://github.com/spring-projects/spring-boot/releases/tag/v4.1.0) +- [Shadow 9.6.1](https://github.com/GradleUp/shadow/releases/tag/9.6.1) +- [JUnit r6.1.2](https://github.com/junit-team/junit-framework/releases/tag/r6.1.2) diff --git a/packages/dotfiles/dot_agents/skills/jvm-helper/references/java-and-kotlin.md b/packages/dotfiles/dot_agents/skills/jvm-helper/references/java-and-kotlin.md new file mode 100644 index 0000000000..2812d00e91 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/jvm-helper/references/java-and-kotlin.md @@ -0,0 +1,82 @@ +# Modern Java and Kotlin + +Read this when using Java previews, virtual threads, structured concurrency, scoped values, Kotlin coroutines, or Java/Kotlin nullability. + +## Java release model + +Java SE does not define LTS. Vendors define support lifecycles; Oracle designates Java 25 as LTS. JDK 26 is the current feature release. + +## Virtual threads + +Use virtual-thread-per-task execution for high-concurrency blocking I/O: + +```java +try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + var future = executor.submit(this::load); + return future.get(); +} +``` + +They do not make CPU work faster. JEP 491 in JDK 24 removed nearly all synchronized pinning, so diagnose rather than replacing locks reflexively. + +## Structured concurrency + +Structured Concurrency is still preview and has changed shape several times. JDK 25's fifth preview uses `StructuredTaskScope.open()` and `Joiner` policies. JDK 26 contains a sixth preview; always use the target JDK's docs and preview flags. + +## Scoped values + +Scoped Values finalized in JDK 25. Bind with `ScopedValue.where(key, value).run(...)` or `.call(...)`. Old preview `runWhere` code no longer compiles. + +## Other current Java APIs + +- Foreign Function and Memory finalized in JDK 22. +- Module import declarations, compact source files, and flexible constructor bodies finalized in JDK 25. +- Compact object headers are default-enabled in JDK 25 with workload-dependent memory effects. +- Non-generational ZGC and `ZGenerational` were removed/obsoleted; use current `UseZGC` behavior. + +## Kotlin language + +Kotlin 2.4 supports Java 26 and stabilizes context parameters. Guard conditions stabilized in Kotlin 2.2. K2 is the normal compiler, not an experimental property. + +## Coroutines + +Structured builders own child lifecycles. Preserve `CancellationException`, close custom executor dispatchers, and understand `Dispatchers.IO` elasticity before adding `limitedParallelism` views. + +## Nullability + +Kotlin platform types come from Java declarations without sufficient nullability information. Choose an annotation ecosystem deliberately. JSpecify provides modern nullness semantics for shared Java APIs; JetBrains and Eclipse annotations remain valid when explicitly configured. + +## Primary documentation + +- [JDK 26](https://openjdk.org/projects/jdk/26/) +- [JDK 25](https://openjdk.org/projects/jdk/25/) +- [JDK 24](https://openjdk.org/projects/jdk/24/) +- [JEP 491](https://openjdk.org/jeps/491) +- [JEP 505](https://openjdk.org/jeps/505) +- [JEP 506](https://openjdk.org/jeps/506) +- [JEP 511](https://openjdk.org/jeps/511) +- [JEP 512](https://openjdk.org/jeps/512) +- [JEP 513](https://openjdk.org/jeps/513) +- [JEP 519](https://openjdk.org/jeps/519) +- [JEP 521](https://openjdk.org/jeps/521) +- [JEP 502](https://openjdk.org/jeps/502) +- [JEP 454](https://openjdk.org/jeps/454) +- [JEP 444](https://openjdk.org/jeps/444) +- [JEP 474](https://openjdk.org/jeps/474) +- [JEP 490](https://openjdk.org/jeps/490) +- [StructuredTaskScope](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html) +- [ScopedValue](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ScopedValue.html) +- [Executors](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/Executors.html) +- [HttpClient](https://docs.oracle.com/en/java/javase/25/docs/api/java.net.http/java/net/http/HttpClient.html) +- [Kotlin releases](https://kotlinlang.org/docs/releases.html) +- [Kotlin 2.2](https://kotlinlang.org/docs/whatsnew22.html) +- [Kotlin 2.3](https://kotlinlang.org/docs/whatsnew23.html) +- [Kotlin 2.4](https://kotlinlang.org/docs/whatsnew24.html) +- [K2 migration](https://kotlinlang.org/docs/k2-compiler-migration-guide.html) +- [Coroutine basics](https://kotlinlang.org/docs/coroutines-basics.html) +- [Coroutine contexts and dispatchers](https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html) +- [Cancellation and timeouts](https://kotlinlang.org/docs/cancellation-and-timeouts.html) +- [Coroutine exception handling](https://kotlinlang.org/docs/exception-handling.html) +- [Java-to-Kotlin nullability](https://kotlinlang.org/docs/java-to-kotlin-nullability-guide.html) +- [Java interoperability](https://kotlinlang.org/docs/java-interop.html) +- [Dispatchers.IO](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-i-o.html) diff --git a/packages/dotfiles/dot_agents/skills/jvm-helper/references/kotlin-patterns.md b/packages/dotfiles/dot_agents/skills/jvm-helper/references/kotlin-patterns.md deleted file mode 100644 index 165664f322..0000000000 --- a/packages/dotfiles/dot_agents/skills/jvm-helper/references/kotlin-patterns.md +++ /dev/null @@ -1,560 +0,0 @@ -# Kotlin Patterns and Idioms - -Comprehensive guide to Kotlin language features, patterns, and modern development with the K2 compiler and Kotlin Multiplatform. - -## Null Safety - -Kotlin's type system distinguishes nullable and non-nullable types at compile time, eliminating most NullPointerExceptions. - -### Nullable Types - -```kotlin -// Non-nullable - cannot hold null -var name: String = "Alice" -// name = null // compile error - -// Nullable - can hold null -var nullable: String? = null -nullable = "hello" -// nullable.length // compile error - must handle null -``` - -### Safe Operators - -```kotlin -// Safe call operator ?. -val length: Int? = nullable?.length // null if nullable is null - -// Chained safe calls -val city: String? = user?.address?.city - -// Safe call with let for non-null execution -nullable?.let { value -> - println("Length: ${value.length}") -} - -// Elvis operator ?: (default value) -val len: Int = nullable?.length ?: 0 -val name: String = input ?: throw IllegalArgumentException("name required") -val result: String = nullable ?: return // early return - -// Not-null assertion !! (throws NPE if null - avoid when possible) -val forced: Int = nullable!!.length - -// Safe cast -val str: String? = value as? String // null if cast fails (instead of ClassCastException) -``` - -### Smart Casts - -The compiler tracks null checks and casts automatically: - -```kotlin -fun process(value: String?) { - if (value == null) return - // Compiler knows value is String (non-null) here - println(value.length) -} - -fun handleResult(result: Any) { - when (result) { - is String -> println(result.length) // smart cast to String - is Int -> println(result + 1) // smart cast to Int - is List<*> -> println(result.size) // smart cast to List - } -} - -// Improved smart casts in Kotlin 2.0 (K2 compiler) -// K2 can smart cast in more scenarios, including: -// - Variables captured in lambdas -// - Inline function calls -// - Property-based smart casts after checks in when/if -``` - -### Platform Types - -When calling Java code, Kotlin infers platform types (noted as `T!`) which can be treated as nullable or non-null. Always annotate nullability when writing Java code consumed by Kotlin, using `@Nullable` and `@NotNull`. - -## Coroutines - -Kotlin coroutines enable asynchronous, non-blocking code that reads like sequential code. - -### Suspend Functions - -```kotlin -// suspend marks a function that can be paused and resumed -suspend fun fetchUser(id: Int): User { - val response = httpClient.get("https://api.example.com/users/$id") - return response.body() -} - -// Suspend functions can only be called from other suspend functions or coroutine builders -``` - -### Coroutine Builders - -```kotlin -// launch - fire and forget, returns Job -val job: Job = scope.launch { - val user = fetchUser(1) - updateUI(user) -} -job.cancel() // cancel if needed -job.join() // wait for completion - -// async - returns Deferred with a result -val deferred: Deferred = scope.async { - fetchUser(1) -} -val user: User = deferred.await() - -// runBlocking - bridges blocking and suspend worlds (for main/tests) -fun main() = runBlocking { - val user = fetchUser(1) - println(user) -} - -// coroutineScope - creates a scope, suspends until all children complete -suspend fun loadData() = coroutineScope { - val users = async { fetchUsers() } - val posts = async { fetchPosts() } - Pair(users.await(), posts.await()) -} -``` - -### Structured Concurrency - -Coroutines follow a parent-child hierarchy. If a parent is cancelled, all children are cancelled. If a child fails, the parent and siblings are cancelled (unless using `supervisorScope`). - -```kotlin -// Regular scope - child failure cancels parent and siblings -suspend fun riskyOperation() = coroutineScope { - launch { task1() } // cancelled if task2 throws - launch { task2() } // cancelled if task1 throws -} - -// supervisorScope - child failure does NOT cancel siblings -suspend fun independentTasks() = supervisorScope { - launch { task1() } // continues even if task2 throws - launch { task2() } // continues even if task1 throws -} - -// Job hierarchy -val parentJob = scope.launch { - val childJob = launch { - delay(1000) - println("child") - } -} -parentJob.cancel() // cancels childJob too -``` - -### Dispatchers - -```kotlin -// Dispatchers.Default - CPU-intensive work (shared thread pool, size = num cores) -withContext(Dispatchers.Default) { - heavyComputation() -} - -// Dispatchers.IO - blocking I/O (expandable thread pool, up to 64 threads) -withContext(Dispatchers.IO) { - readFile() - databaseQuery() -} - -// Dispatchers.Main - UI thread (Android, JavaFX, Swing with coroutine extensions) -withContext(Dispatchers.Main) { - updateUI() -} - -// Dispatchers.Unconfined - starts in caller thread, resumes in whatever thread -// Use sparingly, mainly for testing - -// Custom dispatcher from executor -val dispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() -``` - -### Flow - -Cold asynchronous stream that emits values sequentially: - -```kotlin -// Creating flows -fun numbers(): Flow = flow { - for (i in 1..5) { - delay(100) - emit(i) - } -} - -// Collecting flows -numbers().collect { value -> - println(value) -} - -// Flow operators -numbers() - .filter { it % 2 == 0 } - .map { it * 10 } - .take(3) - .collect { println(it) } - -// flowOf and asFlow -val flow1 = flowOf(1, 2, 3) -val flow2 = listOf("a", "b", "c").asFlow() - -// Combining flows -val combined = flow1.zip(flow2) { num, str -> "$num-$str" } -// Emits: "1-a", "2-b", "3-c" - -// flatMapConcat, flatMapMerge, flatMapLatest -flow1.flatMapConcat { id -> fetchDetails(id) } - -// StateFlow - hot flow with current value (like LiveData) -private val _state = MutableStateFlow(UiState.Loading) -val state: StateFlow = _state.asStateFlow() - -// SharedFlow - hot flow for events -private val _events = MutableSharedFlow() -val events: SharedFlow = _events.asSharedFlow() - -// Convert cold flow to shared -val shared = coldFlow - .shareIn(scope, SharingStarted.WhileSubscribed(5000), replay = 1) -``` - -### Exception Handling in Coroutines - -```kotlin -// try-catch in coroutine -launch { - try { - riskyOperation() - } catch (e: Exception) { - handleError(e) - } -} - -// CoroutineExceptionHandler (last resort, launch only, not async) -val handler = CoroutineExceptionHandler { _, exception -> - log("Caught: $exception") -} -scope.launch(handler) { riskyOperation() } - -// async exceptions are thrown at await() -val deferred = async { riskyOperation() } -try { - deferred.await() -} catch (e: Exception) { - handleError(e) -} -``` - -## Data Classes - -```kotlin -data class User( - val id: Long, - val name: String, - val email: String, - val role: Role = Role.USER // default value -) - -// Auto-generated: equals, hashCode, toString, copy, componentN -val user = User(1, "Alice", "alice@example.com") -val admin = user.copy(role = Role.ADMIN) - -// Destructuring -val (id, name, email) = user -println("$name: $email") - -// In collections -val users = listOf(user, admin) -users.sortedBy { it.name } -users.associateBy { it.id } // Map -``` - -### Data Class vs Record - -| Feature | Kotlin data class | Java record | -| -------------- | -------------------------------------------- | ----------------------------- | -| Mutability | Can use `var` (mutable) or `val` (immutable) | Always immutable | -| Inheritance | Can inherit from classes/interfaces | Can implement interfaces only | -| `copy()` | Generated automatically | Not available | -| Default values | Supported | Not supported | -| componentN | Generated | Not available | - -## Sealed Classes and Interfaces - -```kotlin -// Sealed class - all subtypes known at compile time -sealed class NetworkResult { - data class Success(val data: T) : NetworkResult() - data class Error(val code: Int, val message: String) : NetworkResult() - data object Loading : NetworkResult() -} - -// Exhaustive when - compiler enforces all cases -fun handle(result: NetworkResult) = when (result) { - is NetworkResult.Success -> showData(result.data) - is NetworkResult.Error -> showError(result.message) - NetworkResult.Loading -> showSpinner() - // No else needed - all cases covered -} - -// Sealed interface (can implement multiple) -sealed interface UiEvent { - data class Click(val x: Int, val y: Int) : UiEvent - data class KeyPress(val key: Char) : UiEvent - data object BackPressed : UiEvent -} - -// Nesting sealed hierarchies -sealed interface Animal { - sealed class Dog : Animal { - data object Labrador : Dog() - data object Poodle : Dog() - } - sealed class Cat : Animal { - data object Siamese : Cat() - data object Persian : Cat() - } -} -``` - -### Guard Conditions in when (Kotlin 2.1) - -```kotlin -sealed interface Command { - data class Move(val dx: Int, val dy: Int) : Command - data class Print(val message: String) : Command -} - -fun execute(command: Command) = when (command) { - is Command.Move if command.dx == 0 && command.dy == 0 -> "no-op" - is Command.Move -> "move by (${command.dx}, ${command.dy})" - is Command.Print -> "print: ${command.message}" -} -``` - -## Extension Functions - -```kotlin -// Add methods to existing types without inheritance -fun String.removeWhitespace(): String = this.replace("\\s".toRegex(), "") -"hello world".removeWhitespace() // "helloworld" - -// Extension properties -val String.wordCount: Int - get() = this.split("\\s+".toRegex()).size -"one two three".wordCount // 3 - -// Generic extensions -fun List.secondOrNull(): T? = if (size >= 2) this[1] else null - -// Extension on nullable types -fun String?.orEmpty(): String = this ?: "" - -// Extensions are resolved statically (at compile time, not runtime) -// If a member function exists with same signature, member wins -``` - -### Scope Functions Summary - -| Function | Context object | Return value | Use case | -| -------- | -------------- | -------------- | ----------------------- | -| `let` | `it` | Lambda result | Null-safe transforms | -| `run` | `this` | Lambda result | Object config + compute | -| `with` | `this` | Lambda result | Grouping calls | -| `apply` | `this` | Context object | Object configuration | -| `also` | `it` | Context object | Side effects, logging | - -```kotlin -// Chaining scope functions -val result = fetchData() - .also { log("Fetched: $it") } - .let { transform(it) } - .also { log("Transformed: $it") } -``` - -## K2 Compiler - -The K2 compiler (stable in Kotlin 2.0) is a complete rewrite of the Kotlin compiler frontend. - -### Key Improvements - -- **2x faster compilation** on average (initialization up to 488% faster, analysis up to 376% faster) -- **Unified architecture** for all backends (JVM, JS, Wasm, Native) -- **Improved smart casts** in more scenarios (closures, inline functions, properties) -- **Better type inference** reduces need for explicit type annotations -- **Foundation for future features** like context receivers, name-based destructuring - -### Migration - -```kotlin -// build.gradle.kts - K2 is default in Kotlin 2.0+ -plugins { - kotlin("jvm") version "2.1.0" -} - -// If needed, explicitly set language version -kotlin { - compilerOptions { - languageVersion.set(KotlinVersion.KOTLIN_2_0) - } -} -``` - -### Checking Compatibility - -```bash -# Build with K2 and check for issues -./gradlew build -Pkotlin.experimental.tryK2=true - -# In Kotlin 2.0+, K2 is the default - no flag needed -``` - -## Kotlin Multiplatform (KMP) - -Share code across JVM, JS, Wasm, iOS, Android, desktop, and server. - -### Project Structure - -``` -project/ - src/ - commonMain/ # Shared code - expect declarations - commonTest/ # Shared tests - jvmMain/ # JVM-specific - actual declarations - iosMain/ # iOS-specific - jsMain/ # JavaScript-specific -``` - -### Declaring Targets - -```kotlin -// build.gradle.kts -plugins { - kotlin("multiplatform") version "2.1.0" -} - -kotlin { - jvm() - iosArm64() - iosSimulatorArm64() - js(IR) { browser() } - - sourceSets { - commonMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") - implementation("io.ktor:ktor-client-core:3.0.0") - } - jvmMain.dependencies { - implementation("io.ktor:ktor-client-cio:3.0.0") - } - } -} -``` - -### Expect/Actual - -```kotlin -// commonMain - declare expected API -expect fun platformName(): String -expect class PlatformLogger() { - fun log(message: String) -} - -// jvmMain - provide actual implementation -actual fun platformName(): String = "JVM" -actual class PlatformLogger actual constructor() { - actual fun log(message: String) = println("[JVM] $message") -} - -// iosMain -actual fun platformName(): String = "iOS" -actual class PlatformLogger actual constructor() { - actual fun log(message: String) = NSLog("[iOS] $message") -} -``` - -## Useful Kotlin Idioms - -### Collection Operations - -```kotlin -// Filter and transform -val adults = users.filter { it.age >= 18 }.map { it.name } - -// Grouping -val byCity = users.groupBy { it.city } - -// Associate -val byId: Map = users.associateBy { it.id } - -// Partition -val (minors, adults) = users.partition { it.age < 18 } - -// Null-safe collection operations -val names = users.mapNotNull { it.nickname } // skip nulls - -// Sequences for lazy processing (large collections) -users.asSequence() - .filter { it.isActive } - .map { it.name } - .take(10) - .toList() -``` - -### Delegation - -```kotlin -// Class delegation -interface Repository { fun find(id: Int): Item? } -class CachingRepository(private val delegate: Repository) : Repository by delegate { - override fun find(id: Int): Item? { - return cache.get(id) ?: delegate.find(id)?.also { cache.put(id, it) } - } -} - -// Property delegation -val lazyValue: String by lazy { computeExpensiveValue() } -var observed: String by Delegates.observable("initial") { _, old, new -> - println("$old -> $new") -} -val props: Map = mapOf("name" to "Alice", "age" to 30) -val name: String by props // delegates to map lookup -``` - -### Type-Safe Builders (DSL) - -```kotlin -// HTML DSL example -fun html(init: HTML.() -> Unit): HTML = HTML().apply(init) - -html { - head { title("Page") } - body { - p("Hello") - a(href = "https://example.com") { +"Click here" } - } -} -``` - -### Inline Functions and Reified Types - -```kotlin -// Inline function avoids lambda allocation overhead -inline fun measureTime(block: () -> T): Pair { - val start = System.nanoTime() - val result = block() - return result to (System.nanoTime() - start) -} - -// Reified type parameters (only in inline functions) -inline fun parseJson(json: String): T { - return objectMapper.readValue(json, T::class.java) -} -val user: User = parseJson("""{"name":"Alice"}""") -``` diff --git a/packages/dotfiles/dot_agents/skills/jvm-helper/references/modern-java.md b/packages/dotfiles/dot_agents/skills/jvm-helper/references/modern-java.md deleted file mode 100644 index d23a5c5027..0000000000 --- a/packages/dotfiles/dot_agents/skills/jvm-helper/references/modern-java.md +++ /dev/null @@ -1,572 +0,0 @@ -# Modern Java Features - -Comprehensive guide to Java 21 LTS features and preview features progressing through Java 22-25. Focus is on finalized features in Java 21 with notes on what has since been finalized in Java 22-25. - -## Records (JEP 395, finalized Java 16) - -Records are transparent, immutable data carriers. The compiler generates the constructor, accessors (named after components, not getField), `equals`, `hashCode`, and `toString`. - -### Basic Records - -```java -record Point(int x, int y) {} - -Point p = new Point(3, 4); -p.x(); // 3 (accessor, not getX) -p.y(); // 4 -p.toString(); // Point[x=3, y=4] - -// Equals based on all components -new Point(3, 4).equals(new Point(3, 4)); // true -``` - -### Compact Constructors - -The compact constructor validates/normalizes without repeating field assignments: - -```java -record Range(int lo, int hi) { - Range { // compact constructor - assignments happen implicitly after - if (lo > hi) throw new IllegalArgumentException( - "lo (%d) > hi (%d)".formatted(lo, hi)); - } -} - -// Normalizing constructor -record EmailAddress(String value) { - EmailAddress { - value = value.strip().toLowerCase(); - } -} -``` - -### Custom Constructors and Methods - -```java -record Name(String first, String last) { - // Additional constructor must delegate to canonical - Name(String full) { - this(full.split(" ")[0], full.split(" ")[1]); - } - - String fullName() { return first + " " + last; } - - // Static factory - static Name of(String first, String last) { - return new Name(first, last); - } -} -``` - -### Records with Generics and Interfaces - -```java -record Pair(A first, B second) implements Comparable> { - @Override - public int compareTo(Pair other) { /* ... */ } -} - -// Records can implement interfaces but cannot extend classes -sealed interface Shape permits Circle, Rect {} -record Circle(double radius) implements Shape {} -record Rect(double w, double h) implements Shape {} -``` - -### Limitations - -Records cannot: extend other classes (implicitly extend `java.lang.Record`), declare instance fields beyond components, be abstract. Components are implicitly `final`. Records can: implement interfaces, have static fields/methods, have instance methods, be generic, be local (declared inside methods), be annotated. - -## Sealed Classes (JEP 409, finalized Java 17) - -Sealed classes restrict which classes can extend them, enabling exhaustive pattern matching. - -```java -// Sealed interface with permitted subtypes -public sealed interface Expr - permits Literal, Add, Multiply, Negate { -} - -record Literal(double value) implements Expr {} -record Add(Expr left, Expr right) implements Expr {} -record Multiply(Expr left, Expr right) implements Expr {} -record Negate(Expr operand) implements Expr {} - -// Exhaustive computation - compiler verifies all cases -double compute(Expr expr) { - return switch (expr) { - case Literal(var v) -> v; - case Add(var l, var r) -> compute(l) + compute(r); - case Multiply(var l, var r) -> compute(l) * compute(r); - case Negate(var e) -> -compute(e); - // No default needed - all cases covered - }; -} -``` - -### Sealed Class Modifiers - -Permitted subtypes must use one of: - -- `final` - no further extension -- `sealed` - further restricted extension -- `non-sealed` - opens up for unrestricted extension - -```java -sealed class Account permits SavingsAccount, CheckingAccount, CryptoAccount {} -final class SavingsAccount extends Account {} -sealed class CheckingAccount extends Account permits PremiumChecking {} -non-sealed class CryptoAccount extends Account {} // anyone can extend -final class PremiumChecking extends CheckingAccount {} -``` - -If subtypes are in the same file, `permits` can be omitted: - -```java -sealed interface Json { - record JString(String value) implements Json {} - record JNumber(double value) implements Json {} - record JBool(boolean value) implements Json {} - record JNull() implements Json {} - record JArray(List elements) implements Json {} - record JObject(Map fields) implements Json {} -} -``` - -## Pattern Matching - -### Pattern Matching for instanceof (JEP 394, finalized Java 16) - -```java -// Before: cast after instanceof -if (obj instanceof String) { - String s = (String) obj; - System.out.println(s.length()); -} - -// After: pattern variable bound in scope -if (obj instanceof String s) { - System.out.println(s.length()); -} - -// Works with && (short-circuit) -if (obj instanceof String s && s.length() > 5) { - System.out.println(s.toUpperCase()); -} - -// Negation pattern -if (!(obj instanceof String s)) { - return; // s not in scope here -} -// s IS in scope here (definite assignment) -System.out.println(s.length()); -``` - -### Pattern Matching for switch (JEP 441, finalized Java 21) - -```java -// Type patterns in switch -String describe(Object obj) { - return switch (obj) { - case Integer i -> "int: " + i; - case Long l -> "long: " + l; - case Double d -> "double: " + d; - case String s -> "string: " + s; - case int[] arr -> "int array of length " + arr.length; - case null -> "null"; - default -> obj.getClass().getName(); - }; -} -``` - -### Guarded Patterns - -```java -// when clause adds conditions -String classify(Object obj) { - return switch (obj) { - case Integer i when i < 0 -> "negative"; - case Integer i when i == 0 -> "zero"; - case Integer i -> "positive"; - case String s when s.isBlank() -> "blank string"; - case String s -> "string: " + s; - default -> "other"; - }; -} -``` - -### Record Patterns (JEP 440, finalized Java 21) - -Destructure records directly in patterns: - -```java -record Point(int x, int y) {} -record Line(Point start, Point end) {} - -// instanceof with record pattern -if (obj instanceof Point(int x, int y)) { - System.out.println("(%d, %d)".formatted(x, y)); -} - -// Switch with record pattern -String describe(Object obj) { - return switch (obj) { - case Point(var x, var y) -> "point at %d,%d".formatted(x, y); - case Line(Point(var x1, var y1), Point(var x2, var y2)) -> - "line from (%d,%d) to (%d,%d)".formatted(x1, y1, x2, y2); - default -> "unknown"; - }; -} - -// Combining sealed types + records + patterns -sealed interface Shape permits Circle, Rect {} -record Circle(Point center, double radius) implements Shape {} -record Rect(Point topLeft, Point bottomRight) implements Shape {} - -String info(Shape shape) { - return switch (shape) { - case Circle(Point(var cx, var cy), var r) -> - "circle at (%d,%d) radius %.1f".formatted(cx, cy, r); - case Rect(Point(var x1, var y1), Point(var x2, var y2)) -> - "rect from (%d,%d) to (%d,%d)".formatted(x1, y1, x2, y2); - }; -} -``` - -### Unnamed Variables and Patterns (JEP 456, finalized Java 22) - -Use `_` when a variable or pattern component is not needed: - -```java -// Unused catch variable -try { /* ... */ } catch (NumberFormatException _) { - System.out.println("Not a number"); -} - -// Unused loop variable -int count = 0; -for (var _ : collection) { count++; } - -// Unused lambda parameter -map.forEach((_, value) -> process(value)); - -// Unnamed pattern in record destructuring -case Point(var x, _) -> "x=" + x; // ignore y component - -// Multiple unnamed in same scope (allowed, since no name conflict) -if (obj instanceof Pair(var first, _)) { - System.out.println("first: " + first); -} -``` - -### Primitive Types in Patterns (preview Java 23-25) - -```java -// Expected to finalize soon -switch (statusCode) { - case 200 -> "OK"; - case 404 -> "Not Found"; - case int i when i >= 500 -> "Server Error: " + i; - case int i -> "Other: " + i; -} -``` - -## Virtual Threads (JEP 444, finalized Java 21) - -Virtual threads are lightweight threads managed by the JVM rather than the OS. They enable writing blocking code at massive scale without thread pool tuning. - -### Creating Virtual Threads - -```java -// Simple start -Thread.startVirtualThread(() -> { - var data = blockingHttpCall(); // blocks virtual thread, not OS thread - process(data); -}); - -// Builder pattern -Thread vt = Thread.ofVirtual() - .name("worker-", 0) // prefix + counter - .start(() -> doWork()); - -// Executor (recommended for most use cases) -try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { - List> futures = urls.stream() - .map(url -> executor.submit(() -> fetch(url))) - .toList(); - - for (var future : futures) { - System.out.println(future.get()); - } -} -``` - -### When to Use Virtual Threads - -Use virtual threads for: - -- I/O-bound tasks (HTTP calls, database queries, file I/O) -- High-concurrency servers handling many simultaneous connections -- Fan-out patterns (calling multiple services concurrently) - -Do NOT use virtual threads for: - -- CPU-intensive computation (use platform threads or ForkJoinPool) -- Tasks requiring thread-local caching with large objects (each VT has its own) -- Code using `synchronized` blocks that do I/O inside them (use ReentrantLock instead, as synchronized pins the carrier thread) - -### Virtual Threads with Existing APIs - -```java -// Works with ExecutorService -ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); - -// Works with CompletableFuture -var cf = CompletableFuture.supplyAsync(() -> fetch(url), executor); - -// HttpClient uses virtual threads internally in Java 21+ -HttpClient client = HttpClient.newHttpClient(); -``` - -## Structured Concurrency (preview since Java 21, 5th preview in Java 25) - -Structured concurrency treats groups of related tasks as a unit, ensuring child tasks complete before the parent scope exits. - -```java -// Using StructuredTaskScope (preview API) -// Compile with: javac --enable-preview --source 21 -ScopedValue USER = ScopedValue.newInstance(); - -try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { - Subtask userTask = scope.fork(() -> fetchUser(id)); - Subtask> ordersTask = scope.fork(() -> fetchOrders(id)); - - scope.join(); // wait for all subtasks - scope.throwIfFailed(); // propagate exceptions - - return new UserDashboard(userTask.get(), ordersTask.get()); -} - -// ShutdownOnSuccess - returns first successful result -try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) { - scope.fork(() -> fetchFromPrimary()); - scope.fork(() -> fetchFromBackup()); - - scope.join(); - return scope.result(); // first successful result -} -``` - -## Scoped Values (preview since Java 21, finalized Java 25) - -Scoped values are an alternative to ThreadLocal for sharing immutable data within and across threads in a structured way. - -```java -private static final ScopedValue CURRENT_USER = ScopedValue.newInstance(); - -void handleRequest(Request req) { - User user = authenticate(req); - ScopedValue.runWhere(CURRENT_USER, user, () -> { - processRequest(req); // CURRENT_USER is accessible here - }); -} - -void processRequest(Request req) { - User user = CURRENT_USER.get(); // access without parameter passing - // ... -} - -// Works with StructuredTaskScope - child tasks inherit scoped values -ScopedValue.runWhere(CURRENT_USER, user, () -> { - try (var scope = new StructuredTaskScope<>()) { - scope.fork(() -> { - // CURRENT_USER.get() works here too - return doWork(); - }); - scope.join(); - } -}); -``` - -## Sequenced Collections (JEP 431, finalized Java 21) - -Three new interfaces for collections with defined encounter order: - -```java -// SequencedCollection extends Collection -// Methods: addFirst, addLast, getFirst, getLast, removeFirst, removeLast, reversed - -List list = new ArrayList<>(List.of("a", "b", "c")); -list.getFirst(); // "a" -list.getLast(); // "c" -list.addFirst("z"); -list.reversed().forEach(System.out::println); // c, b, a, z - -// SequencedSet extends SequencedCollection, Set -SequencedSet set = new LinkedHashSet<>(List.of("x", "y", "z")); -set.getFirst(); // "x" -set.getLast(); // "z" -set.reversed(); // reversed view - -// SequencedMap extends Map -SequencedMap map = new LinkedHashMap<>(); -map.put("one", 1); -map.put("two", 2); -map.putFirst("zero", 0); -map.firstEntry(); // zero=0 -map.lastEntry(); // two=2 -map.pollLastEntry(); // removes and returns two=2 -map.sequencedKeySet(); -map.sequencedValues(); -map.sequencedEntrySet(); -``` - -Existing classes that gain these interfaces: `ArrayList`, `LinkedList`, `LinkedHashSet`, `TreeSet`, `LinkedHashMap`, `TreeMap`, `ConcurrentSkipListSet`, `ConcurrentSkipListMap`, and their unmodifiable wrappers. - -## Foreign Function & Memory API (JEP 454, finalized Java 22) - -Replaces JNI for calling native code and managing off-heap memory safely. - -### Key Concepts - -- **Arena** - manages lifecycle of memory segments (auto or confined) -- **MemorySegment** - represents a contiguous region of memory (heap or off-heap) -- **MemoryLayout** - describes memory structure (struct layout, sequence layout) -- **Linker** - links Java code with native functions -- **SymbolLookup** - finds native function addresses - -### Calling Native Functions - -```java -// Call strlen from C standard library -try (Arena arena = Arena.ofConfined()) { - // Look up the native function - Linker linker = Linker.nativeLinker(); - SymbolLookup stdlib = linker.defaultLookup(); - MethodHandle strlen = linker.downcallHandle( - stdlib.find("strlen").orElseThrow(), - FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) - ); - - // Allocate native string - MemorySegment str = arena.allocateFrom("Hello, FFM!"); - - // Call native function - long len = (long) strlen.invoke(str); - System.out.println("Length: " + len); // 11 -} -``` - -### Off-Heap Memory - -```java -try (Arena arena = Arena.ofConfined()) { - // Allocate array of 100 ints - MemorySegment segment = arena.allocate(ValueLayout.JAVA_INT, 100); - - // Write values - for (int i = 0; i < 100; i++) { - segment.setAtIndex(ValueLayout.JAVA_INT, i, i * 2); - } - - // Read values - int val = segment.getAtIndex(ValueLayout.JAVA_INT, 50); // 100 -} -// Memory automatically freed when arena closes -``` - -## Stream Gatherers (JEP 485, finalized Java 24) - -Custom intermediate stream operations: - -```java -// Built-in gatherers -import java.util.stream.Gatherers; - -// Fixed-size windows -Stream.of(1, 2, 3, 4, 5) - .gather(Gatherers.windowFixed(2)) - .toList(); // [[1,2], [3,4], [5]] - -// Sliding windows -Stream.of(1, 2, 3, 4, 5) - .gather(Gatherers.windowSliding(3)) - .toList(); // [[1,2,3], [2,3,4], [3,4,5]] - -// Fold (stateful reduction) -Stream.of(1, 2, 3, 4) - .gather(Gatherers.fold(() -> 0, Integer::sum)) - .toList(); // [10] - -// Scan (running accumulation) -Stream.of(1, 2, 3, 4) - .gather(Gatherers.scan(() -> 0, Integer::sum)) - .toList(); // [1, 3, 6, 10] - -// mapConcurrent - parallel map with virtual threads -Stream.of(url1, url2, url3) - .gather(Gatherers.mapConcurrent(10, this::fetch)) - .toList(); -``` - -## Compact Source Files and Instance Main Methods (finalized Java 25) - -Simplified entry points for new programmers: - -```java -// Before (traditional) -public class HelloWorld { - public static void main(String[] args) { - System.out.println("Hello, World!"); - } -} - -// After (Java 25) - no class declaration needed -void main() { - println("Hello, World!"); // implicit import of IO methods -} - -// Module imports also available (Java 25) -import module java.base; // imports all public types from java.base -``` - -## Other Notable Features - -### Text Blocks (finalized Java 15) - -```java -String json = """ - { - "name": "%s", - "age": %d - } - """.formatted(name, age); - -// Trailing whitespace control with \s -// Line continuation with \ at end of line -String text = """ - This is a long \ - single line\s\ - with trailing space"""; -``` - -### Switch Expressions (finalized Java 14) - -```java -// Arrow form (no fall-through) -int numLetters = switch (day) { - case MONDAY, FRIDAY, SUNDAY -> 6; - case TUESDAY -> 7; - case THURSDAY, SATURDAY -> 8; - case WEDNESDAY -> 9; -}; - -// Block with yield -String result = switch (status) { - case 200 -> "OK"; - case 404 -> { - log("Not found"); - yield "Not Found"; - } - default -> "Unknown"; -}; -``` diff --git a/packages/dotfiles/dot_agents/skills/jvm-helper/references/releases.md b/packages/dotfiles/dot_agents/skills/jvm-helper/references/releases.md new file mode 100644 index 0000000000..048c456752 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/jvm-helper/references/releases.md @@ -0,0 +1,89 @@ +# JVM ecosystem release lifecycle + +Read this when upgrading Java, Kotlin, Gradle, Maven, JUnit, or a major framework. + +## Current status + +JDK 26 is current GA. Java 25 is Oracle's latest designated LTS, but support policy is vendor-specific. Kotlin 2.4.10, Gradle 9.6.1, Maven 3.9.16, and JUnit 6.1.2 were current on 2026-08-03. + +Current does not mean compatible with every project. JUnit 6 and Spring Boot 4 are major migrations. Preview Java APIs can change each feature release and require compile/run preview flags. + +## Research ledger + +The following 75 authoritative pages were fetched and inspected: + +1. [Java support roadmap](https://www.oracle.com/java/technologies/java-se-support-roadmap.html) +2. [JDK 26](https://openjdk.org/projects/jdk/26/) +3. [JDK 25](https://openjdk.org/projects/jdk/25/) +4. [JDK 24](https://openjdk.org/projects/jdk/24/) +5. [JEP 491](https://openjdk.org/jeps/491) +6. [JEP 505](https://openjdk.org/jeps/505) +7. [JEP 506](https://openjdk.org/jeps/506) +8. [JEP 511](https://openjdk.org/jeps/511) +9. [JEP 512](https://openjdk.org/jeps/512) +10. [JEP 513](https://openjdk.org/jeps/513) +11. [JEP 519](https://openjdk.org/jeps/519) +12. [JEP 521](https://openjdk.org/jeps/521) +13. [JEP 502](https://openjdk.org/jeps/502) +14. [JEP 454](https://openjdk.org/jeps/454) +15. [JEP 444](https://openjdk.org/jeps/444) +16. [JEP 474](https://openjdk.org/jeps/474) +17. [JEP 490](https://openjdk.org/jeps/490) +18. [StructuredTaskScope](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html) +19. [ScopedValue](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ScopedValue.html) +20. [Executors](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/Executors.html) +21. [HttpClient](https://docs.oracle.com/en/java/javase/25/docs/api/java.net.http/java/net/http/HttpClient.html) +22. [JDK 25 jlink](https://docs.oracle.com/en/java/javase/25/docs/specs/man/jlink.html) +23. [JDK 26 jlink](https://docs.oracle.com/en/java/javase/26/docs/specs/man/jlink.html) +24. [jpackage](https://docs.oracle.com/en/java/javase/25/docs/specs/man/jpackage.html) +25. [jcmd](https://docs.oracle.com/en/java/javase/25/docs/specs/man/jcmd.html) +26. [jmap](https://docs.oracle.com/en/java/javase/25/docs/specs/man/jmap.html) +27. [JMX agent security](https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html) +28. [Kotlin releases](https://kotlinlang.org/docs/releases.html) +29. [Kotlin 2.2](https://kotlinlang.org/docs/whatsnew22.html) +30. [Kotlin 2.3](https://kotlinlang.org/docs/whatsnew23.html) +31. [Kotlin 2.4](https://kotlinlang.org/docs/whatsnew24.html) +32. [K2 migration](https://kotlinlang.org/docs/k2-compiler-migration-guide.html) +33. [Coroutine basics](https://kotlinlang.org/docs/coroutines-basics.html) +34. [Coroutine contexts](https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html) +35. [Cancellation](https://kotlinlang.org/docs/cancellation-and-timeouts.html) +36. [Exception handling](https://kotlinlang.org/docs/exception-handling.html) +37. [Java-to-Kotlin nullability](https://kotlinlang.org/docs/java-to-kotlin-nullability-guide.html) +38. [Java interoperability](https://kotlinlang.org/docs/java-interop.html) +39. [Kotlin Gradle projects](https://kotlinlang.org/docs/gradle-configure-project.html) +40. [Kotlin JUnit tests](https://kotlinlang.org/docs/jvm-test-using-junit.html) +41. [kotlinx.coroutines API](https://kotlinlang.org/api/kotlinx.coroutines/) +42. [Dispatchers.IO](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-i-o.html) +43. [kotlinx.coroutines 1.11.0](https://github.com/Kotlin/kotlinx.coroutines/releases/tag/1.11.0) +44. [Gradle release notes](https://docs.gradle.org/current/release-notes.html) +45. [Gradle compatibility](https://docs.gradle.org/current/userguide/compatibility.html) +46. [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) +47. [Gradle toolchains](https://docs.gradle.org/current/userguide/toolchains.html) +48. [Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html) +49. [Gradle 8.2](https://docs.gradle.org/8.2/release-notes.html) +50. [Gradle Java testing](https://docs.gradle.org/current/userguide/java_testing.html) +51. [Gradle test suites](https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html) +52. [Gradle build cache](https://docs.gradle.org/current/userguide/build_cache.html) +53. [Gradle performance](https://docs.gradle.org/current/userguide/performance.html) +54. [Gradle 9 upgrade](https://docs.gradle.org/current/userguide/upgrading_version_9.html) +55. [Maven downloads](https://maven.apache.org/download.cgi) +56. [Maven lifecycle](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) +57. [Maven Wrapper](https://maven.apache.org/wrapper/) +58. [Compiler release](https://maven.apache.org/plugins/maven-compiler-plugin/examples/set-compiler-release.html) +59. [Surefire skip behavior](https://maven.apache.org/surefire/maven-surefire-plugin/examples/skipping-tests.html) +60. [Failsafe usage](https://maven.apache.org/surefire/maven-failsafe-plugin/usage.html) +61. [JUnit Platform with Surefire](https://maven.apache.org/surefire/maven-surefire-plugin/examples/junit-platform.html) +62. [JUnit guide](https://docs.junit.org/current/user-guide/) +63. [JUnit assertions](https://docs.junit.org/6.1.2/writing-tests/assertions.html) +64. [JUnit parallel execution](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) +65. [Parameterized tests](https://docs.junit.org/6.1.2/writing-tests/parameterized-classes-and-tests.html) +66. [JUnit build support](https://docs.junit.org/6.1.2/running-tests/build-support.html) +67. [Native Image](https://www.graalvm.org/latest/reference-manual/native-image/) +68. [Tracing agent](https://www.graalvm.org/latest/reference-manual/native-image/guides/configure-with-tracing-agent/) +69. [Native Image compatibility](https://www.graalvm.org/latest/reference-manual/native-image/metadata/Compatibility/) +70. [Native Image memory management](https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/MemoryManagement/) +71. [Ktor releases](https://ktor.io/docs/releases.html) +72. [Ktor 3.5.1](https://github.com/ktorio/ktor/releases/tag/3.5.1) +73. [Spring Boot v4.1.0](https://github.com/spring-projects/spring-boot/releases/tag/v4.1.0) +74. [Shadow 9.6.1](https://github.com/GradleUp/shadow/releases/tag/9.6.1) +75. [JUnit r6.1.2](https://github.com/junit-team/junit-framework/releases/tag/r6.1.2) diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/SKILL.md b/packages/dotfiles/dot_agents/skills/lua-helper/SKILL.md index 96a6faef70..5df8c34d4e 100644 --- a/packages/dotfiles/dot_agents/skills/lua-helper/SKILL.md +++ b/packages/dotfiles/dot_agents/skills/lua-helper/SKILL.md @@ -1,464 +1,131 @@ --- name: lua-helper -description: | - Lua scripting for Neovim and WezTerm configuration - language patterns, vim API, and config management - When user works with .lua files, mentions Lua, Neovim config, WezTerm config, vim.api, or Lua scripting +description: Current Lua guidance for portable Lua, LuaJIT, Neovim's Lua interface, WezTerm configuration, LuaLS, formatting, linting, testing, and security. Use when writing or reviewing Lua, Neovim plugins/config, WezTerm config, rockspecs, or Lua tooling. --- -# Lua Helper Agent +# Lua Helper -## What's New (2025) +Identify the host before writing Lua. Portable Lua 5.5, Neovim's Lua 5.1 interface, optional LuaJIT extensions, and WezTerm's Lua 5.4 runtime have different APIs and semantics. -### Lua Language +## Current baselines -- **Lua 5.5.0** (Dec 2025): Declarations for global variables, named vararg tables, compact arrays (60% memory reduction), incremental major GC, read-only for-loop variables -- **Lua 5.4.8** (Jun 2025): Latest bug-fix release for 5.4 series -- **LuaJIT**: Still based on Lua 5.1 syntax; Neovim permanently targets LuaJIT/5.1 +Verified 2026-08-03: -### Neovim 0.11 +| Host or tool | Current baseline | Compatibility boundary | +| --- | --- | --- | +| Lua | 5.5.0; maintained 5.4 line at 5.4.8 | Version-gate APIs newer than the project's minimum | +| LuaJIT | Active rolling 2.1 branch | Lua 5.1-compatible with implementation extensions and build flags | +| Neovim | 0.12.4 | Permanent Lua 5.1 interface; may use LuaJIT or a compatible fork | +| WezTerm | Stable release still dated 2024 | Embeds Lua 5.4; online docs can describe nightly-only APIs | +| LuaLS | 3.18.2 | Annotations and diagnostics are tooling, not runtime validation | +| StyLua | 2.5.2 | Pin/configure per repository | +| LuaRocks | 3.13.0 | Installation runs package build logic and mutates environments | -- **Native LSP config**: `vim.lsp.config()` and `vim.lsp.enable()` replace nvim-lspconfig for basic setups -- **LSP completion**: `vim.lsp.completion.enable()` provides built-in auto-completion -- **Default LSP mappings**: `grn` (rename), `grr` (references), `gri` (implementation), `gO` (symbols), `gra` (code actions) -- **Async treesitter**: Highlighting, folding, and injection processing run asynchronously -- **Virtual lines diagnostics**: Display diagnostics as separate buffer lines -- **Snippet navigation**: Tab/Shift-Tab jump through `vim.snippet` nodes in insert mode -- **`winborder` option**: Set default borders for all floating windows -- **Grapheme cluster support**: Proper emoji and Unicode display +Read [references/releases.md](references/releases.md) for the 68-page research ledger. Read [references/core-language.md](references/core-language.md) for portable Lua and LuaJIT differences. Read [references/neovim.md](references/neovim.md) for current LSP, diagnostics, process, trust, and buffer APIs. Read [references/wezterm.md](references/wezterm.md) for configuration evaluation, strict mode, events, subprocesses, and mux domains. Read [references/tooling-and-security.md](references/tooling-and-security.md) for LuaLS, formatters, linters, tests, rocks, and security. -### Neovim 0.10 +## Select the runtime -- **`vim.iter()`**: Generic iterator interface for tables and iterator functions -- **`vim.snippet`**: Built-in snippet expansion and navigation -- **`vim.ringbuf()`**: Generic ring buffer data structure -- **`vim.ui.open()`**: Open URIs with system default handler +Before using an API, establish: -## Overview +1. Lua language/interface version. +2. Whether the runtime is PUC Lua, LuaJIT, or a host embedding. +3. Host application version and stable/nightly channel. +4. Tooling version and project configuration. -Lua serves as the primary configuration and extension language for Neovim and WezTerm. Neovim uses LuaJIT (Lua 5.1 compatible), while WezTerm embeds Lua 5.4. Both use Lua's table-based configuration model, but their APIs differ significantly. +In Neovim, target the documented Lua 5.1 interface and check `jit` before LuaJIT-specific behavior. In WezTerm, target Lua 5.4 and check the API's “Since” version against the installed binary. -**Key distinction**: Write Neovim Lua targeting Lua 5.1/LuaJIT semantics. Write WezTerm Lua targeting Lua 5.4 semantics. Avoid Lua 5.4 features (integers, to-be-closed variables, generational GC control) in Neovim code. +## Error discipline -## Core Lua Quick Reference - -### Tables - -```lua --- Array-style (1-indexed) -local list = { 'a', 'b', 'c' } -print(#list) -- 3 - --- Dictionary-style -local map = { name = 'value', ['key-with-dash'] = true } - --- Mixed -local mixed = { 'first', key = 'val', 'second' } - --- Nested -local config = { - ui = { border = 'rounded', width = 80 }, - keys = { 'f', 'g' }, -} - --- Table manipulation -table.insert(list, 'd') -- append -table.insert(list, 2, 'x') -- insert at position -table.remove(list, 1) -- remove at position -table.sort(list) -- in-place sort -table.concat(list, ', ') -- join to string -``` - -### Functions and Closures +Lua APIs commonly return `nil, error` or status tuples. Check every open, read, write, close, subprocess, and host callback result whose failure matters. ```lua --- Named function -local function greet(name) - return 'Hello, ' .. name -end - --- Anonymous / closure -local counter = (function() - local count = 0 - return function() - count = count + 1 - return count +local function read_file(path) + local file, open_error = io.open(path, "rb") + if not file then + return nil, open_error end -end)() - --- Variadic -local function log(level, ...) - local args = { ... } - print(string.format('[%s] %s', level, table.concat(args, ' '))) -end - --- Method syntax (colon passes self) -local obj = { name = 'test' } -function obj:get_name() - return self.name -end -``` - -### Metatables - -```lua -local Vector = {} -Vector.__index = Vector - -function Vector.new(x, y) - return setmetatable({ x = x, y = y }, Vector) -end - -function Vector:length() - return math.sqrt(self.x^2 + self.y^2) -end - -function Vector.__add(a, b) - return Vector.new(a.x + b.x, a.y + b.y) -end -function Vector:__tostring() - return string.format('(%g, %g)', self.x, self.y) -end -``` - -### String Patterns - -```lua --- Lua patterns (NOT regex) --- Character classes: %a (letter), %d (digit), %w (alphanumeric), %s (space), %p (punctuation) --- Uppercase = complement: %A (non-letter), %D (non-digit) - -string.find('hello world', 'world') -- 7, 11 -string.match('key=value', '(%w+)=(%w+)') -- 'key', 'value' -string.gmatch('a,b,c', '[^,]+') -- iterator: 'a', 'b', 'c' -string.gsub('hello', 'l', 'L') -- 'heLLo', 2 -string.format('%s has %d items', 'list', 5) -- 'list has 5 items' -``` - -### Error Handling - -```lua --- Protected call -local ok, result = pcall(function() - return risky_operation() -end) -if not ok then - print('Error: ' .. result) -end - --- With error handler (gets stack trace) -local ok, result = xpcall(risky_fn, debug.traceback) - --- Assert pattern (common in Neovim) -local value = assert(some_function(), 'Expected non-nil result') - --- Result-or-error pattern -local function safe_read(path) - local f, err = io.open(path, 'r') - if not f then return nil, err end - local content = f:read('*a') - f:close() + local content, read_error = file:read("*a") + local close_ok, close_error = file:close() + if not content then + return nil, read_error + end + if not close_ok then + return nil, close_error + end return content end ``` -### Modules +Do not bind an unchecked `io.open` result as a to-be-closed value. Preserve close failures when durability matters. -```lua --- Define a module -local M = {} +## Tables and sequences -function M.setup(opts) - -- configure -end +The length operator does not count arbitrary table keys. For a table with holes, `#table` returns a valid border and is not a reliable element count. Model sequences as contiguous integer keys or count explicitly. -function M.run() - -- execute -end +Version-gate `table.move`, `table.unpack`, `rawlen`, `__pairs`, and related APIs. Lua 5.1 uses global `unpack`; LuaJIT compatibility libraries can depend on build flags. -return M +Avoid memoization keys built from `table.concat({...})`: they reject many value types and can collide. Use nested tables keyed by arguments or a deliberately encoded, restricted input domain. --- Use a module -local mymod = require('mymod') -mymod.setup({ option = true }) -``` +## Neovim -## Neovim Lua Essentials +Current native LSP setup uses `vim.lsp.config` and `vim.lsp.enable`. This replaces nvim-lspconfig's deprecated legacy `require('lspconfig').setup` framework; nvim-lspconfig itself remains maintained and supplies server definitions. -### Options +Use current APIs: -```lua -vim.opt.number = true -vim.opt.relativenumber = true -vim.opt.shiftwidth = 2 -vim.opt.expandtab = true -vim.opt.smartindent = true -vim.opt.wrap = false -vim.opt.signcolumn = 'yes' -vim.opt.completeopt = { 'menu', 'menuone', 'noselect' } -vim.opt.wildignore:append({ '*.o', '*.pyc', 'node_modules' }) - --- Buffer/window local -vim.bo.filetype = 'lua' -vim.wo.foldmethod = 'expr' -``` - -### Key Mappings - -```lua --- vim.keymap.set(mode, lhs, rhs, opts) -vim.keymap.set('n', 'ff', function() - require('telescope.builtin').find_files() -end, { desc = 'Find files' }) - -vim.keymap.set('n', '', 'nohlsearch', { desc = 'Clear search highlight' }) -vim.keymap.set({ 'n', 'v' }, 'y', '"+y', { desc = 'Yank to clipboard' }) -vim.keymap.set('i', 'jk', '', { desc = 'Exit insert mode' }) -vim.keymap.set('n', 'e', vim.diagnostic.open_float, { desc = 'Show diagnostic' }) - --- Buffer-local mapping -vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = true, desc = 'LSP hover' }) - --- Delete a mapping -vim.keymap.del('n', 'ff') -``` - -### Autocommands - -```lua -local group = vim.api.nvim_create_augroup('MyGroup', { clear = true }) - -vim.api.nvim_create_autocmd('BufWritePre', { - group = group, - pattern = '*.lua', - callback = function(args) - -- args.buf, args.match, args.file - vim.lsp.buf.format({ bufnr = args.buf }) - end, -}) - -vim.api.nvim_create_autocmd('FileType', { - group = group, - pattern = { 'javascript', 'typescript' }, - callback = function() - vim.opt_local.shiftwidth = 2 - end, -}) - -vim.api.nvim_create_autocmd('TextYankPost', { - group = group, - callback = function() - vim.hl.on_yank() - end, -}) -``` - -### User Commands - -```lua -vim.api.nvim_create_user_command('Greet', function(opts) - local name = opts.fargs[1] or 'World' - print('Hello, ' .. name .. (opts.bang and '!' or '.')) -end, { - nargs = '?', - bang = true, - desc = 'Greet someone', - complete = function() - return { 'Alice', 'Bob', 'World' } - end, -}) -``` - -### Variables - -```lua -vim.g.mapleader = ' ' -- global variable -vim.g.maplocalleader = '\\' -vim.b.some_flag = true -- buffer variable -vim.g.loaded_netrw = 1 -- disable built-in plugin -``` +- `vim.hl.hl_op()` for yank highlighting. +- `vim.diagnostic.jump({ count = 1 })` and negative count for navigation. +- `vim.api.nvim_set_option_value(name, value, { buf = buffer })` for scoped options. +- `vim.system({ program, argument }, { text = true }):wait()` for ordinary subprocesses, checking `code` and `stderr`. -### LSP Configuration (0.11+) +Use `:trust` and `vim.secure` for project-local configuration. `exrc` executes project code and is a trust boundary. -```lua --- ~/.config/nvim/lsp/lua_ls.lua -return { - cmd = { 'lua-language-server' }, - filetypes = { 'lua' }, - root_markers = { '.luarc.json', '.luarc.jsonc' }, - settings = { - Lua = { - runtime = { version = 'LuaJIT' }, - workspace = { library = vim.api.nvim_get_runtime_file('', true) }, - }, - }, -} - --- init.lua -vim.lsp.enable({ 'lua_ls', 'ts_ls', 'rust_analyzer' }) -``` - -### Vim API Common Functions - -```lua --- Buffer operations -local buf = vim.api.nvim_get_current_buf() -local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) -vim.api.nvim_buf_set_lines(buf, 0, -1, false, { 'new content' }) -vim.api.nvim_buf_set_option(buf, 'modifiable', false) - --- Window operations -local win = vim.api.nvim_get_current_win() -vim.api.nvim_win_set_cursor(win, { 10, 0 }) -- row 10, col 0 -local cursor = vim.api.nvim_win_get_cursor(win) - --- Create floating window -local buf = vim.api.nvim_create_buf(false, true) -local win = vim.api.nvim_open_win(buf, true, { - relative = 'editor', - width = 60, - height = 20, - col = 10, - row = 5, - style = 'minimal', - border = 'rounded', -}) - --- Highlights -vim.api.nvim_set_hl(0, 'MyHighlight', { fg = '#ff0000', bold = true }) -``` - -### Utility Functions - -```lua --- Table utilities -vim.tbl_extend('force', defaults, user_opts) -vim.tbl_deep_extend('force', defaults, user_opts) -vim.tbl_contains(list, 'value') -vim.tbl_keys(map) -vim.tbl_filter(function(v) return v > 0 end, numbers) - --- Iterators (0.10+) -vim.iter(ipairs(list)):map(function(_, v) return v * 2 end):totable() -vim.iter(pairs(map)):filter(function(k, v) return v ~= nil end):totable() - --- File system -vim.fs.find('init.lua', { upward = true }) -vim.fs.root(0, { '.git', 'Makefile' }) -vim.fs.joinpath(vim.fn.stdpath('config'), 'lua') - --- Scheduling (required from vim.uv callbacks) -vim.schedule(function() - vim.api.nvim_echo({ { 'Done!', 'Normal' } }, true, {}) -end) - --- Deferred execution -vim.defer_fn(function() - print('Delayed message') -end, 1000) - --- Inspect -print(vim.inspect({ nested = { data = true } })) -``` +## WezTerm -## WezTerm Config Essentials - -### Basic Structure +Create the configuration with strict mode so invalid options fail: ```lua -local wezterm = require 'wezterm' +local wezterm = require("wezterm") local config = wezterm.config_builder() - -config.font = wezterm.font 'JetBrains Mono' -config.font_size = 14.0 -config.color_scheme = 'Catppuccin Mocha' -config.window_decorations = 'RESIZE' -config.enable_tab_bar = true -config.hide_tab_bar_if_only_one_tab = true -config.window_padding = { left = 8, right = 8, top = 8, bottom = 8 } +config:set_strict_mode(true) return config ``` -### Keybindings - -```lua -local act = wezterm.action - -config.keys = { - { key = 'l', mods = 'SUPER', action = act.ShowLauncher }, - { key = 'f', mods = 'SUPER', action = act.ToggleFullScreen }, - { key = 'd', mods = 'SUPER', action = act.SplitHorizontal { domain = 'CurrentPaneDomain' } }, - { key = 'd', mods = 'SUPER|SHIFT', action = act.SplitVertical { domain = 'CurrentPaneDomain' } }, - { key = 'w', mods = 'SUPER', action = act.CloseCurrentPane { confirm = true } }, - { key = '[', mods = 'SUPER', action = act.ActivatePaneDirection 'Prev' }, - { key = ']', mods = 'SUPER', action = act.ActivatePaneDirection 'Next' }, - { key = 'k', mods = 'SUPER', action = act.ClearScrollback 'ScrollbackAndViewport' }, - { key = 'p', mods = 'SUPER', action = act.ActivateCommandPalette }, -} -``` - -### Events +WezTerm may evaluate configuration repeatedly. Keep top-level evaluation idempotent and free of side effects such as spawning processes. Config precedence includes CLI and environment overrides before standard paths. -```lua -wezterm.on('gui-startup', function(cmd) - local tab, pane, window = wezterm.mux.spawn_window(cmd or {}) - window:gui_window():maximize() -end) - -wezterm.on('format-tab-title', function(tab, tabs, panes, config, hover, max_width) - local title = tab.active_pane.title - if tab.is_active then - return { { Background = { Color = '#1e1e2e' } }, { Text = ' ' .. title .. ' ' } } - end - return ' ' .. title .. ' ' -end) - --- Custom event with action_callback -config.keys = { - { - key = 'r', - mods = 'SUPER|SHIFT', - action = wezterm.action_callback(function(window, pane) - window:perform_action(act.ReloadConfiguration, pane) - end), - }, -} -``` +`wezterm.run_child_process` returns success, stdout, and stderr. Inspect success. Returning `false` from a `wezterm.on` callback stops later callbacks and the default action. -### Multiplexing +## Tooling -```lua -config.unix_domains = { - { name = 'unix' }, -} - -config.ssh_domains = { - { - name = 'my-server', - remote_address = 'server.example.com', - username = 'user', - }, -} - --- Default to multiplexer domain -config.default_gui_startup_args = { 'connect', 'unix' } +```bash +stylua --check . +luacheck . +selene . +busted ``` -## Reference Files +Use the tools configured by the project. Choose Luacheck or Selene deliberately instead of layering both without purpose. Use Busted for behavior-style suites or LuaUnit for lightweight xUnit tests. Use headless Neovim/Plenary only when host integration is part of the contract. -Detailed references in `references/` directory: +LuaLS supports `.luarc.json` / `.luarc.jsonc`, annotations, diagnostics, addons, and stricter type-checking modes. Loading the entire Neovim runtime as a workspace library is valid but broad; prefer an addon or narrow explicit paths when sufficient. -- **neovim-lua-api.md**: Complete Neovim Lua API patterns - vim.api._, vim.fn._, vim.opt, keymaps, autocommands, user commands, highlights, plugin development, LSP, treesitter, diagnostics -- **wezterm-config.md**: WezTerm Lua configuration - keybindings, appearance, multiplexing, events, custom actions, domains, status bar -- **lua-language.md**: Core Lua language patterns - tables, metatables, closures, coroutines, modules, string patterns, error handling, OOP, iterators +## Security -## When to Ask for Help +- Load untrusted source only as text with a constrained environment; never accept untrusted binary chunks. +- Avoid shell construction in `os.execute` and string-form `vim.fn.system`. Never concatenate untrusted input into a shell command. +- WezTerm and Neovim config execute arbitrary code. Keep secrets out of config and use protected identity/agent mechanisms. +- WezTerm config can execute more than once; repeated evaluation magnifies side effects. +- LuaRocks installation executes rock build/package logic. Inspect sources and rockspecs and pin versions in reproducible environments. +- LuaJIT FFI is an implementation extension with native-memory safety implications; do not assume it exists or is sandboxed. -Ask the user for clarification when: +## Review checklist -- Target environment is ambiguous (Neovim LuaJIT vs WezTerm Lua 5.4) -- Plugin manager choice affects configuration structure -- LSP server configuration needs specific project settings -- Keybinding conflicts with existing mappings are possible -- WezTerm multiplexing domain setup needs network details +- Identify portable Lua, LuaJIT, Neovim, or WezTerm before choosing APIs. +- Version-gate language, host, and online-documentation features. +- Check file, process, callback, and cleanup results. +- Do not use `#` as a general map size or unsafe concatenated memo keys. +- Use current Neovim LSP, highlight, diagnostic, option, and process APIs. +- Make WezTerm config strict, idempotent, and side-effect free at top level. +- Configure LuaLS and one intentional lint/test workflow. +- Treat project config, shells, rocks, binary chunks, and FFI as trust boundaries. +- Preserve the distinction between Neovim's Lua 5.1 interface and optional LuaJIT implementation. diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/references/core-language.md b/packages/dotfiles/dot_agents/skills/lua-helper/references/core-language.md new file mode 100644 index 0000000000..7233ff2c11 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/lua-helper/references/core-language.md @@ -0,0 +1,53 @@ +# Lua core language and LuaJIT + +Read this when writing portable Lua, selecting a language version, or considering a LuaJIT extension. + +## Lua 5.5 and 5.4 + +Lua 5.5 adds explicit global declarations, named varargs, table-creation support, and runtime/collector changes. Lua 5.4 introduced generational collection, to-be-closed variables, and const locals. Link the active bug page when exact correctness matters because point releases publish errata. + +`math.type` arrived in Lua 5.3, not 5.4. `table.move`, `table.unpack`, `rawlen`, metamethods, and module search APIs vary by version; check the target manual. + +## LuaJIT + +LuaJIT is actively maintained as a rolling release from its 2.1 branch. Upstream does not publish official binary/tarball releases. It provides a Lua 5.1-compatible interface plus extensions. + +`goto` and labels are always-enabled Lua 5.2 extensions in LuaJIT. Some library compatibility features require `LUAJIT_ENABLE_LUA52COMPAT`. FFI is LuaJIT-specific and must not be assumed on every Lua 5.1-compatible host. + +Do not quote a universal LuaJIT speedup. Results depend on workload, architecture, traces, FFI, host integration, and warmup. + +## To-be-closed values + +Check resource creation before assigning a to-be-closed value: + +```lua +local opened, open_error = io.open(path, "rb") +if not opened then + return nil, open_error +end +local file = opened +``` + +This is Lua 5.4+ syntax and does not apply to Neovim's Lua 5.1 interface. + +## Loading code + +When the target Lua version supports mode/environment parameters, accept untrusted code only as text and provide a constrained environment. Binary chunks are code and must not be accepted from untrusted sources. + +## Process execution + +`os.execute` crosses a shell boundary. Never concatenate untrusted values. Inspect the version-specific returned status tuple; it differs across Lua versions. + +## Primary documentation + +- [Lua versions](https://www.lua.org/versions.html) +- [Lua 5.5 manual](https://www.lua.org/manual/5.5/manual.html) +- [Lua 5.5 readme](https://www.lua.org/manual/5.5/readme.html) +- [Lua 5.4 manual](https://www.lua.org/manual/5.4/manual.html) +- [Lua 5.4 readme](https://www.lua.org/manual/5.4/readme.html) +- [Lua bugs](https://www.lua.org/bugs.html) +- [LuaJIT status](https://luajit.org/status.html) +- [LuaJIT extensions](https://luajit.org/extensions.html) +- [Running LuaJIT](https://luajit.org/running.html) +- [Installing LuaJIT](https://luajit.org/install.html) +- [LuaJIT FFI](https://luajit.org/ext_ffi.html) diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/references/lua-language.md b/packages/dotfiles/dot_agents/skills/lua-helper/references/lua-language.md deleted file mode 100644 index 8144a5d5a4..0000000000 --- a/packages/dotfiles/dot_agents/skills/lua-helper/references/lua-language.md +++ /dev/null @@ -1,827 +0,0 @@ -# Lua Language Reference - -## Language Versions - -- **Lua 5.5** (Dec 2025): Global variable declarations, named vararg tables, compact arrays (60% less memory), incremental major GC, read-only for-loop variables -- **Lua 5.4** (2020): Integers, to-be-closed variables, generational GC -- used by WezTerm -- **LuaJIT** (Lua 5.1 compatible): JIT compiler, FFI, ~2-10x faster -- used by Neovim permanently - -LuaJIT does not support: integers (5.3+), to-be-closed variables (5.4+), bitwise operators as syntax (5.3+), goto labels selectively. Use `bit` library for bitwise ops in LuaJIT. - -## Types - -Lua has 8 types: `nil`, `boolean`, `number`, `string`, `function`, `table`, `userdata`, `thread` (coroutine). - -```lua -type(nil) -- "nil" -type(true) -- "boolean" -type(42) -- "number" -type('hello') -- "string" -type(print) -- "function" -type({}) -- "table" -type(coroutine.create(function() end)) -- "thread" - --- Lua 5.4 distinguishes integer and float subtypes -math.type(42) -- "integer" (5.4 only) -math.type(42.0) -- "float" (5.4 only) - --- Truthiness: only nil and false are falsy --- 0, "", and empty tables are truthy -if 0 then print('0 is truthy') end -- prints -if '' then print('empty string truthy') end -- prints -``` - -## Variables and Scope - -```lua --- Global (avoid in modules) -my_global = 'visible everywhere' - --- Local (block-scoped) -local x = 10 -do - local y = 20 -- only visible in this block - x = x + y -- x from outer scope -end --- y is nil here - --- Multiple assignment -local a, b, c = 1, 2, 3 -local first, rest = 'a', 'b', 'c' -- rest = 'b', 'c' is discarded - --- Swap -a, b = b, a - --- Lua 5.4: const and close -local x = 42 -- cannot reassign -local f = io.open('file') -- __close called on scope exit -``` - -## Numbers - -```lua --- LuaJIT / 5.1: all numbers are double-precision floats --- Lua 5.4: integer (64-bit) and float (double) subtypes - -local i = 42 -- integer in 5.4, float in 5.1 -local f = 42.0 -- float -local h = 0xff -- hex literal = 255 -local e = 1.5e3 -- scientific = 1500.0 - --- Integer division --- 5.3+: // operator --- 5.1/LuaJIT: math.floor(a / b) - --- Bitwise ops --- 5.3+: &, |, ~, <<, >> --- LuaJIT: bit.band, bit.bor, bit.bxor, bit.lshift, bit.rshift -local bit = require('bit') -- LuaJIT -bit.band(0xff, 0x0f) -- 0x0f -bit.bor(0x01, 0x10) -- 0x11 -bit.lshift(1, 4) -- 16 - --- Math library -math.abs(-5) -- 5 -math.ceil(4.2) -- 5 -math.floor(4.8) -- 4 -math.max(1, 2, 3) -- 3 -math.min(1, 2, 3) -- 1 -math.sqrt(16) -- 4 -math.random() -- [0, 1) float -math.random(1, 6) -- [1, 6] integer -math.huge -- infinity -math.pi -- 3.14159... -``` - -## Strings - -```lua --- String literals -local s1 = 'single quotes' -local s2 = "double quotes" -local s3 = [[ - long string literal - preserves newlines - no escape processing -]] -local s4 = [==[ - long string with ]] inside -]==] - --- Concatenation -local full = 'hello' .. ' ' .. 'world' -- 'hello world' -local num = 'count: ' .. tostring(42) - --- Length -#'hello' -- 5 (byte count, not character count for UTF-8) - --- String library -string.byte('A') -- 65 -string.char(65) -- 'A' -string.len(s) -- byte length -string.rep('ab', 3) -- 'ababab' -string.reverse('hello') -- 'olleh' -string.sub('hello', 2, 4) -- 'ell' (1-indexed, inclusive) -string.sub('hello', -3) -- 'llo' (negative = from end) -string.upper('hello') -- 'HELLO' -string.lower('HELLO') -- 'hello' - --- Method syntax -s:upper() -s:lower() -s:sub(1, 3) -s:rep(2) -s:find('pattern') -s:match('pattern') -s:gsub('pattern', 'replacement') -s:format(args) -``` - -## String Patterns - -Lua uses its own pattern system (NOT regex). - -```lua --- Character classes --- %a letter %A non-letter --- %d digit %D non-digit --- %l lowercase %L non-lowercase --- %u uppercase %U non-uppercase --- %w alphanumeric %W non-alphanumeric --- %s whitespace %S non-whitespace --- %p punctuation %P non-punctuation --- %c control char %C non-control --- . any character - --- Quantifiers --- * 0 or more (greedy) --- + 1 or more (greedy) --- - 0 or more (lazy) --- ? 0 or 1 - --- Anchors --- ^ start of string --- $ end of string - --- Captures --- () capture group --- (%w+) capture one or more alphanumeric - --- Escaping special chars: ( ) . % + - * ? [ ] ^ $ --- Use % to escape: %( for literal ( - --- Examples -string.find('hello world', 'world') -- 7, 11 -string.find('hello world', '%a+', 7) -- 7, 11 (from position 7) - -string.match('2025-01-15', '(%d+)-(%d+)-(%d+)') -- '2025', '01', '15' -string.match('key=value', '(%w+)=(.+)') -- 'key', 'value' -string.match(' hello ', '^%s*(.-)%s*$') -- 'hello' (trim) - --- gmatch: iterate all matches -for word in string.gmatch('one two three', '%S+') do - print(word) -end - --- gmatch with captures -for k, v in string.gmatch('a=1&b=2&c=3', '(%w+)=(%w+)') do - print(k, v) -- a 1, b 2, c 3 -end - --- gsub: replace -string.gsub('hello world', 'world', 'lua') -- 'hello lua', 1 -string.gsub('aaa', 'a', 'b', 2) -- 'bba', 2 (limit=2) -string.gsub('hello', '(%w+)', function(w) - return w:upper() -end) -- 'HELLO', 1 - --- Format (like printf) -string.format('%d items at $%.2f', 5, 9.99) -- '5 items at $9.99' -string.format('%q', 'she said "hi"') -- '"she said \\"hi\\""' -string.format('%02x', 255) -- 'ff' -string.format('%-20s|', 'left-aligned') -- 'left-aligned |' -``` - -## Tables - -### Array Operations - -```lua -local arr = { 10, 20, 30, 40, 50 } - --- Length (only counts consecutive integer keys from 1) -#arr -- 5 - --- Access (1-indexed) -arr[1] -- 10 -arr[#arr] -- 50 (last element) - --- Append -arr[#arr + 1] = 60 -table.insert(arr, 70) -- append -table.insert(arr, 1, 0) -- prepend (shifts others) - --- Remove -table.remove(arr) -- remove last -table.remove(arr, 1) -- remove first (shifts others) - --- Sort -table.sort(arr) -- ascending -table.sort(arr, function(a, b) return a > b end) -- descending - --- Sort complex structures -local items = { { name = 'b', val = 2 }, { name = 'a', val = 1 } } -table.sort(items, function(a, b) return a.name < b.name end) - --- Concatenate to string -table.concat({ 'a', 'b', 'c' }, ', ') -- 'a, b, c' -table.concat({ 'a', 'b', 'c' }) -- 'abc' - --- Move (5.3+) -table.move(arr, 1, 3, 5) -- copy elements 1-3 to positions 5-7 -table.move(src, 1, #src, #dst + 1, dst) -- append src to dst - --- Unpack (convert array to multiple returns) -local a, b, c = table.unpack({ 10, 20, 30 }) --- LuaJIT: unpack() (global, not table.unpack) -``` - -### Dictionary Operations - -```lua -local dict = { - name = 'example', - count = 42, - ['special-key'] = true, -} - --- Access -dict.name -- 'example' -dict['special-key'] -- true - --- Set / update -dict.new_key = 'value' -dict['another'] = 123 - --- Delete -dict.name = nil - --- Check existence -if dict.count then ... end -if dict.count ~= nil then ... end -- more explicit - --- Iterate (order not guaranteed) -for key, value in pairs(dict) do - print(key, value) -end - --- Get keys -local keys = {} -for k in pairs(dict) do - keys[#keys + 1] = k -end - --- Merge (shallow) -local function merge(a, b) - local result = {} - for k, v in pairs(a) do result[k] = v end - for k, v in pairs(b) do result[k] = v end - return result -end --- In Neovim: vim.tbl_extend('force', a, b) -``` - -### Iteration Patterns - -```lua --- ipairs: array part (1, 2, 3...), stops at first nil -for i, v in ipairs(arr) do - print(i, v) -end - --- pairs: all keys (unordered) -for k, v in pairs(tbl) do - print(k, v) -end - --- Numeric for -for i = 1, #arr do - print(arr[i]) -end - --- Reverse iteration -for i = #arr, 1, -1 do - print(arr[i]) -end - --- Safe removal during iteration (reverse) -for i = #arr, 1, -1 do - if should_remove(arr[i]) then - table.remove(arr, i) - end -end - --- next() for checking if table is empty -if next(tbl) == nil then - print('table is empty') -end -``` - -## Metatables - -### Core Metamethods - -```lua --- __index: called when key not found in table --- Can be a table (prototype lookup) or function (computed access) -local defaults = { color = 'blue', size = 10 } -local obj = setmetatable({}, { __index = defaults }) -print(obj.color) -- 'blue' (from defaults) -obj.color = 'red' -print(obj.color) -- 'red' (own property now) - --- __index as function -setmetatable(obj, { - __index = function(self, key) - return 'default_' .. key - end, -}) - --- __newindex: called when setting key that doesn't exist -setmetatable(obj, { - __newindex = function(self, key, value) - if type(value) ~= 'number' then - error('only numbers allowed') - end - rawset(self, key, value) -- bypass __newindex - end, -}) - --- Arithmetic metamethods --- __add (+), __sub (-), __mul (*), __div (/), __mod (%), __pow (^) --- __unm (unary -), __idiv (//, 5.3+) - --- Comparison metamethods --- __eq (==), __lt (<), __le (<=) - --- Other metamethods --- __concat (..), __len (#), __call (function call syntax) --- __tostring (tostring()), __pairs (pairs()), __ipairs (ipairs()) --- __gc (garbage collection finalizer) --- __close (to-be-closed, 5.4+) - --- __call: make table callable -local callable = setmetatable({}, { - __call = function(self, ...) - return 'called with: ' .. table.concat({...}, ', ') - end, -}) -callable('a', 'b') -- 'called with: a, b' - --- __tostring: custom string representation -setmetatable(obj, { - __tostring = function(self) - return string.format('Obj(%s)', self.name) - end, -}) -print(obj) -- 'Obj(example)' - --- __len: custom length -setmetatable(obj, { - __len = function(self) - local count = 0 - for _ in pairs(self) do count = count + 1 end - return count - end, -}) -print(#obj) -- number of all keys -``` - -### Raw Access (Bypass Metamethods) - -```lua -rawget(tbl, key) -- get without __index -rawset(tbl, key, value) -- set without __newindex -rawlen(tbl) -- length without __len -rawequal(a, b) -- compare without __eq -``` - -## Object-Oriented Patterns - -### Class with Inheritance - -```lua --- Base class -local Animal = {} -Animal.__index = Animal - -function Animal.new(name, sound) - return setmetatable({ - name = name, - sound = sound, - }, Animal) -end - -function Animal:speak() - return self.name .. ' says ' .. self.sound -end - -function Animal:get_name() - return self.name -end - --- Subclass -local Dog = setmetatable({}, { __index = Animal }) -Dog.__index = Dog - -function Dog.new(name) - local self = Animal.new(name, 'woof') - return setmetatable(self, Dog) -end - -function Dog:fetch(item) - return self.name .. ' fetches ' .. item -end - --- Usage -local rex = Dog.new('Rex') -rex:speak() -- 'Rex says woof' (inherited) -rex:fetch('ball') -- 'Rex fetches ball' (own method) -``` - -### Mixin Pattern - -```lua -local Serializable = {} -function Serializable:serialize() - local parts = {} - for k, v in pairs(self) do - parts[#parts + 1] = k .. '=' .. tostring(v) - end - return '{' .. table.concat(parts, ', ') .. '}' -end - -local Loggable = {} -function Loggable:log(msg) - print('[' .. (self.name or '?') .. '] ' .. msg) -end - --- Apply mixins -local function mixin(class, ...) - for _, m in ipairs({...}) do - for k, v in pairs(m) do - if class[k] == nil then - class[k] = v - end - end - end -end - -local MyClass = {} -MyClass.__index = MyClass -mixin(MyClass, Serializable, Loggable) -``` - -### Encapsulation with Closures - -```lua -local function create_counter(initial) - local count = initial or 0 -- private state - - return { - increment = function() count = count + 1 end, - decrement = function() count = count - 1 end, - get = function() return count end, - } -end - -local c = create_counter(10) -c.increment() -c.increment() -print(c.get()) -- 12 --- count is not accessible directly -``` - -## Closures and Upvalues - -```lua --- A closure captures variables from its enclosing scope -function make_adder(n) - return function(x) - return x + n -- n is an upvalue - end -end - -local add5 = make_adder(5) -add5(10) -- 15 - --- Iterator factory using closure -function range(start, stop, step) - step = step or 1 - local current = start - step - return function() - current = current + step - if current <= stop then - return current - end - end -end - -for i in range(1, 5) do print(i) end - --- Memoization -function memoize(fn) - local cache = {} - return function(...) - local key = table.concat({...}, ',') - if cache[key] == nil then - cache[key] = fn(...) - end - return cache[key] - end -end - -local fib = memoize(function(n) - if n < 2 then return n end - return fib(n - 1) + fib(n - 2) -end) -``` - -## Coroutines - -```lua --- Create coroutine -local co = coroutine.create(function(x) - print('start:', x) - local y = coroutine.yield(x * 2) - print('resumed:', y) - return x + y -end) - --- Resume (first call passes args to function, subsequent calls pass args to yield) -local ok, val = coroutine.resume(co, 10) -- start: 10, ok=true val=20 -local ok, val = coroutine.resume(co, 5) -- resumed: 5, ok=true val=15 -local ok, val = coroutine.resume(co) -- ok=false (dead) - --- Status -coroutine.status(co) -- 'dead', 'suspended', 'running', 'normal' - --- Wrap (returns function that auto-resumes) -local gen = coroutine.wrap(function() - for i = 1, 3 do - coroutine.yield(i) - end -end) - -gen() -- 1 -gen() -- 2 -gen() -- 3 -gen() -- error: cannot resume dead coroutine - --- Producer-consumer pattern -local function producer() - return coroutine.wrap(function() - for i = 1, 10 do - coroutine.yield(i) - end - end) -end - -local function consumer(gen) - for value in gen do - print('consumed:', value) - end -end - -consumer(producer()) - --- Pipeline -local function map(gen, fn) - return coroutine.wrap(function() - for v in gen do - coroutine.yield(fn(v)) - end - end) -end - -local function filter(gen, predicate) - return coroutine.wrap(function() - for v in gen do - if predicate(v) then - coroutine.yield(v) - end - end - end) -end - --- Usage: filter(map(producer(), double), is_even) -``` - -## Modules - -### Module Definition - -```lua --- mymodule.lua -local M = {} - --- Private (not in M table) -local cache = {} - -local function helper() - return 'internal' -end - --- Public -function M.setup(opts) - M.config = opts or {} -end - -function M.process(input) - if cache[input] then return cache[input] end - local result = helper() .. ':' .. input - cache[input] = result - return result -end - -return M -``` - -### Module Loading - -```lua --- require caches by module name -local mod = require('mymodule') -local sub = require('mymodule.submodule') -- searches mymodule/submodule.lua - --- Force reload -package.loaded['mymodule'] = nil -local mod = require('mymodule') - --- Search paths -package.path -- Lua module search path (semicolon separated) --- Example: ./?.lua;./?/init.lua;/usr/share/lua/5.1/?.lua - -package.cpath -- C module search path --- Example: ./?.so;/usr/lib/lua/5.1/?.so - --- Custom searcher -table.insert(package.searchers, function(name) - -- custom module resolution -end) -``` - -## Error Handling - -```lua --- Raise error -error('something went wrong') -error('bad argument', 2) -- level 2 = caller's location -error({ code = 404, msg = 'not found' }) -- error object - --- Assert (raises error if condition is falsy) -assert(x > 0, 'x must be positive') -local f = assert(io.open('file'), 'cannot open file') - --- Protected call -local ok, result = pcall(function() - return dangerous_operation() -end) -if ok then - use(result) -else - handle_error(result) -end - --- Protected call with error handler -local ok, result = xpcall(function() - return dangerous_operation() -end, function(err) - return debug.traceback(err, 2) -- add stack trace -end) - --- Common patterns --- 1. Result-or-error -local function divide(a, b) - if b == 0 then return nil, 'division by zero' end - return a / b -end -local result, err = divide(10, 0) -if err then print(err) end - --- 2. Assert result-or-error -local result = assert(divide(10, 2)) -- raises on error - --- 3. Finally pattern (cleanup) -local function with_file(path, fn) - local f, err = io.open(path, 'r') - if not f then return nil, err end - local ok, result = pcall(fn, f) - f:close() - if not ok then error(result) end - return result -end -``` - -## I/O - -```lua --- Read file -local f = io.open('file.txt', 'r') -local content = f:read('*a') -- read all -f:close() - --- Read lines -for line in io.lines('file.txt') do - print(line) -end - --- Write file -local f = io.open('file.txt', 'w') -- 'w' = write, 'a' = append -f:write('hello\n') -f:write(string.format('count: %d\n', 42)) -f:close() - --- Read modes -f:read('*a') -- all content -f:read('*l') -- line (no newline) -- default -f:read('*L') -- line (with newline, 5.2+) -f:read('*n') -- number -f:read(10) -- 10 bytes - --- Standard I/O -io.read() -- read line from stdin -io.write('out') -- write to stdout -io.stderr:write('err') - --- OS operations -os.clock() -- CPU time -os.time() -- current time (seconds since epoch) -os.date('%Y-%m-%d') -- formatted date -os.getenv('HOME') -- environment variable -os.execute('ls') -- run shell command -os.tmpname() -- temporary file name -os.rename(old, new) -os.remove(path) -``` - -## LuaRocks Package Manager - -```bash -# Install LuaRocks -brew install luarocks # macOS -apt install luarocks # Debian/Ubuntu - -# Install a package -luarocks install luasocket -luarocks install --local penlight # user-local install - -# List installed -luarocks list - -# Search -luarocks search json - -# Show info -luarocks show luasocket - -# Remove -luarocks remove luasocket - -# Install for specific Lua version -luarocks --lua-version=5.1 install lpeg - -# Use with Neovim (rocks.nvim plugin manager) -# rocks.nvim integrates LuaRocks directly into Neovim plugin management -``` - -## Performance Tips - -```lua --- Local access is faster than global -local pairs = pairs -local ipairs = ipairs -local type = type -local insert = table.insert - --- Pre-allocate tables when size is known -local t = {} -for i = 1, 1000 do t[i] = 0 end -- better than repeated insert - --- String concatenation: use table.concat for many strings -local parts = {} -for i = 1, 1000 do - parts[i] = 'item' .. i -end -local result = table.concat(parts, '\n') -- much faster than .. in loop - --- Avoid creating tables in hot loops -local reuse = {} -for i = 1, 1000000 do - reuse[1] = i -- reuse table - process(reuse) -end - --- Use # operator carefully: undefined for tables with holes --- { 1, nil, 3 } -- #t could be 1 or 3 -``` diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/references/neovim-lua-api.md b/packages/dotfiles/dot_agents/skills/lua-helper/references/neovim-lua-api.md deleted file mode 100644 index 88ccb6d5fb..0000000000 --- a/packages/dotfiles/dot_agents/skills/lua-helper/references/neovim-lua-api.md +++ /dev/null @@ -1,857 +0,0 @@ -# Neovim Lua API Reference - -## API Layers - -Neovim exposes three API layers to Lua: - -1. **Vim API** (`vim.cmd()`, `vim.fn`): Inherited Vimscript Ex-commands and functions -2. **Nvim API** (`vim.api`): C-based API for remote plugins and GUIs, prefixed `nvim_` -3. **Lua API** (`vim.*`): Native Lua functions designed specifically for Lua consumers - -Target Lua 5.1/LuaJIT semantics exclusively. Check `jit` global before using LuaJIT extensions. - -## vim.api -- Core C API Bindings - -### Buffer Operations - -```lua --- Get/set current buffer -local buf = vim.api.nvim_get_current_buf() -vim.api.nvim_set_current_buf(buf) - --- List buffers -local bufs = vim.api.nvim_list_bufs() - --- Get buffer info -local name = vim.api.nvim_buf_get_name(buf) -local loaded = vim.api.nvim_buf_is_loaded(buf) -local valid = vim.api.nvim_buf_is_valid(buf) -local line_count = vim.api.nvim_buf_line_count(buf) - --- Read lines (0-indexed, end-exclusive) -local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) -- all lines -local first = vim.api.nvim_buf_get_lines(buf, 0, 1, false) -- first line -local range = vim.api.nvim_buf_get_lines(buf, 5, 10, false) -- lines 6-10 - --- Write lines -vim.api.nvim_buf_set_lines(buf, 0, -1, false, { 'line1', 'line2' }) -- replace all -vim.api.nvim_buf_set_lines(buf, -1, -1, false, { 'appended' }) -- append - --- Get/set text (row/col coordinates, 0-indexed) -local text = vim.api.nvim_buf_get_text(buf, 0, 0, 0, 5, {}) -- first 5 chars of line 1 -vim.api.nvim_buf_set_text(buf, 0, 0, 0, 5, { 'replaced' }) - --- Create scratch buffer -local scratch = vim.api.nvim_create_buf(false, true) -- listed=false, scratch=true -vim.api.nvim_buf_set_name(scratch, 'MyBuffer') - --- Delete buffer -vim.api.nvim_buf_delete(buf, { force = true }) - --- Buffer options -vim.api.nvim_set_option_value('modifiable', false, { buf = buf }) -vim.api.nvim_set_option_value('filetype', 'markdown', { buf = buf }) -vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf }) -``` - -### Window Operations - -```lua --- Get/set current window -local win = vim.api.nvim_get_current_win() -vim.api.nvim_set_current_win(win) - --- List windows -local wins = vim.api.nvim_list_wins() -local tab_wins = vim.api.nvim_tabpage_list_wins(0) - --- Cursor position (1-indexed row, 0-indexed col) -local pos = vim.api.nvim_win_get_cursor(win) -- { row, col } -vim.api.nvim_win_set_cursor(win, { 10, 0 }) - --- Window dimensions -local width = vim.api.nvim_win_get_width(win) -local height = vim.api.nvim_win_get_height(win) -vim.api.nvim_win_set_width(win, 80) -vim.api.nvim_win_set_height(win, 24) - --- Window buffer -local buf = vim.api.nvim_win_get_buf(win) -vim.api.nvim_win_set_buf(win, other_buf) - --- Window options -vim.api.nvim_set_option_value('number', true, { win = win }) -vim.api.nvim_set_option_value('wrap', false, { win = win }) - --- Close window -vim.api.nvim_win_close(win, true) -- force=true -``` - -### Floating Windows - -```lua -local buf = vim.api.nvim_create_buf(false, true) - --- Centered floating window -local width = math.floor(vim.o.columns * 0.8) -local height = math.floor(vim.o.lines * 0.8) -local win = vim.api.nvim_open_win(buf, true, { - relative = 'editor', - width = width, - height = height, - col = math.floor((vim.o.columns - width) / 2), - row = math.floor((vim.o.lines - height) / 2), - style = 'minimal', - border = 'rounded', -- 'none', 'single', 'double', 'rounded', 'solid', 'shadow' - title = 'My Window', - title_pos = 'center', - footer = 'Press q to close', - footer_pos = 'center', -}) - --- Window relative to cursor -vim.api.nvim_open_win(buf, false, { - relative = 'cursor', - width = 40, - height = 10, - col = 0, - row = 1, - style = 'minimal', - border = 'single', -}) - --- Set window config after creation -vim.api.nvim_win_set_config(win, { title = 'Updated Title' }) -``` - -### Tab Pages - -```lua -local tab = vim.api.nvim_get_current_tabpage() -local tabs = vim.api.nvim_list_tabpages() -local tab_win = vim.api.nvim_tabpage_get_win(tab) -local tab_num = vim.api.nvim_tabpage_get_number(tab) -``` - -### Extmarks and Namespaces - -```lua --- Create namespace -local ns = vim.api.nvim_create_namespace('my-plugin') - --- Set extmark (virtual text, highlights, signs) -local mark_id = vim.api.nvim_buf_set_extmark(buf, ns, 0, 0, { - virt_text = { { 'virtual text', 'Comment' } }, - virt_text_pos = 'eol', -- 'eol', 'overlay', 'right_align', 'inline' - hl_group = 'Search', - end_row = 0, - end_col = 5, - priority = 100, - sign_text = '>>', - sign_hl_group = 'DiagnosticSignError', -}) - --- Get extmarks -local marks = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) - --- Delete extmark -vim.api.nvim_buf_del_extmark(buf, ns, mark_id) - --- Clear namespace -vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) -``` - -### Highlights - -```lua --- Set highlight group -vim.api.nvim_set_hl(0, 'MyHighlight', { - fg = '#e06c75', - bg = '#282c34', - bold = true, - italic = false, - underline = false, - sp = '#ff0000', -- special color (underline/undercurl) - undercurl = true, - strikethrough = false, - link = 'OtherGroup', -- link to existing group (overrides other attrs) - default = false, -- only set if not already defined -}) - --- Get highlight info -local hl = vim.api.nvim_get_hl(0, { name = 'Normal' }) - --- Namespace-scoped highlights (for plugins) -vim.api.nvim_set_hl(ns, 'MyPluginHl', { fg = '#00ff00' }) -vim.api.nvim_win_set_hl_ns(win, ns) -- apply namespace to window -``` - -## vim.fn -- Vimscript Function Bridge - -```lua --- File operations -vim.fn.expand('%:p') -- full path of current file -vim.fn.expand('%:t') -- filename only -vim.fn.fnamemodify(path, ':h') -- directory of path -vim.fn.filereadable(path) -- 1 if readable, 0 if not -vim.fn.isdirectory(path) -- 1 if directory -vim.fn.glob('*.lua') -- glob pattern match -vim.fn.globpath('.', '**/*.lua') -- recursive glob -vim.fn.mkdir(path, 'p') -- mkdir -p equivalent - --- String operations -vim.fn.trim(str) -- trim whitespace -vim.fn.toupper(str) -- uppercase -vim.fn.tolower(str) -- lowercase -vim.fn.substitute(str, pat, rep, flags) - --- System interaction -vim.fn.system('ls -la') -- run shell command, return output -vim.fn.systemlist('ls -la') -- run command, return lines -vim.fn.executable('rg') -- 1 if in PATH -vim.fn.getenv('HOME') -- environment variable -vim.fn.shellescape(arg) -- escape for shell - --- Input -vim.fn.input('Enter name: ') -vim.fn.confirm('Delete?', '&Yes\n&No', 2) -vim.fn.inputlist({ 'Select:', '1. Option A', '2. Option B' }) - --- Register and cursor -vim.fn.getreg('"') -- get register content -vim.fn.setreg('"', 'text') -- set register -vim.fn.line('.') -- current line number -vim.fn.col('.') -- current column -vim.fn.getline('.') -- current line text -vim.fn.getpos('.') -- [bufnum, lnum, col, off] - --- Autoload functions (use bracket notation) -vim.fn['my#plugin#func']() -``` - -## vim.opt -- Option Management - -```lua --- Set options (like :set) -vim.opt.number = true -vim.opt.relativenumber = true -vim.opt.tabstop = 4 -vim.opt.shiftwidth = 4 -vim.opt.expandtab = true -vim.opt.smartindent = true -vim.opt.wrap = false -vim.opt.cursorline = true -vim.opt.termguicolors = true -vim.opt.signcolumn = 'yes' -vim.opt.scrolloff = 8 -vim.opt.sidescrolloff = 8 -vim.opt.updatetime = 250 -vim.opt.timeoutlen = 300 -vim.opt.undofile = true -vim.opt.ignorecase = true -vim.opt.smartcase = true -vim.opt.splitbelow = true -vim.opt.splitright = true -vim.opt.clipboard = 'unnamedplus' - --- List/map options -vim.opt.completeopt = { 'menu', 'menuone', 'noselect' } -vim.opt.shortmess:append('c') -vim.opt.wildignore:append({ '*.o', '*.a', '__pycache__' }) -vim.opt.formatoptions:remove('o') - --- Get current value -local sw = vim.opt.shiftwidth:get() - --- Buffer/window specific -vim.opt_local.spell = true -vim.opt_local.spelllang = 'en_us' - --- Global only (like :setglobal) -vim.opt_global.laststatus = 3 -``` - -## vim.keymap -- Key Mapping - -```lua --- Basic mappings -vim.keymap.set('n', 'w', 'write', { desc = 'Save file' }) -vim.keymap.set('n', 'q', 'quit', { desc = 'Quit' }) - --- Lua function as rhs -vim.keymap.set('n', 'ff', function() - require('telescope.builtin').find_files() -end, { desc = 'Find files' }) - --- Multiple modes -vim.keymap.set({ 'n', 'v' }, 'y', '"+y', { desc = 'Yank to system clipboard' }) - --- All options -vim.keymap.set('n', 'lhs', 'rhs', { - desc = 'Description for which-key', - buffer = nil, -- buffer number, or true for current buffer - silent = true, -- default: true - noremap = true, -- default: true (non-recursive) - nowait = false, - expr = false, -- rhs is expression to evaluate - remap = false, -- set true for recursive mapping - replace_keycodes = true, -- when expr=true -}) - --- Expression mapping -vim.keymap.set('n', 'j', function() - return vim.v.count > 0 and 'j' or 'gj' -end, { expr = true, desc = 'Smart j' }) - --- mappings (for plugin authors) -vim.keymap.set('n', '(my-action)', function() - -- plugin action -end) - --- Delete mapping -vim.keymap.del('n', 'ff') -vim.keymap.del('n', 'K', { buffer = 0 }) -- buffer-local -``` - -## Autocommands - -```lua --- Create autocommand group (clear=true removes old entries on re-source) -local group = vim.api.nvim_create_augroup('MyPlugin', { clear = true }) - --- BufWritePre - format on save -vim.api.nvim_create_autocmd('BufWritePre', { - group = group, - pattern = { '*.lua', '*.py', '*.rs' }, - callback = function(args) - vim.lsp.buf.format({ bufnr = args.buf, async = false }) - end, -}) - --- FileType - filetype specific settings -vim.api.nvim_create_autocmd('FileType', { - group = group, - pattern = 'lua', - callback = function() - vim.opt_local.shiftwidth = 2 - vim.opt_local.tabstop = 2 - end, -}) - --- BufEnter - when entering a buffer -vim.api.nvim_create_autocmd('BufEnter', { - group = group, - pattern = '*.md', - callback = function() - vim.opt_local.wrap = true - vim.opt_local.spell = true - end, -}) - --- LspAttach - when LSP client attaches -vim.api.nvim_create_autocmd('LspAttach', { - group = group, - callback = function(args) - local client = vim.lsp.get_client_by_id(args.data.client_id) - if client and client.supports_method('textDocument/formatting') then - vim.keymap.set('n', 'f', function() - vim.lsp.buf.format({ bufnr = args.buf }) - end, { buffer = args.buf, desc = 'Format buffer' }) - end - end, -}) - --- VimEnter - after startup -vim.api.nvim_create_autocmd('VimEnter', { - group = group, - callback = function() - if vim.fn.argc() == 0 then - -- Open dashboard or file picker - end - end, -}) - --- TextYankPost - highlight on yank -vim.api.nvim_create_autocmd('TextYankPost', { - group = group, - callback = function() - vim.hl.on_yank({ timeout = 200 }) - end, -}) - --- Callback args table fields: --- args.id - autocommand id --- args.event - event name --- args.group - group id --- args.match - expanded --- args.buf - buffer number --- args.file - expanded --- args.data - event-specific data -``` - -## User Commands - -```lua --- Simple command -vim.api.nvim_create_user_command('Hello', function(opts) - print('Hello, ' .. (opts.fargs[1] or 'World')) -end, { nargs = '?', desc = 'Say hello' }) - --- With range support -vim.api.nvim_create_user_command('FormatRange', function(opts) - vim.lsp.buf.format({ - range = { - ['start'] = { opts.line1, 0 }, - ['end'] = { opts.line2, 0 }, - }, - }) -end, { range = true, desc = 'Format selection' }) - --- With completion -vim.api.nvim_create_user_command('SetTheme', function(opts) - vim.cmd.colorscheme(opts.fargs[1]) -end, { - nargs = 1, - complete = function() - return vim.fn.getcompletion('', 'color') - end, - desc = 'Set colorscheme', -}) - --- Buffer-local command -vim.api.nvim_buf_create_user_command(0, 'BufOnly', function() - -- buffer-specific command -end, { desc = 'Buffer-local command' }) - --- opts table fields: --- opts.name - command name --- opts.args - raw argument string --- opts.fargs - split arguments table --- opts.bang - true if ! was used --- opts.line1 - start line of range --- opts.line2 - end line of range --- opts.range - number of items in range (0, 1, or 2) --- opts.count - supplied count --- opts.reg - supplied register --- opts.mods - command modifiers (split, vertical, etc.) --- opts.smods - structured modifiers table -``` - -## vim.diagnostic -- Diagnostics - -```lua --- Configure diagnostics display -vim.diagnostic.config({ - virtual_text = { - prefix = '!', - severity = { min = vim.diagnostic.severity.WARN }, - current_line = true, -- 0.11+: only show on current line - }, - signs = { - text = { - [vim.diagnostic.severity.ERROR] = 'E', - [vim.diagnostic.severity.WARN] = 'W', - [vim.diagnostic.severity.INFO] = 'I', - [vim.diagnostic.severity.HINT] = 'H', - }, - }, - underline = true, - update_in_insert = false, - severity_sort = true, - float = { - border = 'rounded', - source = true, - }, -}) - --- Get diagnostics -local diags = vim.diagnostic.get(buf) -- all for buffer -local errors = vim.diagnostic.get(buf, { severity = vim.diagnostic.severity.ERROR }) -local all = vim.diagnostic.get() -- all buffers - --- Navigate -vim.diagnostic.goto_next({ severity = vim.diagnostic.severity.ERROR }) -vim.diagnostic.goto_prev() - --- Show in float -vim.diagnostic.open_float({ scope = 'line' }) -- 'line', 'cursor', 'buffer' - --- Show in location list / quickfix -vim.diagnostic.setloclist() -vim.diagnostic.setqflist() - --- Custom diagnostic source -vim.diagnostic.set(ns, buf, { - { - lnum = 0, -- 0-indexed line - col = 0, -- 0-indexed column - end_lnum = 0, - end_col = 5, - severity = vim.diagnostic.severity.ERROR, - message = 'Something is wrong', - source = 'my-linter', - }, -}) -``` - -## vim.lsp -- Language Server Protocol - -```lua --- LSP setup (0.11+ native config) --- Place in ~/.config/nvim/lsp/.lua --- Return config table from file -vim.lsp.enable({ 'lua_ls', 'ts_ls', 'gopls' }) - --- Manual client start -vim.lsp.start({ - name = 'my-lsp', - cmd = { 'my-language-server', '--stdio' }, - root_dir = vim.fs.root(0, { '.git', 'package.json' }), - capabilities = vim.lsp.protocol.make_client_capabilities(), -}) - --- Common LSP actions -vim.lsp.buf.hover() -vim.lsp.buf.definition() -vim.lsp.buf.declaration() -vim.lsp.buf.type_definition() -vim.lsp.buf.implementation() -vim.lsp.buf.references() -vim.lsp.buf.rename() -vim.lsp.buf.code_action() -vim.lsp.buf.signature_help() -vim.lsp.buf.format({ async = false }) -vim.lsp.buf.document_symbol() -vim.lsp.buf.workspace_symbol('query') - --- Get active clients -local clients = vim.lsp.get_clients({ bufnr = buf }) -for _, client in ipairs(clients) do - print(client.name, client.id) -end - --- Built-in completion (0.11+) -vim.lsp.completion.enable(true, client_id, buf, { autotrigger = true }) - --- Client capabilities (merge with plugin capabilities) -local capabilities = vim.tbl_deep_extend('force', - vim.lsp.protocol.make_client_capabilities(), - require('cmp_nvim_lsp').default_capabilities() -) -``` - -## vim.treesitter -- Tree-sitter Integration - -```lua --- Get parser for buffer -local parser = vim.treesitter.get_parser(buf, 'lua') -local tree = parser:parse()[1] -local root = tree:root() - --- Get node at cursor -local node = vim.treesitter.get_node() -local node_type = node:type() -local node_text = vim.treesitter.get_node_text(node, buf) -local parent = node:parent() -local start_row, start_col, end_row, end_col = node:range() - --- Query -local query = vim.treesitter.query.parse('lua', [[ - (function_declaration - name: (identifier) @function.name - body: (block) @function.body) -]]) - -for id, node, metadata in query:iter_captures(root, buf) do - local name = query.captures[id] - local text = vim.treesitter.get_node_text(node, buf) - print(name, text) -end - --- Highlighting -vim.treesitter.start(buf, 'lua') -- enable TS highlighting -vim.treesitter.stop(buf) -- disable - --- Folds -vim.opt.foldmethod = 'expr' -vim.opt.foldexpr = 'v:lua.vim.treesitter.foldexpr()' -vim.opt.foldlevel = 99 - --- Inspect highlights at cursor -vim.treesitter.inspect_tree() -- open TS playground -vim.show_pos() -- show highlight groups at cursor -``` - -## Plugin Development Patterns - -### Standard Plugin Structure - -``` -my-plugin.nvim/ - lua/ - my-plugin/ - init.lua -- M.setup(), core logic - config.lua -- default options, validation - util.lua -- helpers - my-plugin.lua -- optional: shorthand require - plugin/ - my-plugin.lua -- entry point: commands, lazy require - ftplugin/ - lua.lua -- filetype-specific setup - doc/ - my-plugin.txt -- vimdoc help file -``` - -### Lazy Loading Pattern - -```lua --- plugin/my-plugin.lua (loaded at startup, keep minimal) -vim.api.nvim_create_user_command('MyPlugin', function(opts) - require('my-plugin').run(opts) -- lazy require -end, { nargs = '*' }) - -vim.api.nvim_create_user_command('MyPluginSetup', function() - require('my-plugin').setup() -end, {}) -``` - -### Setup Pattern - -```lua --- lua/my-plugin/init.lua -local M = {} - -local defaults = { - enabled = true, - border = 'rounded', - mappings = { - toggle = 'm', - }, -} - -function M.setup(opts) - M.config = vim.tbl_deep_extend('force', defaults, opts or {}) - if M.config.mappings.toggle then - vim.keymap.set('n', M.config.mappings.toggle, M.toggle, { desc = 'Toggle my plugin' }) - end -end - -function M.toggle() - -- plugin logic -end - -return M -``` - -### lazy.nvim Plugin Spec - -```lua --- In lazy.nvim plugin spec -{ - 'author/my-plugin.nvim', - dependencies = { 'nvim-lua/plenary.nvim' }, - event = 'BufReadPost', -- lazy load on event - cmd = 'MyPlugin', -- lazy load on command - ft = { 'lua', 'python' }, -- lazy load on filetype - keys = { -- lazy load on keymap - { 'm', 'MyPlugin toggle', desc = 'Toggle plugin' }, - }, - opts = { -- passed to setup() - border = 'single', - }, - config = function(_, opts) -- custom config (default calls setup(opts)) - require('my-plugin').setup(opts) - end, -} -``` - -### Health Check - -```lua --- lua/my-plugin/health.lua -local M = {} - -function M.check() - vim.health.start('my-plugin') - - -- Check dependencies - if vim.fn.executable('rg') == 1 then - vim.health.ok('ripgrep found') - else - vim.health.error('ripgrep not found', { 'Install ripgrep: brew install ripgrep' }) - end - - -- Check Neovim version - if vim.fn.has('nvim-0.10') == 1 then - vim.health.ok('Neovim >= 0.10') - else - vim.health.warn('Neovim < 0.10, some features unavailable') - end - - -- Check config - local config = require('my-plugin').config - if config then - vim.health.ok('Configuration loaded') - else - vim.health.info('Plugin not configured yet, call setup()') - end -end - -return M -``` - -## vim.fs -- File System - -```lua --- Path manipulation -vim.fs.normalize('~/config/../.config/nvim') -- /home/user/.config/nvim -vim.fs.dirname('/path/to/file.lua') -- /path/to -vim.fs.basename('/path/to/file.lua') -- file.lua -vim.fs.joinpath('/path', 'to', 'file.lua') -- /path/to/file.lua -vim.fs.abspath('relative/path') -- /cwd/relative/path - --- Find files (search upward from buffer) -local root = vim.fs.root(0, { '.git', 'Makefile', 'package.json' }) -local files = vim.fs.find('init.lua', { - upward = true, - path = vim.fn.expand('%:p:h'), - type = 'file', -}) -local dirs = vim.fs.find('.git', { - upward = true, - type = 'directory', -}) -local matches = vim.fs.find(function(name) - return name:match('%.test%.lua$') -end, { type = 'file', limit = math.huge }) - --- Iterate directory -for name, type in vim.fs.dir('/path/to/dir') do - -- type: 'file', 'directory', 'link', etc. - print(name, type) -end - --- Standard paths -vim.fn.stdpath('config') -- ~/.config/nvim -vim.fn.stdpath('data') -- ~/.local/share/nvim -vim.fn.stdpath('state') -- ~/.local/state/nvim -vim.fn.stdpath('cache') -- ~/.cache/nvim -vim.fn.stdpath('log') -- ~/.local/state/nvim -``` - -## vim.iter -- Iterator Library (0.10+) - -```lua --- From list -local doubled = vim.iter({ 1, 2, 3, 4 }) - :map(function(v) return v * 2 end) - :totable() --- { 2, 4, 6, 8 } - --- From pairs -local keys = vim.iter(pairs({ a = 1, b = 2, c = 3 })) - :filter(function(_, v) return v > 1 end) - :map(function(k) return k end) - :totable() - --- Chaining -vim.iter(ipairs(items)) - :map(function(_, item) return item.name end) - :filter(function(name) return name ~= '' end) - :each(function(name) print(name) end) - --- Take and skip -vim.iter({ 1, 2, 3, 4, 5 }):take(3):totable() -- { 1, 2, 3 } -vim.iter({ 1, 2, 3, 4, 5 }):skip(2):totable() -- { 3, 4, 5 } - --- With predicates (0.11+) -vim.iter({ 1, 2, 3, 4 }):take(function(v) return v < 3 end):totable() -- { 1, 2 } - --- Fold/reduce -local sum = vim.iter({ 1, 2, 3 }):fold(0, function(acc, v) return acc + v end) - --- Find -local found = vim.iter({ 'foo', 'bar', 'baz' }):find(function(v) return v:match('ba') end) - --- Enumerate -vim.iter({ 'a', 'b', 'c' }):enumerate():each(function(i, v) - print(i, v) -end) - --- From custom iterator -local function range(start, stop) - local i = start - 1 - return function() - i = i + 1 - if i <= stop then return i end - end -end -vim.iter(range(1, 5)):totable() -- { 1, 2, 3, 4, 5 } -``` - -## vim.uv -- libuv Bindings - -```lua --- CRITICAL: Cannot call vim.api.* directly from uv callbacks --- Use vim.schedule() or vim.schedule_wrap() to defer - --- Timer -local timer = vim.uv.new_timer() -timer:start(1000, 0, vim.schedule_wrap(function() - print('Fired after 1 second') - timer:stop() - timer:close() -end)) - --- Repeating timer -local interval = vim.uv.new_timer() -interval:start(0, 500, vim.schedule_wrap(function() - -- runs every 500ms -end)) - --- File watching -local handle = vim.uv.new_fs_event() -handle:start('/path/to/file', {}, vim.schedule_wrap(function(err, filename, events) - if events.change then - vim.cmd('checktime') -- reload if changed - end -end)) - --- Async process -local stdout = vim.uv.new_pipe() -local handle, pid = vim.uv.spawn('ls', { - args = { '-la' }, - stdio = { nil, stdout, nil }, -}, vim.schedule_wrap(function(code, signal) - stdout:close() - print('Process exited:', code) -end)) - -stdout:read_start(vim.schedule_wrap(function(err, data) - if data then print(data) end -end)) -``` - -## Scheduling and Async - -```lua --- Schedule to main loop (safe for vim.api calls) -vim.schedule(function() - vim.api.nvim_echo({ { 'Safe from callback', 'Normal' } }, true, {}) -end) - --- Wrap callback to auto-schedule -local safe_cb = vim.schedule_wrap(function(result) - vim.api.nvim_buf_set_lines(0, 0, -1, false, { result }) -end) - --- Deferred execution (one-shot timer + schedule) -vim.defer_fn(function() - print('Runs after 500ms') -end, 500) - --- Wait with timeout -local success = vim.wait(5000, function() - return some_condition -end, 100) -- check every 100ms, timeout after 5000ms -``` diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/references/neovim.md b/packages/dotfiles/dot_agents/skills/lua-helper/references/neovim.md new file mode 100644 index 0000000000..b0f9c0cdd9 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/lua-helper/references/neovim.md @@ -0,0 +1,78 @@ +# Neovim Lua + +Read this when configuring Neovim, writing plugins, using LSP or diagnostics, spawning processes, or loading project-local configuration. + +## Runtime contract + +Neovim guarantees a permanent Lua 5.1 interface and may run LuaJIT or a compatible fork. Check `jit` before using implementation-specific extensions. + +## LSP + +Current native setup: + +```lua +vim.lsp.config("example", { + cmd = { "example-language-server" }, + filetypes = { "example" }, +}) +vim.lsp.enable("example") +``` + +nvim-lspconfig remains maintained for server definitions. Its legacy `require('lspconfig').setup` framework is deprecated. + +Use `cmp_nvim_lsp.default_capabilities()` only when nvim-cmp is intentionally the completion frontend; it can change built-in omnifunc behavior. + +## Diagnostics and highlighting + +```lua +vim.diagnostic.jump({ count = 1 }) +vim.diagnostic.jump({ count = -1 }) +``` + +Use `vim.hl.hl_op()` for yank highlighting. Older `vim.hl.on_yank` and `vim.diagnostic.goto_next` / `goto_prev` are deprecated in 0.12. + +For whole-line range formatting, end at `{ opts.line2, -1 }` rather than column zero of the last line. + +## Options + +Set scoped options with: + +```lua +vim.api.nvim_set_option_value("modifiable", false, { buf = buffer }) +``` + +Do not use deprecated `nvim_buf_set_option` in new code. + +## Processes + +Prefer `vim.system` for ordinary subprocesses: + +```lua +local result = vim.system({ "git", "status", "--short" }, { text = true }):wait() +if result.code ~= 0 then + error(result.stderr) +end +``` + +String-form `vim.fn.system` invokes a shell. Use it only when shell syntax is deliberate and all dynamic input is controlled. + +Use libuv directly only when its event-loop or handle-level API is required. Check constructor/start/spawn/read callback errors and close every handle. + +## Trust + +Project-local config through `exrc` executes code. Use `:trust` and `vim.secure.read`; understand that any trust workflow has a time-of-check/time-of-use boundary when files can change afterward. + +## Primary documentation + +- [Neovim 0.11 news](https://neovim.io/doc/user/news-0.11.html) +- [Neovim 0.12 news](https://neovim.io/doc/user/news-0.12.html) +- [Lua interface](https://neovim.io/doc/user/lua.html) +- [Lua guide](https://neovim.io/doc/user/lua-guide.html) +- [API](https://neovim.io/doc/user/api.html) +- [LSP](https://neovim.io/doc/user/lsp.html) +- [Diagnostics](https://neovim.io/doc/user/diagnostic.html) +- [Tree-sitter](https://neovim.io/doc/user/treesitter.html) +- [Options](https://neovim.io/doc/user/options.html) +- [Deprecated APIs](https://neovim.io/doc/user/deprecated.html) +- [Trust](https://neovim.io/doc/user/starting.html#trust) +- [Neovim v0.12.4](https://github.com/neovim/neovim/releases/tag/v0.12.4) diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/references/releases.md b/packages/dotfiles/dot_agents/skills/lua-helper/references/releases.md new file mode 100644 index 0000000000..19976c78fa --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/lua-helper/references/releases.md @@ -0,0 +1,84 @@ +# Lua ecosystem release lifecycle + +Read this when upgrading Lua, Neovim, WezTerm, LuaLS, LuaRocks, or the formatting/testing toolchain. + +## Version boundaries + +- Lua 5.5.0 is current; 5.4.8 is the latest 5.4 maintenance release. +- LuaJIT is a rolling 2.1 branch rather than a conventional official tarball release. +- Neovim 0.12.4 is current stable and guarantees Lua 5.1 interface semantics. +- WezTerm's online docs can be ahead of its 2024 stable binary; check “Since” annotations. +- Lua tooling release pages are more current than some hosted “stable” documentation. + +## Research ledger + +The following 68 primary pages were fetched and inspected: + +1. [Lua versions](https://www.lua.org/versions.html) +2. [Lua 5.5 manual](https://www.lua.org/manual/5.5/manual.html) +3. [Lua 5.5 readme](https://www.lua.org/manual/5.5/readme.html) +4. [Lua 5.4 manual](https://www.lua.org/manual/5.4/manual.html) +5. [Lua 5.4 readme](https://www.lua.org/manual/5.4/readme.html) +6. [Lua bugs](https://www.lua.org/bugs.html) +7. [LuaJIT status](https://luajit.org/status.html) +8. [LuaJIT extensions](https://luajit.org/extensions.html) +9. [Running LuaJIT](https://luajit.org/running.html) +10. [Installing LuaJIT](https://luajit.org/install.html) +11. [LuaJIT FFI](https://luajit.org/ext_ffi.html) +12. [LuaJIT repository](https://github.com/LuaJIT/LuaJIT) +13. [Neovim 0.11 news](https://neovim.io/doc/user/news-0.11.html) +14. [Neovim 0.12 news](https://neovim.io/doc/user/news-0.12.html) +15. [Neovim Lua](https://neovim.io/doc/user/lua.html) +16. [Neovim Lua guide](https://neovim.io/doc/user/lua-guide.html) +17. [Neovim API](https://neovim.io/doc/user/api.html) +18. [Neovim LSP](https://neovim.io/doc/user/lsp.html) +19. [Neovim diagnostics](https://neovim.io/doc/user/diagnostic.html) +20. [Neovim Tree-sitter](https://neovim.io/doc/user/treesitter.html) +21. [Neovim options](https://neovim.io/doc/user/options.html) +22. [Neovim deprecated APIs](https://neovim.io/doc/user/deprecated.html) +23. [Neovim trust](https://neovim.io/doc/user/starting.html#trust) +24. [Neovim v0.12.4](https://github.com/neovim/neovim/releases/tag/v0.12.4) +25. [WezTerm config files](https://wezterm.org/config/files.html) +26. [WezTerm Lua](https://wezterm.org/config/lua/general.html) +27. [config_builder](https://wezterm.org/config/lua/wezterm/config_builder.html) +28. [wezterm.on](https://wezterm.org/config/lua/wezterm/on.html) +29. [action_callback](https://wezterm.org/config/lua/wezterm/action_callback.html) +30. [run_child_process](https://wezterm.org/config/lua/wezterm/run_child_process.html) +31. [mux.spawn_window](https://wezterm.org/config/lua/wezterm.mux/spawn_window.html) +32. [window.perform_action](https://wezterm.org/config/lua/window/perform_action.html) +33. [SSH domains](https://wezterm.org/config/lua/config/ssh_domains.html) +34. [Unix domains](https://wezterm.org/config/lua/config/unix_domains.html) +35. [Default GUI startup arguments](https://wezterm.org/config/lua/config/default_gui_startup_args.html) +36. [WezTerm multiplexing](https://wezterm.org/multiplexing.html) +37. [WezTerm changelog](https://wezterm.org/changelog.html) +38. [json_parse](https://wezterm.org/config/lua/wezterm/json_parse.html) +39. [target_triple](https://wezterm.org/config/lua/wezterm/target_triple.html) +40. [SshDomain](https://wezterm.org/config/lua/SshDomain.html) +41. [procinfo.pid](https://wezterm.org/config/lua/wezterm.procinfo/pid.html) +42. [WezTerm 20240203-110809-5046fc22](https://github.com/wezterm/wezterm/releases/tag/20240203-110809-5046fc22) +43. [LuaRocks](https://luarocks.org/) +44. [LuaRocks v3.13.0](https://github.com/luarocks/luarocks/releases/tag/v3.13.0) +45. [LuaLS configuration](https://luals.github.io/wiki/configuration/) +46. [LuaLS annotations](https://luals.github.io/wiki/annotations/) +47. [LuaLS diagnostics](https://luals.github.io/wiki/diagnostics/) +48. [LuaLS addons](https://luals.github.io/wiki/addons/) +49. [LuaLS workspace library](https://luals.github.io/wiki/settings/#workspace-library) +50. [LuaLS type checking](https://luals.github.io/wiki/type-checking/) +51. [LuaLS 3.18.2](https://github.com/LuaLS/lua-language-server/releases/tag/3.18.2) +52. [StyLua](https://github.com/JohnnyMorganz/StyLua) +53. [StyLua v2.5.2](https://github.com/JohnnyMorganz/StyLua/releases/tag/v2.5.2) +54. [Luacheck](https://github.com/lunarmodules/luacheck) +55. [Luacheck v1.2.0](https://github.com/lunarmodules/luacheck/releases/tag/v1.2.0) +56. [Luacheck hosted docs](https://luacheck.readthedocs.io/en/stable/) +57. [Selene](https://kampfkarren.github.io/selene/) +58. [Selene 0.31.0](https://github.com/Kampfkarren/selene/releases/tag/0.31.0) +59. [Busted](https://lunarmodules.github.io/busted/) +60. [Busted v2.3.0](https://github.com/lunarmodules/busted/releases/tag/v2.3.0) +61. [LuaUnit](https://luaunit.readthedocs.io/en/latest/) +62. [LuaUnit LUAUNIT_V3_5](https://github.com/bluebird75/luaunit/releases/tag/LUAUNIT_V3_5) +63. [Teal](https://teal-language.org/book/) +64. [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) +65. [cmp-nvim-lsp](https://github.com/hrsh7th/cmp-nvim-lsp) +66. [lazy.nvim](https://github.com/folke/lazy.nvim) +67. [rocks.nvim](https://github.com/lumen-oss/rocks.nvim) +68. [Plenary](https://github.com/nvim-lua/plenary.nvim) diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/references/tooling-and-security.md b/packages/dotfiles/dot_agents/skills/lua-helper/references/tooling-and-security.md new file mode 100644 index 0000000000..903600768d --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/lua-helper/references/tooling-and-security.md @@ -0,0 +1,69 @@ +# Lua tooling and security + +Read this when configuring LuaLS, formatting, linting, tests, LuaRocks, plugin managers, or code-loading and shell trust boundaries. + +## LuaLS + +LuaLS supports `.luarc.json` / `.luarc.jsonc`, structured annotations, diagnostics, workspace libraries, addons, and stricter checking modes. + +Use addons for host libraries when available. Built-in addons are planned for removal, so treat addon selection as external project configuration. Loading the complete Neovim runtime into `workspace.library` is valid but broad. + +Annotations improve tooling; they do not validate data at runtime. Teal is a separate statically typed Lua dialect, not another name for LuaLS annotations. + +## Format, lint, and test + +- StyLua: formatter with check mode. +- Luacheck: maintained, but its hosted “stable” docs can lag the GitHub release. +- Selene: configurable static analyzer. +- Busted: behavior-driven test framework. +- LuaUnit: lightweight xUnit tests and current Lua 5.5 support. +- Plenary: common Neovim utilities/testing dependency; use when host integration is necessary. + +Pin tool releases in reproducible environments. Do not infer current versions from a stale hosted-doc banner. + +## LuaRocks + +Installing a rock downloads and executes package/build logic. Review the rockspec, source, native compilation, and transitive dependencies. Pin versions and verify the repository/index policy. + +`rocks.nvim` moved from `nvim-neorocks` to `lumen-oss`; follow the current repository before writing ownership or install guidance. + +## Neovim plugins + +nvim-lspconfig remains active. `cmp-nvim-lsp` capabilities belong to nvim-cmp integrations, not every LSP client. lazy.nvim remains an active plugin manager; installation and update execute third-party code. + +## Shell and code loading + +- Prefer argv-based host subprocess APIs. +- Never concatenate untrusted input into `os.execute`, `vim.fn.system`, or a shell string. +- Accept untrusted Lua only as text with a constrained environment where supported. +- Do not load untrusted binary chunks. +- Treat LuaJIT FFI as native code access. + +## Primary documentation + +- [LuaRocks](https://luarocks.org/) +- [LuaRocks v3.13.0](https://github.com/luarocks/luarocks/releases/tag/v3.13.0) +- [LuaLS configuration](https://luals.github.io/wiki/configuration/) +- [LuaLS annotations](https://luals.github.io/wiki/annotations/) +- [LuaLS diagnostics](https://luals.github.io/wiki/diagnostics/) +- [LuaLS addons](https://luals.github.io/wiki/addons/) +- [LuaLS workspace library](https://luals.github.io/wiki/settings/#workspace-library) +- [LuaLS type checking](https://luals.github.io/wiki/type-checking/) +- [LuaLS 3.18.2](https://github.com/LuaLS/lua-language-server/releases/tag/3.18.2) +- [StyLua](https://github.com/JohnnyMorganz/StyLua) +- [StyLua v2.5.2](https://github.com/JohnnyMorganz/StyLua/releases/tag/v2.5.2) +- [Luacheck](https://github.com/lunarmodules/luacheck) +- [Luacheck v1.2.0](https://github.com/lunarmodules/luacheck/releases/tag/v1.2.0) +- [Luacheck hosted docs](https://luacheck.readthedocs.io/en/stable/) +- [Selene](https://kampfkarren.github.io/selene/) +- [Selene 0.31.0](https://github.com/Kampfkarren/selene/releases/tag/0.31.0) +- [Busted](https://lunarmodules.github.io/busted/) +- [Busted v2.3.0](https://github.com/lunarmodules/busted/releases/tag/v2.3.0) +- [LuaUnit](https://luaunit.readthedocs.io/en/latest/) +- [LuaUnit LUAUNIT_V3_5](https://github.com/bluebird75/luaunit/releases/tag/LUAUNIT_V3_5) +- [Teal](https://teal-language.org/book/) +- [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) +- [cmp-nvim-lsp](https://github.com/hrsh7th/cmp-nvim-lsp) +- [lazy.nvim](https://github.com/folke/lazy.nvim) +- [rocks.nvim](https://github.com/lumen-oss/rocks.nvim) +- [Plenary](https://github.com/nvim-lua/plenary.nvim) diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/references/wezterm-config.md b/packages/dotfiles/dot_agents/skills/lua-helper/references/wezterm-config.md deleted file mode 100644 index 6798f19c88..0000000000 --- a/packages/dotfiles/dot_agents/skills/lua-helper/references/wezterm-config.md +++ /dev/null @@ -1,554 +0,0 @@ -# WezTerm Lua Configuration Reference - -WezTerm uses Lua 5.4 as its configuration language. Configuration auto-reloads on save. - -## Configuration File Locations - -``` -~/.wezterm.lua -- primary -~/.config/wezterm/wezterm.lua -- XDG alternative -$XDG_CONFIG_HOME/wezterm/wezterm.lua -- XDG explicit -``` - -## Basic Configuration - -```lua -local wezterm = require 'wezterm' -local config = wezterm.config_builder() - --- Font -config.font = wezterm.font 'JetBrains Mono' -config.font_size = 14.0 -config.line_height = 1.2 - --- Font with fallbacks -config.font = wezterm.font_with_fallback { - 'JetBrains Mono', - 'Symbols Nerd Font Mono', - 'Apple Color Emoji', -} - --- Font rules for italic/bold variants -config.font_rules = { - { - italic = true, - font = wezterm.font('JetBrains Mono', { italic = true }), - }, - { - intensity = 'Bold', - font = wezterm.font('JetBrains Mono', { weight = 'Bold' }), - }, -} - -return config -``` - -## Appearance - -```lua --- Color scheme -config.color_scheme = 'Catppuccin Mocha' - --- Custom colors -config.colors = { - foreground = '#cdd6f4', - background = '#1e1e2e', - cursor_bg = '#f5e0dc', - cursor_fg = '#1e1e2e', - cursor_border = '#f5e0dc', - selection_fg = '#1e1e2e', - selection_bg = '#f5e0dc', - ansi = { '#45475a', '#f38ba8', '#a6e3a1', '#f9e2af', '#89b4fa', '#f5c2e7', '#94e2d5', '#bac2de' }, - brights = { '#585b70', '#f38ba8', '#a6e3a1', '#f9e2af', '#89b4fa', '#f5c2e7', '#94e2d5', '#a6adc8' }, - tab_bar = { - background = '#11111b', - active_tab = { bg_color = '#1e1e2e', fg_color = '#cdd6f4' }, - inactive_tab = { bg_color = '#181825', fg_color = '#6c7086' }, - inactive_tab_hover = { bg_color = '#1e1e2e', fg_color = '#cdd6f4' }, - new_tab = { bg_color = '#11111b', fg_color = '#6c7086' }, - new_tab_hover = { bg_color = '#1e1e2e', fg_color = '#cdd6f4' }, - }, -} - --- Window -config.window_decorations = 'RESIZE' -- 'FULL', 'NONE', 'TITLE', 'RESIZE', 'TITLE|RESIZE' -config.window_padding = { left = 12, right = 12, top = 12, bottom = 12 } -config.window_background_opacity = 0.95 -config.macos_window_background_blur = 20 -config.initial_cols = 120 -config.initial_rows = 35 - --- Window background image/gradient -config.window_background_gradient = { - orientation = 'Vertical', - colors = { '#1e1e2e', '#11111b' }, - interpolation = 'Linear', - blend = 'Rgb', -} - --- Tab bar -config.enable_tab_bar = true -config.use_fancy_tab_bar = false -config.tab_bar_at_bottom = true -config.hide_tab_bar_if_only_one_tab = true -config.tab_max_width = 32 -config.show_tab_index_in_tab_bar = true -config.switch_to_last_active_tab_when_closing_tab = true - --- Cursor -config.default_cursor_style = 'BlinkingBar' -- 'SteadyBlock', 'BlinkingBlock', 'SteadyUnderline', 'BlinkingUnderline', 'SteadyBar', 'BlinkingBar' -config.cursor_blink_rate = 500 -config.force_reverse_video_cursor = false - --- Scrollback -config.scrollback_lines = 10000 -config.enable_scroll_bar = false -``` - -## Keybindings - -```lua -local act = wezterm.action - -config.keys = { - -- Pane management - { key = 'd', mods = 'SUPER', action = act.SplitHorizontal { domain = 'CurrentPaneDomain' } }, - { key = 'd', mods = 'SUPER|SHIFT', action = act.SplitVertical { domain = 'CurrentPaneDomain' } }, - { key = 'w', mods = 'SUPER', action = act.CloseCurrentPane { confirm = true } }, - { key = 'z', mods = 'SUPER|SHIFT', action = act.TogglePaneZoomState }, - - -- Pane navigation - { key = 'h', mods = 'SUPER|SHIFT', action = act.ActivatePaneDirection 'Left' }, - { key = 'j', mods = 'SUPER|SHIFT', action = act.ActivatePaneDirection 'Down' }, - { key = 'k', mods = 'SUPER|SHIFT', action = act.ActivatePaneDirection 'Up' }, - { key = 'l', mods = 'SUPER|SHIFT', action = act.ActivatePaneDirection 'Right' }, - - -- Pane resize - { key = 'H', mods = 'SUPER|SHIFT|CTRL', action = act.AdjustPaneSize { 'Left', 5 } }, - { key = 'J', mods = 'SUPER|SHIFT|CTRL', action = act.AdjustPaneSize { 'Down', 5 } }, - { key = 'K', mods = 'SUPER|SHIFT|CTRL', action = act.AdjustPaneSize { 'Up', 5 } }, - { key = 'L', mods = 'SUPER|SHIFT|CTRL', action = act.AdjustPaneSize { 'Right', 5 } }, - - -- Tab management - { key = 't', mods = 'SUPER', action = act.SpawnTab 'CurrentPaneDomain' }, - { key = '1', mods = 'SUPER', action = act.ActivateTab(0) }, - { key = '2', mods = 'SUPER', action = act.ActivateTab(1) }, - { key = '3', mods = 'SUPER', action = act.ActivateTab(2) }, - { key = '9', mods = 'SUPER', action = act.ActivateTab(-1) }, -- last tab - - -- Scrolling - { key = 'k', mods = 'SUPER', action = act.ClearScrollback 'ScrollbackAndViewport' }, - { key = 'u', mods = 'SUPER', action = act.ScrollByPage(-0.5) }, - { key = 'd', mods = 'CTRL', action = act.ScrollByPage(0.5) }, - - -- Utility - { key = 'f', mods = 'SUPER', action = act.ToggleFullScreen }, - { key = 'p', mods = 'SUPER', action = act.ActivateCommandPalette }, - { key = 'l', mods = 'SUPER', action = act.ShowLauncher }, - { key = 'Space', mods = 'SUPER|SHIFT', action = act.QuickSelect }, - { key = '/', mods = 'SUPER', action = act.Search 'CurrentSelectionOrEmptyString' }, - - -- Copy mode - { key = 'x', mods = 'SUPER|SHIFT', action = act.ActivateCopyMode }, - - -- Send key through (when WezTerm captures it) - { key = 'Enter', mods = 'ALT', action = act.SendKey { key = 'Enter', mods = 'ALT' } }, - - -- Disable default binding - { key = 'n', mods = 'SUPER', action = act.DisableDefaultAssignment }, -} - --- Mouse bindings -config.mouse_bindings = { - -- Cmd-click to open hyperlinks - { - event = { Up = { streak = 1, button = 'Left' } }, - mods = 'SUPER', - action = act.OpenLinkAtMouseCursor, - }, - -- Right-click paste - { - event = { Down = { streak = 1, button = 'Right' } }, - mods = 'NONE', - action = act.PasteFrom 'Clipboard', - }, -} -``` - -## Key Tables (Modal Keybinding) - -```lua -config.key_tables = { - resize_pane = { - { key = 'h', action = act.AdjustPaneSize { 'Left', 2 } }, - { key = 'j', action = act.AdjustPaneSize { 'Down', 2 } }, - { key = 'k', action = act.AdjustPaneSize { 'Up', 2 } }, - { key = 'l', action = act.AdjustPaneSize { 'Right', 2 } }, - { key = 'Escape', action = 'PopKeyTable' }, - { key = 'q', action = 'PopKeyTable' }, - }, -} - --- Activate key table from main keys -config.keys = { - { key = 'r', mods = 'LEADER', action = act.ActivateKeyTable { - name = 'resize_pane', - one_shot = false, -- stay in table until Escape - timeout_milliseconds = 3000, - }}, -} - --- Leader key (like tmux prefix) -config.leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1000 } -``` - -## Event System - -### Predefined Events - -```lua --- Startup -wezterm.on('gui-startup', function(cmd) - local tab, pane, window = wezterm.mux.spawn_window(cmd or {}) - window:gui_window():maximize() -end) - --- Attached to mux (reconnection) -wezterm.on('gui-attached', function(domain) - local workspace = wezterm.mux.get_active_workspace() - wezterm.log_info('Attached to ' .. workspace) -end) - --- Config reload -wezterm.on('window-config-reloaded', function(window, pane) - window:toast_notification('wezterm', 'Config reloaded', nil, 2000) -end) - --- Tab title formatting -wezterm.on('format-tab-title', function(tab, tabs, panes, config, hover, max_width) - local title = tab.active_pane.title - local index = tab.tab_index + 1 - - if tab.is_active then - return { - { Background = { Color = '#1e1e2e' } }, - { Foreground = { Color = '#89b4fa' } }, - { Text = ' ' .. index .. ': ' .. title .. ' ' }, - } - end - return ' ' .. index .. ': ' .. title .. ' ' -end) - --- Window title -wezterm.on('format-window-title', function(tab, pane, tabs, panes, config) - return pane.title .. ' - WezTerm' -end) - --- Status bar (right side) -wezterm.on('update-right-status', function(window, pane) - local date = wezterm.strftime '%H:%M' - local workspace = window:active_workspace() - - window:set_right_status(wezterm.format { - { Foreground = { Color = '#89b4fa' } }, - { Text = workspace .. ' ' .. date .. ' ' }, - }) -end) - --- Status bar (left side) -wezterm.on('update-status', function(window, pane) - local mode = window:active_key_table() - if mode then - window:set_left_status(' ' .. mode .. ' ') - else - window:set_left_status('') - end -end) - --- Bell notification -wezterm.on('bell', function(window, pane) - wezterm.log_info('Bell in pane ' .. pane:pane_id()) -end) - --- User variable change (set from shell via OSC) -wezterm.on('user-var-changed', function(window, pane, name, value) - if name == 'CURRENT_DIR' then - -- React to directory changes - end -end) -``` - -### Custom Events - -```lua --- Register custom event handler -wezterm.on('my-custom-event', function(window, pane) - window:perform_action(act.SendString 'hello', pane) -end) - --- Trigger from keybinding -config.keys = { - { key = 'e', mods = 'SUPER', action = act.EmitEvent 'my-custom-event' }, -} - --- Inline callback (action_callback helper) -config.keys = { - { - key = 'i', - mods = 'SUPER', - action = wezterm.action_callback(function(window, pane) - local info = pane:get_foreground_process_info() - wezterm.log_info('Process: ' .. (info and info.name or 'unknown')) - end), - }, -} -``` - -### Event Return Values - -Returning `false` from a callback prevents subsequent callbacks for that event from firing. This enables priority-based event handling. - -## Multiplexing Domains - -### Unix Domain (Local Multiplexer) - -```lua -config.unix_domains = { - { - name = 'unix', - -- socket_path = '/tmp/wezterm-mux', -- optional custom path - }, -} - --- Auto-connect on startup -config.default_gui_startup_args = { 'connect', 'unix' } -``` - -### SSH Domains - -```lua -config.ssh_domains = { - { - name = 'dev-server', - remote_address = 'dev.example.com:22', - username = 'deploy', - -- remote_wezterm_path = '/usr/local/bin/wezterm', -- if not in PATH - -- multiplexing = 'WezTerm', -- default; requires wezterm on remote - -- multiplexing = 'None', -- no mux, single pane - -- ssh_option = { identityfile = '~/.ssh/id_ed25519' }, - }, -} -``` - -### WSL Domains (Windows) - -```lua -config.wsl_domains = { - { - name = 'WSL:Ubuntu', - distribution = 'Ubuntu', - default_cwd = '~', - }, -} -``` - -### Workspaces - -```lua --- Switch workspace -config.keys = { - { key = 's', mods = 'LEADER', action = act.ShowLauncherArgs { flags = 'WORKSPACES' } }, - { - key = 'n', - mods = 'LEADER', - action = act.PromptInputLine { - description = 'Enter workspace name:', - action = wezterm.action_callback(function(window, pane, line) - if line then - window:perform_action(act.SwitchToWorkspace { name = line }, pane) - end - end), - }, - }, -} - --- Startup with specific workspace -wezterm.on('gui-startup', function() - local project_dir = wezterm.home_dir .. '/projects/myapp' - - local tab, build_pane, window = wezterm.mux.spawn_window { - workspace = 'coding', - cwd = project_dir, - } - local edit_pane = build_pane:split { - direction = 'Top', - size = 0.7, - cwd = project_dir, - } - - wezterm.mux.spawn_window { - workspace = 'monitoring', - args = { 'htop' }, - } - - wezterm.mux.set_active_workspace 'coding' -end) -``` - -## Modular Configuration - -### Helper Module Pattern - -```lua --- ~/.config/wezterm/keys.lua -local wezterm = require 'wezterm' -local act = wezterm.action - -local M = {} - -function M.apply_to_config(config) - config.leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1000 } - config.keys = { - { key = 'd', mods = 'LEADER', action = act.SplitHorizontal { domain = 'CurrentPaneDomain' } }, - -- more keys... - } -end - -return M - --- ~/.config/wezterm/appearance.lua -local M = {} - -function M.apply_to_config(config) - config.color_scheme = 'Catppuccin Mocha' - config.font_size = 14 - -- more appearance settings... -end - -return M - --- ~/.config/wezterm/wezterm.lua -local wezterm = require 'wezterm' -local config = wezterm.config_builder() - -require('keys').apply_to_config(config) -require('appearance').apply_to_config(config) - -return config -``` - -### Platform-Specific Configuration - -```lua -local is_macos = wezterm.target_triple:find('darwin') ~= nil -local is_linux = wezterm.target_triple:find('linux') ~= nil -local is_windows = wezterm.target_triple:find('windows') ~= nil - -if is_macos then - config.font_size = 14 - config.window_decorations = 'RESIZE' - config.send_composed_key_when_left_alt_is_pressed = true -elseif is_linux then - config.font_size = 12 - config.enable_wayland = true -end - --- Platform-specific default program -if is_windows then - config.default_prog = { 'pwsh.exe' } -end -``` - -## Useful wezterm Module Functions - -```lua --- Logging -wezterm.log_info('message') -wezterm.log_warn('warning') -wezterm.log_error('error') - --- Time formatting -wezterm.strftime '%Y-%m-%d %H:%M:%S' - --- Home directory -wezterm.home_dir - --- Config directory -wezterm.config_dir - --- Hostname -wezterm.hostname() - --- Running processes -wezterm.procinfo.pid() - --- Color manipulation -local color = wezterm.color.parse('#89b4fa') -local lighter = color:lighten(0.2) -local darker = color:darken(0.2) -local saturated = color:saturate(0.3) -local complement = color:complement() -local h, s, l, a = color:hsla() - --- Nerd font glyphs -wezterm.nerdfonts.fa_code_fork -- access nerd font icon names - --- JSON -local data = wezterm.json_parse(json_string) -local json = wezterm.json_encode(table) - --- Running shell commands -local success, stdout, stderr = wezterm.run_child_process { 'ls', '-la' } -``` - -## Launch Menu - -```lua -config.launch_menu = { - { label = 'Bash', args = { 'bash', '-l' } }, - { label = 'Fish', args = { 'fish', '-l' } }, - { label = 'Htop', args = { 'htop' } }, - { label = 'SSH Dev', args = { 'ssh', 'dev.example.com' } }, -} - --- Dynamic launch menu based on platform -if is_macos then - table.insert(config.launch_menu, { - label = 'Homebrew Update', - args = { 'brew', 'update' }, - }) -end -``` - -## Hyperlink Rules - -```lua --- Add custom hyperlink patterns (clickable links) -config.hyperlink_rules = wezterm.default_hyperlink_rules() - --- Add Jira ticket pattern -table.insert(config.hyperlink_rules, { - regex = [[\b(PROJ-\d+)\b]], - format = 'https://jira.example.com/browse/$1', -}) - --- Add GitHub issue pattern -table.insert(config.hyperlink_rules, { - regex = [[\b(\w+/\w+)#(\d+)\b]], - format = 'https://github.com/$1/issues/$2', -}) -``` - -## Quick Select Patterns - -```lua --- Add patterns for quick selection (Ctrl+Shift+Space) -config.quick_select_patterns = { - -- UUID - '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', - -- IP address - '\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}', - -- Docker container ID - '[0-9a-f]{12,}', -} -``` diff --git a/packages/dotfiles/dot_agents/skills/lua-helper/references/wezterm.md b/packages/dotfiles/dot_agents/skills/lua-helper/references/wezterm.md new file mode 100644 index 0000000000..db661a19f0 --- /dev/null +++ b/packages/dotfiles/dot_agents/skills/lua-helper/references/wezterm.md @@ -0,0 +1,67 @@ +# WezTerm Lua configuration + +Read this when writing WezTerm config, events, key actions, subprocesses, multiplexing, SSH domains, or platform-specific settings. + +## Evaluation and precedence + +WezTerm evaluates Lua 5.4 configuration and may evaluate it repeatedly. Top-level code must be idempotent and should not spawn processes, mutate external files, or perform other repeated side effects. + +Config precedence includes `--config-file` and `WEZTERM_CONFIG_FILE` before standard paths. Diagnose the selected file rather than assuming `~/.wezterm.lua`. + +## Strict configuration + +```lua +local wezterm = require("wezterm") +local config = wezterm.config_builder() +config:set_strict_mode(true) + +return config +``` + +Strict mode turns invalid option names and values into failures. + +## Events and actions + +`wezterm.on` registers callbacks. Returning `false` stops subsequent callbacks and the default action. `wezterm.action_callback` creates key-action callbacks, and `window:perform_action` dispatches actions programmatically. + +## Processes + +```lua +local success, stdout, stderr = wezterm.run_child_process({ "git", "status", "--short" }) +if not success then + error(stderr) +end +``` + +Never ignore the success boolean or construct a shell command from untrusted input. + +## Multiplexing + +Unix domains provide local mux connectivity. SSH domains can use the WezTerm mux server; remote mux requires WezTerm on the remote host. `default_gui_startup_args = { 'connect', 'unix' }` remains supported. + +Keep SSH secrets out of config. `ssh_option.identityfile` can point to a protected key, while an SSH agent is preferable where the environment supports it. + +## Version gates + +The current stable binary remains from 2024 while online documentation includes newer APIs. Check each page's “Since” annotation and the installed `wezterm --version` before adopting it. + +## Primary documentation + +- [Configuration files](https://wezterm.org/config/files.html) +- [Lua overview](https://wezterm.org/config/lua/general.html) +- [config_builder](https://wezterm.org/config/lua/wezterm/config_builder.html) +- [wezterm.on](https://wezterm.org/config/lua/wezterm/on.html) +- [action_callback](https://wezterm.org/config/lua/wezterm/action_callback.html) +- [run_child_process](https://wezterm.org/config/lua/wezterm/run_child_process.html) +- [mux.spawn_window](https://wezterm.org/config/lua/wezterm.mux/spawn_window.html) +- [window.perform_action](https://wezterm.org/config/lua/window/perform_action.html) +- [SSH domains](https://wezterm.org/config/lua/config/ssh_domains.html) +- [Unix domains](https://wezterm.org/config/lua/config/unix_domains.html) +- [Default GUI startup arguments](https://wezterm.org/config/lua/config/default_gui_startup_args.html) +- [Multiplexing](https://wezterm.org/multiplexing.html) +- [Changelog](https://wezterm.org/changelog.html) +- [json_parse](https://wezterm.org/config/lua/wezterm/json_parse.html) +- [target_triple](https://wezterm.org/config/lua/wezterm/target_triple.html) +- [SshDomain](https://wezterm.org/config/lua/SshDomain.html) +- [procinfo.pid](https://wezterm.org/config/lua/wezterm.procinfo/pid.html) +- [Latest WezTerm release 20240203-110809-5046fc22](https://github.com/wezterm/wezterm/releases/tag/20240203-110809-5046fc22)