valise-sh 0.1.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 (88) hide show
  1. package/CHANGELOG.md +320 -9
  2. package/README.md +104 -127
  3. package/bin/valise +67 -3266
  4. package/docs/architecture.md +76 -51
  5. package/docs/contrib-modules.md +180 -173
  6. package/docs/installation.md +210 -69
  7. package/docs/module-abi.md +211 -148
  8. package/docs/projects.md +249 -0
  9. package/docs/release-policy.md +90 -11
  10. package/docs/shared-modules.md +149 -26
  11. package/examples/README.md +20 -8
  12. package/examples/e2e/README.md +61 -68
  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 +151 -0
  31. package/examples/profile-project/bundles.tsv +8 -0
  32. package/examples/profile-project/layers/demo/module.d/contrib_demo_greeting.meta +9 -0
  33. package/examples/profile-project/layers/demo/modules/shared/contrib_demo_greeting.sh +4 -0
  34. package/examples/profile-project/module.d/greeting.meta +9 -0
  35. package/examples/profile-project/module.d/profile_main.meta +8 -0
  36. package/examples/profile-project/modules/greeting.sh +5 -0
  37. package/examples/profile-project/valise.conf +4 -0
  38. package/examples/profile-project/variants.tsv +3 -0
  39. package/examples/shared/README.md +13 -34
  40. package/examples/shared/module.d/contrib_shared_paths.meta +2 -3
  41. package/examples/shared/module.d/contrib_shared_umask.meta +1 -2
  42. package/examples/shared/modules/shared/contrib_shared_paths.sh +2 -2
  43. package/lib/capabilities.sh +40 -13
  44. package/lib/cli/commands.sh +278 -0
  45. package/lib/cli/common.sh +326 -0
  46. package/lib/cli/devtools.sh +539 -0
  47. package/lib/cli/fish-hook.sh +986 -0
  48. package/lib/cli/integration.sh +337 -0
  49. package/lib/cli/lifecycle.sh +578 -0
  50. package/lib/cli/project.sh +1807 -0
  51. package/lib/cli/store.sh +1023 -0
  52. package/lib/cli/usage.sh +308 -0
  53. package/lib/compiler.awk +352 -24
  54. package/lib/fish/loader.fish +163 -28
  55. package/lib/fish/metadata.fish +62 -19
  56. package/lib/fish/profile.fish +170 -0
  57. package/lib/loader.sh +736 -158
  58. package/lib/metadata.sh +137 -55
  59. package/manifests/parser-support.tsv +2 -2
  60. package/module.d/valise_editor.meta +9 -0
  61. package/module.d/valise_history.meta +9 -0
  62. package/module.d/valise_pager.meta +9 -0
  63. package/module.d/valise_path.meta +9 -0
  64. package/module.d/{platform_bsd.meta → valise_platform_bsd.meta} +3 -4
  65. package/module.d/{platform_linux.meta → valise_platform_linux.meta} +3 -4
  66. package/module.d/{platform_macos.meta → valise_platform_macos.meta} +3 -4
  67. package/module.d/{platform_posix.meta → valise_platform_posix.meta} +2 -3
  68. package/module.d/valise_xdg.meta +9 -0
  69. package/modules/system/{contrib_editor.sh → valise_editor.sh} +2 -2
  70. package/modules/system/{contrib_history.sh → valise_history.sh} +2 -2
  71. package/modules/system/{contrib_pager.sh → valise_pager.sh} +2 -2
  72. package/modules/system/{contrib_path.sh → valise_path.sh} +3 -3
  73. package/modules/system/{contrib_xdg.sh → valise_xdg.sh} +2 -2
  74. package/package.json +91 -9
  75. package/docs/variants.md +0 -63
  76. package/entrypoints/.gitkeep +0 -0
  77. package/entrypoints/fish/profile.fish +0 -99
  78. package/examples/e2e/build-manifest.sh +0 -250
  79. package/examples/local/.gitkeep +0 -0
  80. package/lib/.gitkeep +0 -0
  81. package/manifests/.gitkeep +0 -0
  82. package/module.d/.gitkeep +0 -0
  83. package/module.d/contrib_editor.meta +0 -10
  84. package/module.d/contrib_history.meta +0 -10
  85. package/module.d/contrib_pager.meta +0 -10
  86. package/module.d/contrib_path.meta +0 -10
  87. package/module.d/contrib_xdg.meta +0 -10
  88. package/modules/.gitkeep +0 -0
