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,170 @@
1
+ # Copyright (c) 2026 Joseph Cumines
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ # Fish adapter for a release built by `valise build`, installed at
5
+ # runtime/fish/profile.fish.
6
+ #
7
+ # The valise Fish integration authenticates the active sealed release and
8
+ # exports SHELL_PROJECT_ROOT pointing at its root before sourcing this file.
9
+ # The seeds are the release's own roots_fish line in valise-release.conf, so a
10
+ # Fish login loads exactly the bundle the release was built for. VALISE_MODE
11
+ # selects strict or permissive failure handling (default strict, as in the
12
+ # POSIX plans). The adapter sources the release's native Fish metadata and
13
+ # loader, consumes the loader's bootstrap status marker, and publishes
14
+ # VALISE_FISH_READY=1 only after a successful activation.
15
+
16
+ # Readiness and the bootstrap handshake are engine-private state. A forged
17
+ # or stale value must not survive this activation, even one that fails
18
+ # early: erase every visible scope first, failing closed if an erase is
19
+ # refused.
20
+ for _valise_fish_profile_stale in VALISE_FISH_READY _sp_fish_loader_bootstrap_mode _sp_fish_loader_bootstrap_seeds _sp_fish_loader_bootstrap_status _sp_fish_loader_expected_source _sp_fish_metadata_expected_source
21
+ if builtin set -q -g "$_valise_fish_profile_stale"; and not builtin set -e -g "$_valise_fish_profile_stale"
22
+ builtin printf '%s\n' "valise Fish profile: cannot clear stale global $_valise_fish_profile_stale" >&2
23
+ return 1
24
+ end
25
+ if builtin set -q -U "$_valise_fish_profile_stale"; and not builtin set -e -U "$_valise_fish_profile_stale"
26
+ builtin printf '%s\n' "valise Fish profile: cannot clear stale universal $_valise_fish_profile_stale" >&2
27
+ return 1
28
+ end
29
+ end
30
+
31
+ # Each activation starts from nothing. Remove the functions the previous
32
+ # activation's loader recorded as its own, but only where the live function
33
+ # still has the recorded Fish provenance, then drop that loader state. A
34
+ # failure below therefore never leaves the old activation callable without
35
+ # readiness.
36
+ set -l _valise_fish_profile_owned_index 1
37
+ for _valise_fish_profile_owned in $_sp_fish_loader_owned_exports
38
+ set -l _valise_fish_profile_owned_source "$_sp_fish_loader_owned_sources[$_valise_fish_profile_owned_index]"
39
+ if builtin test -n "$_valise_fish_profile_owned_source"; and builtin functions -q -- "$_valise_fish_profile_owned"; and builtin test (builtin functions --details -- "$_valise_fish_profile_owned") = "$_valise_fish_profile_owned_source"
40
+ builtin functions -e -- "$_valise_fish_profile_owned"; or builtin exit 125
41
+ end
42
+ set _valise_fish_profile_owned_index (builtin math $_valise_fish_profile_owned_index + 1)
43
+ end
44
+ for _valise_fish_profile_stale in _sp_fish_loader_owned_exports _sp_fish_loader_owned_sources _sp_fish_loaded_modules _sp_fish_module_roots _sp_fish_metadata_provider
45
+ if builtin set -q -g "$_valise_fish_profile_stale"; and not builtin set -e -g "$_valise_fish_profile_stale"
46
+ builtin exit 125
47
+ end
48
+ if builtin set -q -U "$_valise_fish_profile_stale"; and not builtin set -e -U "$_valise_fish_profile_stale"
49
+ builtin exit 125
50
+ end
51
+ end
52
+
53
+ if not builtin set -q SHELL_PROJECT_ROOT
54
+ builtin printf '%s\n' 'valise Fish profile: SHELL_PROJECT_ROOT is not set' >&2
55
+ return 1
56
+ end
57
+ set -l _valise_fish_profile_root (builtin path resolve -- "$SHELL_PROJECT_ROOT" 2>/dev/null)
58
+ if builtin test (builtin count $_valise_fish_profile_root) -ne 1; or builtin test -z "$_valise_fish_profile_root"
59
+ builtin printf '%s\n' 'valise Fish profile: SHELL_PROJECT_ROOT cannot be resolved' >&2
60
+ return 1
61
+ end
62
+ set -l _valise_fish_profile_runtime "$_valise_fish_profile_root/runtime/fish"
63
+
64
+ for _valise_fish_profile_file in "$_valise_fish_profile_root/valise-release.conf" "$_valise_fish_profile_runtime/metadata.fish" "$_valise_fish_profile_runtime/loader.fish"
65
+ if not builtin test -f "$_valise_fish_profile_file"; or not builtin test -r "$_valise_fish_profile_file"
66
+ builtin printf '%s\n' "valise Fish profile: release file is not readable: $_valise_fish_profile_file" >&2
67
+ return 1
68
+ end
69
+ end
70
+
71
+ set -l _valise_fish_profile_mode strict
72
+ if builtin set -q VALISE_MODE
73
+ set _valise_fish_profile_mode "$VALISE_MODE"
74
+ end
75
+
76
+ # roots_fish holds space-separated module references. The Fish loader
77
+ # resolves modules by name, so a qualified name@source keeps only its name.
78
+ set -l _valise_fish_profile_roots_lines (builtin string match -r '^roots_fish=.*' < "$_valise_fish_profile_root/valise-release.conf")
79
+ if builtin test (builtin count $_valise_fish_profile_roots_lines) -ne 1
80
+ builtin printf '%s\n' 'valise Fish profile: valise-release.conf must hold exactly one roots_fish line' >&2
81
+ return 1
82
+ end
83
+ set -l _valise_fish_profile_seeds
84
+ for _valise_fish_profile_ref in (builtin string split ' ' -- (builtin string replace 'roots_fish=' '' -- $_valise_fish_profile_roots_lines[1]))
85
+ builtin test -n "$_valise_fish_profile_ref"; or continue
86
+ set -a _valise_fish_profile_seeds (builtin string replace -r '@.*$' '' -- $_valise_fish_profile_ref)
87
+ end
88
+
89
+ # The adapter, not the sourced files, names the provenance anchors the
90
+ # loader checks its own definitions against. Pinning them here means a
91
+ # caller cannot preset a forged anchor before activation.
92
+ set -l _valise_fish_profile_loader_anchor "$_valise_fish_profile_runtime/loader.fish"
93
+ set -l _valise_fish_profile_metadata_anchor "$_valise_fish_profile_runtime/metadata.fish"
94
+ builtin set -g _sp_fish_loader_expected_source "$_valise_fish_profile_loader_anchor"
95
+ builtin set -g _sp_fish_metadata_expected_source "$_valise_fish_profile_metadata_anchor"
96
+
97
+ builtin set -gx _sp_fish_loader_bootstrap_mode "$_valise_fish_profile_mode"
98
+ builtin set -gx _sp_fish_loader_bootstrap_seeds $_valise_fish_profile_seeds
99
+
100
+ builtin source "$_valise_fish_profile_metadata_anchor"
101
+ set -l _valise_fish_profile_metadata_status $status
102
+ # Metadata defines functions only. It must leave the activation state it was
103
+ # handed untouched; a source that returns success after rewriting it is
104
+ # rejected the same as one that fails.
105
+ if builtin test $_valise_fish_profile_metadata_status -eq 0
106
+ set -l _valise_fish_profile_tampered
107
+ if not builtin test "$SHELL_PROJECT_ROOT" = "$_valise_fish_profile_root"; and not builtin test (builtin path resolve -- "$SHELL_PROJECT_ROOT" 2>/dev/null) = "$_valise_fish_profile_root"
108
+ set -a _valise_fish_profile_tampered SHELL_PROJECT_ROOT
109
+ end
110
+ if builtin set -q -U _sp_fish_loader_expected_source; or not builtin test "$_sp_fish_loader_expected_source" = "$_valise_fish_profile_loader_anchor"
111
+ set -a _valise_fish_profile_tampered _sp_fish_loader_expected_source
112
+ end
113
+ if builtin set -q -U _sp_fish_metadata_expected_source; or not builtin test "$_sp_fish_metadata_expected_source" = "$_valise_fish_profile_metadata_anchor"
114
+ set -a _valise_fish_profile_tampered _sp_fish_metadata_expected_source
115
+ end
116
+ if builtin set -q -U _sp_fish_loader_bootstrap_mode; or not builtin test "$_sp_fish_loader_bootstrap_mode" = "$_valise_fish_profile_mode"
117
+ set -a _valise_fish_profile_tampered _sp_fish_loader_bootstrap_mode
118
+ end
119
+ if builtin set -q -U _sp_fish_loader_bootstrap_seeds; or not builtin test "$_sp_fish_loader_bootstrap_seeds" = "$_valise_fish_profile_seeds"
120
+ set -a _valise_fish_profile_tampered _sp_fish_loader_bootstrap_seeds
121
+ end
122
+ if builtin set -q _sp_fish_loader_bootstrap_status
123
+ set -a _valise_fish_profile_tampered _sp_fish_loader_bootstrap_status
124
+ end
125
+ if builtin test (builtin count $_valise_fish_profile_tampered) -ne 0
126
+ builtin printf '%s\n' "valise Fish profile: metadata changed activation state: $_valise_fish_profile_tampered" >&2
127
+ set _valise_fish_profile_metadata_status 1
128
+ end
129
+ end
130
+ if builtin test $_valise_fish_profile_metadata_status -ne 0
131
+ for _valise_fish_profile_stale in _sp_fish_loader_bootstrap_mode _sp_fish_loader_bootstrap_seeds _sp_fish_loader_bootstrap_status _sp_fish_loader_expected_source _sp_fish_metadata_expected_source
132
+ builtin set -q -U "$_valise_fish_profile_stale"; and builtin set -e -U "$_valise_fish_profile_stale"
133
+ builtin set -q -g "$_valise_fish_profile_stale"; and builtin set -e -g "$_valise_fish_profile_stale"
134
+ end
135
+ builtin set -g _sp_fish_loader_expected_source "$_valise_fish_profile_loader_anchor"
136
+ builtin set -g _sp_fish_metadata_expected_source "$_valise_fish_profile_metadata_anchor"
137
+ return $_valise_fish_profile_metadata_status
138
+ end
139
+
140
+ builtin source "$_valise_fish_profile_loader_anchor"
141
+ set -l _valise_fish_profile_loader_status $status
142
+ # The loader consumes the bootstrap request when it sources cleanly; erase
143
+ # any request a failed source left behind so it cannot leak to callers.
144
+ if builtin set -q _sp_fish_loader_bootstrap_mode
145
+ builtin set -e _sp_fish_loader_bootstrap_mode
146
+ end
147
+ if builtin set -q _sp_fish_loader_bootstrap_seeds
148
+ builtin set -e _sp_fish_loader_bootstrap_seeds
149
+ end
150
+ if builtin test $_valise_fish_profile_loader_status -ne 0
151
+ if builtin set -q _sp_fish_loader_bootstrap_status
152
+ builtin set -e _sp_fish_loader_bootstrap_status
153
+ end
154
+ return $_valise_fish_profile_loader_status
155
+ end
156
+
157
+ # The loader publishes a fresh bootstrap status marker on every activation;
158
+ # its absence means the loader never completed its handshake.
159
+ if not builtin set -q _sp_fish_loader_bootstrap_status
160
+ builtin printf '%s\n' 'valise Fish profile: loader bootstrap status marker is absent' >&2
161
+ return 1
162
+ end
163
+ set -l _valise_fish_profile_status $_sp_fish_loader_bootstrap_status
164
+ builtin set -e _sp_fish_loader_bootstrap_status
165
+ if builtin test $_valise_fish_profile_status -ne 0
166
+ return $_valise_fish_profile_status
167
+ end
168
+
169
+ builtin set -gx VALISE_FISH_READY 1
170
+ return 0
package/lib/loader.sh CHANGED
@@ -94,7 +94,6 @@ _sp_loader_read_record() {
94
94
  _splrr_platforms=
95
95
  _splrr_side_effect=
96
96
  _splrr_exports=
97
- _splrr_updates=
98
97
  _splrr_version=
99
98
  _splrr_load=
100
99
  while IFS= read -r _splrr_line || [ -n "$_splrr_line" ]; do
@@ -112,7 +111,6 @@ _sp_loader_read_record() {
112
111
  platforms) _splrr_platforms=$_splrr_value ;;
113
112
  side_effect) _splrr_side_effect=$_splrr_value ;;
