@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.
Files changed (52) hide show
  1. package/docs/markdown/_sidebar.md +2 -0
  2. package/docs/markdown/commands.md +10 -6
  3. package/docs/markdown/repositories-authoring.md +19 -11
  4. package/docs/markdown/repositories-consuming.md +34 -13
  5. package/docs/markdown/repositories-file-formats.md +88 -13
  6. package/docs/markdown/repositories-providers.md +8 -6
  7. package/docs/markdown/repositories-troubleshooting.md +6 -0
  8. package/docs/markdown/repositories-variables.md +13 -3
  9. package/docs/markdown/repositories.md +3 -3
  10. package/package.json +1 -1
  11. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +11 -7
  12. package/recipes/core/sous-skills/skills/about-sous-repos/SKILL.tpl.md +7 -4
  13. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  14. package/src/commands/lock/rebuild.ts +7 -6
  15. package/src/commands/recipe/show.ts +163 -64
  16. package/src/commands/repo/contribute.ts +1 -10
  17. package/src/commands/repo/gc.ts +3 -2
  18. package/src/commands/repo/release.ts +20 -3
  19. package/src/commands/repo/search.ts +2 -1
  20. package/src/commands/vars/ask.ts +32 -10
  21. package/src/lib/refs/find.ts +202 -21
  22. package/src/lib/refs/index.ts +2 -0
  23. package/src/lib/refs/parse.ts +844 -0
  24. package/src/lib/refs/scopes.ts +34 -0
  25. package/src/lib/refs/settle.ts +219 -0
  26. package/src/lib/repos/catalog.ts +87 -80
  27. package/src/lib/repos/declarations.ts +177 -0
  28. package/src/lib/repos/formats/common.ts +14 -0
  29. package/src/lib/repos/formats/index-file.ts +42 -2
  30. package/src/lib/repos/formats/lockfile.ts +5 -3
  31. package/src/lib/repos/formats/recipe-manifest.ts +16 -212
  32. package/src/lib/repos/formats/variable-definition.ts +268 -0
  33. package/src/lib/repos/index.ts +0 -1
  34. package/src/lib/repos/lock-service.ts +3 -2
  35. package/src/lib/repos/locked-namespace-resolver.ts +21 -15
  36. package/src/lib/repos/locked-recipes.ts +2 -2
  37. package/src/lib/repos/namespace-resolver.ts +40 -47
  38. package/src/lib/repos/providers/base.ts +30 -0
  39. package/src/lib/repos/providers/github.ts +22 -0
  40. package/src/lib/repos/providers/gitlab.ts +68 -0
  41. package/src/lib/repos/providers/provider.ts +96 -0
  42. package/src/lib/repos/release/index-builder.ts +221 -47
  43. package/src/lib/repos/release/index.ts +1 -0
  44. package/src/lib/repos/release/plan.ts +8 -6
  45. package/src/lib/repos/release/settle.ts +237 -0
  46. package/src/lib/repos/release/tags.ts +3 -7
  47. package/src/lib/repos/resolver.ts +251 -53
  48. package/src/lib/repos/subscription-service.ts +355 -49
  49. package/src/lib/repos/update-plan.ts +3 -2
  50. package/src/lib/vars/mappings.ts +17 -30
  51. package/src/lib/vars/question-plan.ts +44 -21
  52. package/src/lib/repos/ref.ts +0 -513
@@ -2,27 +2,41 @@
2
2
  * `sous recipe show <ref>`.
3
3
  *
4
4
  * Shows one recipe in full: where it is published, every version it publishes,
