harveyz-skill 0.1.2 → 0.2.1
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/bin/cli.js +116 -44
- package/lib/bundles.js +97 -22
- package/lib/installer.js +126 -5
- package/lib/targets.js +1 -0
- package/lib/vars.js +34 -0
- package/package.json +7 -3
- package/skills/harness/diataxis-docs/SKILL.md +128 -0
- package/skills/harness/diataxis-docs/references/custom-categories.md +32 -0
- package/skills/harness/diataxis-docs/references/index-template.md +29 -0
- package/skills/web-fetch/article-fetcher/SKILL.md +625 -0
- package/skills/web-fetch/article-fetcher/references/article_utils.py +225 -0
- package/skills/web-fetch/article-fetcher/references/file-format.md +73 -0
- package/skills/web-fetch/article-fetcher/scripts/import_reading.py +75 -0
- package/skills/web-fetch/article-fetcher/scripts/init_db.py +33 -0
- package/skills/web-fetch/article-fetcher/scripts/url-index.db +0 -0
- package/skills/web-fetch/article-fetcher/scripts/url-index.db.bak +0 -0
- package/skills/web-fetch/article-fetcher/vars.json +12 -0
- package/skills/writing/mermaid-diagram/SKILL.md +220 -0
- package/skills/writing/mermaid-diagram/references/color-templates.md +75 -0
- package/skills/writing/mermaid-diagram/references/flowchart.md +88 -0
- package/skills/writing/mermaid-diagram/references/gantt-timeline.md +62 -0
- package/skills/writing/mermaid-diagram/references/other-charts.md +55 -0
- package/skills/writing/mermaid-diagram/references/sequence.md +143 -0
- package/skills/writing/mermaid-diagram/references/statediagram.md +54 -0
- package/skills/writing/mermaid-diagram/scripts/check_mermaid.py +84 -0
- package/skills-index.json +32 -0
- package/tools/p-launch/p-launch.sh +275 -0
- package/tools/p-launch/tests/e2e.bats +164 -0
- package/tools/p-launch/tests/unit.bats +151 -0
- package/tools/p-launch/vars.json +9 -0
- package/tools/p-launch/zshrc.snippet +5 -0
- package/bundles.json +0 -24
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
# p-launch — interactive project launcher
|
|
3
|
+
# Usage: p-launch launch picker
|
|
4
|
+
# p-launch --config set project directories
|
|
5
|
+
# p-launch --uninstall remove everything
|
|
6
|
+
# p-launch --help show help
|
|
7
|
+
|
|
8
|
+
# ── Config ─────────────────────────────────────────────────────────────────
|
|
9
|
+
PROJECT_DIRS=()
|
|
10
|
+
# Define PROJECT_DIRS in ~/.config/p-launch/config.zsh (written by installer)
|
|
11
|
+
[[ -f "$HOME/.config/p-launch/config.zsh" ]] && source "$HOME/.config/p-launch/config.zsh"
|
|
12
|
+
|
|
13
|
+
# ── Colors ──────────────────────────────────────────────────────────────────
|
|
14
|
+
typeset -A C=(
|
|
15
|
+
[rs]=$'\033[0m' [bd]=$'\033[1m' [dim]=$'\033[2m'
|
|
16
|
+
[rd]=$'\033[31m' [gr]=$'\033[32m' [yl]=$'\033[33m' [cy]=$'\033[36m'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# ── Helpers ─────────────────────────────────────────────────────────────────
|
|
20
|
+
_die() { printf "${C[rd]}error:${C[rs]} %s\n" "$*" >&2; exit 1 }
|
|
21
|
+
|
|
22
|
+
_check_deps() {
|
|
23
|
+
command -v fzf &>/dev/null || _die "fzf not installed — fix with: brew install fzf"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# ── Project Collection ───────────────────────────────────────────────────────
|
|
27
|
+
# Scans all PROJECT_DIRS, emits paths sorted by mtime descending
|
|
28
|
+
_collect() {
|
|
29
|
+
local -a rows=()
|
|
30
|
+
local mtime
|
|
31
|
+
for base in "${PROJECT_DIRS[@]}"; do
|
|
32
|
+
[[ -d "$base" ]] || continue
|
|
33
|
+
for d in "$base"/*/; do
|
|
34
|
+
[[ -d "$d" ]] || continue
|
|
35
|
+
mtime=$(stat -f "%m" "$d" 2>/dev/null) || continue
|
|
36
|
+
rows+=("${mtime}"$'\t'"${d%/}")
|
|
37
|
+
done
|
|
38
|
+
done
|
|
39
|
+
(( ${#rows} == 0 )) && return 1
|
|
40
|
+
printf '%s\n' "${rows[@]}" | sort -t$'\t' -k1,1rn | cut -f2-
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# ── fzf Display Formatting ───────────────────────────────────────────────────
|
|
44
|
+
# Input: one path per line
|
|
45
|
+
# Output: "PADDED_NAME \t PATH \t DISPLAY_PARENT" per line (name padded for alignment)
|
|
46
|
+
_format() {
|
|
47
|
+
local -a names=() paths=() parents=()
|
|
48
|
+
while IFS= read -r p; do
|
|
49
|
+
names+=("${p:t}")
|
|
50
|
+
paths+=("$p")
|
|
51
|
+
parents+=("${${p:h}/$HOME/~}")
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
local maxlen=0
|
|
55
|
+
local n
|
|
56
|
+
for n in "${names[@]}"; do
|
|
57
|
+
(( ${#n} > maxlen )) && maxlen=${#n}
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
local i
|
|
61
|
+
for (( i = 1; i <= ${#names}; i++ )); do
|
|
62
|
+
printf "%-${maxlen}s\t%s\t%s\n" "${names[$i]}" "${paths[$i]}" "${parents[$i]}"
|
|
63
|
+
done
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# ── Launch ───────────────────────────────────────────────────────────────────
|
|
67
|
+
_launch() {
|
|
68
|
+
local path="$1"
|
|
69
|
+
local name="${path:t}"
|
|
70
|
+
local display="${path/$HOME/~}"
|
|
71
|
+
local cursor_ok=false ghostty_ok=false
|
|
72
|
+
|
|
73
|
+
# Cursor IDE
|
|
74
|
+
if command -v cursor &>/dev/null; then
|
|
75
|
+
cursor "$path" &>/dev/null &
|
|
76
|
+
cursor_ok=true
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Ghostty — force a new window at the project directory
|
|
80
|
+
if [[ -d "/Applications/Ghostty.app" ]]; then
|
|
81
|
+
open -na "Ghostty" --args --working-directory="$path"
|
|
82
|
+
ghostty_ok=true
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# ── Launch Report ──────────────────────────────────────────────────────────
|
|
86
|
+
printf '\n'
|
|
87
|
+
printf " ${C[bd]}${C[cy]}%s${C[rs]} ${C[dim]}%s${C[rs]}\n" "$name" "$display"
|
|
88
|
+
printf '\n'
|
|
89
|
+
|
|
90
|
+
if $cursor_ok; then
|
|
91
|
+
printf " ${C[gr]}✓${C[rs]} Cursor opened\n"
|
|
92
|
+
else
|
|
93
|
+
printf " ${C[yl]}⚠${C[rs]} Cursor cursor CLI not found\n"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if $ghostty_ok; then
|
|
97
|
+
printf " ${C[gr]}✓${C[rs]} Ghostty new window opened\n"
|
|
98
|
+
else
|
|
99
|
+
printf " ${C[yl]}⚠${C[rs]} Ghostty /Applications/Ghostty.app not found\n"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
printf '\n'
|
|
103
|
+
printf " ${C[dim]}environment ready — good session.${C[rs]}\n"
|
|
104
|
+
printf '\n'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# ── Uninstall ────────────────────────────────────────────────────────────────
|
|
108
|
+
_uninstall() {
|
|
109
|
+
local self="${ZSH_SCRIPT:A}"
|
|
110
|
+
local zshrc="$HOME/.zshrc"
|
|
111
|
+
local config_dir="$HOME/.config/p-launch"
|
|
112
|
+
local zshrc_marker="# >>> p-launch"
|
|
113
|
+
|
|
114
|
+
local -a targets=("$self")
|
|
115
|
+
[[ -f "$zshrc" ]] && grep -q "$zshrc_marker" "$zshrc" && \
|
|
116
|
+
targets+=("~/.zshrc block (PATH + alias p)")
|
|
117
|
+
[[ -d "$config_dir" ]] && \
|
|
118
|
+
targets+=("${config_dir/$HOME/~}")
|
|
119
|
+
|
|
120
|
+
printf '\n'
|
|
121
|
+
printf " ${C[bd]}${C[rd]}p-launch uninstall${C[rs]}\n\n"
|
|
122
|
+
printf " Will remove:\n"
|
|
123
|
+
for t in "${targets[@]}"; do
|
|
124
|
+
printf " ${C[dim]}%s${C[rs]}\n" "$t"
|
|
125
|
+
done
|
|
126
|
+
printf '\n'
|
|
127
|
+
printf " Confirm removal? [y/N] "
|
|
128
|
+
read -r reply
|
|
129
|
+
[[ "$reply" =~ ^[Yy]$ ]] || { printf '\n aborted.\n\n'; exit 0 }
|
|
130
|
+
printf '\n'
|
|
131
|
+
|
|
132
|
+
# 1. Remove the script itself (runs from memory, safe to delete)
|
|
133
|
+
if rm -- "$self" 2>/dev/null; then
|
|
134
|
+
printf " ${C[gr]}✓${C[rs]} removed %s\n" "$self"
|
|
135
|
+
else
|
|
136
|
+
printf " ${C[rd]}✗${C[rs]} could not remove %s\n" "$self"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# 2. Strip the marked block from .zshrc
|
|
140
|
+
if [[ -f "$zshrc" ]] && grep -q "$zshrc_marker" "$zshrc"; then
|
|
141
|
+
sed -i '' '/^# >>> p-launch/,/^# <<< p-launch/d' "$zshrc"
|
|
142
|
+
printf " ${C[gr]}✓${C[rs]} cleaned ~/.zshrc\n"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# 3. Remove config directory
|
|
146
|
+
if [[ -d "$config_dir" ]]; then
|
|
147
|
+
rm -rf -- "$config_dir"
|
|
148
|
+
printf " ${C[gr]}✓${C[rs]} removed %s\n" "${config_dir/$HOME/~}"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
printf '\n'
|
|
152
|
+
printf " ${C[dim]}done. run: source ~/.zshrc${C[rs]}\n\n"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# ── Config ───────────────────────────────────────────────────────────────────
|
|
156
|
+
_config() {
|
|
157
|
+
local config_file="$HOME/.config/p-launch/config.zsh"
|
|
158
|
+
|
|
159
|
+
printf '\n'
|
|
160
|
+
printf " ${C[bd]}p-launch config${C[rs]}\n\n"
|
|
161
|
+
|
|
162
|
+
# Show current dirs
|
|
163
|
+
if [[ -f "$config_file" ]]; then
|
|
164
|
+
printf " ${C[cy]}Current PROJECT_DIRS:${C[rs]}\n"
|
|
165
|
+
source "$config_file"
|
|
166
|
+
if (( ${#PROJECT_DIRS[@]} > 0 )); then
|
|
167
|
+
for d in "${PROJECT_DIRS[@]}"; do
|
|
168
|
+
printf " ${C[dim]}%s${C[rs]}\n" "$d"
|
|
169
|
+
done
|
|
170
|
+
else
|
|
171
|
+
printf " ${C[dim]}(none)${C[rs]}\n"
|
|
172
|
+
fi
|
|
173
|
+
else
|
|
174
|
+
printf " ${C[yl]}No config file found.${C[rs]}\n"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
printf '\n'
|
|
178
|
+
printf " Enter project dirs, one per line. Empty line when done.\n"
|
|
179
|
+
printf " ${C[dim]}(leave blank to keep current)${C[rs]}\n\n"
|
|
180
|
+
|
|
181
|
+
local -a new_dirs=()
|
|
182
|
+
local line
|
|
183
|
+
local idx=1
|
|
184
|
+
while true; do
|
|
185
|
+
printf " Dir %d: " "$idx"
|
|
186
|
+
read -r line
|
|
187
|
+
[[ -z "$line" ]] && break
|
|
188
|
+
new_dirs+=("$line")
|
|
189
|
+
(( idx++ ))
|
|
190
|
+
done
|
|
191
|
+
|
|
192
|
+
if (( ${#new_dirs[@]} == 0 )); then
|
|
193
|
+
printf '\n No changes made.\n\n'
|
|
194
|
+
return
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
mkdir -p "${config_file:h}"
|
|
198
|
+
{
|
|
199
|
+
printf 'PROJECT_DIRS=(\n'
|
|
200
|
+
for d in "${new_dirs[@]}"; do
|
|
201
|
+
printf ' "%s"\n' "$d"
|
|
202
|
+
done
|
|
203
|
+
printf ')\n'
|
|
204
|
+
} > "$config_file"
|
|
205
|
+
|
|
206
|
+
printf '\n'
|
|
207
|
+
printf " ${C[gr]}✓${C[rs]} Saved to ${config_file/$HOME/~}\n"
|
|
208
|
+
printf '\n'
|
|
209
|
+
printf " ${C[cy]}New PROJECT_DIRS:${C[rs]}\n"
|
|
210
|
+
for d in "${new_dirs[@]}"; do
|
|
211
|
+
printf " ${C[dim]}%s${C[rs]}\n" "$d"
|
|
212
|
+
done
|
|
213
|
+
printf '\n'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ── Main ─────────────────────────────────────────────────────────────────────
|
|
217
|
+
main() {
|
|
218
|
+
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
|
|
219
|
+
printf '\n'
|
|
220
|
+
printf " ${C[bd]}p-launch${C[rs]} — interactive project launcher\n\n"
|
|
221
|
+
printf " ${C[cy]}Usage:${C[rs]}\n"
|
|
222
|
+
printf " p-launch open project picker\n"
|
|
223
|
+
printf " p-launch --config set project directories\n"
|
|
224
|
+
printf " p-launch --uninstall remove all installed files\n"
|
|
225
|
+
printf " p-launch --help show this help\n\n"
|
|
226
|
+
printf " ${C[cy]}Config:${C[rs]}\n"
|
|
227
|
+
printf " ~/.config/p-launch/config.zsh define PROJECT_DIRS\n\n"
|
|
228
|
+
printf " ${C[cy]}Dependencies:${C[rs]}\n"
|
|
229
|
+
printf " fzf brew install fzf\n\n"
|
|
230
|
+
return
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
if [[ "$1" == "--config" || "$1" == "config" ]]; then
|
|
234
|
+
_config
|
|
235
|
+
return
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
if [[ "$1" == "--uninstall" || "$1" == "uninstall" ]]; then
|
|
239
|
+
_uninstall
|
|
240
|
+
return
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
_check_deps
|
|
244
|
+
|
|
245
|
+
local projects
|
|
246
|
+
projects=$(_collect) || {
|
|
247
|
+
printf "${C[yl]}no projects found${C[rs]} — check PROJECT_DIRS in ~/.config/p-launch/config.zsh\n"
|
|
248
|
+
exit 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
local selected
|
|
252
|
+
selected=$(
|
|
253
|
+
_format <<< "$projects" | \
|
|
254
|
+
fzf --ansi \
|
|
255
|
+
--delimiter=$'\t' \
|
|
256
|
+
--with-nth='1,3' \
|
|
257
|
+
--prompt=' › ' \
|
|
258
|
+
--header=' p-launch · ↵ launch · esc cancel · ctrl-p preview' \
|
|
259
|
+
--height=50% \
|
|
260
|
+
--layout=reverse \
|
|
261
|
+
--border=rounded \
|
|
262
|
+
--color='header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold' \
|
|
263
|
+
--preview='ls -1 {2} 2>/dev/null' \
|
|
264
|
+
--preview-window='right:30%:wrap:hidden' \
|
|
265
|
+
--bind='ctrl-p:toggle-preview'
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
[[ -z "$selected" ]] && exit 0
|
|
269
|
+
|
|
270
|
+
local proj_path
|
|
271
|
+
proj_path=$(printf '%s' "$selected" | cut -f2)
|
|
272
|
+
_launch "$proj_path"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
[[ -z "${_P_LAUNCH_TEST:-}" ]] && main "$@"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# E2E tests for p-launch — exercises the script as a real executable.
|
|
3
|
+
# Requires: bats-core (brew install bats-core)
|
|
4
|
+
|
|
5
|
+
ZSHRC_BLOCK=$'
|
|
6
|
+
# >>> p-launch (added by harveyz-skill) >>>\nexport PATH="$HOME/.local/bin:$PATH"\nalias p=p-launch\n# <<< p-launch <<<\n'
|
|
7
|
+
|
|
8
|
+
setup() {
|
|
9
|
+
SCRIPT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)/p-launch.sh"
|
|
10
|
+
TEST_DIR="$(mktemp -d)"
|
|
11
|
+
MOCK_HOME="${TEST_DIR}/home"
|
|
12
|
+
MOCK_BIN="${TEST_DIR}/bin"
|
|
13
|
+
PROJECTS_DIR="${TEST_DIR}/projects"
|
|
14
|
+
|
|
15
|
+
mkdir -p "${MOCK_HOME}" "${MOCK_BIN}" "${PROJECTS_DIR}"
|
|
16
|
+
|
|
17
|
+
# Install a copy of the script (as the real installer would do).
|
|
18
|
+
# Config file provides PROJECT_DIRS so the {{PROJECT_DIR}} placeholder
|
|
19
|
+
# never has to be substituted.
|
|
20
|
+
cp "${SCRIPT}" "${MOCK_BIN}/p-launch"
|
|
21
|
+
chmod +x "${MOCK_BIN}/p-launch"
|
|
22
|
+
mkdir -p "${MOCK_HOME}/.config/p-launch"
|
|
23
|
+
printf 'PROJECT_DIRS=("%s")\n' "${PROJECTS_DIR}" \
|
|
24
|
+
> "${MOCK_HOME}/.config/p-launch/config.zsh"
|
|
25
|
+
|
|
26
|
+
# Minimal mock for fzf — enough to satisfy _check_deps.
|
|
27
|
+
printf '#!/bin/sh\nexit 0\n' > "${MOCK_BIN}/fzf"
|
|
28
|
+
chmod +x "${MOCK_BIN}/fzf"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
teardown() {
|
|
32
|
+
rm -rf "${TEST_DIR}"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Shorthand: run the installed script with MOCK_HOME and mock fzf first in PATH.
|
|
36
|
+
_run_p() {
|
|
37
|
+
run env HOME="${MOCK_HOME}" PATH="${MOCK_BIN}:${PATH}" \
|
|
38
|
+
zsh "${MOCK_BIN}/p-launch" "$@"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ── Syntax ────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@test "script passes zsh -n syntax check" {
|
|
44
|
+
run zsh -n "${SCRIPT}"
|
|
45
|
+
[ "$status" -eq 0 ]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ── Dependency guard ──────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
@test "exits 1 with fzf error when fzf is absent from PATH" {
|
|
51
|
+
# Use an empty PATH so fzf cannot be found, but invoke zsh by full path.
|
|
52
|
+
local ZSH_BIN
|
|
53
|
+
ZSH_BIN="$(command -v zsh)"
|
|
54
|
+
mkdir -p "${TEST_DIR}/empty"
|
|
55
|
+
run bash -c "printf '' | env HOME='${MOCK_HOME}' PATH='${TEST_DIR}/empty' \
|
|
56
|
+
'$ZSH_BIN' '${MOCK_BIN}/p-launch' 2>&1"
|
|
57
|
+
[ "$status" -eq 1 ]
|
|
58
|
+
[[ "$output" == *"fzf"* ]]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# ── No projects ───────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
@test "exits 0 and prints message when project dir is empty" {
|
|
64
|
+
# PROJECTS_DIR exists but has no subdirectories.
|
|
65
|
+
_run_p
|
|
66
|
+
[ "$status" -eq 0 ]
|
|
67
|
+
[[ "$output" == *"no projects found"* ]]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# ── --uninstall: abort ────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
@test "--uninstall: N answer removes nothing" {
|
|
73
|
+
printf '%s\n' "${ZSHRC_BLOCK}" > "${MOCK_HOME}/.zshrc"
|
|
74
|
+
|
|
75
|
+
run bash -c "
|
|
76
|
+
printf 'n\n' | \
|
|
77
|
+
env HOME='${MOCK_HOME}' PATH='${MOCK_BIN}:${PATH}' \
|
|
78
|
+
zsh '${MOCK_BIN}/p-launch' --uninstall
|
|
79
|
+
"
|
|
80
|
+
[ "$status" -eq 0 ]
|
|
81
|
+
[[ "$output" == *"aborted"* ]]
|
|
82
|
+
|
|
83
|
+
# Nothing should have been removed.
|
|
84
|
+
[ -f "${MOCK_BIN}/p-launch" ]
|
|
85
|
+
grep -q "# >>> p-launch" "${MOCK_HOME}/.zshrc"
|
|
86
|
+
[ -d "${MOCK_HOME}/.config/p-launch" ]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── --uninstall: confirm ──────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
@test "--uninstall: Y removes the script itself" {
|
|
92
|
+
run bash -c "
|
|
93
|
+
printf 'y\n' | \
|
|
94
|
+
env HOME='${MOCK_HOME}' PATH='${MOCK_BIN}:${PATH}' \
|
|
95
|
+
zsh '${MOCK_BIN}/p-launch' --uninstall
|
|
96
|
+
"
|
|
97
|
+
[ "$status" -eq 0 ]
|
|
98
|
+
[ ! -f "${MOCK_BIN}/p-launch" ]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@test "--uninstall: Y strips the marker block from .zshrc" {
|
|
102
|
+
printf 'export BEFORE=1\n%s\nexport AFTER=2\n' "${ZSHRC_BLOCK}" \
|
|
103
|
+
> "${MOCK_HOME}/.zshrc"
|
|
104
|
+
|
|
105
|
+
run bash -c "
|
|
106
|
+
printf 'y\n' | \
|
|
107
|
+
env HOME='${MOCK_HOME}' PATH='${MOCK_BIN}:${PATH}' \
|
|
108
|
+
zsh '${MOCK_BIN}/p-launch' --uninstall
|
|
109
|
+
"
|
|
110
|
+
[ "$status" -eq 0 ]
|
|
111
|
+
! grep -q "# >>> p-launch" "${MOCK_HOME}/.zshrc"
|
|
112
|
+
grep -q "BEFORE=1" "${MOCK_HOME}/.zshrc"
|
|
113
|
+
grep -q "AFTER=2" "${MOCK_HOME}/.zshrc"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@test "--uninstall: Y removes the config directory" {
|
|
117
|
+
run bash -c "
|
|
118
|
+
printf 'y\n' | \
|
|
119
|
+
env HOME='${MOCK_HOME}' PATH='${MOCK_BIN}:${PATH}' \
|
|
120
|
+
zsh '${MOCK_BIN}/p-launch' --uninstall
|
|
121
|
+
"
|
|
122
|
+
[ "$status" -eq 0 ]
|
|
123
|
+
[ ! -d "${MOCK_HOME}/.config/p-launch" ]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@test "--uninstall: full cleanup removes all three targets at once" {
|
|
127
|
+
printf '%s\n' "${ZSHRC_BLOCK}" > "${MOCK_HOME}/.zshrc"
|
|
128
|
+
|
|
129
|
+
run bash -c "
|
|
130
|
+
printf 'y\n' | \
|
|
131
|
+
env HOME='${MOCK_HOME}' PATH='${MOCK_BIN}:${PATH}' \
|
|
132
|
+
zsh '${MOCK_BIN}/p-launch' --uninstall
|
|
133
|
+
"
|
|
134
|
+
[ "$status" -eq 0 ]
|
|
135
|
+
[ ! -f "${MOCK_BIN}/p-launch" ]
|
|
136
|
+
! grep -q "p-launch" "${MOCK_HOME}/.zshrc"
|
|
137
|
+
[ ! -d "${MOCK_HOME}/.config/p-launch" ]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@test "--uninstall: skips .zshrc when marker is absent" {
|
|
141
|
+
printf 'export FOO=bar\n' > "${MOCK_HOME}/.zshrc"
|
|
142
|
+
|
|
143
|
+
run bash -c "
|
|
144
|
+
printf 'y\n' | \
|
|
145
|
+
env HOME='${MOCK_HOME}' PATH='${MOCK_BIN}:${PATH}' \
|
|
146
|
+
zsh '${MOCK_BIN}/p-launch' --uninstall
|
|
147
|
+
"
|
|
148
|
+
[ "$status" -eq 0 ]
|
|
149
|
+
# Existing .zshrc content must be intact.
|
|
150
|
+
[ "$(cat "${MOCK_HOME}/.zshrc")" = "export FOO=bar" ]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@test "--uninstall: skips config dir when it does not exist" {
|
|
154
|
+
rm -rf "${MOCK_HOME}/.config/p-launch"
|
|
155
|
+
|
|
156
|
+
run bash -c "
|
|
157
|
+
printf 'y\n' | \
|
|
158
|
+
env HOME='${MOCK_HOME}' PATH='${MOCK_BIN}:${PATH}' \
|
|
159
|
+
zsh '${MOCK_BIN}/p-launch' --uninstall
|
|
160
|
+
"
|
|
161
|
+
[ "$status" -eq 0 ]
|
|
162
|
+
# Output should not mention config dir since it was absent.
|
|
163
|
+
[[ "$output" != *".config/p-launch"* ]]
|
|
164
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# Unit tests for p-launch internal functions.
|
|
3
|
+
# Requires: bats-core (brew install bats-core)
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
SCRIPT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)/p-launch.sh"
|
|
7
|
+
TEST_DIR="$(mktemp -d)"
|
|
8
|
+
MOCK_HOME="${TEST_DIR}/home"
|
|
9
|
+
mkdir -p "${MOCK_HOME}"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
teardown() {
|
|
13
|
+
rm -rf "${TEST_DIR}"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Helper: run a zsh snippet that sources the script in test mode.
|
|
17
|
+
# PROJECT_DIRS is overridden AFTER sourcing so the script's default
|
|
18
|
+
# doesn't interfere.
|
|
19
|
+
_src() {
|
|
20
|
+
local dirs="$1" code="$2"
|
|
21
|
+
zsh -c "
|
|
22
|
+
export _P_LAUNCH_TEST=1
|
|
23
|
+
export HOME='${MOCK_HOME}'
|
|
24
|
+
source '${SCRIPT}'
|
|
25
|
+
PROJECT_DIRS=(${dirs})
|
|
26
|
+
${code}
|
|
27
|
+
"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# ── _collect ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
@test "_collect: fails when project dir does not exist" {
|
|
33
|
+
run _src "'${TEST_DIR}/nonexistent'" "_collect"
|
|
34
|
+
[ "$status" -eq 1 ]
|
|
35
|
+
[ -z "$output" ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "_collect: fails when project dir is empty" {
|
|
39
|
+
mkdir -p "${TEST_DIR}/projects"
|
|
40
|
+
run _src "'${TEST_DIR}/projects'" "_collect"
|
|
41
|
+
[ "$status" -eq 1 ]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test "_collect: lists subdirectories" {
|
|
45
|
+
mkdir -p "${TEST_DIR}/projects/alpha" "${TEST_DIR}/projects/beta"
|
|
46
|
+
run _src "'${TEST_DIR}/projects'" "_collect"
|
|
47
|
+
[ "$status" -eq 0 ]
|
|
48
|
+
[ "${#lines[@]}" -eq 2 ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "_collect: sorts by mtime descending (newest first)" {
|
|
52
|
+
mkdir -p "${TEST_DIR}/projects/old" \
|
|
53
|
+
"${TEST_DIR}/projects/mid" \
|
|
54
|
+
"${TEST_DIR}/projects/new"
|
|
55
|
+
touch -t 202001010000 "${TEST_DIR}/projects/old"
|
|
56
|
+
touch -t 202006010000 "${TEST_DIR}/projects/mid"
|
|
57
|
+
touch -t 202101010000 "${TEST_DIR}/projects/new"
|
|
58
|
+
|
|
59
|
+
run _src "'${TEST_DIR}/projects'" "_collect"
|
|
60
|
+
[ "$status" -eq 0 ]
|
|
61
|
+
[[ "${lines[0]}" == *"/new" ]]
|
|
62
|
+
[[ "${lines[1]}" == *"/mid" ]]
|
|
63
|
+
[[ "${lines[2]}" == *"/old" ]]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@test "_collect: merges multiple base dirs" {
|
|
67
|
+
mkdir -p "${TEST_DIR}/dir1/proj-a" "${TEST_DIR}/dir2/proj-b"
|
|
68
|
+
run _src "'${TEST_DIR}/dir1' '${TEST_DIR}/dir2'" "_collect"
|
|
69
|
+
[ "$status" -eq 0 ]
|
|
70
|
+
[ "${#lines[@]}" -eq 2 ]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "_collect: skips files, only returns directories" {
|
|
74
|
+
mkdir -p "${TEST_DIR}/projects/valid-dir"
|
|
75
|
+
touch "${TEST_DIR}/projects/afile.txt"
|
|
76
|
+
run _src "'${TEST_DIR}/projects'" "_collect"
|
|
77
|
+
[ "$status" -eq 0 ]
|
|
78
|
+
[ "${#lines[@]}" -eq 1 ]
|
|
79
|
+
[[ "${lines[0]}" == *"valid-dir" ]]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@test "_collect: skips non-existent base dirs silently" {
|
|
83
|
+
mkdir -p "${TEST_DIR}/real/proj-a"
|
|
84
|
+
run _src "'${TEST_DIR}/real' '${TEST_DIR}/ghost'" "_collect"
|
|
85
|
+
[ "$status" -eq 0 ]
|
|
86
|
+
[ "${#lines[@]}" -eq 1 ]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── _format ───────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
@test "_format: outputs exactly three tab-delimited fields" {
|
|
92
|
+
run zsh -c "
|
|
93
|
+
export _P_LAUNCH_TEST=1
|
|
94
|
+
export HOME='${MOCK_HOME}'
|
|
95
|
+
source '${SCRIPT}'
|
|
96
|
+
printf '%s\n' '${MOCK_HOME}/projects/myapp' | _format
|
|
97
|
+
"
|
|
98
|
+
[ "$status" -eq 0 ]
|
|
99
|
+
local tabs
|
|
100
|
+
tabs=$(printf '%s' "${lines[0]}" | tr -cd '\t' | wc -c | tr -d ' ')
|
|
101
|
+
[ "$tabs" -eq 2 ]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@test "_format: first field is the project name (basename)" {
|
|
105
|
+
run zsh -c "
|
|
106
|
+
export _P_LAUNCH_TEST=1
|
|
107
|
+
export HOME='${MOCK_HOME}'
|
|
108
|
+
source '${SCRIPT}'
|
|
109
|
+
printf '%s\n' '${TEST_DIR}/some/deep/path/myproject' | _format
|
|
110
|
+
"
|
|
111
|
+
local name
|
|
112
|
+
name=$(printf '%s' "${lines[0]}" | cut -f1)
|
|
113
|
+
[ "$name" = "myproject" ]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@test "_format: second field is the full path unchanged" {
|
|
117
|
+
local proj="${TEST_DIR}/myproject"
|
|
118
|
+
run zsh -c "
|
|
119
|
+
export _P_LAUNCH_TEST=1
|
|
120
|
+
export HOME='${MOCK_HOME}'
|
|
121
|
+
source '${SCRIPT}'
|
|
122
|
+
printf '%s\n' '${proj}' | _format
|
|
123
|
+
"
|
|
124
|
+
local full_path
|
|
125
|
+
full_path=$(printf '%s' "${lines[0]}" | cut -f2)
|
|
126
|
+
[ "$full_path" = "${proj}" ]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@test "_format: third field replaces HOME with ~" {
|
|
130
|
+
run zsh -c "
|
|
131
|
+
export _P_LAUNCH_TEST=1
|
|
132
|
+
export HOME='${MOCK_HOME}'
|
|
133
|
+
source '${SCRIPT}'
|
|
134
|
+
printf '%s\n' '${MOCK_HOME}/projects/myapp' | _format
|
|
135
|
+
"
|
|
136
|
+
local parent
|
|
137
|
+
parent=$(printf '%s' "${lines[0]}" | cut -f3)
|
|
138
|
+
[[ "$parent" == "~"* ]]
|
|
139
|
+
[[ "$parent" != *"${MOCK_HOME}"* ]]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@test "_format: handles multiple projects" {
|
|
143
|
+
run zsh -c "
|
|
144
|
+
export _P_LAUNCH_TEST=1
|
|
145
|
+
export HOME='${MOCK_HOME}'
|
|
146
|
+
source '${SCRIPT}'
|
|
147
|
+
printf '%s\n%s\n' '${TEST_DIR}/proj-a' '${TEST_DIR}/proj-b' | _format
|
|
148
|
+
"
|
|
149
|
+
[ "$status" -eq 0 ]
|
|
150
|
+
[ "${#lines[@]}" -eq 2 ]
|
|
151
|
+
}
|
package/bundles.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"name": "brainstorming",
|
|
4
|
-
"description": "设计与规划工具(brainstorming + writing-plans)",
|
|
5
|
-
"skills": [
|
|
6
|
-
"superpowers-fork/brainstorming",
|
|
7
|
-
"superpowers-fork/writing-plans"
|
|
8
|
-
]
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"name": "dev",
|
|
12
|
-
"description": "开发工作流(executing-plans + systematic-debugging + using-git-worktrees)",
|
|
13
|
-
"skills": [
|
|
14
|
-
"superpowers-fork/executing-plans",
|
|
15
|
-
"superpowers-fork/systematic-debugging",
|
|
16
|
-
"superpowers-fork/using-git-worktrees"
|
|
17
|
-
]
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"name": "all",
|
|
21
|
-
"description": "全部 skill",
|
|
22
|
-
"dynamic": true
|
|
23
|
-
}
|
|
24
|
-
]
|