@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,395 +0,0 @@
1
- /**
2
- * Answering a recipe's questions before it asks them.
3
- *
4
- * A person subscribing at a terminal answers each question as it comes. A
5
- * script, a continuous integration job or an agent has no terminal and knows
6
- * every answer already, so it supplies them up front: `--answer name=value`,
7
- * repeated, or a file of the same pairs. Both `sous subscription add` and
8
- * `sous vars ask` take them, and both hand them here, so the rules are written
9
- * once.
10
- *
11
- * The rules are deliberately unforgiving, because a supplied answer is never
12
- * seen by a human before it is stored:
13
- *
14
- * - Every answer is validated against its definition BEFORE anything is
15
- * written, so a run either stores all of them or none of them.
16
- * - A name no recipe declares fails the run and lists every variable the
17
- * closure does declare, so a typo can never become a stored value under a
18
- * name nothing reads.
19
- * - An answer for a variable that already has one replaces it, where it
20
- * already lives, and the report says so.
21
- */
22
-
23
- import path from "node:path";
24
- import { ConfigError } from "../errors.js";
25
- import { updateEnvFile } from "../env-file.js";
26
- import { loadManifestFile } from "../repos/load-manifest.js";
27
- import {
28
- answerFileFor,
29
- answerHeader,
30
- type AnsweredVariable,
31
- type AskOptions,
32
- type StoragePlan,
33
- } from "./ask.js";
34
- import {
35
- definedVariableKey,
36
- definingRecipeKey,
37
- type DefinedVariable,
38
- } from "./definition-source.js";
39
- import { displayValue } from "./display.js";
40
- import {
41
- recordAnswerInContext,
42
- resolveVariable,
43
- type LadderContext,
44
- } from "./ladder.js";
45
- import { bareName } from "./names.js";
46
- import { validateAnswer } from "./validate.js";
47
-
48
- /** How an answer supplied on the command line is written. */
49
- export const ANSWER_FLAG_FORM = "--answer <name>=<value>";
50
-
51
- /** The sentence appended to every error about a supplied answer. */
52
- export const ANSWER_NAME_HELP =
53
- "A name is spelled exactly as the recipe declares it, in camelCase; the full " +
54
- "'namespace/recipe.name' key works too.";
55
-
56
- /** One answer supplied ahead of the questions. */
57
- export interface ProvidedAnswer {
58
- /** The variable's declared name, or its `namespace/recipe.name` key. */
59
- name: string;
60
- /** The answer, exactly as it was written. */
61
- value: string;
62
- /** Where it came from, named in every error: the flag, or the file's path. */
63
- from: string;
64
- }
65
-
66
- /**
67
- * Splits one `name=value` pair. The split is on the FIRST `=` only, so a value
68
- * may contain as many more as it likes (a connection string, a query, a base64
69
- * blob).
70
- *
71
- * @param entry - The pair as written on the command line.
72
- * @param from - Where it came from, named in the error.
73
- *
74
- * @example
75
- * parseAnswerPair("apiUrl=https://x.test/?a=1&b=2");
76
- * // -> { name: "apiUrl", value: "https://x.test/?a=1&b=2", from: "--answer" }
77
- */
78
- export function parseAnswerPair(entry: string, from = ANSWER_FLAG_FORM): ProvidedAnswer {
79
- const at = entry.indexOf("=");
80
- const name = at === -1 ? "" : entry.slice(0, at).trim();
81
-
82
- if (at === -1 || name.length === 0) {
83
- throw new ConfigError(
84
- `'${entry}' is not an answer sous can read.\n` +
85
- ` Write one as '${ANSWER_FLAG_FORM}', for example ` +
86
- `'--answer apiUrl=https://api.example.com'.\n` +
87
- ` Everything after the first '=' is the answer, so a value may contain more of them.`
88
- );
89
- }
90
-
91
- return { name, value: entry.slice(at + 1), from };
92
- }
93
-
94
- /**
95
- * Reads a file of answers: a YAML or JSON map of the same `name: value` pairs.
96
- * It goes through the manifest loader, so the JSON dialect is the permissive
97
- * one and a file can carry comments explaining its answers.
98
- *
99
- * @param filePath - Absolute path to the answers file.
100
- */
101
- export function loadAnswersFile(filePath: string): ProvidedAnswer[] {
102
- const raw = loadManifestFile(filePath);
103
-
104
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
105
- throw new ConfigError(
106
- `The answers file at ${filePath} is not a map of answers.\n` +
107
- ` It holds one entry per variable, written as 'name: value'.`
108
- );
109
- }
110
-
111
- const answers: ProvidedAnswer[] = [];
112
- for (const [name, value] of Object.entries(raw as Record<string, unknown>)) {
113
- if (typeof value === "object" && value !== null) {
114
- throw new ConfigError(
115
- `The answer for '${name}' in ${filePath} is a list or a map, and an answer is a ` +
116
- `single value.\n` +
117
- ` Answers are stored in environment files, which hold text; write the answer ` +
118
- `as a string, a number or a boolean.`
119
- );
120
- }
121
- if (value === null || value === undefined) {
122
- throw new ConfigError(
123
- `The answer for '${name}' in ${filePath} is empty.\n` +
124
- ` Write the value the variable should be given, or leave the entry out ` +
125
- `entirely so sous asks for it.`
126
- );
127
- }
128
- answers.push({ name: name.trim(), value: String(value), from: filePath });
129
- }
130
-
131
- return answers;
132
- }
133
-
134
- /** The two ways a caller supplies answers ahead of the questions. */
135
- export interface ProvidedAnswerInputs {
136
- /** Every `--answer name=value` pair, in the order they were written. */
137
- answer?: string[];
138
- /** A file of `name: value` pairs, absolute or relative to `cwd`. */
139
- answersFile?: string;
140
- /** Where a relative answers file is resolved from. Defaults to the real cwd. */
141
- cwd?: string;
142
- }
143
-
144
- /**
145
- * Collects every supplied answer into one list, with the file read first and
146
- * the flags laid over it, so `--answer` wins over the file for the same name.
147
- * A name given twice in the same place keeps the last one written, which is how
148
- * every other repeated flag behaves.
149
- *
150
- * @param inputs - The flag values, and where a relative file path resolves from.
151
- */
152
- export function collectProvidedAnswers(inputs: ProvidedAnswerInputs): ProvidedAnswer[] {
153
- const collected = new Map<string, ProvidedAnswer>();
154
-
155
- if (inputs.answersFile !== undefined) {
156
- const filePath = path.resolve(inputs.cwd ?? process.cwd(), inputs.answersFile);
157
- for (const answer of loadAnswersFile(filePath)) collected.set(answer.name, answer);
158
- }
159
-
160
- for (const entry of inputs.answer ?? []) {
161
- const answer = parseAnswerPair(entry);
162
- collected.set(answer.name, answer);
163
- }
164
-
165
- return [...collected.values()];
166
- }
167
-
168
- /** True when a supplied name names this variable, by declared name or by key. */
169
- function matchesName(defined: DefinedVariable, name: string): boolean {
170
- return defined.definition.name === name || definedVariableKey(defined) === name;
171
- }
172
-
173
- /**
174
- * The error a name nothing declares fails with: every variable the closure DOES
175
- * declare, grouped by the recipe that published it, so the caller can see the
176
- * spelling they meant rather than guessing at it again.
177
- *
178
- * @param answer - The supplied answer whose name matched nothing.
179
- * @param defined - Every variable definition in play.
180
- */
181
- export function unknownAnswerError(
182
- answer: ProvidedAnswer,
183
- defined: DefinedVariable[]
184
- ): ConfigError {
185
- const lines = [
186
- `Nothing in this project defines a variable called '${answer.name}', so the answer ` +
187
- `given with ${answer.from} cannot be stored.`,
188
- ];
189
-
190
- if (defined.length === 0) {
191
- lines.push(
192
- " No recipe this project subscribes to defines any variables yet, so there is " +
193
- "nothing to answer."
194
- );
195
- return new ConfigError(lines.join("\n"));
196
- }
197
-
198
- const groups = new Map<string, string[]>();
199
- for (const entry of defined) {
200
- const key = definingRecipeKey(entry.recipe);
201
- const names = groups.get(key) ?? [];
202
- if (!names.includes(entry.definition.name)) names.push(entry.definition.name);
203
- groups.set(key, names);
204
- }
205
-
206
- lines.push("", " These are the variables in play, and the recipes that declare them:", "");
207
- for (const [key, names] of groups) {
208
- lines.push(` ${key}`);
209
- for (const name of names) lines.push(` ${name}`);
210
- }
211
- lines.push("", ` ${ANSWER_NAME_HELP}`);
212
-
213
- return new ConfigError(lines.join("\n"));
214
- }
215
-
216
- /**
217
- * The error a supplied answer that does not fit its definition fails with: the
218
- * variable, the constraint it violated, and the publisher's own example of a
219
- * real answer. A secret's value is never echoed back.
220
- *
221
- * @param answer - The supplied answer.
222
- * @param defined - The definition it was checked against.
223
- * @param message - The plain-language reason from `validateAnswer`.
224
- */
225
- export function invalidAnswerError(
226
- answer: ProvidedAnswer,
227
- defined: DefinedVariable,
228
- message: string
229
- ): ConfigError {
230
- const { definition } = defined;
231
- return new ConfigError(
232
- `The answer given for '${answer.name}' does not fit the definition ` +
233
- `${definingRecipeKey(defined.recipe)} publishes.\n` +
234
- ` ${message}\n` +
235
- ` For example: ${String(definition.example)}\n` +
236
- ` The answer given with ${answer.from} was: ` +
237
- `${displayValue(answer.value, definition.secret)}\n` +
238
- ` Nothing was written; fix the answer and run the command again.`
239
- );
240
- }
241
-
242
- /** What one run of `applyProvidedAnswers` did. */
243
- export interface AppliedAnswers {
244
- /** Every answer that was stored, in the shape the ask report prints. */
245
- stored: AnsweredVariable[];
246
- /**
247
- * The keys of every variable these answers settled. `askForMissing` skips
248
- * them, so a supplied answer is never asked about as well as stored.
249
- */
250
- keys: string[];
251
- }
252
-
253
- /**
254
- * Where a supplied answer goes. A variable with no answer yet is stored exactly
255
- * where the interactive flow would store it: under its declared name, in the
256
- * file its scope asks for. A variable that ALREADY has an answer in one of the
257
- * project's env files is overwritten where that answer lives, because storing
258
- * the new value somewhere less specific would leave the old one winning and the
259
- * caller asking why nothing changed.
260
- *
261
- * @param defined - The variable being answered.
262
- * @param context - The environment layers, as they stand.
263
- */
264
- export function preAnswerPlan(
265
- defined: DefinedVariable,
266
- context: LadderContext
267
- ): { plan: StoragePlan; replaced?: string; shadowedBy?: string } {
268
- const fallback: StoragePlan = {
269
- file: answerFileFor(defined.definition),
270
- envName: bareName(defined.definition),
271
- };
272
- const existing = resolveVariable(defined, context);
273
-
274
- if (existing === undefined) return { plan: fallback };
275
-
276
- // A value the shell environment supplies is not sous's to rewrite, and it
277
- // outranks both env files for as long as it is set; the answer is stored
278
- // where it belongs and the report says what is covering it.
279
- if (existing.source.file === "shell") {
280
- return { plan: fallback, replaced: existing.value, shadowedBy: existing.source.envName };
281
- }
282
-
283
- return {
284
- plan: { file: existing.source.file, envName: existing.source.envName },
285
- replaced: existing.value,
286
- };
287
- }
288
-
289
- /** One supplied answer, matched to a definition and coerced to its stored form. */
290
- export interface ApplicableAnswer {
291
- /** The answer as it was supplied. */
292
- answer: ProvidedAnswer;
293
- /** The definition it answers. */
294
- target: DefinedVariable;
295
- /** The validated answer, in the form an env file holds. */
296
- value: string;
297
- }
298
-
299
- /**
300
- * Matches every supplied answer to the definitions it answers and checks it
301
- * against each of them, without writing anything.
302
- *
303
- * A command calls this early, before it installs or writes anything at all, so
304
- * a bad answer fails the run cleanly rather than halfway through it. One name
305
- * can answer more than one definition, because two recipes may declare the same
306
- * variable; every one of them has to accept the value.
307
- *
308
- * @param defined - Every variable definition in play.
309
- * @param provided - The answers supplied ahead of the questions.
310
- * @returns One entry per definition each answer applies to.
311
- */
312
- export function validateProvidedAnswers(
313
- defined: DefinedVariable[],
314
- provided: ProvidedAnswer[]
315
- ): ApplicableAnswer[] {
316
- const applicable: ApplicableAnswer[] = [];
317
-
318
- for (const answer of provided) {
319
- const targets = defined.filter((entry) => matchesName(entry, answer.name));
320
- if (targets.length === 0) throw unknownAnswerError(answer, defined);
321
-
322
- for (const target of targets) {
323
- const validated = validateAnswer(target.definition, answer.value, {
324
- recipe: definingRecipeKey(target.recipe),
325
- });
326
- if (!validated.ok) throw invalidAnswerError(answer, target, validated.message);
327
- applicable.push({ answer, target, value: String(validated.value) });
328
- }
329
- }
330
-
331
- return applicable;
332
- }
333
-
334
- /**
335
- * Validates every supplied answer, then stores them all.
336
- *
337
- * Validation happens for all of them first, so a run with one bad answer in it
338
- * writes nothing at all rather than half of what it was given. A dry run
339
- * validates and reports without writing, but still records the answers in the
340
- * context it was handed, so the rest of the run plans as though they were
341
- * stored.
342
- *
343
- * @param defined - Every variable definition in play.
344
- * @param provided - The answers supplied ahead of the questions.
345
- * @param context - The environment layers, updated as answers are stored.
346
- * @param options - Where to write, and whether this is a dry run.
347
- * @returns What was stored, and which variables no longer need asking.
348
- */
349
- export function applyProvidedAnswers(
350
- defined: DefinedVariable[],
351
- provided: ProvidedAnswer[],
352
- context: LadderContext,
353
- options: AskOptions
354
- ): AppliedAnswers {
355
- const applicable = validateProvidedAnswers(defined, provided);
356
-
357
- const stored: AnsweredVariable[] = [];
358
- const keys: string[] = [];
359
- /** Env entries this run has already written, so one file line is written once. */
360
- const written = new Set<string>();
361
-
362
- for (const entry of applicable) {
363
- keys.push(definedVariableKey(entry.target));
364
-
365
- const { plan, replaced, shadowedBy } = preAnswerPlan(entry.target, context);
366
- const filePath = path.join(options.sousDir, plan.file);
367
- const slot = `${plan.file}:${plan.envName}`;
368
- if (written.has(slot)) continue;
369
- written.add(slot);
370
-
371
- const outcome =
372
- options.dryRun === true
373
- ? ("not written" as const)
374
- : updateEnvFile(filePath, plan.envName, entry.value, {
375
- header: answerHeader(entry.target),
376
- });
377
-
378
- // Recorded even on a dry run: everything after this point should plan as
379
- // though the answer were already stored.
380
- recordAnswerInContext(context, plan.file, plan.envName, entry.value);
381
-
382
- stored.push({
383
- defined: entry.target,
384
- envName: plan.envName,
385
- file: plan.file,
386
- filePath,
387
- value: entry.value,
388
- outcome,
389
- ...(replaced === undefined || replaced === entry.value ? {} : { replaced }),
390
- ...(shadowedBy === undefined ? {} : { shadowedBy }),
391
- });
392
- }
393
-
394
- return { stored, keys };
395
- }
@@ -1,241 +0,0 @@
1
- /**
2
- * The questions a subscription is going to ask, written out before it asks any
3
- * of them.
4
- *
5
- * A dry run is how a caller with no terminal finds out what a subscription
6
- * wants: every variable the closure declares, what each one is for, where its
7
- * answer will be stored, and whether something already answers it. With that in
8
- * hand the whole subscription can be done in one more command, every answer
9
- * supplied with `--answer`.
10
- *
11
- * The facts are laid out by the renderer `sous vars show` uses, so the labels
12
- * and the vocabulary are the same wherever a variable is described.
13
- */
14
-
15
- import path from "node:path";
16
- import { color } from "@oclif/color";
17
- import { palette, wrapColumns, wrapText } from "../../utils/formatting.js";
18
- import { answerFileFor, type AnswerFile } from "./ask.js";
19
- import { definingRecipeKey, type DefinedVariable } from "./definition-source.js";
20
- import { renderFacts, type LabeledFact } from "./display.js";
21
- import { describeSource, resolveVariable, type LadderContext } from "./ladder.js";
22
- import { bareName } from "./names.js";
23
- import { validateAnswer } from "./validate.js";
24
-
25
- /** One question the closure would ask, and what would happen to its answer. */
26
- export interface PlannedVariable {
27
- /** The variable and the recipe that published it. */
28
- defined: DefinedVariable;
29
- /** The environment variable the answer would be stored under. */
30
- storedAs: string;
31
- /** The env file it would be stored in. */
32
- file: AnswerFile;
33
- /** That file's absolute path. */
34
- filePath: string;
35
- /** True when something already answers this variable, and the answer fits. */
36
- answered: boolean;
37
- /** Where the existing answer came from, in plain language. */
38
- answeredFrom?: string;
39
- }
40
-
41
- /** What `planQuestions` needs beyond the definitions themselves. */
42
- export interface QuestionPlanOptions {
43
- /** The project's `.sous/` directory, which holds both env files. */
44
- sousDir: string;
45
- }
46
-
47
- /**
48
- * The first sentence of a description, which is what a plan shows; the rest is
49
- * in `sous vars show <name>`.
50
- *
51
- * @param text - The publisher's description.
52
- */
53
- export function firstSentence(text: string): string {
54
- const match = /^.*?[.!?](?=\s|$)/s.exec(text.trim());
55
- return (match?.[0] ?? text.trim()).replace(/\s+/g, " ");
56
- }
57
-
58
- /**
59
- * Works out, for every variable in play, where its answer would go and whether
60
- * anything answers it already.
61
- *
62
- * @param defined - Every variable definition the closure declares.
63
- * @param context - The environment layers and mapping records to resolve against.
64
- * @param options - Where the project's env files live.
65
- */
66
- export function planQuestions(
67
- defined: DefinedVariable[],
68
- context: LadderContext,
69
- options: QuestionPlanOptions
70
- ): PlannedVariable[] {
71
- return defined.map((entry) => {
72
- const file = answerFileFor(entry.definition);
73
- const existing = resolveVariable(entry, context);
74
- const answered =
75
- existing !== undefined &&
76
- validateAnswer(entry.definition, existing.value, {
77
- recipe: definingRecipeKey(entry.recipe),
78
- }).ok;
79
-
80
- return {
81
- defined: entry,
82
- storedAs: answered ? existing!.source.envName : bareName(entry.definition),
83
- file,
84
- filePath: path.join(options.sousDir, file),
85
- answered,
86
- ...(answered ? { answeredFrom: describeSource(existing!.source) } : {}),
87
- };
88
- });
89
- }
90
-
91
- /** "1 question" or "3 questions", so no count is printed with the wrong noun. */
92
- function questionCount(count: number): string {
93
- return count === 1 ? "1 question" : `${count} questions`;
94
- }
95
-
96
- /**
97
- * The labeled facts one planned question shows.
98
- *
99
- * @param planned - The planned question.
100
- * @param answerHints - Whether to name the `--answer` flag that answers it ahead of time.
101
- */
102
- export function plannedVariableFacts(
103
- planned: PlannedVariable,
104
- answerHints = true
105
- ): LabeledFact[] {
106
- const { definition } = planned.defined;
107
-
108
- const facts: LabeledFact[] = [
109
- { label: "about", lines: [firstSentence(definition.description)] },
110
- { label: "example", lines: [String(definition.example)] },
111
- { label: "stored-as", lines: [planned.storedAs] },
112
- { label: "storage-path", lines: [planned.filePath] },
113
- {
114
- label: "answered",
115
- lines: [
116
- planned.answered
117
- ? `yes, from ${planned.answeredFrom}`
118
- : definition.required
119
- ? "no, and this recipe requires an answer"
120
- : "no, and an answer is optional",
121
- ],
122
- },
123
- ];
124
- if (answerHints) {
125
- facts.push({ label: "answer-with", lines: [`--answer ${definition.name}=<value>`] });
126
- }
127
- return facts;
128
- }
129
-
130
- /** What `formatQuestionPlan` needs beyond the questions themselves. */
131
- export interface QuestionPlanFormatOptions {
132
- /** The column to wrap descriptions at. */
133
- width?: number;
134
- /**
135
- * Recipes in the closure whose questions are unknown: their files are not on
136
- * this machine, and their repository's index does not record their
137
- * questions. They are named in the plan rather than silently left out of it.
138
- */
139
- unreadable?: string[];
140
- /**
141
- * Whether the plan says how to answer its questions ahead of time: the
142
- * `--answer` flag under each question, and a closing sentence. True for the
143
- * dry run of a command that asks them; false where the plan only describes.
144
- */
145
- answerHints?: boolean;
146
- }
147
-
148
- /**
149
- * The one line that names the recipes whose questions are unknown. Nothing is
150
- * downloaded to plan, so a recipe this machine does not hold yet is described
151
- * from its repository's index, and an index written before sous recorded
152
- * questions there cannot say; naming those recipes is more useful than leaving
153
- * them out of the plan.
154
- *
155
- * @param unreadable - The recipe keys whose questions are unknown.
156
- * @param answerHints - Whether the plan belongs to a dry run that could be run for real.
157
- */
158
- function unreadableLine(unreadable: string[], answerHints: boolean): string {
159
- const line =
160
- `Not on this machine yet, and their repository's index does not record their ` +
161
- `questions, so they cannot be listed: ${unreadable.join(", ")}.`;
162
- return answerHints
163
- ? `${line} A dry run downloads nothing; run this command again without ` +
164
- `'--dry-run' to install them and be asked.`
165
- : line;
166
- }
167
-
168
- /**
169
- * The whole plan as lines to print: one block per recipe, one labeled fact
170
- * sheet per variable, a line naming any recipe that could not be read, and a
171
- * closing sentence naming how to answer them all ahead of time.
172
- *
173
- * @param planned - Every question the closure would ask.
174
- * @param options - The wrap column, and any recipes that could not be read.
175
- * @returns The lines to print, without indentation.
176
- */
177
- export function formatQuestionPlan(
178
- planned: PlannedVariable[],
179
- options: QuestionPlanFormatOptions = {}
180
- ): string[] {
181
- const unreadable = options.unreadable ?? [];
182
- const answerHints = options.answerHints ?? true;
183
-
184
- const columns = options.width ?? wrapColumns();
185
- /** Wraps one sentence to the width the caller's indentation leaves for it. */
186
- const sentence = (text: string, paint = (line: string): string => line): string[] =>
187
- wrapText(text, columns - 2).map(paint);
188
-
189
- if (planned.length === 0) {
190
- return unreadable.length === 0
191
- ? sentence("None of these recipes ask any questions, so nothing needs answering.")
192
- : sentence(unreadableLine(unreadable, answerHints), palette.note);
193
- }
194
-
195
- const unanswered = planned.filter((entry) => !entry.answered).length;
196
- // When part of the closure could not be read, the count below describes only
197
- // the part that could, and the sentence says so rather than overstating it.
198
- const subject =
199
- unreadable.length === 0 ? "These recipes ask" : "The recipes sous could read ask";
200
-
201
- const lines: string[] = sentence(
202
- unanswered === 0
203
- ? `${subject} ${questionCount(planned.length)}, and everything they ask ` +
204
- `is already answered.`
205
- : `${subject} ${questionCount(planned.length)}, ` +
206
- `${unanswered} of which nothing answers yet.`
207
- );
208
-
209
- const groups = new Map<string, PlannedVariable[]>();
210
- for (const entry of planned) {
211
- const key = definingRecipeKey(entry.defined.recipe);
212
- groups.set(key, [...(groups.get(key) ?? []), entry]);
213
- }
214
-
215
- for (const [key, entries] of groups) {
216
- lines.push("", `${key} asks ${questionCount(entries.length)}:`);
217
- for (const entry of entries) {
218
- lines.push("", ` ${color.cyan(entry.defined.definition.name)}`);
219
- // The facts block indents itself, so the plan adds nothing on top of it.
220
- lines.push(...renderFacts(plannedVariableFacts(entry, answerHints), columns - 2));
221
- }
222
- }
223
-
224
- if (unreadable.length > 0) {
225
- lines.push("", ...sentence(unreadableLine(unreadable, answerHints), palette.note));
226
- }
227
-
228
- if (unanswered > 0 && answerHints) {
229
- lines.push(
230
- "",
231
- ...sentence(
232
- "Answer them all ahead of time by running this command again without " +
233
- "'--dry-run', with one '--answer <name>=<value>' for each, or with " +
234
- "'--answers-file <path>'.",
235
- palette.note
236
- )
237
- );
238
- }
239
-
240
- return lines;
241
- }