@sous-io/sous 0.2.28 → 0.2.30
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.
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +10 -6
- package/docs/markdown/repositories-authoring.md +19 -11
- package/docs/markdown/repositories-consuming.md +34 -13
- package/docs/markdown/repositories-file-formats.md +88 -13
- package/docs/markdown/repositories-providers.md +8 -6
- package/docs/markdown/repositories-troubleshooting.md +6 -0
- package/docs/markdown/repositories-variables.md +13 -3
- package/docs/markdown/repositories.md +3 -3
- package/package.json +1 -1
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +11 -7
- package/recipes/core/sous-skills/skills/about-sous-repos/SKILL.tpl.md +7 -4
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/commands/lock/rebuild.ts +7 -6
- package/src/commands/recipe/show.ts +163 -64
- package/src/commands/repo/contribute.ts +1 -10
- package/src/commands/repo/gc.ts +3 -2
- package/src/commands/repo/release.ts +20 -3
- package/src/commands/repo/search.ts +2 -1
- package/src/commands/vars/ask.ts +32 -10
- package/src/lib/refs/find.ts +202 -21
- package/src/lib/refs/index.ts +2 -0
- package/src/lib/refs/parse.ts +844 -0
- package/src/lib/refs/scopes.ts +34 -0
- package/src/lib/refs/settle.ts +219 -0
- package/src/lib/repos/catalog.ts +87 -80
- package/src/lib/repos/declarations.ts +177 -0
- package/src/lib/repos/formats/common.ts +14 -0
- package/src/lib/repos/formats/index-file.ts +42 -2
- package/src/lib/repos/formats/lockfile.ts +5 -3
- package/src/lib/repos/formats/recipe-manifest.ts +16 -212
- package/src/lib/repos/formats/variable-definition.ts +268 -0
- package/src/lib/repos/index.ts +0 -1
- package/src/lib/repos/lock-service.ts +3 -2
- package/src/lib/repos/locked-namespace-resolver.ts +21 -15
- package/src/lib/repos/locked-recipes.ts +2 -2
- package/src/lib/repos/namespace-resolver.ts +40 -47
- package/src/lib/repos/providers/base.ts +30 -0
- package/src/lib/repos/providers/github.ts +22 -0
- package/src/lib/repos/providers/gitlab.ts +68 -0
- package/src/lib/repos/providers/provider.ts +96 -0
- package/src/lib/repos/release/index-builder.ts +221 -47
- package/src/lib/repos/release/index.ts +1 -0
- package/src/lib/repos/release/plan.ts +8 -6
- package/src/lib/repos/release/settle.ts +237 -0
- package/src/lib/repos/release/tags.ts +3 -7
- package/src/lib/repos/resolver.ts +251 -53
- package/src/lib/repos/subscription-service.ts +355 -49
- package/src/lib/repos/update-plan.ts +3 -2
- package/src/lib/vars/mappings.ts +17 -30
- package/src/lib/vars/question-plan.ts +44 -21
- package/src/lib/repos/ref.ts +0 -513
package/src/lib/refs/scopes.ts
CHANGED
|
@@ -23,6 +23,40 @@ export enum SousScope {
|
|
|
23
23
|
EnvVarName = "envVarName",
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Where a ref was written. `parseRef` reads every form wherever a ref comes
|
|
28
|
+
* from, and the place decides which of those forms it allows: a refused form
|
|
29
|
+
* is an error saying what to write there instead.
|
|
30
|
+
*/
|
|
31
|
+
export enum RefSource {
|
|
32
|
+
/**
|
|
33
|
+
* A ref typed on the command line. Every form is allowed: a bare name, a
|
|
34
|
+
* namespace and recipe, a `repo:` qualifier, a version range, a
|
|
35
|
+
* provider-scheme locator, an HTTPS, SSH or scheme-less URL, and a browser
|
|
36
|
+
* URL copied from a host's file view. Names may be written in any case.
|
|
37
|
+
*/
|
|
38
|
+
CommandLine = "commandLine",
|
|
39
|
+
/**
|
|
40
|
+
* A subscription key in a config layer. Only the stored form is allowed:
|
|
41
|
+
* `namespace` or `namespace/recipe`, lowercase. The repository a
|
|
42
|
+
* subscription resolves into is recorded in the lockfile, and its range in
|
|
43
|
+
* the entry's own `range` field.
|
|
44
|
+
*/
|
|
45
|
+
Config = "config",
|
|
46
|
+
/**
|
|
47
|
+
* An entry of a recipe manifest's `depends` or `subscribes` list. A bare ref
|
|
48
|
+
* names a recipe in the same repository; every locator and URL form names
|
|
49
|
+
* one in another repository. A `repo:` qualifier is refused, because it is
|
|
50
|
+
* one project's private name for a repository, and so is a local path.
|
|
51
|
+
*/
|
|
52
|
+
Manifest = "manifest",
|
|
53
|
+
/**
|
|
54
|
+
* A key sous wrote itself: the lockfile, the index and the store. Only the
|
|
55
|
+
* canonical `namespace` or `namespace/recipe` is allowed.
|
|
56
|
+
*/
|
|
57
|
+
Lockfile = "lockfile",
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
/**
|
|
27
61
|
* Every scope, in the order matches of equal specificity are listed in. It is
|
|
28
62
|
* the order the one-word ref search has always used (a whole namespace before
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settling what a ref means, once the index that can say is at hand.
|
|
3
|
+
*
|
|
4
|
+
* `parseRef` returns every reading of a written ref. Some readings are settled
|
|
5
|
+
* the moment they are read (`workflow/alpha`), and some need the index of the
|
|
6
|
+
* repository they point at: a browser URL names a folder, and only the `path`
|
|
7
|
+
* each recipe's index entry records says which recipe that folder is; a GitLab
|
|
8
|
+
* URL with nested groups can be read as several repositories, and only their
|
|
9
|
+
* indexes say which one publishes what was named.
|
|
10
|
+
*
|
|
11
|
+
* Names are matched the same way everywhere: the exact spelling first, then
|
|
12
|
+
* ignoring case. A name that still matches several things is a question for
|
|
13
|
+
* `pickReference`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
isBrowsedReading,
|
|
18
|
+
isNamedReading,
|
|
19
|
+
namespaceOfKey,
|
|
20
|
+
parseRef,
|
|
21
|
+
refKey,
|
|
22
|
+
splitRecipeKey,
|
|
23
|
+
type ParsedRef,
|
|
24
|
+
type RefReading,
|
|
25
|
+
} from "./parse.js";
|
|
26
|
+
import { RefSource } from "./scopes.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The names that match a written one: those spelled exactly as written, or,
|
|
30
|
+
* when there is none, those that differ only in case.
|
|
31
|
+
*
|
|
32
|
+
* matchNames("Workflow", ["workflow", "tooling"]); // -> ["workflow"]
|
|
33
|
+
* matchNames("workflow", ["workflow", "Workflow"]); // -> ["workflow"]
|
|
34
|
+
*
|
|
35
|
+
* @param written - The name as written.
|
|
36
|
+
* @param names - The names it could mean.
|
|
37
|
+
*/
|
|
38
|
+
export function matchNames(written: string, names: Iterable<string>): string[] {
|
|
39
|
+
const all = [...names];
|
|
40
|
+
const exact = all.filter((name) => name === written);
|
|
41
|
+
if (exact.length > 0) return exact;
|
|
42
|
+
const folded = written.toLowerCase();
|
|
43
|
+
return all.filter((name) => name.toLowerCase() === folded);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What settling needs to know about one repository's index. */
|
|
47
|
+
export type SettleIndex = {
|
|
48
|
+
/** Every namespace the index publishes. */
|
|
49
|
+
namespaces: Iterable<string>;
|
|
50
|
+
/** Every recipe it publishes, keyed `namespace/recipe`, with the folder it lives in. */
|
|
51
|
+
recipes: Record<string, { path: string }>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What a reading names in one repository's index, spelled the way the index
|
|
56
|
+
* spells it. A named reading that the index does not publish settles to
|
|
57
|
+
* nothing; a browsed path settles through the recipe paths.
|
|
58
|
+
*
|
|
59
|
+
* settleInIndex({ namespace: "Workflow", recipe: "alpha" }, index);
|
|
60
|
+
* // -> [{ namespace: "workflow", recipe: "alpha" }]
|
|
61
|
+
*
|
|
62
|
+
* @param reading - One reading of a written ref.
|
|
63
|
+
* @param index - The index of the repository the reading points at.
|
|
64
|
+
*/
|
|
65
|
+
export function settleInIndex(reading: RefReading, index: SettleIndex): ParsedRef[] {
|
|
66
|
+
const carried = {
|
|
67
|
+
...(reading.location === undefined ? {} : { location: reading.location }),
|
|
68
|
+
...("range" in reading && reading.range !== undefined ? { range: reading.range } : {}),
|
|
69
|
+
...("repo" in reading && reading.repo !== undefined ? { repo: reading.repo } : {}),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (isBrowsedReading(reading)) {
|
|
73
|
+
return settleBrowsed(reading.browsed, index).map((settled) => ({ ...settled, ...carried }));
|
|
74
|
+
}
|
|
75
|
+
if (!isNamedReading(reading)) return [];
|
|
76
|
+
|
|
77
|
+
if (reading.recipe === undefined) {
|
|
78
|
+
return matchNames(reading.namespace, index.namespaces).map((namespace) => ({
|
|
79
|
+
namespace,
|
|
80
|
+
...carried,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return matchNames(refKey(reading), Object.keys(index.recipes)).map((key) => {
|
|
85
|
+
const { namespace, name } = splitRecipeKey(key);
|
|
86
|
+
return { namespace, recipe: name, ...carried };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* What a browser path names, through the folder each recipe's index entry
|
|
92
|
+
* records. The path still carries the branch in front (which may itself hold
|
|
93
|
+
* slashes), so every split is tried: the folder of a recipe, or anything inside
|
|
94
|
+
* it, names that recipe; a folder holding recipes of exactly one namespace
|
|
95
|
+
* names that namespace.
|
|
96
|
+
*
|
|
97
|
+
* settleBrowsed("main/recipes/workflow/alpha/SKILL.md", index);
|
|
98
|
+
* // -> [{ namespace: "workflow", recipe: "alpha" }]
|
|
99
|
+
*
|
|
100
|
+
* @param browsed - The path after `tree/` or `blob/`, branch included.
|
|
101
|
+
* @param index - The repository's index.
|
|
102
|
+
*/
|
|
103
|
+
export function settleBrowsed(browsed: string, index: SettleIndex): ParsedRef[] {
|
|
104
|
+
const segments = browsed.split("/").filter((segment) => segment.length > 0);
|
|
105
|
+
const recipes = Object.entries(index.recipes).map(([key, entry]) => ({
|
|
106
|
+
key,
|
|
107
|
+
path: entry.path.replace(/^\.?\/+|\/+$/g, ""),
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const found = new Map<string, ParsedRef>();
|
|
111
|
+
|
|
112
|
+
// A recipe's folder, or a file inside it. The deepest recipe wins, so a
|
|
113
|
+
// recipe nested inside another's folder is named by its own path.
|
|
114
|
+
for (let start = 1; start < segments.length; start++) {
|
|
115
|
+
const folder = segments.slice(start).join("/");
|
|
116
|
+
const holding = recipes
|
|
117
|
+
.filter((recipe) => folder === recipe.path || folder.startsWith(`${recipe.path}/`))
|
|
118
|
+
.sort((left, right) => right.path.length - left.path.length);
|
|
119
|
+
const deepest = holding[0];
|
|
120
|
+
if (deepest !== undefined) {
|
|
121
|
+
const { namespace, name } = splitRecipeKey(deepest.key);
|
|
122
|
+
found.set(deepest.key, { namespace, recipe: name });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (found.size > 0) return [...found.values()];
|
|
126
|
+
|
|
127
|
+
// A folder above recipes: a namespace, when everything under it is one.
|
|
128
|
+
for (let start = 1; start < segments.length; start++) {
|
|
129
|
+
const folder = segments.slice(start).join("/");
|
|
130
|
+
const under = recipes.filter((recipe) => recipe.path.startsWith(`${folder}/`));
|
|
131
|
+
const namespaces = new Set(under.map((recipe) => namespaceOfKey(recipe.key)));
|
|
132
|
+
if (namespaces.size === 1) {
|
|
133
|
+
const namespace = [...namespaces][0]!;
|
|
134
|
+
found.set(namespace, { namespace });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...found.values()];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** What a repository's index recorded about one dependency, as far as settling needs. */
|
|
141
|
+
export type RecordedDependencies = Record<string, { repo?: string } | undefined>;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The readings of a manifest dependency that the declaring recipe's index
|
|
145
|
+
* confirms. A release settles a dependency that reads more than one way (a
|
|
146
|
+
* GitLab URL with nested groups, a browser URL) and records the repository it
|
|
147
|
+
* settled on beside each key it resolved; a consumer reads that record and never
|
|
148
|
+
* probes. A dependency that reads one way needs no record and comes back as it
|
|
149
|
+
* is.
|
|
150
|
+
*
|
|
151
|
+
* A named reading is confirmed by a record under its own key (or, for a whole
|
|
152
|
+
* namespace, under a key inside it) carrying its repository; a browsed reading
|
|
153
|
+
* by any record carrying its repository.
|
|
154
|
+
*
|
|
155
|
+
* @param readings - Every reading of the dependency, from `parseRef`.
|
|
156
|
+
* @param recorded - What the declaring version's index entry records, keyed `namespace/recipe`.
|
|
157
|
+
* @returns The confirmed readings: one is the answer; none or several leave it unsettled.
|
|
158
|
+
*/
|
|
159
|
+
export function confirmedReadings(
|
|
160
|
+
readings: RefReading[],
|
|
161
|
+
recorded: RecordedDependencies | undefined
|
|
162
|
+
): RefReading[] {
|
|
163
|
+
if (readings.length === 1) return readings;
|
|
164
|
+
const records = Object.entries(recorded ?? {});
|
|
165
|
+
|
|
166
|
+
return readings.filter((reading) => {
|
|
167
|
+
const identity = reading.location?.identity;
|
|
168
|
+
if (identity === undefined) return false;
|
|
169
|
+
return records.some(([key, record]) => {
|
|
170
|
+
if (record?.repo !== identity) return false;
|
|
171
|
+
if (isBrowsedReading(reading)) return true;
|
|
172
|
+
if (!isNamedReading(reading)) return false;
|
|
173
|
+
return reading.recipe === undefined
|
|
174
|
+
? key.startsWith(`${reading.namespace}/`)
|
|
175
|
+
: key === refKey(reading);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** What `settleDependency` may consult, beyond the dependency itself. */
|
|
181
|
+
export type SettleDependencyOptions = {
|
|
182
|
+
/** What the declaring version's index entry records about its dependencies. */
|
|
183
|
+
recorded?: RecordedDependencies;
|
|
184
|
+
/**
|
|
185
|
+
* Whether a reading is already known to be right from what is on this
|
|
186
|
+
* machine: its repository is trusted and its cached index publishes what it
|
|
187
|
+
* names. Used only when the index recorded nothing, and it never fetches.
|
|
188
|
+
*/
|
|
189
|
+
known?: (reading: RefReading) => boolean;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The one reading a manifest dependency settles on, or undefined when it reads
|
|
194
|
+
* more than one way and nothing at hand says which: the declaring recipe's
|
|
195
|
+
* index recorded no answer, and what this machine already knows does not pick
|
|
196
|
+
* exactly one.
|
|
197
|
+
*
|
|
198
|
+
* settleDependency("workflow/alpha");
|
|
199
|
+
* // -> { namespace: "workflow", recipe: "alpha" }
|
|
200
|
+
*
|
|
201
|
+
* @param written - The dependency as the manifest wrote it.
|
|
202
|
+
* @param options - The index's record, and what is known locally.
|
|
203
|
+
*/
|
|
204
|
+
export function settleDependency(
|
|
205
|
+
written: string,
|
|
206
|
+
options: SettleDependencyOptions = {}
|
|
207
|
+
): RefReading | undefined {
|
|
208
|
+
const readings = parseRef(written, RefSource.Manifest);
|
|
209
|
+
if (readings.length === 1) return readings[0];
|
|
210
|
+
|
|
211
|
+
const confirmed = confirmedReadings(readings, options.recorded);
|
|
212
|
+
if (confirmed.length === 1) return confirmed[0];
|
|
213
|
+
|
|
214
|
+
if (options.known !== undefined) {
|
|
215
|
+
const known = readings.filter(options.known);
|
|
216
|
+
if (known.length === 1) return known[0];
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
package/src/lib/repos/catalog.ts
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* This module answers all four.
|
|
9
9
|
*
|
|
10
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.
|
|
12
|
-
* recipe
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
16
17
|
*
|
|
17
18
|
* Refs resolve the way `sous subscribe` resolves them: `namespace`,
|
|
18
19
|
* `namespace/recipe`, and either of those qualified with `repo:`. A one-word ref
|
|
@@ -24,13 +25,17 @@ import semver from "semver";
|
|
|
24
25
|
import { ConfigError } from "../errors.js";
|
|
25
26
|
import type { IndexDependency, IndexFile } from "./formats/index-file.js";
|
|
26
27
|
import type { Lockfile } from "./formats/lockfile.js";
|
|
27
|
-
import type {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
import type { ContentKind, RecipeManifest } from "./formats/recipe-manifest.js";
|
|
29
|
+
import {
|
|
30
|
+
isNamedReading,
|
|
31
|
+
namespaceOfKey,
|
|
32
|
+
parseRef,
|
|
33
|
+
refKey,
|
|
34
|
+
splitRecipeKey,
|
|
35
|
+
type ParsedRef,
|
|
36
|
+
} from "../refs/parse.js";
|
|
37
|
+
import { settleDependency } from "../refs/settle.js";
|
|
38
|
+
import { declarationFor, type DependencyLists } from "./declarations.js";
|
|
34
39
|
import {
|
|
35
40
|
describeReference,
|
|
36
41
|
findNamespace,
|
|
@@ -166,9 +171,13 @@ export type RecipeVersionListing = {
|
|
|
166
171
|
export type RecipeDependencyListing = {
|
|
167
172
|
/** The recipe key the dependency names, `namespace/recipe`. */
|
|
168
173
|
key: string;
|
|
169
|
-
/**
|
|
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
|
+
*/
|
|
170
179
|
declared?: string;
|
|
171
|
-
/** Whether
|
|
180
|
+
/** Whether that entry makes it a build dependency or a co-subscription. */
|
|
172
181
|
kind?: "depends" | "subscribes";
|
|
173
182
|
/** The exact version the release resolved it to, when the index records one. */
|
|
174
183
|
resolvedVersion?: string;
|
|
@@ -178,22 +187,6 @@ export type RecipeDependencyListing = {
|
|
|
178
187
|
repo?: string;
|
|
179
188
|
};
|
|
180
189
|
|
|
181
|
-
/** One variable a recipe asks about. */
|
|
182
|
-
export type RecipeVariableListing = {
|
|
183
|
-
/** The variable's name, as the manifest declares it. */
|
|
184
|
-
name: string;
|
|
185
|
-
/** The type of answer it takes. */
|
|
186
|
-
type: string;
|
|
187
|
-
/** The environment variable an answer is stored under. */
|
|
188
|
-
env: string;
|
|
189
|
-
/** True when an answer is required before the recipe is usable. */
|
|
190
|
-
required: boolean;
|
|
191
|
-
/** True when the answer is a secret, which sous never prints. */
|
|
192
|
-
secret: boolean;
|
|
193
|
-
/** The one-line question it asks. */
|
|
194
|
-
prompt: string;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
190
|
/** One content kind a recipe contributes, and where its files would land. */
|
|
198
191
|
export type RecipeContentListing = {
|
|
199
192
|
/** The content kind, as the manifest declares it. */
|
|
@@ -238,14 +231,18 @@ export type RecipeDetail = {
|
|
|
238
231
|
versions: RecipeVersionListing[];
|
|
239
232
|
/** What the described version depends on, by key. */
|
|
240
233
|
dependencies: RecipeDependencyListing[];
|
|
241
|
-
/**
|
|
242
|
-
|
|
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;
|
|
243
240
|
/** What the recipe contributes, and where each kind's files land. */
|
|
244
241
|
contents: RecipeContentListing[];
|
|
245
242
|
/**
|
|
246
243
|
* True when the recipe's own manifest could be read. When it is false, the
|
|
247
|
-
*
|
|
248
|
-
*
|
|
244
|
+
* contents are unknown rather than empty, because the recipe's files are not
|
|
245
|
+
* on this machine.
|
|
249
246
|
*/
|
|
250
247
|
manifestRead: boolean;
|
|
251
248
|
};
|
|
@@ -360,7 +357,7 @@ export function describeRecipe(inputs: CatalogInputs, ref: string): RecipeDetail
|
|
|
360
357
|
...(describing === undefined ? {} : { describing }),
|
|
361
358
|
versions: versionListings(entry.versions, listing.latest, listing.pinned),
|
|
362
359
|
dependencies: dependencyListings(published?.dependencies, manifest),
|
|
363
|
-
|
|
360
|
+
dependenciesRecorded: published?.dependencies !== undefined,
|
|
364
361
|
contents: (manifest?.contents ?? []).map((content) => ({
|
|
365
362
|
kind: content.kind,
|
|
366
363
|
include: [...content.include],
|
|
@@ -390,7 +387,7 @@ export function narrowToInstalled(inputs: CatalogInputs): CatalogInputs {
|
|
|
390
387
|
([key]) => inputs.lock.recipes[key]?.repo === repo.name
|
|
391
388
|
)
|
|
392
389
|
);
|
|
393
|
-
const held = new Set(Object.keys(recipes).map(
|
|
390
|
+
const held = new Set(Object.keys(recipes).map(namespaceOfKey));
|
|
394
391
|
const namespaces = Object.fromEntries(
|
|
395
392
|
Object.entries(repo.index.namespaces).filter(([namespace]) => held.has(namespace))
|
|
396
393
|
);
|
|
@@ -468,16 +465,16 @@ export type ResolvedRecipeRef = {
|
|
|
468
465
|
* @param ref - The namespace, optionally qualified with `repo:`.
|
|
469
466
|
*/
|
|
470
467
|
export function resolveNamespaceRef(inputs: CatalogInputs, ref: string): ResolvedNamespace {
|
|
471
|
-
const parsed =
|
|
468
|
+
const { parsed, search } = readCommandLineRef(ref);
|
|
472
469
|
|
|
473
|
-
if (parsed
|
|
470
|
+
if (parsed?.recipe !== undefined) {
|
|
474
471
|
throw new ConfigError(
|
|
475
472
|
`'${ref}' names the recipe '${parsed.recipe}', not a namespace.\n` +
|
|
476
473
|
` The namespace it belongs to is '${parsed.namespace}'.`
|
|
477
474
|
);
|
|
478
475
|
}
|
|
479
476
|
|
|
480
|
-
const matches = findNamespace(
|
|
477
|
+
const matches = findNamespace(search, referenceContext(inputs));
|
|
481
478
|
|
|
482
479
|
if (matches.length === 1) {
|
|
483
480
|
return { repo: repoNamed(inputs, matches[0]!.repo!), namespace: matches[0]!.namespace! };
|
|
@@ -485,8 +482,8 @@ export function resolveNamespaceRef(inputs: CatalogInputs, ref: string): Resolve
|
|
|
485
482
|
|
|
486
483
|
if (matches.length > 1) throw ambiguousError(ref, "namespace", matches);
|
|
487
484
|
|
|
488
|
-
const asRecipe = findRecipe(
|
|
489
|
-
if (parsed.repo === undefined && asRecipe.length > 0) {
|
|
485
|
+
const asRecipe = findRecipe(search, referenceContext(inputs));
|
|
486
|
+
if (parsed !== undefined && parsed.repo === undefined && asRecipe.length > 0) {
|
|
490
487
|
throw new ConfigError(
|
|
491
488
|
`No repository this project trusts publishes a namespace called ` +
|
|
492
489
|
`'${parsed.namespace}'.\n` +
|
|
@@ -495,7 +492,7 @@ export function resolveNamespaceRef(inputs: CatalogInputs, ref: string): Resolve
|
|
|
495
492
|
);
|
|
496
493
|
}
|
|
497
494
|
|
|
498
|
-
throw unknownError(inputs, ref, "namespace", parsed
|
|
495
|
+
throw unknownError(inputs, ref, "namespace", parsed?.repo);
|
|
499
496
|
}
|
|
500
497
|
|
|
501
498
|
/**
|
|
@@ -507,8 +504,7 @@ export function resolveNamespaceRef(inputs: CatalogInputs, ref: string): Resolve
|
|
|
507
504
|
* @param ref - The recipe, as `namespace/recipe`, a bare recipe name, or either qualified with `repo:`.
|
|
508
505
|
*/
|
|
509
506
|
export function resolveRecipeRef(inputs: CatalogInputs, ref: string): ResolvedRecipeRef {
|
|
510
|
-
const parsed =
|
|
511
|
-
const search = refSearchString(parsed);
|
|
507
|
+
const { parsed, search } = readCommandLineRef(ref);
|
|
512
508
|
const context = referenceContext(inputs);
|
|
513
509
|
|
|
514
510
|
const matches = findRecipe(search, context);
|
|
@@ -529,13 +525,13 @@ export function resolveRecipeRef(inputs: CatalogInputs, ref: string): ResolvedRe
|
|
|
529
525
|
if (asNamespace.length > 0) {
|
|
530
526
|
throw new ConfigError(
|
|
531
527
|
`No repository this project trusts publishes a recipe called ` +
|
|
532
|
-
`'${parsed
|
|
528
|
+
`'${parsed?.namespace ?? ref}'.\n` +
|
|
533
529
|
` It is the name of a namespace:\n` +
|
|
534
530
|
asNamespace.map((candidate) => ` ${describeReference(candidate)}`).join("\n")
|
|
535
531
|
);
|
|
536
532
|
}
|
|
537
533
|
|
|
538
|
-
throw unknownError(inputs, ref, "recipe", parsed
|
|
534
|
+
throw unknownError(inputs, ref, "recipe", parsed?.repo);
|
|
539
535
|
}
|
|
540
536
|
|
|
541
537
|
// --- The pieces ---------------------------------------------------------------------------------
|
|
@@ -543,7 +539,7 @@ export function resolveRecipeRef(inputs: CatalogInputs, ref: string): ResolvedRe
|
|
|
543
539
|
/** Every recipe key one index publishes in one namespace, sorted. */
|
|
544
540
|
function recipeKeysIn(index: IndexFile, namespace: string): string[] {
|
|
545
541
|
return Object.keys(index.recipes)
|
|
546
|
-
.filter((key) =>
|
|
542
|
+
.filter((key) => namespaceOfKey(key) === namespace)
|
|
547
543
|
.sort();
|
|
548
544
|
}
|
|
549
545
|
|
|
@@ -583,8 +579,7 @@ function recipeListing(
|
|
|
583
579
|
): RecipeListing {
|
|
584
580
|
const lock = inputs.lock;
|
|
585
581
|
const entry = repo.index.recipes[key]!;
|
|
586
|
-
const namespace =
|
|
587
|
-
const name = key.slice(namespace.length + 1);
|
|
582
|
+
const { namespace, name } = splitRecipeKey(key);
|
|
588
583
|
const latest = latestVersion(Object.keys(entry.versions), entry.versions);
|
|
589
584
|
|
|
590
585
|
// A locked entry pins one recipe from one repository. Two repositories can
|
|
@@ -674,27 +669,50 @@ function dependencyListings(
|
|
|
674
669
|
manifest: RecipeManifest | undefined
|
|
675
670
|
): RecipeDependencyListing[] {
|
|
676
671
|
const rows = new Map<string, RecipeDependencyListing>();
|
|
672
|
+
const lists: DependencyLists | undefined =
|
|
673
|
+
manifest === undefined
|
|
674
|
+
? undefined
|
|
675
|
+
: { depends: manifest.depends ?? [], subscribes: manifest.subscribes ?? [] };
|
|
677
676
|
|
|
678
677
|
for (const [key, entry] of Object.entries(resolved ?? {})) {
|
|
678
|
+
// The index says how the release that published the version saw it. An
|
|
679
|
+
// entry recorded before the index said so is answered from the manifest,
|
|
680
|
+
// by the same rule the release applies.
|
|
681
|
+
const declaration =
|
|
682
|
+
entry.declared !== undefined && entry.kind !== undefined
|
|
683
|
+
? { declared: entry.declared, kind: entry.kind }
|
|
684
|
+
: lists === undefined
|
|
685
|
+
? undefined
|
|
686
|
+
: declarationFor(lists, key, resolved);
|
|
679
687
|
rows.set(key, {
|
|
680
688
|
key,
|
|
681
689
|
...(entry.version === undefined ? {} : { resolvedVersion: entry.version }),
|
|
682
690
|
...(entry.range === undefined ? {} : { resolvedRange: entry.range }),
|
|
683
691
|
...(entry.repo === undefined ? {} : { repo: entry.repo }),
|
|
692
|
+
...(declaration ?? {}),
|
|
684
693
|
});
|
|
685
694
|
}
|
|
686
695
|
|
|
696
|
+
// A manifest entry that covers no row the index recorded is still shown, so a
|
|
697
|
+
// reader sees everything the manifest declares.
|
|
698
|
+
const covered = new Set([...rows.values()].map((row) => row.declared));
|
|
687
699
|
for (const [kind, declared] of [
|
|
688
|
-
["depends",
|
|
689
|
-
["subscribes",
|
|
700
|
+
["depends", lists?.depends ?? []],
|
|
701
|
+
["subscribes", lists?.subscribes ?? []],
|
|
690
702
|
] as Array<["depends" | "subscribes", string[]]>) {
|
|
691
703
|
for (const written of declared) {
|
|
692
|
-
|
|
704
|
+
if (covered.has(written.trim())) continue;
|
|
705
|
+
// A dependency that reads more than one way is listed under the key the
|
|
706
|
+
// index recorded for it. One sous cannot parse or settle is still worth
|
|
707
|
+
// showing; it is listed under what it was written as, so the reader sees
|
|
708
|
+
// the entry as it stands.
|
|
709
|
+
let key = written;
|
|
693
710
|
try {
|
|
694
|
-
|
|
711
|
+
const reading = settleDependency(written, {
|
|
712
|
+
...(resolved === undefined ? {} : { recorded: resolved }),
|
|
713
|
+
});
|
|
714
|
+
if (reading !== undefined && isNamedReading(reading)) key = refKey(reading);
|
|
695
715
|
} catch {
|
|
696
|
-
// A manifest sous cannot parse is still worth showing; it is listed
|
|
697
|
-
// under what it was written as, so the reader sees the bad entry.
|
|
698
716
|
key = written;
|
|
699
717
|
}
|
|
700
718
|
rows.set(key, { ...(rows.get(key) ?? { key }), declared: written, kind });
|
|
@@ -704,25 +722,6 @@ function dependencyListings(
|
|
|
704
722
|
return [...rows.values()].sort((left, right) => compare(left.key, right.key));
|
|
705
723
|
}
|
|
706
724
|
|
|
707
|
-
/**
|
|
708
|
-
* One variable row: what it is called, what kind of answer it takes, and the
|
|
709
|
-
* environment variable an answer is stored under.
|
|
710
|
-
*
|
|
711
|
-
* @param definition - The variable definition from the recipe's manifest.
|
|
712
|
-
*/
|
|
713
|
-
function variableListing(definition: VariableDefinition): RecipeVariableListing {
|
|
714
|
-
return {
|
|
715
|
-
name: definition.name,
|
|
716
|
-
type: definition.type,
|
|
717
|
-
// The manifest may bind an existing environment variable; otherwise the
|
|
718
|
-
// answer is stored under the name sous derives from the variable's own.
|
|
719
|
-
env: bareName(definition),
|
|
720
|
-
required: definition.required,
|
|
721
|
-
secret: definition.secret,
|
|
722
|
-
prompt: definition.prompt,
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
|
|
726
725
|
/**
|
|
727
726
|
* The repository a match named. Every match comes from this catalog's own
|
|
728
727
|
* repositories, so the lookup always finds one.
|
|
@@ -746,14 +745,22 @@ function referenceContext(inputs: CatalogInputs): ReferenceContext {
|
|
|
746
745
|
}
|
|
747
746
|
|
|
748
747
|
/**
|
|
749
|
-
* A
|
|
750
|
-
*
|
|
748
|
+
* A ref from the command line, checked by the one parser, and the spelling to
|
|
749
|
+
* search for: a short ref written back without its version range (the range
|
|
750
|
+
* says which version to use, never which thing is meant), or a location
|
|
751
|
+
* exactly as written, which the search settles through the repository's index.
|
|
751
752
|
*
|
|
752
|
-
* @param
|
|
753
|
+
* @param ref - The ref as typed.
|
|
754
|
+
* @returns The short reading, when the ref was short, and the search term.
|
|
753
755
|
*/
|
|
754
|
-
function
|
|
755
|
-
const
|
|
756
|
-
|
|
756
|
+
function readCommandLineRef(ref: string): { parsed?: ParsedRef; search: string } {
|
|
757
|
+
const readings = parseRef(ref);
|
|
758
|
+
const first = readings[0]!;
|
|
759
|
+
if (first.location !== undefined || !isNamedReading(first)) return { search: ref.trim() };
|
|
760
|
+
return {
|
|
761
|
+
parsed: first,
|
|
762
|
+
search: first.repo === undefined ? refKey(first) : `${first.repo}:${refKey(first)}`,
|
|
763
|
+
};
|
|
757
764
|
}
|
|
758
765
|
|
|
759
766
|
/** The error a ref that could have meant several things raises. */
|