From ffeb95b9cc5926de1b51f2ebb1b7430953bc10b5 Mon Sep 17 00:00:00 2001 From: vitya Date: Mon, 25 May 2026 13:26:06 +0300 Subject: [PATCH] feat(build): add --prune flag to build.{sh,ps1} [skip-tdd: wrapper] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symmetric to install-side --prune (e871c20). Removes dist/.skill files whose is not in skills/*. Design choices match install: - Combined flag (build + prune in one run) - Global scan, ignores -Names / positional filter - Default off, print-and-delete, no confirmation Bash wrinkle: when build.sh delegates to powershell.exe -File build.ps1 (Windows-without-zip case), --prune is NOT forwarded. Bash runs prune itself at the end of the script against the shared dist/. Keeps the delegation surface narrow and the prune logic single-sourced per shell. [skip-tdd: wrapper] — same carve-out as install-side, smoke-test evidence: fake dist/fake-stale-{sh,ps}.skill files created in real dist/, ran build.{sh,ps1} --prune, verified fakes removed, real caveman.skill etc. left intact. Wiki .wiki/concepts/install-cross-platform.md extended: - title broadened (Install → Install / Build) - new "Build-side --prune" section - scope note: dist-hermes/ is separate (managed by build-hermes.py) Closes [install-ps1-build-prune-followup]. --- .tasks/STATUS.md | 11 ++++---- .wiki/concepts/install-cross-platform.md | 33 ++++++++++++++++++---- .wiki/index.md | 2 +- .wiki/log.md | 2 ++ scripts/build.ps1 | 21 ++++++++++++-- scripts/build.sh | 36 ++++++++++++++++++++---- 6 files changed, 86 insertions(+), 19 deletions(-) diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index f782e3a..3a8ab7b 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -31,12 +31,13 @@ _Updated: 2026-05-25 (board-cleanup: done-блоки → .archive/done-2026-05.m --- -## ⚪ [install-ps1-build-prune-followup] — Add matching `--prune` flag to `scripts/build.{sh,ps1}` for `dist/.skill` artefacts (analogue of install-script prune). -**Status:** ready -**Where I stopped:** Surfaced 2026-05-25 from `[install-ps1]` close — install-side prune ships, but `dist/.skill` artefacts have the same staleness problem (e.g. `dist/using-synology-ops.skill` would linger if it had ever been built). Currently no .skill exists for the retired skill, but the gap is real for future renames/retires. -**Next action:** Add `--prune` to `scripts/build.sh` and `scripts/build.ps1`: scan `dist/*.skill`, drop any whose `` is not in `skills/`. Same default-off / print-and-delete pattern as install-side. Update `.wiki/concepts/install-cross-platform.md` to extend the parity contract to build scripts. -**Branch:** (not started) +## 🟢 [install-ps1-build-prune-followup] — Add matching `--prune` flag to `scripts/build.{sh,ps1}` for `dist/.skill` artefacts (analogue of install-script prune). +**Status:** done +**Where I stopped:** Shipped 2026-05-25. `--prune` / `-Prune` added to both `build.sh` and `build.ps1`. Bash arg-parses out `--prune` from positionals, runs prune at the end of the script against `dist/*.skill` (works regardless of which build branch executed — zip-path or powershell.exe delegation). PS `-Prune` switch runs after the build loop. Same default-off / global-scan / print-and-delete pattern as install-side. Smoke-tested with fake `dist/fake-stale-{sh,ps}.skill` files against real `dist/` — both paths pruned correctly without touching real archives. `[skip-tdd: wrapper]` carve-out applies. Wiki page `.wiki/concepts/install-cross-platform.md` extended with new "Build-side --prune" section + scope note that `dist-hermes/` is separate. Index + log updated. +**Next action:** (none — kept until merged) +**Branch:** master + --- diff --git a/.wiki/concepts/install-cross-platform.md b/.wiki/concepts/install-cross-platform.md index b8e76c3..f48ec77 100644 --- a/.wiki/concepts/install-cross-platform.md +++ b/.wiki/concepts/install-cross-platform.md @@ -1,12 +1,12 @@ --- -title: Install Cross-Platform Parity (PS + Bash) +title: Install / Build Cross-Platform Parity (PS + Bash) type: concept updated: 2026-05-25 --- -# Install Cross-Platform Parity (PS + Bash) +# Install / Build Cross-Platform Parity (PS + Bash) -Sibling concept to `install-portability.md` (POSIX-shell compat). This one is about the **paired-script parity contract** between `scripts/install.ps1` (PowerShell) and `scripts/install.sh` (bash). +Sibling concept to `install-portability.md` (POSIX-shell compat). This one is about the **paired-script parity contract** between `scripts/install.ps1` / `scripts/install.sh` (install) and `scripts/build.ps1` / `scripts/build.sh` (build). Both pairs share the same conventions and the same `--prune` / `-Prune` flag pattern. ## Why two scripts @@ -43,14 +43,35 @@ Added 2026-05-25 (commit `6cf0e98`). Motivating case: after retiring `using-syno - **Default off.** Must be passed explicitly. Idempotent re-installs (the common case) don't suddenly delete anything. -## What `--prune` does NOT do +## What install-side `--prune` does NOT do -- It does not remove `dist/.skill` artefacts. That's a build concern, not install. The matching `--prune` flag on `build.sh` / `build.ps1` is a separate task (`[install-ps1]` acceptance was scoped to install scripts; `dist/` is a build artefact directory). - It does not touch plugin-installed skills under `~/.claude/plugins//skills//`. Those are managed by the plugin system, not this repo. - It does not warn if the about-to-be-deleted dir contains user-edited content. The contract is that `~/.claude/skills//` is a managed copy of `skills//` — anything else is user-error. +- It does not remove `dist/.skill` build artefacts. That's the build-script's `--prune` (see next section). + +## Build-side `--prune` / `-Prune` + +Added 2026-05-25 — natural extension of the install-side flag to the build pair (`scripts/build.sh` and `scripts/build.ps1`). Same design choices, applied to files instead of directories: after the build loop, walk `dist/*.skill` and remove any whose `` (basename minus `.skill`) is not in `skills/*`. + +Identical to install-side: combined flag, global scan ignores the name filter, prints `pruning: -> ` per removal, default off, no confirmation prompt. + +The bash path has one extra wrinkle: when `build.sh` is run on Windows without `zip` and delegates to `build.ps1` via `powershell.exe -File`, the `--prune` flag is **not** forwarded to the delegated PS process. Bash runs the prune step itself at the end of the script, against the same `dist/` directory. This keeps the delegation surface narrow (no flag-translation bugs) and the prune logic single-sourced per shell. + +`build.ps1` invoked directly (without the bash wrapper) handles `-Prune` natively. + +## What build-side `-Prune` does NOT do + +- It does not unblock build for skills that have been renamed mid-flight. A user who renamed `skills/foo/` → `skills/bar/` should still run a fresh build (`build.sh bar`) — prune only catches stale archives whose source dir is gone, not stale archives whose source was renamed (those become orphans of a different source, indistinguishable from intentional foreign artefacts). +- It does not touch `dist-hermes/` — that directory is managed by `scripts/build-hermes.py` and follows its own rules (whole-directory rebuild per skill). Hermes has no current prune mechanism; if needed, that's a separate concept page. ## Test evidence -Both scripts smoke-tested 2026-05-25 on disposable target dirs (env-overridden `CLAUDE_SKILLS_DIR`). Pre-populated with 2 fake stale dirs, ran full install + prune, verified: stale dirs removed, all real skills installed, retired `using-synology-ops` absent. No automated test fixture in the repo — install scripts are wrapper-style, smoke-test evidence in the `6cf0e98` commit body suffices under the `[skip-tdd: wrapper]` carve-out. +All four scripts smoke-tested 2026-05-25. + +**Install side** — disposable target dirs (env-overridden `CLAUDE_SKILLS_DIR`). Pre-populated with 2 fake stale dirs, ran full install + prune, verified: stale dirs removed, all real skills installed, retired `using-synology-ops` absent. + +**Build side** — fake `dist/fake-stale.skill` + `dist/another-stale.skill` files created directly in the real `dist/`. Ran `build.sh --prune fake-stale-sh` (positional arg triggers a no-op build via skip path; prune runs at the end) and `build.ps1 -Names fake-stale-ps -Prune`. Both removed the fakes, left real archives like `caveman.skill` untouched. + +No automated test fixture in the repo — install / build scripts are wrapper-style, smoke-test evidence in the respective commit bodies suffices under the `[skip-tdd: wrapper]` carve-out. `[archive-roundtrip-test]` (still ⚪ on the board) is a candidate place to add a real fixture once it lands. diff --git a/.wiki/index.md b/.wiki/index.md index 8e4fabd..ec10a56 100644 --- a/.wiki/index.md +++ b/.wiki/index.md @@ -22,7 +22,7 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever - [bootstrap-skill-deps-check.md](concepts/bootstrap-skill-deps-check.md) — project-bootstrap@1.7.0 — Step 5.6 collapses the per-skill "detect-and-recommend" mirror shape into one generic `trigger → fulfiller` table walker (skill vs plugin kind, never auto-install); subsumes the deferred `[bootstrap-recommend-projects-meta]` and the existing `superpowers`-only detector - [bootstrap-manifest.md](concepts/bootstrap-manifest.md) — record of which `project-bootstrap` / `setup-wiki` / `setup-tasks` versions initialized this project's `.wiki/` and `.tasks/` layout (overwritten on re-bootstrap; history in git) - [build-notes.md](concepts/build-notes.md) — why `build.ps1` exists alongside `build.sh`; PS 5.1 backslash-in-zip gotcha; how to extract a `.skill` -- [install-cross-platform.md](concepts/install-cross-platform.md) — parity contract between `install.ps1` and `install.sh` (paired-script invariant); rationale for `--prune` / `-Prune` flag (combined-with-install, global-scan, default-off) +- [install-cross-platform.md](concepts/install-cross-platform.md) — paired-script parity contract for `install.{ps1,sh}` AND `build.{ps1,sh}`; rationale for the `--prune` / `-Prune` flag (combined-with-action, global-scan, default-off); install-side prunes target dirs, build-side prunes `dist/*.skill` files - [install-portability.md](concepts/install-portability.md) — `install.sh` / `build.sh` rewritten to drop `mapfile` (bash 4+) and `find -printf` (GNU only) so stock macOS (bash 3.2 + BSD find) works - [context7-setup.md](concepts/context7-setup.md) — switched context7 from manual MCP entries to the official plugin; API key in `.mcp.json` as `--api-key`; now also captured as `setup-context7` skill (one-time install/migrate flow with key discovery) - [projects-meta-skills.md](concepts/projects-meta-skills.md) — `setup-projects-meta` + `using-projects-meta` skill pair for the local `projects-meta-mcp` stdio server (cross-project tasks + shared Gitea wiki); local-first rule + two-step mutation pattern diff --git a/.wiki/log.md b/.wiki/log.md index 6e76d67..2a8b39f 100644 --- a/.wiki/log.md +++ b/.wiki/log.md @@ -63,3 +63,5 @@ Parseable: `grep "^## \[" .wiki/log.md | tail -20`. ## [2026-05-25] decision | session-handoff-skill-design — design rationale for the `session-handoff` skill captured in wiki after cluster 7/7 closure; sliding overwrite of `.tasks/NEXT_SESSION.md`, phrase whitelist + substantive-commit heuristic, opt-in PostToolUse hook, orient+ask default, source: `~/projects/.workshop/.archive/2026-05-24-session-handoff-skill.md` Round 1 + Round 2 ## [2026-05-25] decision | install-cross-platform — `install.{ps1,sh}` paired-script parity contract documented; `--prune` / `-Prune` flag rationale (combined-with-install, global-scan ignores names filter, default-off, print-and-delete no prompt); shipped in commit `6cf0e98` with `[skip-tdd: wrapper]` carve-out + smoke-test evidence; closes 2/3 of `[install-ps1]` acceptance (the doc + flag), `dist/`-prune analogue deferred to `build` scripts + +## [2026-05-25] decision | install-cross-platform extended to build scripts — `build.{ps1,sh}` get the symmetric `--prune` / `-Prune` flag (removes `dist/.skill` where `` is not in `skills/`). Bash delegation to `powershell.exe -File build.ps1` does NOT forward the flag — bash runs prune itself against the shared `dist/`. Both paths smoke-tested with fake stale .skill files against real dist/. Closes `[install-ps1-build-prune-followup]`. diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 395e94e..3be27bc 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -1,5 +1,9 @@ # Build .skill archives from skills// into dist/.skill -# Usage: build.ps1 [-Names ,] (no args = all) +# Usage: build.ps1 [-Names ,] [-Prune] +# no args = build all skills/* into dist/.skill +# -Prune = after build, remove dist/.skill files whose +# is NOT in skills/* (analogue of install.ps1 -Prune). +# Prune always scans the full dist/, ignores -Names filter. # # Uses .NET System.IO.Compression.ZipArchive directly to produce # spec-compliant ZIPs with forward-slash entry names (Windows PowerShell 5.1's @@ -7,7 +11,8 @@ [CmdletBinding()] param( - [string[]]$Names = @() + [string[]]$Names = @(), + [switch]$Prune ) $ErrorActionPreference = 'Stop' @@ -69,3 +74,15 @@ foreach ($name in $Names) { New-SkillArchive -SkillName $name -SourceDir $srcDir -OutPath $out Write-Host "built: dist/$name.skill" } + +if ($Prune) { + $sourceNames = @(Get-ChildItem -Path $src -Directory | Select-Object -ExpandProperty Name) + $skillArchives = Get-ChildItem -Path $dist -Filter "*.skill" -File -ErrorAction SilentlyContinue + foreach ($archive in $skillArchives) { + $archiveName = [System.IO.Path]::GetFileNameWithoutExtension($archive.Name) + if ($sourceNames -notcontains $archiveName) { + Write-Host "pruning: $archiveName (not in skills/) -> $($archive.FullName)" + Remove-Item -Force $archive.FullName + } + } +} diff --git a/scripts/build.sh b/scripts/build.sh index 4c46e18..a9f0b9b 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash # Build .skill archives from skills// into dist/.skill -# Usage: build.sh [name...] (no args = all) +# Usage: build.sh [--prune] [name...] +# no args = build all skills/* into dist/.skill +# --prune = after build, remove dist/.skill files whose +# is NOT in skills/* (analogue of install.sh --prune). +# Prune always scans the full dist/, ignores name filter. # # Uses `zip` if available; otherwise delegates to scripts/build.ps1 # (so the script works on Linux, macOS, and Windows-with-git-bash without @@ -12,9 +16,18 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SRC="$ROOT/skills" DIST="$ROOT/dist" +prune=0 +positional=() +for arg in "$@"; do + case "$arg" in + --prune) prune=1 ;; + *) positional+=("$arg") ;; + esac +done + if command -v zip >/dev/null 2>&1; then mkdir -p "$DIST" - if [ "$#" -eq 0 ]; then + if [ "${#positional[@]}" -eq 0 ]; then # Portable across bash 3.2 (stock macOS) and bash 4+ (Linux, git-bash): # avoid `mapfile` (bash 4+) and `find -printf` (GNU find only). names=() @@ -25,7 +38,7 @@ if command -v zip >/dev/null 2>&1; then IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort)) unset IFS else - names=("$@") + names=("${positional[@]}") fi for name in "${names[@]}"; do src_dir="$SRC/$name" @@ -46,12 +59,14 @@ elif command -v powershell.exe >/dev/null 2>&1; then # Windows fallback: delegate to build.ps1 (proper ZIP via .NET API). # PS array binding via -File is fragile (commas don't always split into [string[]]), # so call build.ps1 once per skill and let the bash loop do the work. + # --prune is NOT forwarded — bash handles prune at the end of this script + # against the same dist/, regardless of which build path ran. ps1="$SCRIPT_DIR/build.ps1" ps1_win="$(cygpath -w "$ps1" 2>/dev/null || echo "$ps1")" - if [ "$#" -eq 0 ]; then + if [ "${#positional[@]}" -eq 0 ]; then powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$ps1_win" else - for name in "$@"; do + for name in "${positional[@]}"; do powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$ps1_win" -Names "$name" done fi @@ -59,3 +74,14 @@ else echo "error: need either 'zip' (Linux/macOS) or PowerShell (Windows) to build .skill archives" >&2 exit 1 fi + +if [ "$prune" -eq 1 ]; then + for f in "$DIST"/*.skill; do + [ -f "$f" ] || continue + name="$(basename "$f" .skill)" + if [ ! -d "$SRC/$name" ]; then + echo "pruning: $name (not in skills/) → $f" + rm -f "$f" + fi + done +fi