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.
Files changed (32) hide show
  1. package/bin/cli.js +116 -44
  2. package/lib/bundles.js +97 -22
  3. package/lib/installer.js +126 -5
  4. package/lib/targets.js +1 -0
  5. package/lib/vars.js +34 -0
  6. package/package.json +7 -3
  7. package/skills/harness/diataxis-docs/SKILL.md +128 -0
  8. package/skills/harness/diataxis-docs/references/custom-categories.md +32 -0
  9. package/skills/harness/diataxis-docs/references/index-template.md +29 -0
  10. package/skills/web-fetch/article-fetcher/SKILL.md +625 -0
  11. package/skills/web-fetch/article-fetcher/references/article_utils.py +225 -0
  12. package/skills/web-fetch/article-fetcher/references/file-format.md +73 -0
  13. package/skills/web-fetch/article-fetcher/scripts/import_reading.py +75 -0
  14. package/skills/web-fetch/article-fetcher/scripts/init_db.py +33 -0
  15. package/skills/web-fetch/article-fetcher/scripts/url-index.db +0 -0
  16. package/skills/web-fetch/article-fetcher/scripts/url-index.db.bak +0 -0
  17. package/skills/web-fetch/article-fetcher/vars.json +12 -0
  18. package/skills/writing/mermaid-diagram/SKILL.md +220 -0
  19. package/skills/writing/mermaid-diagram/references/color-templates.md +75 -0
  20. package/skills/writing/mermaid-diagram/references/flowchart.md +88 -0
  21. package/skills/writing/mermaid-diagram/references/gantt-timeline.md +62 -0
  22. package/skills/writing/mermaid-diagram/references/other-charts.md +55 -0
  23. package/skills/writing/mermaid-diagram/references/sequence.md +143 -0
  24. package/skills/writing/mermaid-diagram/references/statediagram.md +54 -0
  25. package/skills/writing/mermaid-diagram/scripts/check_mermaid.py +84 -0
  26. package/skills-index.json +32 -0
  27. package/tools/p-launch/p-launch.sh +275 -0
  28. package/tools/p-launch/tests/e2e.bats +164 -0
  29. package/tools/p-launch/tests/unit.bats +151 -0
  30. package/tools/p-launch/vars.json +9 -0
  31. package/tools/p-launch/zshrc.snippet +5 -0
  32. 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
+ }
@@ -0,0 +1,9 @@
1
+ [
2
+ {
3
+ "name": "PROJECT_DIR",
4
+ "description": "Project root directory (add more paths later in ~/.config/p-launch/config.zsh)",
5
+ "default": "{{HOME}}/Projects",
6
+ "configFile": "~/.config/p-launch/config.zsh",
7
+ "configKey": "PROJECT_DIRS"
8
+ }
9
+ ]
@@ -0,0 +1,5 @@
1
+
2
+ # >>> p-launch (added by harveyz-skill) >>>
3
+ export PATH="$HOME/.local/bin:$PATH"
4
+ alias p=p-launch
5
+ # <<< p-launch <<<
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
- ]