Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- `skills` module on Bash and native Windows: updates agent skills in both project and global scopes via `skills update --project --global` (matching the interactive "Both" option without prompting), falling back to `npx --yes skills update` when the `skills` CLI is not installed; `--non-interactive` appends `--yes` to skip upstream-deletion prompts.

## [2.1.3] - 2026-08-10

### Fixed
Expand Down
17 changes: 17 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# Plan: skills module updates

## Goal

- Add a `skills` module that updates agent skills in both project and global scopes by default, matching `npx skills update`'s interactive "Both" option without prompting.

## Execution checklist

- [ ] Register a default-on `skills` module between `pi` and `mise` on Bash and native Windows.
- [ ] Resolve the backing command as the `skills` CLI first, then `npx --yes skills`; skip gracefully when neither exists and fail under `--only`.
- [ ] Run `skills update --project --global`, appending `--yes` under `--non-interactive`; dry-run prints the resolved command without executing.
- [ ] Cover direct, npx-fallback, non-interactive, dry-run, and missing-dependency behavior in Bash tests; mirror success, fallback, and missing-dependency coverage in native Windows tests.
- [ ] Update README, SPEC (module matrix, execution order, §8.15), CHANGELOG, and this plan.
- [ ] Pass lint and tests before opening the PR.

---

# Plan: v2.1.3 fnm and complete pi updates

## Goal
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,12 @@ Modules are auto-detected: if the underlying command isn’t installed, the modu
- `rustup`: update Rust toolchains via `rustup update`
- `claude`: update Claude Code CLI via `claude update`
- `pi`: update the pi AI CLI and installed extensions via `pi update --all`
- `skills`: update agent skills in both project and global scopes via `skills update` (falls back to `npx --yes skills update`); passing both scopes matches the interactive "Both" option without prompting
- `mise`: update mise and upgrade installed tools (`mise self-update`, `mise upgrade`)
- `go`: update Go binaries from `GO_BINARIES` in `~/.updatesrc` (entries default to `@latest`)
- `macos`: list available macOS software updates via `softwareupdate -l` (disabled by default; enable with `--macos-updates` or `--full`)

Native Windows v2.1 default-on modules: `winget`, `node`, `bun`, `python`, `uv`, `pipx`, `rustup`, `claude`, `pi`, `go`.
Native Windows v2.1 default-on modules: `winget`, `node`, `bun`, `python`, `uv`, `pipx`, `rustup`, `claude`, `pi`, `skills`, `go`.
On native Windows, `--full` selects every supported Windows module even if `SKIP_MODULES` in config would otherwise omit one; explicit `--skip` still wins.

Platform support summary:
Expand All @@ -115,7 +116,7 @@ Platform support summary:
| brew, shell, repos | Yes | Yes | No |
| linux | No | Yes | No |
| winget | No | No | Yes |
| node, bun, python, uv, pipx, rustup, claude, pi, go | Yes | Yes | Yes |
| node, bun, python, uv, pipx, rustup, claude, pi, skills, go | Yes | Yes | Yes |
| mas, macos | Yes | No | No |
| mise | Yes | Yes | Deferred |

Expand Down Expand Up @@ -164,6 +165,7 @@ Install what you actually use:
- `rustup`: from https://rustup.rs
- `claude` (Claude Code CLI) for the `claude` module
- `pi` (npm-installed AI coding CLI) for the `pi` module
- `skills` CLI (or `npx`) for the `skills` module (`npx skills add ...`)
- `go` (for the `go` module)
- On Linux: a supported system package manager (`apt-get`, `dnf`, `yum`, `pacman`, `zypper`, or `apk`) and `sudo` (if not running as root)

Expand Down
30 changes: 23 additions & 7 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ v2.1 preserves the stable v2 contract. Patch release v2.1.1 filters Node upgrade
### 1.3 Success metrics

