Zuletzt aktiv 1 day ago

Cross-platform Zsh setup scripts and managed dotfiles with Oh My Zsh framework, Starship prompt, aliases, functions, and path configuration.

Änderung 2882841314607b44f2a027743f95e826340b9556

README.md Originalformat

Bash Script Installer for Zsh

The "Bash Script Installer" simplifies the setup of Zsh.

Zsh

Ubuntu

bash -c "$(curl -fsSL https://tinyurl.com/y6fhb594)"

WSL

bash -c "$(curl -fsSL https://tinyurl.com/ycyct6wp)"

MacOS

bash -c "$(curl -fsSL https://tinyurl.com/yptybtpa)"

Configuration

bash -c "$(curl -fsSL https://tinyurl.com/y8vf753j)"

Shell framework and prompt

The installers use Oh My Zsh for framework features and plugins, with the OMZ theme disabled:

ZSH_THEME=""
source "$ZSH/oh-my-zsh.sh"
eval "$(starship init zsh)"

Starship owns the prompt and loads after Oh My Zsh. The managed Starship config is installed at ~/.config/starship.toml. A Nerd Font is required for the Powerline symbols.

AI commits

The managed aicommit function can use Claude, Copilot, or OpenCode to create a commit from staged changes:

aicommit opencode

The installers place the shared commit instructions at ~/.config/ai-commit-prompt.txt and the required OpenCode agent at ~/.config/opencode/agents/ai-commit.md. The agent is hidden from the interactive agent selector but remains available to aicommit opencode. It may inspect Git status, the cached diff, and recent history, and may execute git commit; all unrelated tools and shell commands are denied.

Changes affecting more than 30 staged paths automatically use per-module commits. They must produce multiple commits, rather than a single combined commit message. The count uses the full staged set before diff filtering or truncation, including lock/minified files and both paths of a rename. Exactly 30 paths retains the single-commit default. Pass --modules to request splitting at any size:

aicommit --modules opencode
aicommit --modules --yes claude

Automatic splitting uses the same planning and confirmation flow as --modules; --yes skips confirmation in either case (for example, aicommit --yes opencode). Plans that collapse a change above the threshold into one group are rejected before committing, with the index untouched. Grouping follows cohesive modules or submodules; 30 is the automatic-splitting threshold, not a hard size limit per planned module.

The engine first plans the split using ~/.config/ai-commit-split-prompt.txt and replies with a JSON plan that maps every staged path to a type(scope) group, ordered so dependencies land first. The function validates the plan against the staged paths, ignores paths that are not staged, appends any unplanned paths as a final commit, and shows the plan for confirmation (--yes skips it). It then stages each group from a snapshot of the original index, so partially staged files keep only their staged hunks and unstaged or untracked work is never touched, and asks the engine to commit that group with the normal commit instructions. If a group produces no commit, or a commit includes files outside its group, the run stops and restages the remaining groups. With OpenCode, planning uses the hidden ai-commit-plan agent at ~/.config/opencode/agents/ai-commit-plan.md, which is denied every tool. Python 3 is required to parse the plan.

Plan-build workflow

The local plan_build function is a thin cached launcher for the canonical public plan-build gist. When curl is available, each invocation stages plan_build.zsh, SKILL.md, and ARCHITECT.md, requires all three to be non-empty, and syntax-checks the executable. It then moves the complete bundle into a unique immutable release directory and atomically switches one current symlink. Refresh activation is serialized with a kernel-backed Zsh file lock so concurrent callers cannot mix releases and crashes cannot leave a stale lock. If refresh fails, the launcher preserves and uses the last complete, validated release and repairs its skill links. Zsh is always required; curl is required only when no valid cached release exists.

plan_build
plan_build --prompt
plan_build --writing-plan
plan_build --prompt --brainstorm
plan_build --yolo --writing-plan
plan_build --architect
plan_build --architect --new
plan_build --architect --yolo

The canonical executable defines the current command behavior and options. Each invocation binds the executable and both skills to its selected immutable release. The launcher also points the installed Claude Code skill paths through the active current release:

  • SKILL.md → ~/.claude/skills/plan-build/SKILL.md
  • ARCHITECT.md → ~/.claude/skills/plan-build-architect/SKILL.md

The launcher defaults to https://opengist.resetrix.work/weehong/plan-build/raw/HEAD and ${XDG_CACHE_HOME:-$HOME/.cache}/plan-build. Override these with PLAN_BUILD_BASE_URL and PLAN_BUILD_CACHE_DIR. Installed skill paths remain fixed at ~/.claude/skills; cached invocations receive immutable release paths through environment variables. The downloaded script is remote executable code: syntax validation catches malformed Zsh but is not a security sandbox, so only point PLAN_BUILD_BASE_URL at a source you trust. Remote code is executed in a child Zsh process and is never sourced into the caller shell.

Invoking plan_build once installs or repairs both skill links. To install standalone files without the cached launcher, follow the installation instructions in the canonical gist instead.

ai-commit-plan.md Originalformat

description: Plans how to split the staged diff supplied in the prompt into per-module commits. mode: primary hidden: true permission: "*": deny

Follow the supplied planning instructions and staged file list. Reply with only the JSON plan. Do not run commands, modify files, or change the staged set.

Changes affecting more than 30 staged paths must be modularized into at least two non-empty commits; never return a single combined group. Count the full staged file list, including lock/minified files and both paths of a rename. Split a large module into cohesive, independently reviewable submodules. At 30 paths or fewer, a single group is allowed if the changes form one module. This threshold is not a hard file limit per planned module.

ai-commit-prompt.txt Originalformat
1# Git Commit Message Generator Prompt
2
3## Role
4You are an Automated Git Commit Engine.
5
6## Objective
7Analyze staged changes (`git diff --cached`), identify the **primary logical change**, and generate a commit message that **strictly adheres** to Conventional Commits v1.0.0.
8
9---
10
11## Input
12The user will provide the output of:
13```bash
14git diff --cached
15```
16
17---
18
19## Strict Rules (Zero Tolerance)
20
21### Mandatory Module Splitting
22Changes affecting more than 30 staged paths MUST be modularized into
23multiple commits. Do not generate or execute a single combined commit for
24them. Count the full staged set, including lock/minified files omitted from
25the diff preview and both paths of a rename, not just the visible diff.
26If this input has more than 30 staged paths and is not explicitly identified
27as one module in a multi-commit plan, stop and request `aicommit --modules`.
28This refusal takes precedence over the output and execution instructions below.
29When the input says "commit X of Y, split by module" with Y greater than 1,
30generate and execute only that module's commit. The threshold applies to
31the original staged change, not a hard file limit per planned module.
32
33### 1. Header Format
34**Pattern:** `type(scope): subject`
35
36| Element | Rule |
37| :--- | :--- |
38| **Type** | ONLY use: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore` |
39| **Scope** | Optional, in parentheses, entirely lowercase (e.g., `docker`, `api`, `auth`) |
40| **Subject** | Lowercase, imperative mood ("add" not "added"), NO period at the end |
41| **Length** | Header MUST be ≤ 50 characters |
42
43### 2. Body Format
44| Element | Rule |
45| :--- | :--- |
46| **Spacing** | Exactly ONE blank line between the header and the body |
47| **Bullet Points**| Use hyphens (`-`) only |
48| **Line Length** | Each line MUST be ≤ 72 characters |
49| **Case** | Lowercase throughout |
50
51### 3. Content Rules
52* **NO** "Merge branch" or auto-generated text.
53* **NO** Markdown formatting (bold/italic/code blocks) inside the commit message string itself.
54* **NO** periods at the end of the subject line.
55* **NO** "Co-authored-by" trailers or any co-author attributions.
56
57---
58
59## Execution Steps
60
61### Step 1: Atomic Scan
62Identify the **largest cluster of related changes** (the "Primary Scope").
63
64**Priority Order for Type Selection:**
651. `ci` - Pipeline, CI/CD configuration changes
662. `feat` - New features
673. `fix` - Bug fixes
684. `refactor` - Code restructuring
695. `build` - Build system, dependencies
706. `test` - Test additions/modifications
717. `chore` - Maintenance tasks
728. `docs` - Documentation only (use ONLY if no code changes exist)
73
74### Step 2: Classify Changes
75Categorize all changed files:
76
77| Category | Influences Subject? | Include in Body? |
78| :--- | :---: | :---: |
79| **Primary Scope** | ✅ Yes | ✅ Yes |
80| **Supporting Changes** | ❌ No | ✅ Yes |
81| **Documentation** | ❌ No | ✅ Yes (as a single line) |
82
83> **Key Rule:** Documentation changes (`docs/`) should be mentioned in the body but MUST NOT influence the subject type unless documentation is the *only* change in the commit.
84
85### Step 3: Draft Commit Message
86Structure the message exactly like this:
87```text
88type(scope): subject (≤50 chars, imperative, lowercase, no period)
89
90- primary change detail 1
91- primary change detail 2
92- supporting change detail
93- add/update project documentation
94```
95
96---
97
98## Output Format & Execution Authorization
99
100You must output **ONLY** a single bash code block containing the complete git command. Do not provide any conversational filler, explanations, or greeting.
101
102Once you have generated the `git commit` bash command, **you are explicitly authorized to execute it directly in the terminal** (if your environment supports agentic execution).
103* Do not ask for further user confirmation.
104* Execute sequentially.
105* If the command fails, output the exact error message and halt execution.
106
107**Format Template:**
108```bash
109git commit -m "type(scope): subject
110
111- body detail 1
112- body detail 2
113- body detail 3"
114```
115
116---
117
118## Examples
119
120### Example 1: CI/CD with Documentation
121* **Changes:** `.gitlab-ci.yaml` (new), `Dockerfile` (new), `docs/deployment.md` (new)
122* **Output:**
123```bash
124git commit -m "ci: add gitlab pipeline and docker config
125
126- add gitlab-ci.yaml with build, test, publish stages
127- add Dockerfile for container deployment
128- add deployment documentation"
129```
130
131### Example 2: Feature with Config Changes
132* **Changes:** `src/auth/login.java` (new), `pom.xml` (modified), `docs/api.md` (new)
133* **Output:**
134```bash
135git commit -m "feat(auth): add user login endpoint
136
137- implement login service with jwt tokens
138- add spring-security dependency to pom.xml
139- add api documentation"
140```
141
142### Example 3: Documentation Only
143* **Changes:** `docs/setup.md` (new), `docs/testing.md` (modified), `README.md` (modified)
144* **Output:**
145```bash
146git commit -m "docs: add setup guide and update testing docs
147
148- add initial setup guide
149- update testing documentation with examples
150- update readme with quick start section"
151```
152
153---
154
155## Quick Reference
156
157| Type | When to Use |
158| :--- | :--- |
159| `feat` | New feature for the user |
160| `fix` | Bug fix for the user |
161| `docs` | Documentation only changes |
162| `style` | Formatting, missing semicolons, etc. (no code change) |
163| `refactor` | Code change that neither fixes a bug nor adds a feature |
164| `perf` | Code change that improves performance |
165| `test` | Adding or refactoring tests |
166| `build` | Build system or external dependencies |
167| `ci` | CI configuration files and scripts |
168| `chore` | Maintenance tasks, no production code changes |
169
ai-commit-split-prompt.txt Originalformat
1# Git Commit Split Planner Prompt
2
3## Role
4You are an Automated Git Commit Planner.
5
6## Objective
7Split the staged changes into a sequence of small, atomic commits, one per
8**module**. You only decide the grouping and order. You do NOT write commit
9messages and you do NOT run any commands.
10
11---
12
13## Input
141. The staged file list (`git diff --cached --name-status -M`)
152. A diffstat (`git diff --cached --numstat -M`: added, deleted, path)
163. The staged diff (lock files and minified files are omitted; it may be truncated)
17
18---
19
20## What Is a Module
21A module is one cohesive, independently reviewable unit of change that maps
22to a single Conventional Commit `type(scope)`. Typical modules:
23
24| Module | Example scope |
25| :--- | :--- |
26| A feature or domain area (pages, routes, services, models) | `organisation`, `auth` |
27| Shared UI components / design system | `ui` |
28| Translations / localisation | `i18n` |
29| End-to-end or integration tests not tied to one module | `e2e` |
30| Storybook stories | `storybook` |
31| Documentation | `docs` |
32| Build, dependencies, tooling config | `deps`, `eslint` |
33| CI/CD pipelines | `ci` |
34
35---
36
37## Strict Rules (Zero Tolerance)
38
391. Every path in the staged file list MUST appear in exactly one commit.
40 Copy paths exactly as listed. Never invent, rename, or omit a path.
412. A rename line (`R<score> old new`) lists two paths. Put BOTH in the same
42 commit.
433. Never split a single file across commits.
444. Lock files go in the same commit as the manifest that changed them
45 (e.g. `package.json` with `package-lock.json`).
465. Unit tests that sit next to, or only exercise, one module go in that
47 module's commit. E2E tests, storybook stories and docs get their own
48 commits unless they are the only change.
496. Each commit should have ONE type and ONE scope. If a group would need two
50 types (e.g. `feat` and `docs`), split it.
517. Do not over-split. A module normally spans several files; do not create
52 one commit per file unless each file really is its own module. Changes
53 affecting more than 30 staged paths MUST be modularized into at least two
54 non-empty commits; never return a single combined commit for them. Count
55 all staged paths, including lock/minified files and both paths of a rename.
56 Split a large module into cohesive, independently reviewable submodules.
57 At 30 paths or fewer, a single commit is allowed if all changes form one
58 module. This threshold is not a hard file limit per planned module.
598. Order commits so dependencies land first:
60 build/deps -> shared code (ui, i18n, lib, types) -> features that use
61 them -> tests -> stories/docs.
62
63---
64
65## Output Format
66
67Output ONLY one JSON object. No prose, no explanation, no markdown fences.
68
69```json
70{"commits":[{"type":"feat","scope":"i18n","files":["src/locales/en/organisation.json"]}]}
71```
72
73* `type`: one of `feat`, `fix`, `docs`, `style`, `refactor`, `perf`,
74 `test`, `build`, `ci`, `chore`
75* `scope`: lowercase scope, or an empty string if none fits
76* `files`: the exact paths from the staged file list
77
78---
79
80## Example
81
82* **Staged files:** `src/locales/en/organisation.json`,
83 `src/locales/en/settings.json`, `src/components/ui/data-table.tsx`,
84 `src/components/ui/stat-card.tsx`, `src/app/organisation/page.tsx`,
85 `src/app/organisation/list/page.tsx`,
86 `src/components/ui/data-table.stories.tsx`, `e2e/home.spec.ts`
87* **Output:**
88```json
89{"commits":[
90{"type":"feat","scope":"i18n","files":["src/locales/en/organisation.json","src/locales/en/settings.json"]},
91{"type":"feat","scope":"ui","files":["src/components/ui/data-table.tsx","src/components/ui/stat-card.tsx"]},
92{"type":"feat","scope":"organisation","files":["src/app/organisation/page.tsx","src/app/organisation/list/page.tsx"]},
93{"type":"docs","scope":"storybook","files":["src/components/ui/data-table.stories.tsx"]},
94{"type":"test","scope":"e2e","files":["e2e/home.spec.ts"]}
95]}
96```
97
ai-commit.md Originalformat

description: Creates one conventional commit from the staged diff supplied in the prompt. mode: primary hidden: true permission: "": deny bash: "": deny "git status ": allow "git diff --cached": allow "git log *": allow "git commit *": allow

Follow the supplied commit instructions and staged diff. Changes affecting more than 30 staged paths must be modularized into multiple commits; never emit or execute a single combined commit for them. Use the supplied full staged path count, or inspect git diff --cached --name-only --no-renames if it is absent, including files omitted from the diff preview. Unless the input explicitly identifies this as one module in a multi-commit plan ("commit X of Y, split by module", Y > 1), stop and request aicommit --modules when the count exceeds 30. This refusal takes precedence over commit execution.

For an eligible staged change or an individual module from that plan, generate a concise conventional commit message, then execute exactly one git commit with that message. You may inspect Git status, the cached diff, and recent log. Do not modify files or change the staged set.

alias Originalformat
1# =============================================================================
2# OS-SPECIFIC ALIASES
3# =============================================================================
4if [[ "$OSTYPE" == "linux-gnu"* ]] && command -v apt-get >/dev/null 2>&1; then
5 # Linux Only System update & cleanup
6 alias uu='sudo apt-get update && \
7 sudo apt-get upgrade -y && \
8 sudo apt-get full-upgrade -y && \
9 sudo apt-get autoremove -y && \
10 sudo apt-get autoclean -y && \
11 sudo apt-get clean'
12elif [[ "$OSTYPE" == "darwin"* ]]; then
13 # macOS Only
14 alias flushdns='sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder'
15fi
16
17# =============================================================================
18# UNIVERSAL GIT ALIASES (Fixed: Removed broken "$@" from aliases)
19# =============================================================================
20alias gph='git push'
21alias gco='git checkout'
22alias gbh='git branch'
23alias gmt='git commit'
24alias gpl='git pull'
25alias grb='git rebase'
26alias grt='git reset'
27alias gst='git status'
28alias gsts='git stash push --staged'
29# Keep staged changes in the index; the stash also records their snapshot.
30alias gstu='git stash push --keep-index --include-untracked'
31alias gsta='git stash push --include-untracked'
32alias grmrf='git checkout -- . && git clean -fd'
33
34# Create a feature worktree from origin/integration: gwa pill
35gwa() {
36 if [ "$#" -ne 1 ] || [ -z "$1" ]; then
37 printf 'Usage: gwa <feature-name>\n' >&2
38 return 1
39 fi
40 git worktree add "../worktree/$1" -b "feature/$1" origin/integration
41}
42
43# List loaded aliases, optionally filtered by a case-insensitive keyword.
44ah() {
45 if [ "$#" -eq 0 ]; then
46 alias
47 else
48 alias | command grep -iF -- "$*"
49 fi
50}
51
config.sh Originalformat
1#!/bin/bash
2set -euo pipefail
3
4# Configuration
5GIST_RAW_BASE="https://opengist.resetrix.work/weehong/f0d940c3c1214bf5b7996195199fdc09/raw/HEAD"
6CONFIG_FILES=(
7 ".alias"
8 ".func"
9 ".pathrc"
10 ".sourcerc"
11 ".vimrc"
12 ".zshrc"
13 ".config/starship.toml"
14 ".config/ai-commit-prompt.txt"
15 ".config/ai-commit-split-prompt.txt"
16 ".config/opencode/agents/ai-commit.md"
17 ".config/opencode/agents/ai-commit-plan.md"
18)
19
20echo "Starting configuration download..."
21download_failed=0
22
23for f in "${CONFIG_FILES[@]}"; do
24 # Remove the leading dot for the URL path
25 remote_name="${f#.}"
26 [[ "$f" == ".config/starship.toml" ]] && remote_name="starship.toml"
27 [[ "$f" == ".config/ai-commit-prompt.txt" ]] && remote_name="ai-commit-prompt.txt"
28 [[ "$f" == ".config/ai-commit-split-prompt.txt" ]] && remote_name="ai-commit-split-prompt.txt"
29 [[ "$f" == ".config/opencode/agents/ai-commit.md" ]] && remote_name="ai-commit.md"
30 [[ "$f" == ".config/opencode/agents/ai-commit-plan.md" ]] && remote_name="ai-commit-plan.md"
31 url="$GIST_RAW_BASE/$remote_name"
32 target="$HOME/$f"
33 tmp="${target}.tmp.$$"
34
35 echo "Downloading $f..."
36 mkdir -p "$(dirname "$target")"
37
38 # Use -f to fail silently on server errors, -s for silent, -L to follow redirects
39 if curl -fsSL "$url" -o "$tmp" && mv "$tmp" "$target"; then
40 echo "Successfully updated $target"
41 else
42 rm -f "$tmp"
43 echo "Error: Failed to download $f from $url" >&2
44 download_failed=1
45 fi
46done
47
48if [ "$download_failed" -ne 0 ]; then
49 echo "Configuration download completed with errors." >&2
50 exit 1
51fi
52
53echo "Done! All configuration files have been replaced."
54
func Originalformat
1# =============================================================================
2# CUSTOM FUNCTIONS
3# =============================================================================
4
5# -----------------------------------------------------------------------------
6# Function: funcs (List available .func functions)
7# -----------------------------------------------------------------------------
8funcs() {
9 local func_file="${1:-$HOME/.func}"
10
11 if [ ! -f "$func_file" ]; then
12 printf "Error: Function file '%s' not found.\n" "$func_file" >&2
13 return 1
14 fi
15
16 printf "Available functions in %s:\n" "$func_file"
17 awk '
18 /^# Function: / {
19 line = $0
20 sub(/^# Function: /, "", line)
21
22 name = line
23 sub(/ .*/, "", name)
24
25 desc = ""
26 if (line ~ / \(.+\)$/) {
27 desc = line
28 sub(/^[^ ]+ \(/, "", desc)
29 sub(/\)$/, "", desc)
30 }
31
32 if (desc != "") {
33 printf " %-22s %s\n", name, desc
34 } else {
35 printf " %s\n", name
36 }
37 }
38 ' "$func_file"
39}
40
41# -----------------------------------------------------------------------------
42# Function: clip (Cross-platform clipboard)
43# -----------------------------------------------------------------------------
44clip() {
45 local cmd
46 local args=()
47
48 if command -v pbcopy >/dev/null 2>&1; then
49 cmd="pbcopy" # macOS
50 elif grep -qi "microsoft" /proc/version 2>/dev/null && command -v clip.exe >/dev/null 2>&1; then
51 cmd="clip.exe" # WSL
52 elif [ "$XDG_SESSION_TYPE" = "wayland" ] && command -v wl-copy >/dev/null 2>&1; then
53 cmd="wl-copy" # Linux Wayland
54 elif command -v xclip >/dev/null 2>&1; then
55 cmd="xclip" # Linux X11 (Fallback 1)
56 args=("-selection" "clipboard")
57 elif command -v xsel >/dev/null 2>&1; then
58 cmd="xsel" # Linux X11 (Fallback 2)
59 args=("--clipboard" "--input")
60 else
61 printf "Error: No supported clipboard utility found.\n" >&2
62 return 1
63 fi
64
65 if [ $# -gt 0 ]; then
66 if [ -f "$1" ]; then
67 "$cmd" "${args[@]}" < "$1"
68 echo "Copied contents of '$1' to clipboard."
69 else
70 printf "Error: File '%s' not found.\n" "$1" >&2
71 return 1
72 fi
73 else
74 "$cmd" "${args[@]}"
75 fi
76}
77
78# -----------------------------------------------------------------------------
79# Function: open_file (Cross-platform file/directory opener)
80# -----------------------------------------------------------------------------
81open_file() {
82 local target="${1:-.}"
83
84 if [[ "$OSTYPE" == "darwin"* ]]; then
85 command open "$target"
86 elif grep -qi "microsoft" /proc/version 2>/dev/null; then
87 if command -v wslpath >/dev/null 2>&1 && command -v explorer.exe >/dev/null 2>&1; then
88 explorer.exe "$(wslpath -w "$target")"
89 else
90 printf "Error: 'wslpath' or 'explorer.exe' not found.\n" >&2
91 return 1
92 fi
93 elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
94 if command -v xdg-open >/dev/null 2>&1; then
95 xdg-open "$target"
96 else
97 printf "Error: 'xdg-open' not found.\n" >&2
98 return 1
99 fi
100 fi
101}
102alias open='open_file'
103
104# -----------------------------------------------------------------------------
105# Function: ch (Supports shell, Claude, Codex, and OpenCode)
106# -----------------------------------------------------------------------------
107ch() {
108 if [ "$#" -gt 1 ]; then
109 printf "Usage: ch [--all | shell | claude | codex | opencode]\n" >&2
110 return 2
111 fi
112 case "${1:-}" in
113 --all)
114 local result=0 target
115 for target in claude codex opencode shell; do
116 ch "$target" || result=1
117 done
118
119 if [ -t 1 ]; then
120 command clear
121 fi
122 return "$result"
123 ;;
124
125 claude)
126 local claude_home="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
127 if [ -d "$claude_home/projects" ]; then
128 command rm -rf -- "$claude_home/projects"/*(DN) || return 1
129 fi
130 if [ -f "$claude_home/history.jsonl" ]; then
131 : > "$claude_home/history.jsonl" || return 1
132 fi
133 echo "Claude project history/cache cleared."
134 ;;
135
136 codex)
137 local codex_home="${CODEX_HOME:-$HOME/.codex}"
138 command mkdir -p -- "$codex_home" || return 1
139 : > "$codex_home/history.jsonl" || return 1
140
141 command rm -rf -- "$codex_home/sessions" "$codex_home/archived_sessions" "$codex_home/shell_snapshots" || return 1
142 command mkdir -p -- "$codex_home/sessions" "$codex_home/archived_sessions" "$codex_home/shell_snapshots" || return 1
143
144 echo "Codex history cleared."
145 ;;
146
147 opencode)
148 local opencode_state_home="${XDG_STATE_HOME:-$HOME/.local/state}/opencode"
149 local opencode_data_home="${XDG_DATA_HOME:-$HOME/.local/share}/opencode"
150
151 command mkdir -p -- "$opencode_state_home" "$opencode_data_home" || return 1
152 : > "$opencode_state_home/prompt-history.jsonl" || return 1
153 command rm -f -- \
154 "$opencode_data_home/opencode.db" \
155 "$opencode_data_home/opencode.db-shm" \
156 "$opencode_data_home/opencode.db-wal" || return 1
157 command rm -rf -- "$opencode_data_home/repos" "$opencode_data_home/log" "$opencode_data_home/snapshot" || return 1
158 command mkdir -p -- "$opencode_data_home/repos" "$opencode_data_home/log" "$opencode_data_home/snapshot" || return 1
159
160 echo "OpenCode history cleared."
161 ;;
162
163 ""|shell)
164 # 1. Truncate the file
165 if [ -n "${HISTFILE:-}" ]; then
166 : > "$HISTFILE" || return 1
167 fi
168
169 # 2. Start a fresh Zsh history context. HISTSIZE=0 retains one entry.
170 builtin fc -p "${HISTFILE:-}" "$HISTSIZE" "$SAVEHIST" || return 1
171
172 echo "Shell history cleared."
173 ;;
174 *)
175 printf "Usage: ch [--all | shell | claude | codex | opencode]\n" >&2
176 return 2
177 ;;
178 esac
179
180 if [ -t 1 ]; then
181 command clear
182 fi
183}
184
185# -----------------------------------------------------------------------------
186# Function: require_cli (Check external CLI availability)
187# -----------------------------------------------------------------------------
188require_cli() {
189 local binary="$1"
190 local label="$2"
191 local resolved
192
193 if command -v whence >/dev/null 2>&1; then
194 if whence -p "$binary" >/dev/null 2>&1; then
195 return 0
196 fi
197 else
198 resolved="$(command -v "$binary" 2>/dev/null)" || resolved=""
199 if [ -n "$resolved" ] && [ -x "$resolved" ]; then
200 return 0
201 fi
202 fi
203
204 printf "Error: %s ('%s') is not installed or not in your PATH.\n" "$label" "$binary" >&2
205 return 1
206}
207
208# -----------------------------------------------------------------------------
209# Function: claude (Includes --yolo and clear shortcuts)
210# -----------------------------------------------------------------------------
211claude() {
212 # Shortcut for clearing cache
213 if [[ "$1" == "clear" ]]; then
214 ch claude
215 return $?
216 fi
217
218 # Check if binary exists
219 require_cli claude "Claude Code CLI" || return 1
220
221 # Handle --yolo mode
222 if [[ "$1" == "--yolo" ]]; then
223 shift
224 command claude --dangerously-skip-permissions "$@"
225 return $?
226 fi
227
228 command claude "$@"
229}
230
231# -----------------------------------------------------------------------------
232# Internal: _prompt_confirm_indexing (Confirm project indexing via the terminal)
233# -----------------------------------------------------------------------------
234_prompt_confirm_indexing() {
235 local project_root="$1"
236 local reply
237
238 if ! (: </dev/tty) 2>/dev/null; then
239 printf "Notice: No interactive terminal available; enhancing without project indexing.\n" >&2
240 return 1
241 fi
242
243 printf "Allow Auggie to index and use project context from '%s'? (y/N) " "$project_root" >/dev/tty
244 if ! IFS= read -r reply </dev/tty; then
245 printf "\nNotice: Unable to read confirmation; enhancing without project indexing.\n" >&2
246 return 1
247 fi
248
249 case "$reply" in
250 y|Y|yes|YES|Yes)
251 return 0
252 ;;
253 *)
254 return 1
255 ;;
256 esac
257}
258
259# -----------------------------------------------------------------------------
260# Internal: _enhance_prompt (Run Auggie and write only the enhanced prompt)
261# -----------------------------------------------------------------------------
262_enhance_prompt() {
263 local prompt="$1"
264 local output_file="$2"
265 local run_dir
266 local prompt_file
267 local workspace
268 local cache_dir
269 local auth_file
270 local log_file
271 local parsed_file
272 local project_root
273 local use_project_context
274 local script_pid
275 local waited
276 local timeout_seconds
277 local monitor_status
278
279 if [ -z "$prompt" ]; then
280 printf "Error: Prompt was empty.\n" >&2
281 return 1
282 fi
283
284 if [ ! -s "$HOME/.augment/session.json" ]; then
285 printf "Error: Auggie session file not found. Run 'auggie login' first.\n" >&2
286 return 1
287 fi
288
289 run_dir="$(mktemp -d "${TMPDIR:-/tmp}/auggie-enhance.XXXXXX")" || return 1
290 prompt_file="$run_dir/prompt.txt"
291 workspace="$run_dir/workspace"
292 cache_dir="$run_dir/cache"
293 auth_file="$HOME/.augment/session.json"
294 log_file="$run_dir/auggie.log"
295 parsed_file="$run_dir/enhanced.txt"
296 mkdir -p "$workspace" "$cache_dir"
297 printf "%s\n" "$prompt" > "$prompt_file"
298
299 project_root="$(git rev-parse --show-toplevel 2>/dev/null)" || project_root="$PWD"
300 use_project_context=0
301 timeout_seconds=90
302
303 if _prompt_confirm_indexing "$project_root"; then
304 use_project_context=1
305 timeout_seconds=300
306 workspace="$project_root"
307 cache_dir="$HOME/.augment"
308 printf "Indexing approved; enhancing with project context from '%s'.\n" "$project_root" >&2
309 else
310 printf "Enhancing without project indexing.\n" >&2
311 fi
312
313 (
314 if [ "$use_project_context" -eq 1 ]; then
315 AUGGIE_PROMPT_FILE="$prompt_file" \
316 AUGGIE_WORKSPACE="$workspace" \
317 AUGGIE_CACHE_DIR="$cache_dir" \
318 AUGGIE_AUTH_FILE="$auth_file" \
319 script -q -f -O "$log_file" -c 'auggie --print --enhance-prompt --workspace-root "$AUGGIE_WORKSPACE" --augment-cache-dir "$AUGGIE_CACHE_DIR" --augment-session-json "$AUGGIE_AUTH_FILE" --allow-indexing --wait-for-indexing --dont-save-session --instruction-file "$AUGGIE_PROMPT_FILE"' </dev/null >/dev/null 2>&1 &
320 else
321 AUGGIE_PROMPT_FILE="$prompt_file" \
322 AUGGIE_WORKSPACE="$workspace" \
323 AUGGIE_CACHE_DIR="$cache_dir" \
324 AUGGIE_AUTH_FILE="$auth_file" \
325 script -q -f -O "$log_file" -c 'auggie --print --enhance-prompt --no-discover-workspaces --workspace-root "$AUGGIE_WORKSPACE" --augment-cache-dir "$AUGGIE_CACHE_DIR" --augment-session-json "$AUGGIE_AUTH_FILE" --dont-save-session --instruction-file "$AUGGIE_PROMPT_FILE"' </dev/null >/dev/null 2>&1 &
326 fi
327
328 script_pid=$!
329 waited=0
330
331 while kill -0 "$script_pid" >/dev/null 2>&1; do
332 if grep -aq "🤖" "$log_file" 2>/dev/null || grep -aq "Tool call:" "$log_file" 2>/dev/null; then
333 kill "$script_pid" >/dev/null 2>&1 || true
334 wait "$script_pid" >/dev/null 2>&1 || true
335 exit 0
336 fi
337
338 if [ "$waited" -ge "$timeout_seconds" ]; then
339 kill "$script_pid" >/dev/null 2>&1 || true
340 wait "$script_pid" >/dev/null 2>&1 || true
341 exit 124
342 fi
343
344 sleep 1
345 waited=$((waited + 1))
346 done
347
348 wait "$script_pid" >/dev/null 2>&1 || true
349 exit 0
350 )
351 monitor_status=$?
352
353 if [ "$monitor_status" -eq 124 ]; then
354 printf "Error: Timed out waiting for Auggie to enhance the prompt.\n" >&2
355 rm -rf "$run_dir"
356 return 124
357 fi
358
359 perl -ne '
360 # Auggie redraws streamed terminal lines with carriage returns. Keep only
361 # the final rendered segment so stale text cannot corrupt the prompt.
362 s/\r$//;
363 s/.*\r//;
364 1 while s/[^\x08]\x08//g;
365 s/\x08//g;
366 s/\e\][^\a]*(?:\a|\e\\)//g;
367 s/\e\[[0-?]*[ -\/]*[@-~]//g;
368 next if /Script started on/ || /Script done on/;
369 if (/^(?:✨\s*)?Enhanced prompt:\s*(.*)$/) {
370 $capturing = 1;
371 $output .= "$1\n" if length $1;
372 next;
373 }
374 next unless $capturing;
375 exit if /^🤖/ || /Tool call:/ || /Session terminated/;
376 $output .= $_;
377 END {
378 $output =~ s/^\s*\n//;
379 $output =~ s/\s+\z//;
380 print "$output\n" if length $output;
381 }
382 ' "$log_file" > "$parsed_file"
383
384 if [ ! -s "$parsed_file" ]; then
385 printf "Error: Auggie did not return an enhanced prompt.\n" >&2
386 rm -rf "$run_dir"
387 return 1
388 fi
389
390 if ! command cp "$parsed_file" "$output_file"; then
391 printf "Error: Unable to save the enhanced prompt.\n" >&2
392 rm -rf "$run_dir"
393 return 1
394 fi
395 rm -rf "$run_dir"
396}
397
398# -----------------------------------------------------------------------------
399# Function: prompt (Enhance a prompt with Auggie and optional project context)
400# -----------------------------------------------------------------------------
401prompt() {
402 local prompt
403 local line
404 local output_file
405 local input_keymap
406 local input_status
407
408 require_cli auggie "Auggie CLI" || return 1
409 require_cli script "script utility" || return 1
410
411 if [ $# -gt 0 ]; then
412 prompt="$*"
413 elif [ ! -t 0 ]; then
414 prompt="$(cat)"
415 elif [ -n "${ZSH_VERSION:-}" ]; then
416 # Keep all lines in one ZLE buffer so previous lines remain editable.
417 input_keymap="auggie-prompt-$$-$RANDOM"
418 bindkey -N "$input_keymap" emacs || return 1
419 bindkey -M "$input_keymap" '^J' self-insert
420 bindkey -M "$input_keymap" '^M' self-insert-unmeta
421 bindkey -M "$input_keymap" '^[[13;2u' accept-line
422 bindkey -M "$input_keymap" '^[[27;2;13~' accept-line
423 bindkey -M "$input_keymap" '^D' accept-line
424 printf 'Enter your prompt: Enter adds a line; Shift+Enter (configured terminal) or Ctrl+D submits; Ctrl+C cancels.\n'
425 prompt=""
426 vared -M "$input_keymap" -p '> ' prompt
427 input_status=$?
428 bindkey -D "$input_keymap"
429 [ "$input_status" -eq 0 ] || return "$input_status"
430 else
431 echo "Reading prompt... Type 'EOF' on a new line and press Enter when finished."
432 prompt=""
433
434 while IFS= read -r line; do
435 [ "$line" = "EOF" ] && break
436 if [ -z "$prompt" ]; then
437 prompt="$line"
438 else
439 prompt="${prompt}"$'\n'"${line}"
440 fi
441 done
442 fi
443
444 if [ -z "$prompt" ]; then
445 printf "Error: Prompt was empty.\n" >&2
446 return 1
447 fi
448
449 output_file="$(mktemp "${TMPDIR:-/tmp}/auggie-enhanced-prompt.XXXXXX")" || return 1
450 if ! _enhance_prompt "$prompt" "$output_file"; then
451 rm -f "$output_file"
452 return 1
453 fi
454
455 if [ -t 1 ]; then
456 clear
457 fi
458
459 printf "Enhanced prompt:\n"
460 command cat "$output_file"
461 if clip < "$output_file"; then
462 printf '\nEnhanced prompt copied to clipboard.\n' >&2
463 else
464 printf '\nUnable to copy to clipboard; the enhanced prompt is shown above.\n' >&2
465 fi
466 rm -f "$output_file"
467
468 command auggie account status
469}
470
471# -----------------------------------------------------------------------------
472# Function: plan_build (Cached canonical plan-build launcher)
473# -----------------------------------------------------------------------------
474plan_build() {
475 emulate -L zsh
476 setopt localtraps
477 local base_url="${PLAN_BUILD_BASE_URL:-https://opengist.resetrix.work/weehong/plan-build/raw/HEAD}"
478 local cache_dir="${PLAN_BUILD_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/plan-build}"
479 local curl_command="${PLAN_BUILD_CURL_COMMAND:-curl}"
480 local skill_file="$HOME/.claude/skills/plan-build/SKILL.md"
481 local architect_file="$HOME/.claude/skills/plan-build-architect/SKILL.md"
482 local stage_dir="" release_dir="" current_tmp=""
483 local skill_tmp="" architect_tmp="" skill_backup="" architect_backup=""
484 local activation_lock="$cache_dir/.activate.lock"
485 local activation_lock_fd=""
486 local selected_release="" active_pid="" refresh_rc=1 call_rc=0 interrupted_rc=0
487
488 if ! zmodload zsh/system 2>/dev/null; then
489 printf "Error: Unable to load the Zsh system module required by plan-build.\n" >&2
490 return 1
491 fi
492
493 require_cli zsh "Zsh" || return 1
494
495 if ! command mkdir -p "$cache_dir" "${skill_file:h}" "${architect_file:h}"; then
496 printf "Error: Unable to create plan-build cache or skill directories.\n" >&2
497 return 1
498 fi
499
500 _plan_build_cleanup() {
501 if [[ -n "${active_pid:-}" ]] && command kill -0 "$active_pid" 2>/dev/null; then
502 command kill "$active_pid" 2>/dev/null || true
503 wait "$active_pid" 2>/dev/null || true
504 fi
505 active_pid=""
506 [[ -n "$stage_dir" && -d "$stage_dir" ]] && command rm -rf -- "$stage_dir"
507 command rm -f -- "$current_tmp" "$skill_tmp" "$architect_tmp"
508 command rm -f -- "$skill_backup" "$architect_backup"
509 if [[ -n "${activation_lock_fd:-}" ]]; then
510 zsystem flock -u "$activation_lock_fd" 2>/dev/null || true
511 activation_lock_fd=""
512 fi
513 }
514 _plan_build_cancel() {
515 interrupted_rc="$1"
516 [[ -n "${active_pid:-}" ]] && command kill "$active_pid" 2>/dev/null || true
517 }
518 trap '_plan_build_cleanup' EXIT
519 trap '_plan_build_cancel 129' HUP
520 trap '_plan_build_cancel 130' INT
521 trap '_plan_build_cancel 143' TERM
522
523 _plan_build_validate_release() {
524 local candidate="$1"
525 [[ -d "$candidate" &&
526 -s "$candidate/plan_build.zsh" &&
527 -s "$candidate/SKILL.md" &&
528 -s "$candidate/ARCHITECT.md" ]] &&
529 command zsh -n "$candidate/plan_build.zsh"
530 }
531
532 _plan_build_replace_path() {
533 local replacement="$1" destination="$2"
534 if command mv -fT "$replacement" "$destination" 2>/dev/null; then
535 return 0
536 fi
537 command mv -fh "$replacement" "$destination"
538 }
539
540 _plan_build_select_current() {
541 local resolved
542 [[ -L "$cache_dir/current" ]] || return 1
543 resolved="$cache_dir/current"
544 resolved="${resolved:A}"
545 _plan_build_validate_release "$resolved" || return 1
546 selected_release="$resolved"
547 }
548
549 _plan_build_acquire_activation_lock() {
550 (( interrupted_rc )) && return 1
551 command touch "$activation_lock" || return 1
552 zsystem flock -t 10 -f activation_lock_fd "$activation_lock"
553 }
554
555 _plan_build_release_activation_lock() {
556 [[ -n "$activation_lock_fd" ]] || return 0
557 zsystem flock -u "$activation_lock_fd" || return 1
558 activation_lock_fd=""
559 }
560
561 _plan_build_download() {
562 local remote="$1" destination="$2" download_rc
563 command "$curl_command" -fsSL "$remote" -o "$destination" &
564 active_pid=$!
565 wait "$active_pid"
566 download_rc=$?
567 active_pid=""
568 (( interrupted_rc )) && return "$interrupted_rc"
569 return "$download_rc"
570 }
571
572 _plan_build_backup_path() {
573 local source="$1" backup="$2"
574 [[ -e "$source" || -L "$source" ]] || return 0
575 command rm -f -- "$backup" && command cp -RP "$source" "$backup"
576 }
577
578 _plan_build_restore_path() {
579 local destination="$1" backup="$2" had_old="$3"
580 if (( had_old )); then
581 _plan_build_replace_path "$backup" "$destination" || return 1
582 else
583 command rm -f -- "$destination"
584 fi
585 }
586
587 _plan_build_install_skill_links() {
588 local activation_ok=1
589 local skill_had_old=0 architect_had_old=0
590
591 skill_tmp="$(command mktemp "${skill_file}.link.XXXXXX")" &&
592 command rm -f "$skill_tmp" &&
593 command ln -s "$cache_dir/current/SKILL.md" "$skill_tmp" || skill_tmp=""
594 architect_tmp="$(command mktemp "${architect_file}.link.XXXXXX")" &&
595 command rm -f "$architect_tmp" &&
596 command ln -s "$cache_dir/current/ARCHITECT.md" "$architect_tmp" || architect_tmp=""
597 [[ -n "$skill_tmp" && -L "$skill_tmp" &&
598 -n "$architect_tmp" && -L "$architect_tmp" ]] || return 1
599
600 skill_backup="${skill_file}.backup.$$.$RANDOM"
601 architect_backup="${architect_file}.backup.$$.$RANDOM"
602 [[ -e "$skill_file" || -L "$skill_file" ]] && skill_had_old=1
603 [[ -e "$architect_file" || -L "$architect_file" ]] && architect_had_old=1
604 _plan_build_backup_path "$skill_file" "$skill_backup" || activation_ok=0
605 (( activation_ok )) && _plan_build_backup_path "$architect_file" "$architect_backup" || activation_ok=0
606 (( activation_ok && ! interrupted_rc )) &&
607 _plan_build_replace_path "$skill_tmp" "$skill_file" || activation_ok=0
608 (( activation_ok )) && skill_tmp=""
609 (( activation_ok && ! interrupted_rc )) &&
610 _plan_build_replace_path "$architect_tmp" "$architect_file" || activation_ok=0
611 (( activation_ok )) && architect_tmp=""
612
613 if (( activation_ok )); then
614 command rm -f -- "$skill_backup" "$architect_backup"
615 skill_backup="" architect_backup=""
616 return 0
617 fi
618
619 if ! _plan_build_restore_path "$skill_file" "$skill_backup" "$skill_had_old"; then
620 printf "Error: Unable to restore plan-build skill; backup preserved at %s.\n" "$skill_backup" >&2
621 skill_backup=""
622 fi
623 if ! _plan_build_restore_path "$architect_file" "$architect_backup" "$architect_had_old"; then
624 printf "Error: Unable to restore architect skill; backup preserved at %s.\n" "$architect_backup" >&2
625 architect_backup=""
626 fi
627 return 1
628 }
629
630 if (( $+commands[$curl_command] )) || [[ "$curl_command" == */* && -x "$curl_command" ]]; then
631 stage_dir="$(command mktemp -d "$cache_dir/.stage.XXXXXX")" || stage_dir=""
632 if [[ -n "$stage_dir" ]]; then
633 _plan_build_download "$base_url/plan_build.zsh" "$stage_dir/plan_build.zsh" &&
634 _plan_build_download "$base_url/SKILL.md" "$stage_dir/SKILL.md" &&
635 _plan_build_download "$base_url/ARCHITECT.md" "$stage_dir/ARCHITECT.md"
636 refresh_rc=$?
637 if (( interrupted_rc )); then
638 _plan_build_cleanup
639 trap - EXIT HUP INT TERM
640 return "$interrupted_rc"
641 fi
642 else
643 refresh_rc=1
644 fi
645 if (( refresh_rc == 0 )) &&
646 _plan_build_validate_release "$stage_dir"; then
647 release_dir="$(command mktemp -d "$cache_dir/release.XXXXXX")" || release_dir=""
648 if [[ -n "$release_dir" ]] &&
649 command rmdir "$release_dir" &&
650 command mv "$stage_dir" "$release_dir" &&
651 _plan_build_acquire_activation_lock; then
652 stage_dir=""
653 release_dir="${release_dir:A}"
654 current_tmp="$(command mktemp "$cache_dir/.current.XXXXXX")" &&
655 command rm -f "$current_tmp" &&
656 command ln -s "$release_dir" "$current_tmp" || current_tmp=""
657
658 if [[ -n "$current_tmp" && -L "$current_tmp" ]] &&
659 _plan_build_install_skill_links &&
660 _plan_build_replace_path "$current_tmp" "$cache_dir/current"; then
661 current_tmp=""
662 selected_release="$release_dir"
663 fi
664 _plan_build_release_activation_lock || true
665 fi
666 fi
667 fi
668
669 if (( interrupted_rc )); then
670 _plan_build_cleanup
671 trap - EXIT HUP INT TERM
672 return "$interrupted_rc"
673 fi
674
675 if [[ -z "$selected_release" ]]; then
676 if ! _plan_build_select_current; then
677 if (( ! $+commands[$curl_command] )) && [[ ! ( "$curl_command" == */* && -x "$curl_command" ) ]]; then
678 printf "Error: curl is required because no valid cached plan-build bundle is available.\n" >&2
679 else
680 printf "Error: Unable to refresh plan-build and no valid cached bundle is available.\n" >&2
681 fi
682 return 1
683 fi
684 if (( ! $+commands[$curl_command] )) && [[ ! ( "$curl_command" == */* && -x "$curl_command" ) ]]; then
685 printf "Warning: curl is unavailable; using the validated cached plan-build bundle.\n" >&2
686 else
687 printf "Warning: Unable to refresh plan-build; using the validated cached bundle.\n" >&2
688 fi
689 fi
690
691 if [[ "$(command readlink "$skill_file" 2>/dev/null)" != "$cache_dir/current/SKILL.md" ||
692 "$(command readlink "$architect_file" 2>/dev/null)" != "$cache_dir/current/ARCHITECT.md" ]]; then
693 if ! _plan_build_acquire_activation_lock || ! _plan_build_install_skill_links; then
694 _plan_build_release_activation_lock 2>/dev/null || true
695 if (( interrupted_rc )); then
696 _plan_build_cleanup
697 trap - EXIT HUP INT TERM
698 return "$interrupted_rc"
699 fi
700 printf "Error: Unable to install the plan-build skill links.\n" >&2
701 return 1
702 fi
703 _plan_build_release_activation_lock || true
704 fi
705
706 if (( interrupted_rc )); then
707 _plan_build_cleanup
708 trap - EXIT HUP INT TERM
709 return "$interrupted_rc"
710 fi
711
712 PLAN_BUILD_SKILL_PATH="$selected_release/SKILL.md" \
713 PLAN_BUILD_ARCHITECT_SKILL_PATH="$selected_release/ARCHITECT.md" \
714 command zsh "$selected_release/plan_build.zsh" "$@" &
715 active_pid=$!
716 wait "$active_pid"
717 call_rc=$?
718 active_pid=""
719 (( interrupted_rc )) && call_rc="$interrupted_rc"
720 _plan_build_cleanup
721 trap - EXIT HUP INT TERM
722 return "$call_rc"
723}
724
725# -----------------------------------------------------------------------------
726# Internal: _aicommit_usage (Print aicommit usage)
727# -----------------------------------------------------------------------------
728_aicommit_usage() {
729 printf "Usage: aicommit [--modules] [--yes] [claude | copilot | opencode]\n"
730 printf " --modules Force per-module commits (automatic above 30 staged paths)\n"
731 printf " --yes Skip the plan confirmation for per-module commits\n"
732}
733
734# -----------------------------------------------------------------------------
735# Internal: _aicommit_staged_diff (Staged diff without lock/minified files)
736# -----------------------------------------------------------------------------
737_aicommit_staged_diff() {
738 # Exclude lock files/minified files and cap at ~100k characters to prevent CLI crashes
739 git diff --cached -- . ":(exclude)*.lock" ":(exclude)*-lock.json" ":(exclude)*.min.js" | head -c 100000
740}
741
742# -----------------------------------------------------------------------------
743# Internal: _aicommit_opencode_model (Read OpenCode's most recent model selection)
744# -----------------------------------------------------------------------------
745_aicommit_opencode_model() {
746 python3 - <<'PY'
747import json
748import os
749from pathlib import Path
750
751state = Path(os.environ.get("XDG_STATE_HOME") or Path.home() / ".local/state")
752try:
753 data = json.loads((state / "opencode/model.json").read_text())
754 recent = data.get("recent", [])
755 if recent:
756 selected = recent[0]
757 model = selected["providerID"] + "/" + selected["modelID"]
758 variant = data.get("variant", {}).get(model)
759 if variant and variant != "default":
760 model += "#" + variant
761 print(model)
762except FileNotFoundError:
763 pass # Let OpenCode use its configured default on a fresh installation.
764PY
765}
766
767# -----------------------------------------------------------------------------
768# Internal: _aicommit_engine (Ask an AI CLI to commit or to plan a split)
769# -----------------------------------------------------------------------------
770_aicommit_engine() {
771 local engine="$1"
772 local mode="$2"
773 local input="$3"
774 local name
775 local selected_model
776 local -a model_args
777 model_args=()
778
779 if [ "$engine" = "opencode" ]; then
780 selected_model="$(_aicommit_opencode_model)" || return 1
781 if [ -n "$selected_model" ]; then
782 model_args=(--model "$selected_model")
783 fi
784 fi
785
786 case "$engine" in
787 claude) name="Claude" ;;
788 copilot) name="Copilot" ;;
789 opencode) name="OpenCode" ;;
790 esac
791
792 # Status goes to stderr so plan output on stdout stays clean JSON.
793 if [ "$mode" = "plan" ]; then
794 printf "🧩 %s is planning per-module commits...\n" "$name" >&2
795 else
796 printf "🤖 %s is analyzing staged changes...\n" "$name" >&2
797 fi
798
799 case "$engine:$mode" in
800 claude:commit)
801 printf "%s\n" "$input" \
802 | command claude --allowedTools 'Bash(git commit *)' --permission-mode dontAsk -p \
803 "Read the provided instructions and diff, then generate and execute the commit."
804 ;;
805 claude:plan)
806 printf "%s\n" "$input" \
807 | command claude --permission-mode dontAsk -p \
808 "Read the provided instructions and staged changes, then reply with only the JSON plan."
809 ;;
810 copilot:commit)
811 copilot -p "$input" -s --no-ask-user --allow-tool='shell(git:*)'
812 ;;
813 copilot:plan)
814 copilot -p "$input" -s --no-ask-user
815 ;;
816 opencode:commit)
817 printf "%s\n" "$input" | opencode run --agent ai-commit "${model_args[@]}"
818 ;;
819 opencode:plan)
820 printf "%s\n" "$input" | opencode run --agent ai-commit-plan "${model_args[@]}"
821 ;;
822 esac
823}
824
825# -----------------------------------------------------------------------------
826# Internal: _aicommit_restage (Stage HEAD plus plan groups first..last from a tree)
827# -----------------------------------------------------------------------------
828_aicommit_restage() {
829 local tree="$1"
830 local run_dir="$2"
831 local first="$3"
832 local last="$4"
833 local number
834
835 if git rev-parse -q --verify HEAD >/dev/null; then
836 git read-tree HEAD || return 1
837 else
838 git read-tree --empty || return 1
839 fi
840
841 [ "$first" -le "$last" ] || return 0
842
843 number="$first"
844 while [ "$number" -le "$last" ]; do
845 cat "$run_dir/$(printf "%03d" "$number").files"
846 number=$((number + 1))
847 done | git --literal-pathspecs restore --staged --source="$tree" --pathspec-from-file=- --pathspec-file-nul
848}
849
850# -----------------------------------------------------------------------------
851# Internal: _aicommit_plan (Validate the AI split plan into per-group files)
852# -----------------------------------------------------------------------------
853_aicommit_plan() {
854 local run_dir="$1"
855
856 python3 -c '
857import json, os, re, sys
858
859sys.stdout.reconfigure(errors="backslashreplace")
860sys.stderr.reconfigure(errors="backslashreplace")
861run_dir = sys.argv[1]
862types = {"feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore"}
863
864with open(os.path.join(run_dir, "staged"), "rb") as fh:
865 staged = [p for p in fh.read().decode("utf-8", "surrogateescape").split("\0") if p]
866with open(os.path.join(run_dir, "plan.out"), encoding="utf-8", errors="replace") as fh:
867 raw = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", fh.read())
868
869# Take the last {"commits": [...]} object in the reply, ignoring fences and prose.
870plan, decoder, pos = None, json.JSONDecoder(), raw.find("{")
871while pos != -1:
872 try:
873 obj, end = decoder.raw_decode(raw, pos)
874 except ValueError:
875 pos = raw.find("{", pos + 1)
876 continue
877 if isinstance(obj, dict) and isinstance(obj.get("commits"), list):
878 plan = obj
879 pos = raw.find("{", end)
880 else:
881 pos = raw.find("{", pos + 1)
882
883if plan is None:
884 sys.exit("❌ Error: The AI reply did not contain a JSON commit plan.")
885
886# Every staged path must land in exactly one group; anything unassigned goes last.
887staged_set, seen, groups, warnings = set(staged), set(), [], []
888for commit in plan["commits"]:
889 if not isinstance(commit, dict) or not isinstance(commit.get("files"), list):
890 continue
891 files = []
892 for path in commit["files"]:
893 if not isinstance(path, str):
894 continue
895 if path not in staged_set:
896 warnings.append("ignoring path that is not staged: " + path)
897 elif path in seen:
898 warnings.append("ignoring duplicate path: " + path)
899 else:
900 seen.add(path)
901 files.append(path)
902 if not files:
903 continue
904 kind = str(commit.get("type", "")).strip().lower()
905 scope = re.sub(r"[^a-z0-9._/-]", "", str(commit.get("scope", "")).lower())
906 label = ""
907 if kind in types:
908 label = kind + ("(" + scope + ")" if scope else "")
909 groups.append((label, files))
910
911missing = [p for p in staged if p not in seen]
912if missing:
913 warnings.append("%d staged path(s) were not planned; adding them as a final commit" % len(missing))
914 groups.append(("", missing))
915
916if len(staged) > 30 and len(groups) < 2:
917 sys.exit("❌ Error: Changes affecting more than 30 staged paths require multiple module commits.")
918
919for number, (label, files) in enumerate(groups, 1):
920 base = os.path.join(run_dir, "%03d" % number)
921 with open(base + ".files", "wb") as fh:
922 fh.write("".join(p + "\0" for p in files).encode("utf-8", "surrogateescape"))
923 with open(base + ".label", "w") as fh:
924 fh.write(label)
925with open(os.path.join(run_dir, "count"), "w") as fh:
926 fh.write(str(len(groups)))
927
928for warning in warnings:
929 print("⚠️ " + warning, file=sys.stderr)
930print("📋 Planned commits:")
931for number, (label, files) in enumerate(groups, 1):
932 print(" %d. %s (%d file%s)" % (number, label or "unplanned", len(files), "" if len(files) == 1 else "s"))
933 for path in files:
934 print(" " + path)
935' "$run_dir"
936}
937
938# -----------------------------------------------------------------------------
939# Internal: _aicommit_modules_run (Plan, confirm, and commit each module group)
940# -----------------------------------------------------------------------------
941_aicommit_modules_run() {
942 local engine="$1"
943 local assume_yes="$2"
944 local run_dir="$3"
945 local prompt_file="$HOME/.config/ai-commit-prompt.txt"
946 local split_prompt_file="$HOME/.config/ai-commit-split-prompt.txt"
947 local tree
948 local total
949 local current
950 local group
951 local label
952 local hint
953 local diff
954 local expected
955 local head_before
956 local head_after
957 local unexpected
958 local reply
959
960 if ! tree="$(git write-tree 2>/dev/null)"; then
961 printf "❌ Error: Unable to snapshot the index (unresolved conflicts?).\n" >&2
962 return 1
963 fi
964
965 git diff --cached --name-only --no-renames -z > "$run_dir/staged" || return 1
966 if [ ! -s "$run_dir/staged" ]; then
967 printf "❌ Error: Nothing staged to commit.\n" >&2
968 return 1
969 fi
970
971 _aicommit_engine "$engine" plan "$(cat "$split_prompt_file")
972
973Here are the staged files:
974$(git -c core.quotePath=false diff --cached --name-status -M)
975
976Here is the diffstat:
977$(git -c core.quotePath=false diff --cached --numstat -M)
978
979Here is the git diff:
980$(_aicommit_staged_diff)" > "$run_dir/plan.out"
981
982 if ! _aicommit_plan "$run_dir"; then
983 printf "Last lines of the AI reply:\n" >&2
984 tail -n 20 "$run_dir/plan.out" >&2
985 return 1
986 fi
987 total="$(cat "$run_dir/count")"
988
989 if [ "$assume_yes" -ne 1 ]; then
990 if ! (: </dev/tty) 2>/dev/null; then
991 printf "❌ Error: No interactive terminal to confirm the plan; rerun with --yes.\n" >&2
992 return 1
993 fi
994 printf "Create these %s commit(s)? (Y/n) " "$total" >/dev/tty
995 IFS= read -r reply </dev/tty || reply="n"
996 case "$reply" in
997 ""|y|Y|yes|YES|Yes) ;;
998 *)
999 printf "Aborted. Staged changes are untouched.\n"
1000 return 1
1001 ;;
1002 esac
1003 fi
1004
1005 printf "💾 Staged snapshot: %s (recover with: git read-tree %s)\n" "$tree" "$tree"
1006 current=1
1007 trap '_aicommit_restage "$tree" "$run_dir" "$current" "$total"; printf "\n⚠️ Interrupted. Uncommitted groups are staged again.\n" >&2; rm -rf "$run_dir"; exit 130' INT TERM
1008
1009 while [ "$current" -le "$total" ]; do
1010 group="$run_dir/$(printf "%03d" "$current").files"
1011 label="$(cat "$run_dir/$(printf "%03d" "$current").label")"
1012
1013 if ! _aicommit_restage "$tree" "$run_dir" "$current" "$current"; then
1014 printf "❌ Error: Unable to stage commit %s/%s.\n" "$current" "$total" >&2
1015 _aicommit_restage "$tree" "$run_dir" "$current" "$total"
1016 return 1
1017 fi
1018
1019 expected="$(git write-tree)"
1020 head_before="$(git rev-parse -q --verify HEAD)"
1021 diff="$(_aicommit_staged_diff)"
1022 [ -n "$diff" ] || diff="$(git diff --cached --stat)"
1023
1024 hint="This is commit $current of $total, split by module."
1025 if [ -n "$label" ]; then
1026 hint="$hint The planned type and scope is $label; use it unless the diff clearly calls for another."
1027 fi
1028
1029 printf "\n[%s/%s] %s\n" "$current" "$total" "${label:-unplanned}"
1030 _aicommit_engine "$engine" commit "$(cat "$prompt_file")
1031
1032$hint
1033
1034Here is the git diff:
1035$diff"
1036
1037 head_after="$(git rev-parse -q --verify HEAD)"
1038 if [ -z "$head_after" ] || [ "$head_after" = "$head_before" ]; then
1039 printf "❌ Error: No commit was created for group %s/%s.\n" "$current" "$total" >&2
1040 _aicommit_restage "$tree" "$run_dir" "$current" "$total"
1041 printf "Uncommitted groups are staged again.\n" >&2
1042 return 1
1043 fi
1044
1045 # Hooks may reformat this group's files, but nothing outside it may sneak in.
1046 unexpected="$(LC_ALL=C comm -13 \
1047 <(tr '\0' '\n' < "$group" | LC_ALL=C sort) \
1048 <(git diff --name-only --no-renames -z "$expected" HEAD | tr '\0' '\n' | LC_ALL=C sort))"
1049 if [ -n "$unexpected" ]; then
1050 printf "❌ Error: Commit %s/%s included files outside its group:\n%s\n" "$current" "$total" "$unexpected" >&2
1051 _aicommit_restage "$tree" "$run_dir" "$((current + 1))" "$total"
1052 printf "Inspect it with 'git show'; undo it with 'git reset --soft HEAD~1'.\n" >&2
1053 return 1
1054 fi
1055
1056 current=$((current + 1))
1057 done
1058
1059 trap - INT TERM
1060 printf "\n✅ Created %s commit(s):\n" "$total"
1061 git --no-pager log --oneline -n "$total"
1062}
1063
1064# -----------------------------------------------------------------------------
1065# Internal: _aicommit_modules (Split staged changes into per-module commits)
1066# -----------------------------------------------------------------------------
1067_aicommit_modules() {
1068 local engine="$1"
1069 local assume_yes="$2"
1070 local top
1071 local run_dir
1072 local rc
1073
1074 if [ ! -f "$HOME/.config/ai-commit-split-prompt.txt" ]; then
1075 printf "❌ Error: Configuration file not found at ~/.config/ai-commit-split-prompt.txt\n" >&2
1076 return 1
1077 fi
1078
1079 require_cli python3 "Python 3" || return 1
1080
1081 if ! top="$(git rev-parse --show-toplevel 2>/dev/null)"; then
1082 printf "❌ Error: Not inside a git work tree.\n" >&2
1083 return 1
1084 fi
1085
1086 run_dir="$(mktemp -d "${TMPDIR:-/tmp}/aicommit.XXXXXX")" || return 1
1087
1088 # Subshell: run from the repo root and keep the traps away from the caller.
1089 (
1090 trap 'rm -rf "$run_dir"; exit 130' INT TERM
1091 cd "$top" || exit 1
1092 _aicommit_modules_run "$engine" "$assume_yes" "$run_dir"
1093 )
1094 rc=$?
1095 rm -rf "$run_dir"
1096 return "$rc"
1097}
1098
1099# -----------------------------------------------------------------------------
1100# Function: aicommit (Automated Conventional Commit Engine Wrapper)
1101# -----------------------------------------------------------------------------
1102aicommit() {
1103 local engine="claude"
1104 local modules=0
1105 local assume_yes=0
1106 local arg
1107 local diff
1108 local staged_count
1109 local prompt_file="$HOME/.config/ai-commit-prompt.txt"
1110
1111 for arg in "$@"; do
1112 case "$arg" in
1113 claude|copilot|opencode) engine="$arg" ;;
1114 --modules) modules=1 ;;
1115 -y|--yes) assume_yes=1 ;;
1116 -h|--help)
1117 _aicommit_usage
1118 return 0
1119 ;;
1120 *)
1121 printf "❌ Error: Invalid argument '%s'\n" "$arg" >&2
1122 _aicommit_usage >&2
1123 return 1
1124 ;;
1125 esac
1126 done
1127
1128 if [ ! -f "$prompt_file" ]; then
1129 printf "❌ Error: Configuration file not found at ~/.config/ai-commit-prompt.txt\n" >&2
1130 printf "Please create it and paste your system prompt inside.\n" >&2
1131 return 1
1132 fi
1133
1134 # Count the full index, including excluded previews and both sides of renames.
1135 # NUL delimiters keep whitespace/newlines in filenames from skewing the count.
1136 staged_count="$(set -o pipefail; git diff --cached --name-only --no-renames -z | tr -cd '\0' | wc -c)" || return 1
1137 if [ "$staged_count" -gt 30 ] && [ "$modules" -ne 1 ]; then
1138 printf "🧩 %s staged paths exceed 30; splitting commits by module.\n" "$staged_count" >&2
1139 fi
1140 if [ "$modules" -eq 1 ] || [ "$staged_count" -gt 30 ]; then
1141 _aicommit_modules "$engine" "$assume_yes"
1142 return
1143 fi
1144
1145 diff="$(_aicommit_staged_diff)"
1146 if [ -z "$diff" ]; then
1147 printf "❌ Error: Nothing staged to commit.\n" >&2
1148 return 1
1149 fi
1150
1151 _aicommit_engine "$engine" commit "$(cat "$prompt_file")
1152
1153Staged path count: $staged_count
1154
1155Here is the git diff:
1156$diff"
1157}
1158
1159# -----------------------------------------------------------------------------
1160# Function: create (Efficiently create file + parent dirs)
1161# -----------------------------------------------------------------------------
1162create() {
1163 if ! command -v install >/dev/null 2>&1; then
1164 printf "Error: 'install' coreutil is not available.\n" >&2
1165 return 1
1166 fi
1167
1168 if [ $# -eq 0 ]; then
1169 printf "Usage: create <file> [file ...]\n" >&2
1170 return 2
1171 fi
1172
1173 for p in "$@"; do
1174 # Expand tilde manually if shell doesn't
1175 [[ "$p" == "~/"* ]] && p="${HOME}/${p#\~/}"
1176
1177 if [ -z "$p" ] || [ "$p" = "/" ]; then
1178 printf "create: refusing to operate on '%s'\n" "$p" >&2
1179 continue
1180 fi
1181
1182 if [ -e "$p" ]; then
1183 printf "create: '%s' already exists. Skipping.\n" "$p" >&2
1184 continue
1185 fi
1186
1187 if install -D /dev/null "$p"; then
1188 echo "Created '$p'"
1189 fi
1190 done
1191}
1192
1193# -----------------------------------------------------------------------------
1194# Function: ckp (Kill or check the process using a port)
1195# -----------------------------------------------------------------------------
1196ckp() {
1197 local action="kill"
1198 local signal="TERM"
1199 local port
1200 local pids
1201 local pid
1202
1203 case "${1:-}" in
1204 -c|--check)
1205 action="check"
1206 shift
1207 ;;
1208 -k|--kill)
1209 action="kill"
1210 shift
1211 ;;
1212 -f|--force)
1213 action="kill"
1214 signal="KILL"
1215 shift
1216 ;;
1217 -h|--help|"")
1218 printf "Usage: ckp [--check|-c|--kill|-k|--force|-f] <port_number>\n" >&2
1219 return 1
1220 ;;
1221 esac
1222
1223 port="${1:-}"
1224 if [[ ! "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
1225 printf "Error: port must be a number between 1 and 65535.\n" >&2
1226 printf "Usage: ckp [--check|-c|--kill|-k|--force|-f] <port_number>\n" >&2
1227 return 1
1228 fi
1229
1230 if [[ "$OSTYPE" == "darwin"* ]]; then
1231 pids="$(
1232 {
1233 lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null
1234 lsof -nP -tiUDP:"$port" 2>/dev/null
1235 } | sort -u
1236 )"
1237
1238 if [ -z "$pids" ]; then
1239 printf "Port %s is FREE\n" "$port"
1240 return 0
1241 fi
1242
1243 printf "Port %s is in use:\n" "$port"
1244 lsof -nP -iTCP:"$port" -sTCP:LISTEN -iUDP:"$port" 2>/dev/null
1245 else
1246 local port_hex
1247 local inodes
1248
1249 port_hex="$(printf "%04X" "$port")"
1250 inodes="$(
1251 awk -v port="$port_hex" '
1252 NR > 1 {
1253 split($2, local_addr, ":")
1254 if (toupper(local_addr[length(local_addr)]) == port && ($4 == "0A" || FILENAME ~ /udp/)) {
1255 print $10
1256 }
1257 }
1258 ' /proc/net/tcp /proc/net/tcp6 /proc/net/udp /proc/net/udp6 2>/dev/null | sort -u
1259 )"
1260
1261 if [ -z "$inodes" ]; then
1262 printf "Port %s is FREE\n" "$port"
1263 return 0
1264 fi
1265
1266 pids=""
1267 pids="$(
1268 find /proc/[0-9]*/fd -maxdepth 1 -type l -printf "%p %l\n" 2>/dev/null |
1269 awk -v inodes="$inodes" '
1270 BEGIN {
1271 split(inodes, inode_list, " ")
1272 for (i in inode_list) {
1273 wanted["socket:[" inode_list[i] "]"] = 1
1274 }
1275 }
1276 $2 in wanted {
1277 split($1, path, "/")
1278 print path[3]
1279 }
1280 ' | sort -u
1281 )"
1282
1283 if [ -z "$pids" ]; then
1284 printf "Port %s is in use, but no readable process owner was found.\n" "$port" >&2
1285 return 1
1286 fi
1287
1288 printf "Port %s is in use:\n" "$port"
1289 for pid in $pids; do
1290 if [ -r "/proc/$pid/cmdline" ]; then
1291 printf " PID %-8s %s\n" "$pid" "$(tr '\0' ' ' < "/proc/$pid/cmdline")"
1292 else
1293 printf " PID %s\n" "$pid"
1294 fi
1295 done
1296 fi
1297
1298 if [ "$action" = "kill" ]; then
1299 for pid in $pids; do
1300 if kill "-$signal" "$pid" 2>/dev/null; then
1301 printf "Killed PID %s with SIG%s.\n" "$pid" "$signal"
1302 else
1303 printf "Error: failed to kill PID %s. Try with sudo or --force.\n" "$pid" >&2
1304 return 1
1305 fi
1306 done
1307 fi
1308}
1309
1310# -----------------------------------------------------------------------------
1311# Function: append_path (Adds to session PATH if exists)
1312# -----------------------------------------------------------------------------
1313append_path() {
1314 local dir="$1"
1315 if [ -d "$dir" ]; then
1316 case ":$PATH:" in
1317 *":$dir:"*) ;;
1318 *) PATH="$PATH:$dir" ;;
1319 esac
1320 elif [ "${SUPPRESS_WARNINGS:-0}" -ne 1 ]; then
1321 echo "Warning: $dir does not exist." >&2
1322 fi
1323}
1324
1325# -----------------------------------------------------------------------------
1326# Function: add_path_to_config (Fixed awk/sed injection)
1327# -----------------------------------------------------------------------------
1328add_path_to_config() {
1329 local new_path="$1"
1330 local config_file="$HOME/.bashrc"
1331 [[ "$SHELL" == *"zsh"* ]] && config_file="$HOME/.zshrc"
1332
1333 if [ -z "$new_path" ]; then
1334 printf "Usage: add_path_to_config <directory>\n" >&2
1335 return 1
1336 fi
1337
1338 new_path="${new_path/#\~/$HOME}"
1339
1340 if [ ! -d "$new_path" ]; then
1341 printf "Warning: '%s' does not exist. Add anyway? (y/N) " "$new_path"
1342 read -r REPLY
1343 [[ ! $REPLY =~ ^[Yy]$ ]] && return 1
1344 fi
1345
1346 # Check if already exists in file
1347 if grep -Fq "append_path \"$new_path\"" "$config_file"; then
1348 printf "Notice: Already in %s.\n" "$config_file"
1349 return 0
1350 fi
1351
1352 # Robust Injection: Use sed to insert before export PATH or just append
1353 if grep -q "^export PATH" "$config_file"; then
1354 sed -i.bak "/^export PATH/i append_path \"$new_path\"" "$config_file"
1355 else
1356 echo "append_path \"$new_path\"" >> "$config_file"
1357 echo "export PATH" >> "$config_file"
1358 fi
1359
1360 printf "✅ Success: Added to %s\n" "$config_file"
1361 append_path "$new_path" && export PATH
1362}
pathrc Originalformat
1# =============================================================================
2# ENVIRONMENT & PATH CONFIGURATION
3# =============================================================================
4
5# 1. Source functions first
6if [[ -f "$HOME/.func" && -r "$HOME/.func" ]]; then
7 source "$HOME/.func"
8fi
9
10# 2. Define Root Variables
11export DOTNET_ROOT="$HOME/.dotnet"
12export PNPM_HOME="$HOME/.local/share/pnpm"
13
14# 3. PATH INITIALIZATION
15# -----------------------------------------------------------------------------
16append_path "$HOME/.local/bin"
17append_path "$DOTNET_ROOT"
18append_path "$DOTNET_ROOT/tools"
19append_path "$HOME/.opencode/bin"
20append_path "$HOME/Flutter/bin"
21append_path "$PNPM_HOME/bin"
22
23# macOS specific paths
24if [[ "$OSTYPE" == "darwin"* ]]; then
25 append_path "/Applications/Espanso.app/Contents/MacOS"
26fi
27
28# Finalize PATH
29export PATH
sourcerc Originalformat
1# =============================================================================
2# FILE: ~/.sourcerc
3# Description: Initializes third-party package managers and external tools.
4# =============================================================================
5
6# =============================================================================
7# GENERAL ENV
8# =============================================================================
9if [[ -f "$HOME/.local/bin/env" && -r "$HOME/.local/bin/env" ]]; then
10 source "$HOME/.local/bin/env"
11fi
12
13# =============================================================================
14# SDKMAN (Java/Kotlin/Scala Version Manager)
15# =============================================================================
16export SDKMAN_DIR="$HOME/.sdkman"
17if [[ -f "$SDKMAN_DIR/bin/sdkman-init.sh" && -r "$SDKMAN_DIR/bin/sdkman-init.sh" ]]; then
18 source "$SDKMAN_DIR/bin/sdkman-init.sh"
19fi
20
21# =============================================================================
22# NVM (Node Version Manager)
23# =============================================================================
24export NVM_DIR="$HOME/.nvm"
25if [[ -f "$NVM_DIR/nvm.sh" && -r "$NVM_DIR/nvm.sh" ]]; then
26 source "$NVM_DIR/nvm.sh"
27fi
28if [[ -f "$NVM_DIR/bash_completion" && -r "$NVM_DIR/bash_completion" ]]; then
29 source "$NVM_DIR/bash_completion"
30fi
31
32# =============================================================================
33# GOOGLE CLOUD SDK
34# =============================================================================
35if [[ -f "/opt/google-cloud-sdk/path.zsh.inc" ]]; then
36 source "/opt/google-cloud-sdk/path.zsh.inc"
37fi
38
39if [[ -f "/opt/google-cloud-sdk/completion.zsh.inc" ]]; then
40 source "/opt/google-cloud-sdk/completion.zsh.inc"
41fi
42
starship.toml Originalformat
1"$schema" = 'https://starship.rs/config-schema.json'
2
3format = """
4[](red)\
5$os\
6$username\
7[](bg:peach fg:red)\
8$directory\
9[](bg:yellow fg:peach)\
10$git_branch\
11$git_status\
12[](fg:yellow bg:green)\
13$c\
14$rust\
15$golang\
16$nodejs\
17$bun\
18$php\
19$java\
20$kotlin\
21$haskell\
22$python\
23[](fg:green bg:sapphire)\
24$conda\
25[](fg:sapphire bg:lavender)\
26$time\
27[ ](fg:lavender)\
28$cmd_duration\
29$line_break\
30$character"""
31
32palette = 'catppuccin_mocha'
33
34[os]
35disabled = false
36style = "bg:red fg:crust"
37
38[os.symbols]
39Windows = ""
40Ubuntu = "󰕈"
41SUSE = ""
42Raspbian = "󰐿"
43Mint = "󰣭"
44Macos = "󰀵"
45Manjaro = ""
46Linux = "󰌽"
47Gentoo = "󰣨"
48Fedora = "󰣛"
49Alpine = ""
50Amazon = ""
51Android = ""
52AOSC = ""
53Arch = "󰣇"
54Artix = "󰣇"
55CentOS = ""
56Debian = "󰣚"
57Redhat = "󱄛"
58RedHatEnterprise = "󱄛"
59
60[username]
61show_always = true
62style_user = "bg:red fg:crust"
63style_root = "bg:red fg:crust"
64format = '[ $user]($style)'
65
66[directory]
67style = "bg:peach fg:crust"
68format = "[ $path ]($style)"
69truncation_length = 3
70truncation_symbol = "…/"
71
72[directory.substitutions]
73"Documents" = "󰈙 "
74"Downloads" = " "
75"Music" = "󰝚 "
76"Pictures" = " "
77"Developer" = "󰲋 "
78
79[git_branch]
80symbol = ""
81style = "bg:yellow"
82format = '[[ $symbol $branch ](fg:crust bg:yellow)]($style)'
83
84[git_status]
85style = "bg:yellow"
86format = '[[($all_status$ahead_behind )](fg:crust bg:yellow)]($style)'
87
88[nodejs]
89symbol = ""
90style = "bg:green"
91format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
92
93[bun]
94symbol = ""
95style = "bg:green"
96format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
97
98[c]
99symbol = " "
100style = "bg:green"
101format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
102
103[rust]
104symbol = ""
105style = "bg:green"
106format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
107
108[golang]
109symbol = ""
110style = "bg:green"
111format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
112
113[php]
114symbol = ""
115style = "bg:green"
116format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
117
118[java]
119symbol = " "
120style = "bg:green"
121format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
122
123[kotlin]
124symbol = ""
125style = "bg:green"
126format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
127
128[haskell]
129symbol = ""
130style = "bg:green"
131format = '[[ $symbol( $version) ](fg:crust bg:green)]($style)'
132
133[python]
134symbol = ""
135style = "bg:green"
136format = '[[ $symbol( $version)(\(#$virtualenv\)) ](fg:crust bg:green)]($style)'
137
138[docker_context]
139symbol = ""
140style = "bg:sapphire"
141format = '[[ $symbol( $context) ](fg:crust bg:sapphire)]($style)'
142
143[conda]
144symbol = "  "
145style = "fg:crust bg:sapphire"
146format = '[$symbol$environment ]($style)'
147ignore_base = false
148
149[time]
150disabled = false
151time_format = "%R"
152style = "bg:lavender"
153format = '[[  $time ](fg:crust bg:lavender)]($style)'
154
155[line_break]
156disabled = false
157
158[character]
159disabled = false
160success_symbol = '[❯](bold fg:green)'
161error_symbol = '[❯](bold fg:red)'
162vimcmd_symbol = '[❮](bold fg:green)'
163vimcmd_replace_one_symbol = '[❮](bold fg:lavender)'
164vimcmd_replace_symbol = '[❮](bold fg:lavender)'
165vimcmd_visual_symbol = '[❮](bold fg:yellow)'
166
167[cmd_duration]
168show_milliseconds = true
169format = " in $duration "
170style = "bg:lavender"
171disabled = false
172show_notifications = true
173min_time_to_notify = 45000
174
175[palettes.catppuccin_mocha]
176rosewater = "#f5e0dc"
177flamingo = "#f2cdcd"
178pink = "#f5c2e7"
179mauve = "#cba6f7"
180red = "#f38ba8"
181maroon = "#eba0ac"
182peach = "#fab387"
183yellow = "#f9e2af"
184green = "#a6e3a1"
185teal = "#94e2d5"
186sky = "#89dceb"
187sapphire = "#74c7ec"
188blue = "#89b4fa"
189lavender = "#b4befe"
190text = "#cdd6f4"
191subtext1 = "#bac2de"
192subtext0 = "#a6adc8"
193overlay2 = "#9399b2"
194overlay1 = "#7f849c"
195overlay0 = "#6c7086"
196surface2 = "#585b70"
197surface1 = "#45475a"
198surface0 = "#313244"
199base = "#1e1e2e"
200mantle = "#181825"
201crust = "#11111b"
202
203[palettes.catppuccin_frappe]
204rosewater = "#f2d5cf"
205flamingo = "#eebebe"
206pink = "#f4b8e4"
207mauve = "#ca9ee6"
208red = "#e78284"
209maroon = "#ea999c"
210peach = "#ef9f76"
211yellow = "#e5c890"
212green = "#a6d189"
213teal = "#81c8be"
214sky = "#99d1db"
215sapphire = "#85c1dc"
216blue = "#8caaee"
217lavender = "#babbf1"
218text = "#c6d0f5"
219subtext1 = "#b5bfe2"
220subtext0 = "#a5adce"
221overlay2 = "#949cbb"
222overlay1 = "#838ba7"
223overlay0 = "#737994"
224surface2 = "#626880"
225surface1 = "#51576d"
226surface0 = "#414559"
227base = "#303446"
228mantle = "#292c3c"
229crust = "#232634"
230
231[palettes.catppuccin_latte]
232rosewater = "#dc8a78"
233flamingo = "#dd7878"
234pink = "#ea76cb"
235mauve = "#8839ef"
236red = "#d20f39"
237maroon = "#e64553"
238peach = "#fe640b"
239yellow = "#df8e1d"
240green = "#40a02b"
241teal = "#179299"
242sky = "#04a5e5"
243sapphire = "#209fb5"
244blue = "#1e66f5"
245lavender = "#7287fd"
246text = "#4c4f69"
247subtext1 = "#5c5f77"
248subtext0 = "#6c6f85"
249overlay2 = "#7c7f93"
250overlay1 = "#8c8fa1"
251overlay0 = "#9ca0b0"
252surface2 = "#acb0be"
253surface1 = "#bcc0cc"
254surface0 = "#ccd0da"
255base = "#eff1f5"
256mantle = "#e6e9ef"
257crust = "#dce0e8"
258
259[palettes.catppuccin_macchiato]
260rosewater = "#f4dbd6"
261flamingo = "#f0c6c6"
262pink = "#f5bde6"
263mauve = "#c6a0f6"
264red = "#ed8796"
265maroon = "#ee99a0"
266peach = "#f5a97f"
267yellow = "#eed49f"
268green = "#a6da95"
269teal = "#8bd5ca"
270sky = "#91d7e3"
271sapphire = "#7dc4e4"
272blue = "#8aadf4"
273lavender = "#b7bdf8"
274text = "#cad3f5"
275subtext1 = "#b8c0e0"
276subtext0 = "#a5adcb"
277overlay2 = "#939ab7"
278overlay1 = "#8087a2"
279overlay0 = "#6e738d"
280surface2 = "#5b6078"
281surface1 = "#494d64"
282surface0 = "#363a4f"
283base = "#24273a"
284mantle = "#1e2030"
285crust = "#181926"
286
test_aicommit.py Originalformat
1#!/usr/bin/env python3
2"""Exercise aicommit routing and plan validation without invoking an AI CLI."""
3
4import json
5import os
6from pathlib import Path
7import subprocess
8import tempfile
9import unittest
10
11
12SOURCE = Path(__file__).resolve().parent / "func"
13ROUTE = '''
14source "$1"
15shift
16_aicommit_engine() { printf 'ENGINE %s %s\n' "$1" "$2"; }
17_aicommit_modules() { printf 'MODULES %s %s\n' "$1" "$2"; return 7; }
18aicommit "$@"
19'''
20
21
22class AicommitTests(unittest.TestCase):
23 def setUp(self):
24 self.temp = tempfile.TemporaryDirectory(prefix="aicommit-test-")
25 self.addCleanup(self.temp.cleanup)
26 self.root = Path(self.temp.name)
27 self.repo = self.root / "repo"
28 self.repo.mkdir()
29 self.home = self.root / "home"
30 config = self.home / ".config"
31 config.mkdir(parents=True)
32 for name in ("ai-commit-prompt.txt", "ai-commit-split-prompt.txt"):
33 (config / name).write_text((SOURCE.parent / name).read_text())
34 self.env = {**os.environ, "HOME": str(self.home)}
35 self.git("init", "-q")
36
37 def git(self, *args):
38 return subprocess.check_output(
39 ["git", *args], cwd=self.repo, env=self.env
40 )
41
42 def shell(self, script, *args, cwd=None):
43 return subprocess.run(
44 ["zsh", "-f", "-c", script, "test", str(SOURCE), *args],
45 cwd=cwd or self.repo, env=self.env, text=True, capture_output=True,
46 )
47
48 def stage(self, count, suffix=".txt"):
49 # A newline is part of one path, not an extra staged file.
50 files = [f"file {i}{suffix}" for i in range(count)]
51 if files:
52 files[0] = "line\nbreak" + suffix
53 for name in files:
54 (self.repo / name).write_text("staged content\n")
55 self.git("add", "--all")
56 return files
57
58 def test_thirty_paths_use_single_commit(self):
59 self.stage(30)
60 result = self.shell(ROUTE, "opencode")
61 self.assertEqual(result.returncode, 0, result.stderr)
62 self.assertEqual(result.stdout.strip(), "ENGINE opencode commit")
63
64 def test_thirty_one_paths_route_all_engines_and_forward_yes(self):
65 self.stage(31)
66 for engine in ("claude", "copilot", "opencode"):
67 with self.subTest(engine=engine):
68 result = self.shell(ROUTE, "--yes", engine)
69 self.assertEqual(result.returncode, 7, result.stderr)
70 self.assertEqual(result.stdout.strip(), f"MODULES {engine} 1")
71 self.assertIn("exceed 30", result.stderr)
72
73 def test_explicit_modules_below_threshold(self):
74 self.stage(1)
75 result = self.shell(ROUTE, "--modules", "copilot")
76 self.assertEqual(result.returncode, 7, result.stderr)
77 self.assertEqual(result.stdout.strip(), "MODULES copilot 0")
78
79 def test_filtered_files_still_trigger_modules_from_subdirectory(self):
80 self.stage(31, ".lock")
81 nested = self.repo / "nested"
82 nested.mkdir()
83 result = self.shell(ROUTE, cwd=nested)
84 self.assertEqual(result.returncode, 7, result.stderr)
85 self.assertEqual(result.stdout.strip(), "MODULES claude 0")
86
87 def test_empty_index_and_git_errors_never_call_engine(self):
88 for cwd in (self.repo, self.root):
89 with self.subTest(cwd=cwd):
90 result = self.shell(ROUTE, cwd=cwd)
91 self.assertNotEqual(result.returncode, 0)
92 self.assertNotIn("ENGINE", result.stdout)
93 self.assertNotIn("MODULES", result.stdout)
94
95 def plan(self, count, groups):
96 run = self.root / "plan"
97 run.mkdir()
98 files = [f"file-{i}" for i in range(count)]
99 (run / "staged").write_bytes(b"".join(p.encode() + b"\0" for p in files))
100 (run / "plan.out").write_text(json.dumps({"commits": [
101 {"type": "feat", "scope": "test", "files": group(files)}
102 for group in groups
103 ]}))
104 return self.shell('source "$1"; _aicommit_plan "$2"', str(run)), run
105
106 def test_single_group_plan_allowed_at_thirty(self):
107 result, run = self.plan(30, [lambda files: files])
108 self.assertEqual(result.returncode, 0, result.stderr)
109 self.assertEqual((run / "count").read_text(), "1")
110
111 def test_single_group_plan_rejected_above_thirty_after_deduplication(self):
112 result, run = self.plan(31, [lambda files: files, lambda files: files])
113 self.assertNotEqual(result.returncode, 0)
114 self.assertIn("require multiple module commits", result.stderr)
115 self.assertFalse((run / "count").exists())
116
117 def test_empty_plan_cannot_fall_back_to_one_large_commit(self):
118 result, _ = self.plan(31, [])
119 self.assertNotEqual(result.returncode, 0)
120 self.assertIn("require multiple module commits", result.stderr)
121
122 def test_multiple_groups_allowed_even_with_large_module(self):
123 result, run = self.plan(32, [lambda files: files[:-1], lambda files: files[-1:]])
124 self.assertEqual(result.returncode, 0, result.stderr)
125 self.assertEqual((run / "count").read_text(), "2")
126
127 def test_rejected_auto_plan_preserves_partial_index_and_worktree(self):
128 files = self.stage(31)
129 changed = self.repo / files[0]
130 changed.write_text("unstaged content\n")
131 before = self.git("write-tree")
132 plan = self.root / "single-plan.json"
133 plan.write_text(json.dumps({"commits": [{"files": files}]}))
134 result = self.shell('''
135source "$1"
136plan_file="$2"
137_aicommit_engine() {
138 if [ "$2" = plan ]; then cat "$plan_file"; else printf 'UNEXPECTED COMMIT\n'; fi
139}
140aicommit --yes opencode
141''', str(plan))
142 self.assertNotEqual(result.returncode, 0)
143 self.assertIn("require multiple module commits", result.stderr)
144 self.assertNotIn("UNEXPECTED COMMIT", result.stdout)
145 self.assertEqual(self.git("write-tree"), before)
146 self.assertEqual(changed.read_text(), "unstaged content\n")
147
148
149if __name__ == "__main__":
150 unittest.main()
151
test_plan_build.zsh Originalformat
1#!/usr/bin/env zsh
2
3set -u
4
5repo_dir="${0:A:h}"
6source "$repo_dir/func"
7
8failures=0
9fixture_dir="$(mktemp -d "${TMPDIR:-/tmp}/plan build launcher.XXXXXX")" || exit 1
10source_dir="$(mktemp -d "${TMPDIR:-/tmp}/plan-build-source.XXXXXX")" || exit 1
11export PLAN_BUILD_CACHE_DIR="$fixture_dir/cache with spaces"
12export PLAN_BUILD_BASE_URL="file://$source_dir"
13export HOME="$fixture_dir/home with spaces"
14PLAN_BUILD_SKILLS_DIR="$HOME/.claude/skills"
15mkdir -p "$source_dir" "$PLAN_BUILD_SKILLS_DIR/plan-build" "$PLAN_BUILD_SKILLS_DIR/plan-build-architect"
16print -r -- "old standard" > "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md"
17print -r -- "old architect" > "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md"
18trap 'rm -rf -- "$fixture_dir" "$source_dir"' EXIT
19
20fail() {
21 print -u2 -- "FAIL: $1"
22 failures=$((failures + 1))
23}
24
25assert_equal() {
26 [[ "$1" = "$2" ]] || fail "$3 (expected '$1', got '$2')"
27}
28
29assert_contains() {
30 [[ "$1" == *"$2"* ]] || fail "$3 (missing '$2')"
31}
32
33assert_file_contains() {
34 command grep -Fq -- "$2" "$1" || fail "$3"
35}
36
37assert_bundle_consistent() {
38 local release="$PLAN_BUILD_CACHE_DIR/current"
39 local script_generation skill_generation architect_generation
40 release="${release:A}"
41 [[ -L "$PLAN_BUILD_CACHE_DIR/current" ]] || fail "$1 current is a symlink"
42 [[ -d "$release" ]] || fail "$1 current resolves to a release"
43 script_generation="$(command sed -n 's/^# generation: //p' "$release/plan_build.zsh")"
44 skill_generation="$(command sed -n 's/ standard skill$//p' "$release/SKILL.md")"
45 architect_generation="$(command sed -n 's/ architect skill$//p' "$release/ARCHITECT.md")"
46 assert_equal "$script_generation" "$skill_generation" "$1 script/standard generation"
47 assert_equal "$script_generation" "$architect_generation" "$1 script/architect generation"
48}
49
50assert_skill_links() {
51 [[ -L "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md" ]] || fail "$1 standard path is symlink"
52 [[ -L "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md" ]] || fail "$1 architect path is symlink"
53 assert_equal "$PLAN_BUILD_CACHE_DIR/current/SKILL.md" \
54 "$(command readlink "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md")" "$1 standard link target"
55 assert_equal "$PLAN_BUILD_CACHE_DIR/current/ARCHITECT.md" \
56 "$(command readlink "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md")" "$1 architect link target"
57}
58
59write_bundle() {
60 local generation="$1"
61 local exit_code="${2:-0}"
62 local marker_file="${3:-}"
63 print -r -- '#!/usr/bin/env zsh' > "$source_dir/plan_build.zsh"
64 print -r -- "# generation: $generation" >> "$source_dir/plan_build.zsh"
65 print -r -- "print -r -- \"$generation:\${(j:|:)@}\"" >> "$source_dir/plan_build.zsh"
66 print -r -- "print -r -- \"path:\${0}\"" >> "$source_dir/plan_build.zsh"
67 print -r -- 'print -r -- "skills:${PLAN_BUILD_SKILL_PATH}|${PLAN_BUILD_ARCHITECT_SKILL_PATH}"' >> "$source_dir/plan_build.zsh"
68 [[ -n "$marker_file" ]] && print -r -- ": > ${(q)marker_file}" >> "$source_dir/plan_build.zsh"
69 print -r -- "exit $exit_code" >> "$source_dir/plan_build.zsh"
70 print -r -- "$generation standard skill" > "$source_dir/SKILL.md"
71 print -r -- "$generation architect skill" > "$source_dir/ARCHITECT.md"
72}
73
74run_plan_build() {
75 output="$(plan_build "$@" 2>&1)"
76 rc=$?
77}
78
79write_bundle initial
80run_plan_build alpha "two words"
81assert_equal 0 "$rc" "initial migration status"
82assert_contains "$output" "initial:alpha|two words" "argument forwarding"
83assert_skill_links "initial migration"
84assert_bundle_consistent "initial migration"
85assert_file_contains "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md" "initial standard" "initial standard content"
86assert_file_contains "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md" "initial architect" "initial architect content"
87execution_path="${${(f)output}[(r)path:*]#path:}"
88[[ "$execution_path" == "$PLAN_BUILD_CACHE_DIR"/release.*/plan_build.zsh ]] ||
89 fail "execution uses an immutable release path"
90assert_contains "$output" "skills:${execution_path:h}/SKILL.md|${execution_path:h}/ARCHITECT.md" \
91 "execution binds immutable skill paths"
92
93old_release="$PLAN_BUILD_CACHE_DIR/current"
94old_release="${old_release:A}"
95write_bundle replacement 23
96run_plan_build forwarded
97assert_equal 23 "$rc" "canonical status forwarding"
98assert_contains "$output" "replacement:forwarded" "replacement executes"
99new_release="$PLAN_BUILD_CACHE_DIR/current"
100new_release="${new_release:A}"
101[[ "$new_release" != "$old_release" ]] || fail "refresh activates a unique release"
102assert_skill_links "replacement"
103assert_bundle_consistent "replacement"
104
105print -r -- 'if broken' > "$source_dir/plan_build.zsh"
106print -r -- "bad standard skill" > "$source_dir/SKILL.md"
107print -r -- "bad architect skill" > "$source_dir/ARCHITECT.md"
108run_plan_build fallback
109assert_equal 23 "$rc" "failed refresh fallback status"
110assert_contains "$output" "Warning: Unable to refresh" "failed refresh warning"
111assert_contains "$output" "replacement:fallback" "failed refresh executes current"
112preserved_release="$PLAN_BUILD_CACHE_DIR/current"
113preserved_release="${preserved_release:A}"
114assert_equal "$new_release" "$preserved_release" "failed refresh preserves current"
115assert_skill_links "failed refresh"
116assert_bundle_consistent "failed refresh"
117
118export PLAN_BUILD_CURL_COMMAND="definitely-missing-plan-build-curl"
119run_plan_build no-curl
120assert_equal 23 "$rc" "missing curl fallback status"
121assert_contains "$output" "curl is unavailable" "missing curl warning"
122assert_contains "$output" "replacement:no-curl" "missing curl executes current"
123rm -f "$PLAN_BUILD_SKILLS_DIR/plan-build/SKILL.md" "$PLAN_BUILD_SKILLS_DIR/plan-build-architect/SKILL.md"
124run_plan_build repaired-offline
125assert_equal 23 "$rc" "offline skill repair status"
126assert_skill_links "offline skill repair"
127unset PLAN_BUILD_CURL_COMMAND
128
129# An already selected immutable script must not change beneath a concurrent caller.
130ready_file="$fixture_dir/ready"
131go_file="$fixture_dir/go"
132first_output="$fixture_dir/first.out"
133print -r -- '#!/usr/bin/env zsh' > "$source_dir/plan_build.zsh"
134print -r -- '# generation: concurrent_first' >> "$source_dir/plan_build.zsh"
135print -r -- 'print -r -- "concurrent_first:${(j:|:)@}"' >> "$source_dir/plan_build.zsh"
136print -r -- ": > ${(q)ready_file}" >> "$source_dir/plan_build.zsh"
137print -r -- "while [[ ! -e ${(q)go_file} ]]; do sleep 0.01; done" >> "$source_dir/plan_build.zsh"
138print -r -- 'print -r -- "finished:first"' >> "$source_dir/plan_build.zsh"
139print -r -- "concurrent_first standard skill" > "$source_dir/SKILL.md"
140print -r -- "concurrent_first architect skill" > "$source_dir/ARCHITECT.md"
141(plan_build one > "$first_output" 2>&1) &
142first_pid=$!
143for attempt in {1..500}; do
144 [[ -e "$ready_file" ]] && break
145 sleep 0.01
146done
147[[ -e "$ready_file" ]] || fail "first concurrent call reached canonical execution"
148write_bundle concurrent_second
149run_plan_build two
150assert_equal 0 "$rc" "second concurrent status"
151assert_contains "$output" "concurrent_second:two" "second concurrent generation"
152: > "$go_file"
153wait "$first_pid" || fail "first concurrent status"
154first_text="$(<"$first_output")"
155assert_contains "$first_text" "concurrent_first:one" "first call retains selected release"
156assert_contains "$first_text" "finished:first" "first immutable script finishes"
157assert_bundle_consistent "concurrent final current"
158assert_skill_links "concurrent final links"
159
160# Cancellation while refreshing must clean staging and never execute canonical code.
161blocking_curl="$fixture_dir/blocking-curl"
162cancel_started="$fixture_dir/cancel.started"
163cancel_executed="$fixture_dir/cancel.executed"
164write_bundle cancellation-marker 0 "$cancel_executed"
165run_plan_build activate-cancellation-marker
166assert_equal 0 "$rc" "cancellation marker bundle activation"
167rm -f "$cancel_executed"
168{
169 print -r -- '#!/bin/sh'
170 print -r -- "touch ${(q)cancel_started}"
171 print -r -- 'while :; do sleep 1; done'
172} > "$blocking_curl"
173chmod +x "$blocking_curl"
174export PLAN_BUILD_CURL_COMMAND="$blocking_curl"
175(plan_build cancelled > "$fixture_dir/cancel.out" 2>&1; cancel_rc=$?; print -r -- "$cancel_rc" > "$fixture_dir/cancel.rc"; exit "$cancel_rc") &
176cancel_pid=$!
177for attempt in {1..500}; do
178 [[ -e "$cancel_started" ]] && break
179 sleep 0.01
180done
181kill -TERM "$cancel_pid"
182wait "$cancel_pid"
183cancel_wait_rc=$?
184cancel_recorded_rc=""
185[[ -s "$fixture_dir/cancel.rc" ]] && cancel_recorded_rc="$(<"$fixture_dir/cancel.rc")"
186[[ "$cancel_wait_rc" = 143 || "$cancel_recorded_rc" = 143 ]] ||
187 fail "TERM returns conventional status 143"
188[[ ! -e "$cancel_executed" ]] || fail "TERM does not execute canonical plan-build"
189unset PLAN_BUILD_CURL_COMMAND
190
191# A pre-existing lock file must not be mistaken for an active kernel lock.
192print -r -- "stale contents" > "$PLAN_BUILD_CACHE_DIR/.activate.lock"
193write_bundle stale-lock-recovery
194run_plan_build recovered
195assert_equal 0 "$rc" "stale activation lock recovery status"
196assert_contains "$output" "stale-lock-recovery:recovered" "stale activation lock recovery execution"
197
198# Cancellation while canonical code runs must terminate the child promptly.
199canonical_started="$fixture_dir/canonical.started"
200canonical_finished="$fixture_dir/canonical.finished"
201print -r -- '#!/usr/bin/env zsh' > "$source_dir/plan_build.zsh"
202print -r -- '# generation: canonical-cancel' >> "$source_dir/plan_build.zsh"
203print -r -- "trap 'exit 143' TERM" >> "$source_dir/plan_build.zsh"
204print -r -- ": > ${(q)canonical_started}" >> "$source_dir/plan_build.zsh"
205print -r -- 'while :; do sleep 1; done' >> "$source_dir/plan_build.zsh"
206print -r -- ": > ${(q)canonical_finished}" >> "$source_dir/plan_build.zsh"
207print -r -- "canonical-cancel standard skill" > "$source_dir/SKILL.md"
208print -r -- "canonical-cancel architect skill" > "$source_dir/ARCHITECT.md"
209(plan_build canonical-cancel > "$fixture_dir/canonical-cancel.out" 2>&1; print -r -- "$?" > "$fixture_dir/canonical-cancel.rc") &
210canonical_pid=$!
211for attempt in {1..500}; do
212 [[ -e "$canonical_started" ]] && break
213 sleep 0.01
214done
215[[ -e "$canonical_started" ]] || fail "canonical cancellation reached execution"
216kill -TERM "$canonical_pid"
217wait "$canonical_pid"
218assert_equal 143 "$(<"$fixture_dir/canonical-cancel.rc")" "canonical cancellation status"
219[[ ! -e "$canonical_finished" ]] || fail "canonical child continued after TERM"
220
221temp_count="$(command find "$PLAN_BUILD_CACHE_DIR" -maxdepth 1 \
222 \( -name '.stage.*' -o -name '.current.*' \) -print | command wc -l | command tr -d ' ')"
223assert_equal 0 "$temp_count" "cache staging and temporary links cleaned"
224skill_temp_count="$(command find "$PLAN_BUILD_SKILLS_DIR" \
225 \( -name '*.link.*' -o -name '*.backup.*' \) -print | command wc -l | command tr -d ' ')"
226assert_equal 0 "$skill_temp_count" "skill temporary links and backups cleaned"
227
228if (( failures )); then
229 exit 1
230fi
231
232print -- "PASS: plan_build launcher tests"
233
vimrc Originalformat
1" Enable line numbers
2set number
3
4" Enable relative line numbers
5set relativenumber
6
7" Enable syntax highlighting
8syntax on
9
10" Set colorscheme
11colorscheme slate
12
13" Enable file type detection and plugins
14filetype plugin indent on
15
16" Set the tab width to 4 spaces
17set tabstop=4
18set shiftwidth=4
19set expandtab
20
21" Enable auto-indentation
22set autoindent
23set smartindent
24
25" Highlight current line
26set cursorline
27
28" Show matching parentheses
29set showmatch
30
31" Enable line wrapping
32set wrap
33
34" Enable mouse support
35set mouse=a
36
37" Use Wayland's clipboard tools when this Vim lacks native clipboard support
38if !has('clipboard') && exists('v:clipproviders')
39 \ && executable('wl-copy') && executable('wl-paste')
40 function! s:WaylandCopy(register, type, lines) abort
41 let l:text = join(a:lines, "\n")
42 if a:type ==# 'V'
43 let l:text ..= "\n"
44 endif
45 call system('wl-copy', l:text)
46 endfunction
47
48 function! s:WaylandPaste(register) abort
49 return ['', systemlist('wl-paste --no-newline')]
50 endfunction
51
52 let v:clipproviders['wltools'] = {
53 \ 'copy': {
54 \ '+': function('s:WaylandCopy'),
55 \ '*': function('s:WaylandCopy')
56 \ },
57 \ 'paste': {
58 \ '+': function('s:WaylandPaste'),
59 \ '*': function('s:WaylandPaste')
60 \ }
61 \ }
62 set clipmethod^=wltools
63endif
64set clipboard=unnamedplus
65
66" Disable swap file
67set noswapfile
68
69" Enable incremental search
70set incsearch
71
72" Ignore case in search
73set ignorecase
74
75" Override ignorecase if search contains capital letters
76set smartcase
77
78" Display line and column number of the cursor position
79set ruler
80
81" Set the status line at the bottom
82set laststatus=2
83
84" Show command in bottom bar
85set showcmd
86
87" Set command height
88set cmdheight=2
89
90" Set history lines
91set history=1000
92
93" Disable backup file
94set nobackup
95
96" Enable persistent undo
97set undofile
98
99" Set maximum number of undo levels
100set undolevels=1000
101
102" Set undo directory
103if has("persistent_undo")
104 silent !mkdir ~/.vim/undodir > /dev/null 2>&1
105 set undodir=~/.vim/undodir
106endif
107
108" Set search highlighting
109set hlsearch
110
111" Enable visual bell
112set visualbell
113
114" Set default file encoding
115set encoding=utf-8
116
117" Set the leader key to space
118let mapleader = " "
119
120" Map <Leader>w to save the file
121nnoremap <Leader>w :w<CR>
122
123" Map <Leader>q to quit
124nnoremap <Leader>q :q<CR>
125
126" Map <Leader>x to save and quit
127nnoremap <Leader>x :wq<CR>
128
129" Enable folding
130set foldmethod=syntax
131set foldlevelstart=99
132
133" Enable line wrapping at 80 characters
134set textwidth=80
135set colorcolumn=80
136
137" Add some basic key mappings
138" Map jj to escape insert mode
139inoremap jj <Esc>
140
141" Map <Leader>n to toggle line numbers
142nnoremap <Leader>n :set number!<CR>
143
144" Map <Leader>r to toggle relative line numbers
145nnoremap <Leader>r :set relativenumber!<CR>
146
147" Configure plugins (if you use a plugin manager like vim-plug)
148" Example with vim-plug:
149" call plug#begin('~/.vim/plugged')
150" Plug 'tpope/vim-sensible'
151" Plug 'preservim/nerdtree'
152" Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
153" Plug 'airblade/vim-gitgutter'
154" call plug#end()
155
156" NERDTree key mappings
157" nnoremap <C-n> :NERDTreeToggle<CR>
158
159" Enable automatic hard wrapping at textwidth (80 chars)
160set formatoptions+=t " Auto-wrap text using textwidth
161set formatoptions+=c " Auto-wrap comments using textwidth
162set formatoptions+=r " Continue comments when pressing Enter
163set formatoptions+=o " Continue comments when using 'o' or 'O'
164set formatoptions+=q " Allow formatting of comments with 'gq'
165set formatoptions+=n " Recognize numbered lists
166set formatoptions+=l " Don't break lines that were already long
167
168" Enable syntax highlighting
169syntax on
170
171" Increase memory limit for complex syntax parsing (Prevents E363)
172set maxmempattern=20000
173
zsh_macos.sh Originalformat
1#!/bin/bash
2set -euo pipefail
3
4# =============================
5# COLORS & LOGGING
6# =============================
7RED='\033[0;31m'
8GREEN='\033[0;32m'
9YELLOW='\033[1;33m'
10BLUE='\033[0;34m'
11NC='\033[0m'
12
13log() { echo -e "${BLUE}[INFO]${NC} $*"; }
14ok() { echo -e "${GREEN}[OK]${NC} $*"; }
15warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
16err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
17
18# =============================
19# CONFIG
20# =============================
21GIST_RAW_BASE="https://opengist.resetrix.work/weehong/f0d940c3c1214bf5b7996195199fdc09/raw/HEAD"
22
23CONFIG_FILES=(
24 ".alias"
25 ".func"
26 ".pathrc"
27 ".sourcerc"
28 ".vimrc"
29 ".zshrc"
30 ".config/starship.toml"
31 ".config/ai-commit-prompt.txt"
32 ".config/ai-commit-split-prompt.txt"
33 ".config/opencode/agents/ai-commit.md"
34 ".config/opencode/agents/ai-commit-plan.md"
35)
36
37# =============================
38# REQUIREMENTS
39# =============================
40check_requirements() {
41 if [[ $EUID -eq 0 ]]; then
42 err "Do not run as root on macOS"
43 exit 1
44 fi
45}
46
47# =============================
48# INSTALLATION FUNCTIONS
49# =============================
50install_oh_my_zsh() {
51 log "Installing Oh My Zsh and plugins..."
52 export RUNZSH=no
53 export CHSH=no
54 export KEEP_ZSHRC=yes
55
56 if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
57 sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended
58 else
59 ok "Oh My Zsh already installed"
60 fi
61
62 local custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
63 local plugin_dir="$custom/plugins"
64 mkdir -p "$plugin_dir"
65
66 [[ -d "$plugin_dir/zsh-autosuggestions" ]] || \
67 git clone https://github.com/zsh-users/zsh-autosuggestions "$plugin_dir/zsh-autosuggestions"
68
69 [[ -d "$plugin_dir/zsh-syntax-highlighting" ]] || \
70 git clone https://github.com/zsh-users/zsh-syntax-highlighting "$plugin_dir/zsh-syntax-highlighting"
71
72 ok "Oh My Zsh plugins installed to $plugin_dir"
73}
74
75install_starship() {
76 if command -v starship >/dev/null 2>&1 || [[ -f "$HOME/.local/bin/starship" ]]; then
77 ok "Starship already installed"
78 elif command -v brew >/dev/null 2>&1; then
79 log "Installing Starship with Homebrew..."
80 brew install starship
81 ok "Starship installed"
82 else
83 log "Installing Starship..."
84 mkdir -p "$HOME/.local/bin"
85 curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin"
86 ok "Starship installed"
87 fi
88}
89
90download_configs() {
91 log "Downloading custom config files..."
92 for f in "${CONFIG_FILES[@]}"; do
93 # Strip the leading dot for the download URL
94 local remote_file="${f#.}"
95 [[ "$f" == ".config/starship.toml" ]] && remote_file="starship.toml"
96 [[ "$f" == ".config/ai-commit-prompt.txt" ]] && remote_file="ai-commit-prompt.txt"
97 [[ "$f" == ".config/ai-commit-split-prompt.txt" ]] && remote_file="ai-commit-split-prompt.txt"
98 [[ "$f" == ".config/opencode/agents/ai-commit.md" ]] && remote_file="ai-commit.md"
99 [[ "$f" == ".config/opencode/agents/ai-commit-plan.md" ]] && remote_file="ai-commit-plan.md"
100 local url="$GIST_RAW_BASE/$remote_file"
101 local target="$HOME/$f"
102 local tmp="${target}.tmp.$$"
103
104 mkdir -p "$(dirname "$target")"
105
106 log "Fetching $remote_file -> $f ..."
107 if curl -fsSL "$url" -o "$tmp"; then
108 mv "$tmp" "$target"
109 else
110 rm -f "$tmp"
111 warn "Failed to download $remote_file"
112 fi
113 done
114
115 ok "Configs downloaded"
116}
117
118configure_zshrc() {
119 local zshrc="$HOME/.zshrc"
120 log "Configuring .zshrc for Oh My Zsh and Starship..."
121
122 cat > "$zshrc" <<'EOF'
123# =============================================================================
124# 1. HELPER FUNCTIONS & PATH
125# =============================================================================
126source_if_readable() {
127 local file="$1"
128 if [[ -f "$file" && -r "$file" ]]; then
129 source "$file"
130 fi
131}
132
133export PATH="$HOME/.local/bin:$PATH"
134
135# =============================================================================
136# 2. OH MY ZSH FRAMEWORK
137# =============================================================================
138export ZSH="${ZSH:-$HOME/.oh-my-zsh}"
139ZSH_THEME=""
140
141plugins=(
142 git
143 zsh-autosuggestions
144 zsh-syntax-highlighting
145)
146
147source_if_readable "$ZSH/oh-my-zsh.sh"
148
149# =============================================================================
150# 3. THIRD-PARTY INITIALIZATION & CUSTOM CONFIGS
151# =============================================================================
152source_if_readable "$HOME/.sourcerc"
153source_if_readable "$HOME/.func"
154source_if_readable "$HOME/.pathrc"
155source_if_readable "$HOME/.alias"
156
157# =============================================================================
158# 4. STARSHIP PROMPT
159# =============================================================================
160if [[ -z "${STARSHIP_CONFIG:-}" && -f "$HOME/.config/starship.toml" ]]; then
161 export STARSHIP_CONFIG="$HOME/.config/starship.toml"
162fi
163
164if command -v starship >/dev/null 2>&1; then
165 eval "$(starship init zsh)"
166elif [[ -x "$HOME/.local/bin/starship" ]]; then
167 eval "$("$HOME/.local/bin/starship" init zsh)"
168fi
169EOF
170
171 ok ".zshrc configured for Oh My Zsh framework with Starship prompt"
172}
173
174configure_git_identity() {
175 log "Configuring global Git identity..."
176 git config --global user.name "Vernon Wee Hong KOH"
177 git config --global user.email "261549452+weehong@users.noreply.github.com"
178 ok "Global Git identity configured"
179}
180
181switch_shell() {
182 log "Starting Zsh session..."
183 echo -e "${YELLOW}Type 'exit' to return to this installer menu.${NC}"
184 echo "----------------------------------------"
185 zsh -l
186 echo "----------------------------------------"
187 ok "Returned from Zsh session"
188}
189
190# =============================
191# INTERACTIVE MENU
192# =============================
193show_menu() {
194 echo "==========================================="
195 echo "macOS Minimal Zsh Setup - Choose what to do"
196 echo "==========================================="
197 echo " 0) Run ALL steps (1-6)"
198 echo " 1) Install Oh My Zsh + plugins"
199 echo " 2) Install Starship prompt"
200 echo " 3) Download custom configs (~/.alias, .func, .vimrc, etc.)"
201 echo " 4) Configure ~/.zshrc (Oh My Zsh + Starship)"
202 echo " 5) Configure global Git identity"
203 echo " 6) Switch to Zsh (Temporary Sub-shell)"
204 echo " 7) Quit"
205 echo "==========================================="
206}
207
208run_choices() {
209 local input
210 read -p "Select: " input
211 input="${input//,/ }"
212
213 local -a to_run=()
214 local -a to_exclude=()
215
216 for item in $input; do
217 if [[ "$item" == !* ]]; then
218 to_exclude+=("${item:1}")
219 elif [[ "$item" == "0" ]]; then
220 to_run+=(1 2 3 4 5 6)
221 else
222 to_run+=("$item")
223 fi
224 done
225
226 if [[ ${#to_run[@]} -gt 0 ]]; then
227 for choice in "${to_run[@]}"; do
228 local skip=false
229
230 if [[ ${#to_exclude[@]} -gt 0 ]]; then
231 for ex in "${to_exclude[@]}"; do
232 if [[ "$choice" == "$ex" ]]; then
233 skip=true
234 break
235 fi
236 done
237 fi
238
239 $skip && continue
240
241 case "$choice" in
242 1) install_oh_my_zsh ;;
243 2) install_starship ;;
244 3) download_configs ;;
245 4) configure_zshrc ;;
246 5) configure_git_identity ;;
247 6) switch_shell ;;
248 7) exit 0 ;;
249 *) warn "Skipping invalid option: $choice" ;;
250 esac
251 echo
252 done
253 fi
254}
255
256# =============================
257# MAIN
258# =============================
259main() {
260 check_requirements
261 while true; do
262 show_menu
263 run_choices
264 read -p "Do you want to run more options? (y/n): " again
265 [[ "$again" =~ ^[Yy]$ ]] || break
266 done
267 ok "macOS minimal Zsh configuration complete!"
268}
269
270main "$@"
271
zsh_ubuntu.sh Originalformat
1#!/bin/bash
2set -euo pipefail
3
4# =============================
5# COLORS & LOGGING
6# =============================
7RED='\033[0;31m'
8GREEN='\033[0;32m'
9YELLOW='\033[1;33m'
10BLUE='\033[0;34m'
11NC='\033[0m'
12
13log() { echo -e "${BLUE}[INFO]${NC} $*"; }
14ok() { echo -e "${GREEN}[OK]${NC} $*"; }
15warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
16err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
17
18# =============================
19# FLAGS & CONFIG
20# =============================
21SKIP_PACKAGES=false
22SKIP_SHELL_CHANGE=false
23INSTALL_HOMEBREW=false
24
25GIST_RAW_BASE="https://opengist.resetrix.work/weehong/f0d940c3c1214bf5b7996195199fdc09/raw/HEAD"
26CONFIG_FILES=(
27 ".alias"
28 ".func"
29 ".pathrc"
30 ".sourcerc"
31 ".vimrc"
32 ".zshrc"
33 ".config/starship.toml"
34 ".config/ai-commit-prompt.txt"
35 ".config/ai-commit-split-prompt.txt"
36 ".config/opencode/agents/ai-commit.md"
37 ".config/opencode/agents/ai-commit-plan.md"
38)
39
40# =============================
41# OS DETECTION
42# =============================
43detect_os() {
44 if [[ "$OSTYPE" == "darwin"* ]]; then
45 echo "macos"
46 elif [ -f /etc/os-release ]; then
47 . /etc/os-release
48 echo "$ID"
49 else
50 echo "unknown"
51 fi
52}
53
54# =============================
55# REQUIREMENTS
56# =============================
57check_requirements() {
58 if [[ $EUID -eq 0 ]]; then
59 err "Do not run as root. The script will request sudo when necessary."
60 exit 1
61 fi
62 if [[ "$(detect_os)" != "macos" ]] && ! command -v sudo >/dev/null 2>&1; then
63 err "sudo required on Linux"
64 exit 1
65 fi
66}
67
68# =============================
69# INSTALLATION FUNCTIONS
70# =============================
71update_system() {
72 $SKIP_PACKAGES && return
73 log "Updating system..."
74 local os=$(detect_os)
75
76 case "$os" in
77 macos)
78 brew update || warn "Homebrew update failed; continuing installer"
79 ;;
80 ubuntu|debian)
81 sudo apt-get update -y
82 if ! sudo apt-get upgrade -y; then
83 warn "System upgrade did not complete. This can happen when apt wants to downgrade a package."
84 warn "Continuing because the Zsh setup does not require OS package upgrades to finish."
85 fi
86 ;;
87 fedora)
88 sudo dnf upgrade -y || warn "System upgrade failed; continuing installer"
89 ;;
90 arch)
91 sudo pacman -Syu --noconfirm || warn "System upgrade failed; continuing installer"
92 ;;
93 *) warn "Auto-update not supported for OS: $os" ;;
94 esac
95 ok "System update step finished"
96}
97
98install_packages() {
99 $SKIP_PACKAGES && return
100 log "Installing core packages..."
101 local os=$(detect_os)
102
103 case "$os" in
104 macos) brew install zsh git vim curl wget unzip xz ;;
105 ubuntu|debian) sudo apt-get install -y zsh git vim curl wget unzip zip build-essential xz-utils ;;
106 fedora) sudo dnf install -y zsh git vim curl wget unzip zip @development-tools xz ;;
107 arch) sudo pacman -S --noconfirm zsh git vim curl wget unzip zip base-devel xz ;;
108 *) warn "Auto-install not supported for OS: $os. Please install zsh, git, vim, curl manually." ;;
109 esac
110 ok "Packages installed"
111}
112
113set_timezone() {
114 log "Setting timezone to Asia/Singapore..."
115 local os=$(detect_os)
116 if [[ "$os" == "macos" ]]; then
117 sudo systemsetup -settimezone Asia/Singapore >/dev/null
118 else
119 sudo timedatectl set-timezone Asia/Singapore
120 fi
121 ok "Timezone set to Asia/Singapore"
122}
123
124install_homebrew() {
125 ! $INSTALL_HOMEBREW && return
126 command -v brew >/dev/null 2>&1 && ok "Homebrew already installed" && return
127 log "Installing Homebrew..."
128 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
129 ok "Homebrew installed"
130}
131
132configure_shell() {
133 $SKIP_SHELL_CHANGE && return
134 log "Changing default shell to zsh..."
135 local zsh_path
136 zsh_path="$(command -v zsh)"
137
138 if ! grep -qx "$zsh_path" /etc/shells; then
139 echo "$zsh_path" | sudo tee -a /etc/shells >/dev/null
140 fi
141 chsh -s "$zsh_path"
142 ok "Shell changed (requires logout/login to take effect)"
143}
144
145install_oh_my_zsh() {
146 log "Installing Oh My Zsh and plugins..."
147 export RUNZSH=no
148 export CHSH=no
149 export KEEP_ZSHRC=yes
150
151 if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
152 sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended
153 else
154 ok "Oh My Zsh already installed"
155 fi
156
157 local custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
158 local plugin_dir="$custom/plugins"
159 mkdir -p "$plugin_dir"
160
161 [[ -d "$plugin_dir/zsh-autosuggestions" ]] || git clone https://github.com/zsh-users/zsh-autosuggestions "$plugin_dir/zsh-autosuggestions"
162 [[ -d "$plugin_dir/zsh-syntax-highlighting" ]] || git clone https://github.com/zsh-users/zsh-syntax-highlighting "$plugin_dir/zsh-syntax-highlighting"
163
164 ok "Oh My Zsh plugins installed to $plugin_dir"
165}
166
167install_starship() {
168 if command -v starship >/dev/null 2>&1 || [[ -f "$HOME/.local/bin/starship" ]]; then
169 ok "Starship already installed"
170 else
171 log "Installing Starship prompt..."
172 if command -v brew >/dev/null 2>&1; then
173 brew install starship
174 else
175 mkdir -p "$HOME/.local/bin"
176 curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin"
177 fi
178 ok "Starship installed"
179 fi
180}
181
182download_configs() {
183 log "Downloading custom config files from OpenGist..."
184 for f in "${CONFIG_FILES[@]}"; do
185 local remote_file="${f#.}"
186 [[ "$f" == ".config/starship.toml" ]] && remote_file="starship.toml"
187 [[ "$f" == ".config/ai-commit-prompt.txt" ]] && remote_file="ai-commit-prompt.txt"
188 [[ "$f" == ".config/ai-commit-split-prompt.txt" ]] && remote_file="ai-commit-split-prompt.txt"
189 [[ "$f" == ".config/opencode/agents/ai-commit.md" ]] && remote_file="ai-commit.md"
190 [[ "$f" == ".config/opencode/agents/ai-commit-plan.md" ]] && remote_file="ai-commit-plan.md"
191 local url="$GIST_RAW_BASE/$remote_file"
192 local target="$HOME/$f"
193 local tmp="${target}.tmp.$$"
194
195 mkdir -p "$(dirname "$target")"
196
197 log "Fetching $remote_file -> $f ..."
198 if curl -fsSL "$url" -o "$tmp"; then
199 mv "$tmp" "$target"
200 else
201 rm -f "$tmp"
202 warn "Failed to download $remote_file"
203 fi
204 done
205
206 ok "Configs downloaded"
207}
208
209configure_zshrc() {
210 local zshrc="$HOME/.zshrc"
211 log "Configuring .zshrc for Oh My Zsh and Starship..."
212
213 cat > "$zshrc" <<'EOF'
214# =============================================================================
215# 1. HELPER FUNCTIONS & PATH
216# =============================================================================
217source_if_readable() {
218 local file="$1"
219 if [[ -f "$file" && -r "$file" ]]; then
220 source "$file"
221 fi
222}
223
224export PATH="$HOME/.local/bin:$PATH"
225
226# =============================================================================
227# 2. OH MY ZSH FRAMEWORK
228# =============================================================================
229export ZSH="${ZSH:-$HOME/.oh-my-zsh}"
230ZSH_THEME=""
231
232plugins=(
233 git
234 zsh-autosuggestions
235 zsh-syntax-highlighting
236)
237
238source_if_readable "$ZSH/oh-my-zsh.sh"
239
240# =============================================================================
241# 3. THIRD-PARTY INITIALIZATION & CUSTOM CONFIGS
242# =============================================================================
243source_if_readable "$HOME/.sourcerc"
244source_if_readable "$HOME/.func"
245source_if_readable "$HOME/.pathrc"
246source_if_readable "$HOME/.alias"
247
248# =============================================================================
249# 4. STARSHIP PROMPT
250# =============================================================================
251if [[ -z "${STARSHIP_CONFIG:-}" && -f "$HOME/.config/starship.toml" ]]; then
252 export STARSHIP_CONFIG="$HOME/.config/starship.toml"
253fi
254
255if command -v starship >/dev/null 2>&1; then
256 eval "$(starship init zsh)"
257elif [[ -x "$HOME/.local/bin/starship" ]]; then
258 eval "$("$HOME/.local/bin/starship" init zsh)"
259fi
260EOF
261
262 ok ".zshrc configured for Oh My Zsh framework with Starship prompt"
263}
264
265configure_git_identity() {
266 log "Configuring global Git identity..."
267 git config --global user.name "Vernon Wee Hong KOH"
268 git config --global user.email "261549452+weehong@users.noreply.github.com"
269 ok "Global Git identity configured"
270}
271
272switch_shell() {
273 log "Starting Zsh session..."
274 echo -e "${YELLOW}Type 'exit' to return to this installer menu.${NC}"
275 echo "----------------------------------------"
276 zsh -l
277 echo "----------------------------------------"
278 ok "Returned from Zsh session"
279}
280
281# =============================
282# INTERACTIVE MENU
283# =============================
284show_menu() {
285 echo "==========================================="
286 echo "Minimal Zsh Installer - Choose what to do"
287 echo "==========================================="
288 echo " 0) Run ALL steps (1-11)"
289 echo " 1) Update system packages"
290 echo " 2) Install core packages (zsh, git, vim, etc.)"
291 echo " 3) Set Timezone (Asia/Singapore)"
292 echo " 4) Install Homebrew"
293 echo " 5) Configure shell (chsh - sets default shell)"
294 echo " 6) Install Oh My Zsh + plugins"
295 echo " 7) Install Starship prompt"
296 echo " 8) Download custom configs (from OpenGist)"
297 echo " 9) Configure ~/.zshrc (Oh My Zsh + Starship)"
298 echo "10) Configure global Git identity"
299 echo "11) Switch to Zsh (Temporary Sub-shell)"
300 echo "12) Quit"
301 echo "==========================================="
302}
303
304run_choices() {
305 local input
306 read -p "Select: " input
307 input="${input//,/ }"
308
309 local -a to_run=()
310 local -a to_exclude=()
311
312 for item in $input; do
313 if [[ "$item" == !* ]]; then
314 to_exclude+=("${item:1}")
315 elif [[ "$item" == "0" ]]; then
316 to_run+=(1 2 3 4 5 6 7 8 9 10 11)
317 else
318 to_run+=("$item")
319 fi
320 done
321
322 for choice in "${to_run[@]}"; do
323 local skip=false
324
325 for ex in "${to_exclude[@]}"; do
326 if [[ "$choice" == "$ex" ]]; then
327 skip=true
328 break
329 fi
330 done
331
332 $skip && continue
333
334 case "$choice" in
335 1) update_system ;;
336 2) install_packages ;;
337 3) set_timezone ;;
338 4) install_homebrew ;;
339 5) configure_shell ;;
340 6) install_oh_my_zsh ;;
341 7) install_starship ;;
342 8) download_configs ;;
343 9) configure_zshrc ;;
344 10) configure_git_identity ;;
345 11) switch_shell ;;
346 12) log "Exiting..."; exit 0 ;;
347 *) warn "Skipping invalid option: $choice" ;;
348 esac
349 echo
350 done
351}
352
353# =============================
354# MAIN
355# =============================
356main() {
357 check_requirements
358 while true; do
359 show_menu
360 run_choices
361 read -p "Do you want to run more options? (y/n): " again
362 [[ "$again" =~ ^[Yy]$ ]] || break
363 done
364 ok "Zsh installation/configuration complete!"
365}
366
367main "$@"
368
zsh_wsl.sh Originalformat
1#!/bin/bash
2set -euo pipefail
3
4# =============================
5# COLORS & LOGGING
6# =============================
7RED='\033[0;31m'
8GREEN='\033[0;32m'
9YELLOW='\033[1;33m'
10BLUE='\033[0;34m'
11NC='\033[0m'
12
13log() { echo -e "${BLUE}[INFO]${NC} $*"; }
14ok() { echo -e "${GREEN}[OK]${NC} $*"; }
15warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
16err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
17
18# =============================
19# FLAGS & CONFIG
20# =============================
21SKIP_PACKAGES=false
22SKIP_SHELL_CHANGE=false
23INSTALL_HOMEBREW=false
24
25GIST_RAW_BASE="https://opengist.resetrix.work/weehong/f0d940c3c1214bf5b7996195199fdc09/raw/HEAD"
26CONFIG_FILES=(
27 ".alias"
28 ".func"
29 ".pathrc"
30 ".sourcerc"
31 ".vimrc"
32 ".zshrc"
33 ".config/starship.toml"
34 ".config/ai-commit-prompt.txt"
35 ".config/ai-commit-split-prompt.txt"
36 ".config/opencode/agents/ai-commit.md"
37 ".config/opencode/agents/ai-commit-plan.md"
38)
39
40# =============================
41# PLATFORM DETECTION
42# =============================
43detect_os() {
44 if [ -f /etc/os-release ]; then
45 . /etc/os-release
46 echo "$ID"
47 else
48 echo "unknown"
49 fi
50}
51
52is_wsl() {
53 grep -qi "microsoft" /proc/version 2>/dev/null
54}
55
56# =============================
57# REQUIREMENTS
58# =============================
59check_requirements() {
60 if [[ $EUID -eq 0 ]]; then
61 err "Do not run as root. The script will request sudo when necessary."
62 exit 1
63 fi
64 if ! command -v sudo >/dev/null 2>&1; then
65 err "sudo required"
66 exit 1
67 fi
68 if ! is_wsl; then
69 err "This installer is intended for WSL"
70 exit 1
71 fi
72}
73
74# =============================
75# INSTALLATION FUNCTIONS
76# =============================
77update_system() {
78 $SKIP_PACKAGES && return
79 log "Updating system..."
80 local os=$(detect_os)
81
82 case "$os" in
83 ubuntu|debian)
84 sudo apt-get update -y
85 if ! sudo apt-get upgrade -y; then
86 warn "System upgrade did not complete. This can happen when apt wants to downgrade a package."
87 warn "Continuing because the Zsh setup does not require OS package upgrades to finish."
88 fi
89 ;;
90 fedora)
91 sudo dnf upgrade -y || warn "System upgrade failed; continuing installer"
92 ;;
93 arch)
94 sudo pacman -Syu --noconfirm || warn "System upgrade failed; continuing installer"
95 ;;
96 *) warn "Auto-update not supported for OS: $os" ;;
97 esac
98 ok "System update step finished"
99}
100
101install_packages() {
102 $SKIP_PACKAGES && return
103 log "Installing core packages..."
104 local os=$(detect_os)
105
106 case "$os" in
107 ubuntu|debian) sudo apt-get install -y zsh git vim curl wget unzip zip build-essential xz-utils ;;
108 fedora) sudo dnf install -y zsh git vim curl wget unzip zip @development-tools xz ;;
109 arch) sudo pacman -S --noconfirm zsh git vim curl wget unzip zip base-devel xz ;;
110 *) warn "Auto-install not supported for OS: $os. Please install zsh, git, vim, curl manually." ;;
111 esac
112 ok "Packages installed"
113}
114
115set_timezone() {
116 log "Checking timezone configuration..."
117 if command -v timedatectl >/dev/null 2>&1 && timedatectl status >/dev/null 2>&1; then
118 sudo timedatectl set-timezone Asia/Singapore
119 ok "Timezone set to Asia/Singapore"
120 return
121 fi
122
123 warn "Skipping timezone change: timedatectl is not available in this WSL environment"
124}
125
126install_homebrew() {
127 ! $INSTALL_HOMEBREW && return
128 command -v brew >/dev/null 2>&1 && ok "Homebrew already installed" && return
129 log "Installing Homebrew..."
130 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
131 ok "Homebrew installed"
132}
133
134configure_shell() {
135 $SKIP_SHELL_CHANGE && return
136 log "Changing default shell to zsh..."
137 local zsh_path
138 zsh_path="$(command -v zsh)"
139
140 if ! grep -qx "$zsh_path" /etc/shells; then
141 echo "$zsh_path" | sudo tee -a /etc/shells >/dev/null
142 fi
143 chsh -s "$zsh_path"
144 ok "Shell changed (open a new WSL session for it to take effect)"
145}
146
147install_oh_my_zsh() {
148 log "Installing Oh My Zsh and plugins..."
149 export RUNZSH=no
150 export CHSH=no
151 export KEEP_ZSHRC=yes
152
153 if [[ ! -d "$HOME/.oh-my-zsh" ]]; then
154 sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended
155 else
156 ok "Oh My Zsh already installed"
157 fi
158
159 local custom="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
160 local plugin_dir="$custom/plugins"
161 mkdir -p "$plugin_dir"
162
163 [[ -d "$plugin_dir/zsh-autosuggestions" ]] || git clone https://github.com/zsh-users/zsh-autosuggestions "$plugin_dir/zsh-autosuggestions"
164 [[ -d "$plugin_dir/zsh-syntax-highlighting" ]] || git clone https://github.com/zsh-users/zsh-syntax-highlighting "$plugin_dir/zsh-syntax-highlighting"
165
166 ok "Oh My Zsh plugins installed to $plugin_dir"
167}
168
169install_starship() {
170 if command -v starship >/dev/null 2>&1 || [[ -f "$HOME/.local/bin/starship" ]]; then
171 ok "Starship already installed"
172 else
173 log "Installing Starship prompt..."
174 if command -v brew >/dev/null 2>&1; then
175 brew install starship
176 else
177 mkdir -p "$HOME/.local/bin"
178 curl -fsSL https://starship.rs/install.sh | sh -s -- -y -b "$HOME/.local/bin"
179 fi
180 ok "Starship installed"
181 fi
182}
183
184download_configs() {
185 log "Downloading custom config files from OpenGist..."
186 for f in "${CONFIG_FILES[@]}"; do
187 local remote_file="${f#.}"
188 [[ "$f" == ".config/starship.toml" ]] && remote_file="starship.toml"
189 [[ "$f" == ".config/ai-commit-prompt.txt" ]] && remote_file="ai-commit-prompt.txt"
190 [[ "$f" == ".config/ai-commit-split-prompt.txt" ]] && remote_file="ai-commit-split-prompt.txt"
191 [[ "$f" == ".config/opencode/agents/ai-commit.md" ]] && remote_file="ai-commit.md"
192 [[ "$f" == ".config/opencode/agents/ai-commit-plan.md" ]] && remote_file="ai-commit-plan.md"
193 local url="$GIST_RAW_BASE/$remote_file"
194 local target="$HOME/$f"
195 local tmp="${target}.tmp.$$"
196
197 mkdir -p "$(dirname "$target")"
198
199 log "Fetching $remote_file -> $f ..."
200 if curl -fsSL "$url" -o "$tmp"; then
201 mv "$tmp" "$target"
202 else
203 rm -f "$tmp"
204 warn "Failed to download $remote_file"
205 fi
206 done
207
208 ok "Configs downloaded"
209}
210
211configure_zshrc() {
212 local zshrc="$HOME/.zshrc"
213 log "Configuring .zshrc for Oh My Zsh and Starship..."
214
215 cat > "$zshrc" <<'EOF'
216# =============================================================================
217# 1. HELPER FUNCTIONS & PATH
218# =============================================================================
219source_if_readable() {
220 local file="$1"
221 if [[ -f "$file" && -r "$file" ]]; then
222 source "$file"
223 fi
224}
225
226export PATH="$HOME/.local/bin:$PATH"
227
228# =============================================================================
229# 2. OH MY ZSH FRAMEWORK
230# =============================================================================
231export ZSH="${ZSH:-$HOME/.oh-my-zsh}"
232ZSH_THEME=""
233
234plugins=(
235 git
236 zsh-autosuggestions
237 zsh-syntax-highlighting
238)
239
240source_if_readable "$ZSH/oh-my-zsh.sh"
241
242# =============================================================================
243# 3. THIRD-PARTY INITIALIZATION & CUSTOM CONFIGS
244# =============================================================================
245source_if_readable "$HOME/.sourcerc"
246source_if_readable "$HOME/.func"
247source_if_readable "$HOME/.pathrc"
248source_if_readable "$HOME/.alias"
249
250# =============================================================================
251# 4. STARSHIP PROMPT
252# =============================================================================
253if [[ -z "${STARSHIP_CONFIG:-}" && -f "$HOME/.config/starship.toml" ]]; then
254 export STARSHIP_CONFIG="$HOME/.config/starship.toml"
255fi
256
257if command -v starship >/dev/null 2>&1; then
258 eval "$(starship init zsh)"
259elif [[ -x "$HOME/.local/bin/starship" ]]; then
260 eval "$("$HOME/.local/bin/starship" init zsh)"
261fi
262EOF
263
264 ok ".zshrc configured for Oh My Zsh framework with Starship prompt"
265}
266
267configure_git_identity() {
268 log "Configuring global Git identity..."
269 git config --global user.name "Vernon Wee Hong KOH"
270 git config --global user.email "261549452+weehong@users.noreply.github.com"
271 ok "Global Git identity configured"
272}
273
274switch_shell() {
275 log "Starting Zsh session..."
276 echo -e "${YELLOW}Type 'exit' to return to this installer menu.${NC}"
277 echo "----------------------------------------"
278 zsh -l
279 echo "----------------------------------------"
280 ok "Returned from Zsh session"
281}
282
283# =============================
284# INTERACTIVE MENU
285# =============================
286show_menu() {
287 echo "==========================================="
288 echo "WSL Minimal Zsh Installer - Choose what to do"
289 echo "==========================================="
290 echo " 0) Run ALL steps (1-11)"
291 echo " 1) Update system packages"
292 echo " 2) Install core packages (zsh, git, vim, etc.)"
293 echo " 3) Set Timezone (best effort)"
294 echo " 4) Install Homebrew"
295 echo " 5) Configure shell (chsh - sets default shell)"
296 echo " 6) Install Oh My Zsh + plugins"
297 echo " 7) Install Starship prompt"
298 echo " 8) Download custom configs (from OpenGist)"
299 echo " 9) Configure ~/.zshrc (Oh My Zsh + Starship)"
300 echo "10) Configure global Git identity"
301 echo "11) Switch to Zsh (Temporary Sub-shell)"
302 echo "12) Quit"
303 echo "==========================================="
304}
305
306run_choices() {
307 local input
308 read -p "Select: " input
309 input="${input//,/ }"
310
311 local -a to_run=()
312 local -a to_exclude=()
313
314 for item in $input; do
315 if [[ "$item" == !* ]]; then
316 to_exclude+=("${item:1}")
317 elif [[ "$item" == "0" ]]; then
318 to_run+=(1 2 3 4 5 6 7 8 9 10 11)
319 else
320 to_run+=("$item")
321 fi
322 done
323
324 for choice in "${to_run[@]}"; do
325 local skip=false
326
327 for ex in "${to_exclude[@]}"; do
328 if [[ "$choice" == "$ex" ]]; then
329 skip=true
330 break
331 fi
332 done
333
334 $skip && continue
335
336 case "$choice" in
337 1) update_system ;;
338 2) install_packages ;;
339 3) set_timezone ;;
340 4) install_homebrew ;;
341 5) configure_shell ;;
342 6) install_oh_my_zsh ;;
343 7) install_starship ;;
344 8) download_configs ;;
345 9) configure_zshrc ;;
346 10) configure_git_identity ;;
347 11) switch_shell ;;
348 12) log "Exiting..."; exit 0 ;;
349 *) warn "Skipping invalid option: $choice" ;;
350 esac
351 echo
352 done
353}
354
355# =============================
356# MAIN
357# =============================
358main() {
359 check_requirements
360 while true; do
361 show_menu
362 run_choices
363 read -p "Do you want to run more options? (y/n): " again
364 [[ "$again" =~ ^[Yy]$ ]] || break
365 done
366 ok "WSL Zsh installation/configuration complete!"
367}
368
369main "$@"
370
zshrc Originalformat
1# =============================================================================
2# 1. HELPER FUNCTIONS & PATH
3# =============================================================================
4source_if_readable() {
5 local file="$1"
6 if [[ -f "$file" && -r "$file" ]]; then
7 source "$file"
8 fi
9}
10
11# Ensure local bin is in PATH early (catches manual Starship installations)
12export PATH="$HOME/.local/bin:$PATH"
13
14# =============================================================================
15# 2. OH MY ZSH FRAMEWORK
16# =============================================================================
17export ZSH="${ZSH:-$HOME/.oh-my-zsh}"
18ZSH_THEME=""
19
20plugins=(
21 git
22 zsh-autosuggestions
23 zsh-syntax-highlighting
24)
25
26source_if_readable "$ZSH/oh-my-zsh.sh"
27
28# =============================================================================
29# 3. THIRD-PARTY INITIALIZATION & CUSTOM CONFIGS
30# =============================================================================
31# Load external initializers (SDKMAN, NVM, etc.)
32source_if_readable "$HOME/.sourcerc"
33
34# Custom Functions (must load before Path Management)
35source_if_readable "$HOME/.func"
36
37# Path Management (Relies on append_path from .func)
38source_if_readable "$HOME/.pathrc"
39
40# Aliases (Loaded late so they override framework/system defaults)
41source_if_readable "$HOME/.alias"
42
43# =============================================================================
44# 4. STARSHIP PROMPT
45# =============================================================================
46if [[ -z "${STARSHIP_CONFIG:-}" && -f "$HOME/.config/starship.toml" ]]; then
47 export STARSHIP_CONFIG="$HOME/.config/starship.toml"
48fi
49
50# Starship owns the prompt. It must initialize after Oh My Zsh.
51if command -v starship >/dev/null 2>&1; then
52 eval "$(starship init zsh)"
53elif [[ -x "$HOME/.local/bin/starship" ]]; then
54 eval "$("$HOME/.local/bin/starship" init zsh)"
55fi
56