@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,89 +0,0 @@
1
- import { Hash, type Liquid } from "liquidjs";
2
- import type { Context } from "liquidjs/dist/context/context.js";
3
- import type { TagToken } from "liquidjs/dist/tokens/tag-token.js";
4
- import { globFiles, parseGlobList, type GlobFile } from "../lib/glob-files.js";
5
- import { splitPositionalName } from "../lib/tag-args.js";
6
- import { importNamedExport } from "../lib/import-export.js";
7
-
8
- /**
9
- * Registers the `{% getFiles <varName> root="..." include="..." exclude="..." import="..." %}`
10
- * tag. It globs files under `root` and assigns the resulting array to `<varName>`
11
- * in the template scope. It renders no output — use a `{% for %}` loop to present
12
- * the results.
13
- *
14
- * Each result has `{ path, dir, relPath, name }`. When the optional `import`
15
- * attribute names an export (e.g. `import="meta"`), each file is dynamically
16
- * imported and that export is attached under the same key (e.g. `file.meta`);
17
- * files that fail to import or lack the export are omitted from the results.
18
- *
19
- * Attribute values may be quoted strings or scope variables (e.g. `root=tasksDir`).
20
- * `include`/`exclude` are comma-separated glob patterns matched relative to `root`.
21
- */
22
- export function registerGetFilesTag(engine: Liquid): void {
23
- engine.registerTag("getFiles", {
24
- parse(token: TagToken) {
25
- const { name, rest } = splitPositionalName(token.args);
26
- this.varName = name;
27
- this.hash = new Hash(rest, true);
28
- },
29
-
30
- *render(ctx: Context): Generator<unknown, string, Record<string, unknown>> {
31
- const hash = yield this.hash.render(ctx);
32
- const varName: string | null = this.varName;
33
-
34
- const files = (yield resolveFiles(hash)) as unknown;
35
-
36
- if (varName) {
37
- (ctx.bottom() as Record<string, unknown>)[varName] = files;
38
- }
39
- return "";
40
- },
41
- });
42
- }
43
-
44
- /** A file record optionally carrying a dynamically-imported export. */
45
- type ResolvedFile = GlobFile & Record<string, unknown>;
46
-
47
- /**
48
- * Resolve the glob attributes from a rendered hash into file records, applying
49
- * the optional `import` export attachment.
50
- *
51
- * @param hash - The rendered tag attributes (`root`, `include`, `exclude`, `import`).
52
- * @returns The matched files, each optionally carrying the imported export.
53
- */
54
- async function resolveFiles(hash: Record<string, unknown>): Promise<ResolvedFile[]> {
55
- const root = hash.root != null ? String(hash.root) : "";
56
- if (!root) {
57
- throw new Error('getFiles: a "root" attribute is required');
58
- }
59
-
60
- const files = await globFiles({
61
- root,
62
- include: parseGlobList(hash.include != null ? String(hash.include) : undefined),
63
- exclude: parseGlobList(hash.exclude != null ? String(hash.exclude) : undefined),
64
- });
65
-
66
- const importName = hash.import != null ? String(hash.import).trim() : "";
67
- if (!importName) return files as ResolvedFile[];
68
-
69
- return attachImports(files, importName);
70
- }
71
-
72
- /**
73
- * For each file, dynamically import `importName` and attach it under that key.
74
- * Files whose import fails or lacks the export are dropped from the result.
75
- *
76
- * @param files - The globbed file records.
77
- * @param importName - The export to read from each file.
78
- * @returns Files that successfully yielded the export, with it attached.
79
- */
80
- async function attachImports(files: GlobFile[], importName: string): Promise<ResolvedFile[]> {
81
- const enriched = await Promise.all(
82
- files.map(async (file) => {
83
- const value = await importNamedExport(file.path, importName);
84
- if (value === undefined) return null;
85
- return { ...file, [importName]: value };
86
- })
87
- );
88
- return enriched.filter((f): f is ResolvedFile => f !== null);
89
- }
@@ -1,14 +0,0 @@
1
- import type { Liquid } from "liquidjs";
2
- import { registerShowVarsTag } from "./showVars.js";
3
- import { registerExportScalarVarsJsTag } from "./exportScalarVarsJs.js";
4
- import { registerGetFilesTag } from "./getFiles.js";
5
- import { registerListFilesTag } from "./listFiles.js";
6
-
7
- const tagRegistrars: Array<(engine: Liquid) => void> = [
8
- registerShowVarsTag,
9
- registerExportScalarVarsJsTag,
10
- registerGetFilesTag,
11
- registerListFilesTag,
12
- ];
13
-
14
- export default tagRegistrars;
@@ -1,54 +0,0 @@
1
- import { Hash, type Liquid } from "liquidjs";
2
- import type { Context } from "liquidjs/dist/context/context.js";
3
- import type { TagToken } from "liquidjs/dist/tokens/tag-token.js";
4
- import { globFiles, parseGlobList, type GlobFile } from "../lib/glob-files.js";
5
-
6
- /**
7
- * Registers the `{% listFiles root="..." include="..." exclude="..." %}` tag.
8
- * It globs files under `root` and renders them directly as a simple markdown
9
- * bullet list of file names — the convenience counterpart to `getFiles`.
10
- *
11
- * For custom presentation (or to read file metadata), use `getFiles` with a
12
- * `{% for %}` loop instead. `listFiles` is intentionally glob-only and emits a
13
- * fixed format.
14
- *
15
- * Attribute values may be quoted strings or scope variables. `include`/`exclude`
16
- * are comma-separated glob patterns matched relative to `root`. An optional
17
- * `relative="true"` renders relative paths instead of bare file names.
18
- */
19
- export function registerListFilesTag(engine: Liquid): void {
20
- engine.registerTag("listFiles", {
21
- parse(token: TagToken) {
22
- this.hash = new Hash(token.args, true);
23
- },
24
-
25
- *render(ctx: Context): Generator<unknown, string, Record<string, unknown>> {
26
- const hash = yield this.hash.render(ctx);
27
- const files = (yield resolveFiles(hash)) as unknown as GlobFile[];
28
- const useRelative = String(hash.relative ?? "") === "true";
29
-
30
- if (files.length === 0) return "";
31
- return files
32
- .map((f) => `- ${useRelative ? f.relPath : f.name}`)
33
- .join("\n");
34
- },
35
- });
36
- }
37
-
38
- /**
39
- * Resolve the glob attributes from a rendered hash into file records.
40
- *
41
- * @param hash - The rendered tag attributes (`root`, `include`, `exclude`).
42
- * @returns The matched files.
43
- */
44
- async function resolveFiles(hash: Record<string, unknown>): Promise<GlobFile[]> {
45
- const root = hash.root != null ? String(hash.root) : "";
46
- if (!root) {
47
- throw new Error('listFiles: a "root" attribute is required');
48
- }
49
- return globFiles({
50
- root,
51
- include: parseGlobList(hash.include != null ? String(hash.include) : undefined),
52
- exclude: parseGlobList(hash.exclude != null ? String(hash.exclude) : undefined),
53
- });
54
- }
@@ -1,28 +0,0 @@
1
- import type { Liquid } from "liquidjs";
2
- import type { Context } from "liquidjs/dist/context/context.js";
3
- import { sortObjectKeys } from "../../utils/formatting.js";
4
- import { HIDDEN_VALUE, isSecretEntry } from "../lib/secret-scope.js";
5
-
6
- /**
7
- * Dumps all variables currently in scope as a fenced JSON block. A secret (one a
8
- * recipe declared, or one that very probably is) prints as `(hidden)`, so the
9
- * dump never carries it into compiled output.
10
- */
11
- export function registerShowVarsTag(engine: Liquid): void {
12
- engine.registerTag("showVars", {
13
- render(ctx: Context) {
14
- const seen = new WeakSet();
15
- const scope = sortObjectKeys(ctx.getAll() as Record<string, unknown>);
16
- const json = JSON.stringify(scope, (key, value) => {
17
- if (typeof value === "object" && value !== null) {
18
- if (seen.has(value)) return "[Circular]";
19
- seen.add(value);
20
- }
21
- if (key !== "" && isSecretEntry(engine, key, value)) return HIDDEN_VALUE;
22
- return value;
23
- }, 2);
24
-
25
- return "# Sous Debug: Variable Dump\n```json\n" + json + "\n```";
26
- },
27
- });
28
- }
@@ -1,143 +0,0 @@
1
- /**
2
- * The pick-one question sous asks for a recipe variable with a fixed set of
3
- * allowed values.
4
- *
5
- * It behaves like the stock `select` prompt (arrow keys to move, Enter to
6
- * choose, the default preselected) with the one addition every sous question
7
- * shares: Tab resolves the question with `{ kind: "advanced" }` instead of
8
- * moving the cursor, so the caller can show the advanced view and then ask the
9
- * same question again. Built on the public `@inquirer/core` hooks only.
10
- */
11
-
12
- import {
13
- createPrompt,
14
- isDownKey,
15
- isEnterKey,
16
- isTabKey,
17
- isUpKey,
18
- useKeypress,
19
- usePrefix,
20
- useState,
21
- type Status,
22
- } from "@inquirer/core";
23
- import { promptBottom } from "./formatting.js";
24
-
25
- /** One value the question offers, and how it is written on screen. */
26
- export interface PromptChoice {
27
- /** The line shown in the list. */
28
- name: string;
29
- /** The value handed back when the line is chosen. */
30
- value: string;
31
- }
32
-
33
- /** What the pick-one question needs in order to ask itself. */
34
- export interface ChoicePromptConfig {
35
- /** The one-line question. */
36
- message: string;
37
- /** The values on offer, in the order they are listed. */
38
- choices: PromptChoice[];
39
- /** The value that starts out highlighted, so Enter alone accepts it. */
40
- default?: string;
41
- /** A line under the list: the key legend, in the stock prompts' style. */
42
- hint?: string;
43
- }
44
-
45
- /** How the question ended: with a choice, or with a request for the advanced view. */
46
- export type ChoicePromptResult = { kind: "value"; value: string } | { kind: "advanced" };
47
-
48
- /** Everything the renderer draws, so it can be tested without a terminal. */
49
- export interface ChoicePromptView {
50
- /** The prompt prefix (the question mark, or the check mark once answered). */
51
- prefix: string;
52
- /** The one-line question. */
53
- message: string;
54
- /** The values on offer, in the order they are listed. */
55
- choices: PromptChoice[];
56
- /** Which choice is highlighted, counting from zero. */
57
- active: number;
58
- /** A line under the list: the key legend, in the stock prompts' style. */
59
- hint?: string;
60
- /** Whether the question has been answered, which replaces the list with the answer. */
61
- answered?: boolean;
62
- }
63
-
64
- /** The pointer drawn beside the highlighted choice. */
65
- const POINTER = ">";
66
-
67
- /**
68
- * Draws the question: prefix and message on the first line, then one line per
69
- * choice with a pointer beside the highlighted one, and the hint underneath.
70
- * Once the question is answered the list gives way to the chosen value, so the
71
- * scrollback keeps one tidy line per question.
72
- *
73
- * @param view - The prompt's state.
74
- * @returns The question line and the block under it.
75
- */
76
- export function renderChoicePrompt(view: ChoicePromptView): [string, string] {
77
- const chosen = view.choices[view.active];
78
-
79
- if (view.answered === true) {
80
- return [`${view.prefix} ${view.message}: ${chosen?.name ?? ""}`.trimEnd(), ""];
81
- }
82
-
83
- const lines = view.choices.map(
84
- (choice, index) => `${index === view.active ? POINTER : " "} ${choice.name}`
85
- );
86
- if (view.hint !== undefined && view.hint !== "") lines.push(view.hint);
87
-
88
- return [`${view.prefix} ${view.message}`.trimEnd(), promptBottom(lines.join("\n"))];
89
- }
90
-
91
- /** Asks one pick-one question, with Tab bound to the advanced view. */
92
- export const choicePrompt = createPrompt<ChoicePromptResult, ChoicePromptConfig>(
93
- (config, done) => {
94
- const [status, setStatus] = useState<Status>("idle");
95
- const [answered, setAnswered] = useState(false);
96
- const [active, setActive] = useState(
97
- Math.max(
98
- 0,
99
- config.choices.findIndex((choice) => choice.value === config.default)
100
- )
101
- );
102
- const prefix = usePrefix({ status });
103
-
104
- useKeypress((key, rl) => {
105
- if (status !== "idle") return;
106
- // Readline echoes whatever was pressed into its buffer; take it back out.
107
- rl.clearLine(0);
108
-
109
- if (isTabKey(key)) {
110
- setStatus("done");
111
- done({ kind: "advanced" });
112
- return;
113
- }
114
-
115
- if (isEnterKey(key)) {
116
- const chosen = config.choices[active];
117
- if (chosen === undefined) return;
118
- setAnswered(true);
119
- setStatus("done");
120
- done({ kind: "value", value: chosen.value });
121
- return;
122
- }
123
-
124
- if (isUpKey(key)) {
125
- setActive((active - 1 + config.choices.length) % config.choices.length);
126
- return;
127
- }
128
-
129
- if (isDownKey(key)) {
130
- setActive((active + 1) % config.choices.length);
131
- }
132
- });
133
-
134
- return renderChoicePrompt({
135
- prefix,
136
- message: config.message,
137
- choices: config.choices,
138
- active,
139
- ...(config.hint === undefined ? {} : { hint: config.hint }),
140
- ...(answered ? { answered: true } : {}),
141
- });
142
- }
143
- );
@@ -1,186 +0,0 @@
1
- /**
2
- * One way for every sous command to report a failure.
3
- *
4
- * A person who forgets an argument, misspells a flag, or runs sous where there
5
- * is no config has made an ordinary mistake, and what they need back is the
6
- * sentence describing it (plus, when the mistake is a usage one, the command's
7
- * own help). A stack trace pointing into oclif's parser tells them nothing, so
8
- * no expected failure prints one. A trace is still one environment variable
9
- * away: set `SOUS_DEBUG` and every reported error prints its stack to stderr.
10
- *
11
- * The same reporting is used by every command. `BaseCommand.catch` calls it for
12
- * the commands that discover a config; the repository authoring commands
13
- * (`repo init`, `repo release`, `repo submit`) extend oclif's `Command`
14
- * directly and call it from their own `catch`, so all of sous fails the same
15
- * way.
16
- */
17
-
18
- import { Errors, type Command } from "@oclif/core";
19
- import { isConfigError } from "../lib/errors.js";
20
- import { wantsHelp } from "../lib/interactive.js";
21
- import { displayErrorBlock, log } from "./formatting.js";
22
- import { printCommandHelpToStderr } from "./command-help.js";
23
-
24
- /** The environment variable that turns stack traces back on. */
25
- export const DEBUG_ENV_VAR = "SOUS_DEBUG";
26
-
27
- /**
28
- * Values of `SOUS_DEBUG` that mean "not set". Everything else counts as set,
29
- * because people spell it `1`, `true` and `yes` in roughly equal measure. The
30
- * same list gates the `CI` variable in `lib/interactive.ts`.
31
- */
32
- const FALSY_DEBUG_VALUES = new Set(["", "0", "false", "no", "off"]);
33
-
34
- /** The line oclif appends to a parse error, replaced by the help screen itself. */
35
- const OCLIF_HELP_HINT = "See more help with --help";
36
-
37
- /** The sentence an unexpected failure ends with, since sous cannot explain it. */
38
- const UNEXPECTED_REMEDY = `Sous did not expect this error; set '${DEBUG_ENV_VAR}=1' and run the command again for the full stack trace.`;
39
-
40
- /** Where the report is written, and what it reads to decide about traces. */
41
- export type ErrorReportOptions = {
42
- /**
43
- * Line sink for the error text. Defaults to stdout via `log`; commands whose
44
- * stdout must stay machine-readable pass a stderr writer.
45
- */
46
- write?: (line: string) => void;
47
- /** The environment, read for `SOUS_DEBUG`. Defaults to `process.env`. */
48
- env?: NodeJS.ProcessEnv;
49
- };
50
-
51
- /**
52
- * True when this run was asked for stack traces, either by `SOUS_DEBUG` or by
53
- * oclif's own debug setting (which `--debug`-style bootstrapping turns on).
54
- *
55
- * @param env - The environment to read. Defaults to the real one.
56
- */
57
- export function stackTracesRequested(env: NodeJS.ProcessEnv = process.env): boolean {
58
- const oclifDebug = (globalThis as { oclif?: { debug?: boolean } }).oclif?.debug;
59
- if (oclifDebug === true) return true;
60
-
61
- const raw = env[DEBUG_ENV_VAR];
62
- if (raw === undefined) return false;
63
- return !FALSY_DEBUG_VALUES.has(raw.trim().toLowerCase());
64
- }
65
-
66
- /**
67
- * True when the "error" is only a request to end the process, which every
68
- * `this.exit(code)` raises. It carries no message and must reach oclif
69
- * untouched, or a clean exit would be reported as a failure.
70
- *
71
- * @param error - The thrown value.
72
- */
73
- export function isExitSignal(error: unknown): boolean {
74
- if (error instanceof Errors.ExitError) return true;
75
- return typeof error === "object" && error !== null && (error as { code?: string }).code === "EEXIT";
76
- }
77
-
78
- /**
79
- * True when the caller got the command line wrong (a missing argument, an
80
- * unknown flag, a value outside a flag's options) or asked a question sous
81
- * could not ask. Both are answered by showing the command's own help, so both
82
- * are treated the same way.
83
- *
84
- * oclif's parse errors all carry a `parse` property; nothing else oclif throws
85
- * does, which is what separates a usage mistake from a reported failure.
86
- *
87
- * @param error - The thrown value.
88
- */
89
- export function isUsageError(error: unknown): boolean {
90
- if (wantsHelp(error)) return true;
91
- return error instanceof Errors.CLIError && "parse" in error;
92
- }
93
-
94
- /**
95
- * Reports one command failure in sous's own voice: the message, the command's
96
- * help underneath it when the message alone is not enough, and a stack trace
97
- * only when one was asked for.
98
- *
99
- * @param command - The command that failed, whose help may be drawn.
100
- * @param error - The error it failed with.
101
- * @param options - Where to write, and the environment to read.
102
- * @returns The exit code the run should end with, or undefined when this error
103
- * is not sous's to report and must fall through to oclif's own handling.
104
- */
105
- export async function reportCommandError(
106
- command: Command,
107
- error: Error & { exitCode?: number; oclif?: { exit?: number | false } },
108
- options: ErrorReportOptions = {}
109
- ): Promise<number | undefined> {
110
- // A clean exit, and a command rendering its own JSON error, both belong to
111
- // oclif; neither is a failure for sous to describe.
112
- if (isExitSignal(error)) return undefined;
113
- if (jsonRequested(command)) return undefined;
114
-
115
- const write = options.write ?? log;
116
- const showHelp = isUsageError(error);
117
- const expected = showHelp || isConfigError(error) || error instanceof Errors.CLIError;
118
-
119
- const body = [messageOf(error, showHelp)];
120
- if (!expected) body.push("", UNEXPECTED_REMEDY);
121
-
122
- displayErrorBlock(body.join("\n"), write);
123
-
124
- if (stackTracesRequested(options.env ?? process.env)) writeStackToStderr(error);
125
- if (showHelp) await printCommandHelpToStderr(command);
126
-
127
- return exitCodeOf(error);
128
- }
129
-
130
- /**
131
- * The message to print, with oclif's "See more help with --help" line removed
132
- * when the help itself is about to be printed underneath it.
133
- *
134
- * @param error - The error being reported.
135
- * @param showHelp - Whether the command's help follows the message.
136
- */
137
- function messageOf(error: Error, showHelp: boolean): string {
138
- const message = error.message?.trim() === "" ? String(error) : error.message;
139
- if (!showHelp) return message;
140
- return message
141
- .split("\n")
142
- .filter((line) => line.trim() !== OCLIF_HELP_HINT)
143
- .join("\n")
144
- .trimEnd();
145
- }
146
-
147
- /**
148
- * Writes an error's stack to stderr, verbatim and uncolored. It is diagnostic
149
- * output for whoever set `SOUS_DEBUG`, never part of what the command prints.
150
- *
151
- * @param error - The error whose stack to write.
152
- */
153
- function writeStackToStderr(error: Error): void {
154
- const stack = error.stack ?? "";
155
- if (stack.trim() === "") return;
156
- process.stderr.write(`${stack}\n`);
157
- }
158
-
159
- /**
160
- * The exit code a failure ends with: the one oclif assigned when it assigned
161
- * one (a parse error exits 2), and 1 for everything else.
162
- *
163
- * @param error - The error being reported.
164
- */
165
- function exitCodeOf(error: Error & { exitCode?: number; oclif?: { exit?: number | false } }): number {
166
- const fromOclif = error.oclif?.exit;
167
- if (typeof fromOclif === "number" && Number.isInteger(fromOclif)) return fromOclif;
168
- if (typeof error.exitCode === "number" && Number.isInteger(error.exitCode)) return error.exitCode;
169
- return 1;
170
- }
171
-
172
- /**
173
- * True when the command is rendering machine-readable JSON, in which case oclif
174
- * owns the error output and sous must not print a human error over it.
175
- *
176
- * @param command - The command that failed.
177
- */
178
- function jsonRequested(command: Command): boolean {
179
- const enabled = (command as { jsonEnabled?: () => boolean }).jsonEnabled;
180
- if (typeof enabled !== "function") return false;
181
- try {
182
- return enabled.call(command) === true;
183
- } catch {
184
- return false;
185
- }
186
- }
@@ -1,45 +0,0 @@
1
- /**
2
- * Printing a command's own help underneath an error.
3
- *
4
- * A run that failed because a question could not be asked should show the
5
- * caller every flag that would have answered it, without sending them off to
6
- * `sous help <command>`. The same goes for a run that failed because the
7
- * command line itself was wrong. `utils/command-errors.ts` decides when a
8
- * failure earns a help screen; this module draws it, so every command in sous
9
- * draws exactly the same one.
10
- */
11
-
12
- import { loadHelpClass, type Command } from "@oclif/core";
13
-
14
- /**
15
- * Prints one command's help, always to stderr.
16
- *
17
- * An error is not output, and a command whose stdout is being piped must not
18
- * have a help screen spliced into its stream. oclif's help writes to stdout, so
19
- * stdout is pointed at stderr for the duration and put back afterwards. The help
20
- * class is the one oclif itself uses for `--help`, `-h` and the `help` command,
21
- * so all four routes draw the same screen.
22
- *
23
- * Drawing the help is a courtesy: a failure to draw it must never replace the
24
- * error that is actually being reported, so everything here is swallowed.
25
- *
26
- * @param command - The command whose help to draw.
27
- */
28
- export async function printCommandHelpToStderr(command: Command): Promise<void> {
29
- const writeToStdout = process.stdout.write.bind(process.stdout);
30
- process.stdout.write = ((chunk: string | Uint8Array, ...rest: unknown[]) =>
31
- (process.stderr.write as (...args: unknown[]) => boolean)(
32
- chunk,
33
- ...rest
34
- )) as typeof process.stdout.write;
35
-
36
- try {
37
- const HelpClass = await loadHelpClass(command.config);
38
- const help = new HelpClass(command.config);
39
- await help.showHelp([command.id ?? ""]);
40
- } catch {
41
- // The help screen is a courtesy; never let it replace the real error.
42
- } finally {
43
- process.stdout.write = writeToStdout;
44
- }
45
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * The yes-or-no question sous asks for a recipe variable that holds a boolean.
3
- *
4
- * It behaves like the stock `confirm` prompt (the y and n keys answer it, Enter
5
- * alone accepts the default, which is the capitalised letter) with the one
6
- * addition every sous question shares: Tab resolves the question with
7
- * `{ kind: "advanced" }`, so the caller can show the advanced view and then ask
8
- * the same question again. Built on the public `@inquirer/core` hooks only.
9
- */
10
-
11
- import {
12
- createPrompt,
13
- isEnterKey,
14
- isTabKey,
15
- useKeypress,
16
- usePrefix,
17
- useState,
18
- type Status,
19
- } from "@inquirer/core";
20
- import { promptBottom } from "./formatting.js";
21
-
22
- /** What the yes-or-no question needs in order to ask itself. */
23
- export interface ConfirmPromptConfig {
24
- /** The one-line question. */
25
- message: string;
26
- /** The answer Enter alone accepts. Defaults to yes. */
27
- default?: boolean;
28
- /** A line under the question: the key legend, in the stock prompts' style. */
29
- hint?: string;
30
- }
31
-
32
- /** How the question ended: with an answer, or with a request for the advanced view. */
33
- export type ConfirmPromptResult = { kind: "value"; value: boolean } | { kind: "advanced" };
34
-
35
- /** Everything the renderer draws, so it can be tested without a terminal. */
36
- export interface ConfirmPromptView {
37
- /** The prompt prefix (the question mark, or the check mark once answered). */
38
- prefix: string;
39
- /** The one-line question. */
40
- message: string;
41
- /** The answer Enter alone accepts. Defaults to yes. */
42
- default?: boolean;
43
- /** The answer given, once there is one. */
44
- answer?: boolean;
45
- /** A line under the question: the key legend, in the stock prompts' style. */
46
- hint?: string;
47
- }
48
-
49
- /**
50
- * Draws the question: prefix, message and the two keys with the default
51
- * capitalised, replaced by the answer itself once one is given, plus the hint
52
- * on the line underneath. A yes or no is never a secret, so nothing here is
53
- * ever masked.
54
- *
55
- * @param view - The prompt's state.
56
- * @returns The question line and the line under it.
57
- */
58
- export function renderConfirmPrompt(view: ConfirmPromptView): [string, string] {
59
- const fallback = view.default ?? true;
60
- const keys = fallback ? "(Y/n)" : "(y/N)";
61
- const tail = view.answer === undefined ? keys : view.answer ? "yes" : "no";
62
- const line = `${view.prefix} ${view.message} ${tail}`.trimEnd();
63
-
64
- if (view.answer !== undefined) return [line, ""];
65
- return [line, promptBottom(view.hint ?? "")];
66
- }
67
-
68
- /** Asks one yes-or-no question, with Tab bound to the advanced view. */
69
- export const confirmPrompt = createPrompt<ConfirmPromptResult, ConfirmPromptConfig>(
70
- (config, done) => {
71
- const [status, setStatus] = useState<Status>("idle");
72
- const [answer, setAnswer] = useState<boolean | undefined>(undefined);
73
- const prefix = usePrefix({ status });
74
-
75
- /** Settles the question with one answer, and draws it in place of the keys. */
76
- const settle = (value: boolean): void => {
77
- setAnswer(value);
78
- setStatus("done");
79
- done({ kind: "value", value });
80
- };
81
-
82
- useKeypress((key, rl) => {
83
- if (status !== "idle") return;
84
- // Readline echoes whatever was pressed into its buffer; take it back out.
85
- rl.clearLine(0);
86
-
87
- if (isTabKey(key)) {
88
- setStatus("done");
89
- done({ kind: "advanced" });
90
- return;
91
- }
92
-
93
- if (isEnterKey(key)) {
94
- settle(config.default ?? true);
95
- return;
96
- }
97
-
98
- if (key.name === "y") settle(true);
99
- else if (key.name === "n") settle(false);
100
- });
101
-
102
- return renderConfirmPrompt({
103
- prefix,
104
- message: config.message,
105
- ...(config.default === undefined ? {} : { default: config.default }),
106
- ...(answer === undefined ? {} : { answer }),
107
- ...(config.hint === undefined ? {} : { hint: config.hint }),
108
- });
109
- }
110
- );