- **Primary KPIs:**
- All 17 modules pass lint + stub/contract tests across macOS, Linux, and Windows.
- All 18 modules pass lint + stub/contract tests across macOS, Linux, and Windows.
- JSONL output is parseable by `jq` for all event types.
- Config file (`~/.updatesrc`) correctly sets defaults overridden by CLI flags across `HOME`/`USERPROFILE` layouts.
- **Guardrails:**
Expand Down Expand Up @@ -344,7 +344,7 @@ If `UPDATES_SELF_UPDATE_REPO` is set, the CLI **MUST** print an error and exit `

### 7.2 Module list & platform matrix

Execution order: `brew`, `shell`, `repos`, `linux`, `winget`, `node`, `bun`, `python`, `uv`, `mas`, `pipx`, `rustup`, `claude`, `pi`, `mise`, `go`, `macos`.
Execution order: `brew`, `shell`, `repos`, `linux`, `winget`, `node`, `bun`, `python`, `uv`, `mas`, `pipx`, `rustup`, `claude`, `pi`, `skills`, `mise`, `go`, `macos`.

| Module | macOS | Linux | WSL | Windows | Notes |
| -------- | :---: | :---: | :-: | :-----: | ----- |
Expand All @@ -362,15 +362,16 @@ Execution order: `brew`, `shell`, `repos`, `linux`, `winget`, `node`, `bun`, `py
| `rustup` | Yes | Yes | Yes | Yes | Requires `rustup` |
| `claude` | Yes | Yes | Yes | Yes | Requires `claude`; runs `claude update` |
| `pi` | Yes | Yes | Yes | Yes | Requires `pi`; runs `pi update --all` |
| `skills` | Yes | Yes | Yes | Yes | Requires `skills` or `npx`; runs `skills update` for project + global scopes |
| `mise` | Yes | Yes | Yes | No | Requires `mise` |
| `go` | Yes | Yes | Yes | Yes | Requires `go`; binary list from config |
| `macos` | Yes | No | No | No | Requires `softwareupdate` (opt-in) |

Native Windows default runs auto-select `winget`, `node`, `bun`, `python`, `uv`, `pipx`, `rustup`, `claude`, `pi`, and `go` when their backing commands or config are present. `mise` remains deferred on native Windows until installer ownership can be handled safely.
Native Windows default runs auto-select `winget`, `node`, `bun`, `python`, `uv`, `pipx`, `rustup`, `claude`, `pi`, `skills`, and `go` when their backing commands or config are present. `mise` remains deferred on native Windows until installer ownership can be handled safely.

### 7.3 Module execution order

Fixed: `brew` > `shell` > `repos` > `linux` > `winget` > `node` > `bun` > `python` > `uv` > `mas` > `pipx` > `rustup` > `claude` > `pi` > `mise` > `go` > `macos`.
Fixed: `brew` > `shell` > `repos` > `linux` > `winget` > `node` > `bun` > `python` > `uv` > `mas` > `pipx` > `rustup` > `claude` > `pi` > `skills` > `mise` > `go` > `macos`.

Rationale: platform package managers first (`brew`, `linux`, `winget`), then git-backed local repos, then language/runtime tools, then opt-in system modules last.

