valise-sh 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +152 -2
  2. package/README.md +99 -146
  3. package/bin/valise +57 -5376
  4. package/docs/architecture.md +77 -121
  5. package/docs/contrib-modules.md +172 -218
  6. package/docs/installation.md +137 -83
  7. package/docs/module-abi.md +125 -126
  8. package/docs/projects.md +249 -0
  9. package/docs/release-policy.md +68 -93
  10. package/docs/shared-modules.md +40 -39
  11. package/examples/README.md +12 -11
  12. package/examples/e2e/README.md +59 -66
  13. package/examples/e2e/bundles.tsv +6 -0
  14. package/examples/e2e/module.d/e2e_aliases.meta +1 -2
  15. package/examples/e2e/module.d/e2e_completions_bash.meta +0 -1
  16. package/examples/e2e/module.d/e2e_completions_zsh.meta +0 -1
  17. package/examples/e2e/module.d/e2e_env_setup.meta +2 -3
  18. package/examples/e2e/module.d/e2e_fish_features.meta +0 -1
  19. package/examples/e2e/module.d/e2e_generated_config.meta +1 -2
  20. package/examples/e2e/module.d/e2e_ksh_features.meta +0 -1
  21. package/examples/e2e/module.d/e2e_layer_probe.meta +1 -2
  22. package/examples/e2e/module.d/e2e_optional_network.meta +1 -2
  23. package/examples/e2e/module.d/e2e_platform_tools.meta +8 -0
  24. package/examples/e2e/module.d/e2e_platform_tools_bsd.meta +2 -3
  25. package/examples/e2e/module.d/e2e_platform_tools_darwin.meta +2 -3
  26. package/examples/e2e/module.d/e2e_platform_tools_linux.meta +2 -3
  27. package/examples/e2e/valise.conf +4 -0
  28. package/examples/e2e/variants.tsv +3 -0
  29. package/examples/local/module.d/hello_valise.meta +1 -2
  30. package/examples/profile-project/README.md +78 -108
  31. package/examples/profile-project/bundles.tsv +8 -0
  32. package/examples/profile-project/{shell/layers → layers}/demo/module.d/contrib_demo_greeting.meta +1 -2
  33. package/examples/profile-project/{shell/module.d → module.d}/greeting.meta +1 -2
  34. package/examples/profile-project/{shell/module.d → module.d}/profile_main.meta +0 -1
  35. package/examples/profile-project/{shell/modules → modules}/greeting.sh +1 -1
  36. package/examples/profile-project/valise.conf +4 -0
  37. package/examples/profile-project/variants.tsv +3 -0
  38. package/examples/shared/README.md +13 -34
  39. package/examples/shared/module.d/contrib_shared_paths.meta +2 -3
  40. package/examples/shared/module.d/contrib_shared_umask.meta +1 -2
  41. package/examples/shared/modules/shared/contrib_shared_paths.sh +2 -2
  42. package/lib/capabilities.sh +40 -13
  43. package/lib/cli/commands.sh +278 -0
  44. package/lib/cli/common.sh +326 -0
  45. package/lib/cli/devtools.sh +539 -0
  46. package/lib/cli/fish-hook.sh +986 -0
  47. package/lib/cli/integration.sh +337 -0
  48. package/lib/cli/lifecycle.sh +578 -0
  49. package/lib/cli/project.sh +1807 -0
  50. package/lib/cli/store.sh +1023 -0
  51. package/lib/cli/usage.sh +308 -0
  52. package/lib/compiler.awk +11 -4
  53. package/lib/fish/loader.fish +69 -9
  54. package/lib/fish/metadata.fish +49 -5
  55. package/lib/fish/profile.fish +170 -0
  56. package/lib/loader.sh +6 -8
  57. package/lib/metadata.sh +111 -5
  58. package/manifests/parser-support.tsv +2 -2
  59. package/module.d/valise_editor.meta +9 -0
  60. package/module.d/valise_history.meta +9 -0
  61. package/module.d/valise_pager.meta +9 -0
  62. package/module.d/valise_path.meta +9 -0
  63. package/module.d/{platform_bsd.meta → valise_platform_bsd.meta} +3 -4
  64. package/module.d/{platform_linux.meta → valise_platform_linux.meta} +3 -4
  65. package/module.d/{platform_macos.meta → valise_platform_macos.meta} +3 -4
  66. package/module.d/{platform_posix.meta → valise_platform_posix.meta} +2 -3
  67. package/module.d/valise_xdg.meta +9 -0
  68. package/modules/system/{contrib_editor.sh → valise_editor.sh} +2 -2
  69. package/modules/system/{contrib_history.sh → valise_history.sh} +2 -2
  70. package/modules/system/{contrib_pager.sh → valise_pager.sh} +2 -2
  71. package/modules/system/{contrib_path.sh → valise_path.sh} +3 -3
  72. package/modules/system/{contrib_xdg.sh → valise_xdg.sh} +2 -2
  73. package/package.json +91 -10
  74. package/docs/variants.md +0 -327
  75. package/entrypoints/.gitkeep +0 -0
  76. package/entrypoints/fish/profile.fish +0 -99
  77. package/examples/e2e/build-manifest.sh +0 -138
  78. package/examples/local/.gitkeep +0 -0
  79. package/examples/profile-project/shell/release/bundle-roots +0 -9
  80. package/examples/profile-project/shell/release/install-variants +0 -3
  81. package/examples/profile-project/shell/release/module-registry.tsv +0 -4
  82. package/examples/profile-project/shell/release/project.conf +0 -3
  83. package/lib/.gitkeep +0 -0
  84. package/manifests/.gitkeep +0 -0
  85. package/module.d/.gitkeep +0 -0
  86. package/module.d/contrib_editor.meta +0 -10
  87. package/module.d/contrib_history.meta +0 -10
  88. package/module.d/contrib_pager.meta +0 -10
  89. package/module.d/contrib_path.meta +0 -10
  90. package/module.d/contrib_xdg.meta +0 -10
  91. package/modules/.gitkeep +0 -0
  92. /package/examples/profile-project/{shell/layers → layers}/demo/modules/shared/contrib_demo_greeting.sh +0 -0
