@@ -22,6 +22,97 @@ require_command() {
2222 fi
2323}
2424
25+ is_interactive_tty () {
26+ [[ -t 0 && -t 1 ]]
27+ }
28+
29+ is_local_package_ref () {
30+ local ref=" $1 "
31+ case " $ref " in
32+ ./* | ../* | /* | file:* | * .tgz | * .tar.gz | http://* | https://* )
33+ return 0
34+ ;;
35+ esac
36+ return 1
37+ }
38+
39+ scope_list_contains () {
40+ local scope_list=" $1 "
41+ local scope=" $2 "
42+ local normalized=" ,${scope_list// / } ,"
43+ [[ " $normalized " == * " ,$scope ," * ]]
44+ }
45+
46+ has_package_read_scope () {
47+ local scope_list=" $1 "
48+ scope_list_contains " $scope_list " " read:packages" ||
49+ scope_list_contains " $scope_list " " write:packages" ||
50+ scope_list_contains " $scope_list " " delete:packages"
51+ }
52+
53+ get_gh_token_scopes () {
54+ local token=" $1 "
55+ if [[ -z " $token " ]]; then
56+ return 0
57+ fi
58+
59+ GH_TOKEN=" $token " gh api -i /user 2> /dev/null |
60+ tr -d ' \r' |
61+ awk ' BEGIN { IGNORECASE = 1 } /^x-oauth-scopes:/ { sub(/^[^:]*:[[:space:]]*/, ""); print; exit }'
62+ }
63+
64+ ensure_gh_token_has_package_scope () {
65+ local token=" $1 "
66+ local scopes
67+ scopes=" $( get_gh_token_scopes " $token " ) "
68+
69+ if has_package_read_scope " $scopes " ; then
70+ printf ' %s' " $token "
71+ return 0
72+ fi
73+
74+ log " gh auth token is missing read:packages scope."
75+ if ! is_interactive_tty; then
76+ return 1
77+ fi
78+
79+ log " Attempting gh auth scope refresh (read:packages)..."
80+ if ! gh auth refresh -h github.com -s read:packages; then
81+ return 1
82+ fi
83+
84+ token=" $( gh auth token 2> /dev/null || true) "
85+ if [[ -z " $token " ]]; then
86+ return 1
87+ fi
88+
89+ scopes=" $( get_gh_token_scopes " $token " ) "
90+ if has_package_read_scope " $scopes " ; then
91+ log " gh auth token refreshed with package scope."
92+ printf ' %s' " $token "
93+ return 0
94+ fi
95+
96+ return 1
97+ }
98+
99+ install_global_package () {
100+ local package_ref=" $1 "
101+ local install_output
102+
103+ if install_output=" $( npm install -g " $package_ref " 2>&1 ) " ; then
104+ printf ' %s\n' " $install_output "
105+ return 0
106+ fi
107+
108+ printf ' %s\n' " $install_output " >&2
109+ if grep -Eqi " npm\\ .pkg\\ .github\\ .com|permission_denied|e401|e403|read:packages" <<< " $install_output" ; then
110+ printf ' [install] ERROR: GitHub Packages auth failed. Token likely missing read:packages.\n' >&2
111+ printf ' [install] ERROR: Run: gh auth refresh -h github.com -s read:packages\n' >&2
112+ fi
113+ return 1
114+ }
115+
25116upsert_npmrc_line () {
26117 local key_prefix=" $1 "
27118 local line_value=" $2 "
@@ -42,6 +133,19 @@ upsert_npmrc_line() {
42133 fi
43134}
44135
136+ remove_npmrc_line () {
137+ local key_prefix=" $1 "
138+ if [[ ! -f " $NPMRC_PATH " ]]; then
139+ return 0
140+ fi
141+
142+ awk -v key_prefix=" $key_prefix " '
143+ index($0, key_prefix) == 1 { next }
144+ { print }
145+ ' " $NPMRC_PATH " > " ${NPMRC_PATH} .tmp"
146+ mv " ${NPMRC_PATH} .tmp" " $NPMRC_PATH "
147+ }
148+
45149read_token_interactive () {
46150 local token=" "
47151 printf ' GitHub token (read:packages): '
@@ -56,37 +160,76 @@ main() {
56160 local registry_host=" ${REGISTRY_URL# https:// } "
57161 registry_host=" ${registry_host# http:// } "
58162 registry_host=" ${registry_host%%/ } "
163+ local requires_registry_auth=" true"
164+ if is_local_package_ref " $PACKAGE_NAME " ; then
165+ requires_registry_auth=" false"
166+ fi
59167
60168 local token=" ${NODE_AUTH_TOKEN:- } "
61- if [[ -z " $token " ]] && command -v gh > /dev/null 2>&1 ; then
169+ local token_source=" env"
170+ if [[ " $requires_registry_auth " == " true" ]] && [[ -z " $token " ]] && command -v gh > /dev/null 2>&1 ; then
62171 if gh auth status > /dev/null 2>&1 ; then
63172 token=" $( gh auth token 2> /dev/null || true) "
64173 if [[ -n " $token " ]]; then
65- log " Using token from gh auth session."
174+ log " Using token from gh auth session. Checking package scope..."
175+ token=" $( ensure_gh_token_has_package_scope " $token " || true) "
176+ token_source=" gh"
66177 fi
67178 fi
68179 fi
69180
70- if [[ -z " $token " ]]; then
181+ if [[ " $requires_registry_auth " == " true" ]] && [[ -z " $token " ]]; then
182+ if ! is_interactive_tty; then
183+ fail " No usable token found for GitHub Packages. Set NODE_AUTH_TOKEN with read:packages."
184+ fi
71185 log " A GitHub token with read:packages is required to install from GitHub Packages."
72186 token=" $( read_token_interactive) "
187+ token_source=" manual"
73188 fi
74189
75- if [[ -z " $token " ]]; then
190+ if [[ " $requires_registry_auth " == " true " ]] && [[ -z " $token " ]]; then
76191 fail " No GitHub token provided."
77192 fi
78193
79- local npmrc_dir
80- npmrc_dir=" $( dirname " $NPMRC_PATH " ) "
81- mkdir -p " $npmrc_dir "
194+ if [[ " $requires_registry_auth " == " true" ]]; then
195+ local npmrc_dir
196+ npmrc_dir=" $( dirname " $NPMRC_PATH " ) "
197+ mkdir -p " $npmrc_dir "
82198
83- upsert_npmrc_line " ${PACKAGE_SCOPE} :registry=" " ${PACKAGE_SCOPE} :registry=${REGISTRY_URL} "
84- upsert_npmrc_line " //${registry_host} /:_authToken=" " //${registry_host} /:_authToken=${token} "
85- upsert_npmrc_line " always-auth=" " always-auth=true"
86- log " Updated ${NPMRC_PATH} for ${PACKAGE_SCOPE} ."
199+ upsert_npmrc_line " ${PACKAGE_SCOPE} :registry=" " ${PACKAGE_SCOPE} :registry=${REGISTRY_URL} "
200+ upsert_npmrc_line " //${registry_host} /:_authToken=" " //${registry_host} /:_authToken=${token} "
201+ remove_npmrc_line " always-auth="
202+ log " Updated ${NPMRC_PATH} for ${PACKAGE_SCOPE} ."
203+ else
204+ log " Local package reference detected; skipping GitHub Packages auth setup."
205+ fi
87206
88207 log " Installing ${PACKAGE_NAME} globally..."
89- npm install -g " $PACKAGE_NAME "
208+ if ! install_global_package " $PACKAGE_NAME " ; then
209+ if [[ " $requires_registry_auth " == " true" ]] &&
210+ [[ " $token_source " == " gh" ]] &&
211+ command -v gh > /dev/null 2>&1 &&
212+ is_interactive_tty; then
213+ log " Retrying after gh auth refresh (read:packages)..."
214+ if gh auth refresh -h github.com -s read:packages; then
215+ token=" $( gh auth token 2> /dev/null || true) "
216+ if [[ -n " $token " ]]; then
217+ upsert_npmrc_line " //${registry_host} /:_authToken=" " //${registry_host} /:_authToken=${token} "
218+ if install_global_package " $PACKAGE_NAME " ; then
219+ log " Install succeeded after token refresh."
220+ else
221+ fail " Global install failed after token refresh."
222+ fi
223+ else
224+ fail " Global install failed and gh did not return a token after refresh."
225+ fi
226+ else
227+ fail " Global install failed and gh auth refresh was unsuccessful."
228+ fi
229+ else
230+ fail " Global install failed."
231+ fi
232+ fi
90233
91234 if ! command -v " $SETUP_BIN " > /dev/null 2>&1 ; then
92235 fail " Install completed but ${SETUP_BIN} is not in PATH."
0 commit comments