Expand Down Expand Up @@ -561,7 +562,22 @@ Purpose: update the `pi` AI coding CLI and its installed extensions (pinned sour
- Non-dry-run: `pi update --all`
- Side effects: updates `pi` and installed extensions to their latest versions.

### 8.15 `mise`
### 8.15 `skills`

Purpose: update agent skills installed on the machine in both project and global scopes.

- Requires: the `skills` CLI (`npx skills`), or `npx` as a fallback adapter.
- Resolution order: a direct `skills` command on `PATH` first, then `npx --yes skills`.
- If neither is available, default runs warn and skip; explicit `--only skills` fails.
- Scope: runs `skills update --project --global`, which the skills CLI resolves to its interactive "Both" scope without prompting.
- Non-dry-run:
- Bash: `<skills|npx --yes skills> update --project --global [--yes]`
- Native Windows: `skills update --project --global` or `npx --yes skills update --project --global`
- With `-n` / `--non-interactive`: appends `--yes` so upstream-deletion prompts inside the skills CLI are skipped gracefully.
- Dry run: prints the resolved command; executes nothing.
- Side effects: refreshes project-scoped and globally tracked skills from their recorded sources (local/node_modules-sourced skills are skipped by the skills CLI itself).

### 8.16 `mise`

Purpose: update mise itself and upgrade all installed tool versions.

Expand All @@ -572,7 +588,7 @@ Purpose: update mise itself and upgrade all installed tool versions.
- Side effects: updates mise binary and installed tool versions to latest matching constraints.
- Native Windows remains deferred: the official Windows installation paths are Scoop and winget, while `mise self-update` is unavailable for package-managed installs. `updates` has no reliable ownership signal that would distinguish those installs from a manually downloaded binary without adding manager-specific coupling.

### 8.16 `go`
### 8.17 `go`

Purpose: update Go binaries from a user-specified list.

Expand All @@ -585,7 +601,7 @@ Purpose: update Go binaries from a user-specified list.
- `--only go`: error (return `1`)
- Side effects: rebuilds and installs Go binaries to `$GOBIN` or `$GOPATH/bin`.

### 8.17 `macos`
### 8.18 `macos`

Purpose: list available macOS software updates.

Expand Down
99 changes: 96 additions & 3 deletions tests/test_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ if [ -z "$SYSTEM_GIT" ]; then
echo "tests: git is required for Git fixture coverage" >&2
exit 1
fi
# Referenced by heredoc-backed test bodies evaluated through run_test.
# shellcheck disable=SC2034
SYSTEM_BASH="$(command -v bash)"
SYSTEM_PYTHON3="$(command -v python3 2>/dev/null || true)"
if [ -z "$SYSTEM_PYTHON3" ]; then
echo "python3 is required for tests/test_cli.sh" >&2
Expand Down Expand Up @@ -371,6 +374,8 @@ write_stub claude 'echo "claude $*" >>"$CALL_LOG"'
# shellcheck disable=SC2016
write_stub pi 'echo "pi $*" >>"$CALL_LOG"'
# shellcheck disable=SC2016
write_stub skills 'echo "skills $*" >>"$CALL_LOG"'
# shellcheck disable=SC2016
write_stub softwareupdate 'echo "softwareupdate $*" >>"$CALL_LOG"'

test_selected() {
Expand Down Expand Up @@ -563,7 +568,7 @@ echo "$out" | grep -q '^brew'
echo "$out" | grep -q '^shell'
echo "$out" | grep -q '^linux'
actual_modules="$(printf '%s\n' "$out" | awk '{print $1}' | paste -sd' ' -)"
expected_modules='brew shell repos linux winget node bun python uv mas pipx rustup claude pi mise go macos'
expected_modules='brew shell repos linux winget node bun python uv mas pipx rustup claude pi skills mise go macos'
if [ "$actual_modules" != "$expected_modules" ]; then
echo "Expected module order: $expected_modules" >&2
echo "Actual module order: $actual_modules" >&2
Expand Down Expand Up @@ -1187,7 +1192,7 @@ setup_python_guard_fixture
: >"$CALL_LOG"
python_no_report_skip_stderr="${tmp_dir}/python-no-report-skip-stderr.log"
python_no_report_skip_out="$(
PYTHON_GUARD_NO_REPORT_HELP=1 PYTHONUSERBASE="$python_user_base" PYTHONPATH="$python_path" "$SCRIPT" --skip brew,shell,linux,node,uv,mas,pipx,rustup,claude,mise,go,macos,repos,bun,pi --no-emoji --no-color 2>"$python_no_report_skip_stderr"
PYTHON_GUARD_NO_REPORT_HELP=1 PYTHONUSERBASE="$python_user_base" PYTHONPATH="$python_path" "$SCRIPT" --skip brew,shell,linux,node,uv,mas,pipx,rustup,claude,mise,go,macos,repos,bun,pi,skills --no-emoji --no-color 2>"$python_no_report_skip_stderr"
)"
echo "$python_no_report_skip_out" | grep -q '^==> python END (SKIP)'
grep -q '^WARN: python: skipping guarded user-site upgrades: pip does not support --dry-run --report$' "$python_no_report_skip_stderr"
Expand Down Expand Up @@ -1739,6 +1744,94 @@ grep -q '^pi update --all$' "$CALL_LOG"

UPDATES_TEST_CASE

run_test "skills module updates project and global scopes" <<'UPDATES_TEST_CASE'
: >"$CALL_LOG"
out="$("$SCRIPT" --only skills --no-emoji --no-color)"
echo "$out" | grep -q '^==> skills START$'
echo "$out" | grep -q '^==> skills END (OK)'
grep -q '^skills update --project --global$' "$CALL_LOG"

UPDATES_TEST_CASE

run_test "skills module dry-run shows the both-scope command without executing it" <<'UPDATES_TEST_CASE'
: >"$CALL_LOG"
out="$("$SCRIPT" --dry-run --only skills --no-emoji --no-color)"
echo "$out" | grep -q '^DRY RUN: skills update --project --global$'
if [ -s "$CALL_LOG" ]; then
echo "Expected skills dry-run not to execute any command" >&2
exit 1
fi

UPDATES_TEST_CASE

run_test "skills module adds --yes in non-interactive mode" <<'UPDATES_TEST_CASE'
: >"$CALL_LOG"
"$SCRIPT" -n --only skills --no-emoji >/dev/null
grep -q '^skills update --project --global --yes$' "$CALL_LOG"

UPDATES_TEST_CASE

run_test "skills module falls back to npx when the CLI is not installed" <<'UPDATES_TEST_CASE'
rm -f "${stub_bin}/skills"
# shellcheck disable=SC2016
write_stub npx 'echo "npx $*" >>"$CALL_LOG"'
: >"$CALL_LOG"
out="$("$SCRIPT" --only skills --no-emoji --no-color)"
echo "$out" | grep -q '^==> skills END (OK)'
grep -q '^npx --yes skills update --project --global$' "$CALL_LOG"
: >"$CALL_LOG"
out="$("$SCRIPT" --dry-run --only skills --no-emoji --no-color)"
echo "$out" | grep -q '^DRY RUN: npx --yes skills update --project --global$'
if grep -q '^npx ' "$CALL_LOG"; then
echo "Expected skills dry-run not to execute the npx fallback" >&2
exit 1
fi
# shellcheck disable=SC2016
write_stub skills 'echo "skills $*" >>"$CALL_LOG"'
rm -f "${stub_bin}/npx"

UPDATES_TEST_CASE

run_test "skills module skips without skills or npx and errors under --only" <<'UPDATES_TEST_CASE'
rm -f "${stub_bin}/skills" "${stub_bin}/npx"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
# Host tool directories can expose a real npx (system Node installs); isolate
# PATH to fixture-controlled executables so neither command can leak in. Stubs
# embed the interpreter path because `env bash` cannot resolve with this PATH.
skills_iso_bin="${tmp_dir}/skills-isolated-bin"
mkdir -p "$skills_iso_bin"
{
printf '#!%s\n' "$SYSTEM_BASH"
printf 'echo Darwin\n'
} >"${skills_iso_bin}/uname"
{
printf '#!%s\n' "$SYSTEM_BASH"
printf 'echo updates\n'
} >"${skills_iso_bin}/basename"
chmod +x "${skills_iso_bin}/uname" "${skills_iso_bin}/basename"
saved_path="$PATH"
export PATH="$skills_iso_bin"
skip_stderr="${tmp_dir}/skills-skip-stderr.log"
: >"$skip_stderr"
all_but_skills=(--skip "brew,shell,repos,linux,winget,node,bun,python,uv,mas,pipx,rustup,claude,pi,mise,go,macos" --no-emoji --no-color)
out="$("$SYSTEM_BASH" "$SCRIPT" "${all_but_skills[@]}" 2>"$skip_stderr")"
set +e
out_only="$("$SYSTEM_BASH" "$SCRIPT" --only skills --no-emoji --no-color 2>"$skip_stderr")"
rc=$?
set -e
export PATH="$saved_path"
echo "$out" | grep -q '^==> skills END (SKIP)'
echo "$out" | grep -q '^Skipping skills: neither skills nor npx found\.$'
if [ "$rc" -ne 1 ]; then
echo "Expected --only skills to fail without skills or npx (got $rc)" >&2
exit 1
fi
echo "$out_only" | grep -q '^==> skills END (FAIL)'
grep -q 'skills: required command not found' "$skip_stderr"
# shellcheck disable=SC2016
write_stub skills 'echo "skills $*" >>"$CALL_LOG"'

UPDATES_TEST_CASE

run_test "empty ncu output means node module reports up-to-date" <<'UPDATES_TEST_CASE'
rm -f "${stub_bin}/python" "${stub_bin}/python3"
write_ncu_stub '{}'
Expand Down Expand Up @@ -1892,7 +1985,7 @@ fi
exit 1
'
rm -f "${stub_bin}/npx"
node_skip_args=(--skip "brew,shell,repos,linux,winget,bun,python,uv,mas,pipx,rustup,claude,pi,mise,go,macos" --no-emoji --no-color)
node_skip_args=(--skip "brew,shell,repos,linux,winget,bun,python,uv,mas,pipx,rustup,claude,pi,skills,mise,go,macos" --no-emoji --no-color)
node_capability_stderr="${tmp_dir}/node-capability-stderr.log"
out="$("$SCRIPT" "${node_skip_args[@]}" 2>"$node_capability_stderr")"
echo "$out" | grep -q '^==> node END (SKIP)'
Expand Down
31 changes: 31 additions & 0 deletions tests/test_windows_native.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,37 @@ if (Should-RunTest 'native Claude and Pi modules cover missing dry-run failure s
}
}

