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,308 @@
1
+ # Copyright (c) 2026 Joseph Cumines
2
+ # SPDX-License-Identifier: MIT
3
+ # valise CLI: per-command usage text.
4
+ # Sourced by bin/valise; not executable on its own.
5
+
6
+ usage() {
7
+ printf '%s\n' \
8
+ "Usage: valise <command> [options]" \
9
+ "" \
10
+ "Global option:" \
11
+ " -C <dir> Run as if valise was started in dir" \
12
+ "" \
13
+ "Project commands (run anywhere inside a directory holding valise.conf):" \
14
+ " new [dir] Scaffold a new valise project" \
15
+ " module <new|list|remove> Create, list, or remove project modules" \
16
+ " build [--variant=<name>] [--bundle=<name>] [out]" \
17
+ " Build a release tree (default: build/release)" \
18
+ " apply [--variant=<name>] [--bundle=<name>] [--no-integration] [--dry-run]" \
19
+ " Build the project, install it, and integrate it" \
20
+ " status Show project, active release, integration, and drift" \
21
+ " lint [--require-parsers] [module...] Check metadata, layers, bundles, and syntax" \
22
+ " test [--shell=<class>] [--require-shells] [module...] Run module tests" \
23
+ " run [--shell=<class>] [--module=<m>]... -- <script> Run a script with modules loaded" \
24
+ " env Print the engine, project, and storage paths" \
25
+ "" \
26
+ "Release commands:" \
27
+ " install [--no-integration] Install the release tree in the current directory" \
28
+ " release <list|use|rollback|prune|verify> Manage installed releases" \
29
+ " uninstall [--keep-releases] Remove the integration, commands, and releases" \
30
+ " init [posix|fish] Print the shell integration snippet" \
31
+ " doctor [--pre-install] [--no-integration] Run environment diagnostics" \
32
+ "" \
33
+ "Plumbing:" \
34
+ " compile [--bundle=<name>] [--variant=<name>] <output-dir> Compile plans only" \
35
+ " registry [path] Print the module table generated from metadata" \
36
+ " make-manifest <tree> [--output <file>] Write a release manifest for a tree" \
37
+ " explain-variant [--bundle=<name>] [name] Show a variant and its per-shell impact" \
38
+ " explain-bundles [--bundle=<name>] [--variant=<name>] Show each bundle's closure" \
39
+ " layer-lint <name> Validate a contribution layer structure" \
40
+ " version Print version" \
41
+ "" \
42
+ "Environment:" \
43
+ " XDG_DATA_HOME Release storage root (default: ~/.local/share)" \
44
+ " XDG_CONFIG_HOME Active pointer symlink (default: ~/.config)" \
45
+ " XDG_STATE_HOME Lock and journal state (default: ~/.local/state)" \
46
+ "" \
47
+ "Run 'valise <command> --help' for command-specific usage."
48
+ }
49
+
50
+ # Per-command usage. Each command prints its own contract instead of the
51
+ # global list, so `valise compile --help` names the arguments that command
52
+ # actually accepts.
53
+
54
+ usage_init() {
55
+ printf '%s\n' \
56
+ "usage: valise init [posix|fish] [--source-tree]" \
57
+ "" \
58
+ "Print the shell integration snippet for the active release. valise install" \
59
+ "and valise apply write it for you; use this to wire a startup file by hand." \
60
+ "" \
61
+ " posix A managed block for any POSIX shell. It loads the plan for" \
62
+ " the running shell class: ksh, zsh, bash, or sh for any other." \
63
+ " fish The Fish conf.d hook." \
64
+ " --source-tree Skip release verification in the Fish hook (development only)." \
65
+ "" \
66
+ "With no argument, fish when \$SHELL names fish, otherwise posix."
67
+ }
68
+
69
+ usage_install() {
70
+ printf '%s\n' \
71
+ "usage: valise install [--no-integration]" \
72
+ "" \
73
+ "Install the release tree in the current directory and make it active, then" \
74
+ "write the shell integration." \
75
+ "" \
76
+ "A managed block is written to ~/.profile; to the first of ~/.bash_profile and" \
77
+ "~/.bash_login that exists, because it shadows ~/.profile for bash; to ~/.bashrc" \
78
+ "and ~/.zshrc when that shell is \$SHELL or the file exists; and to ~/.zprofile" \
79
+ "when zsh is installed or the file exists. Symlinked files are left alone. A" \
80
+ "Fish conf.d hook is written when Fish is installed. Each install replaces the" \
81
+ "block in place. Each command the release publishes gets a stub in the command" \
82
+ "directory (see valise doctor)." \
83
+ "" \
84
+ "Every startup file is checked before the active release changes. If a later" \
85
+ "step fails, the previous active release is restored." \
86
+ "" \
87
+ " --no-integration Write no startup file, Fish hook, or command stubs."
88
+ }
89
+
90
+ usage_doctor() {
91
+ printf '%s\n' \
92
+ "usage: valise doctor [--pre-install] [--no-integration]" \
93
+ "" \
94
+ "Report the environment, the active release, and the shell integration." \
95
+ "" \
96
+ " --pre-install Skip the active-release and integration checks, for use" \
97
+ " before the first install." \
98
+ " --no-integration Check the environment and active release only."
99
+ }
100
+
101
+ usage_lint() {
102
+ printf '%s\n' \
103
+ "usage: valise lint [--require-parsers] [module...]" \
104
+ "" \
105
+ "Check the project: every metadata record and the files it names, each layer" \
106
+ "(contrib_ names, version=, dependencies inside the layer or the engine), the" \
107
+ "lint_layer_forbid pattern from valise.conf, every bundle compiled for every" \
108
+ "variant and shell, and the syntax of each body and test with the parser of" \
109
+ "every shell it declares. Listing modules limits the syntax checks to them." \
110
+ "" \
111
+ " --require-parsers Fail instead of skipping when a shell's parser is missing."
112
+ }
113
+
114
+ usage_test() {
115
+ printf '%s\n' \
116
+ "usage: valise test [--shell=<class>[,<class>...]] [--require-shells] [module...]" \
117
+ "" \
118
+ "Run the tests= scripts of each module (default: every module that has them)" \
119
+ "in every shell class it declares whose interpreter is installed: dash (or a" \
120
+ "plain sh) for sh, bash, zsh, ksh, and fish. Each run builds a release with" \
121
+ "the module as its only root, starts the shell with env -i, a fresh HOME," \
122
+ "TMPDIR and XDG directories, and PATH=\${VALISE_TEST_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}" \
123
+ "plus the interpreter's directory, loads the module in strict mode, then runs" \
124
+ "the test. A test passes when it exits 0. Fish tests (*.fish) run only in" \
125
+ "Fish, other tests only in POSIX shells. A test can write fixtures and call" \
126
+ "valise_test_reload to load the module again. VALISE_TEST_ROOT, VALISE_TEST_MODULE," \
127
+ "VALISE_TEST_MODULE_DIR, VALISE_TEST_SHELL and VALISE_PROJECT_ROOT are set." \
128
+ "" \
129
+ " --shell=<class> Run only these shell classes." \
130
+ " --require-shells Fail instead of skipping when an interpreter is missing."
131
+ }
132
+
133
+ usage_run() {
134
+ printf '%s\n' \
135
+ "usage: valise run [--shell=<class>] [--module=<m>]... [--bundle=<b>] [--variant=<v>] [--keep-env] -- <script> [args...]" \
136
+ "" \
137
+ "Load modules into a shell and run a script there; the exit status is the" \
138
+ "script's. With --module the named modules are the only roots; otherwise the" \
139
+ "bundle and variant (default: valise.conf) select them. The shell class" \
140
+ "defaults to the one \$SHELL names, else sh. The environment is quarantined as" \
141
+ "for valise test unless --keep-env is given."
142
+ }
143
+
144
+ usage_env() {
145
+ printf '%s\n' \
146
+ "usage: valise env" \
147
+ "" \
148
+ "Print KEY=value lines, shell-quoted, for VALISE_VERSION, VALISE_ENGINE_ROOT," \
149
+ "VALISE_PROJECT_ROOT (empty outside a project), VALISE_DATA_DIR," \
150
+ "VALISE_CONFIG_DIR, VALISE_STATE_DIR, VALISE_RELEASES_DIR, VALISE_ACTIVE_LINK," \
151
+ "and VALISE_BIN_DIR."
152
+ }
153
+
154
+ usage_uninstall() {
155
+ printf '%s\n' \
156
+ "usage: valise uninstall [--keep-releases]" \
157
+ "" \
158
+ "Remove what valise install wrote: the managed block from each startup file" \
159
+ "(every other line is kept, and a file install created is deleted when it is" \
160
+ "left empty), the Fish hook, the command stubs valise owns, the active link," \
161
+ "and the installed releases. Files valise does not own are left alone." \
162
+ "" \
163
+ " --keep-releases Keep the installed releases." \
164
+ "" \
165
+ "The valise CLI itself is not removed."
166
+ }
167
+
168
+ usage_release() {
169
+ printf '%s\n' \
170
+ "usage: valise release <list|use|rollback|prune|verify> [options]" \
171
+ "" \
172
+ " list List installed releases and mark the active one." \
173
+ " use <sha256> Verify a release and make it active." \
174
+ " rollback Verify the release that was active before the last" \
175
+ " switch and make it active again." \
176
+ " prune [--keep=N] Remove older releases, keeping the active one and the" \
177
+ " N newest by modification time. A release that fails" \
178
+ " verification is kept, not removed." \
179
+ " verify [--print-root] Verify the sealed active release and optionally" \
180
+ " print its root." \
181
+ "" \
182
+ "prune --keep=0 keeps only the active release."
183
+ }
184
+
185
+ usage_new() {
186
+ printf '%s\n' \
187
+ "usage: valise new [dir]" \
188
+ "" \
189
+ "Scaffold a new valise project in dir (default: the current directory)." \
190
+ "" \
191
+ "The directory must be empty or absent. The scaffold contains valise.conf," \
192
+ "bundles.tsv with sh, bash, zsh, ksh and fish rows, variants.tsv, the POSIX" \
193
+ "seed module profile_main and the Fish seed module profile_main_fish."
194
+ }
195
+
196
+ usage_module() {
197
+ printf '%s\n' \
198
+ "usage: valise module <new|list|remove> [options]" \
199
+ "" \
200
+ " new <name> [--exports=<list>] [--into=<module>[,...] | --no-wire]" \
201
+ " Create a POSIX module in module.d/ and add it" \
202
+ " to the requires= of the bundle's single POSIX" \
203
+ " root, of the named modules, or of none." \
204
+ " list List the project's and its layers' modules." \
205
+ " remove <name> Remove a module and its references in metadata," \
206
+ " bundle roots, and variant exclusions. Refuses" \
207
+ " if that would leave a bundle or module empty."
208
+ }
209
+
210
+ usage_build() {
211
+ printf '%s\n' \
212
+ "usage: valise build [--variant=<name>] [--bundle=<name>] [out]" \
213
+ "" \
214
+ "Build a release tree from the project in the current directory." \
215
+ "" \
216
+ " --variant=<name> Build variant whose exclusion list applies (default:" \
217
+ " valise.conf variant=)." \
218
+ " --bundle=<name> Bundle whose roots seed each plan (default: valise.conf" \
219
+ " bundle=)." \
220
+ " out Output directory (default: build/release). An existing" \
221
+ " default output is replaced after a successful build." \
222
+ " An explicit output must be empty or absent." \
223
+ "" \
224
+ "The tree holds plans/<shell>.sh for each POSIX shell the bundle names," \
225
+ "runtime/capabilities.sh, valise-release.conf, manifest.tsv, and the metadata" \
226
+ "and bodies of exactly the modules some plan loads. Engine modules are copied" \
227
+ "under valise/, layer modules under layers/<provider>/. A module that" \
228
+ "publishes commands adds plans/commands/<module>.sh and bin/<command>; Fish" \
229
+ "roots add runtime/fish/."
230
+ }
231
+
232
+ usage_apply() {
233
+ printf '%s\n' \
234
+ "usage: valise apply [--variant=<name>] [--bundle=<name>] [--no-integration] [--dry-run]" \
235
+ "" \
236
+ "Build the project, install the result, write the shell integration, and verify" \
237
+ "the active release. See 'valise install --help' for the files it writes." \
238
+ "" \
239
+ " --no-integration Write no startup file, Fish hook, or command stubs." \
240
+ " --dry-run Build, then print what install would change: the release," \
241
+ " each startup file, the Fish hook, and the command stubs." \
242
+ " Nothing outside build/ is written."
243
+ }
244
+
245
+ usage_status() {
246
+ printf '%s\n' \
247
+ "usage: valise status" \
248
+ "" \
249
+ "Report the project defaults, the integration in each startup file, the bundle," \
250
+ "variant and valise version the active release was built with, and whether the" \
251
+ "active release matches build/release."
252
+ }
253
+
254
+ usage_compile() {
255
+ printf '%s\n' \
256
+ "usage: valise compile [--bundle=<name>] [--variant=<name>] <output-dir>" \
257
+ "" \
258
+ "Compile the project's modules into <output-dir>/<shell>.sh plans for the" \
259
+ "POSIX shells the bundle names. Use build for an installable release." \
260
+ "" \
261
+ " --bundle=<name> Bundle whose roots seed each plan (default: valise.conf)." \
262
+ " --variant=<name> Variant whose exclusion list applies (default: valise.conf)." \
263
+ " output-dir Destination directory. It must be empty or absent."
264
+ }
265
+
266
+ usage_registry() {
267
+ printf '%s\n' \
268
+ "usage: valise registry [path]" \
269
+ "" \
270
+ "Print the module table valise builds from metadata: the project's module.d/," \
271
+ "each layers/<provider>/module.d/, then the engine's own modules under the" \
272
+ "source valise. The table is internal; nothing needs to commit it." \
273
+ "" \
274
+ " path Write the table to this file instead of standard output."
275
+ }
276
+
277
+ usage_explain_bundles() {
278
+ printf '%s\n' \
279
+ "usage: valise explain-bundles [--bundle=<name>] [--variant=<name>]" \
280
+ "" \
281
+ "Show the roots and the ordered module closure each bundle resolves to, per" \
282
+ "shell, with each module's source." \
283
+ "" \
284
+ " --bundle=<name> Report one bundle instead of all of them." \
285
+ " --variant=<name> Apply this variant's exclusions (default: valise.conf)."
286
+ }
287
+
288
+ usage_layer_lint() {
289
+ printf '%s\n' \
290
+ "usage: valise layer-lint <layer-name>" \
291
+ "" \
292
+ "Validate a contribution layer's structure, namespace, metadata, load paths," \
293
+ "file modes, and dependency closure." \
294
+ "" \
295
+ " layer-name Provider directory under layers/, or a path to a layer" \
296
+ " tree containing module.d/."
297
+ }
298
+
299
+ usage_explain_variant() {
300
+ printf '%s\n' \
301
+ "usage: valise explain-variant [--bundle=<name>] [variant-name]" \
302
+ "" \
303
+ "Show a variant's description, excluded modules, and how many of the bundle's" \
304
+ "roots it removes per shell." \
305
+ "" \
306
+ " --bundle=<name> Bundle to measure against (default: valise.conf)." \
307
+ " variant-name Variant from variants.tsv (default: valise.conf)."
308
+ }
package/lib/compiler.awk CHANGED
@@ -7,11 +7,13 @@
7
7
  # Lines starting with MOD: identity<TAB>requires<TAB>load_path<TAB>platforms<TAB>side_effect<TAB>exports<TAB>optional
