@sous-io/sous 0.2.32 → 0.2.34

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 (111) hide show
  1. package/README.md +7 -6
  2. package/docs/markdown/_sidebar.md +1 -0
  3. package/docs/markdown/commands.md +8 -4
  4. package/docs/markdown/configuration.md +28 -4
  5. package/docs/markdown/repositories-authoring.md +22 -2
  6. package/docs/markdown/repositories-consuming.md +40 -11
  7. package/docs/markdown/repositories-file-formats.md +28 -13
  8. package/docs/markdown/repositories-quickstart.md +1 -1
  9. package/docs/markdown/repositories-variables.md +16 -0
  10. package/docs/markdown/repositories.md +8 -4
  11. package/package.json +2 -1
  12. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +33 -20
  13. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
  14. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +21 -9
  15. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  16. package/sous.config.schema.json +27 -15
  17. package/src/commands/lock/rebuild.ts +3 -6
  18. package/src/commands/namespace/show.ts +2 -2
  19. package/src/commands/recipe/show.ts +26 -9
  20. package/src/commands/repo/contribute.ts +32 -23
  21. package/src/commands/repo/gc.ts +1 -1
  22. package/src/commands/repo/remove.ts +1 -1
  23. package/src/commands/repo/search.ts +1 -1
  24. package/src/commands/vars/ask.ts +75 -54
  25. package/src/lib/build-service.ts +93 -15
  26. package/src/lib/config-schema.ts +59 -19
  27. package/src/lib/include-resolver.ts +307 -69
  28. package/src/lib/markdown-compiler.ts +289 -82
  29. package/src/lib/project-scaffold/templates.ts +19 -11
  30. package/src/lib/repos/catalog-inputs.ts +1 -1
  31. package/src/lib/repos/catalog.ts +98 -72
  32. package/src/lib/repos/declarations.ts +22 -12
  33. package/src/lib/repos/formats/index-file.ts +1 -1
  34. package/src/lib/repos/formats/recipe-manifest.ts +3 -4
  35. package/src/lib/repos/lock-service.ts +1 -1
  36. package/src/lib/repos/locked-namespace-resolver.ts +14 -6
  37. package/src/lib/repos/locked-recipe-lookup.ts +123 -0
  38. package/src/lib/repos/locked-recipes.ts +1 -1
  39. package/src/lib/repos/memories-hash-name.ts +33 -0
  40. package/src/lib/repos/namespace-resolver.ts +198 -119
  41. package/src/lib/repos/recipe-config-layers.ts +29 -1
  42. package/src/lib/repos/recipe-key-matcher.ts +62 -0
  43. package/src/lib/repos/recipe-memories.ts +175 -0
  44. package/src/lib/repos/recipe-targets.ts +32 -28
  45. package/src/lib/repos/ref-search.ts +1 -1
  46. package/src/lib/repos/release/index-builder.ts +23 -15
  47. package/src/lib/repos/release/plan.ts +14 -7
  48. package/src/lib/repos/release/settle.ts +38 -121
  49. package/src/lib/repos/release/submit-checkout.ts +42 -23
  50. package/src/lib/repos/release/tags.ts +1 -1
  51. package/src/lib/repos/resolver.ts +90 -72
  52. package/src/lib/repos/subscription-service.ts +192 -170
  53. package/src/lib/settings.ts +37 -33
  54. package/src/lib/vars/answers.ts +9 -1
  55. package/src/lib/vars/ask.ts +3 -3
  56. package/src/lib/vars/display.ts +1 -3
  57. package/src/lib/vars/index.ts +1 -0
  58. package/src/lib/vars/mappings.ts +17 -9
  59. package/src/lib/vars/secrets.ts +131 -0
  60. package/src/services/ref-resolver/container.ts +107 -0
  61. package/src/services/ref-resolver/dependency.ts +60 -0
  62. package/src/services/ref-resolver/format.ts +197 -0
  63. package/src/services/ref-resolver/glob.ts +115 -0
  64. package/src/services/ref-resolver/hash-names.ts +155 -0
  65. package/src/services/ref-resolver/index.ts +55 -0
  66. package/src/services/ref-resolver/injectable.ts +36 -0
  67. package/src/services/ref-resolver/location.ts +62 -0
  68. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
  69. package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
  70. package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
  71. package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
  72. package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
  73. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
  74. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
  75. package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
  76. package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
  77. package/src/services/ref-resolver/named-refs.ts +144 -0
  78. package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
  79. package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
  80. package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
  81. package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
  82. package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
  83. package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
  84. package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
  85. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
  86. package/src/services/ref-resolver/parts.ts +115 -0
  87. package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
  88. package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
  89. package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
  90. package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
  91. package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
  92. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
  93. package/src/services/ref-resolver/ref-helpers.ts +62 -0
  94. package/src/services/ref-resolver/ref-picker-service.ts +204 -0
  95. package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
  96. package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
  97. package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
  98. package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
  99. package/src/services/ref-resolver/source.ts +48 -0
  100. package/src/services/ref-resolver/tokens.ts +25 -0
  101. package/src/services/ref-resolver/types.ts +155 -0
  102. package/src/templating/init-liquid-engine.ts +33 -18
  103. package/src/templating/lib/secret-scope.ts +41 -0
  104. package/src/templating/tags/exportScalarVarsJs.ts +5 -2
  105. package/src/templating/tags/showVars.ts +8 -2
  106. package/src/lib/refs/find.ts +0 -770
  107. package/src/lib/refs/index.ts +0 -14
  108. package/src/lib/refs/parse.ts +0 -844
  109. package/src/lib/refs/pick.ts +0 -147
  110. package/src/lib/refs/scopes.ts +0 -95
  111. package/src/lib/refs/settle.ts +0 -219
