@sous-io/sous 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +115 -35
  2. package/bin/run.js +10 -1
  3. package/docs/markdown/README.md +27 -0
  4. package/docs/markdown/_sidebar.md +18 -0
  5. package/docs/markdown/commands.md +308 -0
  6. package/docs/markdown/config-discovery.md +74 -0
  7. package/docs/markdown/config-inspection.md +69 -0
  8. package/docs/markdown/config-layers.md +92 -0
  9. package/docs/markdown/config-variables.md +79 -0
  10. package/docs/markdown/configuration.md +71 -0
  11. package/docs/markdown/design-principles.md +59 -0
  12. package/docs/markdown/repositories-authoring.md +408 -0
  13. package/docs/markdown/repositories-consuming.md +580 -0
  14. package/docs/markdown/repositories-file-formats.md +1084 -0
  15. package/docs/markdown/repositories-variables.md +387 -0
  16. package/docs/markdown/repositories.md +303 -0
  17. package/docs/markdown/skill-categories.md +58 -0
  18. package/package.json +72 -8
  19. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
  20. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
  21. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
  22. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
  23. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
  24. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
  25. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
  26. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
  27. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
  28. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
  29. package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
  30. package/sous.config.schema.json +337 -0
  31. package/src/base-command.ts +220 -67
  32. package/src/commands/build.ts +150 -73
  33. package/src/commands/clear.ts +23 -15
  34. package/src/commands/compile.ts +74 -16
  35. package/src/commands/config/get.ts +110 -0
  36. package/src/commands/config/show.ts +32 -0
  37. package/src/commands/config/validate.ts +53 -0
  38. package/src/commands/help.ts +46 -0
  39. package/src/commands/launch.ts +36 -14
  40. package/src/commands/lock/rebuild.ts +241 -0
  41. package/src/commands/lock/show.ts +115 -0
  42. package/src/commands/namespace/list.ts +117 -0
  43. package/src/commands/namespace/show.ts +110 -0
  44. package/src/commands/prune.ts +3 -11
  45. package/src/commands/recipe/list.ts +95 -0
  46. package/src/commands/recipe/show.ts +301 -0
  47. package/src/commands/repo/add.ts +145 -0
  48. package/src/commands/repo/gc.ts +172 -0
  49. package/src/commands/repo/init.ts +136 -0
  50. package/src/commands/repo/link.ts +500 -0
  51. package/src/commands/repo/list.ts +179 -0
  52. package/src/commands/repo/release.ts +619 -0
  53. package/src/commands/repo/remove.ts +193 -0
  54. package/src/commands/repo/search.ts +189 -0
  55. package/src/commands/repo/submit.ts +133 -0
  56. package/src/commands/repo/unlink.ts +147 -0
  57. package/src/commands/subscription/add.ts +285 -0
  58. package/src/commands/subscription/list.ts +129 -0
  59. package/src/commands/subscription/remove.ts +181 -0
  60. package/src/commands/vars/ask.ts +374 -0
  61. package/src/commands/vars/index.ts +79 -0
  62. package/src/commands/vars/list.ts +67 -0
  63. package/src/commands/vars/show.ts +77 -0
  64. package/src/config-command.ts +30 -0
  65. package/src/lib/build-service.ts +206 -54
  66. package/src/lib/config-discovery.ts +220 -27
  67. package/src/lib/config-inspect.ts +145 -0
  68. package/src/lib/config-kernel.mjs +377 -0
  69. package/src/lib/config-schema.ts +361 -0
  70. package/src/lib/env-file.ts +328 -0
  71. package/src/lib/env-local.ts +18 -1
  72. package/src/lib/errors.ts +32 -0
  73. package/src/lib/include-resolver.ts +108 -15
  74. package/src/lib/interactive.ts +165 -0
  75. package/src/lib/markdown-compiler.ts +118 -37
  76. package/src/lib/package-info.ts +25 -0
  77. package/src/lib/pid-service.ts +32 -21
  78. package/src/lib/refs/find.ts +589 -0
  79. package/src/lib/refs/index.ts +12 -0
  80. package/src/lib/refs/pick.ts +147 -0
  81. package/src/lib/refs/scopes.ts +61 -0
  82. package/src/lib/repos/catalog-display.ts +116 -0
  83. package/src/lib/repos/catalog-inputs.ts +160 -0
  84. package/src/lib/repos/catalog.ts +722 -0
  85. package/src/lib/repos/core-recipe.ts +105 -0
  86. package/src/lib/repos/defaults.ts +175 -0
  87. package/src/lib/repos/formats/common.ts +389 -0
  88. package/src/lib/repos/formats/index-file.ts +215 -0
  89. package/src/lib/repos/formats/links-map.ts +96 -0
  90. package/src/lib/repos/formats/lockfile.ts +167 -0
  91. package/src/lib/repos/formats/patterns.ts +57 -0
  92. package/src/lib/repos/formats/recipe-manifest.ts +395 -0
  93. package/src/lib/repos/formats/repo-manifest.ts +88 -0
  94. package/src/lib/repos/formats/store-entry.ts +84 -0
  95. package/src/lib/repos/freshness.ts +208 -0
  96. package/src/lib/repos/git-clone.ts +312 -0
  97. package/src/lib/repos/identity.ts +89 -0
  98. package/src/lib/repos/index.ts +58 -0
  99. package/src/lib/repos/links.ts +353 -0
  100. package/src/lib/repos/load-manifest.ts +236 -0
  101. package/src/lib/repos/lock-service.ts +453 -0
  102. package/src/lib/repos/locked-namespace-resolver.ts +90 -0
  103. package/src/lib/repos/locked-recipes.ts +254 -0
  104. package/src/lib/repos/managed-layer.ts +422 -0
  105. package/src/lib/repos/namespace-resolver.ts +370 -0
  106. package/src/lib/repos/providers/base.ts +206 -0
  107. package/src/lib/repos/providers/git.ts +233 -0
  108. package/src/lib/repos/providers/github.ts +294 -0
  109. package/src/lib/repos/providers/gitlab.ts +263 -0
  110. package/src/lib/repos/providers/http.ts +102 -0
  111. package/src/lib/repos/providers/index-cache.ts +382 -0
  112. package/src/lib/repos/providers/index.ts +106 -0
  113. package/src/lib/repos/providers/local.ts +391 -0
  114. package/src/lib/repos/providers/provider.ts +401 -0
  115. package/src/lib/repos/recipe-config-layers.ts +287 -0
  116. package/src/lib/repos/recipe-targets.ts +223 -0
  117. package/src/lib/repos/ref-search.ts +46 -0
  118. package/src/lib/repos/ref.ts +513 -0
  119. package/src/lib/repos/reference-report.ts +122 -0
  120. package/src/lib/repos/release/bump.ts +161 -0
  121. package/src/lib/repos/release/git-state.ts +305 -0
  122. package/src/lib/repos/release/index-builder.ts +635 -0
  123. package/src/lib/repos/release/index.ts +16 -0
  124. package/src/lib/repos/release/plan.ts +512 -0
  125. package/src/lib/repos/release/submit-service.ts +496 -0
  126. package/src/lib/repos/release/tags.ts +243 -0
  127. package/src/lib/repos/release/validate.ts +463 -0
  128. package/src/lib/repos/resolver.ts +789 -0
  129. package/src/lib/repos/scaffold/index.ts +238 -0
  130. package/src/lib/repos/scaffold/templates.ts +413 -0
  131. package/src/lib/repos/seed.ts +414 -0
  132. package/src/lib/repos/store/contract.ts +64 -0
  133. package/src/lib/repos/store/hash.ts +114 -0
  134. package/src/lib/repos/store/recipe-store.ts +599 -0
  135. package/src/lib/repos/store/settings.ts +58 -0
  136. package/src/lib/repos/subscription-service.ts +2678 -0
  137. package/src/lib/repos/trust.ts +447 -0
  138. package/src/lib/settings.ts +546 -189
  139. package/src/lib/sous-home.ts +104 -0
  140. package/src/lib/state.ts +52 -20
  141. package/src/lib/vars/ask.ts +1152 -0
  142. package/src/lib/vars/definition-source.ts +252 -0
  143. package/src/lib/vars/display.ts +233 -0
  144. package/src/lib/vars/index.ts +18 -0
  145. package/src/lib/vars/ladder.ts +282 -0
  146. package/src/lib/vars/mappings.ts +265 -0
  147. package/src/lib/vars/names.ts +94 -0
  148. package/src/lib/vars/preanswers.ts +395 -0
  149. package/src/lib/vars/question-plan.ts +218 -0
  150. package/src/lib/vars/report.ts +228 -0
  151. package/src/lib/vars/safe-regex.ts +235 -0
  152. package/src/lib/vars/validate.ts +312 -0
  153. package/src/lib/watch-loop.ts +148 -0
  154. package/src/templating/init-liquid-engine.ts +58 -16
  155. package/src/utils/choice-prompt.ts +143 -0
  156. package/src/utils/command-errors.ts +186 -0
  157. package/src/utils/command-help.ts +45 -0
  158. package/src/utils/confirm-prompt.ts +110 -0
  159. package/src/utils/flags.ts +153 -0
  160. package/src/utils/formatting.ts +540 -55
  161. package/src/utils/prompts.ts +35 -1
  162. package/src/utils/sous-directory.ts +245 -0
  163. package/src/utils/table.ts +603 -0
  164. package/src/utils/value-prompt.ts +119 -0
  165. package/shared-prompts/_partials/resume-task.md +0 -51
  166. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  167. package/shared-prompts/_partials/update-task-file.md +0 -52
  168. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  169. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  189. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  190. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  191. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  192. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  193. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  194. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  195. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  196. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  197. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  198. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  199. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  200. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  201. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  202. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  203. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  204. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  205. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