if (Should-RunTest 'native payload runs Skills update module') {
Invoke-TestCase 'native payload runs Skills update module' {
Invoke-WithTempInstall {
param($installRoot)
Install-RepoWindowsRuntime -RepoRoot $repoRoot -InstallRoot $installRoot -Version $currentReleaseVersion
$stubDir = Join-Path $installRoot 'stubs'
$log = Join-Path $installRoot 'skills-module.log'
$null = New-Item -ItemType Directory -Path $stubDir -Force
Write-CmdStub -Path (Join-Path $stubDir 'skills.cmd') -Lines @(('echo skills:%*>>"{0}"' -f $log))
$result = Invoke-Bootstrap -InstallRoot $installRoot -ArgumentList @('--no-self-update', '--only', 'skills', '--no-emoji', '--no-color') -Environment @{ PATH = $stubDir }
Assert-Equal -Expected 0 -Actual $result.ExitCode -Message "Skills module should succeed`n$($result.Output)"
$calls = Get-Content -LiteralPath $log -Raw
Assert-Match -Text $calls -Pattern '(?m)^skills:update --project --global\s*$' -Message 'Skills module should update project and global scopes without prompting'

$emptyPath = Join-Path $installRoot 'empty-skills-path'
$null = New-Item -ItemType Directory -Path $emptyPath -Force
$missing = Invoke-Bootstrap -InstallRoot $installRoot -ArgumentList @('--no-self-update', '--only', 'skills', '--no-emoji') -Environment @{ PATH = $emptyPath }
Assert-Equal -Expected 1 -Actual $missing.ExitCode -Message 'explicit missing Skills dependency should fail the run'
Assert-Match -Text $missing.Output -Pattern '(?i)skills not found' -Message 'missing Skills dependency should be explicit'

$npxLog = Join-Path $installRoot 'skills-npx.log'
Write-CmdStub -Path (Join-Path $stubDir 'npx.cmd') -Lines @(('echo npx:%*>>"{0}"' -f $npxLog))
Remove-Item -LiteralPath (Join-Path $stubDir 'skills.cmd') -Force
$fallback = Invoke-Bootstrap -InstallRoot $installRoot -ArgumentList @('--no-self-update', '--only', 'skills', '--non-interactive', '--no-emoji', '--no-color') -Environment @{ PATH = $stubDir }
Assert-Equal -Expected 0 -Actual $fallback.ExitCode -Message "Skills npx fallback should succeed`n$($fallback.Output)"
$npxCalls = Get-Content -LiteralPath $npxLog -Raw
Assert-Match -Text $npxCalls -Pattern '(?m)^npx:--yes skills update --project --global --yes\s*$' -Message 'npx fallback should update both scopes non-interactively'
}
}
}