8
8
  # identity is module@meta_source when registry sources are qualified.
9
9
  # Lines starting with PRELUDE: sp_cap function names (space-separated)
10
- # Output: complete installed plan on stdout.
10
+ # Output: complete installed plan on stdout. With -v mode=closure the
11
+ # compiler validates and orders the closure, then prints one module identity
12
+ # per line in load order instead of rendering a plan.
11
13
 
12
14
  BEGIN {
13
- prefix = "_shell_installed_plan_"
14
- prelude_dest = "vendor/shell-profile/lib/capabilities.sh"
15
+ prefix = "_valise_plan_"
16
+ prelude_dest = "runtime/capabilities.sh"
15
17
  seed_count = 0
16
18
  mod_count = 0
17
19
  prelude_fn_count = 0
@@ -306,8 +308,13 @@ END {
306
308
  }
307
309
  }
308
310
 
311
+ if (mode == "closure") {
312
+ for (oi = 1; oi <= ordered_count; oi++) print ordered[oi]
313
+ exit 0
314
+ }
315
+
309
316
  # --- Render plan ---
310
- printf "# shell-profile-installed-modules-v1\n"
317
+ printf "# valise-plan-v2\n"
311
318
  printf "# Generated by valise compile; DO NOT EDIT.\n"
312
319
  printf "# plan=%s\n", plan_name