@@ -0,0 +1,374 @@
1
+ /**
2
+ * `sous vars ask [name]`.
3
+ *
4
+ * Asks the questions the project's variable definitions imply and stores the
5
+ * answers in `.sous/.env` or `.sous/.env.local`. By default it asks only what
6
+ * is unanswered or no longer fits; `--all` asks everything again.
7
+ *
8
+ * The optional name is a reference, resolved through `src/lib/refs/` exactly as
9
+ * every other command resolves one. It may name a variable, an environment
10
+ * variable that answers one, a recipe, a namespace or a repository, at any
11
+ * level of qualification; naming anything larger than a variable asks every
12
+ * question that thing publishes:
13
+ *
14
+ * sous vars ask taskFileRoot one variable
15
+ * sous vars ask SOUS_VAR_TASK_FILE_ROOT the variable that name answers
16
+ * sous vars ask task-files every question one recipe asks
17
+ * sous vars ask workflow every question one namespace asks
18
+ * sous vars ask sous-recipes every question one repository asks
19
+ *
20
+ * `--repo`, `--namespace` and `--var` say outright which kind of thing is
21
+ * meant, and narrow the same way. A reference that means more than one thing is
22
+ * offered as a list to choose between; `--accept-first` takes the first, and a
23
+ * run with no terminal fails naming that flag.
24
+ *
25
+ * `--file` reads a standalone definitions file instead of the project's
26
+ * recipes, which is how a project asks questions no recipe publishes yet.
27
+ *
28
+ * Answers can be supplied ahead of the questions with `--answer name=value` or
29
+ * `--answers-file <path>`; whatever they answer is stored before anything is
30
+ * asked, and only what is left over is asked for.
31
+ *
32
+ * Without a terminal the command never hangs waiting on a prompt: it fails and
33
+ * names the exact environment variables that would answer each question.
34
+ */
35
+
36
+ import path from "node:path";
37
+ import { Args, Flags } from "@oclif/core";
38
+ import { BaseCommand } from "../../base-command.js";
39
+ import { ConfigError } from "../../lib/errors.js";
40
+ import {
41
+ ALL_SCOPES,
42
+ SousScope,
43
+ findNamespace,
44
+ findReference,
45
+ findRepository,
46
+ findVariable,
47
+ pickReference,
48
+ referenceContextFromVariables,
49
+ variableReferenceKey,
50
+ type ReferenceMatch,
51
+ } from "../../lib/refs/index.js";
52
+ import {
53
+ applyProvidedAnswers,
54
+ askForMissing,
55
+ collectProvidedAnswers,
56
+ FileDefinitionSource,
57
+ formatAskReport,
58
+ loadLadderContext,
59
+ loadProjectDefinitions,
60
+ unknownAnswerError,
61
+ type DefinedVariable,
62
+ } from "../../lib/vars/index.js";
63
+ import type { LadderContext } from "../../lib/vars/ladder.js";
64
+ import {
65
+ blankLine,
66
+ dryRunNotice,
67
+ footer,
68
+ heading,
69
+ indent,
70
+ log,
71
+ showCommandVars,
72
+ } from "../../utils/formatting.js";
73
+ import { answerFlags } from "../../utils/flags.js";
74
+
75
+ /** What the caller said should be asked: the reference, and the three flags. */
76
+ type Selection = {
77
+ /** The reference argument, when one was given. */
78
+ name?: string;
79
+ /** The repository named by `--repo`. */
80
+ repo?: string;
81
+ /** The namespace named by `--namespace`. */
82
+ namespace?: string;
83
+ /** Every variable named by `--var`. */
84
+ vars?: string[];
85
+ /** Take the first match rather than asking which was meant. */
86
+ acceptFirst: boolean;
87
+ };
88
+
89
+ /** The line every "this matched nothing" error ends with. */
90
+ const SEE_ALL = " Run 'sous vars' to see every variable this project's recipes define.";
91
+
92
+ export default class VarsAsk extends BaseCommand {
93
+ static description =
94
+ "Answer the variables this project's recipes define, storing the answers in the .sous env files";
95
+
96
+ /**
97
+ * The other spelling of the topic. It lives under a hidden topic, so it is
98
+ * typable everywhere without ever reaching the top-level listing.
99
+ */
100
+ static aliases = ["var:ask"];
101
+
102
+ static examples = [
103
+ "<%= config.bin %> vars ask",
104
+ "<%= config.bin %> vars ask apiUrl",
105
+ "<%= config.bin %> vars ask SOUS_VAR_API_URL",
106
+ "<%= config.bin %> vars ask workflow/task-files",
107
+ "<%= config.bin %> vars ask workflow --accept-first",
108
+ "<%= config.bin %> vars ask --namespace workflow --var apiUrl",
109
+ "<%= config.bin %> vars ask --all",
110
+ "<%= config.bin %> vars ask --file ./questions.yaml",
111
+ "<%= config.bin %> vars ask --answer apiUrl=https://api.example.com",
112
+ "<%= config.bin %> vars ask --answers-file ./answers.yaml",
113
+ ];
114
+
115
+ static args = {
116
+ name: Args.string({
117
+ description:
118
+ "What to ask about: a variable, an environment variable name, a recipe, a namespace or a repository",
119
+ required: false,
120
+ }),
121
+ };
122
+
123
+ static flags = {
124
+ ...BaseCommand.baseFlags,
125
+ all: Flags.boolean({
126
+ description: "Ask every variable again, including the ones already answered",
127
+ default: false,
128
+ }),
129
+ repo: Flags.string({
130
+ description: "Ask only the variables published by this repository",
131
+ }),
132
+ namespace: Flags.string({
133
+ description: "Ask only the variables published in this namespace",
134
+ }),
135
+ var: Flags.string({
136
+ description: "Ask only this variable. Repeat it for each variable",
137
+ multiple: true,
138
+ }),
139
+ "accept-first": Flags.boolean({
140
+ description: "When a name matches several things, take the first one listed",
141
+ default: false,
142
+ }),
143
+ file: Flags.string({
144
+ description:
145
+ "Read the variable definitions from a standalone definitions file instead of the project's recipes",
146
+ }),
147
+ "dry-run": Flags.boolean({
148
+ description: "Report what would be asked and written, without writing anything",
149
+ default: false,
150
+ }),
151
+ ...answerFlags(),
152
+ };
153
+
154
+ async run(): Promise<void> {
155
+ const { args, flags } = await this.parse(VarsAsk);
156
+ const dryRun = flags["dry-run"];
157
+
158
+ const selection: Selection = {
159
+ ...(args.name === undefined ? {} : { name: args.name }),
160
+ ...(flags.repo === undefined ? {} : { repo: flags.repo }),
161
+ ...(flags.namespace === undefined ? {} : { namespace: flags.namespace }),
162
+ ...(flags.var === undefined ? {} : { vars: flags.var }),
163
+ acceptFirst: flags["accept-first"],
164
+ };
165
+
166
+ showCommandVars({
167
+ Project: this.projectLabel,
168
+ Config: this.configContext.configPath,
169
+ Definitions: flags.file ?? "the project's subscribed recipes",
170
+ Asking: describeSelection(selection, flags.all),
171
+ });
172
+
173
+ if (dryRun) dryRunNotice("No answers will be written.");
174
+
175
+ const provided = collectProvidedAnswers({
176
+ ...(flags.answer === undefined ? {} : { answer: flags.answer }),
177
+ ...(flags["answers-file"] === undefined
178
+ ? {}
179
+ : { answersFile: flags["answers-file"] }),
180
+ });
181
+
182
+ const source =
183
+ flags.file === undefined
184
+ ? loadProjectDefinitions(this.settings, this.configContext.sousDir)
185
+ : new FileDefinitionSource(path.resolve(process.cwd(), flags.file));
186
+ const defined = await source.load();
187
+
188
+ heading("Answering variables");
189
+ blankLine();
190
+
191
+ if (defined.length === 0) {
192
+ // A supplied answer names a variable nothing declares, which is a typo
193
+ // until proven otherwise; it fails rather than passing unnoticed.
194
+ if (provided[0] !== undefined) throw unknownAnswerError(provided[0], defined);
195
+
196
+ log(
197
+ indent(
198
+ "No recipe in this project defines any variables yet, so there is nothing " +
199
+ "to ask. Subscribe to a recipe that publishes some, or read a definitions " +
200
+ "file with --file."
201
+ )
202
+ );
203
+ footer();
204
+ return;
205
+ }
206
+
207
+ const context = loadLadderContext({
208
+ sousDir: this.configContext.sousDir,
209
+ settings: this.settings,
210
+ shellEnv: this.shellEnv,
211
+ });
212
+
213
+ // What the reference and the flags name, as the fully qualified reference of
214
+ // every variable to ask. Undefined means everything the usual rules pick.
215
+ const only = await this.resolveSelection(defined, context, selection);
216
+
217
+ const askOptions = {
218
+ sousDir: this.configContext.sousDir,
219
+ // A project whose conf.d directory has never existed still gets one when
220
+ // a mapping record needs writing; the writer creates it.
221
+ confDir:
222
+ this.configContext.confDir ?? path.join(this.configContext.sousDir, "conf.d"),
223
+ interactive: this.interactive,
224
+ dryRun,
225
+ };
226
+
227
+ // Answers supplied on the command line are validated and stored before any
228
+ // question is asked, so what is left is exactly what nobody answered.
229
+ const supplied = applyProvidedAnswers(defined, provided, context, askOptions);
230
+
231
+ const report = await askForMissing(defined, context, {
232
+ ...askOptions,
233
+ ...(only === undefined ? {} : { only }),
234
+ skip: supplied.keys,
235
+ reask: flags.all,
236
+ });
237
+ report.answered.unshift(...supplied.stored);
238
+
239
+ blankLine();
240
+ for (const line of formatAskReport(report, dryRun)) {
241
+ log(line === "" ? "" : indent(line));
242
+ }
243
+
244
+ footer();
245
+ }
246
+
247
+ /**
248
+ * Turns the reference and the three narrowing flags into the exact set of
249
+ * variables to ask, as fully qualified references.
250
+ *
251
+ * Each step narrows the pool and the next step resolves against what is left,
252
+ * so `--namespace workflow apiUrl` asks about the `apiUrl` of that namespace
253
+ * even when another namespace declares one too. A step that narrows the pool
254
+ * to nothing is an error naming what did it, rather than a run that silently
255
+ * asks no questions.
256
+ *
257
+ * @param defined - Every variable definition in play.
258
+ * @param ladder - The environment layers, for resolving environment variable names.
259
+ * @param selection - The reference and the flags, as the caller wrote them.
260
+ * @returns The variables to ask, or undefined when nothing narrowed anything.
261
+ */
262
+ private async resolveSelection(
263
+ defined: DefinedVariable[],
264
+ ladder: LadderContext,
265
+ selection: Selection
266
+ ): Promise<string[] | undefined> {
267
+ const { name, repo, namespace, vars } = selection;
268
+ if (name === undefined && repo === undefined && namespace === undefined && vars === undefined) {
269
+ return undefined;
270
+ }
271
+
272
+ let pool = defined;
273
+
274
+ if (repo !== undefined) {
275
+ const match = await this.pick(findRepository(repo, contextOf(pool, ladder)), repo, selection);
276
+ pool = pool.filter((entry) => entry.recipe.repo === match.repo);
277
+ }
278
+
279
+ if (namespace !== undefined) {
280
+ const match = await this.pick(
281
+ findNamespace(namespace, contextOf(pool, ladder)),
282
+ namespace,
283
+ selection
284
+ );
285
+ pool = narrowTo(pool, match);
286
+ }
287
+
288
+ if (name !== undefined) {
289
+ const match = await this.pick(
290
+ findReference(name, ALL_SCOPES, contextOf(pool, ladder)),
291
+ name,
292
+ selection
293
+ );
294
+ pool = narrowTo(pool, match);
295
+ }
296
+
297
+ if (vars !== undefined) {
298
+ const chosen: DefinedVariable[] = [];
299
+ for (const wanted of vars) {
300
+ const match = await this.pick(
301
+ findVariable(wanted, contextOf(pool, ladder)),
302
+ wanted,
303
+ selection
304
+ );
305
+ chosen.push(...narrowTo(pool, match));
306
+ }
307
+ pool = chosen;
308
+ }
309
+
310
+ if (pool.length === 0) {
311
+ throw new ConfigError(
312
+ `Nothing is left to ask: ${describeSelection(selection, false)} names no ` +
313
+ `variable this project holds.\n${SEE_ALL}`
314
+ );
315
+ }
316
+
317
+ return [...new Set(pool.map(variableReferenceKey))];
318
+ }
319
+
320
+ /**
321
+ * Settles which of the things a reference could have meant this run proceeds
322
+ * with, through the rule every command shares.
323
+ *
324
+ * @param matches - What the reference could have meant, in listing order.
325
+ * @param search - The reference exactly as it was written.
326
+ * @param selection - The selection, read for `--accept-first`.
327
+ */
328
+ private async pick(
329
+ matches: ReferenceMatch[],
330
+ search: string,
331
+ selection: Selection
332
+ ): Promise<ReferenceMatch> {
333
+ return pickReference(matches, {
334
+ search,
335
+ interactive: this.interactive,
336
+ acceptFirst: selection.acceptFirst,
337
+ announce: false,
338
+ details: [SEE_ALL],
339
+ });
340
+ }
341
+ }
342
+
343
+ /** The variables one match covers: a whole repository, namespace, recipe, or one variable. */
344
+ function narrowTo(pool: DefinedVariable[], match: ReferenceMatch): DefinedVariable[] {
345
+ return pool.filter((entry) => {
346
+ if (match.repo !== undefined && entry.recipe.repo !== match.repo) return false;
347
+ if (match.scope === SousScope.Repository) return true;
348
+
349
+ if (match.namespace !== undefined && entry.recipe.namespace !== match.namespace) return false;
350
+ if (match.scope === SousScope.Namespace) return true;
351
+
352
+ if (match.recipe !== undefined && entry.recipe.name !== match.recipe) return false;
353
+ if (match.scope === SousScope.Recipe) return true;
354
+
355
+ return entry.definition.name === match.variable;
356
+ });
357
+ }
358
+
359
+ /** The reference context for the variables still in the running. */
360
+ function contextOf(pool: DefinedVariable[], ladder: LadderContext) {
361
+ return referenceContextFromVariables(pool, ladder);
362
+ }
363
+
364
+ /** What this run is asking about, in the words the header shows. */
365
+ function describeSelection(selection: Selection, all: boolean): string {
366
+ const parts: string[] = [];
367
+ if (selection.repo !== undefined) parts.push(`the repository '${selection.repo}'`);
368
+ if (selection.namespace !== undefined) parts.push(`the namespace '${selection.namespace}'`);
369
+ if (selection.name !== undefined) parts.push(`'${selection.name}'`);
370
+ for (const wanted of selection.vars ?? []) parts.push(`the variable '${wanted}'`);
371
+
372
+ if (parts.length === 0) return all ? "every variable" : "everything unanswered";
373
+ return parts.join(", ");
374
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Bare `sous vars`, and `sous vars <name>`.
3
+ *
4
+ * The canonical commands are `sous vars list` and `sous vars show <name>`; this
5
+ * is the shorthand that keeps working, so it prints the listing with no argument
6
+ * and the detail with one. It is hidden from the command list so `sous --help`
7
+ * names `vars` once, as a topic.
8
+ *
9
+ * The optional argument works because oclif collates leading space-separated
10
+ * words into a command id only while they keep matching a real command:
11
+ * `sous vars show` finds the `vars:show` command, and `sous vars apiUrl` does
12
+ * not, so it lands here with `apiUrl` as the argument. The one consequence is
13
+ * that a variable named `list`, `show` or `ask` cannot be reached this way;
14
+ * `sous vars show list` reaches it.
15
+ */
16
+
17
+ import path from "node:path";
18
+ import { Args, Flags } from "@oclif/core";
19
+ import { BaseCommand } from "../../base-command.js";
20
+ import {
21
+ FileDefinitionSource,
22
+ loadLadderContext,
23
+ loadProjectDefinitions,
24
+ printVariableDetail,
25
+ printVariableList,
26
+ } from "../../lib/vars/index.js";
27
+ import { footer, showCommandVars } from "../../utils/formatting.js";
28
+
29
+ export default class Vars extends BaseCommand {
30
+ static description = "List every variable this project's recipes define, with its answer";
31
+
32
+ /** Hidden so `sous --help` names `vars` once, as a topic. */
33
+ static hidden = true;
34
+
35
+ static examples = ["<%= config.bin %> vars", "<%= config.bin %> vars apiUrl"];
36
+
37
+ static args = {
38
+ name: Args.string({
39
+ description:
40
+ "A variable's name (or its namespace/recipe.name key) to show in full",
41
+ required: false,
42
+ }),
43
+ };
44
+
45
+ static flags = {
46
+ ...BaseCommand.baseFlags,
47
+ file: Flags.string({
48
+ description:
49
+ "Read the variable definitions from a standalone definitions file instead of the project's recipes",
50
+ }),
51
+ };
52
+
53
+ async run(): Promise<void> {
54
+ const { args, flags } = await this.parse(Vars);
55
+
56
+ showCommandVars({
57
+ Project: this.projectLabel,
58
+ Config: this.configContext.configPath,
59
+ Definitions: flags.file ?? "the project's subscribed recipes",
60
+ });
61
+
62
+ const source =
63
+ flags.file === undefined
64
+ ? loadProjectDefinitions(this.settings, this.configContext.sousDir)
65
+ : new FileDefinitionSource(path.resolve(process.cwd(), flags.file));
66
+ const defined = await source.load();
67
+
68
+ const context = loadLadderContext({
69
+ sousDir: this.configContext.sousDir,
70
+ settings: this.settings,
71
+ shellEnv: this.shellEnv,
72
+ });
73
+
74
+ if (args.name === undefined) printVariableList(defined, context);
75
+ else printVariableDetail(defined, context, args.name);
76
+
77
+ footer();
78
+ }
79
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `sous vars list`.
3
+ *
4
+ * Prints every variable in play: what it is called, which recipe published it,
5
+ * the environment variable that answered it, the value (hidden when the
6
+ * definition says the variable is a secret), and where the value came from.
7
+ */
8
+
9
+ import path from "node:path";
10
+ import { Flags } from "@oclif/core";
11
+ import { BaseCommand } from "../../base-command.js";
12
+ import {
13
+ FileDefinitionSource,
14
+ loadLadderContext,
15
+ loadProjectDefinitions,
16
+ printVariableList,
17
+ } from "../../lib/vars/index.js";
18
+ import { footer, showCommandVars } from "../../utils/formatting.js";
19
+
20
+ export default class VarsList extends BaseCommand {
21
+ static description = "List every variable this project's recipes define, with its answer";
22
+
23
+ /**
24
+ * The other spelling of the topic. It lives under a hidden topic, so it is
25
+ * typable everywhere without ever reaching the top-level listing.
26
+ */
27
+ static aliases = ["var:list"];
28
+
29
+ static examples = [
30
+ "<%= config.bin %> vars list",
31
+ "<%= config.bin %> vars list --file ./questions.yaml",
32
+ ];
33
+
34
+ static flags = {
35
+ ...BaseCommand.baseFlags,
36
+ file: Flags.string({
37
+ description:
38
+ "Read the variable definitions from a standalone definitions file instead of the project's recipes",
39
+ }),
40
+ };
41
+
42
+ async run(): Promise<void> {
43
+ const { flags } = await this.parse(VarsList);
44
+
45
+ showCommandVars({
46
+ Project: this.projectLabel,
47
+ Config: this.configContext.configPath,
48
+ Definitions: flags.file ?? "the project's subscribed recipes",
49
+ });
50
+
51
+ const source =
52
+ flags.file === undefined
53
+ ? loadProjectDefinitions(this.settings, this.configContext.sousDir)
54
+ : new FileDefinitionSource(path.resolve(process.cwd(), flags.file));
55
+
56
+ printVariableList(
57
+ await source.load(),
58
+ loadLadderContext({
59
+ sousDir: this.configContext.sousDir,
60
+ settings: this.settings,
61
+ shellEnv: this.shellEnv,
62
+ })
63
+ );
64
+
65
+ footer();
66
+ }
67
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `sous vars show <name>`.
3
+ *
4
+ * Prints everything about one variable: the question it asks, its documentation
5
+ * and constraints, the recipe that published it, where an answer would be
6
+ * stored, every environment variable name on the resolution ladder, and which
7
+ * rung actually answered.
8
+ */
9
+
10
+ import path from "node:path";
11
+ import { Args, Flags } from "@oclif/core";
12
+ import { BaseCommand } from "../../base-command.js";
13
+ import {
14
+ FileDefinitionSource,
15
+ loadLadderContext,
16
+ loadProjectDefinitions,
17
+ printVariableDetail,
18
+ } from "../../lib/vars/index.js";
19
+ import { footer, showCommandVars } from "../../utils/formatting.js";
20
+
21
+ export default class VarsShow extends BaseCommand {
22
+ static description = "Show everything about one variable, including how it was answered";
23
+
24
+ /**
25
+ * The other spelling of the topic. It lives under a hidden topic, so it is
26
+ * typable everywhere without ever reaching the top-level listing.
27
+ */
28
+ static aliases = ["var:show"];
29
+
30
+ static examples = [
31
+ "<%= config.bin %> vars show apiUrl",
32
+ "<%= config.bin %> vars show workflow/task-files.apiUrl",
33
+ ];
34
+
35
+ static args = {
36
+ name: Args.string({
37
+ description: "A variable's name, or its namespace/recipe.name key",
38
+ required: true,
39
+ }),
40
+ };
41
+
42
+ static flags = {
43
+ ...BaseCommand.baseFlags,
44
+ file: Flags.string({
45
+ description:
46
+ "Read the variable definitions from a standalone definitions file instead of the project's recipes",
47
+ }),
48
+ };
49
+
50
+ async run(): Promise<void> {
51
+ const { args, flags } = await this.parse(VarsShow);
52
+
53
+ showCommandVars({
54
+ Project: this.projectLabel,
55
+ Config: this.configContext.configPath,
56
+ Definitions: flags.file ?? "the project's subscribed recipes",
57
+ });
58
+
59
+ const source =
60
+ flags.file === undefined
61
+ ? loadProjectDefinitions(this.settings, this.configContext.sousDir)
62
+ : new FileDefinitionSource(path.resolve(process.cwd(), flags.file));
63
+
64
+ printVariableDetail(
65
+ await source.load(),
66
+ loadLadderContext({
67
+ sousDir: this.configContext.sousDir,
68
+ settings: this.settings,
69
+ shellEnv: this.shellEnv,
70
+ }),
71
+ args.name,
72
+ { sousDir: this.configContext.sousDir }
73
+ );
74
+
75
+ footer();
76
+ }
77
+ }
@@ -0,0 +1,30 @@
1
+ import { BaseCommand } from "./base-command.js";
2
+ import { headerTo } from "./utils/formatting.js";
3
+
4
+ /**
5
+ * Base class for the `sous config *` inspection commands (`show`, `get`).
6
+ *
7
+ * These commands emit machine-readable output (JSON, or a raw scalar) to stdout
8
+ * so `sous config show | jq` and friends work. The decorative CLI header would
9
+ * otherwise land on stdout during BaseCommand.init() and corrupt that output, so
10
+ * it is routed to stderr here instead. Discovery, env loading, flag parsing and
11
+ * ConfigError rendering are all inherited unchanged from BaseCommand.
12
+ */
13
+ export abstract class ConfigCommand extends BaseCommand {
14
+ private static writeStderr(line: string): void {
15
+ process.stderr.write(`${line}\n`);
16
+ }
17
+
18
+ protected emitHeader(): void {
19
+ headerTo(ConfigCommand.writeStderr);
20
+ }
21
+
22
+ /**
23
+ * Route init()/catch() error rendering to stderr too. Otherwise a discovery or
24
+ * config-load failure (a not-found config, an invalid merged config, or a
25
+ * missing `config get` path) would print the red error block to stdout for the
26
+ * very commands whose contract is clean, machine-readable stdout — corrupting
27
+ * `sous config show | jq` and any script capturing stdout.
28
+ */
29
+ protected errorSink: (line: string) => void = ConfigCommand.writeStderr;
30
+ }