@sous-io/sous 0.2.37 → 1.0.2

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 (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1094 -0
  4. package/docs/markdown/README.md +5 -4
  5. package/docs/markdown/_sidebar.md +21 -1
  6. package/docs/markdown/commands.md +294 -431
  7. package/docs/markdown/config-discovery.md +25 -14
  8. package/docs/markdown/config-inspection.md +53 -31
  9. package/docs/markdown/config-layers.md +168 -66
  10. package/docs/markdown/config-variables.md +44 -11
  11. package/docs/markdown/configuration.md +104 -17
  12. package/docs/markdown/design-principles.md +4 -1
  13. package/docs/markdown/repositories-authoring.md +30 -0
  14. package/docs/markdown/repositories-consuming.md +72 -14
  15. package/docs/markdown/repositories-file-formats.md +31 -13
  16. package/docs/markdown/repositories-providers.md +48 -57
  17. package/docs/markdown/repositories-quickstart.md +3 -2
  18. package/docs/markdown/repositories-troubleshooting.md +27 -1
  19. package/docs/markdown/repositories-variables.md +6 -1
  20. package/docs/markdown/repositories.md +18 -8
  21. package/package.json +48 -107
  22. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
  23. package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
  24. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
  25. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
  26. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
  27. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  28. package/sous.config.schema.json +351 -132
  29. package/bin/run.js +0 -51
  30. package/src/base-command.ts +0 -457
  31. package/src/commands/build.ts +0 -181
  32. package/src/commands/clear.ts +0 -79
  33. package/src/commands/compile.ts +0 -155
  34. package/src/commands/config/get.ts +0 -110
  35. package/src/commands/config/show.ts +0 -32
  36. package/src/commands/config/validate.ts +0 -53
  37. package/src/commands/help.ts +0 -46
  38. package/src/commands/init.ts +0 -289
  39. package/src/commands/launch.ts +0 -139
  40. package/src/commands/lock/rebuild.ts +0 -239
  41. package/src/commands/lock/show.ts +0 -115
  42. package/src/commands/namespace/list.ts +0 -138
  43. package/src/commands/namespace/show.ts +0 -130
  44. package/src/commands/prune.ts +0 -43
  45. package/src/commands/recipe/list.ts +0 -112
  46. package/src/commands/recipe/show.ts +0 -439
  47. package/src/commands/repo/add.ts +0 -145
  48. package/src/commands/repo/contribute.ts +0 -604
  49. package/src/commands/repo/gc.ts +0 -173
  50. package/src/commands/repo/init.ts +0 -136
  51. package/src/commands/repo/link.ts +0 -899
  52. package/src/commands/repo/list.ts +0 -236
  53. package/src/commands/repo/release.ts +0 -685
  54. package/src/commands/repo/remove.ts +0 -194
  55. package/src/commands/repo/search.ts +0 -243
  56. package/src/commands/repo/submit.ts +0 -341
  57. package/src/commands/repo/unlink.ts +0 -461
  58. package/src/commands/subscription/add.ts +0 -286
  59. package/src/commands/subscription/list.ts +0 -208
  60. package/src/commands/subscription/remove.ts +0 -182
  61. package/src/commands/subscription/update.ts +0 -216
  62. package/src/commands/update.ts +0 -178
  63. package/src/commands/vars/ask.ts +0 -417
  64. package/src/commands/vars/index.ts +0 -79
  65. package/src/commands/vars/list.ts +0 -67
  66. package/src/commands/vars/show.ts +0 -77
  67. package/src/config-command.ts +0 -30
  68. package/src/lib/build-preparation.ts +0 -128
  69. package/src/lib/build-service.ts +0 -580
  70. package/src/lib/config-discovery.ts +0 -378
  71. package/src/lib/config-inspect.ts +0 -145
  72. package/src/lib/config-kernel.mjs +0 -379
  73. package/src/lib/config-schema.ts +0 -401
  74. package/src/lib/env-file.ts +0 -328
  75. package/src/lib/env-local.ts +0 -212
  76. package/src/lib/errors.ts +0 -32
  77. package/src/lib/glob-pattern.ts +0 -24
  78. package/src/lib/include-resolver.ts +0 -523
  79. package/src/lib/interactive.ts +0 -165
  80. package/src/lib/markdown-compiler.ts +0 -947
  81. package/src/lib/package-info.ts +0 -25
  82. package/src/lib/pid-service.ts +0 -99
  83. package/src/lib/project-install.d.mts +0 -44
  84. package/src/lib/project-install.mjs +0 -234
  85. package/src/lib/project-scaffold/index.ts +0 -332
  86. package/src/lib/project-scaffold/templates.ts +0 -240
  87. package/src/lib/repos/catalog-display.ts +0 -215
  88. package/src/lib/repos/catalog-inputs.ts +0 -288
  89. package/src/lib/repos/catalog.ts +0 -842
  90. package/src/lib/repos/contribute.ts +0 -452
  91. package/src/lib/repos/core-recipe.ts +0 -105
  92. package/src/lib/repos/declarations.ts +0 -187
  93. package/src/lib/repos/defaults.ts +0 -175
  94. package/src/lib/repos/formats/common.ts +0 -440
  95. package/src/lib/repos/formats/index-file.ts +0 -253
  96. package/src/lib/repos/formats/links-map.ts +0 -98
  97. package/src/lib/repos/formats/lockfile.ts +0 -169
  98. package/src/lib/repos/formats/patterns.ts +0 -57
  99. package/src/lib/repos/formats/recipe-manifest.ts +0 -205
  100. package/src/lib/repos/formats/repo-manifest.ts +0 -97
  101. package/src/lib/repos/formats/store-entry.ts +0 -84
  102. package/src/lib/repos/formats/variable-definition.ts +0 -268
  103. package/src/lib/repos/freshness.ts +0 -264
  104. package/src/lib/repos/git-clone.ts +0 -850
  105. package/src/lib/repos/identity.ts +0 -89
  106. package/src/lib/repos/index.ts +0 -57
  107. package/src/lib/repos/links.ts +0 -356
  108. package/src/lib/repos/load-manifest.ts +0 -237
  109. package/src/lib/repos/lock-service.ts +0 -454
  110. package/src/lib/repos/locked-namespace-resolver.ts +0 -232
  111. package/src/lib/repos/locked-recipe-lookup.ts +0 -123
  112. package/src/lib/repos/locked-recipes.ts +0 -254
  113. package/src/lib/repos/managed-layer.ts +0 -422
  114. package/src/lib/repos/memories-hash-name.ts +0 -33
  115. package/src/lib/repos/namespace-resolver.ts +0 -527
  116. package/src/lib/repos/providers/base.ts +0 -268
  117. package/src/lib/repos/providers/git.ts +0 -245
  118. package/src/lib/repos/providers/github.ts +0 -589
  119. package/src/lib/repos/providers/gitlab.ts +0 -332
  120. package/src/lib/repos/providers/http.ts +0 -128
  121. package/src/lib/repos/providers/index-cache.ts +0 -421
  122. package/src/lib/repos/providers/index.ts +0 -106
  123. package/src/lib/repos/providers/local.ts +0 -410
  124. package/src/lib/repos/providers/provider.ts +0 -615
  125. package/src/lib/repos/recipe-config-layers.ts +0 -316
  126. package/src/lib/repos/recipe-key-matcher.ts +0 -62
  127. package/src/lib/repos/recipe-memories.ts +0 -178
  128. package/src/lib/repos/recipe-targets.ts +0 -236
  129. package/src/lib/repos/ref-search.ts +0 -46
  130. package/src/lib/repos/release/bump.ts +0 -161
  131. package/src/lib/repos/release/changelog.ts +0 -461
  132. package/src/lib/repos/release/git-state.ts +0 -460
  133. package/src/lib/repos/release/index-builder.ts +0 -1108
  134. package/src/lib/repos/release/index.ts +0 -19
  135. package/src/lib/repos/release/plan.ts +0 -521
  136. package/src/lib/repos/release/settle.ts +0 -154
  137. package/src/lib/repos/release/submissions.ts +0 -214
  138. package/src/lib/repos/release/submit-checkout.ts +0 -290
  139. package/src/lib/repos/release/submit-questions.ts +0 -153
  140. package/src/lib/repos/release/submit-service.ts +0 -898
  141. package/src/lib/repos/release/tags.ts +0 -239
  142. package/src/lib/repos/release/validate.ts +0 -463
  143. package/src/lib/repos/resolver.ts +0 -1028
  144. package/src/lib/repos/scaffold/index.ts +0 -245
  145. package/src/lib/repos/scaffold/templates.ts +0 -517
  146. package/src/lib/repos/seed.ts +0 -473
  147. package/src/lib/repos/store/contract.ts +0 -64
  148. package/src/lib/repos/store/hash.ts +0 -191
  149. package/src/lib/repos/store/recipe-store.ts +0 -599
  150. package/src/lib/repos/store/settings.ts +0 -58
  151. package/src/lib/repos/subscription-service.ts +0 -3859
  152. package/src/lib/repos/trust.ts +0 -447
  153. package/src/lib/repos/update-plan.ts +0 -235
  154. package/src/lib/self-update/index.ts +0 -14
  155. package/src/lib/self-update/installs.ts +0 -413
  156. package/src/lib/self-update/managers.ts +0 -126
  157. package/src/lib/self-update/plan.ts +0 -315
  158. package/src/lib/self-update/registry.ts +0 -99
  159. package/src/lib/self-update/run.ts +0 -506
  160. package/src/lib/self-update/versions.ts +0 -243
  161. package/src/lib/settings.ts +0 -1107
  162. package/src/lib/sous-home.ts +0 -104
  163. package/src/lib/state.ts +0 -167
  164. package/src/lib/vars/answers.ts +0 -192
  165. package/src/lib/vars/ask.ts +0 -1152
  166. package/src/lib/vars/definition-source.ts +0 -261
  167. package/src/lib/vars/display.ts +0 -231
  168. package/src/lib/vars/index.ts +0 -20
  169. package/src/lib/vars/ladder.ts +0 -282
  170. package/src/lib/vars/mappings.ts +0 -260
  171. package/src/lib/vars/names.ts +0 -94
  172. package/src/lib/vars/preanswers.ts +0 -395
  173. package/src/lib/vars/question-plan.ts +0 -241
  174. package/src/lib/vars/report.ts +0 -228
  175. package/src/lib/vars/safe-regex.ts +0 -235
  176. package/src/lib/vars/secrets.ts +0 -131
  177. package/src/lib/vars/validate.ts +0 -312
  178. package/src/lib/version-report.ts +0 -90
  179. package/src/lib/watch-loop.ts +0 -148
  180. package/src/lib/watch-service.ts +0 -116
  181. package/src/services/ref-resolver/container.ts +0 -107
  182. package/src/services/ref-resolver/dependency.ts +0 -60
  183. package/src/services/ref-resolver/format.ts +0 -197
  184. package/src/services/ref-resolver/glob.ts +0 -115
  185. package/src/services/ref-resolver/hash-names.ts +0 -155
  186. package/src/services/ref-resolver/index.ts +0 -55
  187. package/src/services/ref-resolver/injectable.ts +0 -36
  188. package/src/services/ref-resolver/location.ts +0 -62
  189. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
  190. package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
  191. package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
  192. package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
  193. package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
  194. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
  195. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
  196. package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
  197. package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
  198. package/src/services/ref-resolver/named-refs.ts +0 -144
  199. package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
  200. package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
  201. package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
  202. package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
  203. package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
  204. package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
  205. package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
  206. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
  207. package/src/services/ref-resolver/parts.ts +0 -115
  208. package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
  209. package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
  210. package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
  211. package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
  212. package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
  213. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
  214. package/src/services/ref-resolver/ref-helpers.ts +0 -62
  215. package/src/services/ref-resolver/ref-picker-service.ts +0 -204
  216. package/src/services/ref-resolver/ref-report.ts +0 -84
  217. package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
  218. package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
  219. package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
  220. package/src/services/ref-resolver/source.ts +0 -48
  221. package/src/services/ref-resolver/tokens.ts +0 -25
  222. package/src/services/ref-resolver/types.ts +0 -155
  223. package/src/templating/filters/bullet-list.ts +0 -9
  224. package/src/templating/filters/index.ts +0 -8
  225. package/src/templating/init-liquid-engine.ts +0 -139
  226. package/src/templating/lib/glob-files.ts +0 -74
  227. package/src/templating/lib/import-export.ts +0 -32
  228. package/src/templating/lib/secret-scope.ts +0 -41
  229. package/src/templating/lib/tag-args.ts +0 -19
  230. package/src/templating/tags/exportScalarVarsJs.ts +0 -46
  231. package/src/templating/tags/getFiles.ts +0 -89
  232. package/src/templating/tags/index.ts +0 -14
  233. package/src/templating/tags/listFiles.ts +0 -54
  234. package/src/templating/tags/showVars.ts +0 -28
  235. package/src/utils/choice-prompt.ts +0 -143
  236. package/src/utils/command-errors.ts +0 -186
  237. package/src/utils/command-help.ts +0 -45
  238. package/src/utils/confirm-prompt.ts +0 -110
  239. package/src/utils/flags.ts +0 -177
  240. package/src/utils/formatting.ts +0 -849
  241. package/src/utils/prompts.ts +0 -53
  242. package/src/utils/sous-directory.ts +0 -245
  243. package/src/utils/table.ts +0 -603
  244. package/src/utils/value-prompt.ts +0 -119
@@ -1,7 +1,9 @@
1
1
  # Discovery and Overrides
2
2
 
3
3
  How sous locates a project's configuration, and every way to override it. Discovery runs on
4
- every command; there is no opt-out.
4
+ every command; there is no opt-out. It runs once, when sous starts, and finds exactly one project:
5
+ every sous process (a CLI run, watch mode, or `sous mcp` for its whole life) serves the project it
6
+ started in.
5
7
 
6
8
  ## Locating the primary config
7
9
 
@@ -14,10 +16,19 @@ Precedence, highest first. Flags beat env vars; both beat walk-up discovery:
14
16
  5. Walk UP from the working directory to the filesystem root, taking the first `.sous/`
15
17
  directory that holds a primary config. A `.sous/` without one does not stop the walk.
16
18
 
17
- A primary config is named `sous.config.js`, `sous.config.mjs`, `sous.config.json`,
18
- `sous.config.jsonc` or `sous.config.yaml`. The `.jsonc` form is JSON with comments: line comments,
19
+ A primary config is named `sous.config.js`, `sous.config.mjs`, `sous.config.ts`,
20
+ `sous.config.json`, `sous.config.jsonc` or `sous.config.yaml`. The `.jsonc` form is JSON with comments: line comments,
19
21
  block comments and trailing commas are all allowed in it.
20
22
 
23
+ ## The project root
24
+
25
+ The project root is the directory that holds the `.sous/` directory the config was found in. When
26
+ `--config` names a file outside any `.sous/`, the root is the parent of the directory holding that
27
+ file. A project cannot set its root anywhere else: sous publishes it as the auto-variable
28
+ `${projectRoot}`, and setting `projectRoot` in any `_vars` block is an error.
29
+
30
+ ## How a location value is read
31
+
21
32
  Every flag or env value resolves with the same rules. It may point at:
22
33
 
23
34
  - a config file directly,
@@ -29,22 +40,22 @@ bare `export SOUS_CONFIG=`) is treated as unset and falls through to the next ti
29
40
  hijacks resolution. Error messages name the source that was actually set (`--sous-dir`,
30
41
  `SOUS_CONFIG`, and so on), not a generic flag.