@@ -666,6 +666,12 @@ sp_cap_write_compromise_paths() (
666
666
  tree:0)
667
667
  command -p find "$_spc_path" \( -type l -o ! -user "$_spc_uid" -o \( ! -type l -a \( -perm -020 -o -perm -002 \) \) \) -print
668
668
  ;;
669
+ tree-owned:1)
670
+ command -p find "$_spc_path" \( ! -user "$_spc_uid" -o \( ! -type l -a \( \( -perm -020 ! -group "$_spc_group" \) -o -perm -002 \) \) \) -print
671
+ ;;
672
+ tree-owned:0)
673
+ command -p find "$_spc_path" \( ! -user "$_spc_uid" -o \( ! -type l -a \( -perm -020 -o -perm -002 \) \) \) -print
674
+ ;;
669
675
  *) exit 1 ;;
670
676
  esac
671
677
  ' sh "$_spc_path" "$_spc_scope"
@@ -689,71 +695,92 @@ sp_cap_write_compromise_paths() (
689
695
  tree:0)
690
696
  command -p find "$_spc_path" \( -type l -o ! -user "$_spc_uid" -o \( ! -type l -a \( -perm -020 -o -perm -002 \) \) \) -print
691
697
  ;;
698
+ tree-owned:1)
699
+ command -p find "$_spc_path" \( ! -user "$_spc_uid" -o \( ! -type l -a \( \( -perm -020 ! -group "$_spc_group" \) -o -perm -002 \) \) \) -print
700
+ ;;
701
+ tree-owned:0)
702
+ command -p find "$_spc_path" \( ! -user "$_spc_uid" -o \( ! -type l -a \( -perm -020 -o -perm -002 \) \) \) -print
703
+ ;;
692
704
  *) exit 1 ;;
693
705
  esac
694
706
  fi
695
707
  )
696
708
 
