@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,312 +0,0 @@
1
- /**
2
- * Validating an answer against the definition that asked for it.
3
- *
4
- * A recipe declares a small, deliberately boring constraint vocabulary (a type,
5
- * plus `pattern`, `minLength`, `maxLength`, `min`, `max` and `enum`), all of it
6
- * expressible in plain JSON so a manifest never has to carry code. This module
7
- * turns that vocabulary into a zod schema and reports a failure in the same
8
- * plain language the question was asked in, naming the constraint that was
9
- * violated rather than dumping a schema error.
10
- *
11
- * Answers are stored as text in env files, so validation always starts from a
12
- * string and hands back the coerced value.
13
- *
14
- * A `pattern` comes from a recipe, which is code from somewhere else, so it is
15
- * never run on this thread without a limit; see `safe-regex.ts`. A pattern that
16
- * exceeds its budget fails validation, and the failure says so plainly rather
17
- * than telling the person that their answer is wrong.
18
- */
19
-
20
- import { z } from "zod";
21
- import type { VariableDefinition } from "../repos/formats/recipe-manifest.js";
22
- import { DEFAULT_PATTERN_BUDGET_MS, matchWithBudget } from "./safe-regex.js";
23
-
24
- /** The value an answer coerces to, once it has been validated. */
25
- export type AnswerValue = string | number | boolean;
26
-
27
- /** What the caller knows about where a definition came from, for messages. */
28
- export interface ValidationContext {
29
- /**
30
- * The recipe that published the definition, named the way it should read in a
31
- * message (for example 'acme/web-app'). Omitted when the caller does not know
32
- * it, in which case messages describe it in words instead.
33
- */
34
- recipe?: string;
35
- /** How long a declared pattern may run, in milliseconds. */
36
- patternBudgetMs?: number;
37
- }
38
-
39
- /** A validated answer, or the reason it was refused. */
40
- export type AnswerValidation =
41
- | { ok: true; value: AnswerValue }
42
- | { ok: false; message: string };
43
-
44
- /** The strings accepted as a true answer for a boolean variable. */
45
- const TRUE_WORDS = new Set(["true", "yes", "y", "on", "1"]);
46
-
47
- /** The strings accepted as a false answer for a boolean variable. */
48
- const FALSE_WORDS = new Set(["false", "no", "n", "off", "0"]);
49
-
50
- /**
51
- * Builds the zod schema for one definition. The schema's input is the trimmed
52
- * answer text and its output is the coerced value, so callers get the number or
53
- * boolean a `number` or `boolean` variable promised.
54
- *
55
- * @param definition - The variable definition to build a schema for.
56
- * @param context - What is known about the recipe, for the pattern message.
57
- */
58
- export function validationSchemaFor(
59
- definition: VariableDefinition,
60
- context: ValidationContext = {}
61
- ): z.ZodType<AnswerValue> {
62
- const rules = definition.validate;
63
-
64
- switch (definition.type) {
65
- case "number": {
66
- const schema = z
67
- .string()
68
- .refine((value) => value.length > 0 && Number.isFinite(Number(value)), {
69
- message: "must be a number",
70
- })
71
- .transform((value) => Number(value))
72
- .pipe(numberRules());
73
- return schema as unknown as z.ZodType<AnswerValue>;
74
- }
75
-
76
- case "boolean": {
77
- const schema = z
78
- .string()
79
- .refine((value) => TRUE_WORDS.has(value.toLowerCase()) || FALSE_WORDS.has(value.toLowerCase()), {
80
- message: "must be 'true' or 'false'",
81
- })
82
- .transform((value) => TRUE_WORDS.has(value.toLowerCase()));
83
- return schema as unknown as z.ZodType<AnswerValue>;
84
- }
85
-
86
- case "enum": {
87
- const options = rules?.enum ?? [];
88
- const schema = z.string().refine((value) => options.includes(value), {
89
- message: `must be one of: ${options.join(", ")}`,
90
- });
91
- return stringRules(schema) as unknown as z.ZodType<AnswerValue>;
92
- }
93
-
94
- case "url": {
95
- const schema = z.string().refine(
96
- (value) => {
97
- try {
98
- const parsed = new URL(value);
99
- return parsed.protocol.length > 1;
100
- } catch {
101
- return false;
102
- }
103
- },
104
- { message: "must be a URL, including its scheme (for example 'https://example.com')" }
105
- );
106
- return stringRules(schema) as unknown as z.ZodType<AnswerValue>;
107
- }
108
-
109
- case "path": {
110
- const schema = z.string().refine((value) => !value.includes("\0"), {
111
- message: "must be a filesystem path, with no null characters in it",
112
- });
113
- return stringRules(schema) as unknown as z.ZodType<AnswerValue>;
114
- }
115
-
116
- case "string":
117
- default:
118
- return stringRules(z.string()) as unknown as z.ZodType<AnswerValue>;
119
- }
120
-
121
- /** Applies the numeric constraints to a number schema. */
122
- function numberRules(): z.ZodType<number, number> {
123
- let schema = z.number();
124
- if (rules?.min !== undefined) schema = schema.min(rules.min, `must be at least ${rules.min}`);
125
- if (rules?.max !== undefined) schema = schema.max(rules.max, `must be at most ${rules.max}`);
126
- return schema;
127
- }
128
-
129
- /** Applies the string constraints, including the required-means-non-empty rule. */
130
- function stringRules(base: z.ZodType<string>): z.ZodType<string> {
131
- let schema: z.ZodType<string> = base;
132
-
133
- if (definition.required) {
134
- schema = schema.refine((value) => value.length > 0, { message: "must not be empty" });
135
- }
136
- if (rules?.minLength !== undefined) {
137
- schema = schema.refine((value) => value.length >= rules.minLength!, {
138
- message: `must be at least ${rules.minLength} characters long`,
139
- });
140
- }
141
- if (rules?.maxLength !== undefined) {
142
- schema = schema.refine((value) => value.length <= rules.maxLength!, {
143
- message: `must be at most ${rules.maxLength} characters long`,
144
- });
145
- }
146
- if (rules?.pattern !== undefined) {
147
- const pattern = rules.pattern;
148
- const budget = context.patternBudgetMs ?? DEFAULT_PATTERN_BUDGET_MS;
149
- schema = schema.superRefine((value, ctx) => {
150
- const outcome = matchWithBudget(pattern, value, budget);
151
- if (outcome === "match") return;
152
- ctx.addIssue({
153
- code: "custom",
154
- message:
155
- outcome === "timeout"
156
- ? patternTooSlowMessage(pattern, budget, context.recipe)
157
- : `must match the pattern ${pattern}`,
158
- });
159
- });
160
- }
161
-
162
- return schema;
163
- }
164
- }
165
-
166
- /**
167
- * The failure for a pattern that ran out of time. It names the pattern and the
168
- * recipe that published it, and says plainly that the pattern is the problem;
169
- * the person answering has no way to write an answer that a runaway pattern
170
- * would finish on.
171
- *
172
- * @param pattern - The regular expression source the recipe declared.
173
- * @param budgetMs - The budget it exceeded, in milliseconds.
174
- * @param recipe - The recipe that published it, when the caller knows it.
175
- */
176
- function patternTooSlowMessage(
177
- pattern: string,
178
- budgetMs: number,
179
- recipe?: string
180
- ): string {
181
- const publisher = recipe === undefined ? "the recipe that defines it" : `the recipe ${recipe}`;
182
- return (
183
- `could not be checked: the pattern ${pattern}, published by ${publisher}, ` +
184
- `took longer than ${budgetMs} milliseconds to run, so sous stopped waiting ` +
185
- "for it. The pattern is too slow to run, and the answer was not the problem; " +
186
- "this needs to be reported to whoever publishes the recipe"
187
- );
188
- }
189
-
190
- /**
191
- * Validates one answer against its definition.
192
- *
193
- * @param definition - The variable definition the answer is for.
194
- * @param raw - The answer as text, as typed or as read from an env file.
195
- * @param context - What is known about the recipe, for the pattern message.
196
- * @returns The coerced value, or a plain-language message naming what is wrong.
197
- */
198
- export function validateAnswer(
199
- definition: VariableDefinition,
200
- raw: string,
201
- context: ValidationContext = {}
202
- ): AnswerValidation {
203
- const trimmed = typeof raw === "string" ? raw.trim() : "";
204
-
205
- if (trimmed.length === 0 && !definition.required) {
206
- return { ok: true, value: "" };
207
- }
208
-
209
- const result = validationSchemaFor(definition, context).safeParse(trimmed);
210
- if (result.success) return { ok: true, value: result.data };
211
-
212
- const first = result.error.issues[0];
213
- const reason = first?.message ?? "is not valid";
214
- return { ok: false, message: `${definition.name} ${reason}.` };
215
- }
216
-
217
- /**
218
- * The form an answer is STORED in. Env files hold text, so a coerced number or
219
- * boolean goes back to its canonical string.
220
- *
221
- * @param value - The coerced answer.
222
- */
223
- export function storedForm(value: AnswerValue): string {
224
- return typeof value === "boolean" ? String(value) : String(value);
225
- }
226
-
227
- /**
228
- * A short, plain-language summary of a definition's constraints, shown beside
229
- * the question and by `sous vars <name>`. Returns an empty array when the
230
- * definition constrains nothing beyond its type.
231
- *
232
- * @param definition - The variable definition.
233
- */
234
- export function constraintHints(definition: VariableDefinition): string[] {
235
- const hints: string[] = [];
236
- const rules = definition.validate;
237
-
238
- hints.push(`type: ${definition.type}`);
239
- if (definition.type === "enum" && rules?.enum !== undefined) {
240
- hints.push(`one of: ${rules.enum.join(", ")}`);
241
- }
242
- if (rules?.minLength !== undefined) hints.push(`at least ${rules.minLength} characters`);
243
- if (rules?.maxLength !== undefined) hints.push(`at most ${rules.maxLength} characters`);
244
- if (rules?.min !== undefined) hints.push(`no less than ${rules.min}`);
245
- if (rules?.max !== undefined) hints.push(`no more than ${rules.max}`);
246
- if (rules?.pattern !== undefined) hints.push(`matching ${rules.pattern}`);
247
- if (!definition.required) hints.push("optional");
248
-
249
- return hints;
250
- }
251
-
252
- /**
253
- * A character count with the noun in the right number.
254
- *
255
- * @param count - How many characters.
256
- */
257
- function characters(count: number): string {
258
- return count === 1 ? "1 character" : `${count} characters`;
259
- }
260
-
261
- /**
262
- * One sentence per constraint, in plain words with the raw form the manifest
263
- * declared in parentheses, so a reader can both understand the rule and find it
264
- * in the recipe that published it. This is what the advanced view and
265
- * `sous vars show` list under `@constraints`.
266
- *
267
- * @param definition - The variable definition.
268
- * @returns One sentence per constraint, type first.
269
- *
270
- * @example
271
- * constraintBullets(definition);
272
- * // -> ["must be a value of the type url (type: url)", "must match the pattern /^https/ (pattern: ^https)"]
273
- */
274
- export function constraintBullets(definition: VariableDefinition): string[] {
275
- const rules = definition.validate;
276
- const options = rules?.enum;
277
-
278
- // The type sentence never puts an article in front of the type name, because
279
- // the article would have to change with the name ('a path', but 'an enum').
280
- // An enum is described by the options it allows, which says more than the
281
- // word 'enum' does and reads as one sentence rather than two.
282
- const bullets: string[] =
283
- definition.type === "enum" && options !== undefined
284
- ? [`must be one of: ${options.join(", ")} (type: enum)`]
285
- : [`must be a value of the type ${definition.type} (type: ${definition.type})`];
286
-
287
- if (options !== undefined && definition.type !== "enum") {
288
- bullets.push(`must be one of: ${options.join(", ")} (enum: ${options.join(", ")})`);
289
- }
290
- if (rules?.minLength !== undefined) {
291
- bullets.push(
292
- `must be at least ${characters(rules.minLength)} long (minLength: ${rules.minLength})`
293
- );
294
- }
295
- if (rules?.maxLength !== undefined) {
296
- bullets.push(
297
- `must be at most ${characters(rules.maxLength)} long (maxLength: ${rules.maxLength})`
298
- );
299
- }
300
- if (rules?.min !== undefined) {
301
- bullets.push(`must be no less than ${rules.min} (min: ${rules.min})`);
302
- }
303
- if (rules?.max !== undefined) {
304
- bullets.push(`must be no more than ${rules.max} (max: ${rules.max})`);
305
- }
306
- if (rules?.pattern !== undefined) {
307
- bullets.push(`must match the pattern /${rules.pattern}/ (pattern: ${rules.pattern})`);
308
- }
309
- if (!definition.required) bullets.push("an answer is optional");
310
-
311
- return bullets;
312
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * What `sous --version` prints.
3
- *
4
- * oclif answers `--version` with its user agent string (package, version,
5
- * platform and Node build on one line). Sous answers it itself, in bin/run.js,
6
- * before oclif is loaded: the version alone, or with `--verbose` the same facts
7
- * as a key and value list, so a person asking "which sous is this?" gets one
8
- * short line and a person asking "where is it?" gets the rest.
9
- */
10
-
11
- import fs from "node:fs";
12
- import os from "node:os";
13
- import { log, showVariables } from "../utils/formatting.js";
14
-
15
- /** The one flag oclif treats as a version request, and so does sous. */
16
- export const VERSION_FLAG = "--version";
17
-
18
- /** The flag that adds where the install is, the platform and the Node build. */
19
- export const VERBOSE_FLAG = "--verbose";
20
-
21
- /** The facts a version report is made of. */
22
- export type VersionFacts = {
23
- /** The npm package name. */
24
- name: string;
25
- /** The package version. */
26
- version: string;
27
- /** The absolute path of the install answering. */
28
- root: string;
29
- /** The platform and architecture, the way oclif spells them. */
30
- platform: string;
31
- /** The Node build, with its leading `v`. */
32
- node: string;
33
- };
34
-
35
- /** Whether the command line is a version request: `--version` as its first word. */
36
- export function isVersionRequest(argv: readonly string[]): boolean {
37
- return argv[0] === VERSION_FLAG;
38
- }
39
-
40
- /** Reads the facts about the install rooted at `packageRoot`. */
41
- export function readVersionFacts(packageRoot: string): VersionFacts {
42
- const pkg = JSON.parse(fs.readFileSync(`${packageRoot}/package.json`, "utf8")) as {
43
- name?: unknown;
44
- version?: unknown;
45
- };
46
- return {
47
- name: typeof pkg.name === "string" ? pkg.name : "unknown",
48
- version: typeof pkg.version === "string" ? pkg.version : "unknown",
49
- root: packageRoot,
50
- platform: `${os.platform()}-${os.arch()}`,
51
- node: process.version,
52
- };
53
- }
54
-
55
- /**
56
- * Prints the report: the version on its own line, and with `verbose` the
57
- * facts under it as a key and value list.
58
- *
59
- * @param facts - What to report.
60
- * @param verbose - Whether to print the facts under the version.
61
- * @param write - Where each line goes; standard output by default.
62
- */
63
- export function printVersionReport(
64
- facts: VersionFacts,
65
- verbose: boolean,
66
- write: (line: string) => void = log
67
- ): void {
68
- write(`v${facts.version}`);
69
- if (!verbose) return;
70
- showVariables(
71
- {
72
- Package: facts.name,
73
- Install: facts.root,
74
- Platform: facts.platform,
75
- Node: facts.node,
76
- },
77
- { write }
78
- );
79
- }
80
-
81
- /**
82
- * Answers a version request from the command line: `sous --version`, or
83
- * `sous --version --verbose`.
84
- *
85
- * @param argv - The command line after the program name.
86
- * @param packageRoot - The root of the install answering.
87
- */
88
- export function printVersion(argv: readonly string[], packageRoot: string): void {
89
- printVersionReport(readVersionFacts(packageRoot), argv.includes(VERBOSE_FLAG));
90
- }
@@ -1,148 +0,0 @@
1
- import path from "node:path";
2
- import {
3
- CLI_ROOT,
4
- resolveRootScope,
5
- resolveWatchConfig,
6
- type ConfigContext,
7
- type Settings,
8
- type WatchConfig,
9
- } from "./settings.js";
10
- import type { WatchHandle, WatchService } from "./watch-service.js";
11
- import { readEffectiveLinks } from "./repos/links.js";
12
- import { log } from "../utils/formatting.js";
13
-
14
- /**
15
- * Builds a WatchConfig from current settings, injecting the primary config
16
- * file, the conf.d/ drop-in DIRECTORY, the templating directory and every linked
17
- * repository's working copy into fullRebuildPaths. Watching a directory (not
18
- * each file inside it) covers files appearing, changing, or disappearing at
19
- * runtime, since full-rebuild matching is exact-or-directory-prefix.
20
- *
21
- * A linked repository is watched because that is the whole point of a link: the
22
- * recipes are being edited right now, in that checkout, and a watch that ignored
23
- * them would make the link useless.
24
- *
25
- * Shared by `build --watch` and `compile --watch` so both react to config,
26
- * template and linked-recipe edits identically.
27
- */
28
- export function buildReloadWatchConfig(
29
- settings: Settings,
30
- configContext: ConfigContext
31
- ): WatchConfig {
32
- const rootScope = resolveRootScope(settings, configContext);
33
- const config = resolveWatchConfig(settings, rootScope);
34
- const links = readEffectiveLinks(configContext.sousDir);
35
- const reloadPaths = [
36
- configContext.configPath,
37
- configContext.confDir,
38
- path.join(CLI_ROOT, "src", "templating"),
39
- ...Object.values(links).map((link) => link.path),
40
- ].filter((p): p is string => typeof p === "string");
41
- config.fullRebuildPaths = [...(config.fullRebuildPaths ?? []), ...reloadPaths];
42
- return config;
43
- }
44
-
45
- /** Options for {@link startConfigReloadWatch}. */
46
- export type ConfigReloadWatchOptions = {
47
- /** The watcher factory used to (re)start chokidar. */
48
- watchService: WatchService;
49
- /**
50
- * Produces a fresh WatchConfig from the CURRENT settings each time a watcher
51
- * is (re)started; typically `() => buildReloadWatchConfig(this.settings, this.configContext)`.
52
- */
53
- buildWatchConfig: () => WatchConfig;
54
- /**
55
- * Performs the actual work (build/compile) using the command's CURRENT
56
- * settings. Called for partial rebuilds (with the changed file) and, after a
57
- * successful reload, for full rebuilds (no argument). Owns its own
58
- * heading/footer output.
59
- */
60
- rebuild: (changedFile?: string) => Promise<void>;
61
- /**
62
- * Re-runs discovery + settings load and commits the result onto the command,
63
- * but only if it loads cleanly (last-good semantics). Throws on failure.
64
- */
65
- reloadConfig: () => Promise<void>;
66
- };
67
-
68
- /** The running watch loop; exposes the live handle and a manual full-rebuild trigger. */
69
- export type ConfigReloadWatchController = {
70
- /** Mutable reference to the live watcher handle (swapped on every restart). */
71
- handle: { current: WatchHandle | null };
72
- /** Triggers a full rebuild immediately, bypassing the debounce (e.g. a keypress). */
73
- triggerFullRebuild: (reason: string) => Promise<void>;
74
- };
75
-
76
- /**
77
- * Runs the shared watch loop used by `build --watch` and `compile --watch`.
78
- *
79
- * - Partial events (a watched source file changed) call `rebuild(filePath)`.
80
- * - Full events (config file, conf.d directory, or templating dir changed) stop
81
- * the watcher, reload settings via `reloadConfig`, rebuild, and restart the
82
- * watcher. A failed reload is reported without wedging the session: the
83
- * last-good config stays in place and the watcher restarts so the next edit
84
- * can recover.
85
- *
86
- * A single `isRebuilding` guard serialises overlapping triggers.
87
- */
88
- export function startConfigReloadWatch(
89
- options: ConfigReloadWatchOptions
90
- ): ConfigReloadWatchController {
91
- const { watchService, buildWatchConfig, rebuild, reloadConfig } = options;
92
-
93
- let isRebuilding = false;
94
- const handle: { current: WatchHandle | null } = { current: null };
95
-
96
- const startWatcher = () => {
97
- const watchConfig = buildWatchConfig();
98
- handle.current = watchService.watch(watchConfig, async (event) => {
99
- if (event.type === "partial") {
100
- if (isRebuilding) return;
101
- isRebuilding = true;
102
- log(`\nChange detected: ${event.filePath}`);
103
- await rebuild(event.filePath);
104
- isRebuilding = false;
105
- } else {
106
- // Full rebuild: stop current watcher, reload settings, restart
107
- if (isRebuilding) return;
108
- isRebuilding = true;
109
- log(`\nConfig changed (${event.filePath}), reloading settings and restarting watcher...`);
110
- await handle.current!.stop();
111
-
112
- try {
113
- // Re-run discovery: conf.d layer files can appear or disappear while
114
- // watching, so the ordered layer list must be rebuilt (and the
115
- // duplicate-baseName check re-run) before reloading settings. Only
116
- // committed by reloadConfig once it loads cleanly.
117
- await reloadConfig();
118
- await rebuild();
119
- } catch (error) {
120
- // A broken config edit (bad JSON/JS, colliding conf.d baseNames,
121
- // configure() throw, etc.) must not wedge the session: report it,
122
- // keep the last-good config, and fall through to restart the watcher
123
- // so the next edit can recover.
124
- log(
125
- `\nConfig reload failed; keeping the last-good config. Fix the config and save again to retry.\n ${
126
- error instanceof Error ? error.message : String(error)
127
- }`
128
- );
129
- } finally {
130
- isRebuilding = false;
131
- startWatcher();
132
- }
133
- }
134
- });
135
- };
136
-
137
- startWatcher();
138
-
139
- const triggerFullRebuild = async (reason: string) => {
140
- if (isRebuilding) return;
141
- isRebuilding = true;
142
- log(`\n${reason}`);
143
- await rebuild();
144
- isRebuilding = false;
145
- };
146
-
147
- return { handle, triggerFullRebuild };
148
- }
@@ -1,116 +0,0 @@
1
- import chokidar from "chokidar";
2
- import { minimatch } from "minimatch";
3
- import path from "node:path";
4
- import type { WatchConfig } from "./settings.js";
5
- import { toGlobPattern } from "./glob-pattern.js";
6
- import { log } from "../utils/formatting.js";
7
-
8
- /** Milliseconds to wait after a change before triggering a rebuild. */
9
- const DEBOUNCE_MS = 300;
10
-
11
- /** A partial rebuild triggered by a change to a specific source file. */
12
- export type PartialRebuildEvent = {
13
- type: "partial";
14
- filePath: string;
15
- };
16
-
17
- /** A full rebuild triggered by a change to a high-impact path (config, templating, etc.). */
18
- export type FullRebuildEvent = {
19
- type: "full";
20
- reason: string;
21
- filePath: string;
22
- };
23
-
24
- export type WatchEvent = PartialRebuildEvent | FullRebuildEvent;
25
-
26
- /** A handle returned by WatchService.watch() that allows the caller to stop the watcher. */
27
- export type WatchHandle = {
28
- stop(): Promise<void>;
29
- };
30
-
31
- /**
32
- * Extracts the longest leading non-glob segment of a pattern as a watchable
33
- * directory path. Chokidar v5 does not accept glob patterns directly.
34
- *
35
- * Examples:
36
- * "/foo/bar/**\/*" → "/foo/bar"
37
- * "/foo/bar/*.md" → "/foo/bar"
38
- */
39
- function globBaseDir(pattern: string): string {
40
- const starIndex = pattern.indexOf("*");
41
- if (starIndex === -1) return path.dirname(pattern);
42
- const prefix = pattern.slice(0, starIndex);
43
- return prefix.endsWith("/") || prefix.endsWith(path.sep)
44
- ? prefix.slice(0, -1)
45
- : path.dirname(prefix);
46
- }
47
-
48
- /**
49
- * Watches exact files, glob-backed directories, and full-rebuild paths,
50
- * calling the provided callback (debounced) when a relevant change is detected.
51
- *
52
- * - Changes to `files` or `globs` entries fire a partial rebuild event.
53
- * - Changes to `fullRebuildPaths` entries fire a full rebuild event.
54
- *
55
- * Returns a WatchHandle with a `stop()` method to close the watcher.
56
- */
57
- export class WatchService {
58
- watch(config: WatchConfig, onChange: (event: WatchEvent) => Promise<void>): WatchHandle {
59
- const { files, globs, fullRebuildPaths = [] } = config;
60
- const globBaseDirs = globs.map(globBaseDir);
61
- const watchPaths = [...new Set([...files, ...globBaseDirs, ...fullRebuildPaths])];
62
-
63
- const watcher = chokidar.watch(watchPaths, {
64
- ignoreInitial: true,
65
- persistent: true,
66
- ignored: /sous\.state\.json$/,
67
- });
68
-
69
- let debounceTimer: NodeJS.Timeout | null = null;
70
- let pendingEvent: WatchEvent | null = null;
71
-
72
- const schedule = (event: WatchEvent) => {
73
- pendingEvent = event;
74
- if (debounceTimer) clearTimeout(debounceTimer);
75
- debounceTimer = setTimeout(async () => {
76
- debounceTimer = null;
77
- const evt = pendingEvent!;
78
- pendingEvent = null;
79
- try {
80
- await onChange(evt);
81
- } catch (err) {
82
- const message = err instanceof Error ? err.message : String(err);
83
- log(` Watch error: ${message}`);
84
- }
85
- }, DEBOUNCE_MS);
86
- };
87
-
88
- watcher.on("all", (event, filePath) => {
89
- if (!["add", "change", "unlink"].includes(event)) return;
90
-
91
- // Full-rebuild paths take priority
92
- const isFullRebuildPath = fullRebuildPaths.some(p =>
93
- filePath === p || filePath.startsWith(p + path.sep) || filePath.startsWith(p + "/")
94
- );
95
-
96
- if (isFullRebuildPath) {
97
- schedule({ type: "full", reason: filePath, filePath });
98
- return;
99
- }
100
-
101
- const isExactFile = files.includes(filePath);
102
- const matchesGlob = globs.some(g => minimatch(filePath, toGlobPattern(g)));
103
-
104
- if (isExactFile || matchesGlob) {
105
- schedule({ type: "partial", filePath });
106
- }
107
- });
108
-
109
- const totalPatterns = files.length + globs.length + fullRebuildPaths.length;
110
- log(`\nWatching ${totalPatterns} pattern(s) for changes...\n`);
111
-
112
- return {
113
- stop: () => watcher.close(),
114
- };
115
- }
116
- }