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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

- `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.

### Changed

- Show Unix command phases and elapsed progress every 30 seconds at info/debug log levels, preserving JSONL stdout and allowing long installations to continue until cancelled.
- Stream Unix npm installation stderr and guarded pip installation output; report parallel pip workers as they finish while retaining separate package logs.

### Fixed

- Make Unix command and capture waits interruptible, preserve interactive stdin and exit statuses, and clean owned children, output helpers, and temporary resources on SIGINT/SIGTERM.
- Disable prompts for background pip installations and for discovery/planning under `--non-interactive`.
- Honor `--non-interactive` for Homebrew upgrade-table confirmation with command-scoped `HOMEBREW_NO_ASK=1`; explain the confirmation prompt during interactive runs.

## [2.1.3] - 2026-08-10

### Fixed
Expand Down
27 changes: 27 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
# Plan: macOS stalls and visible update progress

## Goal

- Make Unix updates cancellable and expose the active phase without automatic installation timeouts or changes to public flags and JSONL events.

## Live diagnosis

- Reproduced the apparent stall after Homebrew 6.0.22's upgrade table: its default ask mode was waiting for confirmation, rather than a frozen command.
- The initial diagnostic run updated Homebrew itself, shell repositories, and one npm package. The first Ctrl+C failed the brew module but allowed later modules to run; the second stopped the run at npm preflight. The identified pause is covered by isolated Homebrew prompt regressions; no additional live upgrade is needed for validation.

## Execution checklist

- [x] Introduce Bash 3.2-compatible managed execution/capture helpers with interruptible waits, owned-child cleanup, preserved stdin, and accurate exit statuses.
- [x] Announce slow phases and emit elapsed human progress every 30 seconds at info/debug log levels, routing human output to stderr with JSON.
- [x] Stream npm stderr and guarded pip installation diagnostics; report parallel pip completion with separate package logs.
- [x] Disable background pip prompts and discovery/planning prompts under `-n`.
- [x] Scope `HOMEBREW_NO_ASK=1` to brew commands under `-n`; preserve interactive confirmation with a clear hint.
- [x] Document cancellation, progress, and diagnostics in README, SPEC, and CHANGELOG without a release/version bump.
- [x] Verify synchronized SIGINT/SIGTERM cleanup for commands, captures, npm preflights, and parallel pip workers, plus interactive stdin under a pseudo-terminal.
- [x] Verify live diagnostics, failure propagation, JSONL purity with logging, and existing npm/Python safety behavior.
- [x] Run lint and full tests on macOS Bash 3.2 and shared-runner coverage on Linux; use isolated homes and stub commands for installed-copy checks.
- [x] Reproduce the table pause in a real update run and identify Homebrew confirmation as the cause.
- [x] Validate Homebrew non-interactive environment scoping and interactive prompt behavior.

---

# Plan: skills module updates

## Goal
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ Tests require `python3` with either public `packaging` or pip's vendored packagi

- This script updates _global_ environments (`npm -g`, `pip`), which can be disruptive.
- Use `--dry-run` first, and consider `--only`/`--skip` to control scope.
- Homebrew can pause after its upgrade table to request confirmation. Interactive runs retain this prompt and show a reminder; `updates -n` scopes `HOMEBREW_NO_ASK=1` to brew commands so unattended upgrades proceed without changing your shell or Homebrew configuration.
- On macOS/Linux, slow commands and discovery steps show their current phase and a progress message every 30 seconds with elapsed time. These messages follow `--log-level` (`info` or `debug`) and go to stderr with `--json`. Long updates have no automatic installation timeout; use Ctrl+C to cancel.
- Unix cancellation stops owned commands and output helpers, cleans temporary resources, and exits `130` for SIGINT or `143` for SIGTERM. Foreground interactive commands retain terminal input. An interrupted package installation may be incomplete.
- Unix npm installation stderr and guarded pip installation output appear while commands run. Parallel pip packages keep separate logs and report completion as workers finish; background pip installations always disable prompts. `-n` also disables pip discovery and planning prompts.
- For npm 11+ global installs, `updates` may retry once with npm's suggested one-shot `--allow-scripts=...` list so package postinstall steps can finish without changing persistent npm config.
- Node updates are filtered against the active Node runtime and installed per package. An unexpected incompatible or otherwise failed package does not prevent later compatible packages from being attempted, but still fails the node module.
- Git-backed `shell`/`repos` updates never infer tracking branches or alter local work. Detached HEADs, branches without upstreams, and dirty worktrees warn and skip; diverged histories and failed pulls/post-pull actions fail the module.
Expand Down
17 changes: 13 additions & 4 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ When passed, `~/.updatesrc` is not read. Useful for CI, testing, and debugging.
Output is intended to be stable and easy to grep.

- Normal progress goes to **stdout** (or **stderr** when `--json` is active).
- On Unix, managed commands and potentially blocking npm, pip, and Git captures announce their module/package and phase before waiting. While waiting, human progress messages include elapsed time every 30 seconds at `info`/`debug` log levels. They add no JSONL event types or fields.
- Warnings and errors go to **stderr** and are prefixed:
- `WARN: ...`
- `ERROR: ...`
Expand Down Expand Up @@ -312,7 +313,13 @@ See [Section 3.8](#38---json-jsonl-streaming-output) for the full event type tab

When `--json` is active, the log file receives the human-readable stderr output, not the JSONL stream.

### 5.4 Color / emoji
### 5.4 Unix command execution and cancellation

- Shared Bash 3.2-compatible execution and capture helpers preserve command exit statuses and foreground interactive stdin.
- SIGINT/SIGTERM interrupt waits, terminate and reap owned children and output helpers, clean temporary resources, and exit `130`/`143`, respectively. No later module runs after cancellation; cleanup never uses broad process-name kills.
- Long installations continue until completion or cancellation; no automatic installation timeout is added. Cancellation does not roll back changes already made by an underlying tool.

### 5.5 Color / emoji

- ANSI colors are enabled when stderr/stdout are TTYs and `NO_COLOR` is not set.
- `--no-color` or `NO_COLOR=1` disables colors globally.
Expand Down Expand Up @@ -404,6 +411,7 @@ Purpose: update and upgrade Homebrew formulae (and optionally casks).
- `--brew-mode casks`: `brew upgrade`
- `--brew-mode greedy`: `brew upgrade --greedy`
- If `--brew-cleanup` (default): `brew cleanup`
- On Unix, `-n` / `--non-interactive` sets `HOMEBREW_NO_ASK=1` only for brew commands to disable Homebrew upgrade-table confirmation. Interactive runs retain Homebrew prompts and print an info-level hint before upgrading. No persistent environment or Homebrew configuration changes are made.
- Side effects: upgrades Homebrew-managed packages.

### 8.2 `shell`
Expand Down Expand Up @@ -480,7 +488,7 @@ Purpose: upgrade global npm packages using `npm-check-updates`.
- Per package, if npm fails with `ERESOLVE`, retries once with `--legacy-peer-deps`; configured npm flags are retained, duplicate configured `--legacy-peer-deps` is removed for the retry, and the forced retry flag is appended once.
- Per package, if npm succeeds but reports pending global install scripts, retries once with npm's suggested one-shot `--allow-scripts=...` list while retaining configured npm flags.
- `EBADENGINE` and other fatal failures are not retried. Remaining compatible packages are still attempted; any final package failure makes the node module fail.
- Superseded first-attempt diagnostics are suppressed after a successful retry. Final failures retain raw npm diagnostics plus a concise package-specific error.
- On Unix, installation stderr streams live while being retained for engine checks and bounded retries; earlier attempt diagnostics therefore remain visible even after a successful retry. Native Windows suppresses superseded first-attempt diagnostics after a successful retry. Final failures retain raw npm diagnostics plus a concise package-specific error.
- Side effects: upgrades global npm packages.

The Git and Node hardening above is implemented behind private command-outcome seams. The authoritative Node engine preflight adds no runtime dependency. These mechanics do not add or change public v2 flags, configuration keys, exit codes, JSONL event types, or summary fields.
Expand All @@ -507,7 +515,8 @@ Purpose: upgrade global Python packages with `pip`.
- Bash implementation, normal or `--pip-force`: `<launcher> -m pip list --outdated --format=json [--user]`, then `<launcher> -m pip install -U <pkg>` in parallel batches of `--parallel <N>`.
- Bash implementation, externally-managed default: `<launcher> -m pip list --outdated --format=json --user`, then per-package `pip install -U --user [--break-system-packages if supported] --only-binary=:all: --dry-run --report <report> <pkg>` guard checks. If pip lacks `--dry-run --report`, this path errors under `--only` and skips otherwise. Packages are skipped if the report would install packages absent from the user site, use source distributions, or violate installed/planned dependency requirements. The safe subset is checked again in one combined dry-run, installed in one wheel-only user-site transaction only if that combined plan is safe, then `pip check` runs. If post-install `pip check` failures were already present before install, the guarded path warns instead of failing on the pre-existing environment issue.
- Native Windows PowerShell implementation: same discovery/install flow as the normal path, but upgrades run sequentially and `--parallel <N>` is rejected.
- With `-n`: adds `--no-input` to pip calls.
- With `-n`: disables pip input, including Unix discovery and planning calls. Unix background pip installations always disable input, even without `-n`; foreground interactive commands retain input otherwise.
- On Unix, guarded pip installation output streams live. Parallel workers retain separate package logs, identify active packages, and replay each package log when that worker completes, preserving its exit status.
- Side effects: upgrades Python packages; does not upgrade the Python interpreter itself.

### 8.9 `uv`
Expand Down Expand Up @@ -691,7 +700,7 @@ Purpose: list available macOS software updates.
- **PII:** No user data is collected or transmitted.
- **Abuse cases:**
- Malicious or tampered GitHub release asset: mitigated by immutable releases, GitHub asset digests, `SHA256SUMS`, manifest validation, and HTTPS.
- pip parallel upgrades: stderr interleaving is cosmetic, not a security issue.
- pip parallel upgrades retain separate package logs; replay happens as each worker completes.
- `--pip-force` is explicitly opt-in and documented as unsafe.
- **Privilege escalation:** `sudo` is only used for Linux system package upgrades. Native Windows self-update never elevates; unknown or non-writable layouts warn and skip.

Expand Down
1 change: 1 addition & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."

./tests/test_cli.sh
python3 ./tests/test_execution.py
bash ./tests/test_release.sh

if command -v pwsh >/dev/null 2>&1; then
Expand Down
30 changes: 9 additions & 21 deletions tests/test_cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ if [ -z "$SYSTEM_PYTHON3" ]; then
echo "python3 is required for tests/test_cli.sh" >&2
exit 1
fi
if ! "$SYSTEM_PYTHON3" - <<'PY' >/dev/null 2>&1; then
if ! "$SYSTEM_PYTHON3" -c '
try:
import packaging.requirements
except Exception:
import pip._vendor.packaging.requirements
PY
' >/dev/null 2>&1; then
echo "python3 with packaging or pip vendored packaging is required for guard helper tests" >&2
exit 1
fi
Expand Down Expand Up @@ -1241,9 +1241,9 @@ setup_python_guard_fixture
: >"$CALL_LOG"
"$SCRIPT" --only python --pip-force --no-emoji >/dev/null
grep -q '^python -m pip list --outdated --format=json$' "$CALL_LOG"
grep -q '^python -m pip install -U --break-system-packages idna$' "$CALL_LOG"
grep -q '^python -m pip install -U --break-system-packages pyelftools$' "$CALL_LOG"
grep -q '^python -m pip install -U --break-system-packages unicorn$' "$CALL_LOG"
grep -q '^python -m pip install -U --break-system-packages --no-input idna$' "$CALL_LOG"
grep -q '^python -m pip install -U --break-system-packages --no-input pyelftools$' "$CALL_LOG"
grep -q '^python -m pip install -U --break-system-packages --no-input unicorn$' "$CALL_LOG"

UPDATES_TEST_CASE

Expand Down Expand Up @@ -2066,14 +2066,8 @@ echo "$out" | grep -q '^==> node END (OK)'
grep -q '^npm install -g -- @tarquinen/opencode-dcp@3.1.13$' "$CALL_LOG"
grep -q '^npm install -g --legacy-peer-deps -- @tarquinen/opencode-dcp@3.1.13$' "$CALL_LOG"
grep -q '^npm install -g --allow-scripts=opencode-ai,koffi --legacy-peer-deps -- @tarquinen/opencode-dcp@3.1.13$' "$CALL_LOG"
if grep -q 'npm error code ERESOLVE' "$npm_eresolve_stderr"; then
echo "Expected successful ERESOLVE retry to suppress first-pass npm error details" >&2
exit 1
fi
if grep -q 'npm warn allow-scripts' "$npm_eresolve_stderr"; then
echo "Expected successful allow-scripts retry to suppress first-pass npm warning details" >&2
exit 1
fi
grep -q 'npm error code ERESOLVE' "$npm_eresolve_stderr"
grep -q 'npm warn allow-scripts' "$npm_eresolve_stderr"
grep -q 'retrying with --legacy-peer-deps' "$npm_eresolve_stderr"
grep -q 'retrying once with npm-provided allow-scripts list' "$npm_eresolve_stderr"

Expand Down Expand Up @@ -2173,10 +2167,7 @@ out="$("$SCRIPT" --only node --no-emoji --no-color 2>"$npm_allow_scripts_stderr"
echo "$out" | grep -q '^==> node END (OK)'
grep -q '^npm install -g -- opencode-ai@1.17.8$' "$CALL_LOG"
grep -q '^npm install -g --allow-scripts=opencode-ai,koffi -- opencode-ai@1.17.8$' "$CALL_LOG"
if grep -q 'npm warn allow-scripts' "$npm_allow_scripts_stderr"; then
echo "Expected successful allow-scripts retry to suppress npm warning details" >&2
exit 1
fi
grep -q 'npm warn allow-scripts' "$npm_allow_scripts_stderr"
grep -q 'retrying once with npm-provided allow-scripts list' "$npm_allow_scripts_stderr"

UPDATES_TEST_CASE
Expand Down Expand Up @@ -2224,10 +2215,7 @@ out="$("$SCRIPT" --only node --no-emoji --no-color 2>"$npm_allow_scripts_flag_on
echo "$out" | grep -q '^==> node END (OK)'
grep -q '^npm install -g -- opencode-ai@1.17.8$' "$CALL_LOG"
grep -q '^npm install -g --allow-scripts=opencode-ai,koffi -- opencode-ai@1.17.8$' "$CALL_LOG"
if grep -q 'npm warn allow-scripts' "$npm_allow_scripts_flag_only_stderr"; then
echo "Expected successful flag-only allow-scripts retry to suppress npm warning details" >&2
exit 1
fi
grep -q 'npm warn allow-scripts' "$npm_allow_scripts_flag_only_stderr"
grep -q 'retrying once with npm-provided allow-scripts list' "$npm_allow_scripts_flag_only_stderr"

UPDATES_TEST_CASE
Expand Down
Loading