114
113
  exports) _splrr_exports=$_splrr_value ;;
115
- updates) _splrr_updates=$_splrr_value ;;
116
114
  version) _splrr_version=$_splrr_value ;;
117
115
  load) _splrr_load=$_splrr_value ;;
118
116
  esac
@@ -124,12 +122,11 @@ _sp_loader_read_record() {
124
122
  _spl_record_platforms=$_splrr_platforms
125
123
  _spl_record_side_effect=$_splrr_side_effect
126
124
  _spl_record_exports=$_splrr_exports
127
- _spl_record_updates=$_splrr_updates
128
125
  _spl_record_version=$_splrr_version
129
126
  _spl_record_load=$_splrr_load
130
127
  unset _splrr_file _splrr_line _splrr_key _splrr_value _splrr_name _splrr_requires \
131
128
  _splrr_optional _splrr_shells _splrr_platforms _splrr_side_effect _splrr_exports \
132
- _splrr_updates _splrr_version _splrr_load
129
+ _splrr_version _splrr_load
133
130
  }
134
131
 
135
132
  _sp_loader_reserved_scan() {
@@ -639,7 +636,8 @@ sp_loader_load_modules() {
639
636
  fi
640
637
  IFS=' '
641
638
  for _spl_value in $_spl_record_shells; do
642
- [ "$_spl_value" = sh ] && _spl_shell_match=1
639
+ # posix names every POSIX shell; sh names only the generic class.
640
+ [ "$_spl_value" = posix ] && _spl_shell_match=1
643
641
  [ "$_spl_value" = "$_spl_current_shell" ] && _spl_shell_match=1
644
642
  done
645
643
  for _spl_value in $_spl_record_platforms; do
@@ -742,7 +740,7 @@ sp_loader_load_modules() {
742
740
  fi
743
741
  done
744
742
  unset _spl_record_name _spl_record_requires _spl_record_optional _spl_record_shells \
745
- _spl_record_platforms _spl_record_side_effect _spl_record_exports _spl_record_updates \
743
+ _spl_record_platforms _spl_record_side_effect _spl_record_exports \
746
744
  _spl_record_version _spl_record_load
747
745
  if [ "$_spl_dep_ifs_was_set" -eq 1 ]; then
748
746
  IFS=$_spl_dep_old_ifs
@@ -1613,7 +1611,7 @@ sp_loader_load_modules() {
1613
1611
  _spl_order_words _spl_failed_words _spl_guard_seen_words _spl_guard_strict_required_words \
1614
1612
  _spl_guard_blocked_words _spl_guard_modules_words _spl_guard_order_words _spl_guard_failed_words \
1615
1613
  _spl_record_name _spl_record_requires _spl_record_optional _spl_record_shells \
1616
- _spl_record_platforms _spl_record_side_effect _spl_record_exports _spl_record_updates \
1614
+ _spl_record_platforms _spl_record_side_effect _spl_record_exports \
1617
1615
  _spl_record_version _spl_record_load _spl_run_line _spl_module_root
1618
1616
  unset -f _sp_loader_path_is_physical _sp_loader_read_record _sp_loader_reserved_scan 2>/dev/null || :
1619
1617
  return 0
@@ -1663,7 +1661,7 @@ sp_loader_load_modules() {
1663
1661
  _spl_order_words _spl_failed_words _spl_guard_seen_words _spl_guard_strict_required_words \
1664
1662
  _spl_guard_blocked_words _spl_guard_modules_words _spl_guard_order_words _spl_guard_failed_words \
1665
1663
  _spl_record_name _spl_record_requires _spl_record_optional _spl_record_shells \
1666
- _spl_record_platforms _spl_record_side_effect _spl_record_exports _spl_record_updates \
1664
+ _spl_record_platforms _spl_record_side_effect _spl_record_exports \
1667
1665
  _spl_record_version _spl_record_load _spl_run_line _spl_module_root
1668
1666
  unset -f _sp_loader_path_is_physical _sp_loader_read_record _sp_loader_reserved_scan 2>/dev/null || :
1669
1667
  return 1
package/lib/metadata.sh CHANGED
@@ -144,7 +144,7 @@ sp_metadata_validate_shell_list() {
144
144
  for _spm_shell in $1; do
145
145
  _spm_shell_seen=1
146
146
  case "$_spm_shell" in
147
- sh | bash | zsh | ksh | fish) ;;
147
+ posix | sh | bash | zsh | ksh | fish) ;;
148
148
  *)
149
149
  sp_metadata_error "invalid shell: $_spm_shell"
150
150
  if [ "$_spm_shell_ifs_was_set" -eq 1 ]; then
@@ -285,6 +285,92 @@ sp_metadata_validate_relative_path() {
285
285
  return 0
286
286
  }
287
287
 
288
+ # sp_metadata_validate_path_list <list> <label>
289
+ # Validate space-separated module-relative file paths: each must be a
290
+ # relative path with no empty, "." or ".." segment and no trailing slash.
291
+ sp_metadata_validate_path_list() {
292
+ _spm_plist=$1
293
+ _spm_plabel=$2
294
+ sp_metadata_reject_glob_characters "$_spm_plist" "$_spm_plabel" || {
295
+ unset _spm_plist _spm_plabel
296
+ return 1
297
+ }
298
+ if [ "${IFS+x}" = x ]; then
299
+ _spm_plist_ifs_was_set=1
300
+ _spm_plist_ifs=${IFS-}
301
+ else
302
+ _spm_plist_ifs_was_set=0
303
+ _spm_plist_ifs=
304
+ fi
305
+ _spm_plist_status=0
306
+ IFS=' '
307
+ for _spm_pword in $_spm_plist; do
308
+ case "$_spm_pword" in
309
+ . | ./* | */. | */./* | *//* | */)
310
+ sp_metadata_error "invalid $_spm_plabel path: $_spm_pword"
311
+ _spm_plist_status=1
312
+ break
313
+ ;;
314
+ esac
315
+ if ! sp_metadata_validate_relative_path "$_spm_pword"; then
316
+ _spm_plist_status=1
317
+ break
318
+ fi
319
+ if [ "$_spm_plabel" = executable ]; then
320
+ _spm_pbase=${_spm_pword##*/}
321
+ case "$_spm_pbase" in
322
+ [abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_]*) ;;
323
+ *)
324
+ sp_metadata_error "invalid executable name: $_spm_pbase"
325
+ _spm_plist_status=1
326
+ break
327
+ ;;
328
+ esac
329
+ fi
330
+ done
331
+ if [ "$_spm_plist_ifs_was_set" -eq 1 ]; then
332
+ IFS=$_spm_plist_ifs
333
+ else
334
+ unset IFS
335
+ fi
336
+ _spm_plist_rc=$_spm_plist_status
337
+ unset _spm_plist _spm_plabel _spm_pword _spm_pbase _spm_plist_ifs _spm_plist_ifs_was_set _spm_plist_status
338
+ return "$_spm_plist_rc"
339
+ }
340
+
341
+ # sp_metadata_validate_commands <commands> <exports>
342
+ # Every published command must be one of the module's declared exports.
343
+ sp_metadata_validate_commands() {
344
+ sp_metadata_validate_identifier_list "$1" command || return 1
345
+ if [ "${IFS+x}" = x ]; then
346
+ _spm_cmd_ifs_was_set=1
347
+ _spm_cmd_ifs=${IFS-}
348
+ else
349
+ _spm_cmd_ifs_was_set=0
350
+ _spm_cmd_ifs=
351
+ fi
352
+ _spm_cmd_status=0
353
+ IFS=' '
354
+ for _spm_cmd in $1; do
355
+ case " $2 " in
356
+ *" $_spm_cmd "*) ;;
357
+ *)
358
+ sp_metadata_error "command is not a declared export: $_spm_cmd"
359
+ _spm_cmd_status=1
360
+ break
361
+ ;;
362
+ esac
363
+ done
364
+ if [ "$_spm_cmd_ifs_was_set" -eq 1 ]; then
365
+ IFS=$_spm_cmd_ifs
366
+ else
367
+ unset IFS
368
+ fi
369
+ _spm_cmd_rc=$_spm_cmd_status
370
+ unset _spm_cmd _spm_cmd_ifs _spm_cmd_ifs_was_set _spm_cmd_status
371
+ return "$_spm_cmd_rc"
372
+ }
373
+
288
374
  sp_metadata_validate_load_target() {
289
375
  _spm_target_load=$1
290
376
  _spm_target_requires=${2-}
@@ -327,9 +413,12 @@ sp_metadata_read() (
327
413
  _spm_platforms=
328
414
  _spm_side_effect=
329
415
  _spm_exports=
330
- _spm_updates=
416
+ _spm_commands=
417
+ _spm_executables=
418
+ _spm_tests=
331
419
  _spm_version=
332
420
  _spm_load=
421
+ _spm_seen=' '
333
422
 
334
423
  while IFS= read -r _spm_line || [ -n "$_spm_line" ]; do
335
424
  case "$_spm_line" in
@@ -343,6 +432,13 @@ sp_metadata_read() (
343
432
  _spm_key=${_spm_line%%=*}
344
433
  _spm_value=${_spm_line#*=}
345
434
  sp_metadata_reject_control_characters "$_spm_value" "$_spm_key" || return 1
435
+ case "$_spm_seen" in
436
+ *" $_spm_key "*)
437
+ sp_metadata_error "duplicate metadata field: $_spm_key"
438
+ return 1
439
+ ;;
440
+ esac
441
+ _spm_seen="$_spm_seen$_spm_key "
346
442
  case "$_spm_key" in
347
443
  name) _spm_name=$_spm_value ;;
348
444
  requires) _spm_requires=$_spm_value ;;
@@ -351,7 +447,13 @@ sp_metadata_read() (
351
447
  platforms) _spm_platforms=$_spm_value ;;
352
448
  side_effect) _spm_side_effect=$_spm_value ;;
