diff --git a/bin/omarchy-npm-install b/bin/omarchy-npm-install index 3f1371ce..5c5f7b5b 100755 --- a/bin/omarchy-npm-install +++ b/bin/omarchy-npm-install @@ -1,76 +1,22 @@ #!/bin/bash -# omarchy:summary=Install a pnpm dlx wrapper for a given npm package. -# omarchy:args= [command-name] +# omarchy:summary=Install a small pnpm dlx wrapper for a given npm package. +# omarchy:args= [command-name [bin-name]] if [[ -z $1 ]]; then - echo "Usage: omarchy-npm-install [command-name]" + echo "Usage: omarchy-npm-install [command-name [bin-name]]" exit 1 fi package=$1 command=${2:-$1} +bin=${3:-$command} mkdir -p "$HOME/.local/bin" -cat > "$HOME/.local/bin/$command" <"$HOME/.local/bin/$command" </dev/null - node_root="\$(mise where node@latest)" -fi - -if omarchy-cmd-missing pnpm; then - echo "Installing pnpm for \$package..." - omarchy-pkg-add pnpm - hash -r -fi - -# pnpm reads npm_config_* env vars for CLI configuration. -export npm_config_minimum_release_age=7200 -export npm_config_dlx_cache_max_age=7200 - -ensure_bin_runtime() { - local bin_path=\$1 - local shebang - - IFS= read -r shebang < "\$bin_path" - - if [[ \$shebang == "#!"*"/bun"* || \$shebang == "#!"*"/env bun"* ]]; then - if omarchy-cmd-missing bun; then - echo "Installing bun runtime for \$package..." - omarchy-pkg-add bun - hash -r - fi - fi -} - -exec_package_bin() { - local package_bin_path=\$1 - shift - - if [[ -n \$package_bin_path ]]; then - ensure_bin_runtime "\$package_bin_path" - PATH="\$node_root/bin:\$PATH" exec "\$package_bin_path" "\$@" - fi -} - -# Resolve the package bin inside pnpm dlx, then run it with node@latest available for node shebangs. -# Some wrappers are aliases, e.g. playwright-cli wraps the playwright bin. -PATH="\$node_root/bin:\$PATH" pnpm dlx --package "\$package" true - -package_bin_path=\$(PATH="\$node_root/bin:\$PATH" pnpm dlx --package "\$package" which "\$package" 2>/dev/null) -exec_package_bin "\$package_bin_path" "\$@" - -# Scoped packages like @openai/codex expose an unscoped bin like codex. -package_bin_path=\$(PATH="\$node_root/bin:\$PATH" pnpm dlx --package "\$package" which "\$command" 2>/dev/null) -exec_package_bin "\$package_bin_path" "\$@" - -echo "Could not resolve npm bin for \$package / \$command" >&2 -exit 127 +exec omarchy-npm-run "$package" "$command" "$bin" "\$@" EOF chmod +x "$HOME/.local/bin/$command" diff --git a/bin/omarchy-npm-run b/bin/omarchy-npm-run new file mode 100755 index 00000000..9d84a67d --- /dev/null +++ b/bin/omarchy-npm-run @@ -0,0 +1,76 @@ +#!/bin/bash + +# omarchy:summary=Run a pnpm dlx-backed npm package wrapper. +# omarchy:args= [args...] +# omarchy:hidden=true + +if (($# < 3)); then + echo "Usage: omarchy-npm-run [args...]" + exit 1 +fi + +package=$1 +command=$2 +bin=$3 +shift 3 + +package_spec=$package +state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/npm-wrappers" +version_file="$state_dir/$command.version" +pin_ttl=7200 +uses_pinned_version=0 + +if [[ -s $version_file ]]; then + now=$(date +%s) + pinned_at=$(stat -c %Y "$version_file" 2>/dev/null || printf '0\n') + + if ((now - pinned_at < pin_ttl)); then + pinned_version=$(<"$version_file") + package_spec="$package@$pinned_version" + uses_pinned_version=1 + else + rm -f "$version_file" + fi +fi + +if ! node_root=$(mise where node@latest 2>/dev/null); then + mise use -g node@latest >/dev/null + node_root=$(mise where node@latest) +fi + +export PATH="$node_root/bin:$PATH" + +if omarchy-cmd-missing pnpm; then + echo "Installing pnpm for $package..." + omarchy-pkg-add pnpm + hash -r +fi + +# pnpm reads npm_config_* env vars for CLI configuration. +export npm_config_minimum_release_age=$pin_ttl +export npm_config_dlx_cache_max_age=$pin_ttl + +if (( uses_pinned_version )); then + export npm_config_minimum_release_age=0 +fi + +if (($# == 1)) && [[ $1 == "update" ]]; then + export npm_config_minimum_release_age=0 + export npm_config_dlx_cache_max_age=0 + + if ! latest_version=$(env -u npm_config_minimum_release_age -u npm_config_dlx_cache_max_age pnpm view "$package@latest" version); then + echo "Could not resolve latest npm version for $package" >&2 + exit 1 + fi + + mkdir -p "$state_dir" + printf '%s\n' "$latest_version" >"$version_file" + package_spec="$package@$latest_version" + + pnpm dlx --package "$package_spec" true + + echo "Updated $command to $latest_version" + exit 0 +fi + +exec pnpm dlx --package "$package_spec" "$bin" "$@" diff --git a/install/packaging/npm.sh b/install/packaging/npm.sh index 7ffdc315..65478fbd 100644 --- a/install/packaging/npm.sh +++ b/install/packaging/npm.sh @@ -2,7 +2,7 @@ omarchy-npm-install @openai/codex codex omarchy-npm-install @google/gemini-cli gemini omarchy-npm-install @github/copilot copilot omarchy-npm-install opencode-ai opencode -omarchy-npm-install playwright playwright-cli +omarchy-npm-install playwright playwright-cli playwright omarchy-npm-install @earendil-works/pi-coding-agent pi omarchy-npm-install @kitlangton/ghui ghui omarchy-npm-install hunkdiff hunk diff --git a/test/omarchy-npm-install-test.sh b/test/omarchy-npm-install-test.sh index 6b5dd9ad..42b47488 100644 --- a/test/omarchy-npm-install-test.sh +++ b/test/omarchy-npm-install-test.sh @@ -16,6 +16,21 @@ fail() { exit 1 } +assert_file_contains() { + local description="$1" + local file="$2" + local expected="$3" + + if ! grep -Fq "$expected" "$file"; then + printf 'Expected %s to contain: %s\n' "$file" "$expected" >&2 + printf 'Actual file:\n' >&2 + sed -n '1,120p' "$file" >&2 + fail "$description" + fi + + pass "$description" +} + cleanup() { [[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR" } @@ -24,60 +39,51 @@ trap cleanup EXIT TMPDIR=$(mktemp -d) HOME="$TMPDIR/home" "$ROOT/bin/omarchy-npm-install" @openai/codex codex wrapper="$TMPDIR/home/.local/bin/codex" +runtime="$ROOT/bin/omarchy-npm-run" [[ -x $wrapper ]] || fail "npm wrapper is generated" pass "npm wrapper is generated" -if ! grep -q "npm_config_minimum_release_age=7200" "$wrapper"; then - fail "npm wrapper configures pnpm minimum release age" -fi -pass "npm wrapper configures pnpm minimum release age" +assert_file_contains "npm wrapper defaults bin to command name" "$wrapper" 'exec omarchy-npm-run "@openai/codex" "codex" "codex" "$@"' -if ! grep -q "npm_config_dlx_cache_max_age=7200" "$wrapper"; then - fail "npm wrapper configures pnpm dlx cache max age" +if grep -q "pnpm dlx" "$wrapper"; then + fail "npm wrapper does not inline pnpm runtime" fi -pass "npm wrapper configures pnpm dlx cache max age" +pass "npm wrapper does not inline pnpm runtime" -if grep -q "PNPM_CONFIG_MINIMUM_RELEASE_AGE" "$wrapper"; then - fail "npm wrapper does not use ignored PNPM_CONFIG env vars" +HOME="$TMPDIR/home" "$ROOT/bin/omarchy-npm-install" playwright playwright-cli playwright +assert_file_contains "npm wrapper records explicit package bin" "$TMPDIR/home/.local/bin/playwright-cli" 'exec omarchy-npm-run "playwright" "playwright-cli" "playwright" "$@"' + +assert_file_contains "npm runtime defines pin ttl" "$runtime" "pin_ttl=7200" +assert_file_contains "npm runtime configures pnpm minimum release age" "$runtime" 'npm_config_minimum_release_age=$pin_ttl' +assert_file_contains "npm runtime configures pnpm dlx cache max age" "$runtime" 'npm_config_dlx_cache_max_age=$pin_ttl' + +if grep -q "PNPM_CONFIG_MINIMUM_RELEASE_AGE" "$runtime"; then + fail "npm runtime does not use ignored PNPM_CONFIG env vars" fi -pass "npm wrapper does not use ignored PNPM_CONFIG env vars" +pass "npm runtime does not use ignored PNPM_CONFIG env vars" removed_flag="--force""-update" -if grep -q -- "$removed_flag" "$wrapper"; then - fail "npm wrapper does not expose removed force update flag" +if grep -q -- "$removed_flag" "$runtime"; then + fail "npm runtime does not expose removed force update flag" fi -pass "npm wrapper does not expose removed force update flag" +pass "npm runtime does not expose removed force update flag" -if ! grep -q "npm_config_dlx_cache_max_age=0" "$wrapper"; then - fail "npm wrapper update bypasses dlx cache" +if grep -q " which " "$runtime"; then + fail "npm runtime does not discover package bins dynamically" fi -pass "npm wrapper update bypasses dlx cache" - -if ! grep -q "npm_config_minimum_release_age=0" "$wrapper"; then - fail "npm wrapper update bypasses minimum release age" -fi -pass "npm wrapper update bypasses minimum release age" - -if ! grep -q "wrapper_update=1" "$wrapper"; then - fail "npm wrapper maps update to wrapper update" -fi -pass "npm wrapper maps update to wrapper update" - -if ! grep -q "version_file=" "$wrapper"; then - fail "npm wrapper persists update version" -fi -pass "npm wrapper persists update version" +pass "npm runtime does not discover package bins dynamically" stub_bin="$TMPDIR/bin" node_root="$TMPDIR/node" -package_bin="$TMPDIR/package-bin" +state_home="$TMPDIR/state" pnpm_log="$TMPDIR/pnpm.log" package_args="$TMPDIR/package-args" +version_file="$state_home/omarchy/npm-wrappers/codex.version" mkdir -p "$stub_bin" "$node_root/bin" -cat > "$stub_bin/mise" <<'SH' +cat >"$stub_bin/mise" <<'SH' #!/bin/bash if [[ $1 == "where" && $2 == "node@latest" ]]; then printf '%s\n' "$OMARCHY_NPM_TEST_NODE_ROOT" @@ -87,12 +93,12 @@ fi exit 1 SH -cat > "$stub_bin/omarchy-cmd-missing" <<'SH' +cat >"$stub_bin/omarchy-cmd-missing" <<'SH' #!/bin/bash exit 1 SH -cat > "$stub_bin/pnpm" <<'SH' +cat >"$stub_bin/pnpm" <<'SH' #!/bin/bash printf 'min_age=%s dlx_cache=%s args=%s\n' "${npm_config_minimum_release_age:-}" "${npm_config_dlx_cache_max_age:-}" "$*" >>"$OMARCHY_NPM_TEST_PNPM_LOG" @@ -101,57 +107,79 @@ if [[ $1 == "view" ]]; then exit 0 fi -if [[ $1 == "dlx" && $4 == "true" ]]; then - exit 0 -fi +if [[ $1 == "dlx" ]]; then + if [[ $4 == "true" ]]; then + exit 0 + fi -if [[ $1 == "dlx" && $4 == "which" ]]; then - printf '%s\n' "$OMARCHY_NPM_TEST_PACKAGE_BIN" + shift 4 + printf '%s\n' "$*" >"$OMARCHY_NPM_TEST_PACKAGE_ARGS" exit 0 fi exit 1 SH -cat > "$package_bin" <<'SH' -#!/bin/bash -printf '%s\n' "$*" >"$OMARCHY_NPM_TEST_PACKAGE_ARGS" -SH - -chmod +x "$stub_bin/mise" "$stub_bin/omarchy-cmd-missing" "$stub_bin/pnpm" "$package_bin" +chmod +x "$stub_bin/mise" "$stub_bin/omarchy-cmd-missing" "$stub_bin/pnpm" OMARCHY_NPM_TEST_NODE_ROOT="$node_root" \ -OMARCHY_NPM_TEST_PACKAGE_BIN="$package_bin" \ OMARCHY_NPM_TEST_PACKAGE_ARGS="$package_args" \ OMARCHY_NPM_TEST_PNPM_LOG="$pnpm_log" \ +HOME="$TMPDIR/home" \ +XDG_STATE_HOME="$state_home" \ PATH="$stub_bin:$PATH" \ "$wrapper" -s danger-full-access update alpha beta -if ! grep -q "dlx_cache=0" "$pnpm_log"; then - fail "update sets pnpm dlx cache age to zero" +if ! grep -Fq "args=dlx --package @openai/codex codex -s danger-full-access update alpha beta" "$pnpm_log"; then + fail "non-leading update is forwarded to package bin" fi -pass "update sets pnpm dlx cache age to zero" +pass "non-leading update is forwarded to package bin" -if ! grep -q "min_age=0" "$pnpm_log"; then - fail "update sets pnpm minimum release age to zero" +if ! grep -q "dlx_cache=7200" "$pnpm_log"; then + fail "normal run uses pnpm dlx cache age" fi -pass "update sets pnpm minimum release age to zero" +pass "normal run uses pnpm dlx cache age" -if ! grep -q "args=dlx --package @openai/codex@1.2.3 true" "$pnpm_log"; then - fail "update uses resolved latest package version" +if ! grep -q "min_age=7200" "$pnpm_log"; then + fail "normal run uses pnpm minimum release age" fi -pass "update uses resolved latest package version" +pass "normal run uses pnpm minimum release age" -if [[ -f $package_args ]]; then - fail "update does not run the package bin" +if [[ $(<"$package_args") != "-s danger-full-access update alpha beta" ]]; then + fail "normal run forwards package args" fi -pass "update does not run the package bin" +pass "normal run forwards package args" + +if [[ -f $version_file ]]; then + fail "non-leading update does not pin wrapper version" +fi +pass "non-leading update does not pin wrapper version" rm -f "$pnpm_log" "$package_args" OMARCHY_NPM_TEST_NODE_ROOT="$node_root" \ -OMARCHY_NPM_TEST_PACKAGE_BIN="$package_bin" \ OMARCHY_NPM_TEST_PACKAGE_ARGS="$package_args" \ OMARCHY_NPM_TEST_PNPM_LOG="$pnpm_log" \ +HOME="$TMPDIR/home" \ +XDG_STATE_HOME="$state_home" \ +PATH="$stub_bin:$PATH" \ + "$wrapper" update alpha + +if ! grep -Fq "args=dlx --package @openai/codex codex update alpha" "$pnpm_log"; then + fail "update with extra args is forwarded to package bin" +fi +pass "update with extra args is forwarded to package bin" + +if [[ -f $version_file ]]; then + fail "update with extra args does not pin wrapper version" +fi +pass "update with extra args does not pin wrapper version" + +rm -f "$pnpm_log" "$package_args" +OMARCHY_NPM_TEST_NODE_ROOT="$node_root" \ +OMARCHY_NPM_TEST_PACKAGE_ARGS="$package_args" \ +OMARCHY_NPM_TEST_PNPM_LOG="$pnpm_log" \ +HOME="$TMPDIR/home" \ +XDG_STATE_HOME="$state_home" \ PATH="$stub_bin:$PATH" \ "$wrapper" update @@ -165,25 +193,31 @@ if ! grep -q "min_age=0" "$pnpm_log"; then fi pass "wrapper update bypasses pnpm minimum release age" -if ! grep -q "args=dlx --package @openai/codex@1.2.3 true" "$pnpm_log"; then +if ! grep -Fq "args=dlx --package @openai/codex@1.2.3 true" "$pnpm_log"; then fail "wrapper update uses resolved latest package version" fi pass "wrapper update uses resolved latest package version" if [[ -f $package_args ]]; then - fail "wrapper update does not run package self-updater" + fail "wrapper update does not run package bin" fi -pass "wrapper update does not run package self-updater" +pass "wrapper update does not run package bin" + +if [[ $(<"$version_file") != "1.2.3" ]]; then + fail "wrapper update persists resolved version" +fi +pass "wrapper update persists resolved version" rm -f "$pnpm_log" "$package_args" OMARCHY_NPM_TEST_NODE_ROOT="$node_root" \ -OMARCHY_NPM_TEST_PACKAGE_BIN="$package_bin" \ OMARCHY_NPM_TEST_PACKAGE_ARGS="$package_args" \ OMARCHY_NPM_TEST_PNPM_LOG="$pnpm_log" \ +HOME="$TMPDIR/home" \ +XDG_STATE_HOME="$state_home" \ PATH="$stub_bin:$PATH" \ "$wrapper" gamma -if ! grep -q "args=dlx --package @openai/codex@1.2.3 true" "$pnpm_log"; then +if ! grep -Fq "args=dlx --package @openai/codex@1.2.3 codex gamma" "$pnpm_log"; then fail "wrapper reuses pinned update version" fi pass "wrapper reuses pinned update version" @@ -197,3 +231,28 @@ if [[ $(<"$package_args") != "gamma" ]]; then fail "wrapper forwards args after pinned update" fi pass "wrapper forwards args after pinned update" + +rm -f "$pnpm_log" "$package_args" +touch -d "@$(($(date +%s) - 7201))" "$version_file" +OMARCHY_NPM_TEST_NODE_ROOT="$node_root" \ +OMARCHY_NPM_TEST_PACKAGE_ARGS="$package_args" \ +OMARCHY_NPM_TEST_PNPM_LOG="$pnpm_log" \ +HOME="$TMPDIR/home" \ +XDG_STATE_HOME="$state_home" \ +PATH="$stub_bin:$PATH" \ + "$wrapper" delta + +if ! grep -Fq "args=dlx --package @openai/codex codex delta" "$pnpm_log"; then + fail "wrapper drops expired pinned update version" +fi +pass "wrapper drops expired pinned update version" + +if ! grep -q "min_age=7200" "$pnpm_log"; then + fail "wrapper restores minimum release age after pin expiry" +fi +pass "wrapper restores minimum release age after pin expiry" + +if [[ -f $version_file ]]; then + fail "wrapper removes expired pinned version file" +fi +pass "wrapper removes expired pinned version file"