@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,282 +0,0 @@
1
- /**
2
- * The resolution ladder: how sous decides whether a variable already has an
3
- * answer, and which environment variable supplied it.
4
- *
5
- * Five rungs, most specific first:
6
- *
7
- * 1. mapping a record binding an arbitrary name to this exact variable
8
- * 2. recipe SOUS_VAR_<NAMESPACE>_<RECIPE>_<VARIABLE>
9
- * 3. namespace SOUS_VAR_<NAMESPACE>_<VARIABLE>
10
- * 4. shared SOUS_VAR_<VARIABLE>
11
- * 5. bare the definition's own `env` name, or the shared form
12
- *
13
- * Within a rung the real shell environment wins, then `.sous/.env.local`, then
14
- * `.sous/.env`; the same order the env file loader uses, so what the ladder
15
- * reports is what a build actually sees.
16
- *
17
- * The three sources are read separately rather than off `process.env`, because
18
- * by the time a command runs the env files have already been injected into the
19
- * process environment and the distinction would be lost.
20
- */
21
-
22
- import path from "node:path";
23
- import { ENV_DEFAULTS_NAME, ENV_LOCAL_NAME } from "../config-discovery.js";
24
- import { readEnvFileMap } from "../env-local.js";
25
- import type { Settings } from "../settings.js";
26
- import type { DefinedVariable } from "./definition-source.js";
27
- import { mappedNamesFor } from "./mappings.js";
28
- import {
29
- bareName,
30
- namespaceScopedName,
31
- recipeScopedName,
32
- sharedName,
33
- } from "./names.js";
34
-
35
- /** The rungs of the ladder, most specific first. */
36
- export const LADDER_RUNGS = ["mapping", "recipe", "namespace", "shared", "bare"] as const;
37
-
38
- /** One rung of the ladder. */
39
- export type LadderRung = (typeof LADDER_RUNGS)[number];
40
-
41
- /** Plain-language names for each rung, used in output. */
42
- export const RUNG_LABELS: Record<LadderRung, string> = {
43
- mapping: "mapping record",
44
- recipe: "recipe scope",
45
- namespace: "namespace scope",
46
- shared: "shared scope",
47
- bare: "declared name",
48
- };
49
-
50
- /** Where a value was found: the real environment, or one of the two env files. */
51
- export type EnvSourceFile = "shell" | typeof ENV_LOCAL_NAME | typeof ENV_DEFAULTS_NAME;
52
-
53
- /** Plain-language names for each place a value can come from. */
54
- export const SOURCE_LABELS: Record<string, string> = {
55
- shell: "the shell environment",
56
- [ENV_LOCAL_NAME]: `the ${ENV_LOCAL_NAME} file`,
57
- [ENV_DEFAULTS_NAME]: `the ${ENV_DEFAULTS_NAME} file`,
58
- };
59
-
60
- /** One environment variable name the ladder will look up, and why. */
61
- export interface LadderCandidate {
62
- /** Which rung generated (or recorded) the name. */
63
- rung: LadderRung;
64
- /** The environment variable name. */
65
- envName: string;
66
- }
67
-
68
- /** Where a resolved value came from. */
69
- export interface VariableSource extends LadderCandidate {
70
- /** Which of the three layers held the value. */
71
- file: EnvSourceFile;
72
- }
73
-
74
- /** A resolved answer: the value, and exactly where it came from. */
75
- export interface ResolvedVariable {
76
- /** The value as stored, before validation or coercion. */
77
- value: string;
78
- /** Which name, on which rung, in which layer, supplied it. */
79
- source: VariableSource;
80
- }
81
-
82
- /**
83
- * The three separately parsed environment layers plus the mapping records, all
84
- * the ladder needs to answer a lookup.
85
- */
86
- export interface LadderContext {
87
- /** The real shell environment, captured before the env files were injected. */
88
- shellEnv: Record<string, string>;
89
- /** The parsed `.sous/.env.local` file. */
90
- localEnv: Record<string, string>;
91
- /** The parsed `.sous/.env` file. */
92
- sharedEnv: Record<string, string>;
93
- /** The merged `varMappings` block: environment variable name to target. */
94
- mappings: Record<string, string>;
95
- }
96
-
97
- /** Drops undefined entries from a process environment, keeping the strings. */
98
- function sanitizeEnv(env: NodeJS.ProcessEnv): Record<string, string> {
99
- const out: Record<string, string> = {};
100
- for (const [key, value] of Object.entries(env)) {
101
- if (typeof value === "string") out[key] = value;
102
- }
103
- return out;
104
- }
105
-
106
- /** What `loadLadderContext` needs in order to read the layers. */
107
- export interface LadderContextOptions {
108
- /** The project's `.sous/` directory, which holds both env files. */
109
- sousDir: string;
110
- /** The merged config, read for its `varMappings` block. */
111
- settings?: Settings;
112
- /**
113
- * The real shell environment. Commands pass the snapshot BaseCommand takes
114
- * before it injects the env files; without it the file-supplied values would
115
- * be reported as though the shell had set them.
116
- */
117
- shellEnv?: NodeJS.ProcessEnv;
118
- }
119
-
120
- /**
121
- * Reads the two env files and the mapping records into a ladder context.
122
- *
123
- * @param options - Where to read from; see LadderContextOptions.
124
- */
125
- export function loadLadderContext(options: LadderContextOptions): LadderContext {
126
- const { sousDir, settings, shellEnv = process.env } = options;
127
- return {
128
- shellEnv: sanitizeEnv(shellEnv),
129
- localEnv: readEnvFileMap(path.join(sousDir, ENV_LOCAL_NAME)),
130
- sharedEnv: readEnvFileMap(path.join(sousDir, ENV_DEFAULTS_NAME)),
131
- mappings: settings?.varMappings ?? {},
132
- };
133
- }
134
-
135
- /**
136
- * Every environment variable name that could answer this variable, most
137
- * specific rung first. Duplicate names are dropped, keeping the most specific
138
- * occurrence, so a definition whose `env` field repeats a generated name is
139
- * reported once.
140
- *
141
- * @param defined - The definition and the recipe that published it.
142
- * @param context - The environment layers and mapping records.
143
- */
144
- export function variableCandidates(
145
- defined: DefinedVariable,
146
- context: LadderContext
147
- ): LadderCandidate[] {
148
- const { namespace, name: recipe } = defined.recipe;
149
- const variable = defined.definition.name;
150
-
151
- const ordered: LadderCandidate[] = [
152
- ...mappedNamesFor(context.mappings, defined).map((envName) => ({
153
- rung: "mapping" as const,
154
- envName,
155
- })),
156
- { rung: "recipe", envName: recipeScopedName(namespace, recipe, variable) },
157
- { rung: "namespace", envName: namespaceScopedName(namespace, variable) },
158
- { rung: "shared", envName: sharedName(variable) },
159
- { rung: "bare", envName: bareName(defined.definition) },
160
- ];
161
-
162
- const seen = new Set<string>();
163
- return ordered.filter((candidate) => {
164
- if (seen.has(candidate.envName)) return false;
165
- seen.add(candidate.envName);
166
- return true;
167
- });
168
- }
169
-
170
- /**
171
- * Looks one environment variable name up across the three layers, in
172
- * precedence order.
173
- *
174
- * @param envName - The name to look up.
175
- * @param context - The environment layers.
176
- * @returns The value and the layer that held it, or undefined when nothing did.
177
- */
178
- export function lookupEnvName(
179
- envName: string,
180
- context: LadderContext
181
- ): { value: string; file: EnvSourceFile } | undefined {
182
- const shell = context.shellEnv[envName];
183
- if (shell !== undefined) return { value: shell, file: "shell" };
184
-
185
- const local = context.localEnv[envName];
186
- if (local !== undefined) return { value: local, file: ENV_LOCAL_NAME };
187
-
188
- const shared = context.sharedEnv[envName];
189
- if (shared !== undefined) return { value: shared, file: ENV_DEFAULTS_NAME };
190
-
191
- return undefined;
192
- }
193
-
194
- /**
195
- * Walks the ladder for one variable and returns the first answer it finds.
196
- *
197
- * @param defined - The definition and the recipe that published it.
198
- * @param context - The environment layers and mapping records.
199
- * @returns The value and its source, or undefined when no rung answered.
200
- */
201
- export function resolveVariable(
202
- defined: DefinedVariable,
203
- context: LadderContext
204
- ): ResolvedVariable | undefined {
205
- for (const candidate of variableCandidates(defined, context)) {
206
- const hit = lookupEnvName(candidate.envName, context);
207
- if (hit !== undefined) {
208
- return {
209
- value: hit.value,
210
- source: { rung: candidate.rung, envName: candidate.envName, file: hit.file },
211
- };
212
- }
213
- }
214
- return undefined;
215
- }
216
-
217
- /** A resolution plus the full candidate list, for diagnostics and `sous vars`. */
218
- export interface VariableDiagnosis {
219
- /** The winning answer, when a rung produced one. */
220
- resolved?: ResolvedVariable;
221
- /** Every name the ladder tried, most specific first. */
222
- candidates: LadderCandidate[];
223
- }
224
-
225
- /**
226
- * Resolves a variable and reports every candidate name it considered, which is
227
- * what `sous vars <name>` prints and what a non-interactive failure message
228
- * lists.
229
- *
230
- * @param defined - The definition and the recipe that published it.
231
- * @param context - The environment layers and mapping records.
232
- */
233
- export function diagnoseVariable(
234
- defined: DefinedVariable,
235
- context: LadderContext
236
- ): VariableDiagnosis {
237
- const candidates = variableCandidates(defined, context);
238
- for (const candidate of candidates) {
239
- const hit = lookupEnvName(candidate.envName, context);
240
- if (hit !== undefined) {
241
- return {
242
- resolved: {
243
- value: hit.value,
244
- source: { rung: candidate.rung, envName: candidate.envName, file: hit.file },
245
- },
246
- candidates,
247
- };
248
- }
249
- }
250
- return { candidates };
251
- }
252
-
253
- /**
254
- * Records a value in the context so later lookups in the same run see it, as
255
- * they would on the next run once the file is on disk.
256
- *
257
- * @param context - The context to update.
258
- * @param file - Which layer the value was written to.
259
- * @param envName - The environment variable name.
260
- * @param value - The value that was stored.
261
- */
262
- export function recordAnswerInContext(
263
- context: LadderContext,
264
- file: EnvSourceFile,
265
- envName: string,
266
- value: string
267
- ): void {
268
- if (file === "shell") context.shellEnv[envName] = value;
269
- else if (file === ENV_LOCAL_NAME) context.localEnv[envName] = value;
270
- else context.sharedEnv[envName] = value;
271
- }
272
-
273
- /**
274
- * A one-line, plain-language description of where a value came from, such as
275
- * "the shared scope name SOUS_VAR_API_URL, from the .env file".
276
- *
277
- * @param source - The resolved source.
278
- */
279
- export function describeSource(source: VariableSource): string {
280
- const where = SOURCE_LABELS[source.file] ?? source.file;
281
- return `the ${RUNG_LABELS[source.rung]} name ${source.envName}, from ${where}`;
282
- }
@@ -1,260 +0,0 @@
1
- /**
2
- * Mapping records: the universal conflict resolver for variable answers.
3
- *
4
- * A mapping record binds one environment variable, of any name at all, to one
5
- * fully qualified variable:
6
- *
7
- * SOME_VAR -> sous-recipes:misc/stuff/apiUrl
8
- *
9
- * It is the top rung of the resolution ladder, and it exists because generated
10
- * names can collide: two recipes may both declare `apiUrl`, or an author may
11
- * bind an existing name such as `GITHUB_TOKEN` that something else already
12
- * uses. Rather than inventing a name grammar that sous would have to parse back
13
- * into scopes, a record simply states the binding.
14
- *
15
- * Records live under the top-level `varMappings` config key. sous writes the
16
- * ones it creates into the machine-written `conf.d/520-var-mappings.jsonc`
17
- * layer; a user may also hand-write `varMappings` in the primary config, and
18
- * the layers merge like anything else.
19
- */
20
-
21
- import fs from "node:fs";
22
- import path from "node:path";
23
- import { parse as parseJsonc, type ParseError } from "jsonc-parser";
24
- import { ConfigError } from "../errors.js";
25
- import {
26
- managedLayerHeader,
27
- updateManagedLayer,
28
- } from "../repos/managed-layer.js";
29
- import { VARIABLE_NAME_PATTERN } from "../repos/formats/patterns.js";
30
- import {
31
- RefSource,
32
- formatRef,
33
- namespaceOf,
34
- parseNamedRef,
35
- repoOf,
36
- type NamedRef,
37
- } from "../../services/ref-resolver/index.js";
38
- import type { DefinedVariable } from "./definition-source.js";
39
-
40
- /** File name of the machine-written mapping record layer. */
41
- export const VAR_MAPPINGS_LAYER_FILENAME = "520-var-mappings.jsonc";
42
-
43
- /** What the mapping record layer holds, written into its header comment. */
44
- export const VAR_MAPPINGS_DESCRIPTION = [
45
- "Each entry under 'varMappings' binds an environment variable to one recipe",
46
- "variable, so an answer can be stored under a name of your choosing when the",
47
- "usual names are taken. The 'sous vars ask' command writes them; run",
48
- "'sous vars' to see which name answered what.",
49
- ];
50
-
51
- /** The header comment sous writes at the top of the mapping record layer. */
52
- export const VAR_MAPPINGS_COMMENT = managedLayerHeader(
53
- VAR_MAPPINGS_LAYER_FILENAME,
54
- VAR_MAPPINGS_DESCRIPTION
55
- );
56
-
57
- /** A mapping record's target: one variable, named in full. */
58
- export interface MappingTarget {
59
- /** The repository's short name, when the record names one. */
60
- repo?: string;
61
- /** The recipe's namespace. */
62
- namespace: string;
63
- /** The recipe's name. */
64
- recipe: string;
65
- /** The variable's camelCase name. */
66
- variable: string;
67
- }
68
-
69
- /** The one-line reminder appended to every mapping error. */
70
- const TARGET_HELP =
71
- "A mapping target is written as 'namespace/recipe/variableName', optionally " +
72
- "qualified with a repository as 'repo:namespace/recipe/variableName'.";
73
-
74
- /**
75
- * Parses a mapping target string into its parts, throwing a ConfigError that
76
- * quotes the input and shows the grammar when it does not fit.
77
- *
78
- * @param input - The target as written in the config.
79
- */
80
- export function parseMappingTarget(input: string): MappingTarget {
81
- const fail = (problem: string): never => {
82
- throw new ConfigError(
83
- `Invalid variable mapping target '${input}': ${problem}\n ${TARGET_HELP}`
84
- );
85
- };
86
-
87
- const trimmed = typeof input === "string" ? input.trim() : "";
88
- if (trimmed.length === 0) fail("a target must not be empty.");
89
-
90
- // A target is a recipe ref with the variable after one more slash, so the
91
- // ref part is read by the one ref parser and held to the stored form.
92
- const slash = trimmed.lastIndexOf("/");
93
- const variable = slash === -1 ? "" : trimmed.slice(slash + 1);
94
- let ref: NamedRef | undefined;
95
- try {
96
- ref = parseNamedRef(trimmed.slice(0, Math.max(slash, 0)), RefSource.CommandLine);
97
- } catch (error) {
98
- fail((error as Error).message.split("\n")[0]!.replace(/^Invalid ref '[^']*': /, ""));
99
- }
100
- if (ref?.kind !== "recipe" || ref.range !== undefined) {
101
- fail("a target names a namespace, a recipe and a variable, joined by slashes.");
102
- }
103
- const repo = repoOf(ref!)?.name;
104
- const namespace = namespaceOf(ref!)!.name;
105
- const recipe = ref!.kind === "recipe" ? ref!.name : "";
106
- if (formatRef(ref!) !== formatRef(ref!).toLowerCase()) {
107
- fail(`the repository, namespace and recipe in '${formatRef(ref!)}' must be lowercase.`);
108
- }
109
- if (!VARIABLE_NAME_PATTERN.test(variable)) {
110
- fail(
111
- `the variable name '${variable}' must be camelCase: a lowercase letter, then ` +
112
- "letters or digits."
113
- );
114
- }
115
-
116
- const parsed: MappingTarget = { namespace, recipe, variable };
117
- if (repo !== undefined) parsed.repo = repo;
118
- return parsed;
119
- }
120
-
121
- /**
122
- * Renders a mapping target back into its written form, which round-trips with
123
- * `parseMappingTarget`.
124
- *
125
- * @param target - The target parts.
126
- */
127
- export function formatMappingTarget(target: MappingTarget): string {
128
- const qualifier = target.repo === undefined ? "" : `${target.repo}:`;
129
- return `${qualifier}${target.namespace}/${target.recipe}/${target.variable}`;
130
- }
131
-
132
- /**
133
- * The fully qualified target for one defined variable, repository qualifier
134
- * included, which is what a new record is written with.
135
- *
136
- * @param defined - The definition and the recipe that published it.
137
- */
138
- export function mappingTargetFor(defined: DefinedVariable): MappingTarget {
139
- return {
140
- repo: defined.recipe.repo,
141
- namespace: defined.recipe.namespace,
142
- recipe: defined.recipe.name,
143
- variable: defined.definition.name,
144
- };
145
- }
146
-
147
- /**
148
- * True when a mapping record's target names this variable. A record without a
149
- * repository qualifier matches the variable in any repository; one with a
150
- * qualifier must name the same repository.
151
- *
152
- * @param target - The record's parsed target.
153
- * @param defined - The definition and the recipe that published it.
154
- */
155
- export function mappingMatches(target: MappingTarget, defined: DefinedVariable): boolean {
156
- if (target.repo !== undefined && target.repo !== defined.recipe.repo) return false;
157
- return (
158
- target.namespace === defined.recipe.namespace &&
159
- target.recipe === defined.recipe.name &&
160
- target.variable === defined.definition.name
161
- );
162
- }
163
-
164
- /**
165
- * Every environment variable name bound to this variable by a mapping record,
166
- * sorted so the order never depends on config layer order.
167
- *
168
- * @param mappings - The merged `varMappings` block.
169
- * @param defined - The definition and the recipe that published it.
170
- */
171
- export function mappedNamesFor(
172
- mappings: Record<string, string>,
173
- defined: DefinedVariable
174
- ): string[] {
175
- const names: string[] = [];
176
- for (const [envName, rawTarget] of Object.entries(mappings)) {
177
- if (mappingMatches(parseMappingTarget(rawTarget), defined)) names.push(envName);
178
- }
179
- return names.sort();
180
- }
181
-
182
- /**
183
- * Records a mapping in the machine-written `conf.d/520-var-mappings.jsonc`
184
- * layer, editing only that one entry's bytes so any comments, key order and
185
- * formatting already in the file survive.
186
- *
187
- * The edit stages to a temporary name and renames over the layer, like every
188
- * other file sous writes for a machine. A layer truncated by an interrupt would
189
- * fail to parse and break every later command until someone deleted it by hand;
190
- * a rename either happens or does not, so the previous layer survives.
191
- *
192
- * @param confDir - The project's `conf.d/` directory (`configContext.confDir`).
193
- * @param envName - The environment variable the answer is stored under.
194
- * @param target - The variable the name is bound to.
195
- * @returns The path of the layer file that was written.
196
- */
197
- export function writeMappingRecord(
198
- confDir: string,
199
- envName: string,
200
- target: MappingTarget | string
201
- ): string {
202
- const rendered = typeof target === "string" ? target : formatMappingTarget(target);
203
- // Parse before writing, so a bad target is refused rather than persisted.
204
- parseMappingTarget(rendered);
205
-
206
- return updateManagedLayer(
207
- path.dirname(confDir),
208
- VAR_MAPPINGS_LAYER_FILENAME,
209
- [{ path: ["varMappings", envName], value: rendered }],
210
- { confDir, header: VAR_MAPPINGS_COMMENT }
211
- );
212
- }
213
-
214
- /**
215
- * Reads the records already in the mapping layer file, returning an empty
216
- * object when the file is missing. A layer still under the old
217
- * `520-var-mappings.json` name is read as a fallback; the next write migrates
218
- * it.
219
- *
220
- * @param filePath - Path to the mapping record layer file.
221
- */
222
- export function readMappingRecords(filePath: string): Record<string, string> {
223
- let readPath = filePath;
224
- if (!fs.existsSync(readPath)) {
225
- const legacy = filePath.endsWith(".jsonc") ? filePath.slice(0, -1) : undefined;
226
- if (legacy === undefined || !fs.existsSync(legacy)) return {};
227
- readPath = legacy;
228
- }
229
-
230
- const errors: ParseError[] = [];
231
- const parsed = parseJsonc(fs.readFileSync(readPath, "utf8"), errors, {
232
- allowTrailingComma: true,
233
- disallowComments: false,
234
- });
235
- if (errors.length > 0) {
236
- throw new ConfigError(
237
- `Could not read the variable mapping records at ${readPath}:\n` +
238
- ` The file is not valid JSON with comments.\n` +
239
- ` This file is written by sous; deleting it removes every mapping record.`
240
- );
241
- }
242
-
243
- const block =
244
- parsed !== null && typeof parsed === "object"
245
- ? (parsed as { varMappings?: unknown }).varMappings
246
- : undefined;
247
- if (block === undefined) return {};
248
- if (block === null || typeof block !== "object" || Array.isArray(block)) {
249
- throw new ConfigError(
250
- `The variable mapping records at ${readPath} are malformed: 'varMappings' must ` +
251
- `be an object of environment variable names to targets.`
252
- );
253
- }
254
-
255
- const records: Record<string, string> = {};
256
- for (const [key, value] of Object.entries(block as Record<string, unknown>)) {
257
- if (typeof value === "string") records[key] = value;
258
- }
259
- return records;
260
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * Environment variable names for recipe variable answers.
3
- *
4
- * Every name here is GENERATED and then LOOKED UP. Nothing in sous ever parses
5
- * a name back into the scope that produced it: `_` is both the delimiter and a
6
- * legal identifier character, so `SOUS_VAR_MISC_STUFF_API_URL` could be split
7
- * in several places and no parse would be trustworthy. When a generated name
8
- * collides with something else, a mapping record (see `mappings.ts`) binds an
9
- * arbitrary name to one fully qualified variable instead.
10
- */
11
-
12
- import type { VariableDefinition } from "../repos/formats/recipe-manifest.js";
13
-
14
- /** The prefix every generated answer name carries. */
15
- export const ENV_PREFIX = "SOUS_VAR_";
16
-
17
- /**
18
- * Converts a camelCase or kebab-case identifier to upper snake case.
19
- *
20
- * @param name - The identifier, such as `apiBaseUrl` or `task-files`.
21
- * @returns The upper snake case form, such as `API_BASE_URL` or `TASK_FILES`.
22
- */
23
- export function toUpperSnake(name: string): string {
24
- return name
25
- .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
26
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
27
- .replace(/[-\s.]+/g, "_")
28
- .replace(/_+/g, "_")
29
- .replace(/^_|_$/g, "")
30
- .toUpperCase();
31
- }
32
-
33
- /**
34
- * The default environment variable name for a variable, derived from its
35
- * camelCase name: `apiUrl` becomes `SOUS_VAR_API_URL`. This is the same string
36
- * as the shared rung of the resolution ladder.
37
- *
38
- * @param variableName - The variable's camelCase name.
39
- */
40
- export function deriveEnvName(variableName: string): string {
41
- return `${ENV_PREFIX}${toUpperSnake(variableName)}`;
42
- }
43
-
44
- /**
45
- * The recipe-scoped name: the most specific generated rung, naming both the
46
- * namespace and the recipe. `misc` plus `stuff` plus `apiUrl` becomes
47
- * `SOUS_VAR_MISC_STUFF_API_URL`.
48
- *
49
- * @param namespace - The recipe's namespace.
50
- * @param recipe - The recipe's name.
51
- * @param variableName - The variable's camelCase name.
52
- */
53
- export function recipeScopedName(
54
- namespace: string,
55
- recipe: string,
56
- variableName: string
57
- ): string {
58
- return `${ENV_PREFIX}${toUpperSnake(namespace)}_${toUpperSnake(recipe)}_${toUpperSnake(
59
- variableName
60
- )}`;
61
- }
62
-
63
- /**
64
- * The namespace-scoped name, which answers the same variable for every recipe
65
- * in one namespace. `misc` plus `apiUrl` becomes `SOUS_VAR_MISC_API_URL`.
66
- *
67
- * @param namespace - The recipe's namespace.
68
- * @param variableName - The variable's camelCase name.
69
- */
70
- export function namespaceScopedName(namespace: string, variableName: string): string {
71
- return `${ENV_PREFIX}${toUpperSnake(namespace)}_${toUpperSnake(variableName)}`;
72
- }
73
-
74
- /**
75
- * The shared name, which answers a variable of this name for every recipe that
76
- * declares one. `apiUrl` becomes `SOUS_VAR_API_URL`.
77
- *
78
- * @param variableName - The variable's camelCase name.
79
- */
80
- export function sharedName(variableName: string): string {
81
- return deriveEnvName(variableName);
82
- }
83
-
84
- /**
85
- * The bare declared name: the definition's own `env` field when its author set
86
- * one (so a recipe can bind an existing variable such as `GITHUB_TOKEN`), and
87
- * the shared form otherwise. This is also the name a new answer is written
88
- * under.
89
- *
90
- * @param definition - The variable definition.
91
- */
92
- export function bareName(definition: VariableDefinition): string {
93
- return definition.env ?? sharedName(definition.name);
94
- }