353
449
  exports) _spm_exports=$_spm_value ;;
354
- updates) _spm_updates=$_spm_value ;;
450
+ commands) _spm_commands=$_spm_value ;;
451
+ executables) _spm_executables=$_spm_value ;;
452
+ tests) _spm_tests=$_spm_value ;;
453
+ updates)
454
+ sp_metadata_error 'metadata field updates is not supported since valise 0.3.0; publish commands with commands= or executables='
455
+ return 1
456
+ ;;
355
457
  version) _spm_version=$_spm_value ;;
356
458
  load) _spm_load=$_spm_value ;;
357
459
  load_hook)
@@ -381,7 +483,9 @@ sp_metadata_read() (
381
483
  sp_metadata_validate_platform_list "$_spm_platforms" || return 1
382
484
  sp_metadata_validate_side_effect "$_spm_side_effect" || return 1
383
485
  sp_metadata_validate_identifier_list "$_spm_exports" export || return 1
384
- sp_metadata_validate_identifier_list "$_spm_updates" update_registration || return 1
486
+ sp_metadata_validate_commands "$_spm_commands" "$_spm_exports" || return 1
487
+ sp_metadata_validate_path_list "$_spm_executables" executable || return 1
488
+ sp_metadata_validate_path_list "$_spm_tests" test || return 1
385
489
  sp_metadata_validate_version "$_spm_version" || return 1
386
490
  sp_metadata_validate_relative_path "$_spm_load" || return 1
387
491
 
@@ -392,7 +496,9 @@ sp_metadata_read() (
392
496
  printf 'platforms=%s\n' "$_spm_platforms"
393
497
  printf 'side_effect=%s\n' "$_spm_side_effect"
394
498
  printf 'exports=%s\n' "$_spm_exports"
395
- printf 'updates=%s\n' "$_spm_updates"
499
+ printf 'commands=%s\n' "$_spm_commands"
500
+ printf 'executables=%s\n' "$_spm_executables"
501
+ printf 'tests=%s\n' "$_spm_tests"
396
502
  printf 'version=%s\n' "$_spm_version"
397
503
  printf 'load=%s\n' "$_spm_load"
398
504
  )
@@ -1,7 +1,7 @@
1
1
  # path_pattern class parsers requirement notes
2
2
  entrypoints/profile.sh posix sh,dash,ksh,busybox-ash,bash required Portable profile entrypoint must parse with POSIX-family shells and Bash when those parsers are available.
3
- entrypoints/fish/*.fish fish fish required Generic Fish entrypoint adapter requires the Fish parser, not POSIX shells.
4
- lib/*.sh posix sh,dash,ksh,busybox-ash,bash required Library files are portable core unless a narrower shell-specific path owns them.
3
+ bin/valise posix sh,dash,ksh,busybox-ash,bash required CLI dispatcher is pure POSIX sh.
4
+ lib/*.sh posix sh,dash,ksh,busybox-ash,bash required Library files (including lib/cli) are portable core unless a narrower shell-specific path owns them.
5
5
  lib/*.awk non-shell none required Awk compiler is not a shell script; excluded from shell parser validation.
6
6
  lib/fish/*.fish fish fish required Fish-native library files require Fish parser, not POSIX shells.
7
7
  modules/system/*.sh posix sh,dash,ksh,busybox-ash,bash required System utility modules use portable syntax plus capability gates.
@@ -0,0 +1,9 @@
1
+ name=valise_editor
2
+ requires=
3
+ optional=
4
+ shells=posix
5
+ platforms=any
6
+ side_effect=path-env
7
+ exports=valise_editor_defaults
8
+ version=1.0.0
9
+ load=modules/system/valise_editor.sh
@@ -0,0 +1,9 @@
1
+ name=valise_history
2
+ requires=
3
+ optional=
4
+ shells=posix
5
+ platforms=any
6
+ side_effect=path-env
7
+ exports=valise_history_defaults
8
+ version=1.0.0
9
+ load=modules/system/valise_history.sh
@@ -0,0 +1,9 @@
1
+ name=valise_pager
2
+ requires=
3
+ optional=
4
+ shells=posix
5
+ platforms=any
6
+ side_effect=path-env
7
+ exports=valise_pager_defaults
8
+ version=1.0.0
9
+ load=modules/system/valise_pager.sh
@@ -0,0 +1,9 @@
1
+ name=valise_path
2
+ requires=
3
+ optional=
4
+ shells=posix
5
+ platforms=any
6
+ side_effect=path-env
7
+ exports=valise_path_prepend valise_path_append
8
+ version=1.0.0
9
+ load=modules/system/valise_path.sh
@@ -1,9 +1,8 @@
1
- name=platform_bsd
2
- requires=platform_posix
1
+ name=valise_platform_bsd
2
+ requires=valise_platform_posix
3
3
  optional=
4
- shells=sh bash zsh ksh
4
+ shells=posix
5
5
  platforms=bsd freebsd openbsd netbsd
6
6
  side_effect=read-only
7
7
  exports=sp_platform_bsd_available sp_platform_bsd_cpu_count sp_platform_bsd_file_mtime
8
- updates=
9
8
  load=modules/platform/bsd/capabilities.sh
@@ -1,9 +1,8 @@
1
- name=platform_linux
2
- requires=platform_posix
1
+ name=valise_platform_linux
2
+ requires=valise_platform_posix
3
3
  optional=
4
- shells=sh bash zsh ksh
4
+ shells=posix
5
5
  platforms=linux
6
6
  side_effect=read-only
7
7
  exports=sp_platform_linux_available sp_platform_linux_cpu_count sp_platform_linux_file_mtime
8
- updates=
9
8
  load=modules/platform/linux/capabilities.sh
@@ -1,9 +1,8 @@
1
- name=platform_macos
2
- requires=platform_posix
1
+ name=valise_platform_macos
2
+ requires=valise_platform_posix
3
3
  optional=
4
- shells=sh bash zsh ksh
4
+ shells=posix
5
5
  platforms=darwin
6
6
  side_effect=read-only
7
7
  exports=sp_platform_macos_available sp_platform_macos_homebrew_prefix sp_platform_macos_homebrew_gnubin sp_platform_macos_cpu_count
8
- updates=
9
8
  load=modules/platform/macos/capabilities.sh
@@ -1,9 +1,8 @@
1
- name=platform_posix
1
+ name=valise_platform_posix
2
2
  requires=
3
3
  optional=
4
- shells=sh bash zsh ksh
4
+ shells=posix
5
5
  platforms=any
6
6
  side_effect=read-only
7
7
  exports=sp_platform_os sp_platform_cpu_count sp_platform_file_mtime sp_platform_mktemp_dir sp_platform_canonical_path sp_platform_path_under sp_platform_command_path
8
- updates=
9
8
  load=modules/platform/posix.sh
@@ -0,0 +1,9 @@
1
+ name=valise_xdg
2
+ requires=
3
+ optional=
4
+ shells=posix
5
+ platforms=any
6
+ side_effect=path-env
7
+ exports=valise_xdg_defaults
8
+ version=1.0.0
9
+ load=modules/system/valise_xdg.sh
@@ -1,14 +1,14 @@
1
1
  # Copyright (c) 2026 Joseph Cumines
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
- # contrib_editor: predictable EDITOR/VISUAL defaults.
4
+ # valise_editor: predictable EDITOR/VISUAL defaults.
5
5
  #
6
6
  # Respects values the user already set; otherwise uses the first available
7
7
  # of nvim, vim, vi. vi is POSIX-mandated and present on conforming systems,
8
8
  # so a default is always found there. Prints nothing and succeeds in every
9
9
  # case, including when nothing is available.
10
10
 
11
- contrib_editor_defaults() {
11
+ valise_editor_defaults() {
12
12
  if [ -n "${EDITOR-}" ] && [ -n "${VISUAL-}" ]; then
13
13
  return 0
14
14
  fi
@@ -1,13 +1,13 @@
1
1
  # Copyright (c) 2026 Joseph Cumines
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
- # contrib_history: predictable shell history defaults.
4
+ # valise_history: predictable shell history defaults.
5
5
  #
6
6
  # Sets HISTFILE under HOME and a portable HISTSIZE only when each is unset
7
7
  # or empty; values the user already set are never touched. When HOME is
8
8
  # unset, HISTFILE is left alone. Prints nothing and succeeds in every case.
9
9
 
10
- contrib_history_defaults() {
10
+ valise_history_defaults() {
11
11
  if [ -z "${HISTFILE-}" ] && [ -n "${HOME-}" ]; then
12
12
  HISTFILE=$HOME/.sh_history
13
13
  export HISTFILE
@@ -1,13 +1,13 @@
1
1
  # Copyright (c) 2026 Joseph Cumines
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
- # contrib_pager: predictable PAGER default.
4
+ # valise_pager: predictable PAGER default.
5
5
  #
6
6
  # Respects a value the user already set; otherwise uses the first available
7
7
  # of less, more. When neither exists, PAGER is left unset. Prints nothing
8
8
  # and succeeds in every case.
9
9
 
10
- contrib_pager_defaults() {
10
+ valise_pager_defaults() {
11
11
  if [ -n "${PAGER-}" ]; then
12
12
  return 0
13
13
  fi
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2026 Joseph Cumines
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
- # contrib_path: idempotent PATH augmentation.
4
+ # valise_path: idempotent PATH augmentation.
5
5
  #
6
6
  # Each listed directory ends up in PATH exactly once. With no arguments the
7
7
  # functions normalize PATH in place (duplicates and empty components are
@@ -73,12 +73,12 @@ _contrib_path_rebuild() {
73
73
  unset _contrib_path_mode _contrib_path_wanted _contrib_path_item _contrib_path_kept _contrib_path_field _contrib_path_new _contrib_path_old_ifs _contrib_path_ifs_was_set _contrib_path_glob_off
74
74
  }
75
75
 
76
- contrib_path_prepend() {
76
+ valise_path_prepend() {
77
77
  # Move each argument to the front of PATH, preserving argument order, so
78
78
  # the first argument ends up first.
79
79
  _contrib_path_rebuild prepend "$@"
80
80
  }
81
81
 
82
- contrib_path_append() {
82
+ valise_path_append() {
83
83
  _contrib_path_rebuild append "$@"
84
84
  }
@@ -1,14 +1,14 @@
1
1
  # Copyright (c) 2026 Joseph Cumines
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
- # contrib_xdg: predictable XDG base directory defaults.
4
+ # valise_xdg: predictable XDG base directory defaults.
5
5
  #
6
6
  # Fills in each of XDG_DATA_HOME, XDG_CONFIG_HOME, XDG_STATE_HOME, and
7
7
  # XDG_CACHE_HOME from HOME only when that variable is unset or empty;
8
8
  # values the user already set are never touched. When HOME itself is
9
9
  # unset, nothing is set. Prints nothing and succeeds in every case.
10
10
 
11
- contrib_xdg_defaults() {
11
+ valise_xdg_defaults() {
12
12
  if [ -z "${HOME-}" ]; then
13
13
  return 0
14
14
  fi