313
320
  printf "# roots="
@@ -157,6 +157,12 @@ function _sp_fish_loader_function_copy_matches --no-scope-shadowing --descriptio
157
157
  end
158
158
  set -l original_declaration (string replace -r '^function [^[:space:]]+' 'function __source_backed_copy__' -- "$original_lines[2]")
159
159
  set -l copy_declaration (string replace -r '^function [^[:space:]]+' 'function __source_backed_copy__' -- "$copy_lines[2]")
160
+ # `functions --copy` drops --wraps, which every `alias` sets. The option
161
+ # only links completions; the body and every other option must still
162
+ # agree. A restore re-checks full provenance and fails closed.
163
+ set -l wraps_option ' --wraps=(\'[^\']*\'|"[^"]*"|[^[:space:]]+)'
164
+ set original_declaration (string replace -r -a -- $wraps_option '' "$original_declaration")
165
+ set copy_declaration (string replace -r -a -- $wraps_option '' "$copy_declaration")
160
166
  test "$original_declaration" = "$copy_declaration"; or return 1
161
167
  set -l copy_index 3
162
168
  while test $copy_index -le (builtin count $copy_lines)
@@ -1582,6 +1588,21 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
1582
1588
  set -a reserved_function_before (builtin functions --details "$reserved_name")
