diff --git a/.claude/skills/sofmani/SKILL.md b/.claude/skills/sofmani/SKILL.md new file mode 100644 index 00000000..efd8f5ac --- /dev/null +++ b/.claude/skills/sofmani/SKILL.md @@ -0,0 +1,306 @@ +--- +name: sofmani +description: Add, edit, or manage sofmani installer entries in sofmani.yml. This skill should be used when the user wants to add software to their provisioning manifest, modify existing installer steps, create cross-platform install groups, or troubleshoot sofmani configuration. Triggers on requests involving sofmani.yml, adding packages/tools to the manifest, or provisioning setup. +--- + +# Sofmani + +Work with [sofmani](https://github.com/chenasraf/sofmani) (Software Manifest) — a declarative provisioning tool that automates software installations via YAML config. The config file lives at `~/.dotfiles/.config/sofmani.yml`. + +## Quick Reference + +### CLI Flags + +| Flag | Purpose | +|------|---------| +| `-u` / `-U` | Enable/disable update checking | +| `-d` / `-D` | Enable/disable debug mode | +| `-s` / `-S` | Enable/disable summary | +| `-f ` | Filter installers (see below) | +| `-m` | Show machine ID | + +Filter syntax: `-f `, `-f tag:`, `-f type:`. Negate with `!`: `-f "!tag:system"`. + +### Template Variables + +Available in shell commands and `opts` string fields: + +| Variable | Value | +|----------|-------| +| `{{ .Arch }}` | CPU architecture | +| `{{ .ArchAlias }}` | Architecture alias (e.g. `amd64`) | +| `{{ .OS }}` | Operating system | +| `{{ .DeviceID }}` | Machine unique ID | +| `{{ .DeviceIDAlias }}` | Friendly machine name from `machine_aliases` | +| `{{ .Tag }}` | Release tag (github-release only) | +| `{{ .Version }}` | Version without `v` prefix (github-release only) | + +Environment variables `$DEVICE_ID` and `$DEVICE_ID_ALIAS` are also injected into shell commands. + +## Installer Entry Reference + +Every entry in the `install` array supports these fields: + +| Field | Type | Purpose | +|-------|------|---------| +| `name` | string (required) | Step identifier | +| `type` | string (required) | Installer type | +| `tags` | string | Space-separated tags for filtering | +| `bin_name` | string | Binary name if different from `name` | +| `enabled` | bool/string | Conditional execution (string = shell command) | +| `platforms` | object | `only` / `except` arrays: `macos`, `linux`, `windows` | +| `machines` | object | `only` / `except` arrays of machine alias names | +| `check_installed` | string | Shell command to verify installation | +| `check_has_update` | string | Shell command to check for updates | +| `pre_install` | string | Shell hook before install | +| `post_install` | string | Shell hook after install | +| `pre_update` | string | Shell hook before update | +| `post_update` | string | Shell hook after update | +| `skip_summary` | bool/object | Exclude from summary (`true`, or `{ install: true, update: true }`) | +| `env_shell` | object | Platform-specific shell override (e.g. `{ linux: /bin/bash }`) | +| `opts` | object | Type-specific options (see below) | + +## Installer Types + +### brew + +Homebrew package. The project defaults restrict brew to macOS only — no need to add `platforms` for brew entries. + +```yaml +- name: ripgrep + type: brew +``` + +With tap: + +```yaml +- name: sofmani + type: brew + opts: + tap: chenasraf/tap +``` + +opts: `tap`, `cask` (bool). + +### group + +Sequence multiple steps. The primary pattern for cross-platform installs: brew on macOS + github-release or apt on Linux. + +```yaml +- name: lazygit + type: group + steps: + - name: lazygit + type: brew + - name: lazygit + type: github-release + platforms: + only: ['linux'] + opts: + repository: jesseduffield/lazygit + strategy: tar + destination: ~/.local/bin + download_filename: lazygit_{{ .Version }}_Linux_{{ .ArchAlias }}.tar.gz +``` + +### shell + +Execute arbitrary shell commands. The most flexible type. + +```yaml +- name: git-config + type: shell + check_installed: git config --global user.name > /dev/null 2>&1 + opts: + command: | + git config --global user.name "Name" + git config --global user.email "email@example.com" + update_command: +``` + +opts: `command` (required), `update_command`. + +### git + +Clone a git repository. + +```yaml +- name: my-plugin + type: git + opts: + repository: https://github.com/user/repo.git + destination: ~/.local/share/plugins/repo + ref: main +``` + +opts: `repository` (required), `destination` (required), `ref`. + +GitHub shorthand: `repository: user/repo` expands to `https://github.com/user/repo.git`. + +### github-release + +Download a binary from GitHub releases. + +```yaml +- name: tool + type: github-release + opts: + repository: user/tool + destination: ~/.local/bin + strategy: tar # or: binary, zip + download_filename: tool_{{ .Version }}_Linux_{{ .ArchAlias }}.tar.gz +``` + +opts: `repository` (required), `destination` (required), `strategy` (`tar`/`binary`/`zip`), `download_filename` (supports template vars including `{{ .Version }}`, `{{ .Tag }}`, `{{ .Arch }}`, `{{ .ArchAlias }}`). + +### manifest + +Load an external sofmani config file (local or from a git repo). + +```yaml +- name: lazygit + type: manifest + opts: + source: git@github.com/chenasraf/sofmani.git + path: docs/recipes/lazygit.yml +``` + +opts: `source`, `path`. + +### apt + +Debian/Ubuntu package manager. + +```yaml +- name: stow + type: apt + platforms: + only: ['linux'] +``` + +### npm / pnpm / yarn + +Node.js package managers. Install global packages. + +```yaml +- name: typescript + type: pnpm + opts: + global: true +``` + +### pipx + +Python tool installer. + +```yaml +- name: black + type: pipx +``` + +### cargo + +Rust package installer. + +```yaml +- name: ripgrep + type: cargo +``` + +### rsync + +File synchronization. + +```yaml +- name: sync-config + type: rsync + opts: + source: ./config/ + destination: ~/.config/app/ +``` + +### docker + +Pull and optionally run containers. + +```yaml +- name: my-service + type: docker + opts: + image: nginx:latest +``` + +## Common Patterns + +### Cross-platform group (brew + github-release) + +The standard pattern for CLI tools. Brew handles macOS, github-release handles Linux: + +```yaml +- name: tool-name + type: group + steps: + - name: tool-name + type: brew + - name: tool-name + type: github-release + platforms: + only: ['linux'] + opts: + repository: owner/tool-name + destination: ~/.local/bin + strategy: tar + download_filename: tool-name-linux-{{ .Arch }}.tar.gz +``` + +### Cross-platform group (brew + apt) + +For packages available in both package managers: + +```yaml +- name: stow + type: group + steps: + - name: stow + type: brew + - name: stow + type: apt + platforms: + only: ['linux'] +``` + +### Config installer with idempotency + +Use `check_installed` and `check_has_update` for shell installers that manage config: + +```yaml +- name: tmux-config + type: shell + tags: config tmux + enabled: test -f "$HOME/.config/tmux_{{ .DeviceIDAlias }}.yml" + check_installed: test -f ~/.config/tmux_local.yml + check_has_update: '! diff -q "$HOME/.config/tmux_{{ .DeviceIDAlias }}.yml" ~/.config/tmux_local.yml > /dev/null 2>&1' + opts: + command: cp "$HOME/.config/tmux_{{ .DeviceIDAlias }}.yml" ~/.config/tmux_local.yml + update_command: cp "$HOME/.config/tmux_{{ .DeviceIDAlias }}.yml" ~/.config/tmux_local.yml +``` + +### Machine-specific installer + +Restrict to specific machines using aliases defined in `machine_aliases`: + +```yaml +- name: glab + type: brew + machines: + only: ['planck'] +``` + +## Working with the Config File + +- The config file is at `~/.dotfiles/.config/sofmani.yml` and symlinked to `~/.config/sofmani.yml` via stow. +- Read the full file before making changes to understand existing structure and conventions. +- New entries should be placed in the appropriate category section (marked by comment headers). +- Follow the existing indentation and style conventions in the file. +- When adding a new tool, check if a similar entry already exists that can be extended. +- For detailed installer type documentation, see `references/installer-types.md`. diff --git a/.claude/skills/sofmani/references/installer-types.md b/.claude/skills/sofmani/references/installer-types.md new file mode 100644 index 00000000..65391bba --- /dev/null +++ b/.claude/skills/sofmani/references/installer-types.md @@ -0,0 +1,148 @@ +# Sofmani Installer Types — Detailed Reference + +Full documentation for each installer type's `opts` fields, behavior, and edge cases. + +## shell + +Runs arbitrary shell commands. Most flexible type. + +**opts:** +- `command` (string, required): Shell command to run for installation +- `update_command` (string): Shell command for updates (defaults to `command` if omitted) + +**Behavior:** +- Uses the system shell (overridable with `env_shell`) +- Template variables are expanded in commands +- `$DEVICE_ID` and `$DEVICE_ID_ALIAS` env vars are injected + +## group + +Orchestrates a sequence of sub-installers. The group itself has no `opts`; configuration lives on each child step. + +**Fields:** +- `steps` (array, required): Array of installer entries (same schema as top-level `install`) +- `post_install` / `pre_install`: Hooks run before/after the entire group + +**Behavior:** +- Steps execute sequentially +- If any step fails, subsequent steps are skipped +- The group is considered "installed" if all steps report installed + +## git + +Clones a git repository. + +**opts:** +- `repository` (string, required): Git URL or GitHub shorthand (`user/repo`) +- `destination` (string, required): Local clone path +- `ref` (string): Branch, tag, or commit to check out + +**Behavior:** +- GitHub shorthand `user/repo` expands to `https://github.com/user/repo.git` +- If destination exists, performs `git pull` on update +- Supports template variables in all string opts + +## github-release + +Downloads assets from GitHub releases. + +**opts:** +- `repository` (string, required): GitHub `owner/repo` +- `destination` (string, required): Directory to extract/copy to +- `strategy` (string): `tar` (extract tar.gz), `binary` (direct binary), `zip` (extract zip) +- `download_filename` (string): Asset filename pattern with template variables +- `github_token` (string): GitHub API token for private repos / rate limits + +**Template variables available:** +- `{{ .Tag }}`: Full release tag (e.g. `v1.2.3`) +- `{{ .Version }}`: Tag without `v` prefix (e.g. `1.2.3`) +- `{{ .Arch }}`: Raw architecture string +- `{{ .ArchAlias }}`: Normalized architecture (e.g. `amd64`, `arm64`) +- `{{ .OS }}`: Operating system + +**Behavior:** +- Automatically finds the latest release +- Compares installed version to latest for update detection + +## manifest + +Loads and executes an external sofmani config file. + +**opts:** +- `source` (string): Git repository URL for remote manifests +- `path` (string): Path to the manifest file (relative to repo root for remote, or absolute/relative for local) + +**Behavior:** +- Remote manifests are cloned/cached locally +- The loaded manifest's `install` array is executed inline +- Useful for sharing common recipes across machines + +## rsync + +Synchronizes files/directories. + +**opts:** +- `source` (string, required): Source path +- `destination` (string, required): Destination path +- Additional rsync flags can be set + +**Behavior:** +- Uses rsync under the hood +- `verbose: true` in defaults enables `-v` flag + +## brew + +Homebrew package manager. + +**opts:** +- `tap` (string): Custom tap to install from (e.g. `chenasraf/tap`) +- `cask` (bool): Install as a cask instead of formula + +**Behavior:** +- The project's global defaults restrict brew to `platforms: { only: ['macos'] }` +- No need to add platform restriction on individual brew entries +- Taps are added automatically before install + +## npm / pnpm / yarn + +Node.js package managers. + +**opts:** +- `global` (bool): Install globally + +**Behavior:** +- Defaults to global installation in most configurations + +## apt + +Debian/Ubuntu package manager. + +No special opts. Always requires `platforms: { only: ['linux'] }` unless in a group that already restricts. + +## apk + +Alpine Linux package manager. Same behavior as apt. + +## pacman / yay + +Arch Linux package managers. Yay is an AUR helper that wraps pacman. + +## pipx + +Python application installer. Installs Python CLI tools in isolated environments. + +No special opts — uses the package `name` directly. + +## cargo + +Rust package installer. + +No special opts — uses the package `name` directly. + +## docker + +Docker container management. + +**opts:** +- `image` (string, required): Docker image to pull +- Additional run configuration as needed diff --git a/.claude/skills/wand/SKILL.md b/.claude/skills/wand/SKILL.md new file mode 100644 index 00000000..8ae9e458 --- /dev/null +++ b/.claude/skills/wand/SKILL.md @@ -0,0 +1,157 @@ +--- +name: wand +description: + Refactor shell functions and aliases into wand YAML configs. This skill should be used when the + user wants to extract shell functions, aliases, or scripts into a wand.yml command runner config, + create new wand configs, or add commands to existing wand configs. Triggers on requests like "move + these functions to wand", "create a wand config for X", "refactor this script into wand commands". +--- + +# Wand Refactor + +Extract shell functions and aliases into [wand](https://github.com/chenasraf/wand) YAML configs, +replacing inline logic with declarative command definitions and thin alias wrappers. + +## Wand Config Reference + +Wand is a YAML-driven command runner. Config files are auto-discovered from CWD upward, `~/`, and +`~/.config/`. A custom path can be specified via `--wand-file ` or `WAND_FILE=`. + +### Command Fields + +| Field | Type | Purpose | +| ----------------- | -------------------- | ------------------------------------ | +| `description` | `string` | Help text shown in `--help` | +| `cmd` | `string` | Shell command to execute | +| `children` | `map[string]Command` | Nested subcommands | +| `flags` | `map[string]Flag` | Custom typed flags | +| `env` | `map[string]string` | Environment variables | +| `working_dir` | `string` | Execution directory | +| `aliases` | `string[]` | Alternate command names | +| `confirm` | `bool` or `string` | Confirmation prompt before execution | +| `confirm_default` | `string` | Default answer for confirm | + +### Flag Fields + +| Field | Type | Purpose | +| ------------- | -------- | ------------------------------------------- | +| `alias` | `string` | Single-letter shorthand (e.g. `o` for `-o`) | +| `description` | `string` | Description shown in `--help` | +| `default` | `any` | Default value | +| `type` | `string` | `"bool"` for boolean flags, omit for string | + +Flag values are accessible as `$WAND_FLAG_` env vars (uppercased, hyphens become underscores). + +### Positional Arguments + +Extra CLI arguments are available as `$1`, `$2`, `$@` in the command's `cmd`. + +## Refactoring Process + +### Step 1: Analyze the Source + +Read the source file(s) containing the shell functions/aliases to refactor. Identify: + +- **Command groups**: Functions that share a common prefix or domain (e.g. `nc-dev-*`, `nc-aio-*`) +- **Shared state**: Variables, config paths, or logic used across multiple functions +- **Modal behavior**: Functions that differ only by a mode/target (e.g. dev vs aio) — these become a + single command with a flag +- **Subcommand hierarchies**: Related commands that naturally nest (e.g. + `db-proxy start`/`db-proxy stop`) + +### Step 2: Design the Wand Config + +Map the analyzed functions to wand commands following these principles: + +1. **Merge modal variants into flags**: If two functions differ only by target (e.g. `nc-dev-occ` vs + `nc-aio-occ`), create one command with a `--` flag (default to the more common mode). +2. **Use `children` for related pairs**: Commands that are natural opposites (start/stop, + enable/disable, backup/restore) belong as children of a parent command. +3. **Use `env` for shared config**: Constants like paths, container names, etc. go in the `env` + field rather than hardcoded in `cmd`. +4. **Use `working_dir`** instead of `pushd`/`popd` or `cd`. +5. **Use `confirm`** for destructive or long-running operations. +6. **Keep commands self-contained**: Each command's `cmd` must be independently runnable — do not + call other wand commands or rely on shell aliases being available. +7. **Add `set -euo pipefail`** at the top of multi-line commands that should fail fast. +8. **Use `aliases`** for common shorthand names within wand itself. + +### Step 3: Choose Config File Location + +- If adding to the existing global wand config: edit `~/.dotfiles/.config/wand.yml` +- If creating a domain-specific config (preferred for large command sets): create + `~/.dotfiles/.config/wand/.yml` and define an alias: + `alias ="wand --wand-file \$HOME/.config/wand/.yml"` + +After creating or modifying a config file in `~/.dotfiles/.config/`, run +`stow -v -d $DOTFILES -t ~ .` to symlink it. + +### Step 4: Create Aliases + +Replace the original shell file with thin aliases that point to wand commands. This preserves +backward compatibility with existing muscle memory. + +Alias conventions: + +- Define a **base alias** for the wand config (e.g. + `alias nxc="wand --wand-file $HOME/.config/wand/nextcloud.yml"`) +- Map each old function/alias to ` [--flags] [--]` +- Append `--` before positional args when the command has flags, to prevent flag/arg ambiguity +- Keep old alias names working so existing scripts and habits are preserved + +### Step 5: Clean Up + +- Remove the original shell functions from the source file, keeping only the alias definitions +- Remove any global variables that were only used by the extracted functions (they now live in `env` + fields) +- If the source file becomes aliases-only, consider whether it should stay as-is or merge into + `aliases.zsh` + +## Example: Before and After + +### Before (shell functions) + +```zsh +APP_DIR="$HOME/myapp" +my-build() { pushd $APP_DIR; make build; popd; } +my-test() { pushd $APP_DIR; make test; popd; } +my-deploy() { + echo "Deploying..." + pushd $APP_DIR; make deploy ENV=$1; popd +} +alias my-deploy-prod="my-deploy prod" +alias my-deploy-staging="my-deploy staging" +``` + +### After (wand.yml + aliases) + +```yaml +# ~/.dotfiles/.config/wand/myapp.yml +build: + description: Build the project + working_dir: ~/myapp + cmd: make build + +test: + description: Run tests + working_dir: ~/myapp + cmd: make test + +deploy: + description: Deploy the project + working_dir: ~/myapp + confirm: Deploy to $1? + cmd: | + echo "Deploying..." + make deploy ENV=$1 +``` + +```zsh +# aliases +alias myapp="wand --wand-file \$HOME/.config/wand/myapp.yml" +alias my-build="myapp build" +alias my-test="myapp test" +alias my-deploy="myapp deploy --" +alias my-deploy-prod="myapp deploy prod" +alias my-deploy-staging="myapp deploy staging" +```