709
+ # sp_cap_secure_local_bin_check <home> [reject|allow-owned-symlinks]
710
+ # The default rejects every symlink in ~/.local/bin. allow-owned-symlinks
711
+ # accepts a symlink whose link entry is owned by the current user, which is
712
+ # the shape pipx and uv install; unowned entries and group/other-writable
713
+ # regular files are still rejected.
697
714
  sp_cap_secure_local_bin_check() {
698
- [ "$#" -eq 1 ] && [ -n "$1" ] || return 1
715
+ [ "$#" -ge 1 ] && [ "$#" -le 2 ] && [ -n "$1" ] || return 1
699
716
  _spc_home=$1
717
+ _spc_symlink_policy=${2:-reject}
718
+ case "$_spc_symlink_policy" in
719
+ reject) _spc_bin_scope=tree ;;
720
+ allow-owned-symlinks) _spc_bin_scope=tree-owned ;;
721
+ *)
722
+ sp_cap_error "unknown symlink policy: $_spc_symlink_policy"
723
+ unset _spc_home _spc_symlink_policy
724
+ return 1
725
+ ;;
726
+ esac
700
727
  case "$_spc_home" in
701
728
  /*) ;;
702
729
  *)
703
730
  sp_cap_error 'HOME is not an absolute path'
704
- unset _spc_home
731
+ unset _spc_home _spc_symlink_policy _spc_bin_scope
705
732
  return 1
706
733
  ;;
707
734
  esac
708
735
  [ -d "$_spc_home" ] && [ ! -L "$_spc_home" ] || {
709
736
  sp_cap_error "HOME is not a real directory: $_spc_home"
710
- unset _spc_home
737
+ unset _spc_home _spc_symlink_policy _spc_bin_scope
711
738
  return 1
712
739
  }
713
740
  _spc_local_dir=$_spc_home/.local
714
741
  _spc_local_bin=$_spc_local_dir/bin
715
742
  _spc_home_bad=$(sp_cap_write_compromise_paths "$_spc_home" self) || {
716
743
  sp_cap_error "failed to inspect HOME permissions: $_spc_home"
717
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad
744
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad
718
745
  return 1
719
746
  }
720
747
  if [ -n "$_spc_home_bad" ]; then
721
748
  sp_cap_error "HOME violates ownership, symlink, or external-write policy: $_spc_home"
722
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad
749
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad
723
750
  return 1
724
751
  fi
725
752
  [ -d "$_spc_local_dir" ] && [ ! -L "$_spc_local_dir" ] || {
726
753
  sp_cap_error "local directory is not a real directory: $_spc_local_dir"
727
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad
754
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad
728
755
  return 1
729
756
  }
730
757
  [ -d "$_spc_local_bin" ] && [ ! -L "$_spc_local_bin" ] || {
731
758
  sp_cap_error "local bin directory is not a real directory: $_spc_local_bin"
732
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad
759
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad
733
760
  return 1
734
761
  }
735
762
  _spc_local_bad=$(sp_cap_write_compromise_paths "$_spc_local_dir" self) || {
736
763
  sp_cap_error "failed to inspect local directory permissions: $_spc_local_dir"
737
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad
764
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad
738
765
  return 1
739
766
  }
740
767
  if [ -n "$_spc_local_bad" ]; then
741
768
  sp_cap_error "local directory violates ownership, symlink, or external-write policy: $_spc_local_dir"
742
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad
769
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad
743
770
  return 1
744
771
  fi
745
- _spc_bin_bad=$(sp_cap_write_compromise_paths "$_spc_local_bin" tree) || {
772
+ _spc_bin_bad=$(sp_cap_write_compromise_paths "$_spc_local_bin" "$_spc_bin_scope") || {
746
773
  sp_cap_error "failed to inspect local bin execution chain: $_spc_local_bin"
747
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad _spc_bin_bad
774
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad _spc_bin_bad
748
775
  return 1
749
776
  }
750
777
  if [ -n "$_spc_bin_bad" ]; then
751
778
  sp_cap_error "Compromised execution chain. Writable/unowned files or symlinks found in '$_spc_local_bin'."
752
779
  sp_cap_print_line "$_spc_bin_bad" >&2
753
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad _spc_bin_bad
780
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad _spc_bin_bad
754
781
  return 1
755
782
  fi
756
- unset _spc_home _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad _spc_bin_bad
783
+ unset _spc_home _spc_symlink_policy _spc_bin_scope _spc_local_dir _spc_local_bin _spc_home_bad _spc_local_bad _spc_bin_bad
757
784
  return 0
758
785
  }
759
786
 
@@ -0,0 +1,278 @@
1
+ # Copyright (c) 2026 Joseph Cumines
2
+ # SPDX-License-Identifier: MIT
3
+ # valise CLI: published commands: the stubs that put an active release's
4
+ # bin/ commands on PATH, and the ledger that records which stubs valise owns.
5
+ # Sourced by bin/valise; not executable on its own.
6
+ #
7
+ # A stub is a tiny sh script in the command directory that execs the same
8
+ # name under the active release link, so a release switch never rewrites a
9
+ # stub; it only adds and removes them. The ledger lists every stub valise
10
+ # wrote as name, path, and SHA-256. A file valise did not write, or a stub
11
+ # someone edited, is never overwritten or removed.
12
+
13
+ _rc_tmp=""
14
+ _rc_stub=""
15
+ _rc_pub=""
16
+
17
+ # commands_bin_dir: the directory stubs are written to.
18
+ commands_bin_dir() {
19
+ [ -n "${VALISE_BIN_DIR:-}${XDG_BIN_HOME:-}${HOME:-}" ] || return 1
20
+ path_normalize "${VALISE_BIN_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}"
21
+ }
22
+
23
+ commands_ledger_path() {
24
+ printf '%s\n' "$VALISE_XDG_STATE/commands.tsv"
25
+ }
26
+
27
+ # commands_stub <name>: print the stub for one command.
28
+ commands_stub() {
29
+ _cs_target=$(shell_quote "$VALISE_ACTIVE_LINK/bin/$1") || return 1
30
+ printf '%s\n' \
31
+ '#!/bin/sh' \
32
+ '# valise command stub; managed by valise; do not edit.' \
33
+ "exec $_cs_target \"\$@\""
34
+ unset _cs_target
35
+ }
36
+
37
+ # commands_stub_digest <name>: print the SHA-256 of the stub for one command.
38
+ commands_stub_digest() {
39
+ _csd_tmp=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-stub.XXXXXX") || return 1
40
+ if commands_stub "$1" >"$_csd_tmp"; then
41
+ file_sha256 "$_csd_tmp"
42
+ _csd_status=$?
43
+ else
44
+ _csd_status=1
45
+ fi
46
+ _csd_rm=$(trusted_utility_path rm) || return 1
47
+ "$_csd_rm" -f "$_csd_tmp" || _csd_status=1
48
+ return "$_csd_status"
49
+ }
50
+
51
+ # commands_file_is <path> <sha256>: succeed when path is a regular file, not a
52
+ # symlink, with the given digest.
53
+ commands_file_is() {
54
+ [ -f "$1" ] && [ ! -L "$1" ] || return 1
55
+ _cfi_digest=$(file_sha256 "$1") || return 1
56
+ [ "$_cfi_digest" = "$2" ]
57
+ }
58
+
59
+ # command_name_valid <name>: the build's rule for a published command name.
60
+ # It keeps a stub inside the command directory and free of glob characters.
61
+ command_name_valid() {
62
+ case $1 in
63
+ '' | [!A-Za-z0-9_]* | *[!A-Za-z0-9_.-]*) return 1 ;;
64
+ esac
65
+ return 0
66
+ }
67
+
68
+ # release_commands <release-dir>: print the commands a release publishes.
69
+ # Fails, naming the entry, when an installed release lists a name the build
70
+ # would have rejected, so no caller ever joins such a name to a path.
71
+ release_commands() {
72
+ _rcs_list=$(release_conf_value "$1" commands 2>/dev/null || :)
73
+ _rcs_seen=" "
74
+ _rcs_noglob=0
75
+ case $- in *f*) _rcs_noglob=1 ;; esac
76
+ set -f
77
+ for _rcs_name in $_rcs_list; do
78
+ if ! command_name_valid "$_rcs_name"; then
79
+ [ "$_rcs_noglob" -eq 1 ] || set +f
80
+ printf 'valise: error: release lists an invalid command name: %s\n' "$_rcs_name" >&2
81
+ unset _rcs_list _rcs_seen _rcs_noglob _rcs_name
82
+ return 1
83
+ fi
84
+ case $_rcs_seen in
85
+ *" $_rcs_name "*) ;;
86
+ *) _rcs_seen="$_rcs_seen$_rcs_name " ;;
87
+ esac
88
+ done
89
+ [ "$_rcs_noglob" -eq 1 ] || set +f
90
+ _rcs_seen=${_rcs_seen# }
91
+ printf '%s\n' "${_rcs_seen% }"
92
+ unset _rcs_list _rcs_seen _rcs_noglob _rcs_name
93
+ }
94
+
95
+ # commands_publish_ledger: publish the staged ledger rows as the ledger.
96
+ commands_publish_ledger() {
97
+ _rc_pub=$(trusted_mktemp_at "$VALISE_XDG_STATE" file "$VALISE_XDG_STATE/.commands.XXXXXX") ||
98
+ die "cannot create command ledger staging file"
99
+ "$_rcm_cat" "$_rc_tmp" >"$_rc_pub" || die "cannot write command ledger"
100
+ "$_rcm_chmod" 0644 "$_rc_pub" || die "cannot set command ledger mode"
101
+ "$_rcm_mv" -f "$_rc_pub" "$_rcm_ledger" || die "cannot publish command ledger"
102
+ _rc_pub=""
103
+ }
104
+
105
+ # reconcile_commands <release-dir>
106
+ # Make the stubs in the command directory match the commands the release
107
+ # publishes. Owned stubs for commands the release no longer has are removed;
108
+ # a missing stub is written unless the path holds a file valise does not own,
109
+ # which is left alone and reported. Call with the store lock held.
110
+ reconcile_commands() {
111
+ _rcm_release=$1
112
+ _rcm_want=$(release_commands "$_rcm_release") || die "refusing to publish the commands of $_rcm_release"
113
+ if ! _rcm_bin=$(commands_bin_dir); then
114
+ [ -z "$_rcm_want" ] || info "warning: HOME is not set; no commands were published"
115
+ unset _rcm_release _rcm_want _rcm_bin
116
+ return 0
117
+ fi
118
+ _rcm_ledger=$(commands_ledger_path)
119
+ _rcm_tab=$(printf '\t')
120
+ _rcm_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
121
+ _rcm_mkdir=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
122
+ _rcm_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
123
+ _rcm_ln=$(trusted_utility_path ln) || die "no trusted ln utility available"
124
+ _rcm_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
125
+ _rcm_cat=$(trusted_utility_path cat) || die "no trusted cat utility available"
126
+ _rcm_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
127
+ [ ! -L "$_rcm_ledger" ] || die "command ledger must not be a symlink: $_rcm_ledger"
128
+ "$_rcm_mkdir" -p "$VALISE_XDG_STATE" || die "cannot create state directory: $VALISE_XDG_STATE"
129
+ _rc_tmp=$(trusted_mktemp_at "$VALISE_XDG_STATE" file "$VALISE_XDG_STATE/.commands.XXXXXX") ||
130
+ die "cannot create command ledger staging file"
131
+ _rcm_owned=
132
+
133
+ # Drop or keep each stub the ledger records.
134
+ if [ -f "$_rcm_ledger" ]; then
135
+ while IFS="$_rcm_tab" read -r _rcm_name _rcm_path _rcm_sha; do
136
+ [ -n "$_rcm_name" ] || continue
137
+ case " $_rcm_want " in
138
+ *" $_rcm_name "*) _rcm_keep=1 ;;
139
+ *) _rcm_keep=0 ;;
140
+ esac
141
+ [ "$_rcm_path" = "$_rcm_bin/$_rcm_name" ] || _rcm_keep=0
142
+ if [ ! -e "$_rcm_path" ] && [ ! -L "$_rcm_path" ]; then
143
+ # The stub is gone; it is written again below if still wanted.
144
+ continue
145
+ fi
146
+ if ! commands_file_is "$_rcm_path" "$_rcm_sha"; then
147
+ info "warning: command stub was changed outside valise; leaving it and no longer managing it: $_rcm_path"
148
+ continue
149
+ fi
150
+ if [ "$_rcm_keep" -eq 1 ]; then
151
+ printf '%s\t%s\t%s\n' "$_rcm_name" "$_rcm_path" "$_rcm_sha" >>"$_rc_tmp" ||
152
+ die "cannot write command ledger"
153
+ _rcm_owned="$_rcm_owned $_rcm_name"
154
+ else
155
+ "$_rcm_rm" -f "$_rcm_path" || die "cannot remove command stub: $_rcm_path"
156
+ info "removed command: $_rcm_path"
157
+ fi
158
+ done <"$_rcm_ledger"
159
+ elif [ -e "$_rcm_ledger" ]; then
160
+ die "command ledger is not a regular file: $_rcm_ledger"
161
+ fi
162
+
163
+ # Write each wanted stub valise does not hold yet.
164
+ for _rcm_name in $_rcm_want; do
165
+ case " $_rcm_owned " in *" $_rcm_name "*) continue ;; esac
166
+ _rcm_path="$_rcm_bin/$_rcm_name"
167
+ if [ -e "$_rcm_path" ] || [ -L "$_rcm_path" ]; then
168
+ info "warning: command $_rcm_name not published: $_rcm_path exists and valise does not own it"
169
+ continue
170
+ fi
171
+ "$_rcm_mkdir" -p "$_rcm_bin" || die "cannot create command directory: $_rcm_bin"
172
+ _rc_stub=$(trusted_mktemp_at "$_rcm_bin" file "$_rcm_bin/.valise-stub.XXXXXX") ||
173
+ die "cannot create command stub staging file"
174
+ commands_stub "$_rcm_name" >"$_rc_stub" || die "cannot write command stub: $_rcm_name"
175
+ "$_rcm_chmod" 0755 "$_rc_stub" || die "cannot set command stub mode"
176
+ _rcm_sha=$(file_sha256 "$_rc_stub") || die "no trusted SHA-256 utility available"
177
+ # The ledger names the stub before the stub exists, so an interrupted run
178
+ # never leaves a stub valise does not know it owns; a recorded stub that
179
+ # is missing is simply written again later.
180
+ printf '%s\t%s\t%s\n' "$_rcm_name" "$_rcm_path" "$_rcm_sha" >>"$_rc_tmp" ||
181
+ die "cannot write command ledger"
182
+ commands_publish_ledger
183
+ # A hard link fails when the name exists, so a file created since the
184
+ # check above is never replaced.
185
+ if "$_rcm_ln" "$_rc_stub" "$_rcm_path" 2>/dev/null; then
186
+ info "published command: $_rcm_path"
187
+ else
188
+ info "warning: command $_rcm_name not published: cannot create $_rcm_path"
189
+ _rc_pub=$(trusted_mktemp_at "$VALISE_XDG_STATE" file "$VALISE_XDG_STATE/.commands.XXXXXX") ||
190
+ die "cannot create command ledger staging file"
191
+ "$_rcm_awk" -F '\t' -v n="$_rcm_name" -v p="$_rcm_path" '!($1 == n && $2 == p)' "$_rc_tmp" >"$_rc_pub" &&
192
+ "$_rcm_mv" -f "$_rc_pub" "$_rc_tmp" || die "cannot write command ledger"
193
+ _rc_pub=""
194
+ commands_publish_ledger
195
+ fi
196
+ "$_rcm_rm" -f "$_rc_stub" || die "cannot remove command stub staging file"
197
+ _rc_stub=""
198
+ done
199
+
200
+ "$_rcm_chmod" 0644 "$_rc_tmp" || die "cannot set command ledger mode"
201
+ "$_rcm_mv" -f "$_rc_tmp" "$_rcm_ledger" || die "cannot publish command ledger"
202
+ _rc_tmp=""
203
+ if [ -n "$_rcm_want" ]; then
204
+ case ":${PATH:-}:" in
205
+ *":$_rcm_bin:"*) ;;
206
+ *) info "warning: $_rcm_bin is not on PATH; add it to use the published commands" ;;
207
+ esac
208
+ fi
209
+ unset _rcm_release _rcm_bin _rcm_ledger _rcm_want _rcm_tab _rcm_rm _rcm_mkdir _rcm_chmod _rcm_ln _rcm_mv \
210
+ _rcm_cat _rcm_awk _rcm_owned _rcm_name _rcm_path _rcm_sha _rcm_keep
211
+ }
212
+
213
+ # remove_owned_commands: remove every stub the ledger records that still has
214
+ # the content valise wrote, then the ledger. Call with the store lock held.
215
+ remove_owned_commands() {
216
+ _roc_ledger=$(commands_ledger_path)
217
+ [ -e "$_roc_ledger" ] || [ -L "$_roc_ledger" ] || return 0
218
+ [ -f "$_roc_ledger" ] && [ ! -L "$_roc_ledger" ] || die "command ledger is not a regular file: $_roc_ledger"
219
+ _roc_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
220
+ _roc_tab=$(printf '\t')
221
+ while IFS="$_roc_tab" read -r _roc_name _roc_path _roc_sha; do
222
+ [ -n "$_roc_name" ] || continue
223
+ [ -e "$_roc_path" ] || [ -L "$_roc_path" ] || continue
224
+ if commands_file_is "$_roc_path" "$_roc_sha"; then
225
+ "$_roc_rm" -f "$_roc_path" || die "cannot remove command stub: $_roc_path"
226
+ info "removed command: $_roc_path"
227
+ else
228
+ info "warning: command stub was changed outside valise; leaving it: $_roc_path"
229
+ fi
230
+ done <"$_roc_ledger"
231
+ "$_roc_rm" -f "$_roc_ledger" || die "cannot remove command ledger: $_roc_ledger"
232
+ unset _roc_ledger _roc_rm _roc_tab _roc_name _roc_path _roc_sha
233
+ }
234
+
235
+ # doctor_commands <release-dir>
236
+ # Report the state of each command the release publishes. Prints doctor
237
+ # lines; sets _status=1 when a published command cannot run.
238
+ doctor_commands() {
239
+ if ! _dc_want=$(release_commands "$1"); then
240
+ printf ' FAIL: the active release lists an invalid command name\n'
241
+ _status=1
242
+ return 0
243
+ fi
244
+ [ -n "$_dc_want" ] || return 0
245
+ _dc_bin=$(commands_bin_dir) || {
246
+ printf ' FAIL: cannot resolve the command directory\n'
247
+ _status=1
248
+ return 0
249
+ }
250
+ _dc_ledger=$(commands_ledger_path)
251
+ if [ ! -f "$_dc_ledger" ] || [ -L "$_dc_ledger" ]; then
252
+ printf ' info: release commands are not published (installed without integration): %s\n' "$_dc_want"
253
+ unset _dc_want _dc_bin _dc_ledger
254
+ return 0
255
+ fi
256
+ _dc_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
257
+ for _dc_name in $_dc_want; do
258
+ _dc_path="$_dc_bin/$_dc_name"
259
+ _dc_sha=$("$_dc_awk" -F '\t' -v n="$_dc_name" -v p="$_dc_path" '$1 == n && $2 == p { print $3; exit }' "$_dc_ledger")
260
+ if [ -z "$_dc_sha" ]; then
261
+ if [ -e "$_dc_path" ] || [ -L "$_dc_path" ]; then
262
+ printf ' warn: command %s not published: %s exists and valise does not own it\n' "$_dc_name" "$_dc_path"
263
+ else
264
+ printf ' warn: command %s not published (run valise install or valise release use)\n' "$_dc_name"
265
+ fi
266
+ elif commands_file_is "$_dc_path" "$_dc_sha"; then
267
+ printf ' ok: command published: %s\n' "$_dc_path"
268
+ else
269
+ printf ' FAIL: command stub missing or changed: %s\n' "$_dc_path"
270
+ _status=1
271
+ fi
272
+ done
273
+ case ":${PATH:-}:" in
274
+ *":$_dc_bin:"*) printf ' ok: command directory is on PATH: %s\n' "$_dc_bin" ;;
275
+ *) printf ' warn: command directory is not on PATH: %s\n' "$_dc_bin" ;;
276
+ esac
277
+ unset _dc_want _dc_bin _dc_ledger _dc_awk _dc_name _dc_path _dc_sha
278
+ }