@@ -96,6 +96,7 @@ if (parsedArgs.help || parsedArgs._[0] == "help") {
9696 case "li" :
9797 if ( install_prefix ( ) . string != "/usr/local" ) {
9898 const dst = Path . home ( ) . join ( ".local" ) ;
99+ assert_manifest_resolved ( dst ) ;
99100 await install ( await combined_specs ( dst , args ) , dst . string ) ;
100101 await record_install ( dst , args ) ;
101102 } else {
@@ -329,7 +330,8 @@ async function query_pkgx(
329330 const needs_sudo_backwards = install_prefix ( ) . string == "/usr/local" ;
330331 let cmd = needs_sudo_backwards ? "/usr/bin/sudo" : pkgx ;
331332 if ( needs_sudo_backwards ) {
332- if ( ! Deno . env . get ( "SUDO_USER" ) ) {
333+ const sudo_user = Deno . env . get ( "SUDO_USER" ) ;
334+ if ( ! sudo_user ) {
333335 if ( Deno . uid ( ) == 0 ) {
334336 console . error (
335337 "%cwarning" ,
@@ -338,8 +340,18 @@ async function query_pkgx(
338340 ) ;
339341 }
340342 cmd = pkgx ;
343+ } else if ( reachable_as ( pkgx , sudo_user ) ) {
344+ // drop privileges so pkgx writes its cache as the invoking user, and point
345+ // HOME at their home so it doesn’t cache back under /root/ where they
346+ // couldn’t reach it on the next invocation.
347+ const home = user_home ( sudo_user ) ;
348+ if ( home ) env . HOME = home ;
349+ args . unshift ( "-u" , sudo_user , pkgx ) ;
341350 } else {
342- args . unshift ( "-u" , Deno . env . get ( "SUDO_USER" ) ! , pkgx ) ;
351+ // pkgx lives somewhere the unprivileged user can’t execute it (e.g. only
352+ // under /root/.pkgx). dropping privileges would abort with “Permission
353+ // denied”, so run it as root instead (pkgxdev/pkgm#68).
354+ cmd = pkgx ;
343355 }
344356 }
345357
@@ -379,6 +391,34 @@ async function query_pkgx(
379391 ] ;
380392}
381393
394+ // home directory of the invoking (pre-sudo) user, so pkgx caches under their
395+ // tree rather than /root/. getent is the portable lookup on Linux; on macOS it
396+ // is absent, but the /root/.pkgx scenario this guards against doesn’t arise
397+ // there in practice, so a missing home (→ no override) is fine.
398+ function user_home ( user : string ) : string | undefined {
399+ try {
400+ const out = new Deno . Command ( "/usr/bin/getent" , {
401+ args : [ "passwd" , user ] ,
402+ } ) . outputSync ( ) ;
403+ if ( ! out . success ) return undefined ;
404+ const fields = new TextDecoder ( ) . decode ( out . stdout ) . trim ( ) . split ( ":" ) ;
405+ return fields [ 5 ] || undefined ;
406+ } catch {
407+ return undefined ;
408+ }
409+ }
410+
411+ // can `user` execute the binary at `p`? private home dirs are typically mode
412+ // 700, so a path under another user’s home is unreachable; system paths and the
413+ // user’s own home are assumed reachable. used to decide whether dropping
414+ // privileges to `user` would leave pkgx unrunnable (pkgxdev/pkgm#68).
415+ function reachable_as ( p : string , user : string ) : boolean {
416+ if ( p . startsWith ( "/root/" ) ) return user === "root" ;
417+ const m = p . match ( / ^ \/ h o m e \/ ( [ ^ / ] + ) \/ / ) ;
418+ if ( m ) return m [ 1 ] === user ;
419+ return true ;
420+ }
421+
382422async function mirror_directory ( dst : string , src : string , prefix : string ) {
383423 let warned_copy_fallback = false ;
384424 await processEntry ( join ( src , prefix ) , join ( dst , prefix ) ) ;
@@ -794,6 +834,14 @@ async function update() {
794834 }
795835 }
796836
837+ // nothing satisfies a newer in-range version: everything’s current. return
838+ // before calling install(), which treats an empty arg list as a usage error
839+ // ("no packages specified") and exits non-zero.
840+ if ( update_list . length == 0 ) {
841+ console . error ( "everything is up-to-date" ) ;
842+ return ;
843+ }
844+
797845 for ( const pkgspec of update_list ) {
798846 const pkg = utils . pkg . parse ( pkgspec ) ;
799847 console . log (
@@ -858,7 +906,22 @@ function manifest_entry_kind(value: string): ManifestEntryKind {
858906// finished. no manifest at all → legacy behavior (see hydrate_graph), so
859907// existing installs keep working untouched.
860908function assert_manifest_resolved ( prefix = install_prefix ( ) ) {
861- const manifest = read_manifest ( prefix ) ;
909+ let manifest ;
910+ try {
911+ manifest = read_manifest ( prefix ) ;
912+ } catch ( err ) {
913+ // invalid TOML (e.g. a partially-finished hand-edit) must block commands
914+ // with a clear message, not crash with an uncaught parse stack trace.
915+ console . error (
916+ "%cerror" ,
917+ "color:red" ,
918+ `could not parse ${ manifest_path ( prefix ) } : ${
919+ err instanceof Error ? err . message : err
920+ } `,
921+ ) ;
922+ console . error ( "fix the TOML syntax and re-run" ) ;
923+ Deno . exit ( 1 ) ;
924+ }
862925 if ( ! manifest ) return ;
863926 const unresolved = Object . entries ( manifest )
864927 . filter ( ( [ , value ] ) => manifest_entry_kind ( value ) == "unresolved" )
@@ -1039,9 +1102,10 @@ async function combined_specs(prefix: Path, args: string[]): Promise<string[]> {
10391102}
10401103
10411104// map each requested arg to its canonical project and the constraint you asked
1042- // for. a bare name (`node`) yields `*` — track latest; a versioned spec
1043- // (`node@22`) yields its range (`^22`). pantry hiccups are non-fatal: the
1044- // package will still be picked up as a pin by record_install’s tree walk.
1105+ // for. a bare name (`node`) yields `*` here, which record_install then
1106+ // major-locks to the installed version (`^<installed>`); a versioned spec
1107+ // (`node@22`) yields its range (`^22`) verbatim. pantry hiccups are non-fatal:
1108+ // the package will still be picked up as a pin by record_install’s tree walk.
10451109async function requested_constraints (
10461110 args : string [ ] ,
10471111) : Promise < Record < string , string > > {
0 commit comments