@@ -1,77 +1,102 @@
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 is a shell-profile engine. It reads module metadata as data,
4
+ compiles a static startup plan for each shell, packages the plans and module
5
+ bodies into a sealed release, installs the release, and verifies it. Your
6
+ profile lives in a separate valise project; the engine supplies the tools
7
+ and a few generic modules.
6
8
 
7
9
  ## Components
8
10
 
9
11
  | Path | Responsibility |
10
12
  |---|---|
11
- | `entrypoints/profile.sh` | Sourced POSIX entrypoint. Locates the engine and module roots, loads the libraries, then invokes the loader. |
12
- | `lib/metadata.sh` | Parses and validates `module.d/*.meta` as data; it does not execute metadata. |
13
- | `lib/loader.sh` | Resolves dependencies, selects modules by shell/platform/phase, sources bodies, checks declared exports, and cleans up failures. |
14
- | `lib/capabilities.sh` | Portable OS, command, path, temporary-file, and system-information helpers for modules. |
15
- | `lib/fish/` and `entrypoints/fish/` | Fish-native metadata reader, loader, and adapter. Fish modules are not POSIX shell scripts. |
16
- | `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. |
18
- | `manifests/parser-support.tsv` | Declares parser requirements for shipped shell files. |
13
+ | `bin/valise` | The CLI. It sources the parts under `lib/cli/` and dispatches commands. |
14
+ | `lib/cli/` | CLI parts: `project.sh` (project config, registry, compile, build), `store.sh` (release store, install, `release`), `integration.sh` (startup-file blocks, `doctor`), `fish-hook.sh`, `commands.sh` (published commands), `lifecycle.sh` (rollback, transactions, uninstall, dry runs), `devtools.sh` (`lint`, `test`, `run`, `env`), `usage.sh`, `common.sh`. |
15
+ | `lib/metadata.sh` | Parses and validates `.meta` files as data. |
16
+ | `lib/compiler.awk` | Turns the resolved module graph into a static POSIX plan. |
17
+ | `lib/capabilities.sh` | The `sp_cap_*` helpers. A release carries it as `runtime/capabilities.sh`. |
18
+ | `lib/fish/` | The native Fish metadata reader, loader, and adapter. A release carries them under `runtime/fish/`. |
19
+ | `lib/loader.sh`, `entrypoints/profile.sh` | The development loader, which resolves and sources modules at load time from a checkout. Installed profiles do not use it. |
20
+ | `module.d/`, `modules/` | The engine modules (`valise_*`). |
21
+ | `manifests/parser-support.tsv` | The parsers each shipped file class must pass. |
19
22
  | `tests/`, `examples/` | Quarantined tests and runnable examples. |
20
23
 
21
- See [module-abi.md](module-abi.md) for metadata and loader contracts,
22
- [installation.md](installation.md) for CLI and release behavior, and
23
- [contrib-modules.md](contrib-modules.md) for authoring modules.
24
+ [projects.md](projects.md) describes a project and the compile and build
25
+ contract, [module-abi.md](module-abi.md) the metadata,
26
+ [installation.md](installation.md) installation and the release store, and
27
+ [contrib-modules.md](contrib-modules.md) writing and testing modules.
24
28
 
25
- ## Loading modules
29
+ ## From metadata to a running shell
26
30
 
27
- The POSIX entrypoint is sourced, not executed. A caller can select roots,
28
- modules, and failure policy explicitly:
31
+ 1. `valise build` reads every `.meta` file in the project (`module.d/`, each
32
+ `layers/<provider>/module.d/`) and the engine's own `module.d/`, as data,
33
+ into an internal table. `valise registry` prints that table; nothing reads
34
+ a saved copy.
35
+ 2. For each POSIX shell in the bundle, the compiler resolves the roots'
36
+ closure, applies the variant's exclusions, orders the modules by
37
+ dependency, and writes `plans/<shell>.sh`. A module that publishes
38
+ commands gets its own plan under `plans/commands/`.
39
+ 3. The build copies the modules some shell loads, the capability prelude,
40
+ and the Fish runtime when the bundle has Fish roots, then writes
41
+ `manifest.tsv`. The release id is the SHA-256 of the manifest.
42
+ 4. `valise install` copies the tree into the release store, verifies it,
43
+ switches the active link, writes the startup-file blocks and the Fish
44
+ hook, and publishes command stubs. If any step fails, install restores
45
+ the previous state.
46
+ 5. At startup, the POSIX block picks the plan for the running shell class
47
+ (`bash`, `zsh`, `ksh`, or `sh`) and sources it. The plan contains the
48
+ load order; it reads no metadata. Fish verifies the release, then runs
49
+ the native Fish loader over the release's Fish metadata.
50
+
51
+ There is one release shape. The plan is generated ahead of time so that
52
+ startup does no resolution work and so that you can read exactly what will
53
+ run.
54
+
55
+ ## The development loader
56
+
57
+ `entrypoints/profile.sh` and `lib/loader.sh` load modules straight from a
58
+ checkout, resolving dependencies when they are sourced. They are for working
59
+ on the engine's own modules and for the loader's tests; installed profiles
60
+ and `valise run` use compiled plans. The loader honors the same metadata,
61
+ including optional dependencies, platform gating, export checks, and failure
62
+ cleanup:
29
63
 
30
64
  ```sh
