@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,842 +0,0 @@
1
- /**
2
- * Browsing what a project trusts.
3
- *
4
- * Every trusted repository publishes an index, and the project's lockfile
5
- * records what it currently pins. Put those two together and a project can be
6
- * asked four questions without downloading anything: which namespaces exist,
7
- * what is in one of them, which recipes exist, and everything about one recipe.
8
- * This module answers all four.
9
- *
10
- * Everything here is a pure function over data the caller hands it: the cached
11
- * indexes, the lockfile, and the subscription keys the project declares. The
12
- * content a recipe contributes is known only from its own manifest, inside the
13
- * recipe's files, so the caller supplies a reader for it; a recipe whose files
14
- * are not on this machine is described from its index alone rather than being
15
- * an error, and an index recorded by a current release says how each of its
16
- * dependencies was declared.
17
- *
18
- * Refs resolve the way `sous subscribe` resolves them: `namespace`,
19
- * `namespace/recipe`, and either of those qualified with `repo:`. A one-word ref
20
- * is searched across every trusted repository, and a word that means more than
21
- * one thing is an error listing what it could have meant.
22
- */
23
-
24
- import semver from "semver";
25
- import { ConfigError } from "../errors.js";
26
- import type { IndexDependency, IndexFile } from "./formats/index-file.js";
27
- import type { Lockfile } from "./formats/lockfile.js";
28
- import type { ContentKind, RecipeManifest } from "./formats/recipe-manifest.js";
29
- import {
30
- CachedIndexLookup,
31
- RefResolveArguments,
32
- describeRef,
33
- isNamedRef,
34
- locationOf,
35
- namespaceOf,
36
- namespaceOfKey,
37
- repoOf,
38
- settleDependency,
39
- shortKey,
40
- sharedRefResolver,
41
- splitRecipeKey,
42
- type NamedRef,
43
- type RefKind,
44
- type SousRef,
45
- } from "../../services/ref-resolver/index.js";
46
- import { declarationFor, type DependencyLists } from "./declarations.js";
47
-
48
- // --- What the catalog reads ---------------------------------------------------------------------
49
-
50
- /** One trusted repository whose index has been fetched. */
51
- export type CatalogRepo = {
52
- /** The short name this project calls it. */
53
- name: string;
54
- /** Where it lives, as the project's config records it. */
55
- url?: string;
56
- /** Its cached index. */
57
- index: IndexFile;
58
- };
59
-
60
- /** Where one published recipe's files would be read from. */
61
- export type RecipeLocation = {
62
- /** The recipe key, `namespace/recipe`. */
63
- key: string;
64
- namespace: string;
65
- name: string;
66
- /** The short name of the repository publishing it. */
67
- repo: string;
68
- /** The version being described. */
69
- version: string;
70
- /** The recipe folder, relative to the repository root. */
71
- path: string;
72
- };
73
-
74
- /** Everything the catalog functions read. */
75
- export type CatalogInputs = {
76
- /**
77
- * The trusted repositories with a readable index, in the order a one-word ref
78
- * searches them: the built-in repository first, then the ones the config
79
- * names, in config order.
80
- */
81
- repos: CatalogRepo[];
82
- /** The project's lockfile, which is where a pinned version comes from. */
83
- lock: Lockfile;
84
- /** The ref keys the project subscribes to: namespaces, and `namespace/recipe`. */
85
- subscriptions: string[];
86
- /**
87
- * The repositories read from a linked working copy instead of the store, by
88
- * short name, each with the checkout's path.
89
- */
90
- linked?: Record<string, string>;
91
- /**
92
- * Reads one published recipe's manifest, when its files are on this machine.
93
- * Returning undefined means "not available", and the recipe is described from
94
- * its index alone.
95
- */
96
- readManifest?: (recipe: RecipeLocation) => RecipeManifest | undefined;
97
- /** Where one content kind's files land in this project, when anywhere does. */
98
- destinationsFor?: (kind: ContentKind) => string[];
99
- };
100
-
101
- // --- What the catalog answers -------------------------------------------------------------------
102
-
103
- /** How much of a namespace a project subscribes to. */
104
- export type NamespaceCoverage = "whole namespace" | "some recipes" | "none";
105
-
106
- /** One namespace, as the listing shows it. */
107
- export type NamespaceListing = {
108
- /** The short name of the repository publishing it. */
109
- repo: string;
110
- /** The namespace name. */
111
- namespace: string;
112
- /** The namespace's one-paragraph summary, when its index carries one. */
113
- description?: string;
114
- /** How many recipes the repository publishes in it. */
115
- recipeCount: number;
116
- /** How much of it this project subscribes to. */
117
- subscribed: NamespaceCoverage;
118
- };
119
-
120
- /** One recipe, as a listing shows it. */
121
- export type RecipeListing = {
122
- /** The recipe key, `namespace/recipe`. */
123
- key: string;
124
- namespace: string;
125
- name: string;
126
- /** The short name of the repository publishing it. */
127
- repo: string;
128
- /** The highest published version, prereleases considered only when nothing else is published. */
129
- latest?: string;
130
- /** The version this project's lockfile pins, when it pins one. */
131
- pinned?: string;
132
- /**
133
- * The linked checkout builds read this recipe from instead of the pinned
134
- * version, when the project pins it and its repository is linked.
135
- */
136
- linkedPath?: string;
137
- /** True when the project subscribes to this recipe, or to the whole namespace holding it. */
138
- subscribed: boolean;
139
- /** The recipe's one-paragraph summary, when its index carries one. */
140
- description?: string;
141
- };
142
-
143
- /** One namespace in full: the namespace itself, and every recipe in it. */
144
- export type NamespaceDetail = {
145
- repo: string;
146
- /** Where the repository lives, as the project's config records it. */
147
- repoUrl?: string;
148
- namespace: string;
149
- description?: string;
150
- subscribed: NamespaceCoverage;
151
- /** Every recipe the repository publishes in the namespace, by key. */
152
- recipes: RecipeListing[];
153
- };
154
-
155
- /** What one published version is to this project. */
156
- export type VersionStatus = "latest" | "pinned" | "latest and pinned" | "other";
157
-
158
- /** One published version of one recipe. */
159
- export type RecipeVersionListing = {
160
- /** The exact version. */
161
- version: string;
162
- /** What the version is to this project. */
163
- status: VersionStatus;
164
- /** True when the index marks it a prerelease. */
165
- prerelease: boolean;
166
- /** When it was released, when the index records it. */
167
- releasedAt?: string;
168
- };
169
-
170
- /** One dependency of the version being described. */
171
- export type RecipeDependencyListing = {
172
- /** The recipe key the dependency names, `namespace/recipe`. */
173
- key: string;
174
- /**
175
- * The manifest entry that brings it in, exactly as written (a recipe, a whole
176
- * namespace, or a locator), from the index when it records one and from the
177
- * manifest otherwise.
178
- */
179
- declared?: string;
180
- /** Whether that entry makes it a build dependency or a co-subscription. */
181
- kind?: "depends" | "subscribes";
182
- /** The exact version the release resolved it to, when the index records one. */
183
- resolvedVersion?: string;
184
- /** The range the index recorded instead, when it could not resolve an exact version. */
185
- resolvedRange?: string;
186
- /** The canonical identity of the repository publishing it, for a cross-repository dependency. */
187
- repo?: string;
188
- };
189
-
190
- /** One content kind a recipe contributes, and where its files would land. */
191
- export type RecipeContentListing = {
192
- /** The content kind, as the manifest declares it. */
193
- kind: ContentKind;
194
- /** The glob patterns the manifest includes. */
195
- include: string[];
196
- /** Every directory in this project the files would be written into. */
197
- destinations: string[];
198
- };
199
-
200
- /** One recipe in full. */
201
- export type RecipeDetail = {
202
- /** The recipe key, `namespace/recipe`. */
203
- key: string;
204
- namespace: string;
205
- name: string;
206
- /** The short name of the repository publishing it. */
207
- repo: string;
208
- /** Where that repository lives, as the project's config records it. */
209
- repoUrl?: string;
210
- /** The recipe's one-paragraph summary, when its index carries one. */
211
- description?: string;
212
- /** The recipe folder, relative to the repository root. */
213
- path: string;
214
- /** The highest published version. */
215
- latest?: string;
216
- /** The version this project's lockfile pins, when it pins one. */
217
- pinned?: string;
218
- /**
219
- * The linked checkout builds read this recipe from instead of the pinned
220
- * version, when the project pins it and its repository is linked.
221
- */
222
- linkedPath?: string;
223
- /** True when the project subscribes to this recipe, or to the whole namespace holding it. */
224
- subscribed: boolean;
225
- /**
226
- * The version everything else here describes: the pinned one when the project
227
- * pins one, and the latest published one otherwise.
228
- */
229
- describing?: string;
230
- /** Every published version, newest first. */
231
- versions: RecipeVersionListing[];
232
- /** What the described version depends on, by key. */
233
- dependencies: RecipeDependencyListing[];
234
- /**
235
- * True when the index records the described version's dependencies, even as
236
- * none at all, so an empty list means "depends on nothing" rather than "not
237
- * recorded".
238
- */
239
- dependenciesRecorded: boolean;
240
- /** What the recipe contributes, and where each kind's files land. */
241
- contents: RecipeContentListing[];
242
- /**
243
- * True when the recipe's own manifest could be read. When it is false, the
244
- * contents are unknown rather than empty, because the recipe's files are not
245
- * on this machine.
246
- */
247
- manifestRead: boolean;
248
- };
249
-
250
- // --- Listings -----------------------------------------------------------------------------------
251
-
252
- /**
253
- * Every namespace every trusted repository publishes, sorted by namespace and
254
- * then by repository, so two repositories publishing the same namespace name sit
255
- * beside each other.
256
- *
257
- * @param inputs - The cached indexes, the lockfile and the project's subscriptions.
258
- */
259
- export function listNamespaces(inputs: CatalogInputs): NamespaceListing[] {
260
- const subscriptions = new Set(inputs.subscriptions);
261
- const listings: NamespaceListing[] = [];
262
-
263
- for (const repo of inputs.repos) {
264
- for (const [namespace, declared] of Object.entries(repo.index.namespaces)) {
265
- listings.push({
266
- repo: repo.name,
267
- namespace,
268
- ...(declared.description === undefined ? {} : { description: declared.description }),
269
- recipeCount: recipeKeysIn(repo.index, namespace).length,
270
- subscribed: coverageOf(namespace, repo.index, subscriptions),
271
- });
272
- }
273
- }
274
-
275
- return listings.sort(byNamespaceThenRepo);
276
- }
277
-
278
- /**
279
- * Every recipe every trusted repository publishes, sorted by key and then by
280
- * repository.
281
- *
282
- * @param inputs - The cached indexes, the lockfile and the project's subscriptions.
283
- */
284
- export function listRecipes(inputs: CatalogInputs): RecipeListing[] {
285
- const subscriptions = new Set(inputs.subscriptions);
286
- const listings: RecipeListing[] = [];
287
-
288
- for (const repo of inputs.repos) {
289
- for (const key of Object.keys(repo.index.recipes)) {
290
- listings.push(recipeListing(key, repo, inputs, subscriptions));
291
- }
292
- }
293
-
294
- return listings.sort((left, right) =>
295
- left.key === right.key
296
- ? compare(left.repo, right.repo)
297
- : compare(left.key, right.key)
298
- );
299
- }
300
-
301
- /**
302
- * One namespace in full: what the repository says about it, and every recipe it
303
- * publishes in it.
304
- *
305
- * @param inputs - The cached indexes, the lockfile and the project's subscriptions.
306
- * @param ref - The namespace, optionally qualified with `repo:`.
307
- */
308
- export async function describeNamespace(
309
- inputs: CatalogInputs,
310
- ref: string
311
- ): Promise<NamespaceDetail> {
312
- const found = await resolveNamespaceRef(inputs, ref);
313
- const subscriptions = new Set(inputs.subscriptions);
314
- const declared = found.repo.index.namespaces[found.namespace]!;
315
-
316
- return {
317
- repo: found.repo.name,
318
- ...(found.repo.url === undefined ? {} : { repoUrl: found.repo.url }),
319
- namespace: found.namespace,
320
- ...(declared.description === undefined ? {} : { description: declared.description }),
321
- subscribed: coverageOf(found.namespace, found.repo.index, subscriptions),
322
- recipes: recipeKeysIn(found.repo.index, found.namespace).map((key) =>
323
- recipeListing(key, found.repo, inputs, subscriptions)
324
- ),
325
- };
326
- }
327
-
328
- /**
329
- * One recipe in full: its identity, every version it publishes, what it depends
330
- * on, what it asks about, and where its files would land in this project.
331
- *
332
- * @param inputs - The cached indexes, the lockfile, the subscriptions and the manifest reader.
333
- * @param ref - The recipe, as `namespace/recipe`, a bare recipe name, or either qualified with `repo:`.
334
- */
335
- export async function describeRecipe(
336
- inputs: CatalogInputs,
337
- ref: string
338
- ): Promise<RecipeDetail> {
339
- const found = await resolveRecipeRef(inputs, ref);
340
- const subscriptions = new Set(inputs.subscriptions);
341
- const listing = recipeListing(found.key, found.repo, inputs, subscriptions);
342
- const entry = found.repo.index.recipes[found.key]!;
343
-
344
- const describing = listing.pinned ?? listing.latest;
345
- const published = entry.versions[describing ?? ""];
346
-
347
- const manifest =
348
- describing === undefined || inputs.readManifest === undefined
349
- ? undefined
350
- : inputs.readManifest({
351
- key: found.key,
352
- namespace: found.namespace,
353
- name: found.name,
354
- repo: found.repo.name,
355
- version: describing,
356
- path: entry.path,
357
- });
358
-
359
- return {
360
- ...listing,
361
- ...(found.repo.url === undefined ? {} : { repoUrl: found.repo.url }),
362
- path: entry.path,
363
- ...(describing === undefined ? {} : { describing }),
364
- versions: versionListings(entry.versions, listing.latest, listing.pinned),
365
- dependencies: dependencyListings(published?.dependencies, manifest),
366
- dependenciesRecorded: published?.dependencies !== undefined,
367
- contents: (manifest?.contents ?? []).map((content) => ({
368
- kind: content.kind,
369
- include: [...content.include],
370
- destinations: inputs.destinationsFor?.(content.kind) ?? [],
371
- })),
372
- manifestRead: manifest !== undefined,
373
- };
374
- }
375
-
376
- /**
377
- * The same inputs, narrowed to what this project has installed: each
378
- * repository's index keeps only the recipes the lockfile pins from that
379
- * repository, and only the namespaces holding one of them. Every listing and
380
- * every ref lookup over the result therefore sees only installed recipes. A
381
- * recipe's published version history is kept whole, so the installed version
382
- * still sits among the others.
383
- *
384
- * narrowToInstalled(inputs)
385
- * // -> the index of "r" holds "a/x" only, when the lockfile pins "a/x" from "r"
386
- *
387
- * @param inputs - The catalog's inputs.
388
- */
389
- export function narrowToInstalled(inputs: CatalogInputs): CatalogInputs {
390
- const repos = inputs.repos.map((repo) => {
391
- const recipes = Object.fromEntries(
392
- Object.entries(repo.index.recipes).filter(
393
- ([key]) => inputs.lock.recipes[key]?.repo === repo.name
394
- )
395
- );
396
- const held = new Set(Object.keys(recipes).map(namespaceOfKey));
397
- const namespaces = Object.fromEntries(
398
- Object.entries(repo.index.namespaces).filter(([namespace]) => held.has(namespace))
399
- );
400
- return { ...repo, index: { ...repo.index, recipes, namespaces } };
401
- });
402
- return { ...inputs, repos };
403
- }
404
-
405
- /**
406
- * Looks a ref up among the installed recipes only, so an ambiguity between an
407
- * installed recipe and one the project does not use settles itself. A ref that
408
- * names something published but not installed is an error saying exactly that,
409
- * rather than the "no repository publishes" error the narrowed lookup alone
410
- * would raise.
411
- *
412
- * describeInstalled(inputs, "workflow/task-files", describeRecipe, "recipe")
413
- * // -> the recipe, when the lockfile pins it; otherwise an error saying the
414
- * // project has not installed it
415
- *
416
- * @param inputs - The catalog's inputs, not yet narrowed.
417
- * @param ref - The ref being looked up.
418
- * @param describe - The lookup to run: `describeNamespace` or `describeRecipe`.
419
- * @param what - What the ref names, for the error.
420
- */
421
- export async function describeInstalled<T>(
422
- inputs: CatalogInputs,
423
- ref: string,
424
- describe: (inputs: CatalogInputs, ref: string) => Promise<T>,
425
- what: "namespace" | "recipe"
426
- ): Promise<T> {
427
- try {
428
- return await describe(narrowToInstalled(inputs), ref);
429
- } catch {
430
- // The full lookup either explains the ref better (ambiguous, unknown) or
431
- // proves it is published and simply not installed.
432
- await describe(inputs, ref);
433
- throw what === "namespace"
434
- ? new ConfigError(
435
- `This project has installed no recipe from the namespace '${ref}', so there is ` +
436
- `nothing to show with '--installed'.`
437
- )
438
- : new ConfigError(
439
- `This project has not installed the recipe '${ref}', so there is nothing to ` +
440
- `show with '--installed'.`
441
- );
442
- }
443
- }
444
-
445
- // --- Resolving a ref ----------------------------------------------------------------------------
446
-
447
- /** One namespace a ref resolved to. */
448
- export type ResolvedNamespace = {
449
- /** The repository publishing it. */
450
- repo: CatalogRepo;
451
- /** The namespace name. */
452
- namespace: string;
453
- };
454
-
455
- /** One recipe a ref resolved to. */
456
- export type ResolvedRecipeRef = {
457
- /** The repository publishing it. */
458
- repo: CatalogRepo;
459
- /** The recipe key, `namespace/recipe`. */
460
- key: string;
461
- namespace: string;
462
- name: string;
463
- };
464
-
465
- /**
466
- * The namespace a ref names. A one-word ref is searched across every trusted
467
- * repository; a word that names a namespace in two of them is an error listing
468
- * both.
469
- *
470
- * @param inputs - The cached indexes.
471
- * @param ref - The namespace, optionally qualified with `repo:`.
472
- */
473
- export async function resolveNamespaceRef(
474
- inputs: CatalogInputs,
475
- ref: string
476
- ): Promise<ResolvedNamespace> {
477
- const parsed = readShortRef(ref);
478
-
479
- if (parsed?.kind === "recipe") {
480
- throw new ConfigError(
481
- `'${ref}' names the recipe '${parsed.name}', not a namespace.\n` +
482
- ` The namespace it belongs to is '${parsed.namespace!.name}'.`
483
- );
484
- }
485
-
486
- const matches = await findIn(inputs, ref, ["namespace"]);
487
-
488
- if (matches.length === 1) {
489
- const match = matches[0]!;
490
- return { repo: repoNamed(inputs, repoOf(match)!.name!), namespace: namespaceOf(match)!.name };
491
- }
492
-
493
- if (matches.length > 1) throw ambiguousError(ref, "namespace", matches);
494
-
495
- const asRecipe = await findIn(inputs, ref, ["recipe"]);
496
- if (parsed !== undefined && repoOf(parsed) === undefined && asRecipe.length > 0) {
497
- throw new ConfigError(
498
- `No repository this project trusts publishes a namespace called ` +
499
- `'${parsed.name}'.\n` +
500
- ` It is the name of a recipe:\n` +
501
- asRecipe.map((candidate) => ` ${describeRef(candidate)}`).join("\n")
502
- );
503
- }
504
-
505
- throw unknownError(inputs, ref, "namespace", parsed === undefined ? undefined : repoOf(parsed)?.name);
506
- }
507
-
508
- /**
509
- * The recipe a ref names. A two-segment ref names it exactly; a one-word ref is
510
- * searched as a recipe name across every trusted repository, and a name two of
511
- * them publish is an error listing both.
512
- *
513
- * @param inputs - The cached indexes.
514
- * @param ref - The recipe, as `namespace/recipe`, a bare recipe name, or either qualified with `repo:`.
515
- */
516
- export async function resolveRecipeRef(
517
- inputs: CatalogInputs,
518
- ref: string
519
- ): Promise<ResolvedRecipeRef> {
520
- const parsed = readShortRef(ref);
521
-
522
- const matches = await findIn(inputs, ref, ["recipe"]);
523
-
524
- if (matches.length === 1) {
525
- const match = matches[0]!;
526
- const namespace = namespaceOf(match)!.name;
527
- const name = (match as { name: string }).name;
528
- return {
529
- repo: repoNamed(inputs, repoOf(match)!.name!),
530
- key: `${namespace}/${name}`,
531
- namespace,
532
- name,
533
- };
534
- }
535
-
536
- if (matches.length > 1) throw ambiguousError(ref, "recipe", matches);
537
-
538
- const asNamespace = await findIn(inputs, ref, ["namespace"]);
539
- if (asNamespace.length > 0) {
540
- throw new ConfigError(
541
- `No repository this project trusts publishes a recipe called ` +
542
- `'${parsed === undefined ? ref : namespaceNameOf(parsed)}'.\n` +
543
- ` It is the name of a namespace:\n` +
544
- asNamespace.map((candidate) => ` ${describeRef(candidate)}`).join("\n")
545
- );
546
- }
547
-
548
- throw unknownError(inputs, ref, "recipe", parsed === undefined ? undefined : repoOf(parsed)?.name);
549
- }
550
-
551
- // --- The pieces ---------------------------------------------------------------------------------
552
-
553
- /** Every recipe key one index publishes in one namespace, sorted. */
554
- function recipeKeysIn(index: IndexFile, namespace: string): string[] {
555
- return Object.keys(index.recipes)
556
- .filter((key) => namespaceOfKey(key) === namespace)
557
- .sort();
558
- }
559
-
560
- /**
561
- * How much of one namespace a project subscribes to: the whole namespace when it
562
- * subscribes to the namespace itself, some recipes when it subscribes to at
563
- * least one recipe in it, and none otherwise.
564
- *
565
- * @param namespace - The namespace name.
566
- * @param index - The index publishing it.
567
- * @param subscriptions - The ref keys the project subscribes to.
568
- */
569
- function coverageOf(
570
- namespace: string,
571
- index: IndexFile,
572
- subscriptions: Set<string>
573
- ): NamespaceCoverage {
574
- if (subscriptions.has(namespace)) return "whole namespace";
575
- const some = recipeKeysIn(index, namespace).some((key) => subscriptions.has(key));
576
- return some ? "some recipes" : "none";
577
- }
578
-
579
- /**
580
- * One recipe's listing row: what the index publishes, what the lockfile pins,
581
- * and whether the project subscribes to it.
582
- *
583
- * @param key - The recipe key.
584
- * @param repo - The repository publishing it.
585
- * @param inputs - The lockfile and the linked repositories.
586
- * @param subscriptions - The ref keys the project subscribes to.
587
- */
588
- function recipeListing(
589
- key: string,
590
- repo: CatalogRepo,
591
- inputs: Pick<CatalogInputs, "lock" | "linked">,
592
- subscriptions: Set<string>
593
- ): RecipeListing {
594
- const lock = inputs.lock;
595
- const entry = repo.index.recipes[key]!;
596
- const { namespace, name } = splitRecipeKey(key);
597
- const latest = latestVersion(Object.keys(entry.versions), entry.versions);
598
-
599
- // A locked entry pins one recipe from one repository. Two repositories can
600
- // publish the same key, so the row only claims the pin when the lockfile says
601
- // the recipe came from this repository.
602
- const locked = lock.recipes[key];
603
- const pinned = locked !== undefined && locked.repo === repo.name ? locked.version : undefined;
604
- const linkedPath = pinned === undefined ? undefined : inputs.linked?.[repo.name];
605
-
606
- return {
607
- key,
608
- namespace,
609
- name,
610
- repo: repo.name,
611
- ...(latest === undefined ? {} : { latest }),
612
- ...(pinned === undefined ? {} : { pinned }),
613
- ...(linkedPath === undefined ? {} : { linkedPath }),
614
- subscribed: subscriptions.has(key) || subscriptions.has(namespace),
615
- ...(entry.description === undefined ? {} : { description: entry.description }),
616
- };
617
- }
618
-
619
- /**
620
- * The highest published version. Prereleases are considered only when a recipe
621
- * has published nothing else, so a repository mid-prerelease still reports a
622
- * latest version rather than none.
623
- *
624
- * @param versions - Every published version string.
625
- * @param published - The index entries those versions carry.
626
- */
627
- function latestVersion(
628
- versions: string[],
629
- published: Record<string, { prerelease: boolean }>
630
- ): string | undefined {
631
- const stable = versions.filter((version) => published[version]?.prerelease !== true);
632
- const pool = stable.length > 0 ? stable : versions;
633
- return semver.maxSatisfying(pool, "*", { includePrerelease: true }) ?? undefined;
634
- }
635
-
636
- /**
637
- * Every published version, newest first, each labeled with what it is to this
638
- * project.
639
- *
640
- * @param versions - The index's version entries.
641
- * @param latest - The highest published version.
642
- * @param pinned - The version the lockfile pins, when it pins one.
643
- */
644
- function versionListings(
645
- versions: Record<string, { prerelease: boolean; releasedAt?: string }>,
646
- latest: string | undefined,
647
- pinned: string | undefined
648
- ): RecipeVersionListing[] {
649
- return Object.keys(versions)
650
- .sort((left, right) => semver.rcompare(left, right, { loose: true }))
651
- .map((version) => {
652
- const entry = versions[version]!;
653
- const isLatest = version === latest;
654
- const isPinned = version === pinned;
655
- const status: VersionStatus =
656
- isLatest && isPinned
657
- ? "latest and pinned"
658
- : isLatest
659
- ? "latest"
660
- : isPinned
661
- ? "pinned"
662
- : "other";
663
- return {
664
- version,
665
- status,
666
- prerelease: entry.prerelease,
667
- ...(entry.releasedAt === undefined ? {} : { releasedAt: entry.releasedAt }),
668
- };
669
- });
670
- }
671
-
672
- /**
673
- * What one version depends on, from both sides: the dependencies the recipe's
674
- * manifest declares, and the versions the release resolved them to in the
675
- * index. A dependency that appears on only one side is still a row, because the
676
- * two disagreeing is exactly what somebody reading this needs to see.
677
- *
678
- * @param resolved - The index's record of what the version was released against.
679
- * @param manifest - The recipe's manifest, when its files could be read.
680
- */
681
- function dependencyListings(
682
- resolved: Record<string, IndexDependency> | undefined,
683
- manifest: RecipeManifest | undefined
684
- ): RecipeDependencyListing[] {
685
- const rows = new Map<string, RecipeDependencyListing>();
686
- const lists: DependencyLists | undefined =
687
- manifest === undefined
688
- ? undefined
689
- : { depends: manifest.depends ?? [], subscribes: manifest.subscribes ?? [] };
690
-
691
- for (const [key, entry] of Object.entries(resolved ?? {})) {
692
- // The index says how the release that published the version saw it. An
693
- // entry recorded before the index said so is answered from the manifest,
694
- // by the same rule the release applies.
695
- const declaration =
696
- entry.declared !== undefined && entry.kind !== undefined
697
- ? { declared: entry.declared, kind: entry.kind }
698
- : lists === undefined
699
- ? undefined
700
- : declarationFor(lists, key, resolved);
701
- rows.set(key, {
702
- key,
703
- ...(entry.version === undefined ? {} : { resolvedVersion: entry.version }),
704
- ...(entry.range === undefined ? {} : { resolvedRange: entry.range }),
705
- ...(entry.repo === undefined ? {} : { repo: entry.repo }),
706
- ...(declaration ?? {}),
707
- });
708
- }
709
-
710
- // A manifest entry that covers no row the index recorded is still shown, so a
711
- // reader sees everything the manifest declares.
712
- const covered = new Set([...rows.values()].map((row) => row.declared));
713
- for (const [kind, declared] of [
714
- ["depends", lists?.depends ?? []],
715
- ["subscribes", lists?.subscribes ?? []],
716
- ] as Array<["depends" | "subscribes", string[]]>) {
717
- for (const written of declared) {
718
- if (covered.has(written.trim())) continue;
719
- // A dependency that reads more than one way is listed under the key the
720
- // index recorded for it. One sous cannot parse or settle is still worth
721
- // showing; it is listed under what it was written as, so the reader sees
722
- // the entry as it stands.
723
- let key = written;
724
- try {
725
- const reading = settleDependency(written, {
726
- ...(resolved === undefined ? {} : { recorded: resolved }),
727
- });
728
- if (reading !== undefined && isNamedRef(reading)) key = shortKey(reading);
729
- } catch {
730
- key = written;
731
- }
732
- rows.set(key, { ...(rows.get(key) ?? { key }), declared: written, kind });
733
- }
734
- }
735
-
736
- return [...rows.values()].sort((left, right) => compare(left.key, right.key));
737
- }
738
-
739
- /**
740
- * The repository a match named. Every match comes from this catalog's own
741
- * repositories, so the lookup always finds one.
742
- *
743
- * @param inputs - The cached indexes.
744
- * @param name - The repository's short name.
745
- */
746
- function repoNamed(inputs: CatalogInputs, name: string): CatalogRepo {
747
- return inputs.repos.find((repo) => repo.name === name)!;
748
- }
749
-
750
- /**
751
- * Every ref in this catalog's repositories that a ref written on the command
752
- * line names, of the kinds given, best match first.
753
- *
754
- * @param inputs - The cached indexes.
755
- * @param ref - The ref as typed.
756
- * @param kinds - The kinds of ref the caller accepts.
757
- */
758
- async function findIn(
759
- inputs: CatalogInputs,
760
- ref: string,
761
- kinds: readonly RefKind[]
762
- ): Promise<SousRef[]> {
763
- const lookup = new CachedIndexLookup(
764
- new Map(inputs.repos.map((repo) => [repo.name, repo.index])),
765
- {
766
- order: inputs.repos.map((repo) => repo.name),
767
- urls: Object.fromEntries(inputs.repos.map((repo) => [repo.name, repo.url])),
768
- }
769
- );
770
- const result = await sharedRefResolver().resolve(new RefResolveArguments({ input: ref, lookup, kinds }));
771
- return result.refs;
772
- }
773
-
774
- /**
775
- * The namespace or recipe a ref from the command line names by its short form,
776
- * checked by the one parser: undefined when the ref names a location instead.
777
- *
778
- * @param ref - The ref as typed.
779
- */
780
- function readShortRef(ref: string): NamedRef | undefined {
781
- const { refs } = sharedRefResolver().parse(ref);
782
- return refs.find(
783
- (candidate): candidate is NamedRef => isNamedRef(candidate) && locationOf(candidate) === undefined
784
- );
785
- }
786
-
787
- /** The name a short ref was written with: its namespace for a namespace, its recipe name otherwise. */
788
- function namespaceNameOf(ref: NamedRef): string {
789
- return ref.kind === "namespace" ? ref.name : ref.namespace!.name;
790
- }
791
-
792
- /** The error a ref that could have meant several things raises. */
793
- function ambiguousError(
794
- ref: string,
795
- what: "namespace" | "recipe",
796
- candidates: SousRef[]
797
- ): ConfigError {
798
- return new ConfigError(
799
- `'${ref}' names a ${what} in more than one repository this project trusts:\n` +
800
- candidates.map((candidate) => ` ${describeRef(candidate)}`).join("\n") +
801
- `\n Name the repository as well, as 'repository:${ref}', to say which one you mean.`
802
- );
803
- }
804
-
805
- /** The error a ref that matched nothing raises, naming what was searched. */
806
- function unknownError(
807
- inputs: CatalogInputs,
808
- ref: string,
809
- what: "namespace" | "recipe",
810
- qualifier: string | undefined
811
- ): ConfigError {
812
- if (qualifier !== undefined && !inputs.repos.some((repo) => repo.name === qualifier)) {
813
- return new ConfigError(
814
- `This project trusts no repository called '${qualifier}', so '${ref}' could not ` +
815
- `be looked up.\n` +
816
- ` Repositories with an index sous has read: ${describeRepos(inputs)}.`
817
- );
818
- }
819
-
820
- return new ConfigError(
821
- `No repository this project trusts publishes a ${what} called '${ref}'.\n` +
822
- ` Repositories with an index sous has read: ${describeRepos(inputs)}.`
823
- );
824
- }
825
-
826
- /** The repositories that were searched, in plain language. */
827
- function describeRepos(inputs: CatalogInputs): string {
828
- if (inputs.repos.length === 0) return "none";
829
- return inputs.repos.map((repo) => repo.name).join(", ");
830
- }
831
-
832
- /** Sorts namespace listings by namespace, then by repository. */
833
- function byNamespaceThenRepo(left: NamespaceListing, right: NamespaceListing): number {
834
- return left.namespace === right.namespace
835
- ? compare(left.repo, right.repo)
836
- : compare(left.namespace, right.namespace);
837
- }
838
-
839
- /** Bytewise string ordering, so a listing sorts the same on every machine. */
840
- function compare(left: string, right: string): number {
841
- return left < right ? -1 : left > right ? 1 : 0;
842
- }