1583
1589
  end
1584
1590
 
1591
+ # Introspecting a name materializes Fish 3's autoloadable-but-undefined
1592
+ # functions into `functions --all --names`, so the pre-loop snapshot is
1593
+ # stale by the time the body runs. Re-capture the diff baseline after the
1594
+ # snapshot loop and before sourcing; otherwise Fish's own system functions
1595
+ # would be attributed to the body and published in the ownership ledger
1596
+ # with an unauthenticatable `/usr/share/fish` source. The snapshot copies
1597
+ # above are already defined, but are re-added defensively so a baseline
1598
+ # that failed to enumerate them can never classify them as body output.
1599
+ set -l function_names_before (builtin functions --all --names)
1600
+ for caller_function_snapshot in $caller_function_snapshot_copies
1601
+ if not builtin contains -- "$caller_function_snapshot" $function_names_before
1602
+ set -a function_names_before "$caller_function_snapshot"
1603
+ end
1604
+ end
1605
+
1585
1606
  # Snapshot private loader globals before sourcing. Module bodies are
1586
1607
  # allowed to publish their declared functions, not to rewrite the
1587
1608
  # loader's discovery roots, provider cache, or loaded set.
@@ -1975,14 +1996,22 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
1975
1996
  builtin set loader_state_changed 1
