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
@@ -0,0 +1,326 @@
1
+ # Copyright (c) 2026 Joseph Cumines
2
+ # SPDX-License-Identifier: MIT
3
+ # valise CLI: shared helpers, XDG roots, trusted utilities, file probes.
4
+ # Sourced by bin/valise; not executable on its own.
5
+
6
+ die() {
7
+ printf 'valise: error: %s\n' "$*" >&2
8
+ exit 1
9
+ }
10
+
11
+ info() {
12
+ printf 'valise: %s\n' "$*"
13
+ }
14
+
15
+ # Verification failure reporting. The default mode aborts the command, which
16
+ # is what install and release use. Quiet mode returns a nonzero status
17
+ # instead, so a diagnostic command can report the failure and carry on. The
18
+ # message text is identical in both modes.
19
+ _verify_quiet=0
20
+ _verify_fail() {
21
+ if [ "$_verify_quiet" -eq 1 ]; then
22
+ printf 'valise: error: %s\n' "$*" >&2
23
+ return 1
24
+ fi
25
+ die "$*"
26
+ }
27
+
28
+ # Lexically normalize an absolute path: collapse repeated slashes, drop "."
29
+ # segments, and resolve ".." against the preceding component. Relative paths
30
+ # are resolved against the physical working directory first.
31
+ path_normalize() {
32
+ _path_norm_value=$1
33
+ case "$_path_norm_value" in
34
+ /*) ;;
35
+ *) _path_norm_value=$(pwd -P)/$_path_norm_value ;;
36
+ esac
37
+ _path_norm_out=
38
+ if [ "${IFS+x}" = x ]; then
39
+ _path_norm_ifs_was_set=1
40
+ _path_norm_old_ifs=${IFS-}
41
+ else
42
+ _path_norm_ifs_was_set=0
43
+ _path_norm_old_ifs=
44
+ fi
45
+ case $- in
46
+ *f*) _path_norm_glob_was_set=1 ;;
47
+ *) _path_norm_glob_was_set=0 ;;
48
+ esac
49
+ set -f
50
+ IFS=/
51
+ for _path_norm_part in $_path_norm_value; do
52
+ case "$_path_norm_part" in
53
+ ''|.) continue ;;
54
+ ..)
55
+ case "$_path_norm_out" in
56
+ '') ;;
57
+ */*) _path_norm_out=${_path_norm_out%/*} ;;
58
+ *) _path_norm_out= ;;
59
+ esac
60
+ continue
61
+ ;;
62
+ esac
63
+ _path_norm_out=${_path_norm_out:+$_path_norm_out/}$_path_norm_part
64
+ done
65
+ if [ "$_path_norm_ifs_was_set" -eq 1 ]; then
66
+ IFS=$_path_norm_old_ifs
67
+ else
68
+ unset IFS
69
+ fi
70
+ if [ "$_path_norm_glob_was_set" -eq 0 ]; then
71
+ set +f
72
+ fi
73
+ printf '/%s\n' "$_path_norm_out"
74
+ }
75
+
76
+ # XDG defaults per install-contracts.md.
77
+ #
78
+ # The roots are normalized to absolute paths. A relative XDG_DATA_HOME would
79
+ # otherwise be written into the active symlink verbatim, and a relative
80
+ # symlink target resolves against the directory holding the link rather
81
+ # than the caller's working directory. The pointer would then dangle, and
82
+ # prune would compare the active release by a path spelling that never
83
+ # matches the release directory.
84
+ VALISE_XDG_DATA=$(path_normalize "${XDG_DATA_HOME:-$HOME/.local/share}/valise")
85
+ VALISE_XDG_CONFIG=$(path_normalize "${XDG_CONFIG_HOME:-$HOME/.config}/valise")
86
+ VALISE_XDG_STATE=$(path_normalize "${XDG_STATE_HOME:-$HOME/.local/state}/valise")
87
+ VALISE_RELEASES_DIR="$VALISE_XDG_DATA/releases"
88
+ VALISE_ACTIVE_LINK="$VALISE_XDG_CONFIG/active"
89
+ VALISE_LOCK_DIR="$VALISE_XDG_STATE/lock"
90
+
91
+ # POSIX path parent extraction avoids ambient dirname resolution in manifest
92
+ # discovery and every subsequent source/destination validation path.
93
+ path_parent() {
94
+ _path_parent_value=$1
95
+ # Strip trailing slashes so "out/" and "out" share one parent (".").
96
+ # Without this, path_parent("out/") is "out" itself: parent-mkdir creates
97
+ # the destination, the emptiness check treats it as pre-created-empty, and
98
+ # staging lands inside the destination (publish then fails ENOTEMPTY).
99
+ while [ "$_path_parent_value" != "/" ] &&
100
+ [ "${_path_parent_value%/}" != "$_path_parent_value" ]; do
101
+ _path_parent_value=${_path_parent_value%/}
102
+ done
103
+ case "$_path_parent_value" in
104
+ */*)
105
+ _path_parent_value=${_path_parent_value%/*}
106
+ [ -n "$_path_parent_value" ] || _path_parent_value=/
107
+ ;;
108
+ *) _path_parent_value=. ;;
109
+ esac
110
+ printf '%s\n' "$_path_parent_value"
111
+ }
112
+
113
+ # Physical directory of bin/valise, resolved once by bin/valise before any
114
+ # library is sourced.
115
+ valise_script_dir() {
116
+ printf '%s\n' "$_valise_engine_bin"
117
+ }
118
+
119
+ # Emit a single-quoted literal for generated POSIX shell code. The generated
120
+ # snippets must not interpolate user-controlled XDG paths into double quotes:
121
+ # `$()` and backticks would still execute there. POSIX single quotes preserve
122
+ # every character except apostrophe, which is represented by the standard
123
+ # '\'' sequence.
124
+ shell_quote() {
125
+ _shell_quote_value=$1
126
+ printf "'"
127
+ while [ -n "$_shell_quote_value" ]; do
128
+ _shell_quote_char=${_shell_quote_value%"${_shell_quote_value#?}"}
129
+ _shell_quote_value=${_shell_quote_value#?}
130
+ case "$_shell_quote_char" in
131
+ "'") printf "%s" "'\\''" ;;
132
+ *) printf '%s' "$_shell_quote_char" ;;
133
+ esac
134
+ done
135
+ printf "'"
136
+ }
137
+
138
+ # Fish single-quoted literals use a backslash-escaped apostrophe. Encode each
139
+ # source backslash as two backslashes so a backslash immediately before an
140
+ # apostrophe cannot change the quote grammar. Dollar signs, semicolons, and
141
+ # command-substitution markers remain literal, so arbitrary XDG paths cannot
142
+ # inject Fish commands.
143
+ fish_quote() {
144
+ _fish_quote_value=$1
145
+ printf "'"
146
+ while [ -n "$_fish_quote_value" ]; do
147
+ _fish_quote_char=${_fish_quote_value%"${_fish_quote_value#?}"}
148
+ _fish_quote_value=${_fish_quote_value#?}
149
+ case "$_fish_quote_char" in
150
+ \\) printf '%s' '\\' ;;
151
+ "'") printf '%s' "\\'" ;;
152
+ *) printf '%s' "$_fish_quote_char" ;;
153
+ esac
154
+ done
155
+ printf "'"
156
+ }
157
+
158
+ trusted_uname() {
159
+ if [ -x /usr/bin/uname ]; then
160
+ /usr/bin/uname "$@"
161
+ elif [ -x /bin/uname ]; then
162
+ /bin/uname "$@"
163
+ else
164
+ return 1
165
+ fi
166
+ }
167
+
168
+ # Resolve only platform-owned utility paths for integrity operations. These
169
+ # checks must not be redirected by a caller-controlled PATH.
170
+ trusted_utility_path() {
171
+ case "$1" in
172
+ awk) _trusted_candidates='/usr/bin/awk /bin/awk' ;;
173
+ cat) _trusted_candidates='/bin/cat /usr/bin/cat' ;;
174
+ cp) _trusted_candidates='/bin/cp /usr/bin/cp' ;;
175
+ chmod) _trusted_candidates='/bin/chmod /usr/bin/chmod' ;;
176
+ cmp) _trusted_candidates='/usr/bin/cmp /bin/cmp' ;;
177
+ ln) _trusted_candidates='/bin/ln /usr/bin/ln' ;;
178
+ mkdir) _trusted_candidates='/bin/mkdir /usr/bin/mkdir' ;;
179
+ kill) _trusted_candidates='/bin/kill /usr/bin/kill' ;;
180
+ mktemp) _trusted_candidates='/usr/bin/mktemp /bin/mktemp /opt/homebrew/bin/mktemp /usr/local/bin/mktemp' ;;
181
+ readlink) _trusted_candidates='/usr/bin/readlink /bin/readlink' ;;
182
+ rmdir) _trusted_candidates='/bin/rmdir /usr/bin/rmdir' ;;
183
+ find) _trusted_candidates='/usr/bin/find /bin/find' ;;
184
+ fish) _trusted_candidates='/opt/homebrew/bin/fish /usr/local/bin/fish /usr/bin/fish /bin/fish' ;;
185
+ mv) _trusted_candidates='/bin/mv /usr/bin/mv' ;;
186
+ rm) _trusted_candidates='/bin/rm /usr/bin/rm' ;;
187
+ stat) _trusted_candidates='/usr/bin/stat /bin/stat' ;;
188
+ tail) _trusted_candidates='/usr/bin/tail /bin/tail' ;;
189
+ tr) _trusted_candidates='/usr/bin/tr /bin/tr' ;;
190
+ wc) _trusted_candidates='/usr/bin/wc /bin/wc' ;;
191
+ sha256sum) _trusted_candidates='/usr/bin/sha256sum /bin/sha256sum /sbin/sha256sum /opt/homebrew/bin/sha256sum /usr/local/bin/sha256sum' ;;
192
+ sha256) _trusted_candidates='/usr/bin/sha256 /bin/sha256 /sbin/sha256 /opt/homebrew/bin/sha256 /usr/local/bin/sha256' ;;
193
+ shasum) _trusted_candidates='/usr/bin/shasum /bin/shasum /opt/homebrew/bin/shasum /usr/local/bin/shasum' ;;
194
+ sort) _trusted_candidates='/usr/bin/sort /bin/sort' ;;
195
+ *) return 1 ;;
196
+ esac
197
+ for _trusted_candidate in $_trusted_candidates; do
198
+ if [ -x "$_trusted_candidate" ]; then
199
+ printf '%s\n' "$_trusted_candidate"
200
+ return 0
201
+ fi
202
+ done
203
+ return 1
204
+ }
205
+
206
+ readlink_target() {
207
+ _readlink_path=$(trusted_utility_path readlink) || return 1
208
+ "$_readlink_path" "$1"
209
+ }
210
+
211
+ # active_release_dir
212
+ # Print the physical directory the active pointer names, or fail. The result
213
+ # is canonical, so a trailing slash, a relative target, or an alias symlink
214
+ # never changes the release identity.
215
+ active_release_dir() {
216
+ [ -L "$VALISE_ACTIVE_LINK" ] || return 1
217
+ _ard_target=$(readlink_target "$VALISE_ACTIVE_LINK" 2>/dev/null) || return 1
218
+ [ -n "$_ard_target" ] || return 1
219
+ case "$_ard_target" in
220
+ /*) : ;;
221
+ *)
222
+ _ard_parent=$(cd "$(path_parent "$VALISE_ACTIVE_LINK")" 2>/dev/null && pwd -P) || return 1
223
+ _ard_target="$_ard_parent/$_ard_target"
224
+ ;;
225
+ esac
226
+ _ard_target=$(path_normalize "$_ard_target") || return 1
227
+ (cd "$_ard_target" 2>/dev/null && pwd -P)
228
+ }
229
+
230
+ # active_release_id
231
+ # Print the active release's directory name. Identity is the name, not the
232
+ # path spelling.
233
+ active_release_id() {
234
+ _ari_dir=$(active_release_dir) || return 1
235
+ printf '%s\n' "${_ari_dir##*/}"
236
+ }
237
+
238
+ # Print the canonical root of the engine checkout or package that ships this
239
+ # CLI. Its modules live in module.d/, its libraries in lib/.
240
+ engine_root() {
241
+ (cd "${_valise_engine_bin%/}/.." && pwd -P)
242
+ }
243
+
244
+ VALISE_TAB=$(printf '\t')
245
+ VALISE_NL='
246
+ '
247
+
248
+ # Return a file's three-digit permission mode in the manifest's octal form.
249
+ # BSD stat and GNU stat use different option spellings, so probe both.
250
+ file_mode() {
251
+ _stat_path=$(trusted_utility_path stat) || return 1
252
+ # BSD stat accepts -f; GNU stat accepts -c. GNU stat may nevertheless
253
+ # accept -f as a filesystem-stat format and exit successfully, so select a
254
+ # result by validating the output rather than relying on exit status alone.
255
+ _mode_value=$("$_stat_path" -f '%Lp' "$1" 2>/dev/null || :)
256
+ case "$_mode_value" in
257
+ [0-7][0-7][0-7]) printf '0%s\n' "$_mode_value"; return 0 ;;
258
+ esac
259
+ _mode_value=$("$_stat_path" -c '%a' "$1" 2>/dev/null || :) || return 1
260
+ case "$_mode_value" in
261
+ [0-7][0-7][0-7]) printf '0%s\n' "$_mode_value" ;;
262
+ *) return 1 ;;
263
+ esac
264
+ }
265
+
266
+ # Return a regular file's exact byte count without locale-dependent padding.
267
+ file_bytes() {
268
+ _wc_path=$(trusted_utility_path wc) || return 1
269
+ _tr_path=$(trusted_utility_path tr) || return 1
270
+ "$_wc_path" -c < "$1" | "$_tr_path" -d '[:space:]'
271
+ }
272
+
273
+ # Return a file's modification time in whole epoch seconds. BSD stat and GNU
274
+ # stat use different option spellings, so probe both and select the result by
275
+ # validating the output rather than trusting exit status alone: GNU stat
276
+ # accepts -f as a filesystem-status flag and can exit successfully while
277
+ # printing something that is not a timestamp.
278
+ file_mtime() {
279
+ _mtime_stat=$(trusted_utility_path stat) || return 1
280
+ _mtime_value=$("$_mtime_stat" -c %Y "$1" 2>/dev/null || :)
281
+ case "$_mtime_value" in
282
+ '' | *[!0-9]*) ;;
283
+ *) printf '%s\n' "$_mtime_value"; return 0 ;;
284
+ esac
285
+ _mtime_value=$("$_mtime_stat" -f %m "$1" 2>/dev/null || :)
286
+ case "$_mtime_value" in
287
+ '' | *[!0-9]*) return 1 ;;
288
+ *) printf '%s\n' "$_mtime_value" ;;
289
+ esac
290
+ }
291
+
292
+ file_sha256() {
293
+ _sha_path=$(trusted_utility_path sha256sum 2>/dev/null || :)
294
+ if [ -n "$_sha_path" ]; then
295
+ _sha_output=$("$_sha_path" "$1") || return 1
296
+ else
297
+ _sha_path=$(trusted_utility_path shasum 2>/dev/null || :)
298
+ if [ -n "$_sha_path" ]; then
299
+ _sha_output=$("$_sha_path" -a 256 "$1") || return 1
300
+ else
301
+ _sha_path=$(trusted_utility_path sha256 2>/dev/null || :)
302
+ [ -n "$_sha_path" ] || return 1
303
+ _sha_output=$("$_sha_path" "$1") || return 1
304
+ fi
305
+ fi
306
+ # Supported utilities: sha256sum/shasum print "digest filename";
307
+ # BSD sha256 prints "SHA256 (filename) = digest" or just the digest.
308
+ # The BSD format strip must be gated behind the SHA256 ( prefix: a
309
+ # blanket *=* match corrupts sha256sum/shasum output whenever the
310
+ # absolute file path contains "= " (equals followed by space).
311
+ case "$_sha_output" in
312
+ "SHA256 ("*) _sha_output=${_sha_output##* = } ;;
313
+ esac
314
+ _sha_output=${_sha_output%% *}
315
+ _sha_output=${_sha_output%%$(printf '\t')*}
316
+ printf '%s\n' "$_sha_output"
317
+ }
318
+
319
+ lower_hex() {
320
+ _tr_path=$(trusted_utility_path tr) || return 1
321
+ "$_tr_path" 'A-F' 'a-f'
322
+ }
323
+
324
+ cmd_version() {
325
+ printf '%s\n' "$VALISE_VERSION"
326
+ }