valise-sh 0.1.0 → 0.2.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.
@@ -1,8 +1,9 @@
1
1
  # Architecture and scope
2
2
 
3
- `valise-sh` contains a shell-profile loader, a small catalog of reusable
4
- modules, and a CLI for installing and verifying sealed profile releases. It
5
- can be used from a source checkout or vendored by a consuming profile project.
3
+ `valise-sh` contains a shell-profile loader, a compiler that turns module
4
+ metadata into a static startup plan, a small catalog of reusable modules,
5
+ and a CLI for installing and verifying sealed profile releases. It can be
6
+ used from a source checkout or vendored by a consuming profile project.
6
7
 
7
8
  ## Components
8
9
 
@@ -11,17 +12,57 @@ can be used from a source checkout or vendored by a consuming profile project.
11
12
  | `entrypoints/profile.sh` | Sourced POSIX entrypoint. Locates the engine and module roots, loads the libraries, then invokes the loader. |
12
13
  | `lib/metadata.sh` | Parses and validates `module.d/*.meta` as data; it does not execute metadata. |
13
14
  | `lib/loader.sh` | Resolves dependencies, selects modules by shell/platform/phase, sources bodies, checks declared exports, and cleans up failures. |
15
+ | `lib/compiler.awk` | Compiles the preprocessed module graph into a static POSIX startup plan. |
14
16
  | `lib/capabilities.sh` | Portable OS, command, path, temporary-file, and system-information helpers for modules. |
15
17
  | `lib/fish/` and `entrypoints/fish/` | Fish-native metadata reader, loader, and adapter. Fish modules are not POSIX shell scripts. |
16
18
  | `module.d/`, `modules/` | Metadata and module bodies, including generic platform and `contrib_` utilities. |
17
- | `bin/valise` | POSIX shell CLI for integration snippets, sealed release installation and verification, and source-tree commands used by consumer projects. |
19
+ | `bin/valise` | POSIX shell CLI for integration snippets, sealed release installation and verification, project scaffolding, and the source-tree commands used by consumer projects. |
18
20
  | `manifests/parser-support.tsv` | Declares parser requirements for shipped shell files. |
19
21
  | `tests/`, `examples/` | Quarantined tests and runnable examples. |
20
22
 
21
23
  See [module-abi.md](module-abi.md) for metadata and loader contracts,
22
- [installation.md](installation.md) for CLI and release behavior, and
24
+ [installation.md](installation.md) for CLI and release behavior,
25
+ [variants.md](variants.md) for the compile and build contract, and
23
26
  [contrib-modules.md](contrib-modules.md) for authoring modules.
24
27
 
28
+ ## Two ways to load a profile
29
+
30
+ A profile can be loaded two ways, and both are supported.
31
+
32
+ The **loader** reads the metadata at startup, resolves the dependency
33
+ graph, and sources the module bodies. It is the general mechanism: it
34
+ honours every metadata field, including optional dependencies, and it
35
+ works for a source tree that is still being edited.
36
+
37
+ The **compiled plan** is a single static POSIX script per shell,
38
+ generated ahead of time from the same metadata. It is faster to source,
39
+ because nothing is parsed or resolved at startup. It carries the
40
+ contract that matters for a startup file: platform gating, export
41
+ checks, per-module failure reporting, strict and permissive failure
42
+ policy, and the capability helpers left available to the modules it
43
+ loads.
44
+
45
+ The two are deliberately close in behavior, so a profile can be compiled
46
+ for speed without changing what it does. The compile contract, including
47
+ the exact fields the plan honors, is in [variants.md](variants.md).
48
+
49
+ ## Two release shapes
50
+
51
+ A release directory takes one of two shapes, and the startup snippet
52
+ handles both.
53
+
54
+ A **built release** is what `valise build` produces. It carries
55
+ `generated/installed-modules.<shell>` compiled plans, the capability
56
+ prelude under `vendor/shell-profile/lib/`, the module bodies, and no
57
+ `entrypoints/`.
58
+
59
+ An **engine release** carries `entrypoints/profile.sh` and `lib/`, and no
60
+ `generated/`. It loads through the loader rather than a compiled plan.
61
+
62
+ The POSIX integration snippet prefers the compiled plan for the running
63
+ shell and falls back to the loader, so a startup file written by either
64
+ shape loads the release it is pointed at.
65
+
25
66
  ## Loading modules