31
42
 
32
- ## Locating the conf.d layer directory
43
+ ## Locating the conf.d directory
33
44
 
34
45
  Precedence: `--sous-confd <path>` flag, then `SOUS_CONFD` env var, then the default
35
- `<sousDir>/conf.d`. An override flows everywhere the default would: layer enumeration, the
36
- duplicate-baseName check, and watch-mode reload.
46
+ `<sousDir>/conf.d`. An override applies everywhere the default would: finding the `conf.d` files,
47
+ the check for file names that differ only by extension (below), and reloading in watch mode.
37
48
 
38
49
  ## Env file layering
39
50
 
40
- After discovery, sous loads two optional files from the discovered `.sous/` into the process
41
- environment:
51
+ After discovery, sous reads two optional files from the discovered `.sous/` into the environment
52
+ every plugin sees:
42
53
 
43
54
  - `.env.local`: gitignored; machine-specific values and secrets
44
55
  - `.env`: committed; team-shared defaults
45
56
 
46
57
  Load order is `.env.local` first, then `.env`, and no load ever overwrites a value that is
47
- already set. Effective precedence is therefore: real shell environment, then `.env.local`, then
58
+ already set. Config expressions read the result as `env.<NAME>`. Effective precedence is therefore: real shell environment, then `.env.local`, then
48
59
  `.env`. The syntax is deliberately small (not a shell): `KEY=value` lines, `#` comments, an
49
60
  optional `export ` prefix, single or double quoted values (`\n` and `\t` expand inside double
50
61
  quotes), and inline `# comment` stripped from unquoted values. Lines without `=` are ignored.
@@ -60,12 +71,12 @@ All are hard `ConfigError`s; sous never guesses:
60
71
  - **No config found**: the error lists every directory checked during the walk and names the two
61
72
  fixes: run `sous init` in the project's root directory, which writes the config and everything
62
73
  else a first build needs, or pass `--config <path>`.
63
- - **Multiple primary configs**: two or more of `sous.config.js|mjs|json|jsonc|yaml` in the same
74
+ - **Multiple primary configs**: two or more of `sous.config.js|mjs|ts|json|jsonc|yaml` in the same
64
75
  `.sous/` is an error naming every candidate.
65
- - **Duplicate layer baseNames**: any two loaded files (primary or conf.d) whose names differ
66
- only by extension is an error naming both files, because their merge order would otherwise
67
- depend on extension. `500-repos.json` and `500-repos.jsonc` collide for the same reason, which
68
- is what keeps a layer sous is migrating to `.jsonc` from being loaded twice.
76
+ - **Names that differ only by extension**: any two loaded files (primary or conf.d) whose names
77
+ differ only by extension is an error naming both files, because their merge order would
78
+ otherwise depend on the extension. `500-repos.json` and `500-repos.jsonc` collide for the same reason, which
79
+ is what keeps a file sous is migrating to `.jsonc` from being loaded twice.
69
80
 