if (Should-RunTest 'native payload rejects duplicate or noncanonical version assignments') {
Invoke-TestCase 'native payload rejects duplicate or noncanonical version assignments' {
Invoke-WithTempInstall {
Expand Down
44 changes: 43 additions & 1 deletion updates
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pipx|module_pipx|macos,linux|0|Upgrade pipx-managed apps via pipx
rustup|module_rustup|macos,linux|0|Update Rust toolchains via rustup
claude|module_claude|macos,linux|0|Update Claude Code CLI
pi|module_pi|macos,linux|0|Update pi AI CLI and extensions via pi update --all
skills|module_skills|macos,linux|0|Update agent skills in project + global scopes
mise|module_mise|macos,linux|0|Update mise and upgrade installed tools
go|module_go|macos,linux|0|Update Go binaries from GO_BINARIES config
macos|module_macos|macos|1|List available macOS software updates (opt-in)'
Expand Down Expand Up @@ -2530,7 +2531,7 @@ json_pairs() {

###############################################################################
# SECTION: modules — brew, shell, linux, node, python, uv, mas, pipx,
# rustup, claude, mise, go, macos
# rustup, claude, pi, skills, mise, go, macos
###############################################################################

python_externally_managed() {
Expand Down Expand Up @@ -3671,6 +3672,47 @@ module_pi() {
return 0
}

skills_update_argv() {
# Sets the global array SKILLS_UPDATE_ARGV to the invocation that updates
# both scopes without prompts. `skills update` with both --project and
# --global resolves to the interactive "Both" scope without a prompt.
SKILLS_UPDATE_ARGV=()
if cmd_exists skills; then
SKILLS_UPDATE_ARGV=(skills)
elif cmd_exists npx; then
SKILLS_UPDATE_ARGV=(npx --yes skills)
else
return 1
fi

SKILLS_UPDATE_ARGV+=(update --project --global)
if [ "$NON_INTERACTIVE" -eq 1 ]; then
SKILLS_UPDATE_ARGV+=(--yes)
fi
return 0
}

module_skills() {
log "Agent skills $(emoji "🧠")"

if ! skills_update_argv; then
if missing_dependency_is_error; then
err "skills: required command not found: skills (or npx)"
return 1
fi
log "Skipping skills: neither skills nor npx found."
return 2
fi

if [ "$DRY_RUN" -eq 1 ]; then
log "DRY RUN: ${SKILLS_UPDATE_ARGV[*]}"
return 0
fi

run "${SKILLS_UPDATE_ARGV[@]}" || return 1
return 0
}

module_mise() {
log "mise $(emoji "🧩")"
ensure_cmd_or_skip mise mise || return $?
Expand Down
Loading
Loading