5
- * what the version this project would use depends on, the questions it asks,
6
- * and where its files land in this project. It reads only what sous already has
7
- * on disk: the repository's cached index, the project's lockfile, and the
8
- * recipe's own files when they are in the store or a linked working copy.
9
- * `--latest` reads the repository's index from upstream instead, saving
10
- * nothing, and `--installed` looks the ref up among installed recipes only.
5
+ * what the version this project would use depends on and how each dependency is
6
+ * declared, everything subscribing to it would install, every question that
7
+ * would ask, and where its files land in this project. It reads only what sous
8
+ * already has on disk: the repository's cached index, the project's lockfile,
9
+ * and the recipe's own files when they are in the store or a linked working
10
+ * copy. The index describes each version's dependencies and questions, so a
11
+ * recipe nothing has installed is described as fully as one in the store, its
12
+ * files excepted. `--latest` reads the repository's index from upstream
13
+ * instead, saving nothing, and `--installed` looks the ref up among installed
14
+ * recipes only.
11
15
  */
12
16
 
13
17
  import { Args } from "@oclif/core";
18
+ import semver from "semver";
14
19
  import { BaseCommand } from "../../base-command.js";
15
20
  import { resolveRootScope } from "../../lib/settings.js";
16
- import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
21
+ import {
22
+ subscriptionServiceFor,
23
+ type SubscriptionPreview,
24
+ type SubscriptionService,
25
+ } from "../../lib/repos/subscription-service.js";
17
26
  import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
18
27
  import {
19
28
  describeInstalled,
20
29
  describeRecipe,
30
+ type CatalogInputs,
21
31
  type RecipeContentListing,
22
32
  type RecipeDependencyListing,
23
- type RecipeVariableListing,
33
+ type RecipeDetail,
24
34
  type RecipeVersionListing,
25
35
  } from "../../lib/repos/catalog.js";
