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
@@ -1,15 +1,17 @@
1
- # Shell-profile module ABI
1
+ # Module ABI
2
2
 
3
- This document defines the metadata consumed by the loader before it sources
4
- module bodies. Metadata is data: discovery does not execute module
5
- implementations to learn their names, dependencies, or supported runtimes.
3
+ This document defines module metadata and the rules the compiled plans and
4
+ the loaders apply to it. Metadata is data: valise never executes a module to
5
+ learn its name, dependencies, or supported shells.
6
6
 
7
7
  ## Metadata file format
8
8
 
9
9
  Module metadata lives in `module.d/*.meta` as UTF-8 text with one `key=value`
10
10
  field per line. Blank lines and `#` comments are ignored. Values are plain text;
11
11
  there is no shell quoting, interpolation, command substitution, glob expansion,
12
- or `eval` phase.
12
+ or `eval` phase. Each field may appear once: a repeated key is rejected rather
13
+ than letting the last value win. An unknown field is rejected, and so is
14
+ `updates`, which was removed in 0.3.0.
13
15
 
14
16
  Required fields:
15
17
 
@@ -19,8 +21,8 @@ Required fields:
19
21
  | `shells` | Space-separated supported shell identifiers. |
20
22
  | `platforms` | Space-separated supported platform identifiers. |
21
23
  | `side_effect` | Startup/install/update side-effect class. |
22
- | `load` | Repository-relative path to the module body. |
23
- | `requires` | Required when the module is a **meta module** (see below). |
24
+ | `load` | Path to the module body, relative to the module's source directory. Absent for a **meta module** (see below). |
25
+ | `requires` | Required for a meta module. |
24
26
 
25
27
  Optional fields:
26
28
 
@@ -29,7 +31,9 @@ Optional fields:
29
31
  | `requires` | Space-separated required dependency module identifiers. |
30
32
  | `optional` | Space-separated optional dependency module identifiers. |
31
33
  | `exports` | Space-separated exported command/function identifiers. |
32
- | `updates` | Space-separated update identifiers registered by the consuming project. |
34
+ | `commands` | Space-separated exports a built release publishes as commands on PATH. |
35
+ | `executables` | Space-separated module-relative paths of scripts published as commands. |
36
+ | `tests` | Space-separated module-relative test scripts that `valise test` runs. |
33
37
  | `version` | Optional module version used to reject version skew across roots. |
34
38
 
35
39
  ## Meta modules
@@ -49,12 +53,11 @@ an export on a meta module is rejected.
49
53
  name=profile_main
50
54
  version=1.0.0
51
55
  side_effect=read-only
52
- requires=contrib_path contrib_editor
56
+ requires=valise_path valise_editor
53
57
  optional=
54
- shells=bash zsh ksh
58
+ shells=posix
55
59
  platforms=any
56
60
  exports=
