dotfiles
Health Uyari
- No license — Repository has no license file
- No description — Repository has no description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Uyari
- process.env — Environment variable access in ai/claude/hud-ctx-fix.mjs
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
dotfiles
Personal macOS configuration for khoi. Clone to ~/.dotfiles — the path is
hardcoded in zsh/zshenv, so anywhere else will not work.
git clone https://github.com/khoi/dotfiles.git ~/.dotfiles
cd ~/.dotfiles
./setup.sh
Everything is macOS-only and idempotent: re-running any script is always safe.
Bootstrap
./setup.sh # default modules
./setup.sh --all # default + opt-in modules
./setup.sh vscode git # only the named modules, in the order given
./setup.sh --list # show what's available
setup.sh installs Homebrew if missing, links the shell dotfiles into $HOME,
then runs each module's setup.sh.
| Modules | |
|---|---|
| default | git brew mise macos gpg iterm ai misc |
| opt-in | vscode xcode terminal |
The opt-in ones need a GUI app that may not be installed, or only matter on
some machines.
How things get installed
Two mechanisms, depending on whether the app reads a config file.
Symlinks — the repo file is linked into $HOME or an app's support
directory, so edits in the repo are live immediately.
| Repo | Linked to |
|---|---|
zsh/zshenv, zsh/zshrc |
~/.zshenv, ~/.zshrc |
git/gitconfig |
~/.gitconfig |
git/ignore |
~/.config/git/ignore |
git/lazygit.yml |
lazygit's Application Support dir |
ssh/config |
~/.ssh/config |
curl/curlrc, tmux/tmux.conf, vim/vimrc |
~/.curlrc, ~/.tmux.conf, ~/.vimrc |
mise/global.toml |
~/.config/mise/config.toml |
ai/claude/* |
~/.claude/ |
ai/copilot/* |
~/.copilot/ |
ai/omp/*, ai/shared/skills, ai/shared/rules/common.md (as AGENTS.md) |
~/.omp/agent/ |
ai/shared/skills/* (incl. herdr-team, dispatchable-plan) |
~/.claude/skills/ |
ai/herdr/config.toml |
~/.config/herdr/config.toml |
vscode/{settings,keybindings}.json, vscode/snippets |
VS Code user dir |
ai/ is split by harness (claude/, omp/, copilot/), with what they
share in ai/shared/: rules/common.md, the global instructions every harness
loads, and skills/, Agent Skills that Claude Code and omp both read. Each
harness keeps its own model/provider and MCP config, since the formats differ.
Preference redirection — for apps with no dotfile.
macos/setup.shis a largedefaults writescript (mathiasbynens lineage;
timezoneAsia/Ho_Chi_Minh, forced Dark mode). Needs sudo, and restarts
Finder/Dock at the end.- iTerm2 is pointed at
iterm/via its "load preferences from a custom folder"
setting. iTerm writes the plist back there on quit, so commit from the repo. - Terminal.app imports its theme through
osascript. gpg/setup.shcopiesgpg.confand a renderedgpg-agent.conf(arch-correctpinentry-macpath) into~/.gnupg. Copied, not symlinked — gpg insists on a
real700directory. Keys andtrustdbare never versioned.git/template/is linked to~/.config/git/template(init.templateDir), so
every new clone gets apre-commithook that runsgitleakson staged
changes. Existing repos pick it up withgit init; the hook skips itself
when gitleaks is missing, and--no-verifybypasses it once.
It also installs apost-checkouthook for worktrees: whengit worktree addcreates one, each path listed in the main checkout's.worktreeclone(e.g.node_modules, generated code) is cloned into it
with APFS copy-on-write, so there is nothing to reinstall or regenerate.
Claude Code's--worktreeskips git hooks, so aSessionStarthook inai/claude/settings.jsonruns the same script. Don't list virtualenvs;
they embed absolute paths.git wta <branch>adds~/.worktrees/<repo>/<branch>— one place rather than scattered beside
whichever checkout spawned them, and deliberately not inside the repo, where
every tree walk from the root would see a full copy per worktree and agit clean -xdffwould delete them all. The repo name comes from--git-common-dir, sogit wtaworks the same from a subdirectory or from
inside another worktree.wtjumps between them with fzf,git wtrm <path>
removes one, andgit tidyprunes stale records.- Stats (menu bar monitor) has its prefs imported by
misc/stats/setup.shfrommisc/stats/eu.exelban.Stats.plist; re-dump withbash misc/stats/update.sh.
Shell
zshenv → zshrc, which sources in this order:
zsh/env.zsh—$PATHand toolchain environment, built once and
deduplicated withtypeset -U path.zsh/config.zsh— options, history, editor.zsh/omz.zsh— oh-my-zsh settings; must precede the plugin bundle.zsh/local/plugins.zsh— the antidote bundle.zsh/aliases.zsh,zsh/aliases.macos.zsh,zsh/functions.zsh— after
plugins, so these win.- mise, fzf, zoxide. Their init scripts are cached in
zsh/local/init-*.zsh,
keyed by binary path and rebuilt when the binary orzshrcchanges (delete
them to force a rebuild). The mise cache is also keyed on$PATH, theMISE_*variables and every mise config (and trust state) that applies to
the current directory, so it rebuilds itself when any of those change. A
miserc bypasses it, and a nested shell inside a project usually misses and
runsmise activatelive, as before. ai/aliases.zsh(which sourcesai/claude/providers.zsh) — after mise,
because the launcher cacheproviders.zshkeeps is regenerated by thepython3mise activation puts on$PATH.zsh/local/extra.zsh— machine-local, gitignored.
Plugins. Managed by antidote.zsh/zsh.plugins is the list; zsh/local/plugins.zsh is the generated static
bundle, rebuilt automatically whenever the list is newer. The bundle and every
plugin file it sources are zcompiled once after each regeneration (.zwc next
to the source, in the gitignored cache). After antidote update or a zsh
upgrade, or to force a rebuild:
touch zsh/zsh.plugins && exec zsh
Machine-local state. zsh/local/ is gitignored and holds the zsh, Python
and Node REPL histories, lesshst, the zcompdump, the generated bundle, andextra.zsh. Anything an installer wants to append to ~/.zshrc belongs inzsh/local/extra.zsh instead.
Startup time. Roughly 350 ms. Profile it with:
ZSH_PROFILE=1 zsh -i -c exit
oh-my-zsh.sh is bypassed: antidote sources omz lib/*.zsh and the theme
directly, and zshrc runs compinit -C, rebuilding the dump when it is over a
day old or older than the plugin bundle or env.zsh.
oh-my-pi
oh-my-pi (omp, a fork of pi) is installed from thecan1357/tap Homebrew formula, which ships arm64 and Intel builds and the zsh
completions. It runs DeepSeek V4.1 Flash (deepseek/deepseek-flash, in omp's
bundled catalog) for every role; for harder tasks, raise the thinking level
(/model, --thinking, or :max on a role) rather than change the model.ai/omp/models.yml only supplies the key: omp reads it from 1Password
(op read, the PI_CODING_AGENT field of the "DeepSeek API Keys" note in
Private) the first time a request needs it and caches it for the process.
Rotate it in 1Password and restart omp. The 1Password app must be unlocked
with CLI integration on, and omp gives op 10 seconds, so approve the prompt
promptly. --api-key overrides it.
ai/omp/ holds config.yml (settings), models.yml, mcp.json,APPEND_SYSTEM.md and the /commit, /pr and /explain commands. Plan
mode, todos, handoff, ask, subagents (task), web_search, /review and/ci-green are built in, as is the dark-gruvbox theme. /settings and/model write config.yml through the symlink; review the result withgit diff. Logins, sessions and the key store (agent.db) stay in~/.omp.
Guardrails. config.yml keeps the default yolo approval mode and addsbash.patterns: commands touching secret paths (~/.ssh, ~/.gnupg,~/.claude.json, ai/env.local.zsh, op read, gh auth token, …) are
denied, and recursive rm, sudo, installs, defaults write, running asetup.sh, force-push and history rewrites ask first. Those rules hold in
yolo mode, and subagents, which have no UI, are refused instead of asked.eval (Python/JS) always asks, since the patterns don't see its shells. This
is a guardrail against model mistakes, not a sandbox: the rules match the
command text only ($(…), variables and interpreter one-liners slip past),
and the read/grep tools are not restricted by path.
MCP. ai/omp/mcp.json defines GitHub, read-only and authenticated withgh auth token, and Context7. mcp.enableProjectConfig: false ignores
project MCP files (.mcp.json, .omp/mcp.json, .claude/, …), whose
commands a cloned repo controls. omp does still read other project config,
such as .omp/ settings, AGENTS.md and .claude/commands.
ompc continues the last session and ompr picks one to resume.
Claude Code on other providers
Claude Code can talk to any Anthropic-compatible API. ai/claude/providers.zsh
(sourced by ai/aliases.zsh) sets that up per process, so the Pro login and~/.claude/settings.json are never touched.
| Command | Runs |
|---|---|
cc / ccc / ccr |
Pro (new, --continue, --resume), with every provider variable cleared |
ccd / ccdc / ccdr |
DeepSeek V4.1 Flash, same three forms |
claude-deepseek |
Long form of ccd |
cc-providers |
List configured providers |
A provider is one cc_provider call at the bottom of the file:
cc_provider deepseek \
url=https://api.deepseek.com/anthropic \
key=op://…/PI_CODING_AGENT \
model=deepseek-flash label=DS short=ccd
url, key and model are required. key is an op:// reference (read
when the command starts; 1Password must be unlocked) or env:VAR. small
sets the Sonnet, Haiku and subagent model (default model), pro enables--pro, label is the status-line prefix (default: the name in capitals),short adds the X/Xc/Xr commands, and env.VAR=value sets any extra
variable the provider needs. It always generates claude-<name>. Check a new
provider's endpoint and model ids in its own docs first.
Each launch clears every variable any provider sets before exporting its own,
and refuses to start with an empty key: otherwise Claude Code falls back to
the Pro login and the provider answers "Authentication Fails". The status
line prefixes the model with the label (DS·deepseek-flash). --pro is only
recognised as the first argument.
Which to use. Pro costs quota, DeepSeek costs money but Flash is cheap. If
tests, CI or a quick diff read will catch a wrong answer, use DeepSeek:
executing a written plan, boilerplate, tests, lint and CI fixes, exploration,
docs. If a mistake would ship silently or shape later work, use Pro:
planning, architecture, API and schema design, security, pre-merge/code-review, and debugging after one failed DeepSeek attempt. Web
research goes to omp, since Claude Code's web search doesn't work on other
providers. A DeepSeek session sends the code it reads to DeepSeek, so deny.env reads in private repos first. The quota half of that decision is
automated: see the pro-quota.json cache below. The other half — whether a
task is big enough to be worth handing over at all — is not, and below the
break-even doing it inline on Pro costs less than the handover.
Switching mid-task. Hand off through the plan file, not the transcript:
plan with cc, then ccd "execute .omc/plans/<task>.md", then review withcc. Resuming across providers works in both directions — a Pro session
continued under DeepSeek and the reverse each answered cleanly, with no
rejected thinking block — but --continue picks the directory's latest
session whichever provider ran it, so prefer ccdr/ccr and pick the session
you meant. Only single-turn sessions have been tested; a long tool-heavy
transcript is untried.
herdr
herdr is the workspace manager the coding agents run in:
one workspace per repository, tabs for agents / dev server / tests, one pane per
agent, with a state sidebar showing which agent is working, blocked or done.
The tab level goes unused — navigation is already two-dimensional
(prefix+shift+N picks the repo, prefix+alt+N the agent row) — sohide_tab_bar_when_single_tab drops the row. A second tab brings it back.prefix is ctrl+b (tmux uses C-a, so the two don't collide). prefix+q
detaches and herdr reattaches — herdr server stop is different, it kills the
pane processes.
herdr owns agent work; tmux stays for plain shells and ssh. Never run two agents
in the same directory: herdr gives no file isolation, so use git wta <branch>
for parallel work and open the worktree as its own workspace.
A worktree is a workspace here, so New worktree (prefix+shift+g) creates a
checkout and a space every time it is pressed — a duplicate space when the
branch already has one. Open worktree... is the half that reuses: it lists the
repo's checkouts and focuses the space an open one already has. herdr ships it
unbound, so config.toml binds it to prefix+shift+o. The other half of the
duplication was the path: [worktrees] directory now points at ~/.worktrees,
herdr's own <repo>/<branch with / as -> layout below it being identical togit wta's, so both routes to a branch land on one checkout rather than two.
ai/herdr/config.toml is the only versioned part. ~/.config/herdr/*.log, the
socket, plugin binaries and plugin state are machine-local. ai/setup.sh
installs the claude, omp and copilot integrations, which report agent state
through hooks instead of screen-scraping and are what makes[session] resume_agents_on_restore work. They write intoai/claude/settings.json and ai/copilot/settings.json, which are symlinks into
this repo, so review git diff after running it.
Upgrade with brew upgrade herdr, never herdr update — Homebrew owns the
binary, and [update] version_check = false silences the nag. Re-runsh ai/setup.sh afterwards so the integrations migrate.
Plugins are listed in ai/herdr/plugins.list, one <plugin id> <owner/repo> <tag> per line, and installed by ai/setup.sh at that tag. The id
and the repo are unrelated — each plugin's herdr-plugin.toml declares its own
id, so persiyanov/herdr-reviewr is persiyanov.reviewr — and config-dir
wants the id while install wants the repo, which is why the list carries both.
Once a plugin is installed, the script links the versioned templates underai/herdr/plugins/<plugin id>/ into its herdr plugin config-dir; a plugin
with no template directory gets nothing linked. The pin is not decoration:herdr plugin has no update command, and an unpinned install re-fetches the
default branch, so re-running the script would otherwise move a plugin to
current HEAD.
First install still stops for the manifest preview — never --yes — because
plugins run unsandboxed as your user with your full environment; that is the
reason for the prompt, not for a manual install. To bump one, edit the tag inai/herdr/plugins.list, run herdr plugin uninstall <id>, then re-run the
script.
Installed today: herdr-plus (worktree layouts, project picker), reviewr (line
comments back to the agent) and usagebar (context, prompt-cache and
provider-limit meters in the sidebar, ctrl+shift+u for the limits pane). Its
sidebar rows and keybindings live in ai/herdr/config.toml rather than the
plugin's own config, and $provider, $limit, $cache_* and $context are
all used. A row is named by the built-in workspace and tab tokens, which
say different things — team.sh labels the workspace after the agent and the
tab after its branch — and they get a line each, because herdr joins tokens on
one row with · and truncates the tail, which on a branch is the part that
tells it apart. Under those, terminal_title_stripped: Claude Code keeps the terminal title as a live
summary of what it is doing, which is the only thing that tells two panes in
one tab apart. Then $provider · $limit, which is where the cc/ccd split
shows: $provider names the billing identity, the quota provider on a
subscription pane and the backend actually billed on a pay-as-you-go one, so accd pane reads deepseek · Σ 425k $0.04 against a cc pane'sclaude · 5h 60%. Then the $cache_* trio — the plugin sets exactly one of
high/mid/low by the session prompt-cache hit rate, so all three on one row
render a single value in the band's colour, gruvbox yellow below 80% and red
below 50%. Pane commands in the templates go through
this repo's wrappers (cc, ccd, omp) — notclaude --dangerously-skip-permissions as herdr-plus's README shows, which
unsets the provider environment and silently falls back to the Pro login.
Claude's 5h/7d rate-limit windows and its prompt-cache expiry are reported
nowhere but the statusLine payload, so ai/claude/statusline.sh (thestatusLine command in ai/claude/settings.json) tees that stdin tousagebar statusline before rendering the HUD. It is a side branch, not a
pipeline stage — usagebar statusline prints its own summary rather than
passing the JSON through — so the status line is byte-identical with or without
it, and the script falls back to the plain pipeline when the binary is missing.
The hardcoded plugin path is stable: herdr installs to<plugin id>-<first 12 hex of sha256(plugin id)>, which carries no version or
commit, so it survives reinstalls and tag bumps. Confirm it withherdr plugin list --plugin usagebar --json. This runs wherever Claude runs,
including outside herdr, but the meters themselves are sidebar rows: a
standalone session only keeps the cache warm for the next herdr pane.
The same payload feeds a second tee branch that caches just the two windows to~/.claude/cache/pro-quota.json, because the status line is rendered for you
and Claude never sees it — the model doing the routing would otherwise have no
idea the 5h window is nearly spent. ai/claude/quota-advice.sh, aUserPromptSubmit hook, reads that cache and prints a routing advisory, which
Claude Code injects as context. Account-wide on purpose: the limits are, so
whichever pane rendered last refreshes them for every other one. Three things
keep it honest — a ccd pane reports its own endpoint's limits, soCC_PROVIDER gates the write the same way ai/herdr/team.sh asserts on it; a
window whose resets_at has passed is treated as unknown rather than as
headroom; and the hook stays silent below 50%, since an advisory on every
prompt is noise by the time it matters. The HUD can show the same numbers to
you with omcHud.elements.rateLimits, which is off.
Diff and review. git diff pages through delta
(side-by-side, n/N between files), and git dft runs a structural
difftastic diff where a reindent or a moved
function reads as no change. delta is only used on a TTY, so a diff an agent
captures is still plain text. prefix+alt+g opens lazygit in a popup for staging
and committing; it ignores git's core.pager, so git/lazygit.yml configures
the same renderers again under git.diffRenderers and | cycles delta →
difftastic → --color-words. delta runs there with --features=lazygit, a[delta "lazygit"] block in git/gitconfig that drops navigate and
side-by-side for the narrow panel and turns on clickable line numbers. Stage
lines under delta, not difftastic — an external diff produces no patch for
lazygit to apply. herdr-reviewr is the review surface: mark lines, comment,s to send the comments into the agent's pane. The merge decision still goes
through /code-review and a signed PR — the AI review pass stays in its own
session, separate from the one that wrote the code.
Agent teams. A team is agents in panes, one git worktree each — not to be
confused with OMC's /team skill, which fans work out to in-process subagents
inside a single pane. Reach for /team first; a team costs more and buys
isolation.
ai/herdr/team.sh is the only thing that starts one, because herdr agent start execs the binary directly and drops what ai/claude/providers.zsh
exports — Claude Code then falls back to the Pro login silently. Every spawn
goes through zsh -ic <wrapper> and the provider is asserted afterwards.
ai/herdr/team.sh run new # mint a Run id
ai/herdr/team.sh spawn exec-1 --branch feat/x # worktree + workspace + agent
ai/herdr/team.sh dispatch exec-1 --task T-01 "…" # hand over the contract
ai/herdr/team.sh dispatch exec-1 --task T-01 --from-plan .omc/plans/x.md
ai/herdr/team.sh status # roster and pending handoffs
ai/herdr/team.sh collect [<run-id>] # outcomes from the handoffs
ai/herdr/team.sh collect --plan .omc/plans/x.md # per-task state, exit says what next
ai/herdr/team.sh wait [--timeout <ms>] # block until one dispatch settles
ai/herdr/team.sh surface exec-1 # what a blocked agent is asking
ai/herdr/team.sh plan lint .omc/plans/x.md # check a plan before dispatching
ai/herdr/team.sh settle <name> reuse|retain|release
ai/herdr/team.sh teardown <name> [--force]
ai/herdr/team.sh config show --sources # resolved settings, and their layers
What a role launches is a file, not a team.sh edit. ai/herdr/team.toml
holds the harnesses, the [profile.*] entries a role may launch, the [role.*]
roster (prefix, lifetime, cwd, max_per_run), the [[route]] table that places
a plan row, the fallback chains and the presets; the credentials behind those
profiles — url, key, protocol, launcher and the ceiling bounding how many
panes may spend one — are ai/providers.toml, their only definition.ai/herdr/team.local.toml layers over the shipped file for this machine. A Run
bound to another repo (run new --repo <path>) layers that project's own.config/herdr/team*.toml on top, gated on config trust — see theherdr-team skill for the details.config lint checks the result, config doctor reports the differences that
are not errors (this shell's provider against the orchestrator's role), andconfig show [--sources] prints the resolved keys and the layer each value came
from. HERDR_TEAM_CONFIG=<file> replaces every layer, which is the way back from
a local file that will not parse. A new provider is a [profile.*] plus a[[route]] here — ai/herdr/tests/run.sh does exactly that with a stub
launcher, and nothing in team.sh names it.
spawn opens the worktree with herdr worktree open, not workspace create,
so the agent's space is grouped under the .dotfiles row and Open worktree...
finds it later. That command takes no --env, so OMC_STATE_DIR andHERDR_TEAM_HANDOFFS are exported into the pane's shell ahead of the wrapper
instead of inherited — which is the better half of the trade, since they then
survive the agent exiting and a hand-restarted ccd still writes its handoff to
the main checkout.
prefix+alt+t opens status in a popup. One agent per worktree; agents report
outcomes by writing .omc/handoffs/<task>-<dispatch>.md in the main
checkout, never by leaving them in a transcript. The protocol the agents follow
lives in ai/shared/skills/herdr-team/, which is linked into ~/.claude/skills
and ~/.omp/agent/skills by ai/setup.sh.
dispatch exists so the completion contract — Run, Task and Dispatch ids, the
absolute handoff path, the frontmatter template — is handed over verbatim
instead of retyped from memory. The Run id is kept in .omc/state/team-run, so
it survives a compaction. With no --dispatch it picks the lowest id with no
handoff file yet, which enforces "a settled id is never reused" mechanically;--dry-run prints the prompt instead of sending it.
--from-plan takes the same idea one step further: the body stops being prose
the orchestrator retypes and becomes a pointer into the plan. The plan carries
a ## Tasks json block — one row per task, with its files, its verify
command and the tasks it blocks on — and dispatch reads the row, refuses
(exit 3) when a blocker has no succeeded + verified handoff under the
current Run, and hands the executor the plan path, the section id and the
command that must run before it may claim evidence: verified. The Run clause
is the whole point: task ids restart at T-01 every Run and handoff filenames
carry no Run, so a gate that only globs the directory unblocks work with a
previous Run's result. --force overrides it, because retry is human-gated and
a gate with no key is a trap. A plan with no json block dispatches exactly as
before. ai/herdr/tests/run.sh is the only check the embedded parser
gets — shellcheck cannot see inside a heredoc — so run it after touching it.
plan lint <plan.md> checks a plan against that format before anything is
spawned, printing every problem at once: a row with no ### T-nn section or a
section with no row, blocks naming a task that does not exist, a cycle inblocks, and a verify whose exit code its own pipeline masks. That last one
is the reason the rest exists — cmd | tail -1 exits 0 however cmd exited,
so an executor observes 0 and claims evidence: verified on a check that
cannot fail. Lead a piped verify with set -o pipefail. The format itself is
documented in the dispatchable-plan skill, which is separate fromherdr-team for a routing reason: a planning session never says "herdr", soherdr-team's triggers never fire and the format would never reach the session
that has to produce it.
collect --plan <plan.md> is the other direction, and the reason an
orchestrator does not have to stay in the loop token by token: one row per task
in the plan rather than one per handoff — done, review, failed,running, ready, blocked — with the exit code carrying the decision. 0
dispatch something, 1 a human must look, 2 nothing actionable and a
task failed, 3 nothing to do. Where a task has several handoffs the highestD-nn wins, so a task retried to success stops reading failed forever.running means dispatched and unanswered, which no handoff file can show, sodispatch journals each real dispatch to .omc/handoffs/.dispatched. Plaincollect is unchanged, including naming no Run meaning every Run.
wait is the other half of that loop — dispatch → wait → collect --plan →
dispatch what is ready — so nobody has to watch a pane for the middle step.
It blocks until one outstanding dispatch under the Run settles, prints which
agent and task settled, and deliberately reports nothing about the outcome: the
table is what does that, and collect --plan on the next line reads it. Its
exit codes are stated once, in ai/shared/skills/herdr-team/references/herdr-adapter.md.
The one that changes the loop's shape is a blocked agent: no handoff means no new
table, and the next move is surface <name>, which prints its screen for the
human to answer in the pane — that answer is never typed by the script.
tmux
tmux/tmux.conf predates herdr and keeps the same shape — one pane per agent,
tiled and labelled, layouts that survive a reboot. herdr now runs the agents, so
tmux is for plain shells, ssh and anything herdr shouldn't own. Prefix is C-a,
and [experimental] allow_nested is off in herdr, so don't nest the two.
| Binding | Action |
|---|---|
prefix | / prefix - |
split (keeps cwd) |
prefix Space |
tile all panes |
prefix A |
label the current pane (agent name, shown on its border) |
prefix S |
toggle synchronize-panes — type one prompt into every agent |
prefix b |
break a pane out to its own window |
prefix g |
scratch shell in a popup |
prefix s |
session switcher (one session per project) |
Plugins are managed by tpm, which
bootstraps itself on first launch (sensible, vim-tmux-navigator, yank,
resurrect + continuum). prefix I installs/updates them.
Runtimes
mise is the only version manager. It replaced pyenv,
rbenv, jenv, nvm and asdf, which together cost ~2.5 s of every shell start.
mise ls # what's active
mise use -g node@22 # change a global pin
mise install # install everything pinned in mise/global.toml
Global pins live in mise/global.toml (python, node, ruby, go, java, and the
Swift tooling). Per-project .python-version, .ruby-version, .nvmrc,.node-version and .java-version files are honoured, as are mise.toml and.tool-versions. mise exports JAVA_HOME on its own, so no JDK is installed
through Homebrew.
Homebrew
brew/Brewfile is the single source of truth, grouped by formulae / casks /
fonts / mas.
sh brew/setup.sh # install
sh brew/setup.sh --cleanup # install, then uninstall anything not listed
sh brew/update.sh # dump what's installed, for reconciliation
--cleanup is destructive and asks for confirmation. brew/update.sh writesBrewfile.generated rather than overwriting the grouped Brewfile, so the
comment structure survives; fold in the diff by hand.
Switching machines
Both architectures are supported: every path decision keys off$(uname -m) == arm64 → /opt/homebrew, otherwise /usr/local. Usebrew_prefix from lib/common.sh in new scripts rather than hardcoding
either.
On a new machine:
- Install the Xcode command line tools:
xcode-select --install. git clone https://github.com/khoi/dotfiles.git ~/.dotfiles && cd ~/.dotfiles./setup.sh— installs Homebrew, links dotfiles, runs the default modules.mise install— reinstall the pinned runtimes (nothing is copied across).- Sign in to the App Store, then re-run
sh brew/setup.shso themasentries
install. - Import the GPG key and trust it — commits are signed
(commit.gpgSign = true), so git will refuse to commit until this is done.git/setup.shwrites the correctgpg.programpath for the architecture,
asks foruser.name/user.emailif they are missing, andgpg/setup.shinstallsgpg.conf/gpg-agent.conf. gh auth login, thensh ai/setup.shfor the herdr integrations.- The herdr plugins are installed by
sh ai/setup.sh(step 7), pinned to a
release tag; approve the manifest preview it prints for each one. Plugins run
unsandboxed, so that prompt stays manual. ./setup.sh --allif this machine needs VS Code or Xcode.
Conventions
Each top-level directory is a self-contained module with a setup.sh that can
be run on its own. Scripts start with:
#!/usr/bin/env bash
set -euo pipefail
DOTFILES="${DOTFILES:-$(cd "$(dirname "$0")/.." && pwd)}"
. "${DOTFILES}/lib/common.sh"
require_macos
lib/common.sh provides log/info/ok/warn/die, require_macos,is_arm, brew_prefix, load_brew_shellenv, link (idempotent symlink with
backup) and module_dir.
.editorconfig governs formatting: 2-space indent, LF, final newline, trimmed
trailing whitespace — except in *.md and *.diff. C-family and Python use 4
spaces; Makefiles use tabs. Markdown is linted per .markdownlint-cli2.jsonc.
There is no build or repo-wide test suite; this repo is shell scripts and config
files. Verify changes with bash -n (or shellcheck) and zsh -n. CI runs on
every push to main and every pull request:
lint.yml—shellcheck -x,zsh -n, markdownlint, editorconfig-checker,
JSON and TOML validation, actionlint and zizmor.secrets.yml— a gitleaks scan of every pushed commit, with no path filter.smoke.yml— on macOS, when the shell config,setup.sh,lib/or a plist
changed: lints the plists, then links the zsh config into a throwawayHOME
and fails on any stderr output or a warm start over 1.5 s.test.yml— on macOS, whenai/herdr/orlib/changed: the herdr suite.
It runs withHERDR_TESTS_STRICT=1, so a skip the runner could have
prevented fails the job instead of reporting a green suite that never asked.pr-title.yml— Conventional Commits title; warns past 50 characters,
fails past 72.
The path filters live in the reusable _detect-changes.yml; skipped jobs
report success, so they can be required checks.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi