@maxiaochao/pi-toolkit 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2026-09-11 - v0.3.0
2
+
3
+ - add pi-worktree CLI: one-command worktree + Pi launch with list/info/out/remove/prune and bash completion (postinstall to ~/.local/bin)
4
+
1
5
  ## 2026-09-09 - v0.2.6
2
6
 
3
7
  - fix cache export: segment-view tooltip and event markers now show global request numbers (aligned with the x-axis) instead of relative 1-based; e2e asserts real tooltip values
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-toolkit
2
2
 
3
- Personal Pi toolkit with the `/todos`, `/btw`, and `/cache_export` extensions, bundled agent skills including Chrome/Chromium automation, and the Night Owl theme.
3
+ Personal Pi toolkit with the `/todos`, `/btw`, and `/cache_export` extensions, bundled agent skills including Chrome/Chromium automation, the Night Owl theme, and the `pi-worktree` CLI.
4
4
 
5
5
  ## Install
6
6
 
@@ -44,6 +44,21 @@ Use `/cache_export [path]` to write an interactive per-request cache dashboard.
44
44
 
45
45
  The bundled `nightowl` theme is available to Pi through the package manifest and is selected automatically on npm installation only when no theme is already configured.
46
46
 
47
+ ## pi-worktree CLI
48
+
49
+ The package also ships a standalone shell command, `pi-worktree`, that wraps Git worktree creation and Pi launch into one step. On installation (Unix-like systems) the postinstall hook copies it to `~/.local/bin/pi-worktree` and installs bash completion to `~/.local/share/bash-completion/completions/pi-worktree` (no `.bashrc` edits needed).
50
+
51
+ ```bash
52
+ pi-worktree feat-ui # create/reuse .worktrees/feat-ui, branch feat-ui, launch pi inside
53
+ pi-worktree list # list worktrees (main checkout marked "main checkout")
54
+ pi-worktree info feat-ui # base commit, ahead/behind main, unique commits
55
+ pi-worktree out # open a shell in the main checkout (for merging)
56
+ pi-worktree remove feat-ui # remove the worktree (refuses unmerged branches)
57
+ pi-worktree prune # drop stale registrations for manually deleted dirs
58
+ ```
59
+
60
+ Typical loop: `pi-worktree <name>` to start, commit inside Pi, `pi-worktree out` + `git merge <name>` to land the work, `pi-worktree remove <name>` to clean up. New branches are created from `origin/main` (falling back to `main`, then `HEAD`), and the base commit is recorded in `branch.<name>.base` config so `info` can always tell you where the branch came from. Bash completion covers subcommands and existing worktree names (`pi-worktree feat<Tab>`).
61
+
47
62
  The bundled `web-browser` skill provides reusable scripts for starting Chrome/Chromium with remote debugging, navigating tabs, evaluating JavaScript, emulating devices, taking screenshots, dismissing cookie dialogs, and inspecting browser logs. It auto-detects common Chrome/Chromium installations on macOS and Linux, including Windows Chrome when Pi runs under WSL; set `BROWSER_BIN` when the binary is elsewhere.
48
63
 
