Initial dotfiles repo for Mac and Linux

This commit is contained in:
Mike Williams
2026-03-18 14:38:46 +13:00
parent 5225fafdfe
commit df7445adf3
16 changed files with 1035 additions and 1 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
**/.DS_Store
gh/.config/gh/hosts.yml
cursor/**
# Ignore all Claude Code runtime data, explicitly allow config files
claude/.claude/**
!claude/.claude/CLAUDE.md
!claude/.claude/settings.json
!claude/.claude/skills/
!claude/.claude/skills/**
#!claude/.claude/plugins/
#!claude/.claude/plugins/**

View File

@@ -1 +1,74 @@
Init
# Mike's Dotfiles
Personal dotfiles managed with a Bash Script and GNU Stow. Optimized for both macOS and Linux environments.
## Features
- 🐚 **Zsh with Starship** - Fast, customizable prompt with oh-my-zsh
- 📦 **Simple Setup** - Idempotent, pure bash installation script
- 🔧 **Essential Dev Tools** - btop, gh, fzf, direnv, git-delta, bat, fd, jq, etc. (tmux on Linux only)
- 🎨 **Modern Experience** - Autosuggestions, syntax highlighting, history search
- 🤖 **AI Ready** - Shared rules for **Claude Code** and **Cursor IDE**
## Quick Start
To set up a new Mac or Linux computer, manually run the installation script from the root of this repository:
```bash
./install.sh
```
The script is idempotent and safe to run multiple times. It will:
1. Install Homebrew (if missing)
2. Install core packages and tools
3. Configure Zsh and Starship
4. Symlink configurations to your home directory using GNU Stow
5. Set up AI rules for development tools
## Structure
```
.
├── install.sh # Main installation script
├── zsh/ # Zsh configuration (.zshrc)
├── starship/ # Starship configuration (.config/starship.toml)
├── ai-instructions/ # General AI Assistant rules
├── claude/ # Claude Code configuration
├── cursor/ # Cursor IDE configuration
├── editorconfig/ # EditorConfig settings
├── gh/ # GitHub CLI configuration
└── git/ # Git configuration
```
## Installation Options
The `install.sh` script supports environment variables to customize which runtimes are installed:
```bash
INSTALL_NODE=true \
INSTALL_PYTHON=true \
INSTALL_GO=true \
INSTALL_JAVA=true \
./install.sh
```
## AI Rules System
This repository implements a shared rules system for AI coding assistants. Markdown files in `ai-instructions/` are automatically symlinked into both `~/.claude/rules/` and `~/.cursor/rules/`, ensuring consistent behavior across tools like Claude Code and Cursor.
## Making Changes
Since dotfiles are symlinked, you can edit them in place and they will be reflected in your home directory:
```bash
nano ~/.zshrc # Edit zsh config
source ~/.zshrc # Reload changes
```
Commit changes back to the repository:
```bash
git add -A
git commit -m "Update zsh config"
git push
```

28
ai-instructions/README.md Normal file
View File

@@ -0,0 +1,28 @@
# AI Instructions / Rules System
This directory contains single-source-of-truth instructions that are shared between **Claude Code** and **Cursor IDE**.
## How It Works
```
ai-instructions/*.md (SOURCE OF TRUTH)
├──> claude/.claude/rules/*.md (symlinks for Claude Code)
└──> cursor/.cursor/rules/*.md (symlinks for Cursor IDE)
```
### During Installation
When you run `install.sh` (or when a Coder devbox starts):
1. **Directories created**: `claude/.claude/rules` and `cursor/.cursor/rules` are created.
2. **Rules generated**: The `generate_rules` function in `install.sh` symlinks `ai-instructions/*.md``claude/.claude/rules/*.md` and `cursor/.cursor/rules/*.md`.
3. **Stow creates symlinks**:
- `claude/.claude/*``~/.claude/*`
- `cursor/.cursor/*``~/.cursor/*`
### Result
Both IDEs load the same instructions:
- **Claude Code**: Reads `~/.claude/CLAUDE.md` + `~/.claude/rules/*.md`
- **Cursor IDE**: Reads `~/.cursor/rules/*.md`

View File

@@ -0,0 +1,28 @@
# Slack Message Formatting Guide
When sending Slack messages via MCP tools, use **standard Markdown** formatting.
## What Works
- `**bold**` and `*italic*`, `_italic_`, `~~strikethrough~~`, `` `inline code` ``
- Code blocks with triple backticks (with or without language tag)
- Unordered lists with `-` dashes (nested lists with indentation work)
- Numbered lists with `1.` style
- Links with `[text](url)` — raw URLs are also auto-unfurled
- Blockquotes with `>`
- Blank lines between paragraphs
## What Does NOT Work (Avoid)
- **Tables** — render as garbled text. Use a Google Doc, Slack canvas, or a simple list instead.
- **Horizontal rules** (`---`, `***`) — not rendered. Use blank lines for separation.
- **Headings** (`#`, `##`) — show as plain text with `#` symbols. Use `**bold text**` on its own line instead.
- **HTML tags** — render as plain text. Use Markdown equivalents.
- **Images** (`![alt](url)`) — no inline display. Alt text shows as a clickable link. Use sparingly and only when necessary.
## Key Rules
- Use standard Markdown `[text](url)` — NOT Slack mrkdwn `<url|text>`
- Use `-` dashes for bullets — NOT `•` unicode bullets
- Use `**bold**` on its own line for section headers — NOT `#` headings
- Use blank lines for separation — NOT horizontal rules

28
claude/.claude/CLAUDE.md Normal file
View File

@@ -0,0 +1,28 @@
# Global Claude Code Instructions
## Canva Monorepo Workflow
### Pre-Commit Check (MANDATORY)
When working in the Canva monorepo (`~/work/canva`), ALWAYS run `taz check` before every `git commit`.
1. `taz check` (generate, format, lint, and test changed files)
2. `git add <files>`
3. `git commit`
Never skip step 1.
### AI Attribution Policy
Do NOT add any AI attribution to commits or PRs.
### Write/Modify Operations - IMPORTANT
For ANY write or modify operations:
1. Show the command first
2. Explain what it will do
3. Wait for explicit approval before running
### Additional Instructions
Rules in `~/.claude/rules/` are auto-generated from `~/.dotfiles/ai-instructions/` at install time.

View File

@@ -0,0 +1,37 @@
{
"apiKeyHelper": "/usr/local/bin/otter bedrock-bearer-token",
"enabledPlugins": {
"otter-tools@otter-marketplace": true
},
"env": {
"ANTHROPIC_API_KEY": "",
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "global.anthropic.claude-opus-4-6-v1",
"ANTHROPIC_MODEL": "global.anthropic.claude-sonnet-4-6",
"AWS_REGION": "ap-southeast-2",
"CLAUDE_AGENT_SDK_VERSION": "0.2.51",
"CLAUDE_CODE_API_KEY_HELPER_TTL_MS": "60000",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES": "true",
"CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL": "true",
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"CLAUDE_CODE_ENTRYPOINT": "claude-desktop",
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "32768",
"CLAUDE_CODE_SKIP_BEDROCK_AUTH": "1",
"CLAUDE_CODE_SSE_PORT": "40246",
"CLAUDE_CODE_USE_BEDROCK": "1",
"DISABLE_AUTOUPDATER": "1",
"ENABLE_IDE_INTEGRATION": "true",
"ENABLE_TOOL_SEARCH": "true",
"FORCE_COLOR": "1",
"MAX_THINKING_TOKENS": "1024",
"MCP_CONNECTION_NONBLOCKING": "true",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp"
},
"model": "opus"
}

View File

@@ -0,0 +1,224 @@
---
name: ship-it
description: Get PRs over the line — run tests, fix errors, update visreg, check CI, address review comments, and loop until green.
disable-model-invocation: false
---
# Ship It
Automatically drive one or more PRs to a mergeable state by running local checks, pushing fixes, triggering CI, and addressing review feedback in a loop.
## Step 0: Discover PRs
Determine which PRs to process:
1. Run `gh pr list --author @me --state open --json number,headRefName,baseRefName,title,mergeable,mergeStateStatus` to find all open PRs by the current user.
2. Detect **PR trains** — a PR train is a chain where PR2's base branch is PR1's head branch, PR3's base is PR2's head, etc. Build the dependency graph from the `baseRefName`/`headRefName` relationships.
3. If the current branch belongs to a train, process the **entire train in order** starting from the bottom (closest to `master`). Fixes to an earlier PR may require rebasing later PRs.
4. If the current branch is a standalone PR, process only that PR.
5. Present the discovered PR(s) to the user and confirm before proceeding.
## Step 0b: Check Existing Reviews (per PR)
Before running any local checks or CI, fetch existing review feedback for each PR using the process in Step 4a. If there are unaddressed comments, handle them per Step 5c/5d **before** proceeding to local validation — there is no point running checks if reviewer feedback will change the code.
## Step 1: Local Validation (per PR branch)
Check out the branch for the current PR in the train, then run local validation.
### 1-pre. Check mergeability and branch staleness
**Check for merge conflicts first** using the `mergeable` and `mergeStateStatus` fields already fetched in Step 0 (or re-check with `gh pr view <number> --json mergeable,mergeStateStatus`). If the PR is `CONFLICTING`, **rebase immediately** before running any checks or triggering CI — there is no point validating or running CI on a branch that cannot merge.
Then check staleness:
```bash
git fetch origin master
```
```bash
merge_base=$(git merge-base HEAD origin/master)
diverged_days=$(( ( $(date +%s) - $(git log -1 --format=%ct "$merge_base") ) / 86400 ))
echo "Branch diverged from master ~${diverged_days} days ago"
```
If the branch diverged **more than 4 days ago**, proactively rebase before running any checks — at that age in a fast-moving monorepo, many errors are simply stale-branch artifacts. For fresher branches, note the age and continue; it will inform the triage in Step 5 if errors appear.
**Use a team of agents to parallelise where possible:**
### 1a. Run `pnpm fin --silent` (agent 1)
- This performs formatting, linting, testing, and type-checking on the changeset.
- If errors are found, fix them. Common fixes:
- Type errors: fix the types directly (never use `@ts-ignore` or `as any`)
- Lint errors: apply the suggested fix or refactor
- Test failures: investigate and fix the root cause
- Formatting: `pnpm fin` auto-fixes most formatting issues
- Dependency/import issues: run `taz generate` to regenerate `BUILD.bazel` and `tsconfig.json`
- Re-run `pnpm fin --silent` after fixes to confirm they resolve the issue.
### 1b. Run unit tests for affected areas (agent 2)
- Identify changed files with `git diff --name-only origin/master...HEAD`.
- Run targeted tests: `pnpm test -- --findRelatedTests <changed-files>`.
- Fix any test failures.
### 1c. Visual regression tests (agent 3, if applicable)
- Check if any changed files have associated `.visreg.jsonc` configs or are UI components with visual tests.
- If yes, run `pnpm visreg` with appropriate filters.
- If visreg detects diffs (exit code 3), you **must** visually verify before updating hashes. Use the `visreg` skill's inspection steps (hash URLs or Visual Comparison Service via `playwright-cli`).
- If you **cannot** visually verify the diffs (e.g. Cloudflare auth blocks image URLs, no browser available), **stop and escalate to the user**. Present the old and new hashes, the Visual Comparison Service URL, and the `canv.am/visreg-hash/<hash>.png` URLs so the user can inspect them. Never assume diffs are intentional without actually seeing them.
- Only after visual verification (by you or the user) confirms changes are intentional, update expected images with `pnpm visreg -u`.
- If failures are unexpected, investigate and fix the root cause.
### 1d. Functional/integration tests (agent 4, if applicable)
- Check if changed files have associated `.func.ts` test files.
- If yes, run: `pnpm test:func:ts --browser chrome <test-file>`.
- Fix any failures.
**Wait for all agents to complete before proceeding.**
## Step 2: Commit, Update PR Description, and Push
1. Stage all changes: `git add` specific changed files (not `git add -A`).
2. Commit with a descriptive message following the `<scope>: <description>` convention.
3. **Update the PR description if fixes changed the scope or behavior of the PR:**
- Fetch the current PR description: `gh pr view --json body -q .body`.
- Review whether the fixes you made materially change what the PR does — e.g. new files added, APIs changed, behavior altered, or test strategy updated.
- If yes, update the description using `gh pr edit <number> --body "<updated body>"` to reflect the current state of the PR. Preserve the existing structure (Problem/Solution/Testing sections) and add or revise the relevant parts. Do not rewrite sections unrelated to your changes.
- If the fixes were purely mechanical (formatting, lint, minor type fixes) and don't change the PR's intent or scope, skip this step.
4. Push to the PR branch. Use `git push --force-with-lease` if the branch has been rebased or amended.
5. If this is part of a PR train, after pushing fixes to an earlier PR, rebase all subsequent branches in the train:
```
git checkout <next-branch>
git rebase <previous-branch>
git push --force-with-lease
```
Repeat for each subsequent branch in the train. Resolve any rebase conflicts (consult the user if unsure), then re-validate from Step 1. Track status of each PR in the train using the todo list.
## Step 3: Trigger CI
1. Find the PR number: `gh pr view --json number -q .number`.
2. Comment on the PR to trigger CI: `gh pr comment <number> --body "@canva-ci-bot test"`.
## Step 4: Wait for CI and Reviews
1. On macOS, run `caffeinate -i -t 300 &` to prevent the machine from sleeping. On Linux (devbox), skip this step — headless VMs don't sleep.
2. Wait 5 minutes for CI to run and reviewers to respond.
3. After waiting, check for results in parallel:
### 4a. Poll CI status and review comments together
Check both CI and reviews on every poll cycle — do not wait for CI to finish before looking at reviews:
1. Run `gh pr checks <number>` to see check status.
2. Simultaneously fetch review comments using `gh api repos/Canva/canva/pulls/<NUMBER>/reviews` and `gh api repos/Canva/canva/pulls/<NUMBER>/comments`.
3. Categorize any new feedback: code changes requested, questions, nits, approvals. Distinguish between **human reviewers** and **bot reviewers** (see Step 5 for handling differences).
4. If CI checks are still running **and** no new review comments need attention, wait 2 minutes and re-poll both (up to 3 additional attempts).
5. If new review comments arrive while CI is still running, **proceed to Step 5 immediately** to begin addressing feedback — do not wait for CI to finish first. After addressing the feedback, return here to continue polling CI.
6. Collect any CI failure details once checks complete.
## Step 5: Fix Issues and Resolve Comments
If CI failures or review comments require changes:
### 5a. Triage and fix attempts
Always try to fix errors directly first. Use these signals to inform your diagnosis:
| Signal | What it suggests |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **"Cannot find module" / "Cannot find name" / missing export** | Master may have renamed or moved a symbol. Check whether the PR's imports need updating. |
| **Type errors on interfaces/types the PR didn't modify** | A changed signature on master can cascade. Check `git log origin/master --oneline -- <failing-file>` to see if master changed it recently. |
| **BUILD.bazel / tsconfig resolution failures** | Build-graph changes on master (new packages, moved targets) frequently break stale branches. |
| **Test/snapshot failures in downstream consumers** | If a failing test imports or renders something your PR changed, that's expected cascading breakage — fix the test/snapshot directly. |
| **CI "merge conflict" or "not mergeable" status** | A rebase is required regardless. |
**Fall back to rebasing** only if you can't find a fix or your fix still errors. Also rebase immediately for merge conflicts.
1. `git fetch origin master`
2. `git rebase origin/master`
3. Resolve any conflicts (prefer the PR's intent; consult the user if unsure).
4. Go back to **Step 1** and re-validate.
5. For PR trains, rebase the entire chain bottom-up.
### 5b. CI failures
Read the failing check logs using `bk-get-logs-by-commit` or `bk-get-logs-by-url` MCP tools. Diagnose and fix the root cause.
### 5c. Human review comments
- Summarise each piece of review feedback for the user.
- **Stop and ask the user to confirm** before making any code changes in response to human review comments. Present what you plan to change and wait for approval.
- Only after the user confirms, make the requested code changes and respond to comments.
- These take highest priority.
### 5d. Bot review comments
Address all bot feedback (`developer-platform-bot`, `chatgpt-codex-connector`, etc.) directly in this PR. Do not silently ignore any bot comment.
**Deferral** — only defer if the fix adds unnecessary complexity or belongs in a separate PR. When deferring:
1. Reply to the bot thread explaining why.
2. Create a JIRA ticket in **CVCD** via `jira-create` with a summary, link to the comment, and context. Include the ticket key in the reply.
### 5e. Resolve addressed bot comments only
After fixing issues raised by **bot reviewers**, mark their corresponding review threads as resolved using the GitHub GraphQL API:
- Fetch unresolved threads: `gh api graphql -f query='{ repository(owner: "Canva", name: "canva") { pullRequest(number: <NUMBER>) { reviewThreads(first: 50) { nodes { id isResolved comments(first: 1) { nodes { body author { login } } } } } } } }' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {id, author: .comments.nodes[0].author.login}'`
- Resolve each thread: `gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "<THREAD_ID>"}) { thread { isResolved } } }'`
- Only resolve threads whose feedback has actually been addressed in the code or was already handled before the review was posted.
- Do **not** resolve threads where you replied but did not make the suggested change — leave those open for the author to review.
- **Never resolve human reviewer threads.** Even if you made the requested change and responded, leave human threads open for the reviewer to verify and resolve themselves. Only resolve threads authored by bots (`developer-platform-bot`, `chatgpt-codex-connector`, etc.).
After fixing, go back to **Step 1** and loop.
## Step 6: Exit Conditions
**Success:** Before declaring success, do a **final review check** — fetch reviews and comments one more time to catch any feedback that arrived during the last fix cycle. Only if all CI checks pass, no unaddressed review comments remain (confirmed by this final check), and all PRs in the train are green, report the final status to the user.
**Stop and escalate to the user** if any of these occur:
- **Same error after 3 fix attempts** — you're likely misdiagnosing the root cause.
- **CI failure in code the PR doesn't own** — e.g. a global infra flake, a broken shared pipeline, or a test that also fails on master. Don't try to fix other teams' code. Instead, rebase onto latest master first (the fix may have already landed). If the failure persists after rebasing, escalate.
- **Review comment requires a design decision** — the reviewer is asking "should we do X or Y?", not "please fix this." Surface the question, don't pick for the user.
- **Merge conflict you can't confidently resolve** — when both sides made intentional changes to the same code and the right resolution isn't obvious.
- **Blocked on an external dependency** — e.g. a train PR that needs an earlier PR to land first, or a CI check gated on another service.
- **Max 5 full loops (Steps 1→5)** — if you've gone around 5 times and it's still not green, something structural is wrong. Stop and summarise what's still failing.
## PR Train Handling
When processing a train of PRs:
- Always process bottom-up (closest to master first).
- After fixing and pushing changes to PR N, rebase PR N+1, N+2, etc. on top.
- Run validation on each PR after rebasing — the rebase may introduce new conflicts or failures.
- If a rebase conflict occurs, resolve it, then continue validation.
- Track the status of each PR in the train using the todo list.
## Sandbox Environment & Force Push Safety
This skill runs in a sandboxed environment where `git push --force-with-lease` is required for rebased train branches. To prevent accidental pushes to unrelated branches:
1. **At the start of Step 0**, after discovering PRs, build an **allowlist** of branch names that belong to the train (or the single standalone PR branch).
2. **Before every `git push --force-with-lease`**, verify that the current branch is in the allowlist:
```
current=$(git branch --show-current)
```
If `$current` is not in the allowlist, **abort the push and report the error** — never force-push a branch that isn't part of the train.
3. **Never force-push `master`, `main`, or any base branch that isn't owned by the train.** If a train's root PR targets `master`, only the train's feature branches get force-pushed — never `master` itself.
4. Force-pushing is **expected and safe** for train branches after rebasing. Prefer `--force-with-lease`, but `--force` is also permitted if `--force-with-lease` fails (e.g. due to stale refs in the sandbox).
## PR Commenting Rules
- **Always identify as Claude** when posting any comment on a PR. Start PR comments with "🤖 _Claude:_" or similar so reviewers know the comment is AI-generated, not from the user. Including comments responding to bots.
- **Never post comments impersonating the user.** Every comment must make clear it was written by Claude, not the PR author.
- **CI trigger comments** (e.g. `@canva-ci-bot test`) are mechanical and do not need the Claude prefix.
## Important Notes
- Never skip pre-commit hooks (`--no-verify`).
- Use `pnpm`, not `yarn` or `npm`.

View File

@@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

15
gh/.config/gh/config.yaml Normal file
View File

@@ -0,0 +1,15 @@
# What protocol to use when performing git operations. Supported values: ssh, https
git_protocol: https
# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment.
#editor:
# When to interactively prompt. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled
#prompt: enabled
# A pager program to send command output to, e.g. "less". Set the value to "cat" to disable the pager.
#pager:
# Aliases allow you to create nicknames for gh commands
#aliases:
# co: pr checkout
# The path to a unix socket through which send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport.
#http_unix_socket:
# What web browser gh should use when opening URLs. If blank, will refer to environment.
#browser:

27
gh/.config/gh/config.yml Normal file
View File

@@ -0,0 +1,27 @@
# The current version of the config schema
version: 1
# What protocol to use when performing git operations. Supported values: ssh, https
git_protocol: https
# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment.
editor:
# When to interactively prompt. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled
prompt: enabled
# Preference for editor-based interactive prompting. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled
prefer_editor_prompt: disabled
# A pager program to send command output to, e.g. "less". If blank, will refer to environment. Set the value to "cat" to disable the pager.
pager:
# Aliases allow you to create nicknames for gh commands
aliases:
co: pr checkout
# The path to a unix socket through which to send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport.
http_unix_socket:
# What web browser gh should use when opening URLs. If blank, will refer to environment.
browser:
# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled
color_labels: disabled
# Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled
accessible_colors: disabled
# Whether an accessible prompter should be used. Supported values: enabled, disabled
accessible_prompter: disabled
# Whether to use a animated spinner as a progress indicator. If disabled, a textual progress indicator is used instead. Supported values: enabled, disabled
spinner: enabled

66
git/.gitconfig Normal file
View File

@@ -0,0 +1,66 @@
[diff]
tool = difftastic
[difftool "difftastic"]
cmd = difft "$LOCAL" "$REMOTE"
[color]
ui = auto
[push]
autoSetupRemote = true
[log]
date = local
[core]
excludesfile = ~/.gitignore_global
editor = nano
pager = delta
[pager]
show = delta
diff = delta
[delta]
navigate = true
side-by-side = true
line-numbers = true
[pull]
rebase = true
[interactive]
diffFilter = delta --color-only
[user]
name = Mike Williams
email = msw@canva.com
[filter "lfs"]
smudge = git-lfs smudge -- %f
process = git-lfs filter-process
required = true
clean = git-lfs clean -- %f
[maintenance]
[include]
path = ~/.gitconfig_local
[oh-my-zsh]
git-commit-alias = bec3f2244a978d1e1391bc28964ba36a3cf0ad28
[alias]
build = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"build${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
chore = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"chore${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
ci = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"ci${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
docs = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"docs${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
feat = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"feat${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
fix = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"fix${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
perf = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"perf${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
refactor = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"refactor${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
rev = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"revert${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
style = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"style${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
test = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"test${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"
wip = "!a() {\nlocal _scope _attention _message\nwhile [ $# -ne 0 ]; do\ncase $1 in\n -s | --scope )\n if [ -z $2 ]; then\n echo \"Missing scope!\"\n return 1\n fi\n _scope=\"$2\"\n shift 2\n ;;\n -a | --attention )\n _attention=\"!\"\n shift 1\n ;;\n * )\n _message=\"${_message} $1\"\n shift 1\n ;;\nesac\ndone\ngit commit -m \"wip${_scope:+(${_scope})}${_attention}:${_message}\"\n}; a"

15
git/.gitignore_global Normal file
View File

@@ -0,0 +1,15 @@
*~
.DS_Store
# IntelliJ IDEA
.idea/
*.iml
*.iws
*.ipr
# Maven build output
target/
# Gradle build output (if applicable)
.gradle/
build/
**/*code-workspace
**/canva/.claude/hooks/