26
67
 
27
68
  The POSIX entrypoint is sourced, not executed. A caller can select roots,
@@ -56,10 +97,35 @@ specific integrity boundary.
56
97
 
57
98
  The engine does not define a consumer's module catalog, default startup graph,
58
99
  workstation policy, project wrappers, or project-specific release build.
59
- Those belong in the consuming repository. The `compile`, `explain-variant`,
60
- `explain-bundles`, and `layer-lint` CLI commands therefore need source files
61
- from a consuming project; the rest of the documented bare-checkout commands
62
- are usable without one.
100
+ Those belong in the consuming repository. `valise new` scaffolds an empty
101
+ project so a consumer never has to invent the directory layout by hand, but
102
+ the catalog itself is still the consumer's to write.
103
+
104
+ The CLI splits into two groups by what they need.
105
+
106
+ Commands that work in a bare checkout: `init`, `install`, `release`,
107
+ `doctor`, `make-manifest`, `version`, and `new`.
108
+
109
+ Commands that need a consuming project, because they read or write the
110
+ project's source tree: `registry`, `module`, `build`, `apply`, `status`,
111
+ `compile`, `explain-variant`, `explain-bundles`, and
112
+ `layer-lint`. A project is recognised by `shell/module.d/` or
113
+ `shell/release/module-registry.tsv`; the project commands run from the
114
+ project root and read their defaults from `shell/release/project.conf`.
115
+
116
+ ## The registry is generated, not authored
117
+
118
+ `shell/release/module-registry.tsv` is a build artifact. `valise registry`
119
+ reads every `.meta` file under `shell/module.d/` and
120
+ `shell/layers/<provider>/module.d/` and writes the registry from them, in the
121
+ C locale, so repeated generation is byte-identical. `valise registry --check`
122
+ compares the file against the metadata without rewriting it, which is what
123
+ `valise status` reports as registry freshness. `build` regenerates the
124
+ registry before compiling, so a project that contains only
125
+ metadata and bodies still builds.
126
+
127
+ This makes the `.meta` files the single source of truth. A hand-edited
128
+ registry is not supported: the next build overwrites it.
63
129
 
64
130
  The `updates=` metadata field records identifiers for a consumer's update
65
131
  manifest. It does not make this engine a network updater. Shared modules are
@@ -75,3 +141,6 @@ suite. Extended runtime lanes run when their tools are installed; missing tools
75
141
  are visible skips locally and are required in CI. Set
76
142
  `VALISE_REQUIRE_TOOLING=1` to require them on a developer machine. The GitHub
77
143
  Actions workflow installs the shell runtimes used by the release check.
144
+
145
+ These targets live in the source repository. The npm package ships the
146
+ runtime, the docs, and the examples, not the Makefile or the test suite.
@@ -37,14 +37,20 @@ load=modules/system/example_tool.sh
37
37
  | `name` | Yes | Stable identifier matching `[a-z_][A-Za-z0-9_]*` |
38
38
  | `requires` | No | Space-separated hard dependencies (loaded first) |
39
39
  | `optional` | No | Space-separated best-effort dependencies |
40
- | `shells` | Yes | Supported shells: `sh`, `bash`, `zsh`, `ksh`, `fish` |
40
+ | `shells` | Yes | Supported shells: `sh`, `bash`, `zsh`, `ksh`, `fish`. `sh` means every POSIX shell (sh, bash, zsh, ksh, dash) |
41
41
  | `platforms` | Yes | `any`, `darwin`, `linux`, or BSD family values |
42
- | `side_effect` | Yes | One of: `read-only`, `path-env`, `generated-file`, `update-network`, `install-time`, `local-secret`, `work-only` |
42
+ | `side_effect` | Yes | One of: `read-only`, `path-env`, `generated-file`, `update-network`, `install-time`, `local-secret`, `local-state`, `work-only` |
43
43
  | `exports` | No | Public functions this module declares |
44
44
  | `updates` | No | Update registration identifiers mapped in the project update manifest |
