@sous-io/sous 0.1.1 → 0.2.1

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 +409 -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 +625 -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 +415 -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,252 @@
1
+ /**
2
+ * Where variable definitions come from.
3
+ *
4
+ * A definition is a published specification, not a value: it says what a
5
+ * variable is called, what shape an answer takes, and what question to ask.
6
+ * Definitions ship inside recipe manifests, so the real source is the set of
7
+ * recipes a project's lockfile pins. Every consumer goes through ONE seam,
8
+ * `loadProjectDefinitions`; nothing else in the variables layer knows where
9
+ * definitions came from, which is what lets `sous vars ask --file` read a
10
+ * standalone file through the same machinery.
11
+ */
12
+
13
+ import path from "node:path";
14
+ import { z } from "zod";
15
+ import { parseFormat } from "../repos/formats/common.js";
16
+ import {
17
+ variableDefinitionSchema,
18
+ type VariableDefinition,
19
+ } from "../repos/formats/recipe-manifest.js";
20
+ import { loadManifestFile } from "../repos/load-manifest.js";
21
+ import {
22
+ listLockedRecipes,
23
+ readProjectLockfile,
24
+ readRecipeManifestIn,
25
+ } from "../repos/locked-recipes.js";
26
+ import type { Settings } from "../settings.js";
27
+
28
+ /** Which recipe published a definition, spelled out for display and for naming. */
29
+ export interface DefiningRecipe {
30
+ /** The short name of the repository the recipe came from. */
31
+ repo: string;
32
+ /** The recipe's namespace. */
33
+ namespace: string;
34
+ /** The recipe's name. */
35
+ name: string;
36
+ /** The exact version of the recipe in play. */
37
+ version: string;
38
+ /**
39
+ * Where the repository lives: a URL for a hosted repository, or an absolute
40
+ * path for one read through the `local` provider. Used to show a recipe as a
41
+ * link rather than as a bare name.
42
+ */
43
+ url?: string;
44
+ /** The recipe's folder inside the repository, when it is known. */
45
+ path?: string;
46
+ /** The recipe's directory on this machine, when it is present. */
47
+ dir?: string;
48
+ }
49
+
50
+ /** One variable definition together with the recipe that published it. */
51
+ export interface DefinedVariable {
52
+ /** The published specification. */
53
+ definition: VariableDefinition;
54
+ /** The recipe the definition came from. */
55
+ recipe: DefiningRecipe;
56
+ /**
57
+ * How this variable came to be in play: the subscribed recipe first, then
58
+ * each recipe it depends on, ending with the recipe that declares the
59
+ * definition. A direct subscription has one entry; an indirect one shows the
60
+ * whole chain. Absent when nothing recorded it, in which case the defining
61
+ * recipe stands for itself.
62
+ */
63
+ requiredBy?: DefiningRecipe[];
64
+ }
65
+
66
+ /** Anything that can produce the variable definitions in play for a project. */
67
+ export interface VariableDefinitionSource {
68
+ /** Loads every definition this source knows about. */
69
+ load(): Promise<DefinedVariable[]>;
70
+ }
71
+
72
+ /**
73
+ * A source backed by a fixed array. Used by tests and by any caller that has
74
+ * already assembled its definitions.
75
+ */
76
+ export class StaticDefinitionSource implements VariableDefinitionSource {
77
+ /**
78
+ * @param defined - The definitions this source returns.
79
+ */
80
+ constructor(private readonly defined: DefinedVariable[]) {}
81
+
82
+ /** Returns the definitions handed to the constructor. */
83
+ async load(): Promise<DefinedVariable[]> {
84
+ return this.defined;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * The project's own definitions: every variable declared by every recipe the
90
+ * lockfile pins, whether the project subscribed to it directly or a dependency
91
+ * pulled it in.
92
+ *
93
+ * Both kinds count, deliberately. A recipe may `depends` on another purely to
94
+ * reuse its published variable definitions, and a build that cannot answer
95
+ * those variables is just as broken as one that cannot answer a subscription's.
96
+ *
97
+ * A recipe the store does not hold yet contributes nothing rather than failing:
98
+ * a fresh clone lists what it can until `sous build` restores the rest.
99
+ */
100
+ export class ProjectDefinitionSource implements VariableDefinitionSource {
101
+ /**
102
+ * @param settings - The merged project config, which holds the subscriptions.
103
+ * @param sousDir - The project's `.sous/` directory, where the lockfile lives.
104
+ * @param env - The environment to read; decides where the store is.
105
+ */
106
+ constructor(
107
+ private readonly settings: Settings,
108
+ private readonly sousDir: string,
109
+ private readonly env: NodeJS.ProcessEnv = process.env
110
+ ) {}
111
+
112
+ /** Every variable published by every recipe this project's lockfile pins. */
113
+ async load(): Promise<DefinedVariable[]> {
114
+ void this.settings;
115
+
116
+ const defined: DefinedVariable[] = [];
117
+ const lock = readProjectLockfile(this.sousDir);
118
+
119
+ for (const located of listLockedRecipes({ sousDir: this.sousDir, env: this.env })) {
120
+ if (!located.present) continue;
121
+
122
+ const manifest = readRecipeManifestIn(located.dir);
123
+ if (manifest === undefined) continue;
124
+
125
+ const url = lock.repos[located.repo]?.url;
126
+ const recipe: DefiningRecipe = {
127
+ repo: located.repo,
128
+ namespace: located.namespace,
129
+ name: located.name,
130
+ version: located.version,
131
+ dir: located.dir,
132
+ ...(url === undefined ? {} : { url }),
133
+ };
134
+ for (const definition of manifest.variables ?? []) {
135
+ defined.push({ definition, recipe });
136
+ }
137
+ }
138
+
139
+ return defined;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Builds the definition source for a project. This is the single injection
145
+ * point every `sous vars` command uses.
146
+ *
147
+ * @param settings - The merged project config.
148
+ * @param sousDir - The project's `.sous/` directory.
149
+ * @param env - The environment to read; decides where the store is.
150
+ */
151
+ export function loadProjectDefinitions(
152
+ settings: Settings,
153
+ sousDir: string,
154
+ env: NodeJS.ProcessEnv = process.env
155
+ ): VariableDefinitionSource {
156
+ return new ProjectDefinitionSource(settings, sousDir, env);
157
+ }
158
+
159
+ // --- Standalone definition files ------------------------------------------------------------------
160
+
161
+ /**
162
+ * A standalone definitions document: the same `variables:` array a recipe
163
+ * manifest carries, in a file of its own. `sous vars ask --file` reads one of
164
+ * these, which is how a project asks questions that are not published by any
165
+ * recipe yet.
166
+ *
167
+ * It is the same schema, so the same rules apply: every definition must carry a
168
+ * description and an example, and both are shown when the question is asked.
169
+ */
170
+ export const definitionsFileSchema = z.object({
171
+ /** The definitions to ask, in the recipe manifest's own shape. */
172
+ variables: z.array(variableDefinitionSchema).min(1, "must list at least one variable"),
173
+ });
174
+
175
+ /** A validated standalone definitions document. */
176
+ export type DefinitionsFile = z.infer<typeof definitionsFileSchema>;
177
+
178
+ /** The repository name recorded for definitions that came from a local file. */
179
+ export const LOCAL_FILE_REPO = "local";
180
+
181
+ /** The namespace recorded for definitions that came from a local file. */
182
+ export const LOCAL_FILE_NAMESPACE = "local";
183
+
184
+ /**
185
+ * Turns a file name into a kebab-case pseudo-recipe name, so definitions read
186
+ * from a file still have a recipe to be attributed to and still generate the
187
+ * usual scoped environment variable names.
188
+ *
189
+ * @param filePath - The definitions file's path.
190
+ */
191
+ export function pseudoRecipeName(filePath: string): string {
192
+ const base = path.basename(filePath).replace(/\.[^.]+$/, "");
193
+ const slug = base
194
+ .toLowerCase()
195
+ .replace(/[^a-z0-9]+/g, "-")
196
+ .replace(/^-+|-+$/g, "");
197
+ return slug.length === 0 || !/^[a-z]/.test(slug) ? `file-${slug}` : slug;
198
+ }
199
+
200
+ /**
201
+ * Loads a standalone definitions file (YAML or JSON, with the same permissive
202
+ * JSON dialect manifests use) and attributes every definition in it to a
203
+ * pseudo-recipe named after the file.
204
+ *
205
+ * @param filePath - Absolute path to the definitions file.
206
+ */
207
+ export function loadDefinitionsFile(filePath: string): DefinedVariable[] {
208
+ const raw = loadManifestFile(filePath);
209
+ const parsed = parseFormat(
210
+ definitionsFileSchema,
211
+ raw,
212
+ filePath,
213
+ "variable definitions file"
214
+ );
215
+ const recipe: DefiningRecipe = {
216
+ repo: LOCAL_FILE_REPO,
217
+ namespace: LOCAL_FILE_NAMESPACE,
218
+ name: pseudoRecipeName(filePath),
219
+ version: "0.0.0",
220
+ dir: filePath,
221
+ };
222
+ return parsed.variables.map((definition) => ({ definition, recipe }));
223
+ }
224
+
225
+ /** A source backed by a standalone definitions file. */
226
+ export class FileDefinitionSource implements VariableDefinitionSource {
227
+ /**
228
+ * @param filePath - Absolute path to the definitions file.
229
+ */
230
+ constructor(private readonly filePath: string) {}
231
+
232
+ /** Reads and validates the file, throwing a ConfigError when it does not fit. */
233
+ async load(): Promise<DefinedVariable[]> {
234
+ return loadDefinitionsFile(this.filePath);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * The display key for a defined variable: `namespace/recipe.variableName`.
240
+ * Sorting and lookups use it, so one variable never appears twice under two
241
+ * spellings.
242
+ *
243
+ * @param defined - The definition and the recipe that published it.
244
+ */
245
+ export function definedVariableKey(defined: DefinedVariable): string {
246
+ return `${defined.recipe.namespace}/${defined.recipe.name}.${defined.definition.name}`;
247
+ }
248
+
249
+ /** The recipe's key (`namespace/recipe`), as shown in listings. */
250
+ export function definingRecipeKey(recipe: DefiningRecipe): string {
251
+ return `${recipe.namespace}/${recipe.name}`;
252
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Shared display helpers for the `sous vars` commands: masking secrets, and the
3
+ * labeled facts block both the listing and the ask report use. The tables those
4
+ * commands print are laid out by the shared renderer in `src/utils/table.ts`.
5
+ */
6
+
7
+ import path from "node:path";
8
+ import type { VariableDefinition } from "../repos/formats/recipe-manifest.js";
9
+ import {
10
+ BULLET,
11
+ formatVariable,
12
+ VARIABLE_INDENT,
13
+ wrapColumns,
14
+ type VariableEntry,
15
+ } from "../../utils/formatting.js";
16
+ import {
17
+ definingRecipeKey,
18
+ type DefinedVariable,
19
+ type DefiningRecipe,
20
+ } from "./definition-source.js";
21
+ import { constraintBullets } from "./validate.js";
22
+
23
+ /** What the value column shows for a secret whose answer is known. */
24
+ export const HIDDEN_VALUE = "(hidden)";
25
+
26
+ /** What the value column shows for a variable nothing has answered. */
27
+ export const UNANSWERED_VALUE = "(unanswered)";
28
+
29
+ /**
30
+ * The display form of a value: a secret never prints, so that a terminal
31
+ * recording, a screen share or a scrollback buffer cannot leak one.
32
+ *
33
+ * @param value - The stored value, or undefined when there is no answer.
34
+ * @param secret - Whether the definition declared the variable a secret.
35
+ */
36
+ export function displayValue(value: string | undefined, secret: boolean): string {
37
+ if (value === undefined) return UNANSWERED_VALUE;
38
+ if (secret) return HIDDEN_VALUE;
39
+ return value;
40
+ }
41
+
42
+ /**
43
+ * The two documentation rows every command shows for a variable: the
44
+ * publisher's description, and the sample answer that makes the one-line
45
+ * question concrete. A published definition must carry both, so every caller
46
+ * can show them without checking first.
47
+ *
48
+ * @param definition - The variable definition to document.
49
+ * @returns Label-to-text rows, ready for `showVariables` or an aligned label block.
50
+ */
51
+ export function documentationRows(
52
+ definition: VariableDefinition
53
+ ): Record<string, string> {
54
+ return {
55
+ About: definition.description,
56
+ "For example": String(definition.example),
57
+ };
58
+ }
59
+
60
+ // --- The labeled facts about one variable --------------------------------------------------------
61
+
62
+ /**
63
+ * One line of a fact: the text, and any secondary detail shown after it in
64
+ * muted grey (a repository location, for instance) rather than in parentheses.
65
+ */
66
+ export interface FactLine {
67
+ /** The text shown beside the label. */
68
+ text: string;
69
+ /** The secondary detail that follows it, muted. */
70
+ detail?: string;
71
+ }
72
+
73
+ /** One labeled fact: the label, and the lines shown beside it. */
74
+ export interface LabeledFact {
75
+ /**
76
+ * The label, in the plain-word form every key and value display uses
77
+ * (`default`, `required-by`); the `@` the recipe manifests write is not part
78
+ * of it.
79
+ */
80
+ label: string;
81
+ /** The text shown beside the label, one entry per line. */
82
+ lines: Array<string | FactLine>;
83
+ }
84
+
85
+ /** True when a repository location is a URL rather than a path on this machine. */
86
+ function isHostedUrl(location: string): boolean {
87
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(location) || location.startsWith("git@");
88
+ }
89
+
90
+ /**
91
+ * Where a recipe's source lives: the repository URL with the recipe's folder
92
+ * appended for a hosted repository, the filesystem path for one read from this
93
+ * machine, and nothing at all when neither was recorded.
94
+ *
95
+ * @param recipe - The recipe to locate.
96
+ */
97
+ export function recipeLocation(recipe: DefiningRecipe): string | undefined {
98
+ if (recipe.url !== undefined && isHostedUrl(recipe.url)) {
99
+ const base = recipe.url.replace(/\/+$/, "");
100
+ return recipe.path === undefined ? base : `${base}/${recipe.path}`;
101
+ }
102
+
103
+ return recipe.url !== undefined
104
+ ? recipe.path === undefined
105
+ ? recipe.url
106
+ : path.join(recipe.url, recipe.path)
107
+ : recipe.dir;
108
+ }
109
+
110
+ /**
111
+ * A recipe written as a fact line: its key, with its location following it in
112
+ * muted grey. The location is a trailing detail rather than a parenthetical, so
113
+ * the recipe key stays the thing the eye lands on.
114
+ *
115
+ * @param recipe - The recipe to link to.
116
+ */
117
+ export function recipeLink(recipe: DefiningRecipe): FactLine {
118
+ const key = definingRecipeKey(recipe);
119
+ const location = recipeLocation(recipe);
120
+ return location === undefined ? { text: key } : { text: key, detail: location };
121
+ }
122
+
123
+ /** Everything the facts renderer needs that the definition itself does not carry. */
124
+ export interface VariableFactsInput {
125
+ /** The variable and the recipe that published it. */
126
+ defined: DefinedVariable;
127
+ /** Absolute path of the env file the answer is stored in. */
128
+ storagePath: string;
129
+ /** The environment variable name the answer is stored under. */
130
+ storedAs: string;
131
+ }
132
+
133
+ /**
134
+ * The labeled facts about one variable, in the order both the advanced view and
135
+ * `sous vars show` print them. One function builds them so the two never drift
136
+ * apart in wording or in order.
137
+ *
138
+ * @param input - The variable, where its answer is stored, and under what name.
139
+ * @returns The facts, ready for `renderFacts`.
140
+ */
141
+ export function variableFacts(input: VariableFactsInput): LabeledFact[] {
142
+ const { defined, storagePath, storedAs } = input;
143
+ const { definition } = defined;
144
+ const facts: LabeledFact[] = [];
145
+
146
+ if (definition.default !== undefined) {
147
+ facts.push({ label: "default", lines: [String(definition.default)] });
148
+ }
149
+ facts.push({ label: "example", lines: [String(definition.example)] });
150
+
151
+ const chain = defined.requiredBy ?? [defined.recipe];
152
+ const requiredBy: Array<string | FactLine> = [recipeLink(chain[0] ?? defined.recipe)];
153
+ if (chain.length > 1) {
154
+ requiredBy.push(
155
+ `pulled in through ${chain.map((recipe) => definingRecipeKey(recipe)).join(" then ")}`
156
+ );
157
+ }
158
+ facts.push({ label: "required-by", lines: requiredBy });
159
+ facts.push({ label: "defined-by", lines: [recipeLink(defined.recipe)] });
160
+ facts.push({ label: "storage-path", lines: [storagePath] });
161
+ facts.push({ label: "stored-as", lines: [storedAs] });
162
+ facts.push({
163
+ label: "constraints",
164
+ lines: constraintBullets(definition).map((bullet) => `${BULLET} ${bullet}`),
165
+ });
166
+
167
+ return facts;
168
+ }
169
+
170
+ /**
171
+ * The facts the basic view of a question shows, in the order it shows them. It
172
+ * is a subset of the same list the advanced view prints, selected by label, so
173
+ * the two views can never word a fact differently or lay it out differently.
174
+ */
175
+ export const BASIC_FACT_LABELS = [
176
+ "default",
177
+ "example",
178
+ "stored-as",
179
+ "storage-path",
180
+ ];
181
+
182
+ /**
183
+ * Picks the named facts out of a fact list, in the order the labels were given
184
+ * and skipping any the variable does not have (a variable with no default has
185
+ * no `default` fact).
186
+ *
187
+ * @param facts - Every fact about the variable.
188
+ * @param labels - The labels to keep, in the order they should be shown.
189
+ */
190
+ export function selectFacts(facts: LabeledFact[], labels: string[]): LabeledFact[] {
191
+ const byLabel = new Map(facts.map((fact) => [fact.label, fact]));
192
+ return labels
193
+ .map((label) => byLabel.get(label))
194
+ .filter((fact): fact is LabeledFact => fact !== undefined);
195
+ }
196
+
197
+ /**
198
+ * How far a rendered facts block is indented under the text above it. It is the
199
+ * same depth as every other key and value block, so a facts block never reads
200
+ * as a different kind of list.
201
+ */
202
+ export const FACTS_INDENT = VARIABLE_INDENT;
203
+
204
+ /**
205
+ * Lays the labeled facts out through the one key and value renderer, so a fact
206
+ * about a variable looks exactly like every other key and value sous prints:
207
+ * labels aligned, colons lined up, values in the value color, and a location
208
+ * trailing in muted grey.
209
+ *
210
+ * @param facts - The facts to render.
211
+ * @param width - The column to wrap at, indentation included.
212
+ * @returns The rendered lines, colored for a terminal, indented by `FACTS_INDENT`.
213
+ */
214
+ export function renderFacts(facts: LabeledFact[], width = wrapColumns()): string[] {
215
+ const labelWidth = Math.max(...facts.map((fact) => fact.label.length));
216
+ const lines: string[] = [];
217
+
218
+ for (const fact of facts) {
219
+ fact.lines.forEach((raw, index) => {
220
+ const line: FactLine = typeof raw === "string" ? { text: raw } : raw;
221
+ const entry: VariableEntry = {
222
+ // A fact needing more than one line labels only the first of them; the
223
+ // rest continue underneath it.
224
+ label: index === 0 ? fact.label : "",
225
+ value: line.text,
226
+ ...(line.detail === undefined ? {} : { detail: line.detail }),
227
+ };
228
+ lines.push(...formatVariable(entry, { indent: FACTS_INDENT, labelWidth, width }));
229
+ });
230
+ }
231
+
232
+ return lines;
233
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The variables layer: where variable definitions come from, how a stored
3
+ * answer is found, how an answer is validated, and how a question is asked.
4
+ *
5
+ * Import from here rather than from the individual modules, so the commands and
6
+ * later phases have one place to look for what this layer offers.
7
+ */
8
+
9
+ export * from "./definition-source.js";
10
+ export * from "./display.js";
11
+ export * from "./ladder.js";
12
+ export * from "./mappings.js";
13
+ export * from "./names.js";
14
+ export * from "./validate.js";
15
+ export * from "./ask.js";
16
+ export * from "./report.js";
17
+ export * from "./preanswers.js";
18
+ export * from "./question-plan.js";