36
+ import { PROJECT_REQUESTER } from "../../lib/repos/resolver.js";
37
+ import { describeDeclaration, describeDependencyKind } from "../../lib/repos/declarations.js";
38
+ import { formatQuestionPlan } from "../../lib/vars/question-plan.js";
39
+ import { describeError } from "../../lib/repos/release/validate.js";
26
40
  import {
27
41
  INDENT,
28
42
  describeIndexSource,
@@ -61,14 +75,19 @@ const DEPENDENCY_COLUMNS: TableColumn[] = [
61
75
  { key: "kind", header: "Kind", overflow: "wrap", priority: "medium", minWidth: 16 },
62
76
  ];
63
77
 
64
- /** The columns the variable listing shows. */
65
- const VARIABLE_COLUMNS: TableColumn[] = [
66
- { key: "name", header: "Variable", overflow: "truncate", minWidth: 10 },
67
- { key: "type", header: "Type", priority: "medium" },
68
- { key: "env", header: "Environment variable", overflow: "truncate", minWidth: 12 },
69
- { key: "required", header: "Required", priority: "medium" },
70
- { key: "secret", header: "Secret", priority: "low" },
71
- { key: "prompt", header: "What it asks", overflow: "wrap", flex: 1, priority: "low", minWidth: 16 },
78
+ /** The columns the listing of everything a subscription installs shows. */
79
+ const INSTALLS_COLUMNS: TableColumn[] = [
80
+ { key: "key", header: "Recipe", kind: "path", overflow: "truncate", minWidth: 12 },
81
+ { key: "version", header: "Version", overflow: "truncate", minWidth: 7 },
82
+ { key: "kind", header: "Kind", overflow: "wrap", minWidth: 16 },
83
+ {
84
+ key: "neededBy",
85
+ header: "Needed by",
86
+ overflow: "wrap",
87
+ flex: 1,
88
+ priority: "medium",
89
+ minWidth: 12,
90
+ },
72
91
  ];
73
92
 
74
93
  /** The columns the content listing shows. */
@@ -86,7 +105,8 @@ const CONTENT_COLUMNS: TableColumn[] = [
86
105
  ];
87
106
 
88
107
  export default class RecipeShow extends BaseCommand {
89
- static description = "Show one recipe: its versions, dependencies, variables and files";
108
+ static description =
109
+ "Show one recipe: its versions, dependencies, what subscribing installs and asks, and its files";
90
110
 
91
111
  /**
92
112
  * The other spelling of the topic. It lives under a hidden topic, so it is
@@ -165,24 +185,76 @@ export default class RecipeShow extends BaseCommand {
165
185
  printBrowsingNotes({ notChecked: notChecked.filter((name) => name === detail.repo) });
166
186
 
167
187
  this.printVersions(detail.versions);
168
- this.printDependencies(detail.dependencies, detail.manifestRead);
188
+ this.printDependencies(
189
+ detail.dependencies,
190
+ detail.manifestRead || detail.dependenciesRecorded
191
+ );
169
192
 
170
- if (!detail.manifestRead) {
193
+ const preview = await this.preview(service, inputs, detail);
194
+ if (typeof preview === "string") {
195
+ blankLine();
196
+ paragraph(preview);
197
+ } else {
198
+ this.printInstalls(preview);
199
+ this.printQuestions(preview);
200
+ }
201
+
202
+ if (detail.manifestRead) {
203
+ this.printContents(detail.contents);
204
+ } else {
205
+ blankLine();
206
+ subheading("Where its files land in this project");
171
207
  blankLine();
172
208
  paragraph(
173
- "The recipe's own files are not on this machine, so the questions it asks and " +
174
- "the files it publishes are not known here. Subscribing to it fetches them."
209
+ "The recipe's own files are not on this machine, so the files it publishes are " +
210
+ "not known here. Subscribing to it fetches them."
175
211
  );
176
- footer();
177
- return;
178
212
  }
179
213
 
180
- this.printVariables(detail.variables);
181
- this.printContents(detail.contents);
182
-
183
214
  footer();
184
215
  }
185
216
 
217
+ /**
218
+ * Works out what subscribing to the described version would install and ask,
219
+ * from the same indexes the rest of the page reads, fetching nothing. The
220
+ * answer is a sentence instead when it cannot be worked out.
221
+ *
222
+ * @param service - The project's subscription service.
223
+ * @param inputs - The catalog's inputs, whose indexes the preview resolves against.
224
+ * @param detail - The recipe being described.
225
+ */
226
+ private async preview(
227
+ service: SubscriptionService,
228
+ inputs: CatalogInputs,
229
+ detail: RecipeDetail
230
+ ): Promise<SubscriptionPreview | string> {
231
+ if (detail.describing === undefined) {
232
+ return (
233
+ "This recipe publishes no version a subscription would install, so nothing " +
234
+ "is described below."
235
+ );
236
+ }
237
+ try {
238
+ return await service.previewSubscription(
239
+ {
240
+ repo: detail.repo,
241
+ namespace: detail.namespace,
242
+ recipe: detail.name,
243
+ range: detail.describing,
244
+ },
245
+ {
246
+ indexes: new Map(inputs.repos.map((repo) => [repo.name, repo.index])),
247
+ ...(semver.prerelease(detail.describing) === null ? {} : { prerelease: true }),
248
+ }
249
+ );
250
+ } catch (error) {
251
+ return (
252
+ `Sous could not work out what subscribing to version ${detail.describing} ` +
253
+ `would install. ${describeError(error)}`
254
+ );
255
+ }
256
+ }
257
+
186
258
  /**
187
259
  * Every published version, newest first, with what each one is to this
188
260
  * project.
@@ -207,24 +279,21 @@ export default class RecipeShow extends BaseCommand {
207
279
  }
208
280
 
209
281
  /**
210
- * What the described version depends on, from both sides: what the recipe's
211
- * manifest declares, and what its repository's index resolved that to when it
212
- * was released.
282
+ * What the described version depends on: the manifest entry that brings each
283
+ * dependency in and whether it is a co-subscription or a build dependency,
284
+ * and what its repository's index resolved it to when it was released.
213
285
  *
214
286
  * @param dependencies - The dependencies the catalog listed.
215
- * @param manifestRead - Whether the recipe's own manifest could be read.
287
+ * @param known - Whether the manifest or the index says what the version declares.
216
288
  */
217
- private printDependencies(
218
- dependencies: RecipeDependencyListing[],
219
- manifestRead: boolean
220
- ): void {
289
+ private printDependencies(dependencies: RecipeDependencyListing[], known: boolean): void {
221
290
  blankLine();
222
291
  subheading("What it depends on");
223
292
  blankLine();
224
293
 
225
294
  if (dependencies.length === 0) {
226
295
  paragraph(
227
- manifestRead
296
+ known
228
297
  ? "This recipe depends on nothing else."
229
298
  : "The repository's index records no dependencies for this version."
230
299
  );
@@ -233,7 +302,10 @@ export default class RecipeShow extends BaseCommand {
233
302
 
234
303
  const rows = dependencies.map((entry) => ({
235
304
  key: entry.key,
236
- declared: entry.declared ?? "not declared in the manifest",
305
+ declared:
306
+ entry.declared === undefined
307
+ ? "not recorded in the index"
308
+ : describeDeclaration(entry.declared),
237
309
  resolved:
238
310
  entry.resolvedVersion ??
239
311
  (entry.resolvedRange === undefined
@@ -249,32 +321,69 @@ export default class RecipeShow extends BaseCommand {
249
321
  }
250
322
 
251
323
  /**
252
- * The questions the recipe asks, and the environment variable each answer is
253
- * stored under.
324
+ * Every recipe a subscription to the described version would install, each
325
+ * with whether its files land in the project and what brings it in, and the
326
+ * repositories it would need that the project does not trust yet.
254
327
  *
255
- * @param variables - The variables the catalog listed.
328
+ * @param preview - What subscribing would install and ask.
256
329
  */
257
- private printVariables(variables: RecipeVariableListing[]): void {
330
+ private printInstalls(preview: SubscriptionPreview): void {
258
331
  blankLine();
259
- subheading("What it asks you");
332
+ subheading("What subscribing installs");
260
333
  blankLine();
261
334
 
262
- if (variables.length === 0) {
263
- paragraph("This recipe asks no questions.");
264
- return;
335
+ const rows = preview.resolved.map((recipe) => {
336
+ const subscribed = recipe.requestedBy.includes(PROJECT_REQUESTER);
337
+ const holders = recipe.requestedBy.filter((holder) => holder !== PROJECT_REQUESTER);
338
+ return {
339
+ key: recipe.key,
340
+ version: recipe.version,
341
+ kind: subscribed ? "the subscription itself" : describeDependencyKind(recipe.kind),
342
+ neededBy: holders.length === 0 ? "this subscription" : holders.join(", "),
343
+ };
344
+ });
345
+
346
+ for (const line of renderTable(INSTALLS_COLUMNS, rows, { indent: INDENT })) {
347
+ log(indent(line, INDENT));
265
348
  }
266
349
 
267
- const rows = variables.map((entry) => ({
268
- name: entry.name,
269
- type: entry.type,
270
- env: entry.env,
271
- required: entry.required ? "yes" : "no",
272
- secret: entry.secret ? "yes" : "no",
273
- prompt: entry.prompt,
274
- }));
350
+ if (preview.missingRepos.length > 0) {
351
+ blankLine();
352
+ paragraph(
353
+ `Subscribing also needs ${
354
+ preview.missingRepos.length === 1 ? "a repository" : "repositories"
355
+ } this project does not trust yet, so what ${
356
+ preview.missingRepos.length === 1 ? "it publishes" : "they publish"
357
+ } is not listed above: ` +
358
+ preview.missingRepos
359
+ .map(
360
+ (missing) =>
361
+ `${missing.name}${missing.url === undefined ? "" : ` (${missing.url})`}, ` +
362
+ `needed by ${missing.requiredBy.map((entry) => entry.requestedBy).join(", ")}`
363
+ )
364
+ .join("; ") +
365
+ "."
366
+ );
367
+ }
368
+ }
275
369
 
276
- for (const line of renderTable(VARIABLE_COLUMNS, rows, { indent: INDENT })) {
277
- log(indent(line, INDENT));
370
+ /**
371
+ * Every question a subscription to the described version would ask, laid out
372
+ * exactly as `sous subscription add --dry-run` lays them out, without the
373
+ * flags that answer them ahead of time.
374
+ *
375
+ * @param preview - What subscribing would install and ask.
376
+ */
377
+ private printQuestions(preview: SubscriptionPreview): void {
378
+ blankLine();
379
+ subheading("What subscribing asks you");
380
+ blankLine();
381
+
382
+ for (const line of formatQuestionPlan(preview.questions, {
383
+ unreadable: preview.unreadable,
384
+ answerHints: false,
385
+ })) {
386
+ log(line === "" ? "" : indent(line));
278
387
  }
279
388
  }
280
389
 
@@ -311,13 +420,3 @@ export default class RecipeShow extends BaseCommand {
311
420
  }
312
421
  }
313
422
 
314
- /**
315
- * Plain-language wording for how a dependency was declared.
316
- *
317
- * @param kind - What the manifest declared it as, when the manifest was read.
318
- */
319
- function describeDependencyKind(kind: "depends" | "subscribes" | undefined): string {
320
- if (kind === "depends") return "build dependency";
321
- if (kind === "subscribes") return "co-subscription";
322
- return "unknown";
323
- }
@@ -6,7 +6,6 @@ import {
6
6
  SousScope,
7
7
  findReference,
8
8
  pickReference,
9
- referenceReposFromIndexes,
10
9
  type ReferenceRepo,
11
10
  } from "../../lib/refs/index.js";
12
11
  import {
@@ -374,15 +373,7 @@ export default class RepoContribute extends BaseCommand {
374
373
  shellEnv: this.shellEnv,
375
374
  });
376
375
 
377
- const trusted = service.currentRepos();
378
- const repos: ReferenceRepo[] = Object.keys(trusted).map((name) => {
379
- const url = trusted[name]?.url;
380
- const index = service.cachedIndex(name);
381
- if (index === undefined) {
382
- return { name, ...(url === undefined ? {} : { url }), namespaces: [], recipes: [] };
383
- }
384
- return referenceReposFromIndexes([name], new Map([[name, index]]), { [name]: url })[0]!;
385
- });
376
+ const repos: ReferenceRepo[] = service.cachedReferenceRepos();
386
377
 
387
378
  const matches = findReference(
388
379
  ref,
@@ -17,6 +17,7 @@ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js"
17
17
  import { resolveStoreSettings } from "../../lib/repos/store/settings.js";
18
18
  import type { StoreKey } from "../../lib/repos/store/contract.js";
19
19
  import { renderTable, type TableColumn } from "../../utils/table.js";
20
+ import { splitRecipeKey } from "../../lib/refs/parse.js";
20
21
  import {
21
22
  blankLine,
22
23
  dryRunNotice,
@@ -120,12 +121,12 @@ export default class RepoGc extends BaseCommand {
120
121
  const keep: StoreKey[] = Object.entries(lock.recipes).flatMap(([key, entry]) => {
121
122
  const identity = lock.repos[entry.repo]?.identity;
122
123
  if (identity === undefined) return [];
123
- const namespace = key.slice(0, key.indexOf("/"));
124
+ const { namespace, name } = splitRecipeKey(key);
124
125
  return [
125
126
  {
126
127
  identity,
127
128
  namespace,
128
- name: key.slice(namespace.length + 1),
129
+ name,
129
130
  version: entry.version,
130
131
  },
131
132
  ];
@@ -30,6 +30,7 @@ import {
30
30
  releaseScope,
31
31
  remoteUrl,
32
32
  scopeProblems,
33
+ settleDependencyLocations,
33
34
  uncommittedChanges,
34
35
  validateRepo,
35
36
  warningsIn,
@@ -38,6 +39,7 @@ import {
38
39
  type ReleasePlan,
39
40
  type ReleaseScope,
40
41
  type RepoValidation,
42
+ type SettledDependency,
41
43
  type ValidationProblem,
42
44
  } from "../../lib/repos/release/index.js";
43
45
  import {
@@ -195,7 +197,14 @@ export default class RepoRelease extends Command {
195
197
  }
196
198
  log(` Read ${describeCount(validation.recipes.length, "recipe")}.`);
197
199
 
198
- if (flags.check) return await this.runCheck(rootDir, validation);
200
+ // A dependency that reads more than one way is settled before anything is
201
+ // planned or written, so a repository that cannot be reached stops the run
202
+ // with nothing changed.
203
+ const settling = await settleDependencyLocations(validation);
204
+ reportProblems(settling.problems);
205
+ if (hasErrors(settling.problems)) return this.stopForErrors(settling.problems);
206
+
207
+ if (flags.check) return await this.runCheck(rootDir, validation, settling.settled);
199
208
 
200
209
  // --- Plan -------------------------------------------------------------
201
210
 
@@ -284,6 +293,7 @@ export default class RepoRelease extends Command {
284
293
  existing,
285
294
  sousVersion: SOUS_VERSION,
286
295
  publishing,
296
+ settled: settling.settled,
287
297
  });
288
298
  reportProblems(rebuilt.problems);
289
299
  if (hasErrors(rebuilt.problems)) return this.stopForErrors(rebuilt.problems);
@@ -365,15 +375,22 @@ export default class RepoRelease extends Command {
365
375
  *
366
376
  * @param rootDir - The repository's root directory.
367
377
  * @param validation - The validated repository.
378
+ * @param settled - The dependencies that read more than one way, as settled.
368
379
  */
369
380
  private async runCheck(
370
381
  rootDir: string,
371
- validation: RepoValidation
382
+ validation: RepoValidation,
383
+ settled: Map<string, SettledDependency>
372
384
  ): Promise<void> {
373
385
  section("Checking the index");
374
386
 
375
387
  const existing = readExistingIndex(rootDir);
376
- const result = await buildIndex({ validation, existing, sousVersion: SOUS_VERSION });
388
+ const result = await buildIndex({
389
+ validation,
390
+ existing,
391
+ sousVersion: SOUS_VERSION,
392
+ settled,
393
+ });
377
394
  reportProblems(result.problems);
378
395
  if (hasErrors(result.problems)) return this.stopForErrors(result.problems);
379
396
 
@@ -21,6 +21,7 @@ import {
21
21
  } from "../../lib/repos/catalog-display.js";
22
22
  import { readEffectiveLinks } from "../../lib/repos/links.js";
23
23
  import { renderTable, type TableColumn } from "../../utils/table.js";
24
+ import { namespaceOfKey } from "../../lib/refs/parse.js";
24
25
  import { browsingFlags } from "../../utils/flags.js";
25
26
  import {
26
27
  blankLine,
@@ -142,7 +143,7 @@ export default class RepoSearch extends BaseCommand {
142
143
  const installed = locked?.repo === repo ? locked.version : undefined;
143
144
  if (flags.installed && installed === undefined) continue;
144
145
 
145
- const namespace = key.slice(0, key.indexOf("/"));
146
+ const namespace = namespaceOfKey(key);
146
147
  const haystack = [
147
148
  key,
148
149
  recipe.description ?? "",
@@ -48,7 +48,9 @@ import {
48
48
  referenceContextFromVariables,
49
49
  variableReferenceKey,
50
50
  type ReferenceMatch,
51
+ type ReferenceRepo,
51
52
  } from "../../lib/refs/index.js";
53
+ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
52
54
  import {
53
55
  applyProvidedAnswers,
54
56
  askForMissing,
@@ -212,7 +214,10 @@ export default class VarsAsk extends BaseCommand {
212
214
 
213
215
  // What the reference and the flags name, as the fully qualified reference of
214
216
  // every variable to ask. Undefined means everything the usual rules pick.
215
- const only = await this.resolveSelection(defined, context, selection);
217
+ // A reference written as a location settles against the trusted
218
+ // repositories, read from the cached indexes; a definitions file has none.
219
+ const trusted = flags.file === undefined ? this.trustedRepos() : [];
220
+ const only = await this.resolveSelection(defined, context, selection, trusted);
216
221
 
217
222
  const askOptions = {
218
223
  sousDir: this.configContext.sousDir,
@@ -257,13 +262,17 @@ export default class VarsAsk extends BaseCommand {
257
262
  * @param defined - Every variable definition in play.
258
263
  * @param ladder - The environment layers, for resolving environment variable names.
259
264
  * @param selection - The reference and the flags, as the caller wrote them.
265
+ * @param trusted - The trusted repositories, which settle a reference written as a location.
260
266
  * @returns The variables to ask, or undefined when nothing narrowed anything.
261
267
  */
262
268
  private async resolveSelection(
263
269
  defined: DefinedVariable[],
264
270
  ladder: LadderContext,
265
- selection: Selection
271
+ selection: Selection,
272
+ trusted: ReferenceRepo[] = []
266
273
  ): Promise<string[] | undefined> {
274
+ const contextOf = (pool: DefinedVariable[]) =>
275
+ referenceContextFromVariables(pool, ladder, trusted);
267
276
  const { name, repo, namespace, vars } = selection;
268
277
  if (name === undefined && repo === undefined && namespace === undefined && vars === undefined) {
269
278
  return undefined;
@@ -272,13 +281,13 @@ export default class VarsAsk extends BaseCommand {
272
281
  let pool = defined;
273
282
 
274
283
  if (repo !== undefined) {
275
- const match = await this.pick(findRepository(repo, contextOf(pool, ladder)), repo, selection);
284
+ const match = await this.pick(findRepository(repo, contextOf(pool)), repo, selection);
276
285
  pool = pool.filter((entry) => entry.recipe.repo === match.repo);
277
286
  }
278
287
 
279
288
  if (namespace !== undefined) {
280
289
  const match = await this.pick(
281
- findNamespace(namespace, contextOf(pool, ladder)),
290
+ findNamespace(namespace, contextOf(pool)),
282
291
  namespace,
283
292
  selection
284
293
  );
@@ -287,7 +296,7 @@ export default class VarsAsk extends BaseCommand {
287
296
 
288
297
  if (name !== undefined) {
289
298
  const match = await this.pick(
290
- findReference(name, ALL_SCOPES, contextOf(pool, ladder)),
299
+ findReference(name, ALL_SCOPES, contextOf(pool)),
291
300
  name,
292
301
  selection
293
302
  );
@@ -298,7 +307,7 @@ export default class VarsAsk extends BaseCommand {
298
307
  const chosen: DefinedVariable[] = [];
299
308
  for (const wanted of vars) {
300
309
  const match = await this.pick(
301
- findVariable(wanted, contextOf(pool, ladder)),
310
+ findVariable(wanted, contextOf(pool)),
302
311
  wanted,
303
312
  selection
304
313
  );
@@ -317,6 +326,23 @@ export default class VarsAsk extends BaseCommand {
317
326
  return [...new Set(pool.map(variableReferenceKey))];
318
327
  }
319
328
 
329
+ /**
330
+ * The trusted repositories as a reference searches them, from the cached
331
+ * indexes alone. A project whose repositories cannot be read this way still
332
+ * answers every other form of reference, so a failure here is an empty list.
333
+ */
334
+ private trustedRepos(): ReferenceRepo[] {
335
+ try {
336
+ return subscriptionServiceFor({
337
+ configContext: this.configContext,
338
+ settings: this.settings,
339
+ shellEnv: this.shellEnv,
340
+ }).cachedReferenceRepos();
341
+ } catch {
342
+ return [];
343
+ }
344
+ }
345
+
320
346
  /**
321
347
  * Settles which of the things a reference could have meant this run proceeds
322
348
  * with, through the rule every command shares.
@@ -356,10 +382,6 @@ function narrowTo(pool: DefinedVariable[], match: ReferenceMatch): DefinedVariab
356
382
  });
357
383
  }
358
384
 
359
- /** The reference context for the variables still in the running. */
360
- function contextOf(pool: DefinedVariable[], ladder: LadderContext) {
361
- return referenceContextFromVariables(pool, ladder);
362
- }
363
385
 
364
386
  /** What this run is asking about, in the words the header shows. */
365
387
  function describeSelection(selection: Selection, all: boolean): string {