45
45
  | `version` | No | Module version (digit-first, digits/letters/`.`/`_`/`-`); different versions of one name across roots fail closed with a skew diagnostic |
46
- | `load` | Yes | Repository-relative path to the module body (mutually exclusive with `load_hook`; exactly one required) |
47
- | `load_hook` | No | Shell function hook for bridge-backed records (mutually exclusive with `load`) |
46
+ | `load` | Conditionally | Repository-relative path to the module body |
47
+
48
+ At least one of `load` or `requires` must be present. `load` and `requires`
49
+ are not mutually exclusive — a module
50
+ may declare both and will load its dependencies first, then its own body. A
51
+ module with `requires` and no load target is a **meta module**: it names
52
+ dependencies and has no body. See
53
+ [module-abi.md](module-abi.md#meta-modules).
48
54
 
49
55
  ### Naming
50
56
 
@@ -60,6 +66,13 @@ closed naming both paths.
60
66
  Use `sp_cap_*` functions from `lib/capabilities.sh` instead of open-coding
61
67
  platform-specific commands. The POSIX entrypoint sources the capability
62
68
  library before the loader, so module bodies can call these directly.
69
+
70
+ **The helpers stay available after the profile loads.** A compiled plan
71
+ sources the capability prelude and leaves it in place, so a function your
72
+ module defines can call `sp_cap_os` or any other helper long after the plan
73
+ finished loading. Both the compiled plan and the loader keep the helpers
74
+ available at runtime.
75
+
63
76
  Available capabilities include:
64
77
 
65
78
  - `sp_cap_canonical_path <path>` — resolve absolute path through symlinks
@@ -82,6 +95,31 @@ policy plus the curated index lives in
82
95
 
83
96
  ## Authoring workflow
84
97
 
98
+ ### In a consuming project
99
+
100
+ Inside a project that uses the CLI, the CLI creates the files for you:
101
+
102
+ ```sh
103
+ valise new demo # scaffold a project
104
+ cd demo
105
+ valise module new hello --exports=hello
106
+ ```
107
+
108
+ `valise module new` writes `shell/module.d/hello.meta` and
109
+ `shell/modules/hello.sh`, and wires `hello` into the seed aggregator's
110
+ `requires=` so the module is actually reachable. It refuses a duplicate
111
+ export name. Edit the body, then:
112
+
113
+ ```sh
114
+ valise build # build to build/release
115
+ valise apply # build, install, integrate, verify
116
+ ```
117
+
118
+ You never hand-write the registry: `valise build` and `valise apply`
119
+ regenerate `shell/release/module-registry.tsv` from the metadata.
120
+
121
+ ### In the engine repository
122
+
85
123
  1. Write the two files. Start from an existing pair — `module.d/contrib_pager.meta`
86
124
  plus `modules/system/contrib_pager.sh` is the smallest real example.
87
125
  2. Validate the metadata as data:
@@ -119,12 +157,18 @@ policy plus the curated index lives in
119
157
  make test-parser-support
120
158
  ```
121
159
 
160
+ This target belongs to the engine's source repository. The npm package
161
+ ships the runtime and the docs, not the Makefile or the parser table, so
162
+ a package consumer checks parser coverage by hand: run every POSIX shell
163
+ you claim to support over the body (`sh -n`, `bash -n`, `zsh -n`,
164
+ `ksh -n`) and confirm each one accepts it.
165
+
122
166
  5. Publish a release containing your module tree and prove the whole
123
- path end to end (make-manifest, install, verify-active):
167
+ path end to end (make-manifest, install, release verify):
124
168
 
125
169
  ```sh
126
170
  valise make-manifest ./mytree
127
- cd mytree && valise install && valise verify-active
171
+ cd mytree && valise install && valise release verify
128
172
  ```
129
173
 
130
174
  See [installation.md](installation.md) for release integrity and
@@ -145,11 +189,14 @@ VALISE_MODE=strict \
145
189
  ## Testing checklist
146
190
 
147
191
  For every new module, run each step and confirm the named behavior. The
148
- commands all exist in this repository.
192
+ commands in steps 1 to 3 work from a checkout of this repository or from an
193
+ npm-installed package; the `make` targets belong to the source repository
194
+ only.
149
195
 
150
196
  1. `sp_metadata_read module.d/<name>.meta` prints the normalized record
151
197
  (step 2 above) and the load path is covered by a
152
- `manifests/parser-support.tsv` row (`make test-parser-support`).
198
+ `manifests/parser-support.tsv` row (`make test-parser-support` in the
199
+ source repository).
153
200
  2. Source the module in a clean environment — the body prints nothing when
154
201
  its prerequisites are absent and defines every name in `exports=`:
155
202
 
@@ -166,8 +213,9 @@ commands all exist in this repository.
166
213
  (the loader forbids double-definition), and calling an idempotent
167
214
  function twice must leave the same state.
168
215
  4. The body performs no writes at source time: `side_effect` is
169
- `read-only` or `path-env`, and `generated-file`/`update-network`/
170
- `install-time` bodies are rejected during ordinary source.
216
+ `read-only`, `path-env`, or `local-state`, and `generated-file`/
217
+ `update-network`/`install-time` bodies are rejected during ordinary
218
+ source.
171
219
  5. Every public function appears in `exports=` — the loader verifies each
172
220
  declared export exists after the body runs, and cleans them up on
173
221
  failure.
@@ -184,10 +232,15 @@ make check-fast
184
232
 
185
233
  and the full matrix (`make VALISE_REQUIRE_TOOLING=1 check`) before claiming
186
234
  release readiness. The extended-runtime lanes are strict in CI and can be
187
- required locally with the same override.
235
+ required locally with the same override. Both targets live in the source
236
+ repository; the npm package ships the runtime and docs, not the Makefile.
188
237
 
189
238
  `valise layer-lint <layer-name>` validates a contribution layer's
190
- structure, namespace compliance, file permissions, and metadata integrity,
191
- but it requires a consuming project's `shell/layers/` tree and
192
- `hack/validate-layer.py`, so it does not run from a bare checkout and is
193
- not part of this workflow.
239
+ structure, namespace compliance, file permissions, metadata integrity, and
240
+ dependency closure. It needs no Python, but it does need a layer tree: a
241
+ provider directory under a consuming project's `shell/layers/`, or a path to
242
+ a layer tree containing `module.d/`. In a consuming project:
243
+
244
+ ```sh
245
+ valise layer-lint <provider-name>
246
+ ```
@@ -2,8 +2,9 @@
2
2
 
3
3
  `bin/valise` is a `/bin/sh` CLI. Its normal install and verification commands
4
4
  need the platform utilities used by the release manifest and integrity checks
5
- (for example, a SHA-256 utility). The consuming-project commands `compile`,
6
- `explain-variant`, `explain-bundles`, and `layer-lint` also require Python 3.
5
+ (for example, a SHA-256 utility). Every documented command runs without
6
+ Python 3. `explain-bundles` reads the generated registry and the bundle
7
+ roots; `layer-lint` reads layer metadata directly.
7
8
 
8
9
  ## Install the CLI
9
10
 
@@ -38,38 +39,96 @@ packed CLI without a registry, and loads a module from the extracted package.
38
39
  ## Shell integration
39
40
 
40
41
  `valise install` installs the release described by the manifest in the
41
- current project and deploys shell integration. It adds a POSIX snippet for
42
- bash, zsh, or ksh when that shell is named by `$SHELL`, and a Fish `conf.d`
43
- hook when it finds Fish in a trusted system path. On macOS the POSIX snippet
44
- goes to `.bash_profile`, `.zprofile`, or `.kshrc`; on other systems it goes to
45
- `.bashrc`, `.zshrc`, or `.kshrc`. The Fish hook is
46
- `${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/valise.fish`. Existing POSIX
47
- profile content is preserved; a marker makes repeat installs idempotent.
48
- Symlinked POSIX profile files are left alone.
49
-
50
- To print a POSIX integration snippet without editing a startup file:
42
+ current directory and writes shell integration by default. It always writes
43
+ `~/.profile`, which login `sh`, `ksh`, and bash sessions read when no other
44
+ file shadows it. It then adds the files each shell reads instead:
45
+
46
+ - `~/.bash_profile` or `~/.bash_login` when one exists, because login bash
47
+ reads the first of those instead of `~/.profile`. valise never creates
48
+ either file.
49
+ - `~/.bashrc` when bash is `$SHELL` or the file already exists.
50
+ - `~/.zprofile` when zsh is on `PATH` or the file already exists. It is the
51
+ login file zsh reads instead of `~/.profile`.
52
+ - `~/.zshrc` when zsh is `$SHELL` or the file already exists.
53
+ - `${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/valise.fish` when valise
54
+ finds a Fish executable in a trusted location.
55
+
56
+ `--no-integration` writes no startup file and no Fish hook. `valise apply`
57
+ builds, installs, and then uses the same writer.
58
+
59
+ Each POSIX file receives a managed block between `# BEGIN valise managed
60
+ block` and `# END valise managed block`. Existing content is preserved, and
61
+ a repeat install replaces the block in place. An unpaired marker is refused
62
+ and stops the install; a file already processed keeps its updated block. A
63
+ symlinked POSIX startup file is reported and left untouched. If the Fish
64
+ hook path is a symlink or a directory, install fails rather than write
65
+ through it. Install prints every path it wrote and how to remove the
66
+ integration.
67
+
68
+ The POSIX snippet adapts to the release shape:
69
+
70
+ - A project release built by `valise build` carries
71
+ `generated/installed-modules.<shell>` for each shell its bundle seeds
72
+ (`bash`, `zsh`, and `ksh` for the scaffold and the example). The snippet
73
+ sources the plan for the running shell.
74
+ - An engine release carries `entrypoints/profile.sh` and the loader. The
75
+ snippet sources that entrypoint.
76
+ - If the release has neither a plan for the running shell nor the loader,
77
+ the snippet leaves the session alone. This is the result for a login
78
+ `dash` session with a built release: `dash` has no plan, and a built
79
+ release has no loader.
80
+
81
+ `valise install` writes `~/.profile`, which ksh reads at login. A non-login
82
+ interactive ksh reads the file named by `$ENV` when that variable is set.
83
+ valise does not choose or edit that file; to cover non-login ksh, put the
84
+ POSIX block in the file `$ENV` names.
85
+
86
+ Zsh does not read `~/.profile` on its own: zsh login shells read
87
+ `.zprofile`, not `~/.profile`. Install writes the same managed block to
88
+ `~/.zprofile`, so zsh needs no separate pointer.
89
+
90
+ A built project release has no Fish surface: no Fish entrypoint, generated
91
+ Fish plan, or native Fish loader. The Fish hook verifies the active release,
92
+ then loads nothing and prints nothing. Only an engine release with
93
+ `entrypoints/fish/profile.fish` uses the native Fish loader.
94
+
95
+ The Fish hook verifies the active release before loading it. The check
96
+ validates the active pointer, the release manifest, and every listed file.
97
+ If verification fails, the hook loads no release code. Fish continues
98
+ starting even if a `conf.d` hook fails, so this does not abort the
99
+ interactive shell.
100
+
101
+ To write integration by hand, print it once and paste or save it:
51
102
 
52
103
  ```sh
53
- eval "$(valise init)"
104
+ valise init # POSIX block, or the Fish hook when $SHELL is fish
105
+ valise init posix # POSIX managed block
106
+ valise init fish # Fish conf.d hook; save it as fish/conf.d/valise.fish
54
107
  ```
55
108
 
56
- For Fish, source the generated integration in `~/.config/fish/config.fish`:
109
+ Evaluating `valise init` output directly at every prompt —
110
+ `eval "$(valise init)"` — re-runs CLI startup code in every shell and is
111
+ not the routine path; prefer `valise install` or `valise apply`, which
112
+ write the files once.
57
113
 
58
- ```fish
59
- valise init fish | source
60
- ```
114
+ `valise doctor` verifies the active release against its own manifest, checks
115
+ the startup files the running shell actually reads for valise integration,
116
+ including the `.bash_profile` or `.bash_login` that bash reads instead of
117
+ `~/.profile`. It exits nonzero when the active release is missing or fails
118
+ verification, and when no startup file carries the integration.
119
+
120
+ Two options adjust its scope:
61
121
 
62
- The Fish integration verifies the active release before loading it. The
63
- check validates the active pointer, the release manifest, and every listed
64
- file. If verification fails, the hook loads no release code. Fish continues
65
- starting even if a `conf.d` hook fails, so this does not abort the interactive
66
- shell.
122
+ - `--pre-install` skips the active-release and startup-file checks, so doctor
123
+ can run before the first install.
124
+ - `--no-integration` skips the startup-file check and reports only the
125
+ release.
67
126
 
68
- `valise verify-active` runs the release verification without sourcing shell
127
+ `valise release verify` runs the release verification without sourcing shell
69
128
  code:
70
129
 
71
130
  ```sh
72
- valise verify-active
131
+ valise release verify
73
132
  ```
74
133
 
75
134
  The development-only `valise init fish --source-tree` mode skips installed
@@ -99,18 +158,21 @@ Install and verify from the tree root:
99
158
  ```sh
100
159
  cd mytree
101
160
  valise install
102
- valise verify-active
161
+ valise release verify
103
162
  ```
104
163
 
105
164
  Install stages a sealed, read-only copy under the XDG data directory and
106
- updates the active symlink. `verify-active` checks the installed bytes against
165
+ updates the active symlink. `release verify` checks the installed bytes against
107
166
  their manifest. Fish repeats that check at startup; the POSIX startup snippets
108
- do not, so run `verify-active` separately if you need a check before sourcing
167
+ do not, so run `release verify` separately if you need a check before sourcing
109
168
  in a POSIX shell.
110
169
 
111
- The engine does not build a consuming project's module catalog or release
112
- tree. The included [e2e example](../examples/e2e/README.md) demonstrates a
113
- small complete profile.
170
+ `valise build` builds a consuming project's release tree: the module
171
+ registry, the per-shell plans, every module body, and the manifest inputs.
172
+ The engine itself ships as an engine release rather than a built project
173
+ release; it carries `entrypoints/profile.sh` and `lib/`, not `generated/`.
174
+ The included [e2e example](../examples/e2e/README.md) demonstrates a small
175
+ complete profile.
114
176
 
115
177
  ## Integrity and provenance
116
178
 
@@ -135,6 +197,30 @@ controls.
135
197
 
136
198
  The active path is a symlink to the selected release.
137
199
 
200
+ ## Managing installed releases
201
+
202
+ `valise release list` shows the installed releases and marks the active one.
203
+ `valise release use <release-id>` points the active symlink at an existing
204
+ release.
205
+
206
+ `valise release prune --keep=<n>` removes old releases. It always keeps the
207
+ active release and the `n` newest others, and it never removes a release that
208
+ fails verification against its own manifest — a release that cannot be
209
+ verified is reported and kept, so a damaged tree is not silently discarded.
210
+ The default is `--keep=3`.
211
+
212
+ ## Command usage
213
+
214
+ Every command prints its own contract:
215
+
216
+ ```sh
217
+ valise compile --help
218
+ valise build --help
219
+ valise install --help
220
+ ```
221
+
222
+ `valise help` prints the global command list.
223
+
138
224
  ## Updating and removing
139
225
 
140
226
  Update a source checkout with Git. To update an npm installation, repack and
@@ -142,9 +228,10 @@ reinstall the tarball. `valise install` updates the active profile release;
142
228
  it does not update the CLI itself.
143
229
 
144
230
  There is no `valise uninstall` command. Before removing the CLI, remove the
145
- managed integration from your shell startup file: in POSIX files, delete only
146
- the block beginning `# valise shell integration — managed by valise install`;
147
- for Fish, remove
231
+ managed integration from every startup file valise wrote: delete only the
232
+ block between `# BEGIN valise managed block` and `# END valise managed
233
+ block` in `~/.profile` and in any of `~/.bash_profile`, `~/.bash_login`,
234
+ `~/.bashrc`, `~/.zshrc`, and `~/.zprofile`. For Fish, remove
148
235
  `${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/valise.fish` if it is still
149
236
  the valise-managed hook. Then uninstall the CLI and remove the valise XDG
150
237
  state. Installed release files are read-only, so make them writable first: