@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,849 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { color } from "@oclif/color";
3
-
4
- const HEADER_LINES = [
5
- " ▄█████ ▄████▄ ██ ██ ▄█████ ",
6
- " ▀▀▀▄▄▄ ██ ██ ██ ██ ▀▀▀▄▄▄ ",
7
- " █████▀ ▀████▀ ▀████▀ █████▀ ",
8
- ];
9
-
10
- // --- The Color Palette ---------------------------------------------------------------------------
11
-
12
- /**
13
- * Every color sous writes comes from this one place, so a reader learns the
14
- * vocabulary once: a label is cyan, the value beside it is bright white, an
15
- * aside about that value is grey, a warning is bright yellow with its sharpest
16
- * words in orange, an explanation is bright teal, and an error is bright red.
17
- *
18
- * The orange is given as a hex value on purpose. A terminal that can show it
19
- * does; one limited to sixteen colors has chalk fold it down to bright yellow,
20
- * which keeps a highlighted word inside the warning's own color rather than
21
- * turning it into something that reads as a second kind of message.
22
- */
23
- export const palette = {
24
- /** The name in a key and value pair. */
25
- label: (text: string): string => color.cyan(text),
26
- /** The value beside a label. */
27
- value: (text: string): string => color.whiteBright(text),
28
- /** A trailing aside: a location, a provenance, anything secondary. */
29
- muted: (text: string): string => color.gray(text),
30
- /** The body of a warning. */
31
- warning: (text: string): string => color.yellowBright(text),
32
- /** The words inside a warning that carry the actual risk. */
33
- highlight: (text: string): string => color.hex("#ff8800")(text),
34
- /** An explanatory line that is neither a warning nor an error. */
35
- note: (text: string): string => color.cyanBright(text),
36
- /** The body of an error. */
37
- error: (text: string): string => color.redBright(text),
38
- };
39
-
40
- /**
41
- * The prefix every error message carries, so an error is still findable by
42
- * eye or by grep when the output has no color at all (a pipe, a log file,
43
- * `CI=true`).
44
- */
45
- export const ERROR_PREFIX = "Error: ";
46
-
47
- /**
48
- * Adds the error prefix to a message that does not already begin with one, so
49
- * a message that says "Error:" itself is never made to say it twice.
50
- *
51
- * @param text - The first line of an error message.
52
- */
53
- export function withErrorPrefix(text: string): string {
54
- return /^error:\s/i.test(text.trimStart()) ? text : `${ERROR_PREFIX}${text}`;
55
- }
56
-
57
-
58
- // --- Core Output Functions -----------------------------------------------------------------------
59
-
60
- /**
61
- * Writes a string of text to the console, with a newline.
62
- */
63
- export function log(text: string): void {
64
- console.log(text);
65
- }
66
-
67
- /**
68
- * Writes a string of text to the console. Concurrent calls to this
69
- * function will replace the text of the previous call, making this
70
- * function useful for things like progress indicators.
71
- */
72
- export function logPersistent(text: string): void {
73
- process.stdout.write(`\r${text}`);
74
- }
75
-
76
- // --- Whitespace Helpers --------------------------------------------------------------------------
77
-
78
- /**
79
- * Writes a blank line to the console.
80
- * Note that we output a space character to ensure that the line is not
81
- * completely empty, which some terminal emulators will ignore.
82
- */
83
- export function blankLine(): void {
84
- log(" ");
85
- }
86
-
87
- /**
88
- * Writes multiple blank lines to the console.
89
- */
90
- export function blankLines(blankLineCount = 2): void {
91
- for (let i = 0; i < blankLineCount; i++) {
92
- blankLine();
93
- }
94
- }
95
-
96
- // --- String Builders -----------------------------------------------------------------------------
97
-
98
- /**
99
- * Applies a prefix symbol to a string of text. If `prefixSymbol` is
100
- * an empty string, the text is returned verbatim.
101
- */
102
- function applyPrefixSymbol(text: string, prefixSymbol = "➔"): string {
103
- const prefix = prefixSymbol ? `${prefixSymbol} ` : "";
104
- return `${prefix}${text}`;
105
- }
106
-
107
- /**
108
- * Formats a heading text string. If the heading does not end with a
109
- * period, a colon is appended.
110
- */
111
- function formatHeadingText(heading: string): string {
112
- const append = heading.endsWith(".") ? "" : ":";
113
- return `${heading}${append}`;
114
- }
115
-
116
- /**
117
- * Indents a block of text by a specified number of spaces.
118
- */
119
- export function indent(text: string, count = 2, char = " "): string {
120
- return text
121
- .split("\n")
122
- .map(line => char.repeat(count) + line)
123
- .join("\n");
124
- }
125
-
126
- // --- Header & Footer -----------------------------------------------------------------------------
127
-
128
- /**
129
- * Writes the CLI header using the given line writer.
130
- *
131
- * Factored out so commands whose stdout must stay machine-readable (the
132
- * `sous config *` commands, which emit JSON) can route the decorative banner to
133
- * stderr instead — see ConfigCommand.emitHeader.
134
- *
135
- * @param write - Receives one already-formatted line at a time (no trailing newline).
136
- */
137
- export function headerTo(write: (line: string) => void): void {
138
- write(" ");
139
- write(" ");
140
- write(color.cyan(HEADER_LINES.join("\n")));
141
- write(" Agent Configuration Manager ");
142
- write(" ");
143
- }
144
-
145
- /**
146
- * How many commands are running inside another one right now. While any are,
147
- * `header` writes nothing, because the command that started them has already
148
- * drawn the banner once.
149
- */
150
- let nestedCommandDepth = 0;
151
-
152
- /**
153
- * Writes the CLI header to stdout, unless the command asking for it runs
154
- * inside another command (see `withoutHeader`).
155
- */
156
- export function header(): void {
157
- if (nestedCommandDepth > 0) return;
158
- headerTo(log);
159
- }
160
-
161
- /**
162
- * Runs another command in this process without drawing the CLI header a
163
- * second time. A command that chains others (`sous repo contribute`) wraps each
164
- * one in this, so the output opens with one banner however many run.
165
- *
166
- * @param run - Runs the nested command.
167
- * @returns Whatever `run` resolves to.
168
- */
169
- export async function withoutHeader<T>(run: () => Promise<T>): Promise<T> {
170
- nestedCommandDepth += 1;
171
- try {
172
- return await run();
173
- } finally {
174
- nestedCommandDepth -= 1;
175
- }
176
- }
177
-
178
- /**
179
- * Writes the CLI footer to the console.
180
- */
181
- export function footer(): void {
182
- blankLines(2);
183
- }
184
-
185
- // --- Headings & Subheadings ----------------------------------------------------------------------
186
-
187
- /**
188
- * Writes a heading to the console.
189
- *
190
- * @example
191
- * heading("Compiling targets");
192
- * // ▶ Compiling targets:
193
- */
194
- export function heading(text: string, prefixSymbol = "▶"): void {
195
- const formatted = formatHeadingText(text);
196
- const final = applyPrefixSymbol(formatted, prefixSymbol);
197
- blankLine();
198
- log(color.yellowBright(final));
199
- }
200
-
201
- /**
202
- * Writes a section heading followed by a blank line, which is how a command
203
- * opens a section of its output.
204
- *
205
- * `heading` on its own leaves the first line of content pressed right up under
206
- * the heading, which reads as one crowded block. Every command that lays out
207
- * sections of prose, tables or variable lists uses this instead, so the spacing
208
- * is decided in one place rather than by a `blankLine()` call remembered at
209
- * each site.
210
- *
211
- * @param text - The heading text; a colon is appended unless it ends in a period.
212
- * @param prefixSymbol - The marker drawn before the text.
213
- *
214
- * @example
215
- * section("Adding a repository");
216
- * // ▶ Adding a repository:
217
- * // (blank line)
218
- */
219
- export function section(text: string, prefixSymbol = "▶"): void {
220
- heading(text, prefixSymbol);
221
- blankLine();
222
- }
223
-
224
- /**
225
- * Writes a subheading to the console.
226
- *
227
- * @example
228
- * subheading("Target 1");
229
- * // ➔ Target 1:
230
- */
231
- export function subheading(text: string, prefixSymbol = "➔"): void {
232
- const formatted = formatHeadingText(text);
233
- const final = applyPrefixSymbol(formatted, prefixSymbol);
234
- blankLine();
235
- log(color.whiteBright(final));
236
- }
237
-
238
- // --- Data Display Functions ----------------------------------------------------------------------
239
-
240
- /**
241
- * Dumps the contents of an object to the console.
242
- */
243
- export function dump<T>(obj: T, headingText?: string): void {
244
- if (headingText) {
245
- subheading(headingText);
246
- }
247
- const normalized =
248
- typeof obj === "object" && obj !== null
249
- ? (sortObjectKeys(obj as Record<string, any>) as T)
250
- : obj;
251
- console.dir(normalized);
252
- }
253
-
254
- /**
255
- * Displays a count of something.
256
- *
257
- * @example
258
- * showCount(5, "tokens");
259
- * // Found [5] tokens
260
- */
261
- export function showCount(count: number, entity = "items", headingText = "", verb = "Found"): void {
262
- if (headingText) {
263
- subheading(headingText);
264
- }
265
-
266
- const prefix = verb ? `${verb} ` : "";
267
- let strCount = count.toLocaleString();
268
- strCount = color.whiteBright(strCount);
269
- strCount = color.cyan("[") + strCount + color.cyan("]");
270
-
271
- let line = `${prefix}${strCount} ${entity}`;
272
- if (headingText) {
273
- line = indent(line);
274
- }
275
-
276
- log(line);
277
- }
278
-
279
- // --- The One Key and Value Display ---------------------------------------------------------------
280
-
281
- /**
282
- * How far a key and value block is indented. Every one of them sits at the same
283
- * depth, whether it is a command's opening block, a result, or the facts about
284
- * a variable.
285
- */
286
- export const VARIABLE_INDENT = 4;
287
-
288
- /** One key and value pair, as the display functions take it. */
289
- export interface VariableEntry {
290
- /**
291
- * The name, written on the left. An empty label means "the same label as the
292
- * line above, continued": the label column and its colon are left blank, so a
293
- * fact that needs several lines still lines up under itself.
294
- */
295
- label: string;
296
- /** The value, written after the colon. Anything printable. */
297
- value: unknown;
298
- /**
299
- * A secondary fact about the value: where it came from, where it lives. It
300
- * follows the value in muted grey rather than in parentheses, so the value
301
- * itself stays the thing the eye lands on.
302
- */
303
- detail?: string;
304
- }
305
-
306
- /** How a key and value block is laid out. */
307
- export interface ShowVariablesOptions {
308
- /** How far the block is indented. Four by default. */
309
- indent?: number;
310
- /**
311
- * The column the colons line up at, counted from the start of the label.
312
- * Worked out from the longest label when it is not given, which is what
313
- * `showVariables` does for a whole block.
314
- */
315
- labelWidth?: number;
316
- /** The column to wrap values at. Defaults to the wrap width. */
317
- width?: number;
318
- /** Where the lines go. The console by default. */
319
- write?: (line: string) => void;
320
- }
321
-
322
- /**
323
- * Lays one key and value pair out: the label in the label color, padded so
324
- * every colon in the block lines up, then the value in the value color, then
325
- * any detail in muted grey. A value too long for the line wraps and hangs under
326
- * the value column rather than under the label.
327
- *
328
- * @param entry - The label, the value and any trailing detail.
329
- * @param options - Indentation, label column and wrap column.
330
- * @returns The rendered lines.
331
- */
332
- export function formatVariable(
333
- entry: VariableEntry,
334
- options: ShowVariablesOptions = {},
335
- ): string[] {
336
- const pad = " ".repeat(Math.max(0, options.indent ?? VARIABLE_INDENT));
337
- const labelWidth = Math.max(options.labelWidth ?? entry.label.length, entry.label.length);
338
- const gutter = labelWidth + 2; // the padded label, then ": "
339
- const width = options.width ?? wrapColumns();
340
- const valueWidth = Math.max(20, width - pad.length - gutter);
341
-
342
- const valueText = entry.value === undefined || entry.value === null ? "" : String(entry.value);
343
- const valueLines = wrapText(valueText, valueWidth, { hangingIndent: 0 });
344
- const detailLines =
345
- entry.detail === undefined || entry.detail === ""
346
- ? []
347
- : wrapText(entry.detail, valueWidth, { hangingIndent: 0 });
348
-
349
- // The detail joins the value's last line when both fit, and starts a line of
350
- // its own when they do not, so a long location is never cut in half.
351
- const cells: string[] = valueLines.map(line => palette.value(line));
352
- if (detailLines.length > 0) {
353
- const last = valueLines[valueLines.length - 1] ?? "";
354
- const [first, ...rest] = detailLines;
355
- if (last !== "" && displayWidth(last) + 1 + displayWidth(first ?? "") <= valueWidth) {
356
- cells[cells.length - 1] = `${palette.value(last)} ${palette.muted(first ?? "")}`;
357
- } else {
358
- cells.push(palette.muted(first ?? ""));
359
- }
360
- for (const line of rest) cells.push(palette.muted(line));
361
- }
362
-
363
- if (cells.length === 0) cells.push("");
364
-
365
- return cells.map((cell, index) =>
366
- index === 0 && entry.label !== ""
367
- ? `${pad}${palette.label(entry.label.padEnd(labelWidth))}: ${cell}`
368
- : `${pad}${" ".repeat(gutter)}${cell}`,
369
- );
370
- }
371
-
372
- /**
373
- * Writes one key and value pair to the console.
374
- *
375
- * @param label - The name, written on the left.
376
- * @param value - The value, written after the colon.
377
- * @param options - Indentation, label column, wrap column and where to write.
378
- *
379
- * @example
380
- * showVariable("Config", "./my-config.js");
381
- * // Config: ./my-config.js
382
- */
383
- export function showVariable(
384
- label: string,
385
- value: unknown,
386
- options: ShowVariablesOptions & { detail?: string } = {},
387
- ): void {
388
- const write = options.write ?? log;
389
- const entry: VariableEntry = {
390
- label,
391
- value,
392
- ...(options.detail === undefined ? {} : { detail: options.detail }),
393
- };
394
- for (const line of formatVariable(entry, options)) write(line);
395
- }
396
-
397
- /**
398
- * Writes a list of key and value pairs to the console, with the colons lined
399
- * up. Every key and value display in sous goes through this function or through
400
- * `showVariable`, so there is exactly one of them to change.
401
- *
402
- * @param entries - The pairs, either as a plain object or as a list carrying details.
403
- * @param options - Indentation, wrap column and where to write.
404
- *
405
- * @example
406
- * showVariables({ Config: "./my-config.js", Strict: "false" });
407
- * // Config: ./my-config.js
408
- * // Strict: false
409
- */
410
- export function showVariables(
411
- entries: Record<string, unknown> | VariableEntry[],
412
- options: ShowVariablesOptions = {},
413
- ): void {
414
- const list: VariableEntry[] = Array.isArray(entries)
415
- ? entries
416
- : Object.entries(entries).map(([label, value]) => ({ label, value }));
417
- if (list.length === 0) return;
418
-
419
- const labelWidth =
420
- options.labelWidth ?? Math.max(...list.map(entry => entry.label.length));
421
- for (const entry of list) showVariable(entry.label, entry.value, {
422
- ...options,
423
- labelWidth,
424
- ...(entry.detail === undefined ? {} : { detail: entry.detail }),
425
- });
426
- }
427
-
428
- /**
429
- * Displays a heading labelled "Command Variables" followed by a variable list.
430
- *
431
- * The "Dry Run" entry is a special case: it is noise when dry-run mode is off, so it
432
- * is only shown when its value is `true`. Every other entry is always shown.
433
- */
434
- export function showCommandVars(vars: Record<string, any>): void {
435
- const filtered = Object.fromEntries(
436
- Object.entries(vars).filter(([name, value]) => name !== "Dry Run" || value === true),
437
- );
438
- subheading("Command Variables", "$");
439
- showVariables(filtered);
440
- }
441
-
442
- /**
443
- * Displays the status of an in-progress scan using an in-place log line.
444
- */
445
- export function scanStatus(matchedRecordCount: number, totalScanCount: number): void {
446
- const displayMatched = `[${color.white(matchedRecordCount.toLocaleString())}]`;
447
- const displayScanned = `[${color.white(totalScanCount.toLocaleString())}]`;
448
- logPersistent(
449
- color.gray(
450
- `Scan in progress: ${displayMatched} records matched (so far) of ${displayScanned} records scanned ...`,
451
- ),
452
- );
453
- }
454
-
455
- /**
456
- * Displays the status of an in-progress deletion using an in-place log line.
457
- */
458
- export function deleteStatus(recordsDeleted: number, totalRecordsToDelete: number): void {
459
- const displayDeleted = `[${color.white(recordsDeleted.toLocaleString())}]`;
460
- const displayTotal = `[${color.white(totalRecordsToDelete.toLocaleString())}]`;
461
- logPersistent(
462
- color.gray(
463
- `Deletion in progress: ${displayDeleted} of ${displayTotal} records deleted ...`,
464
- ),
465
- );
466
- }
467
-
468
- // --- Special Notices -----------------------------------------------------------------------------
469
-
470
- /**
471
- * Writes an error message to the console in red.
472
- *
473
- * @param text - The message to display.
474
- * @param write - Line sink (default stdout via `log`). Commands whose stdout must
475
- * stay machine-readable (the `sous config *` JSON commands) pass a stderr writer
476
- * so error text never corrupts a piped stdout stream.
477
- */
478
- export function displayError(text: string, write: (line: string) => void = log): void {
479
- const lines = text.split("\n").filter(line => line.trim() !== "");
480
- write("");
481
- lines.forEach((line, index) => {
482
- const body = index === 0 ? withErrorPrefix(line.trim()) : line.trim();
483
- for (const wrapped of wrapText(body, wrapColumns() - 2)) {
484
- write(indent(palette.error(wrapped)));
485
- }
486
- });
487
- write("");
488
- write("");
489
- }
490
-
491
- /**
492
- * Writes a pre-formatted, multi-line error message to the console in red,
493
- * preserving the message's own indentation and blank lines.
494
- *
495
- * Use this instead of `displayError` when the message contains deliberate
496
- * structure (a checked-paths list, a code sample, numbered steps). `displayError`
497
- * trims every line and drops blanks, which flattens that structure.
498
- *
499
- * @param text - The pre-formatted, multi-line message to display.
500
- * @param write - Line sink (default stdout via `log`). Commands whose stdout must
501
- * stay machine-readable (the `sous config *` JSON commands) pass a stderr writer
502
- * so error text never corrupts a piped stdout stream.
503
- *
504
- * @example
505
- * displayErrorBlock("No config found.\n\n Checked:\n /a/.sous/");
506
- */
507
- export function displayErrorBlock(text: string, write: (line: string) => void = log): void {
508
- const lines = text.split("\n");
509
- // The prefix goes on the first line that has any text on it, which is the
510
- // line a reader (or a grep) looks at.
511
- const first = lines.findIndex(line => line.trim() !== "");
512
-
513
- write("");
514
- lines.forEach((line, index) => {
515
- if (line === "") {
516
- write(" ");
517
- return;
518
- }
519
- const body = index === first ? withErrorPrefix(line) : line;
520
- for (const wrapped of wrapText(body, wrapColumns() - 2)) {
521
- write(indent(palette.error(wrapped)));
522
- }
523
- });
524
- write("");
525
- write("");
526
- }
527
-
528
- /**
529
- * Writes a notice indicating that the operation is running in dry-run mode. It
530
- * is an explanation of what is about to not happen, so it carries the note
531
- * color rather than the warning one.
532
- *
533
- * @example
534
- * dryRunNotice("File will not be written.");
535
- * // [Dry Run] File will not be written.
536
- */
537
- export function dryRunNotice(text: string): void {
538
- note(`[Dry Run] ${text}`, { indent: 4 });
539
- }
540
-
541
- /**
542
- * Writes a warning message to the console with a yellow banner. The body is
543
- * bright yellow; words written in capitals inside it are taken as the ones
544
- * carrying the risk and are drawn in orange, so a reader skimming the block
545
- * still takes in the part that matters.
546
- *
547
- * @param text - The message to display.
548
- * @param write - Line sink (default stdout via `log`). Commands whose stdout must
549
- * stay machine-readable (the `sous config *` JSON commands) pass a stderr writer
550
- * so warning text never corrupts a piped stdout stream.
551
- */
552
- export function warning(text: string, write: (line: string) => void = log): void {
553
- write("");
554
- write("");
555
- write(color.bgYellowBright(color.black(" WARNING: ")));
556
-
557
- for (const line of text.split("\n")) {
558
- if (line.trim() === "") {
559
- write(" ");
560
- continue;
561
- }
562
- for (const wrapped of wrapText(line.trim(), wrapColumns() - 2)) {
563
- write(indent(palette.warning(highlightUpperCaseWords(wrapped))));
564
- }
565
- }
566
- write("");
567
- }
568
-
569
- // --- Miscellaneous Helpers -----------------------------------------------------------------------
570
-
571
- /**
572
- * Highlights all UPPERCASE words in the given string.
573
- */
574
- function highlightUpperCaseWords(
575
- str: string,
576
- highlightFn: (word: string) => string = palette.highlight,
577
- ): string {
578
- return str.replace(/\b[A-Z][A-Z_'"()\[\]{}<>|&*!@#%^\\-]+\b/g, match => highlightFn(match));
579
- }
580
-
581
- /**
582
- * Sorts the keys of an object alphabetically.
583
- */
584
- export function sortObjectKeys<T extends Record<string, any>>(obj: T): T {
585
- if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
586
- return obj;
587
- }
588
-
589
- try {
590
- return Object.keys(obj)
591
- .sort()
592
- .reduce((acc, key) => {
593
- // @ts-ignore
594
- acc[key] = obj[key];
595
- return acc;
596
- }, {} as T);
597
- } catch {
598
- /* c8 ignore next */
599
- return obj;
600
- }
601
- }
602
-
603
- // --- ANSI-Safe Measurement -----------------------------------------------------------------------
604
-
605
- /**
606
- * Matches the SGR (Select Graphic Rendition) escape sequences the coloring
607
- * helpers emit, which is every escape sequence sous writes into a string.
608
- */
609
- const SGR_PATTERN = /\u001B\[[0-9;]*m/g;
610
-
611
- /**
612
- * Removes the color escape sequences from a string, leaving the characters a
613
- * reader actually sees.
614
- *
615
- * @param text - The text to strip.
616
- * @returns The same text, without any color escape sequence.
617
- */
618
- export function stripAnsi(text: string): string {
619
- return String(text ?? "").replace(SGR_PATTERN, "");
620
- }
621
-
622
- /**
623
- * How many terminal columns a string occupies once its color codes are taken
624
- * out. Every alignment decision measures with this, because a colored cell is
625
- * longer than it looks.
626
- *
627
- * East Asian wide characters and emoji are out of scope: this counts one column
628
- * per code unit, so a cell holding them aligns a little short. Everything sous
629
- * lays out in a table is an identifier, a path, a URL or English prose, so the
630
- * simple measure holds; widening it later means changing this one function.
631
- *
632
- * @param text - The text to measure.
633
- * @returns The visible width, in columns.
634
- */
635
- export function displayWidth(text: string): number {
636
- return stripAnsi(text).length;
637
- }
638
-
639
- // --- Width-Aware Wrapping ------------------------------------------------------------------------
640
-
641
- /**
642
- * The width sous wraps prose to when the output is not a terminal, or when the
643
- * terminal never said how wide it is.
644
- */
645
- export const DEFAULT_WRAP_COLUMNS = 100;
646
-
647
- /**
648
- * How many columns the output has to work with: the real terminal width when
649
- * there is a terminal, and the default width otherwise, so a piped or recorded
650
- * run always wraps the same way.
651
- *
652
- * @param stream - The stream to measure. Defaults to stdout.
653
- */
654
- export function terminalColumns(stream: { columns?: number } = process.stdout): number {
655
- const columns = stream.columns;
656
- return typeof columns === "number" && columns > 0 ? columns : DEFAULT_WRAP_COLUMNS;
657
- }
658
-
659
- /**
660
- * How far short of the right edge text stops, so a wrapped paragraph never
661
- * touches the last column of the terminal.
662
- */
663
- export const RIGHT_MARGIN = 2;
664
-
665
- /**
666
- * The width every paragraph sous prints is wrapped to: the columns available,
667
- * less the right margin.
668
- *
669
- * @param stream - The stream to measure. Defaults to stdout.
670
- */
671
- export function wrapColumns(stream: { columns?: number } = process.stdout): number {
672
- return Math.max(20, terminalColumns(stream) - RIGHT_MARGIN);
673
- }
674
-
675
- /** How a paragraph is wrapped. */
676
- export interface WrapOptions {
677
- /**
678
- * How far a continuation line hangs past the first line of its paragraph.
679
- * Two by default, which is what makes a wrapped sentence read as one
680
- * paragraph rather than as several. Pass zero where the caller lays out its
681
- * own hanging indent, as the labeled blocks do.
682
- */
683
- hangingIndent?: number;
684
- }
685
-
686
- /**
687
- * Wraps a paragraph to a width, breaking on spaces and never inside a word. A
688
- * word longer than the width (a URL, a path) is left whole on a line of its
689
- * own, because half a URL is worse than a long line.
690
- *
691
- * Newlines already in the text are honored: each line is wrapped on its own,
692
- * and each keeps whatever indentation it was written with, so an already laid
693
- * out block survives being passed through. Continuation lines hang two spaces
694
- * past the line they continue, unless the caller asks for something else.
695
- *
696
- * Width is measured with the color codes taken out, so a colored word is
697
- * counted by what a reader sees rather than by what the escape sequences add.
698
- *
699
- * @param text - The prose to wrap.
700
- * @param width - The column to wrap at. Defaults to the terminal width, less the right margin.
701
- * @param options - How far continuation lines hang.
702
- * @returns One string per rendered line, without trailing spaces.
703
- *
704
- * @example
705
- * wrapText("one two three", 7, { hangingIndent: 0 });
706
- * // -> ["one two", "three"]
707
- */
708
- export function wrapText(
709
- text: string,
710
- width: number = wrapColumns(),
711
- options: WrapOptions = {},
712
- ): string[] {
713
- const limit = Math.max(1, Math.floor(width));
714
- const hanging = Math.max(0, Math.floor(options.hangingIndent ?? 2));
715
- const lines: string[] = [];
716
-
717
- for (const paragraph of String(text ?? "").split("\n")) {
718
- // The line's own indentation is kept, and continuation lines are measured
719
- // and drawn from inside it, so a block that arrives already laid out is
720
- // still laid out when it leaves.
721
- const leading = /^[ \t]*/.exec(paragraph)?.[0] ?? "";
722
- const body = paragraph.slice(leading.length);
723
- const words = body.split(/\s+/).filter(word => word.length > 0);
724
- if (words.length === 0) {
725
- lines.push("");
726
- continue;
727
- }
728
-
729
- const continuation = leading + " ".repeat(hanging);
730
- let prefix = leading;
731
- let current = "";
732
-
733
- for (const word of words) {
734
- if (current.length === 0) {
735
- current = word;
736
- continue;
737
- }
738
- const projected = displayWidth(prefix) + displayWidth(current) + 1 + displayWidth(word);
739
- if (projected <= limit) {
740
- current = `${current} ${word}`;
741
- continue;
742
- }
743
- lines.push(`${prefix}${current}`);
744
- prefix = continuation;
745
- current = word;
746
- }
747
-
748
- lines.push(`${prefix}${current}`);
749
- }
750
-
751
- return lines;
752
- }
753
-
754
- /** How a paragraph is laid out for printing. */
755
- export interface ParagraphOptions extends WrapOptions {
756
- /** How far the whole paragraph is indented. Two by default. */
757
- indent?: number;
758
- /** The column to wrap at, indentation included. Defaults to the wrap width. */
759
- width?: number;
760
- /** A color to paint every line with. */
761
- color?: (text: string) => string;
762
- }
763
-
764
- /**
765
- * Lays a paragraph out for printing: wrapped to the width left after its own
766
- * indentation, indented, and colored.
767
- *
768
- * @param text - The prose to lay out.
769
- * @param options - Indentation, wrap column and color.
770
- * @returns The rendered lines.
771
- */
772
- export function formatParagraph(text: string, options: ParagraphOptions = {}): string[] {
773
- const pad = Math.max(0, options.indent ?? 2);
774
- const width = options.width ?? wrapColumns();
775
- const wrapOptions: WrapOptions =
776
- options.hangingIndent === undefined ? {} : { hangingIndent: options.hangingIndent };
777
- const paint = options.color ?? ((line: string) => line);
778
-
779
- return wrapText(text, Math.max(20, width - pad), wrapOptions).map(line =>
780
- line === "" ? " " : " ".repeat(pad) + paint(line),
781
- );
782
- }
783
-
784
- /**
785
- * Writes a wrapped, indented paragraph to the console. Every long sentence the
786
- * CLI prints goes through this, so nothing is ever left to the terminal's own
787
- * idea of where a line should break.
788
- *
789
- * @param text - The prose to print.
790
- * @param options - Indentation, wrap column and color.
791
- */
792
- export function paragraph(text: string, options: ParagraphOptions = {}): void {
793
- for (const line of formatParagraph(text, options)) log(line);
794
- }
795
-
796
- /**
797
- * Writes an explanatory line: something that is neither a warning nor an error,
798
- * but a note about what just happened or what is about to. It is bright teal,
799
- * which is the one color reserved for explanation.
800
- *
801
- * @param text - The note to print.
802
- * @param options - Indentation and wrap column.
803
- */
804
- export function note(text: string, options: Omit<ParagraphOptions, "color"> = {}): void {
805
- paragraph(text, { ...options, color: palette.note });
806
- }
807
-
808
- // --- Key Bindings --------------------------------------------------------------------------------
809
-
810
- /**
811
- * The one-line legend a prompt draws under itself, in the style the stock
812
- * `@inquirer/select` prompt uses: the key in bold, what it does dimmed beside
813
- * it, pairs separated by a dimmed bullet.
814
- *
815
- * @param keys - Key and action pairs, in the order they are shown.
816
- * @returns The legend line.
817
- *
818
- * @example
819
- * keysHelpTip([["↑↓", "navigate"], ["⏎", "select"]]);
820
- * // -> "↑↓ navigate • ⏎ select"
821
- */
822
- export function keysHelpTip(keys: Array<[key: string, action: string]>): string {
823
- return keys
824
- .map(([key, action]) => `${color.bold(key)} ${color.dim(action)}`)
825
- .join(color.dim(" • "));
826
- }
827
-
828
- /**
829
- * The character every bulleted line in the CLI is drawn with, so a list of
830
- * points always looks like a list of points.
831
- */
832
- export const BULLET = "•";
833
-
834
- /** How many blank lines a question keeps under itself while it is waiting. */
835
- export const PROMPT_BOTTOM_PADDING = 2;
836
-
837
- /**
838
- * The block a prompt draws under its input line: whatever it has to say
839
- * (a validation message, the key legend), then two blank lines, so a question
840
- * never sits on the terminal's very last row with its legend scrolled away.
841
- *
842
- * @param content - The line under the prompt, or an empty string for none.
843
- * @returns The bottom block, ready to hand back from a prompt's renderer.
844
- */
845
- export function promptBottom(content: string): string {
846
- const lines = content === "" ? [] : [content];
847
- for (let index = 0; index < PROMPT_BOTTOM_PADDING; index++) lines.push(" ");
848
- return lines.join("\n");
849
- }