@@ -0,0 +1,123 @@
1
+ import type { DefinedVariable } from "../../../lib/vars/definition-source.js";
2
+ import { matchName, type NameMatch } from "../glob.js";
3
+ import { refKey } from "../format.js";
4
+ import type { RepoRef, SousRef, VariableRef } from "../types.js";
5
+ import { CatalogMatcher, type CatalogRepo } from "./catalog-matcher.js";
6
+ import type { RefLookup, RefMatch } from "./ref-lookup.js";
7
+
8
+ /**
9
+ * The ref a defined variable is known as: its name, with the recipe that
10
+ * published it, that recipe's namespace and the repository's short name nested
11
+ * as parents.
12
+ *
13
+ * @param defined - The definition and the recipe that published it.
14
+ */
15
+ export function variableRefOf(defined: DefinedVariable): VariableRef {
16
+ const { repo, namespace, name } = defined.recipe;
17
+ return {
18
+ kind: "variable",
19
+ name: defined.definition.name,
20
+ description: defined.definition.prompt,
21
+ recipe: {
22
+ kind: "recipe",
23
+ name,
24
+ namespace: { kind: "namespace", name: namespace, repo: { kind: "repo", name: repo } },
25
+ },
26
+ };
27
+ }
28
+
29
+ /** The weaker of two matches: one folded part makes the whole thing folded. */
30
+ function both(left: NameMatch, right: NameMatch): NameMatch {
31
+ if (left === undefined || right === undefined) return undefined;
32
+ return left === "folded" || right === "folded" ? "folded" : "exact";
33
+ }
34
+
35
+ /**
36
+ * Answers from the variable definitions in play, which is what a variables
37
+ * command searches: variables themselves (by name, or qualified by recipe,
38
+ * namespace and repository, in any combination) and the repositories,
39
+ * namespaces and recipes that publish them. A namespace that publishes no
40
+ * variable this project holds has no questions to ask, so naming it would
41
+ * resolve to nothing rather than to an empty set of questions. Building the
42
+ * answer from the definitions also means `sous vars ask --file` resolves refs
43
+ * exactly as a subscribed project does.
44
+ */
45
+ export class VariableLookup implements RefLookup {
46
+ private readonly matcher: CatalogMatcher;
47
+
48
+ /**
49
+ * @param variables - The variable definitions in play, with the recipe that published each.
50
+ * @param trusted - The trusted repositories, which lend each repository its location
51
+ * and each recipe its folder, so a ref written as a location settles here too.
52
+ */
53
+ constructor(
54
+ private readonly variables: DefinedVariable[],
55
+ private readonly trusted: CatalogRepo[] = []
56
+ ) {
57
+ const repos = new Map<string, CatalogRepo>();
58
+
59
+ for (const defined of variables) {
60
+ const { repo: repoName, namespace, name: recipe } = defined.recipe;
61
+ const known = trusted.find((entry) => entry.name === repoName);
62
+
63
+ let repo = repos.get(repoName);
64
+ if (repo === undefined) {
65
+ repo = {
66
+ name: repoName,
67
+ namespaces: [],
68
+ recipes: [],
69
+ ...(known?.location === undefined ? {} : { location: known.location }),
70
+ };
71
+ repos.set(repoName, repo);
72
+ }
73
+ if (!repo.namespaces.some((entry) => entry.name === namespace)) {
74
+ repo.namespaces.push({ name: namespace });
75
+ }
76
+ if (!repo.recipes.some((entry) => entry.namespace === namespace && entry.name === recipe)) {
77
+ const path = known?.recipes.find(
78
+ (entry) => entry.namespace === namespace && entry.name === recipe
79
+ )?.path;
80
+ repo.recipes.push({ namespace, name: recipe, ...(path === undefined ? {} : { path }) });
81
+ }
82
+ }
83
+ this.matcher = new CatalogMatcher([...repos.values()]);
84
+ }
85
+
86
+ async find(candidate: SousRef): Promise<RefMatch[]> {
87
+ if (candidate.kind !== "variable") return this.matcher.match(candidate);
88
+
89
+ const qualifier: RepoRef | undefined =
90
+ candidate.recipe?.namespace?.repo ?? candidate.recipe?.repo ?? candidate.repo;
91
+ const matches: RefMatch[] = [];
92
+
93
+ for (const defined of this.variables) {
94
+ const { repo, namespace, name: recipe } = defined.recipe;
95
+ const location = this.trusted.find((entry) => entry.name === repo)?.location;
96
+
97
+ let spelling: NameMatch = "exact";
98
+ if (qualifier?.name !== undefined) spelling = matchName(qualifier.name, repo);
99
+ if (qualifier?.location !== undefined && location?.identity !== qualifier.location.identity) {
100
+ spelling = undefined;
101
+ }
102
+ if (candidate.recipe !== undefined) {
103
+ spelling = both(spelling, matchName(candidate.recipe.name, recipe));
104
+ if (candidate.recipe.namespace !== undefined) {
105
+ spelling = both(spelling, matchName(candidate.recipe.namespace.name, namespace));
106
+ }
107
+ }
108
+ spelling = both(spelling, matchName(candidate.name, defined.definition.name));
109
+ if (spelling === undefined) continue;
110
+
111
+ matches.push({
112
+ ref: {
113
+ ...variableRefOf(defined),
114
+ ...(candidate.vars === undefined ? {} : { vars: candidate.vars }),
115
+ },
116
+ exactSpelling: spelling === "exact",
117
+ });
118
+ }
119
+ return matches.sort((left, right) =>
120
+ refKey(left.ref) < refKey(right.ref) ? -1 : refKey(left.ref) > refKey(right.ref) ? 1 : 0
121
+ );
122
+ }
123
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Namespaces and recipes by their stored names: what a lockfile, an index, a
3
+ * config key and a manifest entry hold. A `NamedRef` is a namespace, or a
4
+ * recipe with its namespace, and `shortKey` (in `pruners/rule-helpers.ts`) is
5
+ * the `namespace` or `namespace/recipe` it is stored under.
6
+ *
7
+ * Callers with a stored key use `splitRecipeKey` and `namespaceOfKey`, which
8
+ * read it through the resolver with `RefSource.Lockfile`, so a key is read by
9
+ * the same code wherever it came from.
10
+ */
11
+
12
+ import { ConfigError } from "../../lib/errors.js";
13
+ import { sharedRefResolver } from "./container.js";
14
+ import { locationOf } from "./parts.js";
15
+ import { RefSource } from "./source.js";
16
+ import type { NamespaceRef, RecipeRef, SousRef } from "./types.js";
17
+
18
+ /** A namespace, or a recipe that names its namespace. */
19
+ export type NamedRef = NamespaceRef | RecipeRef;
20
+
21
+ /**
22
+ * True when a ref is a namespace or a recipe that names its namespace.
23
+ *
24
+ * @param ref - Any ref.
25
+ */
26
+ export function isNamedRef(ref: SousRef): ref is NamedRef {
27
+ return ref.kind === "namespace" || (ref.kind === "recipe" && ref.namespace !== undefined);
28
+ }
29
+
30
+ /**
31
+ * A namespace ref, optionally inside a repository known by short name.
32
+ *
33
+ * @param name - The namespace.
34
+ * @param repo - The repository's short name, when the ref is qualified by one.
35
+ */
36
+ export function namespaceRef(name: string, repo?: string): NamespaceRef {
37
+ return {
38
+ kind: "namespace",
39
+ name,
40
+ ...(repo === undefined ? {} : { repo: { kind: "repo", name: repo } }),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * A recipe ref with its namespace, optionally inside a repository known by
46
+ * short name and with a version range.
47
+ *
48
+ * @param namespace - The recipe's namespace.
49
+ * @param name - The recipe.
50
+ * @param options - The repository's short name and the range, when there are any.
51
+ */
52
+ export function recipeRef(
53
+ namespace: string,
54
+ name: string,
55
+ options: { repo?: string; range?: string } = {}
56
+ ): RecipeRef {
57
+ return {
58
+ kind: "recipe",
59
+ name,
60
+ namespace: namespaceRef(namespace, options.repo),
61
+ ...(options.range === undefined ? {} : { range: options.range }),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * The same namespace or recipe, qualified by a repository's short name. A
67
+ * recipe holds its repository through its namespace.
68
+ *
69
+ * @param ref - A namespace or a recipe.
70
+ * @param repo - The repository's short name; left undefined, the ref comes back as it is.
71
+ */
72
+ export function withRepoName(ref: NamedRef, repo: string | undefined): NamedRef {
73
+ if (repo === undefined) return ref;
74
+ if (ref.kind === "namespace") return { ...ref, repo: { kind: "repo", name: repo } };
75
+ return { ...ref, namespace: namespaceRef(ref.namespace?.name ?? "", repo) };
76
+ }
77
+
78
+ /**
79
+ * The one reading of a ref that must name a namespace or a recipe by its short
80
+ * form: a config key, a stored key, or a ref a caller has already settled.
81
+ *
82
+ * @param input - The ref as written.
83
+ * @param from - Where it was written.
84
+ * @throws A ConfigError when the place refuses it, or it names a location or reads several ways.
85
+ */
86
+ export function parseNamedRef(input: string, from: RefSource = RefSource.CommandLine): NamedRef {
87
+ const { refs } = sharedRefResolver().parse(input, from);
88
+ const named = refs.filter((ref): ref is NamedRef => isNamedRef(ref) && locationOf(ref) === undefined);
89
+ if (named.length === 1 && !refs.some((ref) => locationOf(ref) !== undefined)) return named[0]!;
90
+ throw new ConfigError(
91
+ `Invalid ref '${input}': this needs a namespace or a recipe named by its short form, such ` +
92
+ `as 'workflow/alpha', and this ref names a location.`
93
+ );
94
+ }
95
+
96
+ /** What a stored key was read as, by key. A key's reading never changes, and listings read every key many times. */
97
+ const storedKeys = new Map<string, { namespace: string; name?: string }>();
98
+
99
+ /**
100
+ * Reads a stored key through the resolver, once per distinct key.
101
+ *
102
+ * @param key - A key sous stored.
103
+ */
104
+ function readStoredKey(key: string): { namespace: string; name?: string } {
105
+ const known = storedKeys.get(key);
106
+ if (known !== undefined) return known;
107
+ const parsed = parseNamedRef(key, RefSource.Lockfile);
108
+ const read =
109
+ parsed.kind === "namespace"
110
+ ? { namespace: parsed.name }
111
+ : { namespace: parsed.namespace!.name, name: parsed.name };
112
+ storedKeys.set(key, read);
113
+ return read;
114
+ }
115
+
116
+ /**
117
+ * The namespace and name of a stored recipe key, `namespace/recipe`.
118
+ *
119
+ * splitRecipeKey("workflow/alpha"); // -> { namespace: "workflow", name: "alpha" }
120
+ *
121
+ * @param key - A key sous stored.
122
+ */
123
+ export function splitRecipeKey(key: string): { namespace: string; name: string } {
124
+ const { namespace, name } = readStoredKey(key);
125
+ if (name === undefined) {
126
+ throw new ConfigError(
127
+ `Invalid ref '${key}': a recipe key names a namespace and a recipe.\n` +
128
+ ` A stored key is written as 'namespace' or 'namespace/recipe'.`
129
+ );
130
+ }
131
+ return { namespace, name };
132
+ }
133
+
134
+ /**
135
+ * The namespace a stored key belongs to: the key itself for a namespace, and
136
+ * its namespace for a recipe.
137
+ *
138
+ * namespaceOfKey("workflow/alpha"); // -> "workflow"
139
+ *
140
+ * @param key - A key sous stored.
141
+ */
142
+ export function namespaceOfKey(key: string): string {
143
+ return readStoredKey(key).namespace;
144
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Reads an environment variable name.
3
+ */
4
+
5
+ import { makeInjectable } from "../injectable.js";
6
+ import { ENV_VAR_NAME } from "../glob.js";
7
+ import { BaseRefSplitter, finishRef, type PartialRef } from "./partial-ref.js";
8
+
9
+ /**
10
+ * Any text that is a legal environment variable name (a letter or underscore,
11
+ * then letters, digits or underscores) is also read as one, whole: `_` is both
12
+ * the delimiter and an identifier character, so a name is never split into the
13
+ * scopes that might have produced it. The text stays unread for the other
14
+ * splitters too, because `workflow` is a namespace as well.
15
+ */
16
+ export class EnvVarSplitter extends BaseRefSplitter {
17
+ readonly order = 600;
18
+
19
+ protected read(state: PartialRef): PartialRef[] {
20
+ if (state.repo !== undefined || state.range !== undefined || !ENV_VAR_NAME.test(state.rest)) {
21
+ return [state];
22
+ }
23
+ return [state, finishRef(state, { kind: "envVar", name: state.rest })];
24
+ }
25
+ }
26
+
27
+ makeInjectable(EnvVarSplitter);
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Reads a repository written as a location: a URL, an SSH remote, a
3
+ * provider-scheme locator or a host path.
4
+ *
5
+ * Nothing host-specific lives here. Where a repository path ends and what a
6
+ * browser path looks like are each provider's own rule, asked through
7
+ * `RepoProvider.readLocation`, which may answer more than one way (a GitLab
8
+ * nested group does not say where the project path ends).
9
+ */
10
+
11
+ import { makeInjectable } from "../injectable.js";
12
+ import { REF_TOKENS } from "../tokens.js";
13
+ import {
14
+ detectProvider,
15
+ providerById,
16
+ type RepoProvider,
17
+ } from "../../../lib/repos/providers/index.js";
18
+ import { locationFor } from "../location.js";
19
+ import type { RepoRef } from "../types.js";
20
+ import { BaseRefSplitter, finishRef, type PartialRef } from "./partial-ref.js";
21
+
22
+ /** A URL that names its scheme, such as `https://` or `github://`. */
23
+ const SCHEME_PATTERN = /^([a-z][a-z0-9+.-]*):\/\/(.*)$/is;
24
+
25
+ /** The `scp`-style SSH form, as in `git@github.com:owner/name.git`. */
26
+ const SCP_PATTERN = /^[^@\s/:]+@([^@\s/:]+):(.+)$/s;
27
+
28
+ /** Schemes that are ordinary URLs rather than a provider's own identifier. */
29
+ const URL_SCHEMES = new Set(["http", "https", "ssh", "git", "git+ssh", "ssh+git"]);
30
+
31
+ /** Schemes that name a repository on this machine. */
32
+ const LOCAL_SCHEMES = new Set(["local", "file"]);
33
+
34
+ /**
35
+ * True when a written ref names a location (a URL, an SSH remote, a
36
+ * provider-scheme locator or a host path) rather than a short name. A short
37
+ * name never carries `://`, an `@` before a `:`, or a dot in its first segment.
38
+ *
39
+ * @param ref - The ref as written.
40
+ */
41
+ export function looksLikeLocation(ref: string): boolean {
42
+ const trimmed = ref.trim();
43
+ if (SCHEME_PATTERN.test(trimmed)) return true;
44
+ if (SCP_PATTERN.test(trimmed)) return true;
45
+ const first = trimmed.split("/")[0] ?? "";
46
+ return trimmed.includes("/") && first.includes(".") && !first.includes(":");
47
+ }
48
+
49
+ /** A location, taken apart only as far as every host agrees. */
50
+ type Taken = { provider: RepoProvider; host: string; segments: string[] };
51
+
52
+ /** A URL path's segments, decoded, with empty ones dropped. */
53
+ function pathSegments(pathname: string): string[] {
54
+ return pathname
55
+ .split(/[?#]/)[0]!
56
+ .split("/")
57
+ .map((segment) => {
58
+ try {
59
+ return decodeURIComponent(segment);
60
+ } catch {
61
+ return segment;
62
+ }
63
+ })
64
+ .filter((segment) => segment.length > 0);
65
+ }
66
+
67
+ /**
68
+ * Reads a location into a repository, and whatever is named inside it. A
69
+ * repository that a short name does not already qualify gets the location; the
70
+ * names after it are left in `rest` for the name splitter, and a browser path
71
+ * becomes a finished repository ref of its own.
72
+ */
73
+ export class LocationSplitter extends BaseRefSplitter {
74
+ readonly order = 300;
75
+
76
+ /**
77
+ * @param providers - The providers that read locations.
78
+ */
79
+ constructor(private readonly providers: RepoProvider[]) {
80
+ super();
81
+ }
82
+
83
+ protected read(state: PartialRef): PartialRef[] {
84
+ if (state.repo !== undefined || !looksLikeLocation(state.rest)) return [state];
85
+
86
+ const body = state.rest;
87
+ const scheme = SCHEME_PATTERN.exec(body);
88
+ if (scheme !== null && LOCAL_SCHEMES.has(scheme[1]!.toLowerCase())) {
89
+ return [this.local(state, scheme[2]!)];
90
+ }
91
+
92
+ const taken = this.take(state, body, scheme);
93
+ if (taken === undefined) return [];
94
+
95
+ const { provider, host, segments } = taken;
96
+ const specific: PartialRef[] = [];
97
+ const repositories: RepoRef[] = [];
98
+
99
+ for (const reading of provider.readLocation({ host, segments })) {
100
+ const location = locationFor(provider, host, reading.repoPath);
101
+ if (location === undefined) continue;
102
+
103
+ if (reading.browsed !== undefined) {
104
+ const browsed = reading.browsed.replace(/^\/+|\/+$/g, "");
105
+ if (browsed.length === 0) continue;
106
+ specific.push(
107
+ finishRef(state, {
108
+ kind: "repo",
109
+ location,
110
+ browsed,
111
+ ...(state.range === undefined ? {} : { range: state.range }),
112
+ })
113
+ );
114
+ continue;
115
+ }
116
+
117
+ const named = reading.named ?? [];
118
+ if (named.length === 0) {
119
+ repositories.push({ kind: "repo", location });
120
+ continue;
121
+ }
122
+ specific.push({ ...state, rest: named.join("/"), repo: { kind: "repo", location } });
123
+ }
124
+
125
+ // A reading of the whole repository is only kept when nothing more
126
+ // specific was read, because a namespace or a recipe is what a ref is for.
127
+ if (specific.length > 0) return specific;
128
+
129
+ if (repositories.length === 0) {
130
+ state.problems.push(
131
+ `the ${provider.id} provider cannot read a namespace or a recipe from this location. ` +
132
+ `After the repository, a location names a namespace and optionally a recipe, as in ` +
133
+ `'${provider.formatLocator(host, "owner/repository", "workflow/alpha")}'.`
134
+ );
135
+ return [];
136
+ }
137
+ if (state.range !== undefined) {
138
+ state.problems.push("a version range applies to a recipe, not to a repository.");
139
+ return [];
140
+ }
141
+ return repositories.map((repo) => finishRef(state, repo));
142
+ }
143
+
144
+ /** A repository on this machine: kept as a location the pruners can refuse. */
145
+ private local(state: PartialRef, path: string): PartialRef {
146
+ return finishRef(state, {
147
+ kind: "repo",
148
+ location: {
149
+ provider: "local",
150
+ host: "localhost",
151
+ repoPath: path,
152
+ identity: `localhost/${path.replace(/^\/+/, "")}`,
153
+ url: state.rest,
154
+ },
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Takes the host-agnostic parts out of a location: which provider reads it,
160
+ * the host, and the path segments after the host. Records why not, and
161
+ * returns nothing, when the location cannot be read.
162
+ */
163
+ private take(
164
+ state: PartialRef,
165
+ body: string,
166
+ scheme: RegExpExecArray | null
167
+ ): Taken | undefined {
168
+ const providers = this.providers;
169
+ const known = providers
170
+ .filter((provider) => provider.defaultHost !== undefined)
171
+ .map((provider) => provider.id)
172
+ .sort()
173
+ .join(", ");
174
+
175
+ if (scheme !== null) {
176
+ const name = scheme[1]!.toLowerCase();
177
+
178
+ if (URL_SCHEMES.has(name)) {
179
+ let url: URL;
180
+ try {
181
+ url = new URL(body);
182
+ } catch {
183
+ state.problems.push("the URL could not be read.");
184
+ return undefined;
185
+ }
186
+ const host = url.host.toLowerCase();
187
+ const segments = pathSegments(url.pathname);
188
+ return this.forHost(state, host, segments);
189
+ }
190
+
191
+ const provider = providerById(name, providers);
192
+ if (provider === undefined || provider.defaultHost === undefined) {
193
+ state.problems.push(
194
+ `'${name}' is not a provider sous can read a location from. The scheme of a locator ` +
195
+ `is the provider's own identifier; sous ships these: ${known}. An ordinary ` +
196
+ "'https://' URL works too."
197
+ );
198
+ return undefined;
199
+ }
200
+ let segments = pathSegments(scheme[2]!);
201
+ let host = provider.defaultHost;
202
+ if (segments[0]?.includes(".") === true) {
203
+ host = segments[0].toLowerCase();
204
+ segments = segments.slice(1);
205
+ }
206
+ return { provider, host, segments };
207
+ }
208
+
209
+ const scp = SCP_PATTERN.exec(body);
210
+ if (scp !== null) {
211
+ return this.forHost(state, scp[1]!.toLowerCase(), pathSegments(scp[2]!));
212
+ }
213
+
214
+ const all = pathSegments(body);
215
+ return this.forHost(state, all[0]!.toLowerCase(), all.slice(1));
216
+ }
217
+
218
+ /** The provider that recognizes a host, asked with the URL the location would start with. */
219
+ private forHost(state: PartialRef, host: string, segments: string[]): Taken | undefined {
220
+ const probe = `https://${host}/${segments.slice(0, 2).join("/")}`;
221
+ const provider = detectProvider(probe, this.providers);
222
+ if (provider !== undefined && provider.defaultHost !== undefined) {
223
+ return { provider, host, segments };
224
+ }
225
+ const ids = this.providers
226
+ .filter((entry) => entry.defaultHost !== undefined)
227
+ .map((entry) => entry.id)
228
+ .sort();
229
+ state.problems.push(
230
+ `sous does not recognize the host '${host}'. For a self-hosted instance, write the ` +
231
+ `location with the provider's scheme and the host, as in ` +
232
+ `'${ids[0] ?? "gitlab"}://${host}/owner/repository/namespace/recipe' (sous ships these ` +
233
+ `providers: ${ids.join(", ")}).`
234
+ );
235
+ return undefined;
236
+ }
237
+ }
238
+
239
+ makeInjectable(LocationSplitter, [{ multi: REF_TOKENS.Provider }]);