1976
1997
  end
1977
1998
  # Classify all protected-state and function mutations before rollback.
1978
- # A combined handshake mutation plus embedded `cd` mutation must never
1979
- # enter a return path that leaves the embedded function installed.
1999
+ # A combined handshake mutation plus `cd` mutation must never enter a
2000
+ # return path that leaves a replacement critical function installed.
2001
+ # Fish 4 embeds `cd`; Fish 3 loads it from the installation function
2002
+ # path. Both are Fish-provided definitions the loader cannot restore
2003
+ # with provenance after a body replaces them, so both fail closed.
1980
2004
  set -l preflight_embedded_mutation ''
1981
2005
  set -l preflight_embedded_index 1
1982
2006
  for preflight_reserved_name in $reserved_function_names
1983
2007
  set -l preflight_before_source "$reserved_function_before[$preflight_embedded_index]"
1984
2008
  set -l preflight_after_source (builtin functions --details "$preflight_reserved_name")
1985
- if builtin string match -q 'embedded:*' -- "$preflight_before_source"; and not builtin test "$preflight_after_source" = "$preflight_before_source"
2009
+ set -l preflight_before_dir (builtin path dirname -- "$preflight_before_source")
2010
+ set -l preflight_fish_provided 0
2011
+ if builtin string match -q 'embedded:*' -- "$preflight_before_source"; or builtin test "$preflight_before_source" = builtin; or builtin contains -- "$preflight_before_dir" /usr/share/fish/functions /usr/local/share/fish/functions /opt/local/share/fish/functions /opt/homebrew/share/fish/functions
2012
+ set preflight_fish_provided 1
2013
+ end
2014
+ if builtin test $preflight_fish_provided -eq 1; and not builtin test "$preflight_after_source" = "$preflight_before_source"
1986
2015
  set preflight_embedded_mutation "$preflight_reserved_name"
