@sous-io/sous 0.2.15 → 0.2.17
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/commands.md +63 -16
- package/docs/markdown/repositories-authoring.md +52 -11
- package/docs/markdown/repositories-consuming.md +32 -1
- package/docs/markdown/repositories-file-formats.md +20 -0
- package/docs/markdown/repositories-providers.md +20 -10
- package/package.json +1 -1
- package/recipes/core/sous-skills/sous.recipe.yaml +8 -1
- package/src/commands/namespace/list.ts +42 -21
- package/src/commands/namespace/show.ts +32 -12
- package/src/commands/recipe/list.ts +40 -23
- package/src/commands/recipe/show.ts +28 -6
- package/src/commands/repo/list.ts +67 -10
- package/src/commands/repo/release.ts +41 -0
- package/src/commands/repo/search.ts +68 -15
- package/src/commands/repo/submit.ts +245 -35
- package/src/commands/subscription/list.ts +98 -19
- package/src/lib/build-preparation.ts +44 -1
- package/src/lib/repos/catalog-display.ts +101 -2
- package/src/lib/repos/catalog-inputs.ts +145 -17
- package/src/lib/repos/catalog.ts +92 -5
- package/src/lib/repos/formats/common.ts +20 -0
- package/src/lib/repos/formats/recipe-manifest.ts +7 -0
- package/src/lib/repos/formats/repo-manifest.ts +8 -0
- package/src/lib/repos/freshness.ts +56 -0
- package/src/lib/repos/providers/base.ts +33 -1
- package/src/lib/repos/providers/github.ts +276 -3
- package/src/lib/repos/providers/gitlab.ts +1 -0
- package/src/lib/repos/providers/http.ts +7 -2
- package/src/lib/repos/providers/index-cache.ts +56 -17
- package/src/lib/repos/providers/provider.ts +121 -3
- package/src/lib/repos/release/changelog.ts +448 -0
- package/src/lib/repos/release/git-state.ts +101 -15
- package/src/lib/repos/release/index.ts +2 -0
- package/src/lib/repos/release/submissions.ts +214 -0
- package/src/lib/repos/release/submit-checkout.ts +271 -0
- package/src/lib/repos/release/submit-questions.ts +153 -0
- package/src/lib/repos/release/submit-service.ts +581 -174
- package/src/lib/repos/subscription-service.ts +138 -9
- package/src/utils/flags.ts +24 -0
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
* `sous subscription list`.
|
|
3
3
|
*
|
|
4
4
|
* Prints every subscription this project declares: the ref, the version range
|
|
5
|
-
* it resolves within, the versions its lockfile currently pins,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* project
|
|
5
|
+
* it resolves within, the versions its lockfile currently pins, the latest
|
|
6
|
+
* version each of those recipes has published, where the subscription came
|
|
7
|
+
* from, and whether it is switched on. Entries switched off with
|
|
8
|
+
* `enabled: false` are listed too, because an opt-out is part of what a project
|
|
9
|
+
* declares.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
* downloads anything.
|
|
11
|
+
* By default it reads only the config, the lockfile and the cached indexes, so
|
|
12
|
+
* it is safe offline and never downloads anything. `--latest` reads the latest
|
|
13
|
+
* versions from upstream instead, saving nothing, and `--installed` narrows the
|
|
14
|
+
* listing to the subscriptions the lockfile pins something for.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import { BaseCommand } from "../../base-command.js";
|
|
15
18
|
import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
|
|
16
19
|
import { BUILT_IN_ADDED_BY } from "../../lib/repos/defaults.js";
|
|
17
20
|
import { USER_ADDED_BY } from "../../lib/repos/trust.js";
|
|
21
|
+
import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
|
|
22
|
+
import { listRecipes } from "../../lib/repos/catalog.js";
|
|
23
|
+
import {
|
|
24
|
+
describeIndexSource,
|
|
25
|
+
pinnedCell,
|
|
26
|
+
printBrowsingNotes,
|
|
27
|
+
} from "../../lib/repos/catalog-display.js";
|
|
18
28
|
import { renderTable, type TableColumn } from "../../utils/table.js";
|
|
29
|
+
import { browsingFlags } from "../../utils/flags.js";
|
|
19
30
|
import {
|
|
20
31
|
blankLine,
|
|
21
32
|
footer,
|
|
@@ -39,6 +50,7 @@ const COLUMNS: TableColumn[] = [
|
|
|
39
50
|
{ key: "key", header: "Subscription", kind: "path", overflow: "truncate", minWidth: 12 },
|
|
40
51
|
{ key: "range", header: "Range", overflow: "truncate", minWidth: 7 },
|
|
41
52
|
{ key: "pinned", header: "Pinned version", flex: 1, minWidth: 14 },
|
|
53
|
+
{ key: "latest", header: "Latest version", overflow: "wrap", priority: "medium", minWidth: 14 },
|
|
42
54
|
{ key: "origin", header: "Origin", priority: "medium" },
|
|
43
55
|
{ key: "enabled", header: "Enabled", priority: "medium" },
|
|
44
56
|
];
|
|
@@ -52,16 +64,21 @@ export default class SubscriptionList extends BaseCommand {
|
|
|
52
64
|
*/
|
|
53
65
|
static aliases = ["subscriptions:list"];
|
|
54
66
|
|
|
55
|
-
static examples = [
|
|
67
|
+
static examples = [
|
|
68
|
+
"<%= config.bin %> subscription list",
|
|
69
|
+
"<%= config.bin %> subscription list --installed --latest",
|
|
70
|
+
];
|
|
56
71
|
|
|
57
|
-
static flags = { ...BaseCommand.baseFlags };
|
|
72
|
+
static flags = { ...BaseCommand.baseFlags, ...browsingFlags() };
|
|
58
73
|
|
|
59
74
|
async run(): Promise<void> {
|
|
60
|
-
await this.parse(SubscriptionList);
|
|
75
|
+
const { flags } = await this.parse(SubscriptionList);
|
|
61
76
|
|
|
62
77
|
showCommandVars({
|
|
63
78
|
Project: this.projectLabel,
|
|
64
79
|
Config: this.configContext.configPath,
|
|
80
|
+
Reading: describeIndexSource(flags.latest),
|
|
81
|
+
...(flags.installed ? { Showing: "only what this project has installed" } : {}),
|
|
65
82
|
});
|
|
66
83
|
|
|
67
84
|
heading("Subscriptions");
|
|
@@ -72,28 +89,69 @@ export default class SubscriptionList extends BaseCommand {
|
|
|
72
89
|
shellEnv: this.shellEnv,
|
|
73
90
|
});
|
|
74
91
|
|
|
75
|
-
const listings = service
|
|
92
|
+
const listings = service
|
|
93
|
+
.listSubscriptions()
|
|
94
|
+
.filter((entry) => !flags.installed || entry.pinned.length > 0);
|
|
76
95
|
|
|
77
96
|
blankLine();
|
|
78
97
|
|
|
79
98
|
if (listings.length === 0) {
|
|
80
|
-
paragraph(
|
|
99
|
+
paragraph(
|
|
100
|
+
flags.installed
|
|
101
|
+
? "No subscription of this project has installed anything yet."
|
|
102
|
+
: "This project subscribes to nothing yet."
|
|
103
|
+
);
|
|
81
104
|
footer();
|
|
82
105
|
return;
|
|
83
106
|
}
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
// The latest version of each pinned recipe, from the repository the
|
|
109
|
+
// lockfile says it came from; the linked checkout, when there is one.
|
|
110
|
+
const { inputs, notChecked } = await loadCatalogContext({
|
|
111
|
+
service,
|
|
112
|
+
sousDir: this.configContext.sousDir,
|
|
113
|
+
settings: this.settings,
|
|
114
|
+
latest: flags.latest,
|
|
115
|
+
});
|
|
116
|
+
const published = new Map(
|
|
117
|
+
listRecipes(inputs).map((recipe) => [`${recipe.repo}:${recipe.key}`, recipe])
|
|
118
|
+
);
|
|
119
|
+
const lock = inputs.lock;
|
|
120
|
+
|
|
121
|
+
let anyLinked = false;
|
|
122
|
+
const rows = listings.map((entry) => {
|
|
123
|
+
const held = entry.pinned.map((pin) => {
|
|
124
|
+
const recipe = published.get(`${lock.recipes[pin.key]?.repo}:${pin.key}`);
|
|
125
|
+
if (recipe?.linkedPath !== undefined) anyLinked = true;
|
|
126
|
+
return { ...pin, recipe };
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
key: entry.key,
|
|
130
|
+
range: entry.range ?? "any version",
|
|
131
|
+
pinned:
|
|
132
|
+
held.length === 0
|
|
133
|
+
? describePinned([], entry.enabled)
|
|
134
|
+
: held
|
|
135
|
+
.map((pin) => `${pin.key} ${pinnedCell(pin.version, pin.recipe?.linkedPath)}`)
|
|
136
|
+
.join(", "),
|
|
137
|
+
latest: describeLatest(held.map((pin) => ({ key: pin.key, latest: pin.recipe?.latest }))),
|
|
138
|
+
origin: describeOrigin(entry.addedBy),
|
|
139
|
+
enabled: entry.enabled ? "yes" : "no",
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const columns = flags.installed
|
|
144
|
+
? COLUMNS.map((column) =>
|
|
145
|
+
column.key === "pinned" ? { ...column, header: "Installed version" } : column
|
|
146
|
+
)
|
|
147
|
+
: COLUMNS;
|
|
92
148
|
|
|
93
|
-
for (const line of renderTable(
|
|
149
|
+
for (const line of renderTable(columns, rows, { indent: INDENT })) {
|
|
94
150
|
log(indent(line, INDENT));
|
|
95
151
|
}
|
|
96
152
|
|
|
153
|
+
printBrowsingNotes({ notChecked, anyLinked });
|
|
154
|
+
|
|
97
155
|
footer();
|
|
98
156
|
}
|
|
99
157
|
}
|
|
@@ -117,6 +175,27 @@ export function describePinned(
|
|
|
117
175
|
return enabled ? "pinned on first build" : "not pinned";
|
|
118
176
|
}
|
|
119
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The latest published version of each recipe a subscription holds. One held
|
|
180
|
+
* recipe is the usual case, and it shows the version alone; several (a
|
|
181
|
+
* namespace subscription) are each named with the version beside them. A
|
|
182
|
+
* recipe no readable index publishes says so; a subscription holding nothing
|
|
183
|
+
* yet has no latest version to show.
|
|
184
|
+
*
|
|
185
|
+
* describeLatest([{ key: "workflow/task-files", latest: "1.2.0" }])
|
|
186
|
+
* // -> "1.2.0"
|
|
187
|
+
*
|
|
188
|
+
* @param held - Each held recipe, with its latest version when an index names one.
|
|
189
|
+
*/
|
|
190
|
+
export function describeLatest(held: Array<{ key: string; latest: string | undefined }>): string {
|
|
191
|
+
if (held.length === 0) return "";
|
|
192
|
+
const unknown = "not in any index sous has read";
|
|
193
|
+
if (held.length === 1) return held[0]!.latest ?? unknown;
|
|
194
|
+
return held
|
|
195
|
+
.map((entry) => `${entry.key} ${entry.latest ?? unknown}`)
|
|
196
|
+
.join(", ");
|
|
197
|
+
}
|
|
198
|
+
|
|
120
199
|
/**
|
|
121
200
|
* Plain-language wording for a subscription entry's `addedBy` field.
|
|
122
201
|
*
|
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { SubscriptionService } from "./repos/subscription-service.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
blankLine,
|
|
12
|
+
footer,
|
|
13
|
+
heading,
|
|
14
|
+
note,
|
|
15
|
+
paragraph,
|
|
16
|
+
section,
|
|
17
|
+
showVariables,
|
|
18
|
+
warning,
|
|
19
|
+
} from "../utils/formatting.js";
|
|
11
20
|
|
|
12
21
|
/**
|
|
13
22
|
* Gets this project's recipes ready to compile: restores whatever the store is
|
|
@@ -83,4 +92,38 @@ export async function prepareRepositoriesForBuild(
|
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
if (needsRestore) footer();
|
|
95
|
+
|
|
96
|
+
reportNewerVersions(upstream.newer);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* States which recipes this project uses have a newer version within the range
|
|
101
|
+
* declared for them, as a fact: one line per recipe, the newer version beside the
|
|
102
|
+
* pinned one. Nothing moved, and the closing sentence says so. Prints nothing
|
|
103
|
+
* when there is nothing newer.
|
|
104
|
+
*
|
|
105
|
+
* @param newer - What the upstream check found.
|
|
106
|
+
*/
|
|
107
|
+
export function reportNewerVersions(
|
|
108
|
+
newer: Array<{ key: string; from: string; to: string }>
|
|
109
|
+
): void {
|
|
110
|
+
if (newer.length === 0) return;
|
|
111
|
+
|
|
112
|
+
section("Newer versions published");
|
|
113
|
+
showVariables(
|
|
114
|
+
newer.map((entry) => ({
|
|
115
|
+
label: entry.key,
|
|
116
|
+
value: entry.to,
|
|
117
|
+
detail: `this project pins ${entry.from}`,
|
|
118
|
+
}))
|
|
119
|
+
);
|
|
120
|
+
blankLine();
|
|
121
|
+
note(
|
|
122
|
+
newer.length === 1
|
|
123
|
+
? "This version is within the range declared for the recipe. No pin was changed, " +
|
|
124
|
+
"so this build uses the pinned version."
|
|
125
|
+
: "Each version is within the range declared for its recipe. No pin was changed, " +
|
|
126
|
+
"so this build uses the pinned versions."
|
|
127
|
+
);
|
|
128
|
+
footer();
|
|
86
129
|
}
|
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { renderFacts, type LabeledFact } from "../vars/display.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
blankLine,
|
|
15
|
+
log,
|
|
16
|
+
note,
|
|
17
|
+
palette,
|
|
18
|
+
paragraph,
|
|
19
|
+
wrapColumns,
|
|
20
|
+
} from "../../utils/formatting.js";
|
|
14
21
|
import type { TableColumn } from "../../utils/table.js";
|
|
15
22
|
import type {
|
|
16
23
|
NamespaceCoverage,
|
|
@@ -42,6 +49,19 @@ export const RECIPE_COLUMNS: TableColumn[] = [
|
|
|
42
49
|
},
|
|
43
50
|
];
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The recipe columns for one run. Narrowed to what the project has installed,
|
|
54
|
+
* the pinned column is the installed version, and is headed that way.
|
|
55
|
+
*
|
|
56
|
+
* @param options - Whether the listing is narrowed to installed recipes.
|
|
57
|
+
*/
|
|
58
|
+
export function recipeColumns(options: { installed?: boolean } = {}): TableColumn[] {
|
|
59
|
+
if (options.installed !== true) return RECIPE_COLUMNS;
|
|
60
|
+
return RECIPE_COLUMNS.map((column) =>
|
|
61
|
+
column.key === "pinned" ? { ...column, header: "Installed" } : column
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
/** One rendered recipe row, in the shape `RECIPE_COLUMNS` reads. */
|
|
46
66
|
export type RecipeRow = {
|
|
47
67
|
key: string;
|
|
@@ -64,12 +84,91 @@ export function recipeRows(listings: RecipeListing[]): RecipeRow[] {
|
|
|
64
84
|
key: entry.key,
|
|
65
85
|
repo: entry.repo,
|
|
66
86
|
latest: entry.latest ?? "none published",
|
|
67
|
-
pinned: entry.pinned
|
|
87
|
+
pinned: pinnedCell(entry.pinned, entry.linkedPath),
|
|
68
88
|
subscribed: entry.subscribed ? "yes" : "no",
|
|
69
89
|
description: entry.description ?? "no description published",
|
|
70
90
|
}));
|
|
71
91
|
}
|
|
72
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The pinned version as a cell: the version alone, or the version marked
|
|
95
|
+
* `linked` in muted grey when builds read the recipe from a linked checkout.
|
|
96
|
+
*
|
|
97
|
+
* @param pinned - The version the lockfile pins, when it pins one.
|
|
98
|
+
* @param linkedPath - The linked checkout, when the repository is linked.
|
|
99
|
+
*/
|
|
100
|
+
export function pinnedCell(pinned: string | undefined, linkedPath: string | undefined): string {
|
|
101
|
+
if (pinned === undefined) return "";
|
|
102
|
+
return linkedPath === undefined ? pinned : `${pinned} ${palette.muted("linked")}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** What a listing says about the linked recipes it marked. */
|
|
106
|
+
export const LINKED_NOTE =
|
|
107
|
+
"A recipe marked linked is pinned at the version shown, but builds currently read it " +
|
|
108
|
+
"from the linked checkout of its repository.";
|
|
109
|
+
|
|
110
|
+
/** What a repository listing says when an installed repository is linked. */
|
|
111
|
+
export const LINKED_REPO_NOTE =
|
|
112
|
+
"A recipe installed from a linked repository is pinned at the version shown, but " +
|
|
113
|
+
"builds currently read it from the linked checkout.";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Where a browsing command read the indexes from, in the words its opening
|
|
117
|
+
* block shows.
|
|
118
|
+
*
|
|
119
|
+
* @param latest - Whether the latest was asked for.
|
|
120
|
+
*/
|
|
121
|
+
export function describeIndexSource(latest: boolean): string {
|
|
122
|
+
return latest ? "each repository, upstream" : "the cached indexes";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** What a browsing command could not read, and whether any row is linked. */
|
|
126
|
+
export type BrowsingNotes = {
|
|
127
|
+
/** Trusted repositories with no index at all. */
|
|
128
|
+
notFetched?: string[];
|
|
129
|
+
/** Repositories upstream could not answer for, shown from the cache. */
|
|
130
|
+
notChecked?: string[];
|
|
131
|
+
/** True when some row was marked linked. */
|
|
132
|
+
anyLinked?: boolean;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Prints what a browsing command's rows could not say themselves: repositories
|
|
137
|
+
* that were not listed at all, repositories shown from the cache because
|
|
138
|
+
* upstream could not be reached, and what `linked` means when a row carries
|
|
139
|
+
* it. Prints nothing when there is nothing to say.
|
|
140
|
+
*
|
|
141
|
+
* @param notes - What the command could not read, and whether a row was linked.
|
|
142
|
+
*/
|
|
143
|
+
export function printBrowsingNotes(notes: BrowsingNotes): void {
|
|
144
|
+
const lines: Array<{ text: string; kind: "fact" | "note" }> = [];
|
|
145
|
+
|
|
146
|
+
if ((notes.notFetched ?? []).length > 0) {
|
|
147
|
+
lines.push({
|
|
148
|
+
kind: "fact",
|
|
149
|
+
text:
|
|
150
|
+
`These repositories are trusted and their index has not been fetched yet, so ` +
|
|
151
|
+
`nothing in them is listed: ${notes.notFetched!.join(", ")}.`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if ((notes.notChecked ?? []).length > 0) {
|
|
155
|
+
lines.push({
|
|
156
|
+
kind: "fact",
|
|
157
|
+
text:
|
|
158
|
+
`These repositories could not be reached, so they are shown from the cache and ` +
|
|
159
|
+
`were not checked: ${notes.notChecked!.join(", ")}.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (notes.anyLinked === true) lines.push({ kind: "note", text: LINKED_NOTE });
|
|
163
|
+
|
|
164
|
+
if (lines.length === 0) return;
|
|
165
|
+
blankLine();
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
if (line.kind === "note") note(line.text);
|
|
168
|
+
else paragraph(line.text);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
73
172
|
/**
|
|
74
173
|
* Plain-language wording for how much of a namespace a project subscribes to.
|
|
75
174
|
*
|
|
@@ -8,15 +8,18 @@
|
|
|
8
8
|
* is pinned, the links map and the store for a recipe's own files, and
|
|
9
9
|
* `recipeOutputs` for where a content kind lands.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* fetched is left out of the catalog and named separately, so a
|
|
13
|
-
* is safe offline.
|
|
11
|
+
* By default nothing here downloads anything: a repository whose index has
|
|
12
|
+
* never been fetched is left out of the catalog and named separately, so a
|
|
13
|
+
* browsing command is safe offline. Asked for the latest, it reads each index
|
|
14
|
+
* from upstream instead and writes none of it to the cache; a repository that
|
|
15
|
+
* cannot be reached is read from the cache and named as not checked.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import type { Settings, VarScope } from "../settings.js";
|
|
17
19
|
import type { SubscriptionService } from "./subscription-service.js";
|
|
18
20
|
import type { CatalogInputs, CatalogRepo } from "./catalog.js";
|
|
19
|
-
import {
|
|
21
|
+
import type { IndexFile } from "./formats/index-file.js";
|
|
22
|
+
import { linkedPathFor, readEffectiveLinks } from "./links.js";
|
|
20
23
|
import { mapLinkedRecipes, readRecipeManifestIn } from "./locked-recipes.js";
|
|
21
24
|
import { WRITABLE_CONTENT_KINDS, destinationsFor } from "./recipe-targets.js";
|
|
22
25
|
import type { WritableContentKind } from "./recipe-targets.js";
|
|
@@ -37,8 +40,94 @@ export type CatalogInputsOptions = {
|
|
|
37
40
|
scope?: VarScope;
|
|
38
41
|
/** The environment to read; decides where the store and the links map are. */
|
|
39
42
|
env?: NodeJS.ProcessEnv;
|
|
43
|
+
/**
|
|
44
|
+
* The indexes to read, when the caller has already gathered them (with
|
|
45
|
+
* `readTrustedIndexes`, say, to read upstream). The cached indexes otherwise.
|
|
46
|
+
*/
|
|
47
|
+
indexes?: TrustedIndexes;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Where to read the trusted repositories' indexes from. */
|
|
51
|
+
export type TrustedIndexesOptions = {
|
|
52
|
+
/** Read each index from upstream rather than the cache, writing none of it. */
|
|
53
|
+
latest?: boolean;
|
|
40
54
|
};
|
|
41
55
|
|
|
56
|
+
/** One trusted repository's index, and where it was read from. */
|
|
57
|
+
export type TrustedIndex = {
|
|
58
|
+
/** The repository's short name. */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Where it lives, as the project's config records it. */
|
|
61
|
+
url?: string;
|
|
62
|
+
/** The index that was read. */
|
|
63
|
+
index: IndexFile;
|
|
64
|
+
/** Whether it came from upstream just now or from the cache. */
|
|
65
|
+
source: "upstream" | "cache";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Every trusted repository's index sous could read, and what it could not. */
|
|
69
|
+
export type TrustedIndexes = {
|
|
70
|
+
/** The indexes, by repository short name, sorted. */
|
|
71
|
+
repos: TrustedIndex[];
|
|
72
|
+
/**
|
|
73
|
+
* Trusted repositories with no index at all: never fetched, and (when the
|
|
74
|
+
* latest was asked for) not reachable either. Sorted.
|
|
75
|
+
*/
|
|
76
|
+
notFetched: string[];
|
|
77
|
+
/**
|
|
78
|
+
* Repositories the latest was asked for that could not be reached, and were
|
|
79
|
+
* read from the cache instead. Sorted; always empty when reading the cache.
|
|
80
|
+
*/
|
|
81
|
+
notChecked: string[];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reads the index of every repository this project trusts. From the cache by
|
|
86
|
+
* default, which downloads nothing. With `latest`, from upstream, all at once,
|
|
87
|
+
* and nothing fetched is written to the cache: only a command that resolves
|
|
88
|
+
* versions changes what the cache holds. A repository upstream cannot answer
|
|
89
|
+
* for is read from the cache and named in `notChecked`.
|
|
90
|
+
*
|
|
91
|
+
* @param service - The subscription service for this project.
|
|
92
|
+
* @param options - Whether to read upstream.
|
|
93
|
+
*/
|
|
94
|
+
export async function readTrustedIndexes(
|
|
95
|
+
service: SubscriptionService,
|
|
96
|
+
options: TrustedIndexesOptions = {}
|
|
97
|
+
): Promise<TrustedIndexes> {
|
|
98
|
+
if (options.latest !== true) return cachedTrustedIndexes(service);
|
|
99
|
+
|
|
100
|
+
const trusted = service.currentRepos();
|
|
101
|
+
const names = Object.keys(trusted).sort();
|
|
102
|
+
const answers = await Promise.allSettled(names.map((name) => service.upstreamIndex(name)));
|
|
103
|
+
|
|
104
|
+
const result: TrustedIndexes = { repos: [], notFetched: [], notChecked: [] };
|
|
105
|
+
names.forEach((name, position) => {
|
|
106
|
+
const url = trusted[name]?.url;
|
|
107
|
+
const answer = answers[position]!;
|
|
108
|
+
if (answer.status === "fulfilled") {
|
|
109
|
+
result.repos.push({
|
|
110
|
+
name,
|
|
111
|
+
...(url === undefined ? {} : { url }),
|
|
112
|
+
index: answer.value,
|
|
113
|
+
source: "upstream",
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Upstream could not answer, so the cached copy stands in and says so. A
|
|
119
|
+
// repository with no cached copy either is named once, as never fetched.
|
|
120
|
+
const cached = service.cachedIndex(name);
|
|
121
|
+
if (cached === undefined) {
|
|
122
|
+
result.notFetched.push(name);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
result.notChecked.push(name);
|
|
126
|
+
result.repos.push({ name, ...(url === undefined ? {} : { url }), index: cached, source: "cache" });
|
|
127
|
+
});
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
42
131
|
/** The catalog's inputs, plus what could not be read. */
|
|
43
132
|
export type CatalogContext = {
|
|
44
133
|
/** What the catalog functions read. */
|
|
@@ -48,6 +137,11 @@ export type CatalogContext = {
|
|
|
48
137
|
* could be listed. Sorted.
|
|
49
138
|
*/
|
|
50
139
|
notFetched: string[];
|
|
140
|
+
/**
|
|
141
|
+
* Repositories the latest was asked for that could not be reached, listed
|
|
142
|
+
* from the cache instead. Sorted.
|
|
143
|
+
*/
|
|
144
|
+
notChecked: string[];
|
|
51
145
|
};
|
|
52
146
|
|
|
53
147
|
/**
|
|
@@ -60,25 +154,23 @@ export type CatalogContext = {
|
|
|
60
154
|
export function catalogContextFor(options: CatalogInputsOptions): CatalogContext {
|
|
61
155
|
const { service } = options;
|
|
62
156
|
const env = options.env ?? process.env;
|
|
157
|
+
const indexes = options.indexes ?? cachedTrustedIndexes(service);
|
|
63
158
|
|
|
64
|
-
const repos: CatalogRepo[] =
|
|
65
|
-
|
|
159
|
+
const repos: CatalogRepo[] = indexes.repos.map((entry) => ({
|
|
160
|
+
name: entry.name,
|
|
161
|
+
...(entry.url === undefined ? {} : { url: entry.url }),
|
|
162
|
+
index: entry.index,
|
|
163
|
+
}));
|
|
66
164
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (index === undefined) {
|
|
71
|
-
notFetched.push(name);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const url = trusted[name]?.url;
|
|
75
|
-
repos.push({ name, ...(url === undefined ? {} : { url }), index });
|
|
76
|
-
}
|
|
165
|
+
const links = readEffectiveLinks(options.sousDir, env);
|
|
166
|
+
const linked: Record<string, string> = {};
|
|
167
|
+
for (const [name, link] of Object.entries(links)) linked[name] = link.path;
|
|
77
168
|
|
|
78
169
|
const inputs: CatalogInputs = {
|
|
79
170
|
repos,
|
|
80
171
|
lock: service.lockService.read(),
|
|
81
172
|
subscriptions: Object.keys(service.allSubscriptions()).sort(),
|
|
173
|
+
linked,
|
|
82
174
|
readManifest: (recipe) => {
|
|
83
175
|
const directory = recipeFilesDirectory({
|
|
84
176
|
service,
|
|
@@ -105,7 +197,43 @@ export function catalogContextFor(options: CatalogInputsOptions): CatalogContext
|
|
|
105
197
|
},
|
|
106
198
|
};
|
|
107
199
|
|
|
108
|
-
return { inputs, notFetched };
|
|
200
|
+
return { inputs, notFetched: indexes.notFetched, notChecked: indexes.notChecked };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The catalog's inputs, reading the indexes the way the options say: from the
|
|
205
|
+
* cache, or with `latest` from upstream without writing to the cache.
|
|
206
|
+
*
|
|
207
|
+
* @param options - The subscription service, the project's directory and config,
|
|
208
|
+
* and whether to read upstream.
|
|
209
|
+
*/
|
|
210
|
+
export async function loadCatalogContext(
|
|
211
|
+
options: Omit<CatalogInputsOptions, "indexes"> & TrustedIndexesOptions
|
|
212
|
+
): Promise<CatalogContext> {
|
|
213
|
+
const indexes = await readTrustedIndexes(options.service, {
|
|
214
|
+
...(options.latest === undefined ? {} : { latest: options.latest }),
|
|
215
|
+
});
|
|
216
|
+
return catalogContextFor({ ...options, indexes });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Every trusted repository's cached index, read synchronously.
|
|
221
|
+
*
|
|
222
|
+
* @param service - The subscription service for this project.
|
|
223
|
+
*/
|
|
224
|
+
function cachedTrustedIndexes(service: SubscriptionService): TrustedIndexes {
|
|
225
|
+
const trusted = service.currentRepos();
|
|
226
|
+
const result: TrustedIndexes = { repos: [], notFetched: [], notChecked: [] };
|
|
227
|
+
for (const name of Object.keys(trusted).sort()) {
|
|
228
|
+
const index = service.cachedIndex(name);
|
|
229
|
+
if (index === undefined) {
|
|
230
|
+
result.notFetched.push(name);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const url = trusted[name]?.url;
|
|
234
|
+
result.repos.push({ name, ...(url === undefined ? {} : { url }), index, source: "cache" });
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
109
237
|
}
|
|
110
238
|
|
|
111
239
|
/** Which recipe, at which version, in which of this project's repositories. */
|