31
65
  VALISE_ROOT=/path/to/valise-sh
32
- VALISE_MODULE_ROOT=/path/to/profile
66
+ VALISE_MODULE_ROOT=/path/to/modules
33
67
  VALISE_MODULES='module_a module_b'
34
68
  VALISE_MODE=strict
35
69
  . "$VALISE_ROOT/entrypoints/profile.sh"
36
70
  ```
37
71
 
38
- `VALISE_MODULES` unset means no modules are requested. A consuming profile is
39
- responsible for choosing its default module set and for setting the roots in
40
- its startup integration. Searching upward from the current directory when
41
- `VALISE_ROOT` is omitted is a checkout convenience, not an installed-profile
42
- configuration strategy.
43
-
44
- Metadata is validated before module bodies run. Required dependencies load
45
- first; present optional dependencies are also considered. Cycles and invalid
46
- or conflicting records are rejected. Strict mode returns failure for a
47
- requested module's failure; permissive mode reports the affected module and
48
- continues with unrelated requests. The loader has no unload operation.
49
-
50
- The Fish adapter uses the native Fish loader and native `.fish` bodies; it
51
- never sources POSIX bodies. Fish startup integration verifies the installed
52
- release before loading it. See [installation.md](installation.md) for the
53
- specific integrity boundary.
72
+ `VALISE_MODULES` unset means nothing is loaded. Its search path is described
73
+ in [module-abi.md](module-abi.md#the-development-loaders-search-path).
54
74
 
55
75
  ## Scope boundaries
56
76
 
57
- The engine does not define a consumer's module catalog, default startup graph,
58
- 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.
77
+ The engine does not define your modules, your bundles, your workstation
78
+ policy, or wrappers around other tools. Those belong in your project.
79
+ `valise new` writes an empty project so you do not have to invent the
80
+ layout.
81
+
82
+ Project commands (`module`, `build`, `apply`, `status`, `lint`, `test`,
83
+ `run`, `compile`, `registry`, `explain-variant`, `explain-bundles`) run
84
+ inside a directory that holds `valise.conf`, found by walking up from the
85
+ working directory. Release commands (`install`, `release`, `uninstall`,
86
+ `init`, `doctor`, `make-manifest`) and `new`, `env`, `layer-lint`, and
87
+ `version` run anywhere.
63
88
 
64
- The `updates=` metadata field records identifiers for a consumer's update
65
- manifest. It does not make this engine a network updater. Shared modules are
66
- vendored snapshots: consumers review provider changes, update the snapshot,
67
- and rebuild their release. The engine does not authenticate authors or sign
68
- releases; its manifest check detects changes to bytes after the manifest is
69
- created.
89
+ Shared modules are vendored copies. You review a provider's changes, copy
90
+ them into `layers/<provider>/`, and rebuild. The engine does not fetch
91
+ updates, authenticate authors, or sign releases; the manifest detects bytes
92
+ that changed after the release was built.
70
93
 
71
94
  ## Verification
72
95
 
73
- Run `make check-fast` while developing and `make check` for the available
74
- suite. Extended runtime lanes run when their tools are installed; missing tools
75
- are visible skips locally and are required in CI. Set
76
- `VALISE_REQUIRE_TOOLING=1` to require them on a developer machine. The GitHub
77
- Actions workflow installs the shell runtimes used by the release check.
96
+ Run `make check-fast` while developing and `make check` for the full suite.
97
+ Extended runtime lanes run when their tools are installed; missing tools are
98
+ visible skips locally and failures in CI. Set `VALISE_REQUIRE_TOOLING=1` to
99
+ require them on a developer machine.
100
+
101
+ The Makefile and the tests live in the source repository. The npm package
102
+ ships the runtime, the docs, and the examples.
@@ -1,193 +1,200 @@
1
- # Contributing Modules
1
+ # Writing modules
2
2
 
3
- This guide covers modules loaded by the valise-sh engine. A module is
4
- usually a metadata file and a shell body; consumer projects may add their
5
- own naming, review, and release rules.
3
+ A module is a metadata file and, usually, a shell body. This guide covers
4
+ writing one in a valise project, checking and testing it, and deciding
5
+ whether it belongs in a shareable layer. [module-abi.md](module-abi.md) is
6
+ the full metadata contract; [projects.md](projects.md) covers bundles,
7
+ variants, and builds.
6
8
 
7
- ## Module structure
8
-
9
- A file-backed module has a metadata file and a body. POSIX bodies usually
10
- end in `.sh`; Fish bodies end in `.fish`:
9
+ ## Create a module
11
10
 
11
+ ```sh
12
+ valise new demo
13
+ cd demo
14
+ valise module new hello --exports=hello
12
15
  ```
13
- module.d/<name>.meta
14
- modules/<area>/<name>.sh
16
+
17
+ `valise module new` writes `module.d/hello.meta` and `modules/hello.sh` and
18
+ adds `hello` to the `requires=` of the bundle's POSIX root, so a build
19
+ loads it. When the default bundle has several POSIX roots, name the ones to
20
+ change with `--into=<module>[,<module>...]`, or pass `--no-wire` to wire
21
+ the module yourself. Edit the body, then:
22
+
23
+ ```sh
24
+ valise lint # check metadata, bundles, variants, and syntax
25
+ valise apply # build, install, write the integration, verify
15
26
  ```
16
27
 
17
- The metadata file is read by the engine before any body is sourced. It uses
18
- one `key=value` field per line:
28
+ A module's metadata looks like this:
19
29
 
20
30
  ```text
