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,337 @@
1
+ # Copyright (c) 2026 Joseph Cumines
2
+ # SPDX-License-Identifier: MIT
3
+ # valise CLI: POSIX integration, init, and doctor.
4
+ # Sourced by bin/valise; not executable on its own.
5
+
6
+ # Emit the POSIX integration block for the active release.
7
+ #
8
+ # It works in every POSIX shell. A release carries plans/<shell>.sh for the
9
+ # shell classes its bundle covers; a shell class with no plan loads nothing.
10
+ # The guard makes several startup files safe: a login shell that reads more
11
+ # than one of them loads the profile once. It is set only after a plan was sourced, so a shell started
12
+ # before the first install, or while the release was missing, tries again from
13
+ # the next startup file. A plan that failed is not re-run in the same shell;
14
+ # VALISE_RELOAD=1 forces another attempt. The shell is named by the first of
15
+ # KSH_VERSION, ZSH_VERSION and BASH_VERSION that is set; any other shell uses
16
+ # the generic sh plan.
17
+ print_posix_integration() {
18
+ _pix_active_literal=$(shell_quote "$VALISE_ACTIVE_LINK") || return 1
19
+ printf '%s\n' \
20
+ '# BEGIN valise managed block' \
21
+ "_valise_active_link=$_pix_active_literal" \
22
+ 'if [ -z "${_valise_profile_loaded:-}" ] || [ "${VALISE_RELOAD:-}" = 1 ]; then' \
23
+ ' if [ -n "${KSH_VERSION-}" ]; then _valise_plan=ksh; elif [ -n "${ZSH_VERSION-}" ]; then _valise_plan=zsh; elif [ -n "${BASH_VERSION-}" ]; then _valise_plan=bash; else _valise_plan=sh; fi' \
24
+ ' _valise_active=$(cd "$_valise_active_link" 2>/dev/null && pwd -P)' \
25
+ ' if [ -n "$_valise_active" ]; then' \
26
+ ' if [ -f "$_valise_active/plans/$_valise_plan.sh" ]; then' \
27
+ ' _valise_profile_loaded=1' \
28
+ ' VALISE_MODULE_ROOT="$_valise_active"' \
29
+ ' . "$_valise_active/plans/$_valise_plan.sh"' \
30
+ ' unset VALISE_MODULE_ROOT' \
31
+ ' fi' \
32
+ ' fi' \
33
+ ' unset _valise_active _valise_plan' \
34
+ 'fi' \
35
+ 'unset _valise_active_link' \
36
+ '# END valise managed block'
37
+ unset _pix_active_literal
38
+ }
39
+
40
+ # integration_markers_paired <file>
41
+ # Succeed when every BEGIN marker of the managed block is followed by an END
42
+ # marker before the next BEGIN, and no END appears without a BEGIN.
43
+ integration_markers_paired() {
44
+ _imp_awk=$(trusted_utility_path awk) || return 1
45
+ "$_imp_awk" '
46
+ /^# BEGIN valise managed block$/ { if (open) bad = 1; open = 1; next }
47
+ /^# END valise managed block$/ { if (!open) bad = 1; open = 0; next }
48
+ END { exit (bad || open) ? 1 : 0 }
49
+ ' "$1"
50
+ }
51
+
52
+ cmd_init() {
53
+ _shell=""
54
+ _fish_source_tree_mode=0
55
+ for _arg do
56
+ case "$_arg" in
57
+ --source-tree) _fish_source_tree_mode=1 ;;
58
+ --help|-h) usage_init; return 0 ;;
59
+ -*) die "unknown init option: $_arg" ;;
60
+ *)
61
+ [ -z "$_shell" ] || die "usage: valise init [posix|fish] [--source-tree]"
62
+ _shell=$_arg
63
+ ;;
64
+ esac
65
+ done
66
+ if [ -z "$_shell" ]; then
67
+ case "${SHELL:-}" in
68
+ *fish) _shell=fish ;;
69
+ *) _shell=posix ;;
70
+ esac
71
+ fi
72
+ case "$_shell" in
73
+ posix) print_posix_integration || return 1 ;;
74
+ fish) print_fish_integration ;;
75
+ *) die "unsupported shell family: $_shell (use posix or fish)" ;;
76
+ esac
77
+ }
78
+
79
+ # Report whether a startup file carries valise integration: the POSIX managed
80
+ # block, or the header of the Fish hook, which valise owns as a whole file.
81
+ integration_has_managed_block() {
82
+ _ihb_awk=$(trusted_utility_path awk) || return 1
83
+ "$_ihb_awk" '
84
+ index($0, "# BEGIN valise managed block") { found = 1; exit }
85
+ index($0, "# valise Fish integration") { found = 1; exit }
86
+ END { exit !found }
87
+ ' "$1" 2>/dev/null
88
+ }
89
+
90
+ # The startup files a shell reads at login and for interactive shells. A login
91
+ # bash reads the first of .bash_profile, .bash_login and .profile that exists.
92
+ #
93
+ # integration_shell_files <shell> <home>
94
+ # prints one candidate path per line.
95
+ integration_shell_files() {
96
+ case "$1" in
97
+ bash)
98
+ if [ -e "$2/.bash_profile" ]; then
99
+ printf '%s\n' "$2/.bash_profile"
100
+ elif [ -e "$2/.bash_login" ]; then
101
+ printf '%s\n' "$2/.bash_login"
102
+ else
103
+ printf '%s\n' "$2/.profile"
104
+ fi
105
+ printf '%s\n' "$2/.bashrc"
106
+ ;;
107
+ zsh)
108
+ printf '%s\n' "$2/.zprofile" "$2/.zshrc"
109
+ ;;
110
+ fish)
111
+ printf '%s\n' "${XDG_CONFIG_HOME:-$2/.config}/fish/conf.d/valise.fish"
112
+ ;;
113
+ *)
114
+ printf '%s\n' "$2/.profile"
115
+ ;;
116
+ esac
117
+ }
118
+
119
+ # The startup files install manages. ~/.profile is always included: it serves
120
+ # sh, ksh and login bash. Files that belong to another shell are only created
121
+ # for the user's own shell, except ~/.zprofile, because zsh reads nothing else at
122
+ # login. Existing files are always kept current.
123
+ #
124
+ # posix_startup_targets <home>
125
+ # prints one path per line.
126
+ posix_startup_targets() {
127
+ printf '%s\n' "$1/.profile"
128
+ if [ -e "$1/.bash_profile" ] || [ -L "$1/.bash_profile" ]; then
129
+ printf '%s\n' "$1/.bash_profile"
130
+ elif [ -e "$1/.bash_login" ] || [ -L "$1/.bash_login" ]; then
131
+ printf '%s\n' "$1/.bash_login"
132
+ fi
133
+ case "${SHELL:-}" in
134
+ */bash | bash) printf '%s\n' "$1/.bashrc" ;;
135
+ *) if [ -e "$1/.bashrc" ] || [ -L "$1/.bashrc" ]; then printf '%s\n' "$1/.bashrc"; fi ;;
136
+ esac
137
+ if [ -e "$1/.zprofile" ] || [ -L "$1/.zprofile" ] || command -v zsh >/dev/null 2>&1; then
138
+ printf '%s\n' "$1/.zprofile"
139
+ fi
140
+ case "${SHELL:-}" in
141
+ */zsh | zsh) printf '%s\n' "$1/.zshrc" ;;
142
+ *) if [ -e "$1/.zshrc" ] || [ -L "$1/.zshrc" ]; then printf '%s\n' "$1/.zshrc"; fi ;;
143
+ esac
144
+ }
145
+
146
+ # Write the managed block to every startup file install manages.
147
+ write_posix_integration() {
148
+ if [ -z "${HOME:-}" ]; then
149
+ info "warning: HOME is not set; no startup file was written"
150
+ return 0
151
+ fi
152
+ _wpi_home=$(path_normalize "$HOME") || die "cannot resolve HOME"
153
+ _wpi_snippet=$(print_posix_integration) || die "cannot generate integration block"
154
+ _wpi_targets=$(posix_startup_targets "$_wpi_home")
155
+ while IFS= read -r _wpi_target; do
156
+ [ -n "$_wpi_target" ] || continue
157
+ project_write_integration "$_wpi_target" "$_wpi_snippet"
158
+ done <<EOF
159
+ $_wpi_targets
160
+ EOF
161
+ unset _wpi_home _wpi_snippet _wpi_targets _wpi_target
162
+ }
163
+
164
+ cmd_doctor() {
165
+ _status=0
166
+ _doctor_pre_install=0
167
+ _doctor_check_integration=1
168
+ for _doctor_arg do
169
+ case "$_doctor_arg" in
170
+ --pre-install) _doctor_pre_install=1; _doctor_check_integration=0 ;;
171
+ --no-integration) _doctor_check_integration=0 ;;
172
+ --help|-h) usage_doctor; return 0 ;;
173
+ *) die "unknown doctor option: $_doctor_arg (usage: valise doctor [--pre-install] [--no-integration])" ;;
174
+ esac
175
+ done
176
+
177
+ info "checking environment..."
178
+
179
+ # Check POSIX utilities
180
+ for _cmd in mkdir rm cp mv ln cat stat cmp find; do
181
+ if command -v "$_cmd" >/dev/null 2>&1; then
182
+ printf ' ok: %s\n' "$_cmd"
183
+ else
184
+ printf ' FAIL: missing required utility: %s\n' "$_cmd"
185
+ _status=1
186
+ fi
187
+ done
188
+ # file_sha256 accepts any one of these, found in a trusted location.
189
+ _doctor_sha=
190
+ for _cmd in sha256sum shasum sha256; do
191
+ _doctor_sha=$(trusted_utility_path "$_cmd" 2>/dev/null || :)
192
+ [ -z "$_doctor_sha" ] || break
193
+ done
194
+ if [ -n "$_doctor_sha" ]; then
195
+ printf ' ok: sha256 (via %s)\n' "$_doctor_sha"
196
+ else
197
+ printf ' FAIL: no SHA-256 utility (need sha256sum, shasum, or sha256)\n'
198
+ _status=1
199
+ fi
200
+
201
+ # Check XDG directories
202
+ for _dir in "$VALISE_XDG_DATA" "$VALISE_XDG_CONFIG" "$VALISE_XDG_STATE"; do
203
+ if [ -d "$_dir" ]; then
204
+ printf ' ok: directory exists: %s\n' "$_dir"
205
+ else
206
+ printf ' warn: directory missing (will create on install): %s\n' "$_dir"
207
+ fi
208
+ done
209
+
210
+ # Check active symlink
211
+ _doctor_active_ok=0
212
+ if [ -L "$VALISE_ACTIVE_LINK" ]; then
213
+ _target=$(active_release_dir 2>/dev/null || :)
214
+ if [ -n "$_target" ]; then
215
+ printf ' ok: active release: %s\n' "$_target"
216
+ _doctor_active_target=$_target
217
+ _doctor_active_ok=1
218
+ else
219
+ printf ' FAIL: active symlink does not resolve to a release directory\n'
220
+ _status=1
221
+ fi
222
+ elif [ -e "$VALISE_ACTIVE_LINK" ]; then
223
+ printf ' FAIL: active pointer is not a symlink: %s\n' "$VALISE_ACTIVE_LINK"
224
+ _status=1
225
+ elif [ "$_doctor_pre_install" -eq 1 ]; then
226
+ printf ' info: no active release configured\n'
227
+ else
228
+ # With no release there is nothing for a shell to load, so doctor reports
229
+ # the missing release as a failure. --pre-install is the explicit
230
+ # opt-out for a machine that has not installed a release yet.
231
+ printf ' FAIL: no active release configured\n'
232
+ _status=1
233
+ fi
234
+
235
+ # The active release must verify against its own manifest. A release that
236
+ # fails verification is not usable, so doctor reports it as a failure.
237
+ if [ "$_doctor_active_ok" -eq 1 ] && [ "$_doctor_pre_install" -eq 0 ]; then
238
+ _doctor_active_manifest="$_doctor_active_target/.valise-manifest"
239
+ if [ -f "$_doctor_active_manifest" ] && [ ! -L "$_doctor_active_manifest" ]; then
240
+ _doctor_active_id=${_doctor_active_target##*/}
241
+ if prepare_manifest_rows "$_doctor_active_manifest" &&
242
+ validate_manifest "$_manifest_rows"; then
243
+ _verify_quiet=1
244
+ if verify_release_dir "$_doctor_active_target" "$_manifest_rows" "$_doctor_active_id"; then
245
+ _verify_quiet=0
246
+ printf ' ok: active release verifies: %s\n' "$_doctor_active_id"
247
+ else
248
+ _verify_quiet=0
249
+ printf ' FAIL: active release does not verify: %s\n' "$_doctor_active_id"
250
+ _status=1
251
+ fi
252
+ else
253
+ printf ' FAIL: active release manifest is invalid: %s\n' "$_doctor_active_manifest"
254
+ _status=1
255
+ fi
256
+ else
257
+ printf ' FAIL: active release manifest is missing: %s\n' "$_doctor_active_manifest"
258
+ _status=1
259
+ fi
260
+ elif [ "$_doctor_pre_install" -eq 1 ]; then
261
+ printf ' info: skipping the active release check (--pre-install)\n'
262
+ fi
263
+ if [ "$_doctor_active_ok" -eq 1 ] && [ "$_doctor_check_integration" -eq 1 ]; then
264
+ doctor_commands "$_doctor_active_target"
265
+ fi
266
+
267
+ if [ -n "${HOME:-}" ]; then
268
+ # Check the startup files the running shell actually reads. The shell is
269
+ # taken from $SHELL, which is the same signal install uses to pick its
270
+ # per-shell startup file.
271
+ if [ "$_doctor_check_integration" -eq 1 ]; then
272
+ _doctor_shell=""
273
+ case "${SHELL:-}" in
274
+ */bash|bash) _doctor_shell=bash ;;
275
+ */zsh|zsh) _doctor_shell=zsh ;;
276
+ */ksh|ksh) _doctor_shell=ksh ;;
277
+ */fish|fish) _doctor_shell=fish ;;
278
+ esac
279
+ if [ -n "$_doctor_shell" ]; then
280
+ printf ' integration shell: %s\n' "$_doctor_shell"
281
+ else
282
+ printf ' integration shell: unknown (using ~/.profile)\n'
283
+ fi
284
+ _doctor_found=0
285
+ _doctor_candidates=""
286
+ # The candidate paths are read one per line. Word splitting on $(...)
287
+ # would break a home directory containing whitespace, so the list goes
288
+ # through a staging file instead.
289
+ _doctor_cand_file=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-doctor-candidates.XXXXXX") ||
290
+ die "cannot create doctor candidate staging file"
291
+ integration_shell_files "$_doctor_shell" "$HOME" >"$_doctor_cand_file"
292
+ while IFS= read -r _doctor_candidate; do
293
+ [ -n "$_doctor_candidate" ] || continue
294
+ _doctor_candidates="$_doctor_candidates $_doctor_candidate"
295
+ if [ -L "$_doctor_candidate" ]; then
296
+ printf ' warn: startup file is a symlink: %s\n' "$_doctor_candidate"
297
+ continue
298
+ fi
299
+ if [ ! -f "$_doctor_candidate" ]; then
300
+ printf ' info: startup file is not present: %s\n' "$_doctor_candidate"
301
+ continue
302
+ fi
303
+ if integration_has_managed_block "$_doctor_candidate"; then
304
+ printf ' ok: startup file contains the valise managed block: %s\n' "$_doctor_candidate"
305
+ _doctor_found=1
306
+ else
307
+ printf ' warn: startup file exists but has no valise managed block: %s\n' "$_doctor_candidate"
308
+ fi
309
+ done <"$_doctor_cand_file"
310
+ _doctor_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
311
+ "$_doctor_rm" -f "$_doctor_cand_file" || die "cannot remove doctor candidate staging file"
312
+ if ! _doctor_unmanaged=$(integration_unmanaged_report); then
313
+ printf ' FAIL: valise is loaded outside a managed block (it may load twice):\n'
314
+ printf ' %s\n' "$_doctor_unmanaged"
315
+ _status=1
316
+ fi
317
+ if [ "$_doctor_found" -eq 0 ]; then
318
+ printf ' FAIL: no valise integration found for %s (checked:%s)\n' \
319
+ "${_doctor_shell:-the current shell}" "$_doctor_candidates"
320
+ _status=1
321
+ fi
322
+ else
323
+ printf ' info: skipping the shell integration check\n'
324
+ fi
325
+ fi
326
+
327
+ # Platform check (informational only; install and release use are portable
328
+ # POSIX and not gated on any particular uname).
329
+ _uname=$(uname -s 2>/dev/null || echo unknown)
330
+ printf ' platform: %s\n' "$_uname"
331
+
332
+ if [ "$_status" -eq 0 ]; then
333
+ info "all checks passed"
334
+ else
335
+ die "some checks failed"
336
+ fi
337
+ }