70
81
  ## Interaction with `sous launch` pass-through
71
82
 
@@ -2,42 +2,60 @@
2
2
 
3
3
  ## The validation pipeline
4
4
 
5
- Every command validates the config on load, in this order:
5
+ Every command builds and validates the config before it does its work, in this order:
6
6
 
7
- 1. The kernel merges all layers (any layer failure names the file).
8
- 2. The legacy-schema guard rejects the removed multi-project keys (`projects`,
7
+ 1. Sous builds the config from every file, recipe and plugin (any config file failure names the
8
+ file; see [Layers and merging](config-layers.md)). Config expressions are evaluated as each file
9
+ is applied.
10
+ 2. A check for the old config format rejects the removed multi-project keys (`projects`,
9
11
  `defaultProject`) with a migration message.
10
- 3. A strict schema validates the merged result: unknown keys at ANY level are rejected with the
11
- full path and a typo hint; a target must have exactly one of `entryPoint` / `entryGlob`;
12
- `version`, when present, must be `1` (anything else fails as unsupported by this sous
13
- version).
12
+ 3. The built config is checked against the schema of every config key. Each plugin declares the
13
+ keys it reads, with their schema and defaults, so a key no loaded plugin declares is unknown.
14
+ Unknown keys at ANY level are rejected with the full path and a typo hint; a target must have
15
+ exactly one of `entryPoint`, `entryGlob` or `content`; `version`, when present, must be `1`
16
+ (anything else fails as unsupported by this sous version). Validation runs after values
17
+ resolve, so a value built from a variable is checked.
14
18
  4. Variable resolution and substitution run as values are used; `sous config validate` runs them
15
19
  eagerly (below).
16
20
 
17
- Validation applies to the MERGED config only. A `conf.d/` fragment on its own does not need to
18
- be a complete config; it only needs to use schema-known keys.
21
+ Validation applies to the BUILT config only. A `conf.d/` file on its own does not need to be a
22
+ complete config; it only needs to use known keys.
19
23
 
20
24
  ## The JSON Schema artifact
21
25
 
22
- Sous ships `sous.config.schema.json` at the package root (generated from the same schema that
23
- validates at load time). A JSON config can bind it for editor autocompletion and external
24
- tooling:
26
+ Sous ships `sous.config.schema.json` at the package root, for editor autocompletion and external
27
+ tooling. A JSON config can bind it:
25
28
 
26
29
  ```json
27
30
  { "$schema": "./path/to/sous.config.schema.json" }
28
31
  ```
29
32
 
30
- The `$schema` key is accepted at the top level and ignored by sous itself. A config written by
31
- `sous init --format json` binds it to the same artifact as published on GitHub for the running
32
- version, so an editor resolves it without a path into the installed package; point it at the
33
- package-local copy instead when the editor should work offline.
33
+ The `$schema` key is accepted at the top level and ignored by sous itself. Point it at the
34
+ package-local copy, or at the artifact published on GitHub for the version you run.
35
+
36
+ ?> Because each plugin declares its own config keys, the published schema file may not know a key
37
+ that a plugin adds. An editor may flag such a key even though sous accepts it; `sous config validate`
38
+ is the check that counts.
34
39
 
35
40
  ## sous config show
36
41
 
37
- Prints the full merged config as pretty JSON: the config as written, after layer merging but
38
- BEFORE variable resolution. Output is machine-readable by contract: the decorative header and
39
- any error text go to stderr, so `sous config show | jq .` always receives either valid JSON or
40
- nothing. Colorized only when stdout is a TTY.
42
+ Prints the full built config as pretty JSON, with config expressions evaluated: what sous will do on
43
+ this machine. Output is machine-readable by contract: the decorative header and any error text go to
44
+ stderr, so `sous config show | jq .` always receives either valid JSON or nothing. Colorized only
45
+ when stdout is a TTY.
46
+
47
+ - `--layers` shows each change, in order, and what made it (a file, a recipe or a plugin): for a
48
+ config file, the expression as written, what it evaluated to and the values it read; for code,
49
+ each builder operation it ran and the primitive patches it made
50
+ ([Patches](config-layers.md#patches) explains both). It ends with the result and the number of passes the config took to
51
+ settle.
52
+ - `--raw` prints each config file as written, in order.
53
+
54
+ ```text
55
+ confd/620-no-diagrams.js
56
+ removeWhere compilation.targets ← a builder operation
57
+ removeIndex compilation.targets[7] ← a primitive patch
58
+ ```
41
59
 
42
60
  ## sous config get
43
61
 
@@ -51,22 +69,26 @@ sous config get compilation.targets[0].entryPoint
51
69
  Scalars print raw (no quotes); objects and arrays print as pretty JSON. A missing path exits
52
70
  non-zero with a message on stderr.
53
71
 
54
- `--layers` adds provenance: for each loaded layer whose cumulative merge CHANGED the value at
55
- that path, one line prints the layer file and the old and new values, starting from `(unset)`.
56
- This answers "which file set this?" directly:
72
+ `--layers` adds where the value came from: every file, recipe or plugin that changed the value at
73
+ that path, in order, with its change. This answers "which file or plugin set this?" directly:
57
74
 
58
- ```bash
59
- sous config get tools.claude.command --layers
75
+ ```text
76
+ $ sous config get subscriptions."tool-usage/automated-browser-tasks".enabled --layers
77
+ conf.d/510-subscriptions.jsonc (not set)
78
+ conf.d/610-mac.jsonc { "$if": { "expr": "host.platform == 'darwin'", "then": false } }
79
+ → false (host.platform = "darwin")
80
+ result false (settled after 1 pass)
60
81
  ```
61
82
 
83
+ `sous plugins show` lists every plugin and what it adds or replaces, and `--timings` on any command
84
+ prints how long each step took, each config file included.
85
+
62
86
  ## sous config validate
63
87
 
64
- Runs the whole pipeline eagerly: discovery, kernel merge, legacy guard, schema, then FULL
88
+ Runs every check above at once: discovery, the config build, the old-format check, schema, then FULL
65
89
  variable resolution including compilation, tools and watch config. This surfaces the errors
66
90
  schema validation alone cannot: variable cycles, undefined `${refs}`, and bad substitutions in
67
- entry points and destinations. On success it prints a short summary (config file, layer count,
68
- target count, tools) and exits 0; on failure it prints the `ConfigError` and exits non-zero.
91
+ entry points and destinations. On success it prints a short summary of the config and exits 0; on failure it prints the `ConfigError` and exits non-zero.
69
92
 
70
- All three commands honor the same config-locating flags and env vars as every other command
71
- (`--config`/`-c`/`--sous-config`, `--sous-dir`, `--sous-confd`, `SOUS_CONFIG`, `SOUS_DIR`,
72
- `SOUS_CONFD`); see [Discovery and overrides](config-discovery.md).
93
+ All three commands honor the same config-locating flags and env vars as every other command; see
94
+ [Discovery and overrides](config-discovery.md).
@@ -1,92 +1,194 @@
1
1
  # Layers and Merging
2
2
 
3
- Every config source (the primary file plus each `conf.d/` layer) is loaded by one subprocess,
4
- the config kernel, which merges everything into a single cumulative config and returns it as
5
- plain JSON.
3
+ Sous builds one config for a project from many sources: the primary `sous.config.*` file, each
4
+ file in `conf.d/`, the config files recipes ship, the entries sous adds itself (the official
5
+ repository and the `core` subscription), and any plugin that changes the config. Sous combines
6
+ them all before any command does its work, and again after a command that changed the config.
6
7
 
7
- ## Load order
8
+ ## Which files are read
8
9
 
9
- 1. The primary `sous.config.*` file.
10
- 2. Every `*.js|mjs|json|jsonc|yaml` file directly inside `conf.d/` (non-recursive), sorted BYTEWISE
11
- by filename. The sort is locale-independent and per-machine stable, but it is not numeric:
12
- `10-a.json` sorts before `2-b.json` because the character `1` precedes `2`. Zero-pad numeric
13
- prefixes (`02-`, `10-`) when ordering matters.
10
+ - The primary `sous.config.*` file in `.sous/`.
11
+ - Every file directly inside `conf.d/` (not its subdirectories).
14
12
 
15
- A `.jsonc` layer is JSON with comments: line comments, block comments and trailing commas are all
16
- allowed in it, and it merges exactly like a `.json` one. The layers sous manages for you are
17
- written that way, so each can say in the file itself what it holds.
13
+ A `.json`, `.jsonc` or `.yaml` file is parsed; a `.js`, `.mjs` or `.ts` file is imported in
14
+ the sous process itself. A `.jsonc` file is JSON with comments: line comments, block comments and
15
+ trailing commas are all allowed in it. A file is read from disk once per build.
18
16
 
19
- ## JSON forcing
17
+ ## Load order, and why your files win
20
18
 
21
- Each layer's contribution passes through a JSON round-trip BEFORE merging:
19
+ The `conf.d/` files are applied after the primary config, in the order of the number each name
20
+ starts with. Files with the same number are applied in filename order (bytewise, the same on every
21
+ machine), and a file whose name starts with no number is applied as if it were numbered 500; a number above 9999 counts as 9999:
22
22
 
23
- - Functions and `undefined` values are dropped.
24
- - `RegExp` becomes `{}`; `Date` becomes its ISO string.
25
- - The final merged config is round-tripped once more on the way out.
23
+ ```text
24
+ plugins and recipes → sous.config.* → conf.d/500-repos → conf.d/510-subscriptions → conf.d/610-mac
25
+ ```
26
26
 
27
- Write configs as plain data; behavior belongs in `configure()` (below), which runs inside the
28
- kernel before serialization.
27
+ Give every file a number of the same length (three digits, as the files sous writes have) so the
28
+ order is obvious from a directory listing.
29
29
 
30
- ## Deep merge semantics
30
+ Your files are applied last, after every plugin and recipe. Whenever a plugin or a recipe's config
31
+ file changes the config, sous applies your `sous.config.*` and `conf.d` files again after it. So
32
+ when a plugin or a recipe sets a key that one of your files also sets, your value wins:
31
33
 
32
- Applied key by key when a later layer meets the cumulative config:
34
+ ```js
35
+ // .sous/sous.config.js (excerpt)
36
+ export const config = {
37
+ compilation: {
38
+ includeSourceComments: false,
39
+ },
40
+ };
41
+ ```
42
+
43
+ If a plugin sets `compilation.includeSourceComments` to `true`, the result is still `false`. In a
44
+ project whose files do not set it, the result is `true`.
45
+
46
+ Sous repeats this until nothing changes. A file that is applied again never doubles its own array
47
+ items. A config that never settles (two files that keep changing a value the other reads, for
48
+ example) stops sous with a `ConfigError` naming the values that kept changing and where each change
49
+ came from. Validation runs last, after variables resolve.
50
+
51
+ ## Merge semantics
52
+
53
+ A config file is merged key by key into the config built so far:
33
54
 
34
55
  - plain object + plain object: recurse
35
- - array + array: CONCATENATE, cumulative first, layer second. No dedupe; loading the same
36
- fragment twice duplicates its array entries. There is no declarative way to remove or replace
37
- an array entry; use `configure()` and mutate directly when you need that.
38
- - anything else (scalar, mixed types, null): the later layer replaces
39
- - an own `__proto__`, `constructor`, or `prototype` key in layer data is skipped
40
- (prototype-pollution guard)
41
-
42
- ## The JS/MJS layer contract
43
-
44
- A `.js` or `.mjs` layer may export a config object, a configure function, or both:
45
-
46
- - **Object**: `export const config = {...}`, or a default export that is a non-function object.
47
- Merged first.
48
- - **Function**: `export function configure(currentConfig, builder)`, or a default export that is
49
- a function. Runs AFTER the object (if any) merges. It may be async and is awaited. It may
50
- mutate `currentConfig` by reference freely. A returned object is merged after it resolves,
51
- UNLESS the return value IS `currentConfig` itself (the mutate-and-return-for-chaining idiom),
52
- which is skipped so arrays are not duplicated by a self-merge.
53
-
54
- ## The builder
55
-
56
- `configure` receives a builder with:
57
-
58
- - `builder.config`: the live cumulative config (same object as `currentConfig`)
59
- - `builder.sousDir`, `builder.confDir`: resolved directories
60
- - `builder.currentFile`: absolute path of the layer being loaded
61
- - `builder.env(name, fallback)`: read an environment variable (env files are already loaded at
62
- this point)
63
- - `builder.merge(obj)`: JSON-force `obj` and deep-merge it into the cumulative config
64
- - `builder.loadConfig(path)`: load another file (any supported extension) under the full layer
65
- contract, including its own nested `configure`. Async.
66
- - `builder.loadConfigs(globPattern)`: glob, sort matches bytewise, and load each. Async.
67
-
68
- Builder path arguments resolve BEFORE variable resolution, so user `_vars` do not exist yet.
69
- Only the auto-vars may appear in them: `${sousDir}`, `${sousConfDir}`, `${sousRootPath}`,
70
- `${sousVersion}`. Any other `${name}` in a builder path is a hard error listing the allowed
71
- names. Relative paths resolve against the directory of `builder.currentFile`. Load cycles (a
72
- file loading itself, directly or indirectly) are detected and reported.
56
+ - array + array: CONCATENATE, existing items first, the file's second. Duplicates are kept.
57
+ - anything else (scalar, mixed types, null): the later file replaces
58
+ - an own `__proto__`, `constructor`, or `prototype` key in file data is skipped
59
+
60
+ To remove or replace an array item without code, write a [patch file](#patch-files).
61
+
62
+ ## Plain JSON data
63
+
64
+ The config is always plain JSON data, so `sous config show` can print it exactly. Every value passes
65
+ through a JSON round-trip: functions and `undefined` values are dropped, and a regular expression is
66
+ written as a string `/.../`. [Config expressions](configuration.md#config-expressions) rely on
67
+ `undefined` values being dropped.
68
+
69
+ ## JavaScript and TypeScript files
70
+
71
+ A `.js`, `.mjs` or `.ts` config file may export plain data (`export const config = {...}`, or a
72
+ default export that is an object), which is merged like a JSON file. It may also export
73
+ `configure(config)`, which is given a builder object for changing the config:
74
+
75
+ ```js
76
+ // .sous/conf.d/620-no-diagrams.js
77
+ export function configure(config) {
78
+ config.atPath("compilation.targets").removeWhere((target) => target.id === "diagrams");
79
+ }
80
+ ```
81
+
82
+ `configure` runs again whenever sous applies the file again, so it must give the same result for
83
+ the same input. When a `.js` file changes on disk during watch mode, it is imported again.
84
+
85
+ Some of the builder's operations (the full list, with each one's documentation, is the `DocumentNode`
86
+ type in `packages/plugin-api/src/node.ts`):
87
+
88
+ ```js
89
+ const node = config.atPath("compilation.targets");
90
+
91
+ // Inspecting
92
+ node.exists();
93
+ node.type(); // "object" | "array" | "string" | "number" | "boolean" | "null"
94
+ node.isArray();
95
+ node.isObject();
96
+ node.get();
97
+ node.valueOr(fallback);
98
+
99
+ // Objects and values
100
+ node.set(value);
101
+ node.delete();
102
+ node.rename("newKey");
103
+ node.merge(object); // key by key; arrays joined
104
+ node.replace(object); // whole
105
+ config.atPath("tools").keys();
106
+
107
+ // Arrays
108
+ node.append(item);
109
+ node.prepend(item);
110
+ node.insertAt(2, item);
111
+ node.removeAt(2);
112
+ node.removeWhere((target) => target.id === "diagrams");
113
+ node.upsertWhere((target) => target.id === "diagrams", item); // replace the match, or append
114
+ node.findWhere((target) => target.id === "diagrams"); // a node for the match, to edit in place
115
+ node.atIndex(0);
116
+ node.move(from, to);
117
+ node.dedupe();
118
+
119
+ // The whole config
120
+ config.root().merge(data);
121
+ ```
122
+
123
+ ?> A `configure(currentConfig, builder)` written for sous 0.2.x, which changes `currentConfig` in
124
+ place and calls the old builder (`builder.merge`, `builder.loadConfig`, `builder.loadConfigs`,
125
+ `builder.env`), keeps working. Sous records what it changed like any other change.
126
+
127
+ ## Patches
128
+
129
+ Every change to the config, from a file, a recipe or a plugin, ends up as primitive operations,
130
+ each called a patch:
131
+
132
+ | `op` | What it does |
133
+ | --- | --- |
134
+ | `setKey` | sets one key of an object |
135
+ | `deleteKey` | deletes one key of an object |
136
+ | `renameKey` | renames one key of an object |
137
+ | `setIndex` | sets one item of an array |
138
+ | `insertAt` | inserts one item into an array |
139
+ | `removeIndex` | removes one item from an array |
140
+
141
+ Every other builder operation is built from these. `removeWhere` above, for example, records one
142
+ `removeIndex` per matching item. `sous config show --layers` shows both: each operation a file or
143
+ plugin ran, and the patches it made.
144
+
145
+ ### Patch files
146
+
147
+ A file of raw patches can sit in `conf.d/` or beside `sous.config.*`:
148
+
149
+ - A `.patch.json` file holds one patch, unless its top-level object has a single key, `$patches`,
150
+ holding an array: then each item is a patch.
151
+ - A `.patch.jsonl` file holds one patch per line, so a tool or an agent can add a line without
152
+ parsing the file.
153
+
154
+ ```jsonc
155
+ // .sous/conf.d/720-my-special-thing.patch.json: several patches, so a single $patches key
156
+ {
157
+ "$patches": [
158
+ { "op": "setKey", "path": "compilation", "key": "overwriteUntracked", "value": false },
159
+ { "op": "deleteKey", "path": "tools", "key": "codex" }
160
+ ]
161
+ }
162
+ ```
163
+
164
+ ```jsonl
165
+ // .sous/conf.d/721-my-list-of-things.patch.jsonl: one patch per line
166
+ {"op": "insertAt", "path": "compilation.targets", "index": 0, "value": {"id": "first", "entryPoint": "a.tpl.md"}}
167
+ {"op": "setKey", "path": "_vars", "key": "machineRole", "value": "laptop"}
168
+ ```
169
+
170
+ !> An index written in a patch file is never recomputed: `removeIndex 7` removes whatever is at
171
+ index 7 at that point. Use patch files for arrays only when you know the order the files, recipes and
172
+ plugins before it apply in. Key and path operations have no such catch.
73
173
 
74
174
  ## Failure behavior
75
175
 
76
- Any layer failure (a JSON/YAML parse error, an import error, a `configure` throw, a load cycle,
77
- the legacy multi-project schema) halts the whole load with an error naming the layer file. There
78
- is no warn-and-skip; a broken layer never silently drops out of the merge.
176
+ Any config file failure (a JSON/YAML parse error, an import error, a `configure` throw) halts sous
177
+ with a `ConfigError` naming the file. There is no warn-and-skip; a broken file never silently drops
178
+ out of the config.
79
179
 
80
180
  ## The managed 5xx layer band
81
181
 
82
- `conf.d/500-*` through `conf.d/599-*` is reserved for layers the sous CLI writes on your behalf.
83
- They are `.jsonc`, and each one opens with a header comment saying what it holds.
182
+ `conf.d/500-*` through `conf.d/599-*` is reserved for files the sous CLI writes on your behalf.
183
+ They are `.jsonc`, and each one opens with a header comment saying what it holds. They are ordinary
184
+ config files, merged like any other: sous always reads the built config, never one file on its own.
84
185
 
85
186
  **Sous edits these files by key; you may edit them too.** A write rewrites only the bytes of the
86
187
  entry that changes, so your comments, your key order and your formatting survive it, and repeated
87
188
  edits produce minimal version-control diffs. Sous never edits a hand-written primary config or a
88
- layer outside the 5xx band. To override a managed value rather than change it in place, add a
89
- higher-sorting layer (for example `conf.d/600-overrides.json`).
189
+ file outside the 5xx band. To override a managed value rather than change it in place, add a
190
+ higher-sorting file (for example `conf.d/600-overrides.json`), or a
191
+ [config expression](configuration.md#config-expressions) that switches it on some machines only.
90
192
 
91
193
  Where a comment is impossible because a format really is strict JSON, the convention is a `//`
92
194
  key, which sous ignores wherever it appears.
@@ -10,12 +10,23 @@ This page is about the config side only.
10
10
  Later scopes override earlier ones:
11
11
 
12
12
  ```
13
- auto-vars -> _env scope -> _vars -> compilation._vars -> target._vars -> output._vars
13
+ auto-vars -> recipe answers -> _env scope -> _vars -> compilation._vars -> target._vars -> output._vars
14
14
  ```
15
15
 
16
- Nested `_vars` blocks (on `compilation`, on a target, on an output) resolve with their parent
17
- scope inherited, so a target var may reference a top-level var, and an output var may override
18
- both for that output only.
16
+ `_vars` may be set at four levels, each inheriting the levels above it, inner values winning:
17
+
18
+ ```text
19
+ root _vars
20
+ └── compilation._vars
21
+ └── compilation.targets[]._vars
22
+ └── targets[].outputs[]._vars
23
+ ```
24
+
25
+ So a target var may reference a top-level var, and an output var may override both for that output
26
+ only. A [config expression](configuration.md#config-expressions) sees the `_vars` of every level
27
+ that contains it, and no others; an expression under `subscriptions`, `repos` or any other top-level
28
+ key sees only the root `_vars`. Recipe outputs also get a scope per recipe, holding that recipe's
29
+ answers (see [Recipe variables](repositories-variables.md)).
19
30
 
20
31
  ## Auto-vars
21
32
 
@@ -23,6 +34,7 @@ Always injected first, before `_env` and `_vars`:
23
34
 
24
35
  | Variable | Value |
25
36
  |----------|-------|
37
+ | `projectRoot` | the project root: the directory that holds `.sous/` (see [The project root](config-discovery.md#the-project-root)) |
26
38
  | `sousDir` | the discovered `.sous/` directory holding the active config |
27
39
  | `sousConfDir` | the `conf.d/` drop-in directory for the active config |
28
40
  | `sousConfigPath` | absolute path of the active primary config file |
@@ -31,6 +43,10 @@ Always injected first, before `_env` and `_vars`:
31
43
  | `sousTemplatePath` | absolute path of the `.tpl.` file being rendered (render time only) |
32
44
  | `sousTemplateDir` | directory of the `.tpl.` file being rendered (render time only) |
33
45
 
46
+ !> Sous sets `projectRoot`. Setting it in any `_vars` block is an error saying so. A config that
47
+ wrote `projectRoot: "${sousDir}/.."` before 1.0 drops that line; every `${projectRoot}` keeps
48
+ working.
49
+
34
50
  The `sous*` namespace is reserved: defining any var whose name starts with `sous` earns a
35
51
  warning and risks colliding with a future auto-var.
36
52
 
@@ -42,14 +58,31 @@ shell environment, `.sous/.env.local`, or `.sous/.env` (first writer wins, in th
42
58
  mapped env var that is not set anywhere is a hard `ConfigError` telling you which file to define
43
59
  it in.
44
60
 
45
- ## Fixpoint resolution
61
+ ## Variables set in a later file
62
+
63
+ A [config expression](configuration.md#config-expressions) always sees a variable's final value,
64
+ even when a later file sets it. Sous notes every variable an expression reads, and when that
65
+ variable's value changes, it applies the file again
66
+ ([Layers and merging](config-layers.md#load-order-and-why-your-files-win)):
67
+
68
+ ```text
69
+ Files: sous.config (machineRole: laptop) → 610-browser ($if machineRole == 'laptop') → 700-ci (machineRole: ci)
70
+
71
+ 610-browser reads machineRole → "laptop" → enabled: true; 700-ci then sets "ci"
72
+ 610-browser is applied again: machineRole is now "ci" → enabled: false
73
+ nothing it read changed → done
74
+ ```
75
+
76
+ A variable still unknown when the config settles is an error naming it.
77
+
78
+ ## Variables that refer to other variables
46
79
 
47
- Each `_vars` block resolves by a fixpoint loop, not top-to-bottom: every round re-scans the
48
- still-unresolved entries and finalizes any whose `${refs}` all resolve, repeating until a round
49
- finalizes nothing. Declaration order therefore never matters; `{ file: "${root}/x", root:
80
+ A variable may refer to others in the same `_vars` block, in any order. Sous resolves every entry
81
+ whose `${refs}` are all known, then tries the rest again, until nothing more resolves. Declaration
82
+ order therefore never matters; `{ file: "${root}/x", root:
50
83
  "/data" }` resolves as readily as the reverse.
51
84
 
52
- If entries remain unresolved when progress stops, that is a hard `ConfigError`. The message
85
+ If entries remain unresolved at the end, that is a hard `ConfigError`. The message
53
86
  separates:
54
87
 
55
88
  - **Cycles**: entries that reference each other (members are named), and
@@ -64,8 +97,8 @@ reference.
64
97
  ## Path normalization
65
98
 
66
99
  Absolute `entryPoint`, `entryGlob`, `globBase`, `destinationFile` and `destinationDir` values
67
- are normalized after substitution, so the common `${sousDir}/..` idiom collapses to the real
68
- parent directory. This matters: prune compares tracked outputs against `destinationDir` by
100
+ are normalized after substitution, so a path such as `${sousDir}/../docs` collapses to the real
101
+ directory. This matters: prune compares tracked outputs against `destinationDir` by
69
102
  string prefix, and watch mode matches event paths against watched entries; both need the
70
103
  normalized form.
71
104