21
- name=example_tool
22
- requires=
31
+ name=hello
32
+ version=1.0.0
33
+ side_effect=read-only
34
+ requires=valise_path
23
35
  optional=
24
- shells=sh bash zsh ksh
36
+ shells=posix
25
37
  platforms=any
26
- side_effect=read-only
27
- exports=example_tool_loaded
28
- updates=
29
- version=1.0.0
30
- load=modules/system/example_tool.sh
38
+ exports=hello
39
+ commands=hello
40
+ tests=tests/hello.sh
41
+ load=modules/hello.sh
31
42
  ```
32
43
 
33
- ### Metadata fields
34
-
35
- | Field | Required | Description |
36
- |---|---|---|
37
- | `name` | Yes | Stable identifier matching `[a-z_][A-Za-z0-9_]*` |
38
- | `requires` | No | Space-separated hard dependencies (loaded first) |
39
- | `optional` | No | Space-separated best-effort dependencies |
40
- | `shells` | Yes | Supported shells: `sh`, `bash`, `zsh`, `ksh`, `fish` |
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` |
43
- | `exports` | No | Public functions this module declares |
44
- | `updates` | No | Update registration identifiers mapped in the project update manifest |
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`) |
48
-
49
- ### Naming
50
-
51
- Modules that live only in a layer root (see
52
- [module-abi.md](module-abi.md) multi-root search path) must carry the
53
- `contrib_` name prefix; the loader rejects unprefixed layer-only modules
54
- fail closed. Modules in the primary root have no prefix requirement.
55
- Same-name modules across roots must be byte-identical or the load fails
56
- closed naming both paths.
57
-
58
- ## Engine capabilities
59
-
60
- Use `sp_cap_*` functions from `lib/capabilities.sh` instead of open-coding
61
- platform-specific commands. The POSIX entrypoint sources the capability
62
- library before the loader, so module bodies can call these directly.
63
- Available capabilities include:
64
-
65
- - `sp_cap_canonical_path <path>` — resolve absolute path through symlinks
66
- - `sp_cap_path_under <base> <target>` — check if target is under base
67
- - `sp_cap_file_mtime <path>` — portable modification time (epoch seconds)
68
- - `sp_cap_mktemp_dir [prefix] [parent]` — secure temporary directory
69
- - `sp_cap_mktemp_file <parent> <prefix>` — secure temporary file
70
- - `sp_cap_homebrew_prefix` — locate Homebrew installation
71
- - `sp_cap_command_exists <name>` — portable command existence check
72
- - `sp_cap_command_path <name>` — resolve command to absolute path
73
- - `sp_cap_os` — print normalized OS name
74
- - `sp_cap_cpu_count` — print available CPU count
75
-
76
- See [module-abi.md](module-abi.md) for the full capability contract and
77
- [architecture.md](architecture.md) for scope. Shared modules — the ones
78
- other people consume — carry additional governance: namespace ownership,
79
- review, deprecation, and removal are policy questions, and the written
80
- policy plus the curated index lives in
81
- [shared-modules.md](shared-modules.md).
82
-
83
- ## Authoring workflow
84
-
85
- 1. Write the two files. Start from an existing pair — `module.d/contrib_pager.meta`
86
- plus `modules/system/contrib_pager.sh` is the smallest real example.
87
- 2. Validate the metadata as data:
88
-
89
- ```sh
90
- . ./lib/metadata.sh
91
- sp_metadata_read module.d/example_tool.meta
92
- ```
93
-
94
- The reader prints the normalized record or a precise diagnostic
95
- (`invalid side_effect`, `unknown metadata field`, and so on). It never
96
- sources the body.
97
- 3. Load it through the entrypoint and call the export:
98
-
99
- ```sh
100
- VALISE_MODULE_ROOT="$PWD" \
101
- VALISE_MODULES='example_tool' \
102
- VALISE_MODE=strict \
103
- . ./entrypoints/profile.sh
104
- example_tool_loaded
105
- ```
106
-
107
- Strict mode fails closed on any problem: missing dependency, a declared
108
- export missing after the body runs, an export that already exists,
109
- reserved-utility shadowing, shell/platform mismatch. The loader checks
110
- what `exports=` declares; it does not reject a body that defines extra
111
- private functions, so list everything you consider public. Under zsh
112
- the entrypoint handles word-splitting itself; source it the same way.
113
- 4. Check parser coverage. `manifests/parser-support.tsv` declares which
114
- POSIX shells must parse each file class. Place portable bodies under a
115
- path pattern the table already covers (for example `modules/system/*.sh`)
116
- or add a row for your pattern, then run the gate:
117
-
118
- ```sh
119
- make test-parser-support
120
- ```
121
-
122
- 5. Publish a release containing your module tree and prove the whole
123
- path end to end (make-manifest, install, verify-active):
124
-
125
- ```sh
126
- valise make-manifest ./mytree
127
- cd mytree && valise install && valise verify-active
128
- ```
129
-
130
- See [installation.md](installation.md) for release integrity and
131
- provenance.
132
-
133
- ## Shipped modules
134
-
135
- The reusable `contrib_` modules are listed in
136
- [shared-modules.md](shared-modules.md). For example, load PATH and editor
137
- defaults from a checkout with:
44
+ Choose `shells` by what the body needs:
45
+
46
+ - `posix` for a body that runs in every POSIX shell: dash, BusyBox ash,
47
+ bash, zsh, and ksh.
48
+ - `sh` for a body that runs only in the generic class (dash, ash, and other
49
+ plain `sh` implementations), listed together with the explicit shells it
50
+ also supports: `shells=sh bash ksh` loads in dash, bash, and ksh, and not
51
+ in zsh.
52
+ - `bash`, `zsh`, or `ksh` for a body that uses that shell's features.
53
+ - `fish` for a native Fish body (`.fish`). A Fish module never runs a POSIX
54
+ body, so a feature that needs both shells is two modules.
55
+
56
+ Zsh runs module functions with the user's options. A body that relies on
57
+ word splitting of unquoted expansions behaves differently in zsh, where it
58
+ is off by default; quote expansions, split explicitly, or leave zsh out of
59
+ `shells`.
60
+
61
+ `side_effect` says what the body does when sourced. Startup modules use
62
+ `read-only`, `path-env`, `local-state` (writes under the XDG state
63
+ directory), `local-secret` (reads secrets that live on this machine), or
64
+ `work-only`. A build refuses `generated-file`, `update-network`, and
65
+ `install-time` in a startup closure.
66
+
67
+ List every public function in `exports`. The plan refuses to load a module
68
+ whose export already exists, checks that each export is defined after the
69
+ body runs, and removes them again when the module fails. List exports that
70
+ should also run from scripts and other programs in `commands`; see
71
+ [module-abi.md](module-abi.md#published-commands).
72
+
73
+ Use the `sp_cap_*` helpers from the capability prelude instead of
74
+ platform-specific commands. They stay defined after the profile loads, so
75
+ your functions can call them later:
76
+
77
+ - `sp_cap_os`, `sp_cap_cpu_count`, `sp_cap_homebrew_prefix`
78
+ - `sp_cap_command_exists <name>`, `sp_cap_command_path <name>`
79
+ - `sp_cap_canonical_path <path>`, `sp_cap_path_under <base> <target>`
80
+ - `sp_cap_file_mtime <path>`
81
+ - `sp_cap_secure_local_bin_check <home> [reject|allow-owned-symlinks]`
82
+ - `sp_cap_mktemp_dir [prefix] [parent]`, `sp_cap_mktemp_file <parent> <prefix>`
83
+
84
+ `sp_cap_secure_local_bin_check` defaults to `reject`, which refuses every
85
+ symlink in `~/.local/bin`. `allow-owned-symlinks` accepts a symlink whose
86
+ link entry is owned by the current user (the pipx and uv shape) while still
87
+ rejecting unowned entries and group- or other-writable regular files.
88
+
89
+ The engine also ships modules a project can require: `valise_path`
90
+ (`valise_path_prepend` and `valise_path_append`), `valise_editor`,
91
+ `valise_history`, `valise_pager`, `valise_xdg`, and the `valise_platform_*`
92
+ modules. [shared-modules.md](shared-modules.md) lists them.
93
+
94
+ ## Checking and testing modules
95
+
96
+ `valise lint [--require-parsers] [module...]` checks the project and reports
97
+ every problem it finds:
98
+
99
+ - each metadata record, its name, its prefix, and the files it names;
100
+ - each layer: `contrib_` names, `version=`, safe file modes, and
101
+ dependencies that stay inside the layer or the engine;
102
+ - the `lint_layer_forbid` pattern from `valise.conf`, if set;
103
+ - every bundle compiled for every variant and shell, which catches unknown
104
+ roots and exclusions, missing dependencies, and exclusions that break a
105
+ remaining module;
106
+ - the syntax of each body and test, with the parser of every shell the module
107
+ declares (`dash` or a plain `sh`, `bash -n`, `zsh -n`, `ksh -n`, and
108
+ `fish --no-execute`). A missing parser is reported as a skip, or as a
109
+ failure with `--require-parsers`.
110
+
111
+ `valise test [--shell=<class>] [--require-shells] [module...]` runs each
112
+ module's `tests=` scripts. For every shell class the module declares whose
113
+ interpreter is installed, it builds a release with the module as its only
114
+ root, starts the shell with `env -i` and a fresh `HOME`, `TMPDIR`, and XDG
115
+ tree, loads the module in strict mode, and runs the test. `PATH` is
116
+ `${VALISE_TEST_PATH:-/usr/bin:/bin:/usr/sbin:/sbin}` plus the interpreter's
117
+ directory. A test passes when it exits 0:
138
118
 
139
119
  ```sh
140
- VALISE_MODULE_ROOT="$PWD" \
141
- VALISE_MODULES='contrib_path contrib_editor' \
142
- VALISE_MODE=strict \
143
- . ./entrypoints/profile.sh
120
+ # tests/hello.sh
121
+ [ "$(hello world)" = 'hello world' ] || { echo "got: $(hello world)"; exit 1; }
144
122
  ```
145
- ## Testing checklist
146
-
147
- For every new module, run each step and confirm the named behavior. The
148
- commands all exist in this repository.
149
-
150
- 1. `sp_metadata_read module.d/<name>.meta` prints the normalized record
151
- (step 2 above) and the load path is covered by a
152
- `manifests/parser-support.tsv` row (`make test-parser-support`).
153
- 2. Source the module in a clean environment — the body prints nothing when
154
- its prerequisites are absent and defines every name in `exports=`:
155
-
156
- ```sh
157
- env -i HOME="$PWD/.fresh-home" PATH=/usr/bin:/bin \
158
- VALISE_MODULE_ROOT="$PWD" VALISE_MODULES='<name>' VALISE_MODE=strict \
159
- /bin/sh -c '. ./entrypoints/profile.sh'
160
- ```
161
-
162
- (Create `./.fresh-home` first or point `HOME` at any empty directory;
163
- never source against your live profile.)
164
- 3. Source it twice in one strict session or call the entrypoint repeatedly:
165
- the second load must fail closed with `declared export already exists`
166
- (the loader forbids double-definition), and calling an idempotent
167
- function twice must leave the same state.
168
- 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.
171
- 5. Every public function appears in `exports=` — the loader verifies each
172
- declared export exists after the body runs, and cleans them up on
173
- failure.
174
- 6. Tests run in quarantined homes: this repository's suites already use
175
- fresh temporary `HOME`, controlled `TMPDIR` and `PATH`, and cleared
176
- startup hooks (`make check-fast` to confirm the engine gate). Stub
177
- network and secrets in your own tests; never read a live profile.
178
-
179
- Run the engine's own fast gate while authoring:
123
+
124
+ The output names each run (`ok hello [bash] tests/hello.sh`), shows a
125
+ failing test's output, and ends with a count; any failure makes the command
126
+ fail. A test that needs a file before the module loads (a secret, a config
127
+ file) writes it and calls `valise_test_reload`, which loads the module again.
128
+ A test that starts another shell loads the module there from
129
+ `VALISE_TEST_PLAN`, the plan the test was loaded with, with
130
+ `VALISE_MODULE_ROOT` set to `VALISE_TEST_RELEASE`:
180
131
 
181
132
  ```sh
182
- make check-fast
133
+ bash -c 'VALISE_MODULE_ROOT=$VALISE_TEST_RELEASE; . "$VALISE_TEST_PLAN"; hello world'
183
134
  ```
184
135
 
185
- and the full matrix (`make VALISE_REQUIRE_TOOLING=1 check`) before claiming
186
- release readiness. The extended-runtime lanes are strict in CI and can be
187
- required locally with the same override.
136
+ Tests see `VALISE_TEST_ROOT` (the scratch directory, which holds `HOME`),
137
+ `VALISE_TEST_MODULE`, `VALISE_TEST_MODULE_DIR`, `VALISE_TEST_SHELL`,
138
+ `VALISE_TEST_RELEASE`, `VALISE_TEST_PLAN` (`runtime/fish/profile.fish` in
139
+ Fish), and `VALISE_PROJECT_ROOT`. Fish tests (`.fish`) run only in Fish; other tests
140
+ run only in POSIX shells.
141
+
142
+ `valise run [--shell=<class>] [--module=<m>]... [--bundle=<b>]
143
+ [--variant=<v>] [--keep-env] -- <script> [args...]` loads modules into a
144
+ shell and runs a script there, for quick checks:
145
+
146
+ ```sh
147
+ valise run --shell=zsh --module=hello -- 'hello "$@"' a b
148
+ valise run --bundle=work -- 'command -v my_work_function'
149
+ ```
150
+
151
+ Its environment is quarantined like `valise test` unless `--keep-env` is
152
+ given, and its exit status is the script's.
153
+
154
+ None of these commands installs anything or reads your startup files. To
155
+ check a layer tree outside a project, run `valise layer-lint <path>`.
156
+
157
+ ## What belongs in a layer
158
+
159
+ A layer under `layers/<provider>/` is a set of modules another person could
160
+ vendor unchanged. Before moving a module into a layer, check:
161
+
162
+ - **No employer material**: no account IDs, hostnames, cloud profiles,
163
+ internal project names, or anything else tied to one organization.
164
+ - **No personal configuration**: no home-directory paths, chosen editor,
165
+ toolchain, model, or theme.
166
+ - **No dependency outside the layer** other than engine modules. A
167
+ dependency on one of your primary modules cannot be satisfied in someone
168
+ else's project.
169
+ - **Mechanism, not preset.** Keep a reusable engine (a git helper, a path
170
+ function) separate from the presets that configure it for one person. Ship
171
+ the engine in the layer and keep presets in the primary modules, or in a
172
+ separate layer module that requires the engine.
173
+ - **One purpose per module.** A grab-bag module forces a consumer to take
174
+ everything or nothing.
175
+ - **An owner segment**: name modules `contrib_<owner>_<thing>`, so two
176
+ providers do not collide on a name.
177
+ - **A version.** Every layer module needs `version=`; bump it on an
178
+ incompatible change.
179
+
180
+ Set `lint_layer_forbid` in `valise.conf` to a pattern for the identifiers
181
+ that must never reach the layer, such as an employer name, and `valise lint`
182
+ will fail when one appears. [shared-modules.md](shared-modules.md) covers
183
+ ownership, review, and deprecation for layers other people consume.
184
+
185
+ ## Modules in the engine repository
186
+
187
+ The engine's own modules live in this repository's `module.d/` and
188
+ `modules/` and use the `valise_` prefix. `manifests/parser-support.tsv`
189
+ declares which parsers must accept each file class; it ships in the package,
190
+ and `make test-parser-support` checks it in the source repository. Load an
191
+ engine module from a checkout without installing anything through the
192
+ development entrypoint:
193
+
194
+ ```sh
195
+ env -i HOME="$(mktemp -d)" PATH=/usr/bin:/bin /bin/sh -c '
196
+ VALISE_MODULE_ROOT="$PWD" VALISE_MODULES="valise_path" VALISE_MODE=strict
197
+ . ./entrypoints/profile.sh && command -v valise_path_prepend'
198
+ ```
188
199
 
189
- `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.
200
+ Run `make check-fast` while working and `make check` before committing.