@sous-io/sous 0.2.14 → 0.2.16
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 +64 -13
- package/docs/markdown/repositories-authoring.md +53 -0
- package/docs/markdown/repositories-consuming.md +32 -1
- package/package.json +1 -1
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -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/link.ts +400 -1
- package/src/commands/repo/list.ts +67 -10
- package/src/commands/repo/search.ts +68 -15
- 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/freshness.ts +56 -0
- package/src/lib/repos/git-clone.ts +468 -7
- package/src/lib/repos/providers/github.ts +1 -0
- 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 +2 -0
- package/src/lib/repos/subscription-service.ts +138 -9
- package/src/utils/flags.ts +24 -0
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Shows every repository this project trusts, what it publishes, and whether it
|
|
5
5
|
* is currently being read from a working copy instead of a published version.
|
|
6
|
-
*
|
|
7
|
-
* never been fetched says so in its own row rather than triggering a
|
|
8
|
-
* so the command is safe to run offline.
|
|
6
|
+
* By default it reads only what sous already has on disk: a repository whose
|
|
7
|
+
* index has never been fetched says so in its own row rather than triggering a
|
|
8
|
+
* download, so the command is safe to run offline. `--latest` reads each index
|
|
9
|
+
* from upstream instead, saving nothing, and `--installed` narrows the listing
|
|
10
|
+
* to the repositories the lockfile pins a recipe from, naming each one under
|
|
11
|
+
* its row.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import { Flags } from "@oclif/core";
|
|
@@ -15,14 +18,24 @@ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js"
|
|
|
15
18
|
import { readEffectiveLinks } from "../../lib/repos/links.js";
|
|
16
19
|
import { BUILT_IN_ADDED_BY } from "../../lib/repos/defaults.js";
|
|
17
20
|
import { requireProvider } from "../../lib/repos/providers/index.js";
|
|
21
|
+
import { readTrustedIndexes } from "../../lib/repos/catalog-inputs.js";
|
|
22
|
+
import {
|
|
23
|
+
LINKED_REPO_NOTE,
|
|
24
|
+
describeIndexSource,
|
|
25
|
+
printBrowsingNotes,
|
|
26
|
+
} from "../../lib/repos/catalog-display.js";
|
|
18
27
|
import { renderTable, type TableColumn } from "../../utils/table.js";
|
|
28
|
+
import { browsingFlags } from "../../utils/flags.js";
|
|
19
29
|
import {
|
|
20
30
|
footer,
|
|
21
31
|
indent,
|
|
22
32
|
log,
|
|
33
|
+
note,
|
|
23
34
|
paragraph,
|
|
24
35
|
section,
|
|
25
36
|
showCommandVars,
|
|
37
|
+
wrapColumns,
|
|
38
|
+
wrapText,
|
|
26
39
|
} from "../../utils/formatting.js";
|
|
27
40
|
|
|
28
41
|
/** How far every line of this command's output is indented. */
|
|
@@ -71,10 +84,12 @@ export default class RepoList extends BaseCommand {
|
|
|
71
84
|
static examples = [
|
|
72
85
|
"<%= config.bin %> repo list",
|
|
73
86
|
"<%= config.bin %> repo list --verbose",
|
|
87
|
+
"<%= config.bin %> repo list --installed --latest",
|
|
74
88
|
];
|
|
75
89
|
|
|
76
90
|
static flags = {
|
|
77
91
|
...BaseCommand.baseFlags,
|
|
92
|
+
...browsingFlags(),
|
|
78
93
|
verbose: Flags.boolean({
|
|
79
94
|
description: "Show the namespaces each repository publishes, under its row",
|
|
80
95
|
default: false,
|
|
@@ -87,9 +102,15 @@ export default class RepoList extends BaseCommand {
|
|
|
87
102
|
showCommandVars({
|
|
88
103
|
Project: this.projectLabel,
|
|
89
104
|
Config: this.configContext.configPath,
|
|
105
|
+
Reading: describeIndexSource(flags.latest),
|
|
106
|
+
...(flags.installed ? { Showing: "only what this project has installed" } : {}),
|
|
90
107
|
});
|
|
91
108
|
|
|
92
|
-
section(
|
|
109
|
+
section(
|
|
110
|
+
flags.installed
|
|
111
|
+
? "Repositories this project has installed recipes from"
|
|
112
|
+
: "Repositories this project trusts"
|
|
113
|
+
);
|
|
93
114
|
|
|
94
115
|
const service = subscriptionServiceFor({
|
|
95
116
|
configContext: this.configContext,
|
|
@@ -98,7 +119,20 @@ export default class RepoList extends BaseCommand {
|
|
|
98
119
|
});
|
|
99
120
|
|
|
100
121
|
const repos = service.currentRepos();
|
|
101
|
-
const
|
|
122
|
+
const lock = service.lockService.read();
|
|
123
|
+
const names = Object.keys(repos)
|
|
124
|
+
.sort()
|
|
125
|
+
.filter(
|
|
126
|
+
(name) =>
|
|
127
|
+
!flags.installed ||
|
|
128
|
+
Object.values(lock.recipes).some((locked) => locked.repo === name)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (names.length === 0 && flags.installed && Object.keys(repos).length > 0) {
|
|
132
|
+
paragraph("This project has installed no recipe from the repositories it trusts.");
|
|
133
|
+
footer();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
102
136
|
|
|
103
137
|
if (names.length === 0) {
|
|
104
138
|
paragraph(
|
|
@@ -110,10 +144,12 @@ export default class RepoList extends BaseCommand {
|
|
|
110
144
|
}
|
|
111
145
|
|
|
112
146
|
const links = readEffectiveLinks(this.configContext.sousDir);
|
|
147
|
+
const indexes = await readTrustedIndexes(service, { latest: flags.latest });
|
|
148
|
+
const indexFor = new Map(indexes.repos.map((entry) => [entry.name, entry.index]));
|
|
113
149
|
|
|
114
150
|
const rows = names.map((name) => {
|
|
115
151
|
const entry = repos[name]!;
|
|
116
|
-
const index =
|
|
152
|
+
const index = indexFor.get(name);
|
|
117
153
|
const namespaces =
|
|
118
154
|
index === undefined ? "not fetched" : Object.keys(index.namespaces).sort().join(", ");
|
|
119
155
|
// A repository whose index has never been fetched says so in the cell
|
|
@@ -130,19 +166,40 @@ export default class RepoList extends BaseCommand {
|
|
|
130
166
|
namespaces: namespaces.length > 0 ? namespaces : "none",
|
|
131
167
|
recipes,
|
|
132
168
|
linked: links[name] === undefined ? "no" : `yes: ${links[name]!.path}`,
|
|
169
|
+
installed: Object.entries(lock.recipes)
|
|
170
|
+
.filter(([, locked]) => locked.repo === name)
|
|
171
|
+
.map(([key, locked]) => `${key} ${locked.version}`)
|
|
172
|
+
.sort()
|
|
173
|
+
.join(", "),
|
|
133
174
|
};
|
|
134
175
|
});
|
|
135
176
|
|
|
136
177
|
for (const line of renderTable(COLUMNS, rows, {
|
|
137
178
|
indent: INDENT,
|
|
138
|
-
rowNote:
|
|
139
|
-
|
|
140
|
-
:
|
|
179
|
+
rowNote: (row) => {
|
|
180
|
+
const notes: string[] = [];
|
|
181
|
+
if (flags.verbose) notes.push(`Namespaces: ${row.namespaces}`);
|
|
182
|
+
if (flags.installed) notes.push(`Installed: ${row.installed}`);
|
|
183
|
+
// Each note wraps under itself, inside the two indents it is printed at.
|
|
184
|
+
const width = Math.max(20, wrapColumns() - INDENT * 2);
|
|
185
|
+
return notes.length === 0
|
|
186
|
+
? undefined
|
|
187
|
+
: notes
|
|
188
|
+
.flatMap((text) => wrapText(text, width))
|
|
189
|
+
.map((line) => color.gray(indent(line, INDENT)))
|
|
190
|
+
.join("\n");
|
|
191
|
+
},
|
|
141
192
|
})) {
|
|
142
193
|
log(indent(line, INDENT));
|
|
143
194
|
}
|
|
144
195
|
|
|
145
|
-
//
|
|
196
|
+
// The table says everything about each repository; what sits under it is
|
|
197
|
+
// only what the rows could not: a repository upstream could not answer for,
|
|
198
|
+
// and what a linked row means for the installed recipes it names.
|
|
199
|
+
printBrowsingNotes({ notChecked: indexes.notChecked.filter((name) => names.includes(name)) });
|
|
200
|
+
if (flags.installed && names.some((name) => links[name] !== undefined)) {
|
|
201
|
+
note(LINKED_REPO_NOTE);
|
|
202
|
+
}
|
|
146
203
|
footer();
|
|
147
204
|
}
|
|
148
205
|
}
|
|
@@ -2,21 +2,32 @@
|
|
|
2
2
|
* `sous repo search <text>`.
|
|
3
3
|
*
|
|
4
4
|
* Searches the cached index of every repository this project trusts: recipe
|
|
5
|
-
* names, namespace names and descriptions.
|
|
6
|
-
* disk, so it works offline and never downloads anything; a
|
|
7
|
-
* index has not been fetched yet is named at the end rather
|
|
8
|
-
* out of the results.
|
|
5
|
+
* names, namespace names and descriptions. By default it reads only what is
|
|
6
|
+
* already on disk, so it works offline and never downloads anything; a
|
|
7
|
+
* repository whose index has not been fetched yet is named at the end rather
|
|
8
|
+
* than silently left out of the results. `--latest` searches each index as
|
|
9
|
+
* upstream serves it, saving nothing, and `--installed` searches only the
|
|
10
|
+
* recipes the lockfile pins, showing the installed version.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { Args, Flags } from "@oclif/core";
|
|
12
14
|
import { BaseCommand } from "../../base-command.js";
|
|
13
15
|
import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
|
|
16
|
+
import { readTrustedIndexes } from "../../lib/repos/catalog-inputs.js";
|
|
17
|
+
import {
|
|
18
|
+
LINKED_NOTE,
|
|
19
|
+
describeIndexSource,
|
|
20
|
+
pinnedCell,
|
|
21
|
+
} from "../../lib/repos/catalog-display.js";
|
|
22
|
+
import { readEffectiveLinks } from "../../lib/repos/links.js";
|
|
14
23
|
import { renderTable, type TableColumn } from "../../utils/table.js";
|
|
24
|
+
import { browsingFlags } from "../../utils/flags.js";
|
|
15
25
|
import {
|
|
16
26
|
blankLine,
|
|
17
27
|
footer,
|
|
18
28
|
indent,
|
|
19
29
|
log,
|
|
30
|
+
note,
|
|
20
31
|
paragraph,
|
|
21
32
|
section,
|
|
22
33
|
showCommandVars,
|
|
@@ -45,12 +56,24 @@ const COLUMNS: TableColumn[] = [
|
|
|
45
56
|
},
|
|
46
57
|
];
|
|
47
58
|
|
|
59
|
+
/** The column an `--installed` search adds: the version the lockfile pins. */
|
|
60
|
+
const INSTALLED_COLUMN: TableColumn = {
|
|
61
|
+
key: "installed",
|
|
62
|
+
header: "Installed",
|
|
63
|
+
overflow: "truncate",
|
|
64
|
+
minWidth: 9,
|
|
65
|
+
};
|
|
66
|
+
|
|
48
67
|
/** One recipe that matched, ready to be shown. */
|
|
49
68
|
type Match = {
|
|
50
69
|
repo: string;
|
|
51
70
|
key: string;
|
|
52
71
|
description: string;
|
|
53
72
|
versions: string[];
|
|
73
|
+
/** The version the lockfile pins, when it pins this recipe from this repository. */
|
|
74
|
+
installed?: string;
|
|
75
|
+
/** The linked checkout builds read it from, when it is installed and its repository is linked. */
|
|
76
|
+
linkedPath?: string;
|
|
54
77
|
};
|
|
55
78
|
|
|
56
79
|
export default class RepoSearch extends BaseCommand {
|
|
@@ -67,6 +90,7 @@ export default class RepoSearch extends BaseCommand {
|
|
|
67
90
|
static examples = [
|
|
68
91
|
"<%= config.bin %> repo search task",
|
|
69
92
|
"<%= config.bin %> repo search browser --limit 50",
|
|
93
|
+
"<%= config.bin %> repo search task --installed --latest",
|
|
70
94
|
];
|
|
71
95
|
|
|
72
96
|
static args = {
|
|
@@ -78,6 +102,7 @@ export default class RepoSearch extends BaseCommand {
|
|
|
78
102
|
|
|
79
103
|
static flags = {
|
|
80
104
|
...BaseCommand.baseFlags,
|
|
105
|
+
...browsingFlags(),
|
|
81
106
|
limit: Flags.integer({
|
|
82
107
|
description: "How many matches to show",
|
|
83
108
|
default: 25,
|
|
@@ -92,6 +117,8 @@ export default class RepoSearch extends BaseCommand {
|
|
|
92
117
|
Project: this.projectLabel,
|
|
93
118
|
Config: this.configContext.configPath,
|
|
94
119
|
Searching: args.text,
|
|
120
|
+
Reading: describeIndexSource(flags.latest),
|
|
121
|
+
...(flags.installed ? { Showing: "only what this project has installed" } : {}),
|
|
95
122
|
});
|
|
96
123
|
|
|
97
124
|
section("Recipes matching your search");
|
|
@@ -104,16 +131,17 @@ export default class RepoSearch extends BaseCommand {
|
|
|
104
131
|
|
|
105
132
|
const repos = Object.keys(service.currentRepos()).sort();
|
|
106
133
|
const matches: Match[] = [];
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (index === undefined) {
|
|
112
|
-
notFetched.push(repo);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
134
|
+
const indexes = await readTrustedIndexes(service, { latest: flags.latest });
|
|
135
|
+
const { notFetched, notChecked } = indexes;
|
|
136
|
+
const lock = service.lockService.read();
|
|
137
|
+
const links = readEffectiveLinks(this.configContext.sousDir);
|
|
115
138
|
|
|
139
|
+
for (const { name: repo, index } of indexes.repos) {
|
|
116
140
|
for (const [key, recipe] of Object.entries(index.recipes)) {
|
|
141
|
+
const locked = lock.recipes[key];
|
|
142
|
+
const installed = locked?.repo === repo ? locked.version : undefined;
|
|
143
|
+
if (flags.installed && installed === undefined) continue;
|
|
144
|
+
|
|
117
145
|
const namespace = key.slice(0, key.indexOf("/"));
|
|
118
146
|
const haystack = [
|
|
119
147
|
key,
|
|
@@ -129,6 +157,10 @@ export default class RepoSearch extends BaseCommand {
|
|
|
129
157
|
key,
|
|
130
158
|
description: recipe.description ?? "",
|
|
131
159
|
versions: Object.keys(recipe.versions).sort(),
|
|
160
|
+
...(installed === undefined ? {} : { installed }),
|
|
161
|
+
...(installed === undefined || links[repo] === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { linkedPath: links[repo]!.path }),
|
|
132
164
|
});
|
|
133
165
|
}
|
|
134
166
|
}
|
|
@@ -148,7 +180,9 @@ export default class RepoSearch extends BaseCommand {
|
|
|
148
180
|
repos.length === 0
|
|
149
181
|
? "This project trusts no repositories yet, so there is nothing to search. " +
|
|
150
182
|
"Add one with 'sous repo add <url>'."
|
|
151
|
-
:
|
|
183
|
+
: flags.installed
|
|
184
|
+
? `Nothing this project has installed matches '${args.text}'.`
|
|
185
|
+
: `Nothing in the repositories this project trusts matches '${args.text}'.`
|
|
152
186
|
);
|
|
153
187
|
} else {
|
|
154
188
|
const shown = matches.slice(0, flags.limit);
|
|
@@ -156,11 +190,18 @@ export default class RepoSearch extends BaseCommand {
|
|
|
156
190
|
key: match.key,
|
|
157
191
|
repo: match.repo,
|
|
158
192
|
versions: match.versions.join(", "),
|
|
193
|
+
installed: pinnedCell(match.installed, match.linkedPath),
|
|
159
194
|
description:
|
|
160
195
|
match.description.length > 0 ? match.description : "no description published",
|
|
161
196
|
}));
|
|
162
197
|
|
|
163
|
-
|
|
198
|
+
// Narrowed to what is installed, the installed version sits right after
|
|
199
|
+
// the recipe, ahead of every version the repository publishes.
|
|
200
|
+
const columns = flags.installed
|
|
201
|
+
? [COLUMNS[0]!, INSTALLED_COLUMN, ...COLUMNS.slice(1)]
|
|
202
|
+
: COLUMNS;
|
|
203
|
+
|
|
204
|
+
for (const line of renderTable(columns, rows, { indent: INDENT })) {
|
|
164
205
|
log(indent(line, INDENT));
|
|
165
206
|
}
|
|
166
207
|
|
|
@@ -179,9 +220,21 @@ export default class RepoSearch extends BaseCommand {
|
|
|
179
220
|
);
|
|
180
221
|
}
|
|
181
222
|
|
|
223
|
+
if (notChecked.length > 0) {
|
|
224
|
+
summary.push(
|
|
225
|
+
`These repositories could not be reached, so they were searched as the cache ` +
|
|
226
|
+
`has them and were not checked: ${notChecked.join(", ")}.`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
182
230
|
if (summary.length > 0) {
|
|
183
231
|
blankLine();
|
|
184
|
-
for (const line of summary)
|
|
232
|
+
for (const line of summary) paragraph(line);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (matches.slice(0, flags.limit).some((match) => match.linkedPath !== undefined)) {
|
|
236
|
+
blankLine();
|
|
237
|
+
note(LINKED_NOTE);
|
|
185
238
|
}
|
|
186
239
|
|
|
187
240
|
footer();
|
|
@@ -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
|
*
|