49
64
  The browser skill can be loaded explicitly with `/skill:web-browser`. Its scripts are available relative to the skill directory, for example:
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env bash
2
+ # pi-worktree: create/reuse a git worktree, then launch pi inside it.
3
+ #
4
+ # Usage:
5
+ # pi-worktree [name] [pi args...] # enter worktree <name> and start pi
6
+ # pi-worktree -l | --list # list worktrees (main checkout marked "main checkout")
7
+ # pi-worktree -i <name> # show base commit, ahead/behind, unique commits
8
+ # pi-worktree -o | --out # cd back to the main checkout
9
+ # pi-worktree -r <name> # remove worktree <name> (must be clean & merged)
10
+ # pi-worktree -p | --prune # drop stale worktree registrations
11
+ set -euo pipefail
12
+
13
+ # Colors: only when stdout is a TTY and NO_COLOR is unset (no-color.org)
14
+ if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
15
+ C_BOLD=$'\e[1m' C_DIM=$'\e[2m' C_RED=$'\e[31m' C_GREEN=$'\e[32m' C_YELLOW=$'\e[33m' C_CYAN=$'\e[36m' C_RESET=$'\e[0m'
16
+ else
17
+ C_BOLD='' C_DIM='' C_RED='' C_GREEN='' C_YELLOW='' C_CYAN='' C_RESET=''
18
+ fi
19
+
20
+ ok() { printf '%spi-worktree:%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
21
+ warn() { printf '%s⚠️ %s%s\n' "$C_YELLOW" "$C_RESET" "$*"; }
22
+ die() { printf '%spi-worktree: %s%s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; }
23
+
24
+ require_repo() {
25
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "not inside a git repository"
26
+ }
27
+
28
+ main_checkout() {
29
+ git rev-parse --path-format=absolute --git-common-dir | sed 's|/.git$||'
30
+ }
31
+
32
+ in_linked_worktree() {
33
+ local git_dir common_dir
34
+ git_dir=$(git rev-parse --git-dir 2>/dev/null) || return 1
35
+ common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || return 1
36
+ [[ "$(cd "$git_dir" && pwd -P)" != "$(cd "$common_dir" && pwd -P)" ]] \
37
+ && [[ -z "$(git rev-parse --show-superproject-working-tree 2>/dev/null)" ]]
38
+ }
39
+
40
+ wt_dir() { echo ".worktrees/$1"; }
41
+
42
+ # Best available base ref: origin/main > main > HEAD
43
+ pick_base() {
44
+ if git show-ref --verify --quiet refs/remotes/origin/main; then
45
+ echo origin/main
46
+ elif git show-ref --verify --quiet refs/heads/main; then
47
+ echo main
48
+ else
49
+ echo HEAD
50
+ fi
51
+ }
52
+
53
+ ensure_gitignore() {
54
+ if ! git check-ignore -q .worktrees 2>/dev/null; then
55
+ printf '.worktrees/\n' >> .gitignore
56
+ ok "added .worktrees/ to .gitignore"
57
+ fi
58
+ }
59
+
60
+ cmd_list() {
61
+ require_repo
62
+ local main_dir; main_dir=$(main_checkout)
63
+ # Annotate the primary checkout and stale (prunable) entries.
64
+ git worktree list --porcelain | awk -v main="$main_dir" \
65
+ -v D="${C_DIM}" -v Y="${C_YELLOW}" -v C="${C_CYAN}" -v R="${C_RESET}" '
66
+ /^worktree / { path=substr($0,10); next }
67
+ /^HEAD / { sha=substr($0,6); next }
68
+ /^branch / { b=substr($0,8); sub("refs/heads/","",b); next }
69
+ /^detached/ { b="(detached)"; next }
70
+ /^bare/ { b="(bare)"; next }
71
+ /^prunable/ { stale=1; next }
72
+ /^$/ {
73
+ if (b=="") b="?"
74
+ mark=(path==main) ? " " D "(main checkout)" R : ""
75
+ branch=(path==main) ? b : C b R
76
+ hint=(stale) ? " " Y "← stale: dir gone, run: pi-worktree prune" R : ""
77
+ printf "%s %s%s%s %s%s%s\n", path, D, substr(sha,1,7), R, branch, mark, hint
78
+ path=""; sha=""; b=""; stale=0
79
+ }
80
+ '
81
+ }
82
+
83
+ subcmd_prune() {
84
+ require_repo
85
+ git worktree prune -v
86
+ ok "pruned stale worktree registrations"
87
+ }
88
+
89
+ cmd_out() {
90
+ cd "$(main_checkout)" && exec "${SHELL:-/bin/bash}"
91
+ }
92
+
93
+ cmd_info() {
94
+ require_repo
95
+ [[ $# -eq 1 ]] || die "usage: pi-worktree -i <name>"
96
+ local dir; dir="$(wt_dir "$1")"
97
+ [[ -d "$dir" ]] || die "worktree not found: $dir"
98
+ local branch; branch=$(git -C "$dir" branch --show-current)
99
+ echo "worktree: $dir"
100
+ if git -C "$dir" diff --quiet 2>/dev/null && git -C "$dir" diff --cached --quiet 2>/dev/null \
101
+ && [[ -z "$(git -C "$dir" ls-files --others --exclude-standard)" ]]; then
102
+ echo "status: clean"
103
+ else
104
+ echo "status: DIRTY"
105
+ git -C "$dir" status --short | head -10
106
+ fi
107
+ echo "branch: $branch @ $(git -C "$dir" rev-parse --short HEAD)"
108
+ local base baseRef ahead behind
109
+ base=$(git config --get "branch.$branch.base" 2>/dev/null || true)
110
+ baseRef=$(git config --get "branch.$branch.baseRef" 2>/dev/null || true)
111
+ if [[ -n "$base" ]]; then
112
+ echo "based on: $baseRef @ $base"
113
+ ahead=$(git rev-list --count "$base..$branch" 2>/dev/null || echo "?")
114
+ echo "ahead: $ahead commits since base"
115
+ else
116
+ echo "based on: (not recorded; run: git merge-base main $branch)"
117
+ fi
118
+ if git show-ref --verify --quiet refs/heads/main && [[ "$branch" != main ]]; then
119
+ behind=$(git rev-list --count "$branch..main" 2>/dev/null || echo "?")
120
+ echo "behind: $behind commits vs main"$([[ "${behind:-0}" -gt 0 ]] && echo " ← consider rebase")
121
+ fi
122
+ if [[ -n "$base" ]]; then
123
+ echo "unique commits:"
124
+ git log --oneline "$base..$branch" | head -5 || true
125
+ fi
126
+ }
127
+
128
+ cmd_remove() {
129
+ require_repo
130
+ [[ $# -eq 1 ]] || die "usage: pi-worktree -r <name>"
131
+ local dir; dir="$(wt_dir "$1")"
132
+ [[ -d "$dir" ]] || die "worktree not found: $dir"
133
+ # refuse to remove if the branch is not merged into main
134
+ local branch; branch=$(git -C "$dir" branch --show-current)
135
+ if [[ -n "$branch" ]] && ! git merge-base --is-ancestor "$branch" main 2>/dev/null; then
136
+ warn "branch '$branch' does not appear merged into 'main' (or no main branch exists)."
137
+ read -rp "Remove anyway? [y/N] " ans
138
+ [[ "$ans" =~ ^[Yy]$ ]] || die "aborted"
139
+ fi
140
+ git worktree remove "$dir" && ok "removed: $dir"
141
+ }
142
+
143
+ cd_wt() {
144
+ local name="$1"; shift
145
+ require_repo
146
+
147
+ # Already isolated? Just launch pi here (superpowers Step 0).
148
+ if in_linked_worktree; then
149
+ ok "already in an isolated worktree ($(pwd)), launching pi here"
150
+ exec pi "$@"
151
+ fi
152
+
153
+ # Dirty workspace: uncommitted changes do NOT follow into the worktree.
154
+ if ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]]; then
155
+ warn "Uncommitted changes stay in THIS directory; they will NOT appear in the worktree:"
156
+ git status --short | head -20
157
+ read -rp "Continue? [y/N] " ans
158
+ [[ "$ans" =~ ^[Yy]$ ]] || die "aborted"
159
+ fi
160
+
161
+ local dir; dir="$(wt_dir "$name")"
162
+ ensure_gitignore
163
+
164
+ if [[ -d "$dir" ]]; then
165
+ # Reuse: must be a registered worktree for this repo
166
+ git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || die "$dir exists but is not a git worktree"
167
+ ok "reusing existing worktree $dir"
168
+ else
169
+ mkdir -p .worktrees
170
+ # Reuse branch if it exists, else create it
171
+ if git show-ref --verify --quiet "refs/heads/$name"; then
172
+ git worktree add "$dir" "$name"
173
+ ok "checked out existing branch '$name'"
174
+ # record a best-effort base for -i display
175
+ local mb; mb=$(git merge-base main "$name" 2>/dev/null || true)
176
+ [[ -n "$mb" ]] && git config "branch.$name.base" "$(git rev-parse --short "$mb")" \
177
+ && git config "branch.$name.baseRef" main
178
+ else
179
+ local base; base=$(pick_base)
180
+ git worktree add "$dir" -b "$name" "$base"
181
+ git config "branch.$name.base" "$(git rev-parse --short "$base")"
182
+ git config "branch.$name.baseRef" "$base"
183
+ ok "created branch '$name' from $base @ $(git rev-parse --short "$base")"
184
+ fi
185
+ fi
186
+
187
+ cd "$dir"
188
+ exec pi "$@"
189
+ }
190
+
191
+ case "${1:-}" in
192
+ -l|--list|list|ls) shift; cmd_list "$@" ;;
193
+ -i|--info|info) shift; cmd_info "$@" ;;
194
+ -o|--out|out) shift; cmd_out "$@" ;;
195
+ -r|--remove|rm|remove) shift; cmd_remove "$@" ;;
196
+ -p|--prune|prune) shift; subcmd_prune ;;
197
+ -h|--help|help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//' ;;
198
+ "") die "usage: pi-worktree <name> [pi args...] (or list / info / out / remove / prune)" ;;
199
+ *) name="$1"; shift; cd_wt "$name" "$@" ;;
200
+ esac
@@ -0,0 +1,36 @@
1
+ # bash completion for pi-worktree
2
+ _pi_worktree() {
3
+ local cur first
4
+ cur="${COMP_WORDS[COMP_CWORD]}"
5
+ first="${COMP_WORDS[1]}"
6
+
7
+ local SUBS="list ls info out remove rm prune -l --list -i --info -o --out -r --remove -p --prune -h --help"
8
+
9
+ # existing worktree names (only linked worktrees, i.e. under .worktrees/)
10
+ local wts=""
11
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
12
+ local main_dir
13
+ main_dir=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/.git$||')
14
+ wts=$(git worktree list --porcelain 2>/dev/null \
15
+ | sed -n 's|^worktree ||p' \
16
+ | while read -r p; do [[ "$p" != "$main_dir" ]] && basename "$p"; done || true)
17
+ fi
18
+
19
+ if (( COMP_CWORD == 1 )); then
20
+ # first word: subcommands, flags, and existing worktree names (to enter)
21
+ COMPREPLY=($(compgen -W "$SUBS $wts" -- "$cur"))
22
+ return
23
+ fi
24
+
25
+ case "$first" in
26
+ -i|--info|info|-r|--remove|remove|rm)
27
+ # second word of info/remove: worktree names only
28
+ COMPREPLY=($(compgen -W "$wts" -- "$cur"))
29
+ ;;
30
+ *)
31
+ # after a name: default (file) completion for pi args
32
+ COMPREPLY=()
33
+ ;;
34
+ esac
35
+ }
36
+ complete -F _pi_worktree pi-worktree
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Personal pi coding agent toolkit: /todos, /btw, /cache_export extensions, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "extensions",
12
12
  "themes",
13
+ "bin",
13
14
  "README.md",
14
15
  "CHANGELOG.md",
15
16
  "global/AGENTS.md",
@@ -33,6 +34,9 @@
33
34
  "./themes"
34
35
  ]
35
36
  },
37
+ "bin": {
38
+ "pi-worktree": "bin/pi-worktree"
39
+ },
36
40
  "peerDependencies": {
37
41
  "@earendil-works/pi-ai": "*",
38
42
  "@earendil-works/pi-coding-agent": "*",
@@ -1,10 +1,12 @@
1
1
  // postinstall hook: sync the bundled global AGENTS.md and seed the default theme
2
2
  // without overriding a theme the user has already selected.
3
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
4
- import { homedir } from "node:os";
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, chmodSync } from "node:fs";
4
+ import { homedir, platform } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
9
+
8
10
  const agentDir = join(homedir(), ".pi", "agent");
9
11
  const bundled = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "global", "AGENTS.md"), "utf8");
10
12
  const agentsTarget = join(agentDir, "AGENTS.md");
@@ -41,3 +43,22 @@ if (!existsSync(settingsTarget)) {
41
43
  console.warn("[pi-toolkit] could not set default theme:", error instanceof Error ? error.message : String(error));
42
44
  }
43
45
  }
46
+
47
+ // Install the pi-worktree CLI + bash completion on Unix-like systems.
48
+ // Copy (not symlink) so the running command does not depend on the repo/node_modules layout.
49
+ if (platform() !== "win32") {
50
+ try {
51
+ const binTarget = join(homedir(), ".local", "bin", "pi-worktree");
52
+ mkdirSync(dirname(binTarget), { recursive: true });
53
+ copyFileSync(join(packageRoot, "bin", "pi-worktree"), binTarget);
54
+ chmodSync(binTarget, 0o755);
55
+ console.log("[pi-toolkit] pi-worktree installed ->", binTarget);
56
+
57
+ const compTarget = join(homedir(), ".local", "share", "bash-completion", "completions", "pi-worktree");
58
+ mkdirSync(dirname(compTarget), { recursive: true });
59
+ copyFileSync(join(packageRoot, "bin", "pi-worktree-completion.bash"), compTarget);
60
+ console.log("[pi-toolkit] pi-worktree bash completion installed ->", compTarget);
61
+ } catch (error) {
62
+ console.warn("[pi-toolkit] could not install pi-worktree CLI:", error instanceof Error ? error.message : String(error));
63
+ }
64
+ }