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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
name: Benchmark

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read
pull-requests: write

concurrency:
group: benchmark-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
zsh-startup:
name: Zsh startup
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Install benchmark environment
shell: bash
run: |
set -euo pipefail

sudo apt-get update
sudo apt-get install -y hyperfine zsh
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install atuin fnm fzf zoxide

mapfile -t insecure_dirs < <(
zsh -fc 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"; autoload -Uz compaudit; compaudit'
)
if (( ${#insecure_dirs[@]} )); then
sudo chmod go-w "${insecure_dirs[@]}"
fi

- name: Prepare benchmark homes
shell: bash
run: |
set -euo pipefail

clone_at() {
local repository="$1"
local revision="$2"
local destination="$3"

git init --quiet "$destination"
git -C "$destination" remote add origin "$repository"
git -C "$destination" fetch --quiet --depth=1 origin "$revision"
git -C "$destination" checkout --quiet --detach FETCH_HEAD
}

fixture_template="$RUNNER_TEMP/fixture-template"
zinit_template="$fixture_template/.local/share/zinit"
mkdir -p "$zinit_template/plugins"
clone_at https://github.com/zdharma-continuum/zinit.git fcd2501fa347c51a7048452becad61f86ed0ad0c "$zinit_template/zinit.git"
clone_at https://github.com/zsh-users/zsh-autosuggestions.git 85919cd1ffa7d2d5412f6d3fe437ebdbeeec4fc5 "$zinit_template/plugins/zsh-users---zsh-autosuggestions"
clone_at https://github.com/zsh-users/zsh-syntax-highlighting.git 1d85c692615a25fe2293bdd44b34c217d5d2bf04 "$zinit_template/plugins/zsh-users---zsh-syntax-highlighting"
clone_at https://github.com/romkatv/powerlevel10k.git 9253fb1c5034410c43a0c681ff8294181c54016c "$zinit_template/plugins/romkatv---powerlevel10k"

(
export FNM_DIR="$fixture_template/.local/share/fnm"
eval "$(/home/linuxbrew/.linuxbrew/bin/fnm env --shell bash)"
/home/linuxbrew/.linuxbrew/bin/fnm install --lts --use
/home/linuxbrew/.linuxbrew/bin/fnm default "$(/home/linuxbrew/.linuxbrew/bin/fnm current)"
)

prepare_ref() {
local ref="$1"
local label="$2"
local source="$RUNNER_TEMP/suitup-$label"
local home="$RUNNER_TEMP/home-$label"

git worktree add --quiet --detach "$source" "$ref"
mkdir -p "$home/.config/zsh" "$home/.local/share"
cp -R "$source/configs/core" "$source/configs/shared" "$source/configs/local" "$home/.config/zsh/"
cp -R "$zinit_template" "$fixture_template/.local/share/fnm" "$home/.local/share/"
cp "$source/configs/zshrc.template" "$home/.zshrc"
cp "$source/configs/zshenv.template" "$home/.zshenv"
}

prepare_ref HEAD current
prepare_ref origin/main main
prepare_ref HEAD^ previous

for label in current main previous; do
home="$RUNNER_TEMP/home-$label"
startup_log="$RUNNER_TEMP/$label-startup.log"

if ! env HOME="$home" ZDOTDIR="$home" SUITUP_STARTUP_REPORT_THRESHOLD_MS=999999 zsh -i -c exit >"$startup_log" 2>&1; then
cat "$startup_log"
exit 1
fi

for cache in .zcompdump atuin-init.zsh fnm-init.zsh zoxide-init.zsh; do
if [[ ! -s "$home/.cache/zsh/$cache" ]]; then
echo "::error::Missing $cache after warming $label"
env HOME="$home" ZDOTDIR="$home" zsh -i -c 'command -v brew atuin fnm fzf zoxide'
ls -la "$home/.cache/zsh" || true
cat "$startup_log"
exit 1
fi
done
done

sleep 1

- name: Compare startup time
shell: bash
run: |
set -euo pipefail

current="current ($(git rev-parse --short HEAD))"
main="main ($(git rev-parse --short origin/main))"
previous="previous commit ($(git rev-parse --short HEAD^))"
current_command="env HOME='$RUNNER_TEMP/home-current' ZDOTDIR='$RUNNER_TEMP/home-current' SUITUP_STARTUP_REPORT_THRESHOLD_MS=999999 zsh -i -c exit"
main_command="env HOME='$RUNNER_TEMP/home-main' ZDOTDIR='$RUNNER_TEMP/home-main' SUITUP_STARTUP_REPORT_THRESHOLD_MS=999999 zsh -i -c exit"
previous_command="env HOME='$RUNNER_TEMP/home-previous' ZDOTDIR='$RUNNER_TEMP/home-previous' SUITUP_STARTUP_REPORT_THRESHOLD_MS=999999 zsh -i -c exit"

hyperfine \
--warmup 5 \
--runs 30 \
--export-json warm-benchmark.json \
--export-markdown warm-benchmark.md \
--command-name "$current" "$current_command" \
--command-name "$main" "$main_command" \
--command-name "$previous" "$previous_command"

hyperfine \
--prepare "rm -rf '$RUNNER_TEMP/home-current/.cache/zsh' '$RUNNER_TEMP/home-main/.cache/zsh' '$RUNNER_TEMP/home-previous/.cache/zsh'" \
--warmup 2 \
--runs 10 \
--export-json cache-miss-benchmark.json \
--export-markdown cache-miss-benchmark.md \
--command-name "$current" "$current_command" \
--command-name "$main" "$main_command" \
--command-name "$previous" "$previous_command"

{
echo "## Zsh startup benchmark"
echo
echo "Ubuntu 24.04 with Linuxbrew, atuin, fnm, fzf, zoxide, zinit, and the shipped plugins. Lower is better."
echo
echo "### Warm startup (daily use)"
echo
cat warm-benchmark.md
echo
echo "### Cache-miss startup"
echo
echo "Completion and tool-init caches are removed before every run; installed tools and plugins remain."
echo
cat cache-miss-benchmark.md
} | tee benchmark-comment.md >> "$GITHUB_STEP_SUMMARY"

- name: Comment benchmark on PR
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
run: gh pr comment "${{ github.event.pull_request.number }}" --edit-last --create-if-none --body-file benchmark-comment.md

- name: Upload benchmark results
uses: actions/upload-artifact@v7
with:
name: zsh-startup-benchmark
path: |
warm-benchmark.json
warm-benchmark.md
cache-miss-benchmark.json
cache-miss-benchmark.md
retention-days: 30
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The actual config lives under `~/.config/zsh/`.
### Local files

- `configs/local/machine.zsh`: machine-specific overrides placeholder
- `configs/local/aliases.zsh`: user-owned aliases, initialized once and preserved across clean/reset
- `~/.config/zsh/local/secrets.zsh` is user-managed and intentionally not shipped by suitup

## Templates
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Named after Barney's catchphrase from [How I Met Your Mother](https://www.themov
- **Clean mode** — remove suitup config files
- `--help` output for quick command discovery
- Backs up existing zsh startup files to `~/.config/zsh/backups/` before changing shell startup config
- Keeps personal aliases in `~/.config/zsh/local/aliases.zsh`, outside suitup-managed updates and clean/reset removal
- Powerlevel10k is optional; recommended because its async git status stays responsive in large repositories
- Idempotent — safe to run multiple times
- No private/company-specific content — clean, generic configs
Expand Down Expand Up @@ -152,6 +153,7 @@ node src/cli.js append
Uses idempotent marker blocks (`# >>> suitup/... >>>`) to safely append selected configs and re-run related installers when required tools are missing:

- Suitup aliases
- User aliases (`edit-user-alias` opens `~/.config/zsh/local/aliases.zsh`)
- Zinit plugins
- Powerlevel10k prompt or basic prompt preset (replaces `~/.config/zsh/shared/prompt.zsh`)
- Tool initialization (atuin, fzf, zoxide, fnm)
Expand All @@ -160,6 +162,8 @@ Uses idempotent marker blocks (`# >>> suitup/... >>>`) to safely append selected
- Startup performance monitor
- FZF configuration

The startup timing table is shown only when startup takes at least `100ms`. Set `SUITUP_STARTUP_REPORT_THRESHOLD_MS=0` to always show it.

For suitup-managed shared config files such as aliases and zinit plugins, append mode also previews safe line additions before applying them. Prompt/theme files are not line-merged because they often contain generated or user-tuned state that cannot be reliably reconciled.

### Verify
Expand Down Expand Up @@ -198,6 +202,7 @@ Attempts a safe uninstall of suitup-managed config:
- removes suitup-generated `~/.config/zsh/` files when they still match shipped templates
- strips `# >>> suitup/... >>>` blocks from an existing `~/.zshrc` if you used `append`
- preserves user-modified files instead of deleting them blindly
- preserves `~/.config/zsh/local/aliases.zsh`

### Help

Expand Down Expand Up @@ -265,6 +270,7 @@ After setup, your shell config looks like:
prompt.zsh # Prompt/theme (p10k)
local/
machine.zsh # Machine-specific overrides
aliases.zsh # User aliases, preserved across updates/reset
config.vim # Vim config
secrets.zsh # API keys (create manually, gitignored)
```
Expand Down
6 changes: 6 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- **清理模式**:删除 suitup 生成的配置
- 提供 `--help`,方便快速查看命令
- 修改 Shell 启动配置前,会先把现有 zsh 启动文件备份到 `~/.config/zsh/backups/`
- 个人 alias 保存在 `~/.config/zsh/local/aliases.zsh`,不会被 suitup 更新或 clean/reset 删除
- Powerlevel10k 为可选项;推荐开启,因为它在大型 Git 仓库里的异步 git 状态会更流畅
- 幂等执行,可安全重复运行
- 不包含私有/公司特定内容
Expand Down Expand Up @@ -152,6 +153,7 @@ node src/cli.js append
通过幂等标记块(`# >>> suitup/... >>>`)安全追加;如果相关工具缺失,也会一起重试安装:

- aliases
- 用户 alias(`edit-user-alias` 会打开 `~/.config/zsh/local/aliases.zsh`)
- zinit 插件
- Powerlevel10k prompt 或基础 prompt 预设(会替换 `~/.config/zsh/shared/prompt.zsh`)
- 工具初始化(atuin、fzf、zoxide、fnm)
Expand All @@ -160,6 +162,8 @@ node src/cli.js append
- 启动性能报告
- FZF 配置

启动耗时达到 `100ms` 时才会显示计时表;设置 `SUITUP_STARTUP_REPORT_THRESHOLD_MS=0` 可始终显示。

对于 aliases、zinit plugins 等 suitup 管理的共享配置文件,append 模式也会在应用安全的行级增量前展示 diff 预览。Prompt / theme 文件不会做行级合并,因为其中常包含生成内容或用户调校状态,无法可靠自动协调。

### Verify(验证)
Expand Down Expand Up @@ -198,6 +202,7 @@ node src/cli.js clean
- 对仍与项目模板一致的文件,删除 `~/.config/zsh/` 下的 suitup 生成内容
- 如果你用过 `append`,会从现有 `~/.zshrc` 中移除 `# >>> suitup/... >>>` 标记块
- 对用户自己改过的文件会保留,不会盲删
- 保留 `~/.config/zsh/local/aliases.zsh`

### Help(帮助)

Expand Down Expand Up @@ -263,6 +268,7 @@ node src/cli.js --help
prompt.zsh # 提示符主题(p10k)
local/
machine.zsh # 机器本地覆盖
aliases.zsh # 用户 alias,更新/reset 时保留
config.vim # Vim 配置
secrets.zsh # 个人密钥(手动创建,不纳入 git)
```
Expand Down
27 changes: 16 additions & 11 deletions configs/core/paths.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@

# Homebrew can live outside the default PATH on fresh macOS/Linux installs.
# Load its shellenv early so later tool detection works after suitup rewrites ~/.zshrc.
for _suitup_brew_bin in \
"${HOMEBREW_PREFIX:+$HOMEBREW_PREFIX/bin/brew}" \
/opt/homebrew/bin/brew \
/home/linuxbrew/.linuxbrew/bin/brew \
/usr/local/bin/brew
do
[[ -n "$_suitup_brew_bin" && -x "$_suitup_brew_bin" ]] || continue
eval "$("$_suitup_brew_bin" shellenv zsh)"
break
done
unset _suitup_brew_bin
if [[ -z "${_SUITUP_BREW_ENV_CHECKED:-}" ]]; then
typeset -g _SUITUP_BREW_ENV_CHECKED=1
if (( ! $+commands[brew] )); then
for _suitup_brew_bin in \
"${HOMEBREW_PREFIX:+$HOMEBREW_PREFIX/bin/brew}" \
/opt/homebrew/bin/brew \
/home/linuxbrew/.linuxbrew/bin/brew \
/usr/local/bin/brew
do
[[ -n "$_suitup_brew_bin" && -x "$_suitup_brew_bin" ]] || continue
eval "$("$_suitup_brew_bin" shellenv zsh)"
break
done
unset _suitup_brew_bin
fi
fi

# fnm (Fast Node Manager) — keep the fnm binary itself on PATH after suitup
# rewrites ~/.zshrc, then expose the default Node installation to all shells
Expand Down
13 changes: 12 additions & 1 deletion configs/core/perf.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,19 @@ _zsh_report() {

local -F 6 end=${EPOCHREALTIME:-0}
local -F 6 total_ms=$(( (end - _zsh_start_time) * 1000.0 ))
local -F 6 threshold_ms=${SUITUP_STARTUP_REPORT_THRESHOLD_MS:-100}
local show_completion_hint=false
local i

if [[ "${SUITUP_SHOW_COMPLETION_CACHE_HINT:-0}" == '1' && "${_zsh_completion_cache_mode:-}" == 'cache-hit' && -n "${_zsh_compdump_file:-}" ]]; then
show_completion_hint=true
fi

if (( total_ms < threshold_ms )); then
[[ "$show_completion_hint" == 'true' ]] && printf 'completion cache hit; remove %s to rebuild\n' "$_zsh_compdump_file"
return 0
fi

echo ''
echo '┌────────────────────────────┐'

Expand All @@ -76,7 +87,7 @@ _zsh_report() {
_print_duration_row 'total' "$total_ms"
echo '└────────────────────────────┘'

if [[ "${SUITUP_SHOW_COMPLETION_CACHE_HINT:-0}" == '1' && "${_zsh_completion_cache_mode:-}" == 'cache-hit' && -n "${_zsh_compdump_file:-}" ]]; then
if [[ "$show_completion_hint" == 'true' ]]; then
printf 'completion cache hit; remove %s to rebuild\n' "$_zsh_compdump_file"
fi
}
8 changes: 8 additions & 0 deletions configs/local/aliases.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# ============================================================================
# User aliases
# ============================================================================
# Add personal aliases here. Suitup creates this file once and never overwrites
# or removes it during updates and clean/reset flows.
#
# Example:
# alias work="cd ~/workspace"
1 change: 1 addition & 0 deletions configs/shared/aliases.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ alias reload-zsh="source ~/.zshrc"
alias edit-zsh="${EDITOR:-vi} ~/.zshrc"
alias edit-plugins="${EDITOR:-vi} ~/.config/zsh/shared/plugins.zsh"
alias edit-aliases="${EDITOR:-vi} ~/.config/zsh/shared/aliases.zsh"
alias edit-user-alias="${EDITOR:-vi} ~/.config/zsh/local/aliases.zsh"
alias ll="eza -abghlS --color=always --icons=always"
alias ls="eza -s=name --group-directories-first --color=always --icons=always"
alias ltree="eza -abghS --icons=always --tree --git-ignore"
Expand Down
5 changes: 4 additions & 1 deletion configs/shared/completion.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ autoload -Uz compinit

_zsh_compdump_file="${XDG_CACHE_HOME:-$HOME/.cache}/zsh/.zcompdump"
typeset -g _zsh_completion_cache_mode='refresh'
typeset -a _zsh_fresh_compdump=("$_zsh_compdump_file"(N.m-7L+0))

if [[ -s "$_zsh_compdump_file" && -n "$(command find "$_zsh_compdump_file" -mtime -7 -print 2>/dev/null)" ]]; then
if (( ${#_zsh_fresh_compdump} )); then
_zsh_completion_cache_mode='cache-hit'
compinit -C -d "$_zsh_compdump_file"
else
compinit -d "$_zsh_compdump_file"
zcompile "$_zsh_compdump_file"
fi
unset _zsh_fresh_compdump

bindkey -M emacs '^I' expand-or-complete
bindkey -M viins '^I' expand-or-complete
Expand Down
Loading