@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,378 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { ConfigError } from "./errors.js";
4
- import {
5
- listRecipeConfigLayers,
6
- type RecipeConfigLayer,
7
- } from "./repos/recipe-config-layers.js";
8
-
9
- /**
10
- * The directory name sous looks for when walking up from the working directory.
11
- */
12
- export const SOUS_DIR_NAME = ".sous";
13
-
14
- /**
15
- * Primary config file names searched inside a `.sous/` directory. Exactly ONE of
16
- * these may exist per `.sous/`; more than one is a hard error (no silent
17
- * first-match-wins).
18
- */
19
- export const CONFIG_FILE_NAMES = [
20
- "sous.config.js",
21
- "sous.config.mjs",
22
- "sous.config.json",
23
- "sous.config.jsonc",
24
- "sous.config.yaml",
25
- ] as const;
26
-
27
- /**
28
- * The drop-in config layer directory inside `.sous/`. Every
29
- * `conf.d/*.{js,mjs,json,jsonc,yaml}` file (non-recursive) is loaded after the
30
- * primary config and deep-merged in bytewise filename order.
31
- */
32
- export const CONFD_DIR_NAME = "conf.d";
33
-
34
- /**
35
- * File extensions recognised as config layers inside `conf.d/`. A `.jsonc`
36
- * layer is JSON with comments: line comments, block comments and trailing
37
- * commas are all allowed in it, which is why the layers sous manages for a
38
- * project are written that way.
39
- */
40
- export const LAYER_EXTENSIONS = [".js", ".mjs", ".json", ".jsonc", ".yaml"] as const;
41
-
42
- /**
43
- * The name of the optional shared-defaults env file inside `.sous/`. This file
44
- * is meant to be committed: it holds values a whole team shares, never secrets.
45
- */
46
- export const ENV_DEFAULTS_NAME = ".env";
47
-
48
- /** The name of the optional machine-specific env file inside `.sous/`. */
49
- export const ENV_LOCAL_NAME = ".env.local";
50
-
51
- /** A located sous configuration. */
52
- export type DiscoveredConfig = {
53
- /** Absolute path to the primary config file itself. */
54
- configPath: string;
55
- /**
56
- * Absolute path to the `.sous/` directory holding the config, when the config
57
- * was found by walking up. For an explicit `--config` path this is the config
58
- * file's parent directory, whatever it is called.
59
- */
60
- sousDir: string;
61
- /** Absolute path to the `conf.d/` drop-in directory (may not exist). */
62
- confDir: string;
63
- /**
64
- * Ordered absolute paths of every config layer: the primary config first,
65
- * then any config layers subscribed recipes contribute, then the `conf.d/`
66
- * layers in bytewise filename order. Recipes sit in the middle so a recipe can
67
- * supply defaults and the project always wins over them.
68
- */
69
- layerPaths: string[];
70
- /** The subset of `layerPaths` that came from subscribed recipes. */
71
- recipeLayerPaths: string[];
72
- /**
73
- * The recipe layers themselves, already read and already filtered down to the
74
- * keys a recipe is allowed to set. Sous reads these instead of handing their
75
- * paths to the config kernel, so a recipe cannot set a key that would change
76
- * what sous trusts or what sous runs. See `repos/recipe-config-layers.ts`.
77
- */
78
- recipeLayers: RecipeConfigLayer[];
79
- /**
80
- * Complete, plain-language sentences about anything a recipe contributed that
81
- * sous declined to load. Printed by the command, since discovery runs before
82
- * there is anywhere good to print.
83
- */
84
- recipeLayerWarnings: string[];
85
- /** How the config was located. */
86
- source: "flag" | "walk-up";
87
- };
88
-
89
- /**
90
- * Returns the list of directories to check, starting at `startDir` and walking
91
- * up to the filesystem root. Used by discovery and by the not-found error message.
92
- */
93
- export function candidateDirs(startDir: string): string[] {
94
- const dirs: string[] = [];
95
- let current = path.resolve(startDir);
96
-
97
- for (;;) {
98
- dirs.push(current);
99
- const parent = path.dirname(current);
100
- if (parent === current) break;
101
- current = parent;
102
- }
103
-
104
- return dirs;
105
- }
106
-
107
- /**
108
- * Returns the primary config file inside `sousDir`, or null when none exists.
109
- *
110
- * @throws ConfigError when MORE THAN ONE primary candidate exists — sous never
111
- * silently picks one of several `sous.config.*` files.
112
- */
113
- export function findConfigInSousDir(sousDir: string): string | null {
114
- const found: string[] = [];
115
- for (const name of CONFIG_FILE_NAMES) {
116
- const candidate = path.join(sousDir, name);
117
- if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
118
- found.push(candidate);
119
- }
120
- }
121
-
122
- if (found.length > 1) {
123
- throw new ConfigError(
124
- `Multiple primary sous config files found in ${sousDir}:\n` +
125
- found.map((f) => ` ${f}`).join("\n") +
126
- `\n A .sous/ directory may hold exactly one of: ${CONFIG_FILE_NAMES.join(", ")}.\n` +
127
- ` Keep one primary config and move the rest into ${CONFD_DIR_NAME}/ (with unique names) or delete them.`
128
- );
129
- }
130
-
131
- return found[0] ?? null;
132
- }
133
-
134
- /**
135
- * Compares two strings bytewise (plain `<` on the string), locale-independent
136
- * so layer order is identical on every machine. Note this is NOT numeric:
137
- * `10-` sorts before `2-` (zero-pad layer prefixes if that matters).
138
- */
139
- function bytewiseCompare(a: string, b: string): number {
140
- return a < b ? -1 : a > b ? 1 : 0;
141
- }
142
-
143
- /**
144
- * Lists the config layer files inside a `conf.d/` directory: every
145
- * `*.{js,mjs,json,jsonc,yaml}` file directly inside it (non-recursive), sorted
146
- * bytewise by filename. A missing directory yields an empty list.
147
- */
148
- export function listConfDirLayers(confDir: string): string[] {
149
- if (!fs.existsSync(confDir) || !fs.statSync(confDir).isDirectory()) return [];
150
-
151
- return fs
152
- .readdirSync(confDir)
153
- .filter((name) => {
154
- const ext = path.extname(name).toLowerCase();
155
- if (!(LAYER_EXTENSIONS as readonly string[]).includes(ext)) return false;
156
- const full = path.join(confDir, name);
157
- return fs.statSync(full).isFile();
158
- })
159
- .sort(bytewiseCompare)
160
- .map((name) => path.join(confDir, name));
161
- }
162
-
163
- /**
164
- * Asserts that every loaded config file (primary + conf.d layers) has a unique
165
- * baseName; the filename minus its FINAL extension. Two layers named
166
- * `500-repos.json` and `500-repos.jsonc` would otherwise merge in an order that
167
- * depends on their extensions, which is never what the author meant. It is also
168
- * what stops a managed layer from existing under both its old `.json` name and
169
- * its `.jsonc` one.
170
- *
171
- * @throws ConfigError naming both conflicting files.
172
- */
173
- export function assertUniqueLayerBaseNames(layerPaths: string[]): void {
174
- const seen = new Map<string, string>();
175
- for (const layerPath of layerPaths) {
176
- const base = path.basename(layerPath, path.extname(layerPath));
177
- const existing = seen.get(base);
178
- if (existing !== undefined) {
179
- throw new ConfigError(
180
- `Duplicate config layer baseName '${base}':\n` +
181
- ` ${existing}\n` +
182
- ` ${layerPath}\n` +
183
- ` Every loaded config file (the primary config and all ${CONFD_DIR_NAME}/ layers) must have a\n` +
184
- ` unique filename once its final extension is removed. Rename one of them.`
185
- );
186
- }
187
- seen.set(base, layerPath);
188
- }
189
- }
190
-
191
- /**
192
- * Builds a full DiscoveredConfig from a located primary config: computes the
193
- * conf.d directory, enumerates its layers and the layers subscribed recipes
194
- * contribute, and runs the duplicate-baseName check.
195
- *
196
- * Recipe layers load after the primary config and before the `conf.d/` layers,
197
- * so a recipe supplies defaults and the project always wins over them. They are
198
- * left out of the duplicate-baseName check deliberately: that check exists so a
199
- * person never has to guess which of two files they wrote merges last, and a
200
- * recipe's file names are not theirs to rename.
201
- *
202
- * @param confDirOverride - Absolute path to use as the conf.d directory instead
203
- * of `<sousDir>/conf.d`. Set from the `--sous-confd` flag or `SOUS_CONFD` env
204
- * var; discovery then builds its layers from the overridden directory.
205
- */
206
- function buildDiscoveredConfig(
207
- configPath: string,
208
- sousDir: string,
209
- source: DiscoveredConfig["source"],
210
- confDirOverride?: string
211
- ): DiscoveredConfig {
212
- const confDir = confDirOverride ?? path.join(sousDir, CONFD_DIR_NAME);
213
- const projectLayers = [configPath, ...listConfDirLayers(confDir)];
214
- assertUniqueLayerBaseNames(projectLayers);
215
-
216
- const recipes = listRecipeConfigLayers(sousDir);
217
- const layerPaths = [
218
- configPath,
219
- ...recipes.paths,
220
- ...projectLayers.slice(1),
221
- ];
222
-
223
- return {
224
- configPath,
225
- sousDir,
226
- confDir,
227
- layerPaths,
228
- recipeLayerPaths: recipes.paths,
229
- recipeLayers: recipes.layers,
230
- recipeLayerWarnings: recipes.warnings,
231
- source,
232
- };
233
- }
234
-
235
- /**
236
- * Re-runs the conf.d enumeration, the recipe layer enumeration and the
237
- * duplicate-baseName check for an existing discovery.
238
- *
239
- * Two callers need it. Watch mode calls it because layer files can appear or
240
- * disappear while watching. `BaseCommand.init()` calls it once after the
241
- * `.sous/` env files are loaded, because `SOUS_HOME` is file-settable and it
242
- * decides where the store holding the recipe layers is.
243
- *
244
- * The existing `confDir` is preserved (not recomputed from `sousDir`), so a
245
- * `SOUS_CONFD` / `--sous-confd` override survives across watch reloads.
246
- */
247
- export function refreshDiscoveredConfig(discovered: DiscoveredConfig): DiscoveredConfig {
248
- return buildDiscoveredConfig(
249
- discovered.configPath,
250
- discovered.sousDir,
251
- discovered.source,
252
- discovered.confDir
253
- );
254
- }
255
-
256
- /**
257
- * Walks up from `startDir` looking for a `.sous/` directory that contains one of
258
- * CONFIG_FILE_NAMES. The first directory with a match wins; a `.sous/` directory
259
- * without a config file does not stop the walk.
260
- *
261
- * @param startDir - Directory to start from (normally `process.cwd()`).
262
- * @param confDirOverride - Absolute conf.d directory to use instead of
263
- * `<sousDir>/conf.d` (from `--sous-confd` / `SOUS_CONFD`).
264
- * @returns The discovered config, or null when nothing was found.
265
- * @throws ConfigError when a `.sous/` holds several primary configs, or when
266
- * loaded layer baseNames collide.
267
- */
268
- export function discoverConfig(
269
- startDir: string = process.cwd(),
270
- confDirOverride?: string
271
- ): DiscoveredConfig | null {
272
- for (const dir of candidateDirs(startDir)) {
273
- const sousDir = path.join(dir, SOUS_DIR_NAME);
274
- if (!fs.existsSync(sousDir) || !fs.statSync(sousDir).isDirectory()) continue;
275
-
276
- const configPath = findConfigInSousDir(sousDir);
277
- if (configPath) return buildDiscoveredConfig(configPath, sousDir, "walk-up", confDirOverride);
278
- }
279
-
280
- return null;
281
- }
282
-
283
- /**
284
- * Resolves an explicit `--config` value into a DiscoveredConfig.
285
- *
286
- * The value may point at a config file or at a directory. A directory is searched
287
- * for CONFIG_FILE_NAMES, and if the directory itself is not named `.sous`, its
288
- * `.sous/` child is searched too — so `--config .` works inside a project root.
289
- *
290
- * @param configFlag - The raw `--config` value (relative paths resolve against cwd).
291
- * @param cwd - Base directory for relative paths.
292
- * @param confDirOverride - Absolute conf.d directory to use instead of
293
- * `<sousDir>/conf.d` (from `--sous-confd` / `SOUS_CONFD`).
294
- * @param sourceLabel - How the caller supplied the value (`--config`,
295
- * `--sous-config`, `SOUS_CONFIG`, `--sous-dir`, `SOUS_DIR`). Used only in error
296
- * messages so a user who set `SOUS_DIR` is not told to fix `--config`.
297
- * @returns The resolved config.
298
- * @throws When the path does not exist or holds no recognised config file.
299
- */
300
- export function resolveConfigFlag(
301
- configFlag: string,
302
- cwd: string = process.cwd(),
303
- confDirOverride?: string,
304
- sourceLabel = "--config"
305
- ): DiscoveredConfig {
306
- const resolved = path.resolve(cwd, expandHome(configFlag));
307
-
308
- if (!fs.existsSync(resolved)) {
309
- throw new Error(`${sourceLabel} path not found: ${resolved}`);
310
- }
311
-
312
- if (fs.statSync(resolved).isDirectory()) {
313
- const direct = findConfigInSousDir(resolved);
314
- if (direct) {
315
- return buildDiscoveredConfig(direct, resolved, "flag", confDirOverride);
316
- }
317
-
318
- const nested = path.join(resolved, SOUS_DIR_NAME);
319
- if (fs.existsSync(nested) && fs.statSync(nested).isDirectory()) {
320
- const nestedConfig = findConfigInSousDir(nested);
321
- if (nestedConfig) {
322
- return buildDiscoveredConfig(nestedConfig, nested, "flag", confDirOverride);
323
- }
324
- }
325
-
326
- throw new Error(
327
- `${sourceLabel} points at a directory with no sous config file: ${resolved}\n` +
328
- `Looked for: ${CONFIG_FILE_NAMES.join(", ")}`
329
- );
330
- }
331
-
332
- return buildDiscoveredConfig(resolved, path.dirname(resolved), "flag", confDirOverride);
333
- }
334
-
335
- /**
336
- * Expands a leading `~` to the user's home directory. Leaves other paths alone.
337
- */
338
- export function expandHome(inputPath: string): string {
339
- if (inputPath === "~") return process.env.HOME ?? inputPath;
340
- if (inputPath.startsWith("~/")) {
341
- const home = process.env.HOME;
342
- if (home) return path.join(home, inputPath.slice(2));
343
- }
344
- return inputPath;
345
- }
346
-
347
- /**
348
- * Builds the message shown when no config could be found. Names every directory
349
- * that was checked and shows how to fix it.
350
- *
351
- * @param startDir - The directory discovery started from.
352
- */
353
- export function formatNotFoundMessage(startDir: string = process.cwd()): string {
354
- const dirs = candidateDirs(startDir);
355
- const shown = dirs.slice(0, 8);
356
- const omitted = dirs.length - shown.length;
357
-
358
- const checked = shown
359
- .map((dir) => ` ${path.join(dir, SOUS_DIR_NAME)}/`)
360
- .join("\n");
361
-
362
- const more = omitted > 0 ? `\n ... and ${omitted} more parent director${omitted === 1 ? "y" : "ies"}` : "";
363
-
364
- return [
365
- "No sous config found.",
366
- "",
367
- `Starting at ${startDir}, sous walked up looking for a ${SOUS_DIR_NAME}/ directory`,
368
- `containing one of: ${CONFIG_FILE_NAMES.join(", ")}`,
369
- "",
370
- " Checked:",
371
- checked + more,
372
- "",
373
- " To fix this, either:",
374
- ` 1. Set the project up: run 'sous init' in its root directory, which writes`,
375
- ` ${SOUS_DIR_NAME}/${CONFIG_FILE_NAMES[0]} and everything else a first build needs, or`,
376
- " 2. Pass the config explicitly: sous <command> --config <path>",
377
- ].join("\n");
378
- }
@@ -1,145 +0,0 @@
1
- import { styleText } from "node:util";
2
- import { ConfigError } from "./errors.js";
3
-
4
- /**
5
- * Shared helpers for the `sous config *` inspection commands: dot-path lookup
6
- * with `[n]` array indexing, colorized pretty-JSON rendering, and value
7
- * truncation for the `--layers` provenance view.
8
- */
9
-
10
- /** Sentinel returned by lookupPath when a path segment does not exist. */
11
- export const NOT_FOUND = Symbol("sous.config.path.not-found");
12
-
13
- /**
14
- * Splits a dot-path with optional `[n]` array indices into ordered segments.
15
- * `compilation.targets[0].entryPoint` → `["compilation", "targets", 0, "entryPoint"]`.
16
- * Numeric-looking bracket segments become numbers; everything else is a string key.
17
- *
18
- * @throws ConfigError on a malformed path (empty, unbalanced brackets, etc.).
19
- */
20
- export function parsePath(path: string): (string | number)[] {
21
- const trimmed = path.trim();
22
- if (trimmed === "") {
23
- throw new ConfigError("Empty config path. Provide a dot-path like 'compilation.targets[0].entryPoint'.");
24
- }
25
-
26
- const segments: (string | number)[] = [];
27
- // Consume, in order: bare keys, `[n]` array indices, and `.` separators
28
- // between them. Any character that fits none of these (e.g. `a..b`, `a[]`,
29
- // `a[x]`) leaves a gap the scanner reports as malformed.
30
- const tokenRe = /([^.[\]]+)|\[(\d+)\]|(\.)/g;
31
- let lastIndex = 0;
32
- let expectKey = false; // a `.` was just consumed, so a key must follow
33
- let match: RegExpExecArray | null;
34
-
35
- while ((match = tokenRe.exec(trimmed)) !== null) {
36
- if (match.index !== lastIndex) {
37
- throw new ConfigError(`Malformed config path near '${trimmed.slice(lastIndex)}' in '${path}'.`);
38
- }
39
- if (match[1] !== undefined) {
40
- segments.push(match[1]);
41
- expectKey = false;
42
- } else if (match[2] !== undefined) {
43
- if (expectKey) {
44
- throw new ConfigError(`Malformed config path near '${trimmed.slice(match.index)}' in '${path}'.`);
45
- }
46
- segments.push(Number(match[2]));
47
- } else {
48
- // A `.` separator: a key must follow it, and one may not lead the path or
49
- // directly follow another `.` (i.e. `.a`, `a..b` are malformed).
50
- if (expectKey || segments.length === 0) {
51
- throw new ConfigError(`Malformed config path near '${trimmed.slice(match.index)}' in '${path}'.`);
52
- }
53
- expectKey = true;
54
- }
55
- lastIndex = tokenRe.lastIndex;
56
- }
57
-
58
- if (lastIndex !== trimmed.length || expectKey) {
59
- throw new ConfigError(`Malformed config path near '${trimmed.slice(lastIndex) || "end of path"}' in '${path}'.`);
60
- }
61
-
62
- return segments;
63
- }
64
-
65
- /**
66
- * Walks `root` following `segments`, returning the value found or NOT_FOUND when
67
- * any segment is missing (or the path descends into a non-indexable value).
68
- */
69
- export function lookupPath(root: unknown, segments: (string | number)[]): unknown {
70
- let current: unknown = root;
71
- for (const segment of segments) {
72
- if (current === null || current === undefined || typeof current !== "object") {
73
- return NOT_FOUND;
74
- }
75
- if (typeof segment === "number") {
76
- if (!Array.isArray(current) || segment < 0 || segment >= current.length) {
77
- return NOT_FOUND;
78
- }
79
- current = current[segment];
80
- } else {
81
- // Use hasOwnProperty, NOT the `in` operator: `in` walks the prototype
82
- // chain, so inherited Object.prototype members (`constructor`, `toString`,
83
- // `hasOwnProperty`, `__proto__`, …) would falsely "resolve" and print
84
- // garbage instead of NOT_FOUND.
85
- if (!Object.prototype.hasOwnProperty.call(current, segment)) {
86
- return NOT_FOUND;
87
- }
88
- current = (current as Record<string, unknown>)[segment];
89
- }
90
- }
91
- return current;
92
- }
93
-
94
- /** True for JSON scalar values (string, finite/any number, boolean, null). */
95
- export function isScalar(value: unknown): value is string | number | boolean | null {
96
- return (
97
- value === null ||
98
- typeof value === "string" ||
99
- typeof value === "number" ||
100
- typeof value === "boolean"
101
- );
102
- }
103
-
104
- /**
105
- * Colorizes an already-serialized pretty-JSON string using node:util styleText:
106
- * object keys cyan, string values green, numbers yellow, booleans/null magenta.
107
- * Operates purely on JSON token syntax, so it never mangles structure.
108
- *
109
- * Callers should only invoke this when writing to a TTY; piped output must stay
110
- * plain so it parses as JSON.
111
- */
112
- export function colorizeJson(json: string): string {
113
- const tokenRe = /"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:true|false|null)\b/g;
114
- return json.replace(tokenRe, (match, offset: number, whole: string) => {
115
- if (match[0] === '"') {
116
- const rest = whole.slice(offset + match.length);
117
- // A string immediately followed by a colon is an object key.
118
- return /^\s*:/.test(rest)
119
- ? styleText("cyan", match)
120
- : styleText("green", match);
121
- }
122
- if (match === "true" || match === "false" || match === "null") {
123
- return styleText("magenta", match);
124
- }
125
- return styleText("yellow", match);
126
- });
127
- }
128
-
129
- /**
130
- * Renders a value as pretty JSON (2-space), colorized when `useColor` is true.
131
- */
132
- export function renderJson(value: unknown, useColor: boolean): string {
133
- const json = JSON.stringify(value, null, 2);
134
- return useColor ? colorizeJson(json) : json;
135
- }
136
-
137
- /**
138
- * JSON-encodes a value on a single line and truncates it to `max` characters
139
- * (with an ellipsis) for the compact `--layers` provenance lines.
140
- */
141
- export function truncateJson(value: unknown, max = 80): string {
142
- const encoded = JSON.stringify(value);
143
- if (encoded === undefined) return "undefined";
144
- return encoded.length > max ? `${encoded.slice(0, max - 1)}…` : encoded;
145
- }