1987
2016
  break
1988
2017
  end
@@ -2175,8 +2204,24 @@ function _sp_fish_loader_load_ordered_impl --description 'Load modules from seed
2175
2204
  set -g _sp_fish_loaded_modules $_sp_fish_loaded_modules $mod
2176
2205
  for owned_function in $new_function_names $mod_exports
2177
2206
  if test -n "$owned_function"; and not builtin contains -- $owned_function $_sp_fish_loader_owned_exports
2207
+ set -l owned_source (builtin functions --details "$owned_function")
2208
+ # Fish's own system functions are not module state. A body can
2209
+ # materialize them by sourcing a completion or function file or
2210
+ # by running completions, but the ledger must not claim them:
2211
+ # their source is not an authenticated release path, so the next
2212
+ # activation would fail closed, and its cleanup would erase real
2213
+ # Fish functions. Fish 3 reports a file under the installation
2214
+ # prefix; Fish 4 reports an embedded: path. A module's own
2215
+ # functions report their body file (or "-" for an alias), so they
2216
+ # are unaffected. Rollback still removes these from
2217
+ # new_function_names because, for a failed body, they are this
2218
+ # activation's residue.
2219
+ switch "$owned_source"
2220
+ case '/usr/share/fish/*' '/usr/local/share/fish/*' '/opt/local/share/fish/*' '/opt/homebrew/share/fish/*' 'embedded:completions/*' 'embedded:functions/*' 'autoloaded:*'
2221
+ continue
2222
+ end
2178
2223
  set -a _sp_fish_loader_owned_exports $owned_function
2179
- set -a _sp_fish_loader_owned_sources (builtin functions --details "$owned_function")
2224
+ set -a _sp_fish_loader_owned_sources $owned_source
2180
2225
  end
2181
2226
  end
2182
2227
  end
@@ -2235,13 +2280,28 @@ function _sp_fish_loader_init --description 'Initialize the Fish loader with sta
2235
2280
  # providers, followed by engine generic metadata and project generic
2236
2281
  # metadata. Duplicate names are accepted only when metadata and native
2237
2282
  # Fish bodies are byte-identical; conflicting providers fail closed.
