@sous-io/sous 0.2.36 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1093 -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 -237
  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 -391
  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 -412
  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 -1105
  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,523 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { globSync } from "glob";
4
- import { minimatch } from "minimatch";
5
- import { compareBytewise, hasGlob, type ViewFile } from "../services/ref-resolver/index.js";
6
- import { expandHome } from "./config-discovery.js";
7
- import { toGlobPattern } from "./glob-pattern.js";
8
- import type {
9
- NamespaceResolution,
10
- NamespaceResolver,
11
- RecipeRoot,
12
- } from "./repos/namespace-resolver.js";
13
-
14
- /**
15
- * @include path resolution: aliases, `#` names, recipe references, globs,
16
- * variable substitution and the ordered candidate search.
17
- *
18
- * An include path (the part after `@`) resolves to an ordered list of groups.
19
- * The caller uses the first group that names at least one file, and gets every
20
- * file in it; if none does, it errors listing every path tried.
21
- *
22
- * Resolution pipeline for a raw path P (with leading `@` already stripped):
23
- * 1. Substitute ${vars} in P, then expand a leading `~/` to the home
24
- * directory (the `~` sigil with nothing but a separator after it names no
25
- * namespace, so `@~/notes/x.md` is unambiguous). If the result is
26
- * absolute, it is the sole candidate (`@${sousRootPath}/x.md`).
27
- * 2. Split the first segment (up to the first `/` or `:`) as the alias key,
28
- * the remainder as `rest`. If the key is a registered alias, push
29
- * join(base, rest) for EACH base in the alias's ordered array. A `#name`
30
- * (`#project`) is registered in the alias map by the `#` name registry.
31
- * 3. If P begins with `~` and holds a `/`, and a namespace resolver was
32
- * supplied, hand the reference (P without the `~`) to it. The resolver
33
- * reads it with the ref resolver service: `namespace/recipe/path`, any of
34
- * the three possibly a glob, optionally with a `repo:` qualifier.
35
- * 4. Always push the relative candidate: join(baseDir, P), the FULL path
36
- * including the first segment. This lets an alias augment a real relative
37
- * directory of the same name (e.g. `@stuff/x` tries the alias bases, then
38
- * `./stuff/x`).
39
- * 5. Follow every NON-glob candidate with its `.tpl.` twin: `x.md` is
40
- * followed by `x.tpl.md`, and `x.tpl.md` by `x.md`. The literal spelling
41
- * is always tried first, and the twin comes right after it, so an alias
42
- * base still beats the relative fallback.
43
- *
44
- * Any path may use glob syntax (`*`, `**`, `?`, `[..]`, `{a,b}`) after
45
- * substitution. A glob names every file it matches, in bytewise path order, and
46
- * a `~` glob also matches recipes (`@~*` + `/*` + `/memories/*.md`).
47
- *
48
- * A key WITHOUT a sigil never reaches the namespace resolver: a bare `@path` is
49
- * always a relative path or a declared alias, so include lines never masquerade
50
- * as filesystem paths.
51
- *
52
- * Aliases whose names begin with `~` or `#` are reserved: `~` for the home
53
- * directory and recipe namespaces, `#` for names sous or a plugin registers.
54
- * The primary separator is `/` (`@alias/path`); `:` is accepted as an
55
- * equivalent for an alias (`@alias:path`).
56
- */
57
-
58
- /** An alias maps a name to an ordered list of absolute base directories. */
59
- export type AliasMap = Record<string, string[]>;
60
-
61
- /** The files each view lists (`#memories` and so on), keyed by the name with its `#`. */
62
- export type ViewMap = Record<string, ViewFile[]>;
63
-
64
- /**
65
- * Substitute ${varName} references in a string from a scope. Unknown
66
- * references are left untouched (matches settings.substituteVars behavior).
67
- *
68
- * @param str - The string to substitute into.
69
- * @param scope - Map of variable names to values.
70
- * @returns The substituted string.
71
- */
72
- export function substituteVars(str: string, scope: Record<string, string>): string {
73
- return str.replace(/\$\{([^}]+)\}/g, (match, name: string) => scope[name] ?? match);
74
- }
75
-
76
- /** The marker that makes a file a template, always right before the final extension. */
77
- const TEMPLATE_MARKER = ".tpl";
78
-
79
- /**
80
- * The `.tpl.` twin of a path: `x.md` gives `x.tpl.md`, `x.tpl.md` gives `x.md`,
81
- * and a path with no extension has no twin.
82
- *
83
- * @param filePath - Any path, absolute or not.
84
- * @returns The twin, or undefined when there is none.
85
- */
86
- export function templateTwin(filePath: string): string | undefined {
87
- const dir = path.dirname(filePath);
88
- const base = path.basename(filePath);
89
- const ext = path.extname(base);
90
- if (ext === "" || ext === base) return undefined;
91
- const stem = base.slice(0, -ext.length);
92
- const twin = stem.endsWith(TEMPLATE_MARKER)
93
- ? `${stem.slice(0, -TEMPLATE_MARKER.length)}${ext}`
94
- : `${stem}${TEMPLATE_MARKER}${ext}`;
95
- return path.join(dir, twin);
96
- }
97
-
98
- /** Every candidate followed by its `.tpl.` twin, de-duplicated, order kept. */
99
- function withTemplateTwins(candidates: readonly string[]): string[] {
100
- const out: string[] = [];
101
- for (const candidate of candidates) {
102
- out.push(candidate);
103
- const twin = templateTwin(candidate);
104
- if (twin !== undefined) out.push(twin);
105
- }
106
- return [...new Set(out)];
107
- }
108
-
109
- /**
110
- * Split an include path into its leading alias key and the remainder. The key
111
- * is the run of characters up to the first `/` or `:` separator.
112
- *
113
- * @param p - The include path (no leading `@`).
114
- * @returns `{ key, rest }`; `rest` has no leading separator.
115
- */
116
- export function splitAliasKey(p: string): { key: string; rest: string } {
117
- const m = p.match(/^([^/:]+)[/:]([\s\S]*)$/);
118
- if (!m) return { key: p, rest: "" };
119
- return { key: m[1], rest: m[2] };
120
- }
121
-
122
- /** Options shared by {@link resolveInclude} and {@link resolveIncludeFiles}. */
123
- export type IncludeResolveOptions = {
124
- /** The resolved alias map (name → ordered base dirs); `#` names are in it too. */
125
- aliases?: AliasMap;
126
- /**
127
- * The files each view lists. An include of `#name/<path or glob>` for a name
128
- * here selects among those virtual paths and names the real files behind the
129
- * ones that match, in the listed order.
130
- */
131
- views?: ViewMap;
132
- /** Variable scope for ${var} substitution. */
133
- scope?: Record<string, string>;
134
- /** Directory of the including file (for the relative candidate). */
135
- baseDir: string;
136
- /** Resolver consulted for a `~namespace` first segment; omitted means namespaces are unavailable. */
137
- namespaceResolver?: NamespaceResolver;
138
- /**
139
- * Absolute path of the including file, handed to the namespace resolver so it
140
- * can tell whether the include comes from inside a recipe. Defaults to
141
- * `baseDir` for callers that only know the directory.
142
- */
143
- fromFile?: string;
144
- };
145
-
146
- /** A `~` reference that produced no usable candidates, kept for error reporting. */
147
- export type NamespaceIssue = {
148
- /** The reference with its `~` sigil taken off. */
149
- reference: string;
150
- /** The file (or directory) that performed the include. */
151
- fromFile: string;
152
- /** What the resolver returned. */
153
- resolution: NamespaceResolution;
154
- };
155
-
156
- /**
157
- * One place an include may be found. A group is found when any of its paths
158
- * names at least one file, and then it supplies ALL of them: a plain include
159
- * has one path per group, a glob over recipes has one per recipe.
160
- */
161
- export type IncludeGroup = {
162
- /** Absolute paths, or glob patterns when `glob` is true. */
163
- paths: string[];
164
- /** True when the paths are patterns to expand. */
165
- glob: boolean;
166
- /**
167
- * The recipe each path belongs to (same length and order as `paths`), when
168
- * they came from a `~` reference. Every file a path names must have a real
169
- * path inside that recipe's real directory; one that does not (a link
170
- * leading out) is refused.
171
- */
172
- roots?: RecipeRoot[];
173
- };
174
-
175
- /** The full result of resolving one include path. */
176
- export type IncludeResolution = {
177
- /** Every group, in the order they are tried; the first that names a file wins. */
178
- groups: IncludeGroup[];
179
- /** Every path or pattern, flattened, in order and de-duplicated: what an error lists as tried. */
180
- candidates: string[];
181
- /** True when the include path holds glob syntax. */
182
- glob: boolean;
183
- /**
184
- * Present when a `~` reference could not be satisfied. The groups are still
185
- * usable (they hold the alias and relative fallbacks); this only explains
186
- * what went wrong with the reference.
187
- */
188
- namespaceIssue?: NamespaceIssue;
189
- /** Present when the path starts with a `#` name nothing registered. */
190
- hashIssue?: string;
191
- /** True when the path names a view; a glob over a view may match nothing without being an error. */
192
- view?: boolean;
193
- };
194
-
195
- /** The files an include names, and how the search went. */
196
- export type IncludeFiles = {
197
- /** Every file the include names, each once, in the order they are included. */
198
- files: string[];
199
- /** Everything that was tried. */
200
- candidates: string[];
201
- /** True when the include path holds glob syntax. */
202
- glob: boolean;
203
- /** Why a `~` reference failed, when it did. */
204
- namespaceIssue?: NamespaceIssue;
205
- /** Why a `#` name failed, when it did. */
206
- hashIssue?: string;
207
- /** True when the path names a view. */
208
- view?: boolean;
209
- };
210
-
211
- /** The `?name=value&...` query an include line may end with, after its `.md`. */
212
- const QUERY_SUFFIX = /^(.*\.md)(\?[^?\s]*=\S*)$/;
213
-
214
- /**
215
- * Splits the query off an include path: `a/b.md?name=value` gives the path
216
- * `a/b.md` and the query `?name=value`.
217
- *
218
- * @param raw - The include path, without its `@`.
219
- */
220
- export function splitIncludeQuery(raw: string): { path: string; query: string } {
221
- const match = QUERY_SUFFIX.exec(raw);
222
- return match === null ? { path: raw, query: "" } : { path: match[1]!, query: match[2]! };
223
- }
224
-
225
- /** The group of one literal path followed by its `.tpl.` twin. */
226
- function literalGroups(candidate: string, root?: RecipeRoot): IncludeGroup[] {
227
- return withTemplateTwins([candidate]).map((entry) => ({
228
- paths: [entry],
229
- glob: false,
230
- ...(root === undefined ? {} : { roots: [root] }),
231
- }));
232
- }
233
-
234
- /**
235
- * Whether a file's real path sits inside a directory's real path, so a link
236
- * cannot lead out of it. A path that cannot be resolved (it does not exist) is
237
- * not an escape.
238
- *
239
- * @param file - An absolute file path.
240
- * @param dir - An absolute directory path.
241
- */
242
- export function realPathInside(file: string, dir: string): boolean {
243
- let realFile: string;
244
- let realDir: string;
245
- try {
246
- realFile = fs.realpathSync(file);
247
- realDir = fs.realpathSync(dir);
248
- } catch {
249
- return true;
250
- }
251
- return realFile === realDir || realFile.startsWith(realDir + path.sep);
252
- }
253
-
254
- /**
255
- * Resolve an include path to its ordered groups plus any diagnostic. Nothing is
256
- * read from disk. Use this when the caller wants to report WHY a `~` or `#`
257
- * reference failed; {@link resolveIncludeFiles} reads the disk and returns the
258
- * files.
259
- *
260
- * @param rawPath - The include path with the leading `@` already stripped.
261
- * @param opts - Aliases, variable scope, including directory and optional namespace resolver.
262
- * @returns The groups, the flat candidate list and, when a reference failed, the reason.
263
- */
264
- export function resolveInclude(rawPath: string, opts: IncludeResolveOptions): IncludeResolution {
265
- const aliases = opts.aliases ?? {};
266
- const scope = opts.scope ?? {};
267
- const withQuery = expandHome(substituteVars(rawPath, scope));
268
- const substituted = splitIncludeQuery(withQuery).path;
269
- const glob = hasGlob(substituted);
270
-
271
- const finish = (
272
- groups: IncludeGroup[],
273
- extra: Pick<IncludeResolution, "namespaceIssue" | "hashIssue" | "view"> = {}
274
- ): IncludeResolution => ({
275
- groups,
276
- candidates: [...new Set(groups.flatMap((group) => group.paths))],
277
- glob,
278
- ...extra,
279
- });
280
- const single = (candidate: string): IncludeGroup[] =>
281
- glob ? [{ paths: [candidate], glob: true }] : literalGroups(candidate);
282
-
283
- // 1. Substituted (or home-expanded) to an absolute path: that, and its twin.
284
- if (path.isAbsolute(substituted)) {
285
- return finish(single(path.normalize(substituted)));
286
- }
287
-
288
- // A view lists virtual files; the include selects among them by glob or by
289
- // exact virtual path and names the real files behind the matches.
290
- const views = opts.views ?? {};
291
- const viewKey = splitAliasKey(substituted);
292
- if (viewKey.key.startsWith("#") && Object.prototype.hasOwnProperty.call(views, viewKey.key)) {
293
- const includingRecipe = opts.namespaceResolver?.includingRecipe?.(opts.fromFile ?? opts.baseDir) ?? null;
294
- if (includingRecipe !== null) {
295
- return finish([], {
296
- view: true,
297
- hashIssue:
298
- `The recipe "${includingRecipe}" may not include "${viewKey.key}", because a view lists ` +
299
- `other recipes' files.\n` +
300
- `A recipe includes another recipe's files with "@~namespace/recipe/..." and a ` +
301
- `dependency declared in its manifest ("depends" or "subscribes").`,
302
- });
303
- }
304
- const listed = views[viewKey.key]!;
305
- const wanted = viewKey.rest;
306
- const matched = listed.filter((entry) =>
307
- glob ? minimatch(entry.path, wanted, { dot: true }) : entry.path === wanted
308
- );
309
- const issue =
310
- !glob && matched.length === 0
311
- ? `The view "${viewKey.key}" lists no file at "${wanted}".\n` +
312
- (listed.length > 0
313
- ? `It lists:\n${listed.map((entry) => ` ${viewKey.key}/${entry.path}`).join("\n")}`
314
- : "It lists no files in this project.")
315
- : undefined;
316
- return finish(
317
- matched.length > 0 ? [{ paths: [...new Set(matched.map((entry) => entry.file))], glob: false }] : [],
318
- { view: true, ...(issue === undefined ? {} : { hashIssue: issue }) }
319
- );
320
- }
321
-
322
- const groups: IncludeGroup[] = [];
323
- let namespaceIssue: NamespaceIssue | undefined;
324
- let hashIssue: string | undefined;
325
-
326
- // 2. Alias bases (ordered), if the first segment is a registered alias. A
327
- // `#` name is an alias too: the registry puts every `#name` in the map.
328
- const { key, rest } = splitAliasKey(substituted);
329
- if (key && Object.prototype.hasOwnProperty.call(aliases, key)) {
330
- for (const base of aliases[key]!) groups.push(...single(path.resolve(base, rest)));
331
- } else if (key.startsWith("#") && rest.length > 0) {
332
- const known = [...Object.keys(aliases), ...Object.keys(views)].filter((name) =>
333
- name.startsWith("#")
334
- );
335
- hashIssue =
336
- `There is no built-in name "${key}" available here.\n` +
337
- (known.length > 0
338
- ? `Available names: ${known.sort().join(", ")}.`
339
- : "No '#' names are available in this project.");
340
- }
341
-
342
- // 3. A recipe reference: a `~` sigil, then namespace, recipe and path. Only
343
- // when a resolver was supplied and a `/` follows, so a file that is
344
- // simply named `~notes.md` is still a relative path.
345
- if (
346
- opts.namespaceResolver &&
347
- substituted.startsWith("~") &&
348
- substituted.length > 1 &&
349
- substituted.includes("/")
350
- ) {
351
- const reference = withQuery.slice(1);
352
- const fromFile = opts.fromFile ?? opts.baseDir;
353
- const resolution = opts.namespaceResolver.resolve({ reference, fromFile });
354
-
355
- if (resolution.kind === "candidates" && resolution.glob === true) {
356
- groups.push({
357
- paths: resolution.candidates.map((c) => c),
358
- glob: true,
359
- ...(resolution.roots === undefined ? {} : { roots: resolution.roots }),
360
- });
361
- } else if (resolution.kind === "candidates" && resolution.candidates.length > 0) {
362
- resolution.candidates.forEach((candidate, index) => {
363
- groups.push(...literalGroups(path.normalize(candidate), resolution.roots?.[index]));
364
- });
365
- } else if (resolution.kind !== "candidates") {
366
- namespaceIssue = { reference, fromFile, resolution };
367
- }
368
- }
369
-
370
- // 4. Relative fallback: the FULL substituted path under the including dir.
371
- groups.push(...single(path.resolve(opts.baseDir, substituted)));
372
-
373
- return finish(groups, {
374
- ...(namespaceIssue === undefined ? {} : { namespaceIssue }),
375
- ...(hashIssue === undefined ? {} : { hashIssue }),
376
- });
377
- }
378
-
379
- /**
380
- * Expands a glob pattern to the files it matches, sorted by path bytewise so
381
- * the order is the same on every machine.
382
- *
383
- * @param pattern - An absolute glob pattern.
384
- */
385
- function expandPattern(pattern: string): string[] {
386
- return globSync(toGlobPattern(pattern), { absolute: true, nodir: true, dot: true }).sort(
387
- compareBytewise
388
- );
389
- }
390
-
391
- /**
392
- * Resolve an include path to the files it names. The groups are tried in
393
- * order and the first that names any file supplies all of them: a plain path
394
- * is one file, a glob is every file it matches in sorted path order.
395
- *
396
- * @param rawPath - The include path with the leading `@` already stripped.
397
- * @param opts - Aliases, variable scope, including directory and optional namespace resolver.
398
- * @returns The files (empty when nothing was found) and what was tried.
399
- */
400
- export function resolveIncludeFiles(rawPath: string, opts: IncludeResolveOptions): IncludeFiles {
401
- const resolution = resolveInclude(rawPath, opts);
402
- const out: IncludeFiles = {
403
- files: [],
404
- candidates: resolution.candidates,
405
- glob: resolution.glob,
406
- ...(resolution.namespaceIssue === undefined ? {} : { namespaceIssue: resolution.namespaceIssue }),
407
- ...(resolution.hashIssue === undefined ? {} : { hashIssue: resolution.hashIssue }),
408
- ...(resolution.view === true ? { view: true } : {}),
409
- };
410
- for (const group of resolution.groups) {
411
- const found = group.glob
412
- ? group.paths.flatMap(expandPattern)
413
- : group.paths.filter((candidate) => fs.existsSync(candidate));
414
- if (group.roots !== undefined) {
415
- const roots = group.roots;
416
- const leaving = group.paths.flatMap((entry, index) => {
417
- const root = roots[index];
418
- if (root === undefined) return [];
419
- const files = group.glob ? expandPattern(entry) : fs.existsSync(entry) ? [entry] : [];
420
- return files.filter((file) => !realPathInside(file, root.dir)).map(() => root);
421
- });
422
- const { recipe } = leaving[0] ?? { recipe: "" };
423
- if (leaving.length > 0) {
424
- out.files = [];
425
- out.namespaceIssue = {
426
- reference: rawPath.replace(/^~/, ""),
427
- fromFile: opts.fromFile ?? opts.baseDir,
428
- resolution: { kind: "escapes-recipe", recipe, reference: rawPath.replace(/^~/, "") },
429
- };
430
- return out;
431
- }
432
- }
433
- if (found.length > 0) {
434
- out.files = [...new Set(found)];
435
- return out;
436
- }
437
- }
438
- return out;
439
- }
440
-
441
- /**
442
- * Compute the ordered list of candidate absolute paths for an include.
443
- *
444
- * @param rawPath - The include path with the leading `@` already stripped.
445
- * @param opts - Aliases, variable scope, including directory and optional namespace resolver.
446
- * @returns Ordered, de-duplicated absolute candidate paths (patterns, for a glob).
447
- */
448
- export function resolveIncludeCandidates(rawPath: string, opts: IncludeResolveOptions): string[] {
449
- return resolveInclude(rawPath, opts).candidates;
450
- }
451
-
452
- /**
453
- * Expand a leading alias segment in a path or glob pattern to one candidate
454
- * per alias base, in the alias's base order. Used for config entry paths
455
- * (`entryGlob`/watch patterns), where, unlike @include resolution, there is
456
- * no including file to supply a relative fallback, so a non-alias path is
457
- * returned unchanged as the sole candidate.
458
- *
459
- * @param p - The path or glob pattern (already ${var}-substituted).
460
- * @param aliases - The resolved alias map (name → ordered base dirs).
461
- * @returns Ordered candidate paths/patterns; `[p]` when no alias applies.
462
- */
463
- export function resolveAliasPrefix(p: string, aliases: AliasMap): string[] {
464
- if (path.isAbsolute(p)) return [p];
465
- const { key, rest } = splitAliasKey(p);
466
- if (!key || !Object.prototype.hasOwnProperty.call(aliases, key)) return [p];
467
- return aliases[key].map((base) => path.resolve(base, rest));
468
- }
469
-
470
- /**
471
- * Build the resolved alias map from built-in aliases and user-defined
472
- * `_aliases` entries.
473
- *
474
- * Precedence (later prepends to earlier so user/project entries are tried
475
- * FIRST, then fall through to built-in bases):
476
- * built-ins → root _aliases → project _aliases
477
- *
478
- * Each user alias value may be a single string or an array of strings, and
479
- * each is run through ${var} substitution against `scope`.
480
- *
481
- * User aliases may NOT use names beginning with `~` or `#` (reserved);
482
- * such entries are rejected via `onError` and ignored.
483
- *
484
- * @param opts.builtIns - Built-in alias map (already absolute; `#`-prefixed names).
485
- * @param opts.userAliases - Ordered list of user `_aliases` blocks (root, then project).
486
- * @param opts.scope - Variable scope for substituting alias values.
487
- * @param opts.onError - Called with a message for each rejected/invalid entry.
488
- * @returns The merged alias map.
489
- */
490
- export function buildAliasMap(opts: {
491
- builtIns?: AliasMap;
492
- userAliases?: Array<Record<string, string | string[]> | undefined>;
493
- scope?: Record<string, string>;
494
- onError?: (message: string) => void;
495
- }): AliasMap {
496
- const scope = opts.scope ?? {};
497
- const out: AliasMap = {};
498
-
499
- // Start with built-ins.
500
- for (const [name, bases] of Object.entries(opts.builtIns ?? {})) {
501
- out[name] = [...bases];
502
- }
503
-
504
- // Apply user blocks in order; each PREPENDS its bases so user entries win
505
- // but still fall through to any built-in bases of the same name.
506
- for (const block of opts.userAliases ?? []) {
507
- if (!block) continue;
508
- for (const [name, value] of Object.entries(block)) {
509
- if (name.startsWith("~") || name.startsWith("#")) {
510
- opts.onError?.(
511
- `Alias "${name}" is invalid: names beginning with "~" or "#" are reserved ` +
512
- `("~" for the home directory and recipe namespaces, "#" for built-in names).`
513
- );
514
- continue;
515
- }
516
- const values = Array.isArray(value) ? value : [value];
517
- const bases = values.map((v) => substituteVars(v, scope));
518
- out[name] = [...bases, ...(out[name] ?? [])];
519
- }
520
- }
521
-
522
- return out;
523
- }
@@ -1,165 +0,0 @@
1
- /**
2
- * The one rule for "may sous ask a question right now?".
3
- *
4
- * Every prompt in sous (the trust question, the subscribe confirmation, the
5
- * choice between candidate refs, a variable question) is gated by this single
6
- * predicate, so a run either can ask all of them or none of them. Three things
7
- * can take the terminal away:
8
- *
9
- * - the global `--non-interactive` flag, which says so outright;
10
- * - a truthy `CI` environment variable, which every continuous integration
11
- * runner sets and which means no human is watching;
12
- * - stdin or stdout not being a terminal, which is what piping or scripting
13
- * a command looks like from in here.
14
- *
15
- * When a prompt cannot be shown, the run fails rather than guessing, and the
16
- * failure names both the question that could not be asked and the flag (or the
17
- * environment variables) that would have answered it ahead of time. Every
18
- * confirmation in sous is answered by one shared flag, `--yes` (also `-y`,
19
- * `--force`, and `--trust` on the commands that trust a repository), so a
20
- * remedy names that flag and lists its other spellings once. The command layer
21
- * prints the command's own help underneath that error, so the caller can see
22
- * every flag without going looking; `sous help <command>` prints the same
23
- * screen on demand.
24
- *
25
- * Every input is injectable, so a test can describe a terminal, a pipe or a CI
26
- * runner without touching the real process.
27
- */
28
-
29
- import { ConfigError } from "./errors.js";
30
-
31
- /** Everything the rule reads. Each field defaults to the real process. */
32
- export type InteractiveInputs = {
33
- /** The command line, scanned for `--non-interactive`. Defaults to `process.argv`. */
34
- argv?: string[];
35
- /** The environment, read for `CI`. Defaults to `process.env`. */
36
- env?: NodeJS.ProcessEnv;
37
- /** Whether stdin is a terminal. Defaults to the real stdin. */
38
- stdinIsTTY?: boolean;
39
- /** Whether stdout is a terminal. Defaults to the real stdout. */
40
- stdoutIsTTY?: boolean;
41
- };
42
-
43
- /** The global flag that turns every prompt into an error. */
44
- export const NON_INTERACTIVE_FLAG = "--non-interactive";
45
-
46
- /**
47
- * Values of `CI` that mean "not set". Everything else counts as set, because
48
- * runners spell it `1`, `true` and `yes` in roughly equal measure and a run
49
- * that guesses wrong hangs forever waiting on a question nobody can see.
50
- */
51
- const FALSY_CI_VALUES = new Set(["", "0", "false", "no", "off"]);
52
-
53
- /**
54
- * True when sous is attached to a terminal in both directions, was not told to
55
- * stay quiet, and is not running inside a continuous integration runner.
56
- *
57
- * @param inputs - Overrides for the command line, environment and streams.
58
- */
59
- export function isInteractive(inputs: InteractiveInputs = {}): boolean {
60
- return nonInteractiveReason(inputs) === undefined;
61
- }
62
-
63
- /**
64
- * Why this run cannot ask a question, as a plain sentence, or undefined when it
65
- * can. The reason is quoted in the error a blocked prompt raises, because "sous
66
- * is not running where it can ask" is baffling on its own when the caller is
67
- * sitting at a terminal and only `CI=1` in their shell made it true.
68
- *
69
- * @param inputs - Overrides for the command line, environment and streams.
70
- */
71
- export function nonInteractiveReason(inputs: InteractiveInputs = {}): string | undefined {
72
- const argv = inputs.argv ?? process.argv;
73
- const env = inputs.env ?? process.env;
74
- const stdinIsTTY = inputs.stdinIsTTY ?? process.stdin.isTTY === true;
75
- const stdoutIsTTY = inputs.stdoutIsTTY ?? process.stdout.isTTY === true;
76
-
77
- if (hasNonInteractiveFlag(argv)) {
78
- return `the '${NON_INTERACTIVE_FLAG}' flag was passed`;
79
- }
80
-
81
- const ci = env.CI;
82
- if (ci !== undefined && !FALSY_CI_VALUES.has(ci.trim().toLowerCase())) {
83
- return `the 'CI' environment variable is set to '${ci}'`;
84
- }
85
-
86
- if (!stdinIsTTY && !stdoutIsTTY) return "neither input nor output is a terminal";
87
- if (!stdinIsTTY) return "input is not a terminal";
88
- if (!stdoutIsTTY) return "output is not a terminal";
89
-
90
- return undefined;
91
- }
92
-
93
- /**
94
- * True when the command line carries `--non-interactive`. Scanning stops at a
95
- * bare `--`, exactly as the config-flag readers do: anything after it belongs to
96
- * a launched tool, never to sous.
97
- *
98
- * @param argv - The raw command line.
99
- */
100
- function hasNonInteractiveFlag(argv: string[]): boolean {
101
- for (const arg of argv) {
102
- if (arg === "--") return false;
103
- if (arg === NON_INTERACTIVE_FLAG) return true;
104
- }
105
- return false;
106
- }
107
-
108
- /**
109
- * A ConfigError raised because a question could not be asked. It carries a flag
110
- * telling the command layer to print the command's own help underneath it, so
111
- * the caller sees every flag that could have answered the question.
112
- */
113
- export class NonInteractiveError extends ConfigError {
114
- /** Tells `BaseCommand` to print the command's help after this error. */
115
- readonly showHelp = true;
116
-
117
- constructor(message: string) {
118
- super(message);
119
- this.name = "NonInteractiveError";
120
- }
121
- }
122
-
123
- /** True when an error asked for the command's help to be printed with it. */
124
- export function wantsHelp(error: unknown): boolean {
125
- return (
126
- typeof error === "object" &&
127
- error !== null &&
128
- (error as { showHelp?: boolean }).showHelp === true
129
- );
130
- }
131
-
132
- /** What a blocked prompt needs to describe itself. */
133
- export type BlockedPrompt = {
134
- /** The question that could not be asked, named as a person would name it. */
135
- prompt: string;
136
- /**
137
- * How to answer it ahead of time: a flag, or the environment variables. A
138
- * remedy names the flag's primary spelling, and lists its alternate spellings
139
- * once, in one parenthetical; the alternates are defined in
140
- * `utils/flags.ts` and the command's own help (printed underneath this error)
141
- * lists them too.
142
- */
143
- remedy: string;
144
- /** Extra lines of context, such as the candidates that could not be chosen between. */
145
- details?: string[];
146
- /** Overrides for the command line, environment and streams. */
147
- inputs?: InteractiveInputs;
148
- };
149
-
150
- /**
151
- * Builds the error a blocked prompt raises: what could not be asked, why sous
152
- * could not ask it, and what would have answered it without a terminal.
153
- *
154
- * @param blocked - The prompt, the remedy and any extra context.
155
- */
156
- export function nonInteractiveError(blocked: BlockedPrompt): NonInteractiveError {
157
- const reason = nonInteractiveReason(blocked.inputs ?? {}) ?? "there is no terminal to ask on";
158
- const lines = [
159
- `Sous has to ask ${blocked.prompt}, and it is not running where it can ask.`,
160
- ` Why: ${reason}.`,
161
- ];
162
- for (const detail of blocked.details ?? []) lines.push(` ${detail}`);
163
- lines.push(` Answer it ahead of time: ${blocked.remedy}`);
164
- return new NonInteractiveError(lines.join("\n"));
165
- }