57
- updates=
58
61
  ```
59
62
 
60
63
  At least one of `load` or `requires` is required: a module with neither a
@@ -75,67 +78,74 @@ whitespace inside one identifier, path separators, quotes, shell metacharacters,
75
78
  glob characters, and control characters are invalid. Invalid identifiers are
76
79
  rejected before the loader can build function names or source a body.
77
80
 
78
- Accepted `shells` values are `sh`, `bash`, `zsh`, `ksh`, and `fish`.
79
- `shells=sh` means the module body is POSIX-shell syntax, and it is loaded by
80
- **every POSIX shell** — `sh`, `bash`, `zsh`, `ksh`, and dash — because those
81
- shells all run POSIX syntax. The engine classifies a running shell as `bash`,
82
- `zsh`, or `ksh` when it can prove it, and as `sh` otherwise (dash, BusyBox
83
- ash, and any other POSIX shell); `shells=sh` matches both the `sh` class and
84
- the specific classes, so a `shells=sh` module loads in all of them. Write
85
- portable bodies for `shells=sh` and you cover every POSIX shell at once.
86
-
87
- `fish` is the exception: it selects the native Fish surface under
88
- `lib/fish/`, and the Fish loader never sources POSIX shell bodies. A module
89
- whose `shells=` list does not contain `fish` is skipped under Fish rather
90
- than failed — a directly requested module fails closed in strict mode.
91
-
92
- The engine ships a generic Fish adapter at `entrypoints/fish/profile.fish` as
93
- the reference implementation of this handshake: it sets
94
- `_sp_fish_loader_bootstrap_mode` and `_sp_fish_loader_bootstrap_seeds`, then sources `loader.fish`. The loader
95
- consumes and clears that request and publishes a fresh
96
- `_sp_fish_loader_bootstrap_status`; callers must fail closed when the marker is
97
- absent. Fish provider precedence is project-native, engine-native, engine
98
- generic, and project generic. Duplicate providers are valid only when their
99
- metadata and native bodies are byte-identical; conflicting metadata or bodies
100
- is rejected before any body is sourced. Public adapters must not execute the
101
- generated Fish plan directly.
81
+ Accepted `shells` values are `posix`, `sh`, `bash`, `zsh`, `ksh`, and
82
+ `fish`. Each running POSIX shell belongs to one class: `bash`, `zsh`, or
83
+ `ksh` when the shell can be proven to be that shell, and `sh` for every
84
+ other POSIX shell (dash, BusyBox ash, and so on). The values match classes
85
+ as follows:
86
+
87
+ - `posix` matches every POSIX class: `sh`, `bash`, `zsh`, and `ksh`. Use it
88
+ for a body written in portable shell.
89
+ - `sh` matches only the `sh` class. Combine it with the shells the body also
90
+ supports: `shells=sh bash ksh` loads in dash, bash, and ksh, and is skipped
91
+ in zsh, for a body that needs word splitting zsh does not do.
92
+ - `bash`, `zsh`, and `ksh` match only that class.
93
+ - `fish` selects the native Fish runtime. Fish never sources a POSIX body,
94
+ and no POSIX value matches Fish.
95
+
96
+ A module whose shells do not include the running class is skipped; a bundle
97
+ root, or a module a root requires, that does not support its shell is an
98
+ error.
99
+
100
+ In a built release, the native Fish loader resolves the release's Fish roots
101
+ (`roots_fish=` in `valise-release.conf`) against the release's `module.d/`,
102
+ each `layers/<provider>/module.d/`, and `valise/module.d/`, in that order.
103
+ `runtime/fish/profile.fish` is the adapter the Fish hook sources: it sets
104
+ `_sp_fish_loader_bootstrap_mode` and `_sp_fish_loader_bootstrap_seeds`,
105
+ sources `runtime/fish/loader.fish`, and requires the
106
+ `_sp_fish_loader_bootstrap_status` marker the loader publishes, failing
107
+ closed when it is absent. Duplicate providers of one name are valid only when
108
+ their metadata and Fish bodies are byte-identical.
102
109
 
103
110
  Supported `platforms` values are `any`, `darwin`, `linux`, `bsd`, `freebsd`,
104
111
  `openbsd`, and `netbsd`. `platforms=any` matches every supported platform.
105
112
  Specific platform values are ORed: `platforms=darwin linux` matches macOS or
106
113
  Linux, not BSD.
107
114
 
108
- ## Multi-root layer search path
109
-
110
- The loader resolves modules across a priority-ordered root list. The primary
111
- root is the `sp_loader_load_modules <root> ...` argument and always wins for
112
- metadata it provides. `VALISE_MODULE_PATH` lists additional layer roots as a
113
- colon-separated path, searched in priority order after the primary root.
114
- With it unset the behavior is exactly the historical single-root
115
- behavior.
116
-
117
- Semantics, enforced by `tests/loader/multi-root.test.sh`:
118
-
119
- - Every search-path element must be a non-empty readable, executable directory; empty
120
- elements, missing, unreadable, or unsearchable directories are fail-closed
121
- data errors (element validation runs with globbing disabled, so
122
- glob-metacharacter elements are not expanded and must name a real readable,
123
- executable directory). Identical duplicate elements are deduplicated
124
- at validation; priority belongs to the first occurrence.
125
- - A module name present in multiple roots must have identical metadata and
126
- body bytes. Identical copies resolve to the highest-priority root; any
127
- difference is an error naming both paths. A layer cannot silently replace
128
- a module from the primary root or an earlier layer.
129
- - A module provided only by a layer root must carry the `contrib_` name
130
- prefix. Non-prefixed layer-only modules fail closed with a diagnostic
131
- naming the layer.
132
- - Module bodies load from the root that provided the metadata; cross-root
133
- dependency discovery works in both directions (layer modules may require
134
- primary modules), and deterministic `sort -u` + topological ordering is
135
- preserved across roots.
136
- - The duplicate-export fail-closed check is global across roots: a layer
137
- module may not re-export an identifier already declared by a loaded module.
138
- - No loader-private state leaks after multi-root loads.
115
+ ## Layers
116
+
117
+ A project composes provider modules by vendoring the provider's tree as
118
+ `layers/<provider>/` (its `module.d/` and bodies). Every module in a layer
119
+ uses the `contrib_` prefix, and the `valise_` prefix belongs to the engine.
120
+ Dependencies resolve by name: the same source first, then the only module
121
+ with that name; a name defined in several sources must be written as
122
+ `name@source` where it is used as a root or an exclusion. A built release
123
+ copies the layer modules it loads to `layers/<provider>/`, and one manifest
124
+ covers the project, layer, and engine files together.
125
+
126
+ To update a provider, copy a reviewed revision into `layers/<provider>/`,
127
+ review the diff, and rebuild; record the provider revision beside it if you
128
+ need to know later which one you have. The manifest proves that the
129
+ installed files match the release, not who wrote them.
130
+
131
+ ### The development loader's search path
132
+
133
+ The POSIX loader in `lib/loader.sh`, used by `entrypoints/profile.sh` from an
134
+ engine checkout, resolves modules across a priority-ordered root list
135
+ instead: the primary root first, then the colon-separated
136
+ `VALISE_MODULE_PATH`. Its rules, enforced by
137
+ `tests/loader/multi-root.test.sh`:
138
+
139
+ - Every element must be a non-empty, readable, searchable directory. An
140
+ empty element, a missing directory, a newline, or any of the glob
141
+ characters `[`, `?`, and `*` is rejected. Duplicate elements are
142
+ deduplicated; the first occurrence keeps its priority.
143
+ - A module present in several roots must have identical metadata and body
144
+ bytes. Identical copies resolve to the highest-priority root; any
145
+ difference is an error naming both paths.
146
+ - A module provided only by a layer root must carry the `contrib_` prefix.
147
+ - Bodies load from the root that provided the metadata. Dependencies resolve
148
+ across roots in both directions, and the duplicate-export check is global.
139
149
 
140
150
  ## Versioning
141
151
 
@@ -143,10 +153,11 @@ Shared modules carry an optional `version` field so independent releases
143
153
  of the same name can be told apart. The value must start with a digit
144
154
  and contain only digits, letters, `.`, `_`, `-`; an empty value means
145
155
  unversioned. Providers should bump the version for a breaking change.
146
- Consumers vendor a specific provider snapshot; the loader does not resolve
147
- version ranges. See [Cross-repo composition](#cross-repo-composition).
156
+ Consumers vendor a specific provider snapshot; nothing resolves version
157
+ ranges. See [Layers](#layers).
148
158
 
149
- Enforcement, in both the POSIX loader and the native Fish loader:
159
+ Enforcement, where one name has several providers (the development loader's
160
+ roots, or the Fish loader's providers):
150
161
 
151
162
  - The same name with different valid versions across roots fails closed
152
163
  with a version-skew diagnostic naming both versions and both paths,
@@ -160,32 +171,6 @@ A versioned record that disagrees with itself is not a version at all:
160
171
  malformed versions are rejected by `sp_metadata_read` before any body
161
172
  is sourced.
162
173
 
163
- ## Cross-repo composition
164
-
165
- The supported composition is a primary consumer root plus one
166
- version-pinned layer root per provider.
167
- The provider tree is vendored under `layer/<name>/` of the release
168
- (preserving its internal `module.d/` + `modules/` layout), and a single
169
- manifest covers the whole closure, so `release verify` checks consumer
170
- and provider bytes together before anything is sourced.
171
-
172
- - Pinning: record the provider source and revision beside the vendored
173
- bytes (the e2e example writes `layer/shared/layer-pin.txt` from
174
- `git rev-parse HEAD` at vendor time). Re-pinning means copying the
175
- provider at a new revision, reviewing the diff, and rebuilding the
176
- manifest. Pin by git SHA and review the diff; the seal proves the
177
- release matches its manifest, not who authored it.
178
- - Loading: the consumer sets `VALISE_MODULE_ROOT` to the release and
179
- `VALISE_MODULE_PATH` to the layer directory inside it. Deployed login
180
- snippets carry only the primary root; the layer path is a consumer
181
- setting applied when sourcing the entrypoint.
182
- - Naming: layer-only modules must carry the `contrib_` prefix, and
183
- same-name modules across roots must be byte-identical or the load
184
- fails closed naming both paths. Dependencies resolve across the
185
- boundary in both directions: layer modules may require primary
186
- modules, and primary modules may follow optional dependencies into
187
- the layer.
188
-
189
174
  ## Runtime detection contract
190
175
 
191
176
  The loader classifies the running shell and platform before matching module
@@ -270,30 +255,42 @@ Only the module for the running platform loads. The other is skipped without a
270
255
  message, because its absence is expected. The POSIX loader and compiled plans
271
256
  follow the same rules.
272
257
 
273
- `exports` lists public commands or shell functions the module declares. The loader
274
- uses this list for post-load export checks and failure cleanup; project catalogs
275
- and wrapper manifests use it for documentation and registration. An exported name
258
+ `exports` lists the public functions the module defines. Plans and loaders use
259
+ it for the pre-load conflict check, the post-load check, and failure cleanup.
260
+ An exported name
276
261
  must be defined by the module body before the module is considered
277
262
  successfully loaded. An export must name a function, so shell reserved words
278
263
  (`case`, `do`, `done`, `elif`, `else`, `esac`, `fi`, `for`, `if`, `in`,
279
264
  `then`, `until`, `while`, and `time`) are rejected.
280
265
 
281
- `updates` lists update registrations declared by the module. Each value is an ABI
282
- identifier that maps one-to-one to a row in the project update manifest. The
283
- project validator rejects missing, duplicate, or unowned registrations. URL,
284
- destination, digest, trust policy, and rollback state belong in that manifest;
285
- module metadata states registration only and does not execute update lifecycle work.
286
-
287
- ## Update channel for shared modules
288
-
289
- Shared modules are vendored snapshots. `valise` does not fetch provider
290
- updates. To update a provider, vendor a reviewed revision and rebuild the
291
- release manifest. Bump `version=` when making an incompatible module change;
292
- the loader rejects conflicting versions of the same module across roots.
293
- The manifest detects changes to installed files but does not authenticate
294
- their author; see [installation.md](installation.md). The `updates=` field
295
- is registration for a consuming project's separate update manifest, not an
296
- engine update mechanism.
266
+ ### Published commands
267
+
268
+ `commands` names exports that should also run as ordinary commands, from any
269
+ program and not only from an interactive shell. Every entry must also appear in
270
+ `exports`. `executables` names standalone scripts in the module's directory;
271
+ each is published under its file name, which must start with a letter, digit,
272
+ or underscore and contain only letters, digits, `.`, `_`, and `-`. The
273
+ metadata field `updates` was removed in 0.3.0; a metadata file that still sets
274
+ it is rejected.
275
+
276
+ `valise build` turns both into entries under the release's `bin/`:
277
+
278
+ - For a module with `commands`, it compiles `plans/commands/<module>.sh`, a
279
+ plan whose only root is that module, for the module's command shell: `sh`
280
+ when the module supports `posix` or `sh`, otherwise the first of `bash`,
281
+ `zsh`, and `ksh` it lists. Each command gets a launcher `bin/<command>` that
282
+ runs under that shell, loads the plan in strict mode from the release that
283
+ contains it, restores the caller's `VALISE_MODE`, and calls the function
284
+ with the command's arguments. The function's exit status is the command's.
285
+ - Each `executables` path is copied to `bin/<file name>` and keeps its own
286
+ interpreter line.
287
+
288
+ The plan's closure ships in the release even when no shell bundle loads it.
289
+ A Fish-only module cannot declare `commands`; it may declare `executables`.
290
+ Two modules that publish the same command name, from either field, fail the
291
+ build. `valise-release.conf` lists the published names in `commands=`.
292
+ Installation puts a stub for each name on PATH; see
293
+ [installation.md](installation.md#published-commands).
297
294
 
298
295
  ## Side-effect phases
299
296
 
@@ -315,12 +312,13 @@ before ordinary source invokes a load path.
315
312
 
316
313
  ## Capability layer available to modules
317
314
 
318
- The POSIX entrypoint sources `lib/capabilities.sh` before it calls the loader.
319
- Portable module bodies may therefore depend on `sp_cap_*` helpers without
320
- sourcing OS-specific files themselves. This is the ABI boundary for behavior that
321
- varied in the flat monolith: CPU count, file mtime, temp directories/files, command
322
- lookup, OS detection, canonical path containment, symlink-component rejection,
323
- Homebrew path discovery, secure local-bin permission checks, and background
315
+ Every plan sources the capability prelude (`runtime/capabilities.sh` in a
316
+ release, `lib/capabilities.sh` in the engine) before any module, and the
317
+ development entrypoint does the same before the loader. Module bodies may
318
+ therefore call the `sp_cap_*` helpers without sourcing platform files
319
+ themselves: CPU count, file mtime, temporary directories and files, command
320
+ lookup, OS detection, canonical path containment, symlink-component
321
+ rejection, Homebrew discovery, secure local-bin checks, and background
324
322
  process launch.
325
323
 
326
324
  **The helpers stay available after the profile loads.** A compiled plan
@@ -344,15 +342,15 @@ Read-only/status paths may degrade only by quietly skipping unsupported details
344
342
  or by returning a clear nonzero status with a diagnostic from the caller.
345
343
 
346
344
  Platform modules under `modules/platform/` are thin wrappers around the library.
347
- They expose stable module dependencies (`platform_posix`, `platform_macos`,
348
- `platform_linux`, and `platform_bsd`) while keeping compatibility logic in one
345
+ They expose stable module dependencies (`valise_platform_posix`, `valise_platform_macos`,
346
+ `valise_platform_linux`, and `valise_platform_bsd`) while keeping compatibility logic in one
349
347
  library.
350
348
 
351
349
  ## Load target and loader mode
352
350
 
353
- For file-backed modules, `load` is the repository-relative shell file sourced by
354
- the loader after metadata validation, dependency sorting, shell/platform matching,
355
- and phase side-effect checks have passed. The ABI has no unload hook: the loader
351
+ For file-backed modules, `load` is the shell file sourced after metadata
352
+ validation, dependency ordering, shell and platform matching, and the
353
+ side-effect checks have passed. The ABI has no unload hook: the loader
356
354
  does not implement module rollback or unload semantics.
357
355
 
358
356
  A meta module has no `load`; it has `requires` and no body. Nothing is
@@ -388,7 +386,8 @@ Permissive mode reports the affected module and continues with unrelated modules
388
386
 
389
387
  ## Path fields
390
388
 
391
- `load` paths are repository-relative implementation paths. Absolute
389
+ `load`, `executables`, and `tests` paths are relative to the module's source
390
+ directory (the project root, `layers/<provider>/`, or the engine). Absolute
392
391
  paths, `..` path segments, spaces, quotes, shell metacharacters, and glob
393
392
  characters are invalid. Reading metadata must not source these paths.
394
393
 
@@ -0,0 +1,249 @@
1
+ # Projects, bundles, variants, and builds
2
+
3
+ This document describes a valise project: the files it contains, how
4
+ `valise compile` and `valise build` turn them into plans and a release, and
5
+ how those commands fail. [module-abi.md](module-abi.md) covers the metadata
6
+ format; [installation.md](installation.md) covers what happens to a release
7
+ once it is built.
8
+
9
+ ## The project layout
10
+
11
+ A project is a directory that holds `valise.conf`:
12
+
13
+ ```
14
+ valise.conf project settings
15
+ module.d/ primary modules: <name>.meta
16
+ modules/ primary module bodies (any layout below modules/)
17
+ layers/<provider>/ vendored provider layers (optional)
18
+ module.d/ layer modules: contrib_<name>.meta
19
+ modules/ layer module bodies
20
+ bundles.tsv the root modules of each bundle, per shell
21
+ variants.tsv build variants and what each leaves out (optional)
22
+ build/ build output; keep it out of version control
23
+ ```
24
+
25
+ `valise new [dir]` writes this layout with a POSIX seed module
26
+ `profile_main`, a Fish seed module `profile_main_fish`, a `bundles.tsv` with
27
+ `sh`, `bash`, `zsh`, `ksh`, and `fish` rows, `variants.tsv`, and a
28
+ `.gitignore` for `build/`.
29
+
30
+ Project commands find the project by walking up from the working directory to
31
+ the first directory that holds `valise.conf`. `valise -C <dir> <command>` runs
32
+ a command as if it had been started in `dir`.
33
+
34
+ `valise.conf` has one `key=value` per line. Blank lines and `#` comments are
35
+ ignored; an unknown or repeated key is an error.
36
+
37
+ | Key | Meaning |
38
+ |---|---|
39
+ | `bundle` | Default bundle for build, apply, compile, and run (default `default`). |
40
+ | `variant` | Default variant (default `default`). |
41
+ | `requires_valise` | Oldest valise version that may build the project, such as `0.3.0`. An older valise refuses with `this project requires valise >= ...`. |
42
+ | `lint_layer_forbid` | Extended regular expression that must not match anything under `layers/` (case-insensitive). `valise lint` enforces it; use it to keep personal or employer material out of a layer you share. |
43
+
44
+ A layer's directory name is made of letters, digits, `_`, `-`, and `.`, and
45
+ may not start with `.` or contain `..`. Metadata and bodies must be regular
46
+ files reached without a symlink.
47
+
48
+ ## Modules and the registry
49
+
50
+ The `.meta` files are the only source of truth. valise reads them as data,
51
+ never by sourcing them. Each build collects three groups of modules:
52
+
53
+ 1. the project's `module.d/`, with source `module.d`;
54
+ 2. each `layers/<provider>/module.d/`, with source
55
+ `layers/<provider>/module.d`;
56
+ 3. the engine's own modules, with source `valise`.
57
+
58
+ The engine modules (`valise_path`, `valise_editor`, `valise_history`,
59
+ `valise_pager`, `valise_xdg`, and the `valise_platform_*` modules) are always
60
+ available. They ship in a release only when the release loads them. The
61
+ `valise_` prefix is reserved for them, and every layer module must use the
62
+ `contrib_` prefix.
63
+
64
+ `valise registry [path]` prints the table valise builds from these files, for
65
+ inspection. It is internal: nothing reads a saved copy, so do not commit one.
66
+ The table is tab-separated, with one header row and one row per module, in
67
+ a fixed order: primary modules, then each layer, then the engine, each in
68
+ C-locale name order.
69
+
70
+ | # | Column | Meaning |
71
+ |---|---|---|
72
+ | 1 | `name` | Module identifier. |
73
+ | 2 | `version` | Version string; empty means unversioned. |
74
+ | 3 | `side_effect` | Side-effect class. |
75
+ | 4 | `requires` | Required dependencies. |
76
+ | 5 | `optional` | Optional dependencies. |
77
+ | 6 | `shells` | Shell classes. |
78
+ | 7 | `platforms` | Platforms. |
79
+ | 8 | `exports` | Exported functions. |
80
+ | 9 | `load` | Body path relative to the module's source directory; empty for a meta module. |
81
+ | 10 | `commands` | Exports published as commands. |
82
+ | 11 | `executables` | Scripts published as commands. |
83
+ | 12 | `tests` | Test scripts for `valise test`. |
84
+ | 13 | `source` | `module.d`, `layers/<provider>/module.d`, or `valise`. |
85
+
86
+ Building the table also validates it. A `name` that differs from its file
87
+ name, a missing body, executable or test, a symlink, a misplaced prefix, or
88
+ invalid metadata stops the command and names the file.
89
+
90
+ A module reference is a bare name or a qualified `name@source`. A bare
91
+ dependency resolves to the module with that name in the same source when
92
+ there is one, otherwise to the only module with that name. A bare bundle root
93
+ or exclusion resolves to the primary module when there is one, otherwise to
94
+ the only module with that name; when several sources define the name, qualify
95
+ it.
96
+
97
+ ## Bundles: the root modules for each shell
98
+
99
+ `bundles.tsv` is tab-separated: bundle, shell, and a space-separated list of
100
+ root modules. The first row may be a header.
101
+
102
+ ```
103
+ bundle shell roots
104
+ default sh profile_main
105
+ default bash profile_main profile_bash
106
+ default zsh profile_main
107
+ default ksh profile_main
108
+ default fish profile_main_fish
109
+ work bash profile_main profile_work
110
+ ```
111
+
112
+ The shell column is one of `sh`, `bash`, `zsh`, `ksh`, and `fish`. `sh` is the
113
+ plan for any POSIX shell that is not bash, zsh, or ksh, such as dash or
114
+ BusyBox ash. A bundle without a row for a shell loads nothing there.
115
+
116
+ Roots are starting points. The plan loads each root's whole closure of
117
+ `requires` and available `optional` dependencies, in dependency order.
118
+ `valise explain-bundles [--bundle=<name>] [--variant=<name>]` prints each
119
+ bundle's roots and ordered closure per shell, with each module's source.
120
+
121
+ A root, and every module it requires, must support the shell and the
122
+ platform it runs on: in strict mode an unsupported platform is a failure. A
123
+ module that exists only for some platforms belongs behind an optional edge.
124
+ Write an aggregator whose `optional=` lists the per-platform modules and use
125
+ the aggregator as the root; the module for the running platform loads, and
126
+ the others are skipped without a message.
127
+
128
+ ## Variants: what to leave out
129
+
130
+ `variants.tsv` is tab-separated: variant name, description, and a
131
+ space-separated list of excluded modules. Without the file, the only variant
132
+ is `default`, which excludes nothing.
133
+
134
+ ```
135
+ variant description excluded
136
+ default All project modules
137
+ lean No editor defaults valise_editor
138
+ ```
139
+
140
+ Each exclusion must resolve to exactly one module; an unknown or ambiguous
141
+ exclusion is an error. An excluded module is removed from every closure, and
142
+ its optional edges are dropped. **If a remaining module requires an excluded
143
+ module, the build fails**, naming both; exclude the dependents too. Nothing is
144
+ removed silently.
145
+
146
+ `valise explain-variant [--bundle=<name>] [name]` shows a variant's
147
+ description, its exclusions, and how many of the bundle's roots it removes
148
+ per shell. The bundle chooses the roots, and the variant then removes
149
+ modules from what they reach.
150
+
151
+ ## `valise compile`
152
+
153
+ ```sh
154
+ valise compile [--bundle=<name>] [--variant=<name>] <output-dir>
155
+ ```
156
+
157
+ Compile writes `<output-dir>/<shell>.sh` for each POSIX shell the bundle has
158
+ roots for, and nothing else. The output directory must be empty or absent;
159
+ compile stages beside it and publishes only after every plan compiled.
160
+
161
+ A plan is a POSIX shell file:
162
+
163
+ ```
164
+ # valise-plan-v2
165
+ # Generated by valise compile; DO NOT EDIT.
166
+ # plan=bash
167
+ # roots=profile_main
168
+ # order=valise_path profile_main
169
+ ```
170
+
171
+ The header names the format, the shell, the roots, and the resolved load
172
+ order. Sourced with `VALISE_MODULE_ROOT` naming a release, the plan:
173
+
174
+ - reads `VALISE_MODE` (`strict` or `permissive`; default `strict`);
175
+ - probes the platform once and gates each module by `platforms=`;
176
+ - sources `runtime/capabilities.sh`, so the `sp_cap_*` helpers stay available
177
+ to module functions afterwards;
178
+ - refuses to load a module whose declared export already exists as an alias,
179
+ function, or builtin (an export may shadow a command on PATH), and checks
180
+ after loading that each export is defined;
181
+ - skips a module whose required dependency failed or was skipped, and says
182
+ which;
183
+ - in strict mode, returns nonzero at the first failure among the roots and
184
+ everything they require;
185
+ - in permissive mode, reports each failure, continues with unrelated
186
+ modules, and returns zero;
187
+ - loads an optional dependency first when it can; its absence never affects
188
+ the module that declared it;
189
+ - with `VALISE_RELOAD=1`, removes the plan's declared exports first, so the
190
+ plan can be sourced again.
191
+
192
+ Compile fails before writing anything when the variant or bundle is unknown,
193
+ an exclusion or root does not resolve, a variant exclusion breaks a required
194
+ dependency, a module in a closure has a side-effect class that is not safe at
195
+ startup (`generated-file`, `update-network`, or `install-time`), or a module
196
+ is not representable as plan source.
197
+
198
+ ## `valise build`
199
+
200
+ ```sh
201
+ valise build [--bundle=<name>] [--variant=<name>] [out]
202
+ ```
203
+
204
+ Build produces an installable release tree at `build/release`, or at `out`,
205
+ which must then be empty or absent. The tree contains:
206
+
207
+ ```
208
+ valise-release.conf format=2, valise_version, bundle, variant,
209
+ roots_<shell>=, modules=, commands=
210
+ manifest.tsv the release manifest
211
+ plans/<shell>.sh one plan per POSIX shell with roots
212
+ plans/commands/<module>.sh a plan per module that publishes commands
213
+ bin/<command> command launchers and executables
214
+ runtime/capabilities.sh the capability prelude
215
+ runtime/fish/{metadata,loader,profile}.fish when the bundle has fish roots
216
+ module.d/, modules/... primary modules some shell loads
217
+ layers/<provider>/module.d, ... layer modules some shell loads
218
+ valise/module.d, valise/modules/... engine modules some shell loads
219
+ ```
220
+
221
+ Only modules in some shell's closure are copied, with their metadata. A
222
+ module that a variant excludes, or that no root reaches, is absent from the
223
+ release, together with its body. The Fish runtime resolves the Fish roots
224
+ recorded in `valise-release.conf` against the copied metadata when Fish
225
+ starts. See [module-abi.md](module-abi.md#published-commands) for `bin/`.
226
+
227
+ Directories are `0755`, files `0644`, and `bin/` entries `0755`. Build
228
+ stages beside the destination and publishes only after the tree and its
229
+ manifest are complete. A previous `build/release` is moved aside first and
230
+ put back if publication fails, so a failed build leaves the previous output
231
+ in place. Building the same project twice produces byte-identical output.
232
+
233
+ ## `valise apply` and `valise status`
234
+
235
+ `valise apply [--bundle=<name>] [--variant=<name>] [--no-integration]
236
+ [--dry-run]` builds to `build/release`, installs it, writes the shell
237
+ integration and command stubs, and verifies the active release. Running it
238
+ again changes nothing. `--dry-run` stops after the build and prints what
239
+ install would change; see [installation.md](installation.md).
240
+
241
+ `valise status` reports the project defaults, the integration in each startup
242
+ file, the bundle, variant, and valise version the active release was built
243
+ with, and whether the active release matches `build/release`.
244
+
245
+ ## Checking a project
246
+
247
+ `valise lint` and `valise test` check a project without installing it, and
248
+ `valise run` loads modules into a throwaway shell; see
249
+ [contrib-modules.md](contrib-modules.md#checking-and-testing-modules).