2283
+ # A built release (valise-release.conf beside runtime/) ships its loader at
2284
+ # runtime/fish/loader.fish, so ../.. is the release root. Its providers are
2285
+ # the primary modules, each layers/<provider>, then the engine modules
2286
+ # under valise/, in the order valise build resolves them.
2287
+ if test -n "$engine_root"; and test -f "$engine_root/valise-release.conf"
2288
+ if test -d "$engine_root/module.d"
2289
+ sp_fish_loader_add_root "$engine_root"; or return 1
2290
+ end
2291
+ for layer_root in "$engine_root"/layers/*
2292
+ if test -d "$layer_root/module.d"
2293
+ sp_fish_loader_add_root "$layer_root"; or return 1
2294
+ end
2295
+ end
2296
+ if test -d "$engine_root/valise/module.d"
2297
+ sp_fish_loader_add_root "$engine_root/valise"; or return 1
2298
+ end
2299
+ return 0
2300
+ end
2301
+
2238
2302
  set -l project_root ''
2239
2303
  if set -q SHELL_PROJECT_ROOT
2240
- if test -d "$SHELL_PROJECT_ROOT/shell/module.d"; and test -d "$SHELL_PROJECT_ROOT/shell/modules"
2241
- set project_root "$SHELL_PROJECT_ROOT/shell"
2242
- else
2243
- set project_root "$SHELL_PROJECT_ROOT"
2244
- end
2304
+ set project_root "$SHELL_PROJECT_ROOT"
2245
2305
  end
2246
2306
  if test -n "$project_root"; and test -d "$project_root/modules/fish"
2247
2307
  sp_fish_loader_add_root "$project_root/modules/fish"; or return 1
@@ -76,7 +76,7 @@ function sp_fish_metadata_validate_shell_list --description 'Validate shell list
76
76
  end
77
77
  set seen 1
78
78
  switch "$sh"
79
- case sh bash zsh ksh fish
79
+ case posix sh bash zsh ksh fish
80
80
  # valid
81
81
  case '*'
82
82
  sp_fish_metadata_error "invalid shell: $sh"
@@ -190,7 +190,10 @@ function sp_fish_metadata_read --description 'Read and validate a .meta file, ou
190
190
  set -l m_platforms ''
191
191
  set -l m_side_effect ''
192
192
  set -l m_exports ''
193
- set -l m_updates ''
193
+ set -l m_commands ''
194
+ set -l m_executables ''
195
+ set -l m_tests ''
196
+ set -l m_seen
194
197
  set -l m_version ''
195
198
  set -l m_load ''
196
199
 
@@ -206,6 +209,11 @@ function sp_fish_metadata_read --description 'Read and validate a .meta file, ou
206
209
  end
207
210
  set -l key (string split -m1 '=' -- "$line")[1]
208
211
  set -l val (string split -m1 '=' -- "$line")[2]
212
+ if contains -- "$key" $m_seen
213
+ sp_fish_metadata_error "duplicate metadata field: $key"
214
+ return 1
215
+ end
216
+ set -a m_seen "$key"
209
217
  switch "$key"
210
218
  case name; set m_name $val
211
219
  case requires; set m_requires $val
@@ -214,7 +222,12 @@ function sp_fish_metadata_read --description 'Read and validate a .meta file, ou
214
222
  case platforms; set m_platforms $val
215
223
  case side_effect; set m_side_effect $val
216
224
  case exports; set m_exports $val
217
- case updates; set m_updates $val
225
+ case commands; set m_commands $val
226
+ case executables; set m_executables $val
227
+ case tests; set m_tests $val
228
+ case updates
229
+ sp_fish_metadata_error 'metadata field updates is not supported since valise 0.3.0; publish commands with commands= or executables='
230
+ return 1
218
231
  case version; set m_version $val
219
232
  case load; set m_load $val
220
233
  case load_hook
@@ -275,9 +288,38 @@ function sp_fish_metadata_read --description 'Read and validate a .meta file, ou
275
288
  if not sp_fish_metadata_validate_identifier_list "$m_exports" export
276
289
  return 1
277
290
  end
278
- if not sp_fish_metadata_validate_identifier_list "$m_updates" update_registration
291
+ if not sp_fish_metadata_validate_identifier_list "$m_commands" command
279
292
  return 1
280
293
  end
294
+ for cmd in (string split ' ' -- $m_commands)
295
+ test -n "$cmd"; or continue
296
+ if not contains -- "$cmd" (string split ' ' -- $m_exports)
297
+ sp_fish_metadata_error "command is not a declared export: $cmd"
298
+ return 1
299
+ end
300
+ end
301
+ for kind in executable test
302
+ set -l paths $m_executables
303
+ test "$kind" = test; and set paths $m_tests
304
+ if string match -qr '[*?\[\]\\\\]' -- "$paths"
305
+ sp_fish_metadata_error "invalid $kind: contains glob metacharacter"
306
+ return 1
307
+ end
308
+ for p in (string split ' ' -- $paths)
309
+ test -n "$p"; or continue
310
+ if string match -qr '(^|/)\.(/|$)|//|/$' -- "$p"
311
+ sp_fish_metadata_error "invalid $kind path: $p"
312
+ return 1
313
+ end
314
+ if not sp_fish_metadata_validate_relative_path "$p"
315
+ return 1
316
+ end
317
+ if test "$kind" = executable; and not string match -qr '^[A-Za-z0-9_]' -- (string split -r -m1 / -- "$p")[-1]
318
+ sp_fish_metadata_error "invalid executable name: "(string split -r -m1 / -- "$p")[-1]
319
+ return 1
320
+ end
321
+ end
322
+ end
281
323
  if not sp_fish_metadata_validate_version "$m_version"
282
324
  return 1
283
325
  end
@@ -292,7 +334,9 @@ function sp_fish_metadata_read --description 'Read and validate a .meta file, ou
292
334
  echo "platforms=$m_platforms"
293
335
  echo "side_effect=$m_side_effect"
294
336
  echo "exports=$m_exports"
295
- echo "updates=$m_updates"
337
+ echo "commands=$m_commands"
338
+ echo "executables=$m_executables"
339
+ echo "tests=$m_tests"
296
340
  echo "version=$m_version"
297
341
  echo "load=$m_load"
298
342
  return 0