236
install.sh Executable file
View File

@@ -0,0 +1,236 @@
#!/usr/bin/env bash
# Mike's Dotfiles Install Script
# Safe to run multiple times (idempotent).
# Targets both macOS and Linux (Coder devboxes).
set -euo pipefail
# --- Configuration ---
export HOMEBREW_TEMP="/tmp"
# Optional components (default to true)
INSTALL_NODE=${INSTALL_NODE:-true}
INSTALL_PYTHON=${INSTALL_PYTHON:-true}
INSTALL_GO=${INSTALL_GO:-true}
INSTALL_JAVA=${INSTALL_JAVA:-true}
BREW_PACKAGES=(
git diff-so-fancy btop gh zsh starship stow bat fd jq wget eza
git-lfs difftastic fzf direnv git-delta zsh-completions
gemini-cli claude-code zsh-autosuggestions zsh-syntax-highlighting
zsh-history-substring-search zoxide ripgrep
)
# Only install tmux if not on macOS
[[ "$(uname)" != "Darwin" ]] && BREW_PACKAGES+=(tmux)
# Add optional packages to Homebrew list
[[ "$INSTALL_GO" == "true" ]] && BREW_PACKAGES+=(go)
[[ "$INSTALL_JAVA" == "true" ]] && BREW_PACKAGES+=(openjdk)
[[ "$INSTALL_PYTHON" == "true" ]] && BREW_PACKAGES+=(pyenv)
MAC_CASK_PACKAGES=(iterm2 font-hack-nerd-font)
STOW_PACKAGES=(editorconfig gh git claude cursor starship zsh)
# --- Utilities ---
info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
warn() { echo -e "\033[0;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*"; exit 1; }
OS_TYPE=$(uname)
DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- macOS System Setup ---
if [[ "$OS_TYPE" == "Darwin" ]]; then
info "Configuring macOS system defaults..."
defaults write com.apple.finder AppleShowAllFiles YES
defaults write com.apple.finder ShowPathbar -bool true
defaults write com.apple.finder ShowStatusBar -bool true
# Refresh Finder
killall Finder 2>/dev/null || true
fi
# --- Homebrew Setup ---
if ! command -v brew &> /dev/null; then
info "Installing Homebrew..."
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
# Initialize Homebrew environment for the current script
if [[ "$OS_TYPE" == "Darwin" ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)" 2>/dev/null || eval "$(/usr/local/bin/brew shellenv)" 2>/dev/null
ZSH_PATH="$(brew --prefix)/bin/zsh"
else
# Linux / Coder
if [ -d "/home/linuxbrew/.linuxbrew" ]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
fi
ZSH_PATH="/home/linuxbrew/.linuxbrew/bin/zsh"
fi
BREW_BIN=$(command -v brew)
# --- Package Installation ---
info "Installing Homebrew packages..."
$BREW_BIN install "${BREW_PACKAGES[@]}" || warn "Some packages might have failed to install or are already present."
if [[ "$OS_TYPE" == "Darwin" ]]; then
info "Installing macOS casks..."
$BREW_BIN install --cask "${MAC_CASK_PACKAGES[@]}" || warn "Some casks might have failed to install."
# Permissions fix for completions
chmod go-w "$(brew --prefix)/share" 2>/dev/null || true
chmod -R go-w "$(brew --prefix)/share/zsh" 2>/dev/null || true
fi
# --- Shell Setup ---
info "Setting up Zsh..."
# Add to /etc/shells if missing
if ! grep -q "$ZSH_PATH" /etc/shells; then
info "Adding $ZSH_PATH to /etc/shells"
if command -v sudo &> /dev/null; then
echo "$ZSH_PATH" | sudo tee -a /etc/shells
else
warn "Sudo not found, skipping /etc/shells update"
fi
fi
# Change default shell
CURRENT_LOGIN_SHELL=""
if [[ "$OS_TYPE" == "Darwin" ]]; then
CURRENT_LOGIN_SHELL=$(dscl . -read "/Users/$USER" UserShell | awk '{print $2}')
else
CURRENT_LOGIN_SHELL=$(getent passwd "$USER" | cut -d: -f7)
fi
if [[ "$CURRENT_LOGIN_SHELL" != "$ZSH_PATH" ]]; then
info "Changing default shell to $ZSH_PATH"
if command -v sudo &> /dev/null; then
sudo chsh -s "$ZSH_PATH" "$USER" || warn "Failed to change shell with sudo"
else
warn "Sudo not found, skipping shell change"
fi
fi
# Oh My Zsh
if [ ! -d "$HOME/.oh-my-zsh" ]; then
info "Installing Oh My Zsh..."
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
fi
# --- Runtime Environments ---
# NVM / Node
if [[ "$INSTALL_NODE" == "true" ]]; then
info "Configuring NVM..."
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
info "Installing NVM via official script..."
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
fi
# Remove incompatible prefix/globalconfig from .npmrc for nvm compatibility
if [ -f "$HOME/.npmrc" ]; then
info "Cleaning .npmrc of incompatible settings..."
if [[ "$OS_TYPE" == "Darwin" ]]; then
sed -i '' '/^prefix=/d; /^globalconfig=/d' "$HOME/.npmrc"
else
sed -i '/^prefix=/d; /^globalconfig=/d' "$HOME/.npmrc"
fi
fi
# shellcheck source=/dev/null
if [ -s "$NVM_DIR/nvm.sh" ]; then
source "$NVM_DIR/nvm.sh"
nvm install --lts
npm install -g npm@latest
fi
fi
# Pyenv / Python
if [[ "$INSTALL_PYTHON" == "true" ]] && command -v pyenv &> /dev/null; then
info "Configuring Pyenv..."
eval "$(pyenv init -)"
pyenv install 3.10 -s
pyenv global 3.10
fi
# FZF Setup
if command -v fzf &> /dev/null; then
info "Configuring FZF..."
"$(brew --prefix)/opt/fzf/install" --key-bindings --completion --no-update-rc --no-bash --no-zsh || true
fi
# --- AI Rules Generation ---
generate_rules() {
local target_dir="$DOTFILES_DIR/$1"
info "Generating rules in $target_dir"
mkdir -p "$target_dir"
# Remove stale symlinks
find "$target_dir" -type l -delete
# Create new symlinks
for rule in "$DOTFILES_DIR/ai-instructions"/*.md; do
[ -e "$rule" ] || continue
ln -sf "$rule" "$target_dir/$(basename "$rule")"
done
}
generate_rules "claude/.claude/rules"
generate_rules "cursor/.cursor/rules"
# --- Stow ---
info "Applying dotfile configurations via Stow..."
for pkg in "${STOW_PACKAGES[@]}"; do
info "Stowing $pkg..."
# Detect conflicts and remove non-stow files
stow --simulate "$pkg" -t "$HOME" -d "$DOTFILES_DIR" 2>&1 | \
grep -E "(existing target is (not owned by stow|neither a link nor a directory):|over existing target .+ since neither a link)" | \
awk '/over existing target/ { for(i=1;i<=NF;i++) if($i=="target") { print $(i+1); break } next } /:/ { sub(/.*: /, ""); print }' | \
while read -r conflict; do
if [ -n "$conflict" ]; then
warn "Removing conflict: $HOME/$conflict"
rm -rf "$HOME/$conflict"
fi
done || true
stow "$pkg" -t "$HOME" -d "$DOTFILES_DIR"
done
# --- Git Local Config ---
info "Configuring machine-specific Git settings..."
GIT_LOCAL_CONFIG="$HOME/.gitconfig_local"
WORK_DIR="$HOME/work"
if [[ "$OS_TYPE" == "Darwin" ]]; then
GH_PATH="/opt/homebrew/bin/gh"
else
GH_PATH="/home/linuxbrew/.linuxbrew/bin/gh"
fi
# Set credential helper
git config --file "$GIT_LOCAL_CONFIG" credential."https://github.com".helper "!$GH_PATH auth git-credential"
git config --file "$GIT_LOCAL_CONFIG" credential."https://gist.github.com".helper "!$GH_PATH auth git-credential"
# --- Final Tools & Linux Extras ---
if [[ "$OS_TYPE" == "Linux" ]]; then
if command -v apt-get &> /dev/null; then
info "Installing nano via apt..."
sudo apt-get update -qq && sudo apt-get install -y -qq nano || warn "Failed to install nano"
fi
# Bash auto-switch to zsh
if ! grep -q "exec \"$ZSH_PATH\"" "$HOME/.bashrc" 2>/dev/null; then
info "Adding Zsh auto-switch to .bashrc"
cat >> "$HOME/.bashrc" <<EOF
# Auto-switch to zsh if available and interactive
if [ -t 1 ] && [ -n "\$PS1" ] && [ "\$BASH" ] && [ -x "$ZSH_PATH" ]; then
export SHELL="$ZSH_PATH"
exec "$ZSH_PATH"
fi
EOF
fi
fi
info "Installation complete! Please restart your terminal or run 'source $HOME/.zshrc'"

View File

@@ -0,0 +1,54 @@
# Starship configuration
# Disable the blank line at the start of the prompt
add_newline = false
# Custom palette for consistent branding
palette = "canva"
[palettes.canva]
blue = "#00c4cc"
purple = "#7d2ae8"
# Module configurations
[character]
success_symbol = "[](bold green)"
error_symbol = "[](bold red)"
vicmd_symbol = "[](bold green)"
[directory]
truncation_length = 3
truncation_symbol = "…/"
[git_branch]
symbol = " "
style = "bold purple"
[git_status]
ahead = "⇡${count}"
behind = "⇣${count}"
staged = "+${count}"
modified = "!${count}"
untracked = "?${count}"
[package]
disabled = true
[nix_shell]
symbol = "❄️ "
format = "via [$symbol$state]($style) "
[java]
symbol = "☕ "
[python]
symbol = "🐍 "
[nodejs]
symbol = " "
[rust]
symbol = "🦀 "
[golang]
symbol = "🐹 "

1
zsh/.tmux.conf Normal file
View File

@@ -0,0 +1 @@
set-option -g mouse on

180
zsh/.zshrc Normal file
View File

@@ -0,0 +1,180 @@
# Path to your Oh My Zsh installation.
export ZSH="$HOME/.oh-my-zsh"
# History configuration
HISTFILE=$HOME/.zsh_history
HISTSIZE=10000
SAVEHIST=10000
setopt APPEND_HISTORY # Append to history file, don't overwrite
setopt SHARE_HISTORY # Share history across all sessions
setopt HIST_IGNORE_DUPS # Don't record duplicate consecutive commands
setopt HIST_IGNORE_ALL_DUPS # Delete old duplicate entries
setopt HIST_FIND_NO_DUPS # Don't show duplicates when searching
setopt HIST_IGNORE_SPACE # Don't record commands starting with space
setopt HIST_SAVE_NO_DUPS # Don't save duplicates to history file
setopt HIST_REDUCE_BLANKS # Remove extra blanks from commands
setopt INC_APPEND_HISTORY # Write to history file immediately, not on shell exit
# Homebrew Setup
if [[ "$(uname)" == "Darwin" ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)" 2>/dev/null || eval "$(/usr/local/bin/brew shellenv)" 2>/dev/null
BREW_PREFIX=$(brew --prefix)
elif [[ "$(uname)" == "Linux" ]]; then
[ -d "/home/linuxbrew/.linuxbrew" ] && eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
BREW_PREFIX="/home/linuxbrew/.linuxbrew"
fi
# Zsh Completions (must be set before sourcing Oh My Zsh)
if [ -d "$BREW_PREFIX/share/zsh-completions" ]; then
FPATH="$BREW_PREFIX/share/zsh-completions:$FPATH"
fi
# Set name of the theme to load
ZSH_THEME="" # Using Starship instead
# Plugin configuration
plugins=(
brew
globalias
git
git-commit
direnv
nvm
pyenv
zoxide
fzf
history-substring-search
colored-man-pages
sudo
gh
golang
extract
command-not-found
)
# Only use tmux if not on macOS
if [[ "$(uname)" != "Darwin" ]]; then
plugins+=(tmux)
ZSH_TMUX_AUTOSTART=true
ZSH_TMUX_AUTOCONNECT=true
ZSH_TMUX_AUTOSTART_ONCE=true
ZSH_TMUX_ITERM2=true
fi
ZOXIDE_CMD_OVERRIDE=cd
GLOBALIAS_FILTER_VALUES=(ls cat find grep top htop diff cd regen_protos cherrypick deploy_page deploy_service setup_venv claude_local claude_devbox)
DISABLE_LS_COLORS="true"
source $ZSH/oh-my-zsh.sh
# Brew-installed plugins (source manually since they aren't in $ZSH/plugins)
if [ -f "$BREW_PREFIX/share/zsh-autosuggestions/zsh-autosuggestions.zsh" ]; then
source "$BREW_PREFIX/share/zsh-autosuggestions/zsh-autosuggestions.zsh"
fi
if [ -f "$BREW_PREFIX/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" ]; then
source "$BREW_PREFIX/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"
fi
# History substring search bindings (functions provided by history-substring-search plugin)
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down
bindkey '^[OA' history-substring-search-up
bindkey '^[OB' history-substring-search-down
# Starship initialization
eval "$(starship init zsh)"
# --- User Settings ---
# Preferred editor for local and remote sessions
export EDITOR='nano'
# Java Setup (Homebrew only)
if command -v brew &> /dev/null && brew --prefix openjdk &>/dev/null; then
export PATH="$(brew --prefix openjdk)/bin:$PATH"
fi
# CUSTOM ALIASES
alias z="nano $HOME/.zshrc"
alias s="omz reload"
alias h="history"
alias c="cursor ."
# Modern replacements
alias ls='eza --icons --group-directories-first --git --git-repos --header "$@"'
alias cat='bat --style plain'
alias find='fd'
alias grep='rg'
alias top='btop'
alias htop='btop'
alias diff='difft'
# Navigation & Utilities
alias ..="cd .."
alias ...="cd ../.."
alias ll='ls -l'
alias la='ls -a'
alias lla='ls -la'
alias q="exit"
alias path='echo -e ${PATH//:/\\n}'
alias j="java -jar"
# Git aliases
alias ga="git add"
alias gco="git checkout"
alias gca="git commit --amend"
alias gcm="git commit -m"
alias gl="git log"
alias gp="git push"
alias gpl="git pull"
alias gplr="git pull --rebase"
alias gs="git status"
alias gd='git diff'
alias gds='git diff --staged'
alias glg="git log --graph --oneline --decorate --all"
alias gbn="git rev-parse --abbrev-ref HEAD"
# msw-dotfiles sync function
function update_dotfiles() {
local repo="$HOME/work/msw-dotfiles"
local msg="${1:-Backing up dotfile changes}"
git -C "$repo" add -A
# If the index is different from origin/main, we have work to do
if ! git -C "$repo" diff-index --quiet origin/main; then
# 1. Commit if Index != HEAD
if ! git -C "$repo" diff-index --quiet HEAD; then
echo "Committing local changes..."
git -C "$repo" commit -m "$msg"
fi
# 2. Sync with remote
echo "Syncing with GitHub..."
git -C "$repo" pull --rebase origin main && git -C "$repo" push origin main
else
echo "Dotfiles are already up to date with origin/main."
fi
}
alias dotup="update_dotfiles"
# Source Canva-specific configuration if present
[ -f "$HOME/.canva.zsh" ] && source "$HOME/.canva.zsh"
# Expand aliases on Enter before executing (respects globalias filter)
function expand-alias-and-accept-line() {
local -a words
# Use (z) to split into shell words, handle potential empty LBUFFER
words=(${(z)LBUFFER})
if (( ${#words} > 0 )); then
local last_word=${words[-1]}
# Check if the last word is in our filter list
if [[ ! " ${GLOBALIAS_FILTER_VALUES[*]} " == *" $last_word "* ]]; then
zle _expand_alias
fi
fi
zle accept-line
}
zle -N expand-alias-and-accept-line
bindkey '^M' expand-alias-and-accept-line
bindkey '^J' expand-alias-and-accept-line