@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,36 +0,0 @@
1
- /**
2
- * Marking a class as injectable without decorator syntax.
3
- *
4
- * sous runs TypeScript through tsx, and the published `bin/run.js` registers
5
- * tsx from the directory a command runs in, so the `experimentalDecorators`
6
- * flag in this repository's tsconfig does not reach it; esbuild also emits no
7
- * decorator metadata. Inversify's decorators are plain functions, though, so
8
- * calling them here marks every constructor parameter with its token
9
- * explicitly and needs neither a compiler flag nor `reflect-metadata`.
10
- */
11
-
12
- import { decorate, inject, injectable, multiInject } from "inversify";
13
-
14
- /** One constructor parameter: a token bound once, or a token bound many times. */
15
- export type ConstructorParameter = symbol | { multi: symbol };
16
-
17
- /**
18
- * Marks a class injectable and names the token of every constructor parameter,
19
- * in order.
20
- *
21
- * makeInjectable(RefParser, [{ multi: REF_TOKENS.Splitter }]);
22
- *
23
- * @param target - The class.
24
- * @param parameters - The token of each constructor parameter, in order.
25
- */
26
- export function makeInjectable(
27
- target: new (...args: never[]) => unknown,
28
- parameters: ConstructorParameter[] = []
29
- ): void {
30
- decorate(injectable(), target);
31
- parameters.forEach((parameter, index) => {
32
- const decorator =
33
- typeof parameter === "symbol" ? inject(parameter) : multiInject(parameter.multi);
34
- decorate(decorator, target, index);
35
- });
36
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * Building a `RefLocation`: where a repository lives, in the form every ref
3
- * and every machine-wide key agrees on.
4
- */
5
-
6
- import { repoIdentity } from "../../lib/repos/identity.js";
7
- import {
8
- requireProvider,
9
- type RepoProvider,
10
- } from "../../lib/repos/providers/index.js";
11
- import type { RefLocation } from "./types.js";
12
-
13
- /**
14
- * Where one repository lives, or undefined when its provider cannot take the
15
- * path apart (a path of one segment, say).
16
- *
17
- * @param provider - The provider that reads the location.
18
- * @param host - The repository's host.
19
- * @param repoPath - The repository's path on that host.
20
- */
21
- export function locationFor(
22
- provider: RepoProvider,
23
- host: string,
24
- repoPath: string
25
- ): RefLocation | undefined {
26
- const url = `https://${host}/${repoPath}`;
27
- try {
28
- return {
29
- provider: provider.id,
30
- host,
31
- repoPath,
32
- identity: repoIdentity(provider.canonicalize(url)),
33
- url,
34
- };
35
- } catch {
36
- return undefined;
37
- }
38
- }
39
-
40
- /**
41
- * Where the repository at a configured URL lives, or undefined when no
42
- * provider recognizes it. The location's `url` is the URL as the project
43
- * configured it, which is what a person is shown.
44
- *
45
- * @param url - The repository's URL, as a project's config records it.
46
- * @param providers - The providers to ask. Defaults to the built-ins.
47
- */
48
- export function locationFromUrl(url: string, providers?: RepoProvider[]): RefLocation | undefined {
49
- try {
50
- const provider = requireProvider(url, undefined, providers);
51
- const canonical = provider.canonicalize(url);
52
- return {
53
- provider: provider.id,
54
- host: canonical.host,
55
- repoPath: `${canonical.owner}/${canonical.name}`,
56
- identity: repoIdentity(canonical),
57
- url,
58
- };
59
- } catch {
60
- return undefined;
61
- }
62
- }
@@ -1,53 +0,0 @@
1
- import type { IndexFile } from "../../../lib/repos/formats/index-file.js";
2
- import { locationFromUrl } from "../location.js";
3
- import type { SousRef } from "../types.js";
4
- import { CatalogMatcher, catalogRepoOfIndex, type CatalogRepo } from "./catalog-matcher.js";
5
- import type { RefLookup, RefMatch } from "./ref-lookup.js";
6
-
7
- /** What a `CachedIndexLookup` needs to know beyond the indexes themselves. */
8
- export type CachedIndexLookupOptions = {
9
- /**
10
- * The repositories' short names, in the order their matches should be
11
- * listed: the built-in repository first, then the ones the config names.
12
- * Defaults to the order the indexes were added to the map.
13
- */
14
- order?: readonly string[];
15
- /**
16
- * Where each repository lives, by short name. It is how a ref written as a
17
- * location (a URL, a locator, a URL copied from the browser) finds the
18
- * repository whatever short name this project gave it.
19
- */
20
- urls?: Record<string, string | undefined>;
21
- };
22
-
23
- /**
24
- * Answers repositories, namespaces and recipes from the repository indexes
25
- * already on this machine. It reads nothing, fetches nothing and never fails:
26
- * a candidate no index publishes simply has no match.
27
- */
28
- export class CachedIndexLookup implements RefLookup {
29
- private readonly matcher: CatalogMatcher;
30
-
31
- /**
32
- * @param indexes - Each repository's cached index, keyed by short name.
33
- * @param options - The listing order, and where each repository lives.
34
- */
35
- constructor(indexes: Map<string, IndexFile>, options: CachedIndexLookupOptions = {}) {
36
- const order = options.order ?? [...indexes.keys()];
37
- const repos: CatalogRepo[] = [];
38
-
39
- for (const name of order) {
40
- const index = indexes.get(name);
41
- if (index === undefined) continue;
42
- const url = options.urls?.[name];
43
- const location = url === undefined ? undefined : locationFromUrl(url);
44
-
45
- repos.push(catalogRepoOfIndex(index, { name, ...(location === undefined ? {} : { location }) }));
46
- }
47
- this.matcher = new CatalogMatcher(repos);
48
- }
49
-
50
- async find(candidate: SousRef): Promise<RefMatch[]> {
51
- return this.matcher.match(candidate);
52
- }
53
- }
@@ -1,23 +0,0 @@
1
- import type { SousRef } from "../types.js";
2
- import { CatalogMatcher, type CatalogRepo } from "./catalog-matcher.js";
3
- import type { RefLookup, RefMatch } from "./ref-lookup.js";
4
-
5
- /**
6
- * Answers repositories, namespaces and recipes from a catalog a caller has
7
- * already built (which repositories it knows, and what each publishes). It
8
- * reads nothing, fetches nothing and never fails.
9
- */
10
- export class CatalogLookup implements RefLookup {
11
- private readonly matcher: CatalogMatcher;
12
-
13
- /**
14
- * @param repos - The repositories to search, in the order their matches are listed.
15
- */
16
- constructor(repos: CatalogRepo[]) {
17
- this.matcher = new CatalogMatcher(repos);
18
- }
19
-
20
- async find(candidate: SousRef): Promise<RefMatch[]> {
21
- return this.matcher.match(candidate);
22
- }
23
- }
@@ -1,261 +0,0 @@
1
- /**
2
- * Matching repositories, namespaces and recipes against a catalog of what is
3
- * published. Every lookup that knows repositories (cached indexes, fetched
4
- * indexes, the definitions in play) builds a catalog and asks this.
5
- *
6
- * Names are matched the same way everywhere: the exact spelling first, then
7
- * ignoring case, and a name may be a glob pattern. A match is returned in the
8
- * spelling the catalog publishes.
9
- */
10
-
11
- import type { IndexFile } from "../../../lib/repos/formats/index-file.js";
12
- import { matchName, type NameMatch } from "../glob.js";
13
- import { refKey } from "../format.js";
14
- import type { NamespaceRef, RecipeRef, RefLocation, RepoRef, SousRef } from "../types.js";
15
- import type { RefMatch } from "./ref-lookup.js";
16
-
17
- /** One repository, and what it publishes. */
18
- export type CatalogRepo = {
19
- /** The short name this project calls it, when it has one. */
20
- name?: string;
21
- /** Where it lives, which is how a ref written as a location finds it. */
22
- location?: RefLocation;
23
- /** Every namespace it publishes. */
24
- namespaces: Array<{ name: string; description?: string }>;
25
- /** Every recipe it publishes. */
26
- recipes: Array<{ namespace: string; name: string; description?: string; path?: string }>;
27
- };
28
-
29
- /**
30
- * The catalog entry for one repository's index.
31
- *
32
- * @param index - The repository's index.
33
- * @param repo - What the project knows of the repository: its short name and where it lives.
34
- */
35
- export function catalogRepoOfIndex(
36
- index: IndexFile,
37
- repo: { name?: string; location?: RefLocation } = {}
38
- ): CatalogRepo {
39
- return {
40
- ...(repo.name === undefined ? {} : { name: repo.name }),
41
- ...(repo.location === undefined ? {} : { location: repo.location }),
42
- namespaces: Object.entries(index.namespaces).map(([namespace, declared]) => ({
43
- name: namespace,
44
- ...(declared?.description === undefined ? {} : { description: declared.description }),
45
- })),
46
- recipes: Object.entries(index.recipes).map(([key, recipe]) => {
47
- const slash = key.indexOf("/");
48
- return {
49
- namespace: key.slice(0, slash),
50
- name: key.slice(slash + 1),
51
- path: recipe.path,
52
- ...(recipe.description === undefined ? {} : { description: recipe.description }),
53
- };
54
- }),
55
- };
56
- }
57
-
58
- /** The weaker of two matches: one folded part makes the whole thing folded. */
59
- function both(left: NameMatch, right: NameMatch): NameMatch {
60
- if (left === undefined || right === undefined) return undefined;
61
- return left === "folded" || right === "folded" ? "folded" : "exact";
62
- }
63
-
64
- /** Matches refs against a catalog, repository by repository, in catalog order. */
65
- export class CatalogMatcher {
66
- /**
67
- * @param repos - The repositories to search, in the order their matches are listed.
68
- */
69
- constructor(private readonly repos: CatalogRepo[]) {}
70
-
71
- /**
72
- * Every repository, namespace or recipe in the catalog a candidate names.
73
- * Candidates of any other kind match nothing.
74
- *
75
- * @param candidate - One reading of a written ref.
76
- */
77
- match(candidate: SousRef): RefMatch[] {
78
- const found: Array<RefMatch & { repoIndex: number }> = [];
79
-
80
- this.repos.forEach((repo, repoIndex) => {
81
- const add = (ref: SousRef, spelling: NameMatch): void => {
82
- if (spelling === undefined) return;
83
- found.push({ ref, exactSpelling: spelling === "exact", repoIndex });
84
- };
85
-
86
- switch (candidate.kind) {
87
- case "repo": {
88
- if (candidate.browsed !== undefined) {
89
- for (const settled of this.settleBrowsed(candidate, repo)) add(settled, "exact");
90
- } else {
91
- add(...this.matchRepo(candidate, repo));
92
- }
93
- break;
94
- }
95
- case "namespace": {
96
- const qualifier = this.qualifies(candidate.repo, repo);
97
- for (const namespace of repo.namespaces) {
98
- const spelling = both(qualifier, matchName(candidate.name, namespace.name));
99
- if (spelling === undefined) continue;
100
- add(
101
- this.carry(candidate, {
102
- kind: "namespace",
103
- name: namespace.name,
104
- repo: this.knownRepo(repo),
105
- ...(namespace.description === undefined ? {} : { description: namespace.description }),
106
- }),
107
- spelling
108
- );
109
- }
110
- break;
111
- }
112
- case "recipe": {
113
- const qualifier = this.qualifies(candidate.namespace?.repo ?? candidate.repo, repo);
114
- for (const recipe of repo.recipes) {
115
- const inNamespace =
116
- candidate.namespace === undefined
117
- ? "exact"
118
- : matchName(candidate.namespace.name, recipe.namespace);
119
- const spelling = both(
120
- both(qualifier, inNamespace),
121
- matchName(candidate.name, recipe.name)
122
- );
123
- if (spelling === undefined) continue;
124
- add(this.carry(candidate, this.knownRecipe(repo, recipe)), spelling);
125
- }
126
- break;
127
- }
128
- default:
129
- break;
130
- }
131
- });
132
-
133
- return found
134
- .sort((left, right) =>
135
- left.repoIndex !== right.repoIndex
136
- ? left.repoIndex - right.repoIndex
137
- : refKey(left.ref) < refKey(right.ref)
138
- ? -1
139
- : refKey(left.ref) > refKey(right.ref)
140
- ? 1
141
- : 0
142
- )
143
- .map(({ ref, exactSpelling }) => ({ ref, exactSpelling }));
144
- }
145
-
146
- /** How a repository qualifier matches one catalog repository (no qualifier matches all). */
147
- private qualifies(qualifier: RepoRef | undefined, repo: CatalogRepo): NameMatch {
148
- if (qualifier === undefined) return "exact";
149
- let result: NameMatch = "exact";
150
- if (qualifier.name !== undefined) {
151
- if (repo.name === undefined) return undefined;
152
- result = matchName(qualifier.name, repo.name);
153
- if (result === undefined) return undefined;
154
- }
155
- if (qualifier.location !== undefined) {
156
- if (repo.location?.identity !== qualifier.location.identity) return undefined;
157
- }
158
- return result;
159
- }
160
-
161
- /** The repository ref a catalog repository is known as. */
162
- private knownRepo(repo: CatalogRepo, revision?: string): RepoRef {
163
- return {
164
- kind: "repo",
165
- ...(repo.name === undefined ? {} : { name: repo.name }),
166
- ...(repo.location === undefined ? {} : { location: repo.location }),
167
- ...(revision === undefined ? {} : { revision }),
168
- };
169
- }
170
-
171
- /** The recipe ref a catalog recipe is known as, parents and all. */
172
- private knownRecipe(repo: CatalogRepo, recipe: CatalogRepo["recipes"][number], revision?: string): RecipeRef {
173
- const namespace: NamespaceRef = {
174
- kind: "namespace",
175
- name: recipe.namespace,
176
- repo: this.knownRepo(repo, revision),
177
- };
178
- return {
179
- kind: "recipe",
180
- name: recipe.name,
181
- namespace,
182
- ...(recipe.description === undefined ? {} : { description: recipe.description }),
183
- };
184
- }
185
-
186
- /** What a candidate carries over to the known ref it matched: its range and query values. */
187
- private carry(candidate: SousRef, known: SousRef): SousRef {
188
- return {
189
- ...known,
190
- ...(candidate.kind === "recipe" && candidate.range !== undefined
191
- ? { range: candidate.range }
192
- : {}),
193
- ...(candidate.vars === undefined ? {} : { vars: candidate.vars }),
194
- } as SousRef;
195
- }
196
-
197
- /** A repository ref, named by short name or by location. */
198
- private matchRepo(candidate: RepoRef, repo: CatalogRepo): [SousRef, NameMatch] {
199
- const known = this.knownRepo(repo);
200
- if (candidate.name === undefined && candidate.location === undefined) return [known, undefined];
201
- return [
202
- { ...known, ...(candidate.vars === undefined ? {} : { vars: candidate.vars }) },
203
- this.qualifies(candidate, repo),
204
- ];
205
- }
206
-
207
- /**
208
- * What a browser path names in one repository, through the folder each
209
- * recipe lives in. The path still carries the branch in front of it (which
210
- * may itself hold slashes), so every split is tried: the folder of a recipe,
211
- * or anything inside it, names that recipe; a folder holding recipes of
212
- * exactly one namespace names that namespace.
213
- *
214
- * settleBrowsed("main/recipes/workflow/alpha/SKILL.md")
215
- * // -> the recipe workflow/alpha, with the revision "main"
216
- */
217
- private settleBrowsed(candidate: RepoRef, repo: CatalogRepo): SousRef[] {
218
- if (candidate.location === undefined || repo.location?.identity !== candidate.location.identity) {
219
- return [];
220
- }
221
- const segments = (candidate.browsed ?? "").split("/").filter((segment) => segment.length > 0);
222
- const recipes = repo.recipes
223
- .map((recipe) => ({ recipe, path: (recipe.path ?? "").replace(/^\.?\/+|\/+$/g, "") }))
224
- .filter((entry) => entry.path.length > 0);
225
-
226
- const range = candidate.range === undefined ? {} : { range: candidate.range };
227
- const found = new Map<string, SousRef>();
228
-
229
- // A recipe's folder, or a file inside it. The deepest recipe wins, so a
230
- // recipe nested inside another's folder is named by its own path.
231
- for (let start = 1; start < segments.length; start++) {
232
- const folder = segments.slice(start).join("/");
233
- const holding = recipes
234
- .filter((entry) => folder === entry.path || folder.startsWith(`${entry.path}/`))
235
- .sort((left, right) => right.path.length - left.path.length);
236
- const deepest = holding[0];
237
- if (deepest === undefined) continue;
238
- const revision = segments.slice(0, start).join("/");
239
- found.set(`${deepest.recipe.namespace}/${deepest.recipe.name}`, {
240
- ...this.knownRecipe(repo, deepest.recipe, revision),
241
- ...range,
242
- });
243
- }
244
- if (found.size > 0) return [...found.values()];
245
-
246
- // A folder above recipes: a namespace, when everything under it is one.
247
- for (let start = 1; start < segments.length; start++) {
248
- const folder = segments.slice(start).join("/");
249
- const under = recipes.filter((entry) => entry.path.startsWith(`${folder}/`));
250
- const namespaces = new Set(under.map((entry) => entry.recipe.namespace));
251
- if (namespaces.size !== 1) continue;
252
- const name = [...namespaces][0]!;
253
- found.set(name, {
254
- kind: "namespace",
255
- name,
256
- repo: this.knownRepo(repo, segments.slice(0, start).join("/")),
257
- });
258
- }
259
- return [...found.values()];
260
- }
261
- }
@@ -1,27 +0,0 @@
1
- import type { SousRef } from "../types.js";
2
- import type { RefLookup, RefMatch } from "./ref-lookup.js";
3
-
4
- /**
5
- * Asks several lookups in order. The first one that answers a candidate with a
6
- * non-empty list wins and the rest are never asked, so a cheap, authoritative
7
- * source (a release's record) can sit in front of a broad one (every cached
8
- * index). A lookup that throws stops the chain: a failure is never skipped.
9
- */
10
- export class ChainedLookup implements RefLookup {
11
- private readonly lookups: RefLookup[];
12
-
13
- /**
14
- * @param lookups - The lookups to ask, most authoritative first.
15
- */
16
- constructor(...lookups: RefLookup[]) {
17
- this.lookups = lookups;
18
- }
19
-
20
- async find(candidate: SousRef): Promise<RefMatch[]> {
21
- for (const lookup of this.lookups) {
22
- const matches = await lookup.find(candidate);
23
- if (matches.length > 0) return matches;
24
- }
25
- return [];
26
- }
27
- }
@@ -1,65 +0,0 @@
1
- import type { DefinedVariable } from "../../../lib/vars/definition-source.js";
2
- import { variableCandidates, type LadderContext } from "../../../lib/vars/ladder.js";
3
- import { bareName } from "../../../lib/vars/names.js";
4
- import type { SousRef, VariableRef } from "../types.js";
5
- import type { RefLookup, RefMatch } from "./ref-lookup.js";
6
- import { variableRefOf } from "./variable-lookup.js";
7
-
8
- /**
9
- * Answers an environment variable name with the variables it answers.
10
- *
11
- * A name is held whole and compared in its exact spelling only, because a
12
- * variable (`apiUrl`) and an environment variable name can differ only in case
13
- * and the spelling typed is the one meant. The names searched are the one each
14
- * definition declares (a recipe may bind an existing variable such as
15
- * `GITHUB_TOKEN`) plus every generated name on the resolution ladder that is
16
- * actually in use in `.sous/.env` or `.sous/.env.local`; generated names are
17
- * only searched when in use, because every variable generates several and a
18
- * project would otherwise be told that a name nothing has ever set names one of
19
- * its variables.
20
- */
21
- export class EnvVarLookup implements RefLookup {
22
- private readonly byName = new Map<string, VariableRef[]>();
23
-
24
- /**
25
- * @param variables - The variable definitions in play.
26
- * @param ladder - The environment layers, which decide which generated names are in use.
27
- */
28
- constructor(variables: DefinedVariable[], ladder?: LadderContext) {
29
- const inUse =
30
- ladder === undefined
31
- ? new Set<string>()
32
- : new Set([...Object.keys(ladder.localEnv), ...Object.keys(ladder.sharedEnv)]);
33
-
34
- for (const defined of variables) {
35
- const names = new Set<string>([bareName(defined.definition)]);
36
- if (ladder !== undefined) {
37
- for (const candidate of variableCandidates(defined, ladder)) {
38
- if (inUse.has(candidate.envName)) names.add(candidate.envName);
39
- }
40
- }
41
- for (const name of names) {
42
- const answered = this.byName.get(name) ?? [];
43
- answered.push(variableRefOf(defined));
44
- this.byName.set(name, answered);
45
- }
46
- }
47
- }
48
-
49
- async find(candidate: SousRef): Promise<RefMatch[]> {
50
- if (candidate.kind !== "envVar") return [];
51
- const variables = this.byName.get(candidate.name);
52
- if (variables === undefined) return [];
53
- return [
54
- {
55
- ref: {
56
- kind: "envVar",
57
- name: candidate.name,
58
- variables,
59
- ...(candidate.vars === undefined ? {} : { vars: candidate.vars }),
60
- },
61
- exactSpelling: true,
62
- },
63
- ];
64
- }
65
- }
@@ -1,150 +0,0 @@
1
- /**
2
- * Settling, at release time, a dependency that reads more than one way.
3
- *
4
- * Most dependencies say exactly what they mean: `workflow/alpha`, or
5
- * `github://owner/repo/workflow/alpha`. Two kinds do not: a GitLab URL with
6
- * nested groups, which does not say where the project path ends, and a browser
7
- * URL copied from a host's file view, which names a folder whose recipe only
8
- * that repository's index can say.
9
- *
10
- * `sous repo release` settles each of them once, by fetching the index of every
11
- * candidate repository through the provider layer and keeping the reading whose
12
- * index publishes what was named. A network failure fails the release: a
13
- * release never falls through to the next reading, because the reading it fell
14
- * through to could be the wrong one. Two readings that both publish what is
15
- * named are a genuine tie, and an error naming the spelling that reads one way.
16
- */
17
-
18
- import { ConfigError } from "../../../lib/errors.js";
19
- import { INDEX_FILENAME } from "../../../lib/repos/formats/common.js";
20
- import { parseIndexFile, type IndexFile } from "../../../lib/repos/formats/index-file.js";
21
- import {
22
- builtInProviders,
23
- providerById,
24
- type ProviderOptions,
25
- type RepoProvider,
26
- } from "../../../lib/repos/providers/index.js";
27
- import { formatRef } from "../format.js";
28
- import { locationOf } from "../parts.js";
29
- import type { RefLocation, SousRef } from "../types.js";
30
- import { CatalogMatcher, catalogRepoOfIndex } from "./catalog-matcher.js";
31
- import type { RefLookup, RefMatch } from "./ref-lookup.js";
32
-
33
- /** Fetches the index of the repository at a location. */
34
- export type IndexFetcher = (location: RefLocation) => Promise<IndexFile>;
35
-
36
- /** What a `FetchedIndexLookup` may be given. */
37
- export type FetchedIndexLookupOptions = {
38
- /** The providers to fetch through. Defaults to the built-ins. */
39
- providers?: RepoProvider[];
40
- /** Testing seams handed to every index fetch by the default fetcher. */
41
- providerOptions?: ProviderOptions;
42
- /** Replaces the default fetcher, which goes through the provider layer. */
43
- fetchIndex?: IndexFetcher;
44
- /** What the ref being settled is called in an error, such as "dependency". Defaults to "ref". */
45
- subject?: string;
46
- };
47
-
48
- /** A failure, in a sentence. */
49
- function describe(error: unknown): string {
50
- return error instanceof Error ? error.message : String(error);
51
- }
52
-
53
- /**
54
- * Answers a located candidate by fetching the index of the repository it names
55
- * (once per repository) and matching the candidate against it. A candidate
56
- * with no location names no repository to fetch from and matches nothing.
57
- */
58
- export class FetchedIndexLookup implements RefLookup {
59
- private readonly providers: RepoProvider[];
60
- private readonly fetcher: IndexFetcher;
61
- private readonly indexes = new Map<string, Promise<IndexFile>>();
62
-
63
- /**
64
- * @param options - The providers and testing seams, or a replacement fetcher.
65
- */
66
- constructor(private readonly options: FetchedIndexLookupOptions = {}) {
67
- this.providers = options.providers ?? builtInProviders();
68
- this.fetcher = options.fetchIndex ?? ((location) => this.fetchThroughProvider(location));
69
- }
70
-
71
- async find(candidate: SousRef): Promise<RefMatch[]> {
72
- const location = locationOf(candidate);
73
- if (location === undefined) return [];
74
-
75
- let index: IndexFile;
76
- try {
77
- index = await this.indexAt(location);
78
- } catch (error) {
79
- const subject = this.options.subject ?? "ref";
80
- const folder = candidate.kind === "repo" && candidate.browsed !== undefined;
81
- throw new ConfigError(
82
- `the ${subject} reads ${folder ? "as a folder in" : "as something in"} the repository ` +
83
- `at ${location.url}, and its index could not be read, so the release cannot settle ` +
84
- `what it means. A release never guesses past an index it could not read.\n` +
85
- ` ${describe(error)}`
86
- );
87
- }
88
-
89
- return new CatalogMatcher([catalogRepoOfIndex(index, { location })]).match(candidate);
90
- }
91
-
92
- /**
93
- * The one reading of a written ref that the fetched indexes confirm: every
94
- * reading is looked up, and exactly one may publish what it names.
95
- *
96
- * @param candidates - Every reading of the written ref.
97
- * @returns The one match.
98
- * @throws A ConfigError when no repository publishes it, when several do (naming the
99
- * `/*` spelling that reads one way), or when an index could not be fetched.
100
- */
101
- async settle(candidates: SousRef[]): Promise<RefMatch> {
102
- const choices: RefMatch[] = [];
103
- for (const candidate of candidates) choices.push(...(await this.find(candidate)));
104
-
105
- if (choices.length === 0) {
106
- throw new ConfigError(
107
- `no repository it could name publishes what it names. It was read as:\n` +
108
- candidates.map((candidate) => ` ${formatRef(candidate, this.providers)}`).join("\n")
109
- );
110
- }
111
- if (choices.length > 1) {
112
- const namespace = choices.find((choice) => choice.ref.kind === "namespace") ?? choices[0]!;
113
- const written = formatRef(namespace.ref, this.providers);
114
- throw new ConfigError(
115
- `it names more than one thing, and each of these publishes it:\n` +
116
- choices.map((choice) => ` ${formatRef(choice.ref, this.providers)}`).join("\n") +
117
- `\n Write the one you mean. A namespace is written with '/*' after it, as in ` +
118
- `'${namespace.ref.kind === "namespace" ? `${written}/*` : written}', and a recipe as ` +
119
- `its canonical locator above.`
120
- );
121
- }
122
- return choices[0]!;
123
- }
124
-
125
- /**
126
- * The index at a location, fetched once per repository.
127
- *
128
- * @param location - Where the repository lives.
129
- */
130
- indexAt(location: RefLocation): Promise<IndexFile> {
131
- let pending = this.indexes.get(location.identity);
132
- if (pending === undefined) {
133
- pending = this.fetcher(location);
134
- this.indexes.set(location.identity, pending);
135
- }
136
- return pending;
137
- }
138
-
139
- /** Fetches and validates the index at a location, through its provider. */
140
- private async fetchThroughProvider(location: RefLocation): Promise<IndexFile> {
141
- const provider = providerById(location.provider, this.providers);
142
- /* c8 ignore next */
143
- if (provider === undefined) throw new Error(`sous has no '${location.provider}' provider.`);
144
- const fetched = await provider.fetchIndex(
145
- provider.canonicalize(location.url),
146
- this.options.providerOptions ?? {}
147
- );
148
- return parseIndexFile(JSON.parse(fetched.text), `${location.url}/${INDEX_FILENAME}`);
149
- }
150
- }