@sous-io/sous 0.2.32 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +7 -6
  2. package/docs/markdown/_sidebar.md +1 -0
  3. package/docs/markdown/commands.md +8 -4
  4. package/docs/markdown/configuration.md +28 -4
  5. package/docs/markdown/repositories-authoring.md +22 -2
  6. package/docs/markdown/repositories-consuming.md +40 -11
  7. package/docs/markdown/repositories-file-formats.md +28 -13
  8. package/docs/markdown/repositories-quickstart.md +1 -1
  9. package/docs/markdown/repositories-variables.md +16 -0
  10. package/docs/markdown/repositories.md +8 -4
  11. package/package.json +2 -1
  12. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +33 -20
  13. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
  14. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +21 -9
  15. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  16. package/sous.config.schema.json +27 -15
  17. package/src/commands/lock/rebuild.ts +3 -6
  18. package/src/commands/namespace/show.ts +2 -2
  19. package/src/commands/recipe/show.ts +26 -9
  20. package/src/commands/repo/contribute.ts +32 -23
  21. package/src/commands/repo/gc.ts +1 -1
  22. package/src/commands/repo/remove.ts +1 -1
  23. package/src/commands/repo/search.ts +1 -1
  24. package/src/commands/vars/ask.ts +75 -54
  25. package/src/lib/build-service.ts +93 -15
  26. package/src/lib/config-schema.ts +59 -19
  27. package/src/lib/include-resolver.ts +307 -69
  28. package/src/lib/markdown-compiler.ts +289 -82
  29. package/src/lib/project-scaffold/templates.ts +19 -11
  30. package/src/lib/repos/catalog-inputs.ts +1 -1
  31. package/src/lib/repos/catalog.ts +98 -72
  32. package/src/lib/repos/declarations.ts +22 -12
  33. package/src/lib/repos/formats/index-file.ts +1 -1
  34. package/src/lib/repos/formats/recipe-manifest.ts +3 -4
  35. package/src/lib/repos/lock-service.ts +1 -1
  36. package/src/lib/repos/locked-namespace-resolver.ts +14 -6
  37. package/src/lib/repos/locked-recipe-lookup.ts +123 -0
  38. package/src/lib/repos/locked-recipes.ts +1 -1
  39. package/src/lib/repos/memories-hash-name.ts +33 -0
  40. package/src/lib/repos/namespace-resolver.ts +198 -119
  41. package/src/lib/repos/recipe-config-layers.ts +29 -1
  42. package/src/lib/repos/recipe-key-matcher.ts +62 -0
  43. package/src/lib/repos/recipe-memories.ts +175 -0
  44. package/src/lib/repos/recipe-targets.ts +32 -28
  45. package/src/lib/repos/ref-search.ts +1 -1
  46. package/src/lib/repos/release/index-builder.ts +23 -15
  47. package/src/lib/repos/release/plan.ts +14 -7
  48. package/src/lib/repos/release/settle.ts +38 -121
  49. package/src/lib/repos/release/submit-checkout.ts +42 -23
  50. package/src/lib/repos/release/tags.ts +1 -1
  51. package/src/lib/repos/resolver.ts +90 -72
  52. package/src/lib/repos/subscription-service.ts +192 -170
  53. package/src/lib/settings.ts +37 -33
  54. package/src/lib/vars/answers.ts +9 -1
  55. package/src/lib/vars/ask.ts +3 -3
  56. package/src/lib/vars/display.ts +1 -3
  57. package/src/lib/vars/index.ts +1 -0
  58. package/src/lib/vars/mappings.ts +17 -9
  59. package/src/lib/vars/secrets.ts +131 -0
  60. package/src/services/ref-resolver/container.ts +107 -0
  61. package/src/services/ref-resolver/dependency.ts +60 -0
  62. package/src/services/ref-resolver/format.ts +197 -0
  63. package/src/services/ref-resolver/glob.ts +115 -0
  64. package/src/services/ref-resolver/hash-names.ts +155 -0
  65. package/src/services/ref-resolver/index.ts +55 -0
  66. package/src/services/ref-resolver/injectable.ts +36 -0
  67. package/src/services/ref-resolver/location.ts +62 -0
  68. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
  69. package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
  70. package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
  71. package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
  72. package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
  73. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
  74. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
  75. package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
  76. package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
  77. package/src/services/ref-resolver/named-refs.ts +144 -0
  78. package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
  79. package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
  80. package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
  81. package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
  82. package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
  83. package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
  84. package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
  85. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
  86. package/src/services/ref-resolver/parts.ts +115 -0
  87. package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
  88. package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
  89. package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
  90. package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
  91. package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
  92. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
  93. package/src/services/ref-resolver/ref-helpers.ts +62 -0
  94. package/src/services/ref-resolver/ref-picker-service.ts +204 -0
  95. package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
  96. package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
  97. package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
  98. package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
  99. package/src/services/ref-resolver/source.ts +48 -0
  100. package/src/services/ref-resolver/tokens.ts +25 -0
  101. package/src/services/ref-resolver/types.ts +155 -0
  102. package/src/templating/init-liquid-engine.ts +33 -18
  103. package/src/templating/lib/secret-scope.ts +41 -0
  104. package/src/templating/tags/exportScalarVarsJs.ts +5 -2
  105. package/src/templating/tags/showVars.ts +8 -2
  106. package/src/lib/refs/find.ts +0 -770
  107. package/src/lib/refs/index.ts +0 -14
  108. package/src/lib/refs/parse.ts +0 -844
  109. package/src/lib/refs/pick.ts +0 -147
  110. package/src/lib/refs/scopes.ts +0 -95
  111. package/src/lib/refs/settle.ts +0 -219
@@ -1,770 +0,0 @@
1
- /**
2
- * Working out what a word on the command line names.
3
- *
4
- * Sous commands take a reference where a person would say a name: `sous
5
- * subscribe task-files`, `sous namespace show workflow`, `sous vars ask apiUrl`.
6
- * One module settles what such a word means, for every command, so a reference
7
- * that resolves in one place resolves the same way everywhere.
8
- *
9
- * A reference may be written at any level of qualification, and the fully
10
- * qualified spelling is always accepted:
11
- *
12
- * sous-recipes a repository
13
- * sous-recipes:workflow a namespace, fully qualified
14
- * workflow the same namespace, bare
15
- * sous-recipes:workflow/task-files a recipe, fully qualified
16
- * workflow/task-files the same recipe, partly qualified
17
- * task-files the same recipe, bare
18
- * workflow/task-files.taskFileRoot a variable, partly qualified
19
- * taskFileRoot the same variable, bare
20
- * SOUS_VAR_TASK_FILE_ROOT the environment variable answering it
21
- *
22
- * A location names the same things: an HTTPS, SSH or scheme-less URL, a
23
- * provider-scheme locator, or a URL copied from the browser, matched against
24
- * the trusted repository at that location and settled through its index.
25
- *
26
- * Matching tries the exact spelling first, then ignores case; a name that still
27
- * matches several things is a question for `pickReference`. The exact pass
28
- * comes first because a variable (`apiUrl`) and an environment variable name
29
- * can differ only in case, and the spelling typed is the one meant.
30
- *
31
- * The order matches come back in is part of the contract, because it is the
32
- * order they are offered in and the one `--accept-first` picks from. Matches
33
- * are sorted by how qualified the spelling that matched was (a fully qualified
34
- * reference first, then a partly qualified one, then a bare name, and an
35
- * environment variable name last), then by scope in the order `SCOPE_ORDER`
36
- * documents, then by repository in search order, then alphabetically by
37
- * namespace, recipe and variable.
38
- */
39
-
40
- import type { IndexFile } from "../repos/formats/index-file.js";
41
- import { repoIdentity } from "../repos/identity.js";
42
- import { requireProvider } from "../repos/providers/index.js";
43
- import {
44
- isRepositoryReading,
45
- looksLikeLocation,
46
- parseRef,
47
- splitRecipeKey,
48
- type ParsedRef,
49
- type RefReading,
50
- } from "./parse.js";
51
- import { settleInIndex } from "./settle.js";
52
- import type { DefinedVariable } from "../vars/definition-source.js";
53
- import { variableCandidates, type LadderContext } from "../vars/ladder.js";
54
- import { bareName } from "../vars/names.js";
55
- import { SCOPE_LABELS, SousScope, scopeRank } from "./scopes.js";
56
-
57
- // --- What the search reads ----------------------------------------------------------------------
58
-
59
- /** One namespace a reference could name. */
60
- export type ReferenceNamespace = {
61
- /** The namespace name. */
62
- name: string;
63
- /** Its one-paragraph summary, when the index carries one. */
64
- description?: string;
65
- };
66
-
67
- /** One recipe a reference could name. */
68
- export type ReferenceRecipe = {
69
- /** The namespace publishing it. */
70
- namespace: string;
71
- /** The recipe name. */
72
- name: string;
73
- /** Its one-paragraph summary, when the index carries one. */
74
- description?: string;
75
- /** The folder it lives in, relative to the repository root, when the index says. */
76
- path?: string;
77
- };
78
-
79
- /** One repository a reference could name, and what it publishes. */
80
- export type ReferenceRepo = {
81
- /** The short name this project calls it. */
82
- name: string;
83
- /** Where it lives, as the project's config records it. */
84
- url?: string;
85
- /**
86
- * Its canonical identity, which is how a reference written as a location
87
- * finds it whatever short name this project gave it.
88
- */
89
- identity?: string;
90
- /** Every namespace it publishes. */
91
- namespaces: ReferenceNamespace[];
92
- /** Every recipe it publishes. */
93
- recipes: ReferenceRecipe[];
94
- };
95
-
96
- /**
97
- * Everything a reference resolves against. Each part is optional, because a
98
- * command supplies only what it can see: a browsing command has repositories
99
- * and their indexes, `sous vars ask --file` has definitions and no repository
100
- * at all.
101
- */
102
- export type ReferenceContext = {
103
- /**
104
- * The repositories to search, in the order their matches should be listed:
105
- * the built-in repository first, then the ones the config names, in config
106
- * order.
107
- */
108
- repos?: ReferenceRepo[];
109
- /** The variable definitions in play, with the recipe that published each. */
110
- variables?: DefinedVariable[];
111
- /**
112
- * The environment layers, which decide which generated environment variable
113
- * names are in use. Without it only a definition's declared `env` name is
114
- * searched.
115
- */
116
- ladder?: LadderContext;
117
- };
118
-
119
- // --- What the search answers --------------------------------------------------------------------
120
-
121
- /**
122
- * How qualified the spelling that matched was. Lower is more specific, and the
123
- * listing order follows it.
124
- */
125
- export enum Qualification {
126
- /** The reference named the whole identity, repository included. */
127
- Full = 0,
128
- /** The reference named part of the identity, such as `namespace/recipe`. */
129
- Partial = 1,
130
- /** The reference was a bare name. */
131
- Bare = 2,
132
- /** The reference was an environment variable name. */
133
- EnvName = 3,
134
- }
135
-
136
- /** One thing a reference could have meant. */
137
- export type ReferenceMatch = {
138
- /** What kind of thing it is. */
139
- scope: SousScope;
140
- /**
141
- * The fully qualified name of it: `repo`, `repo:namespace`,
142
- * `repo:namespace/recipe`, or `repo:namespace/recipe.variable`. Two matches
143
- * never share a key within one scope, so it is safe to compare and to print.
144
- */
145
- key: string;
146
- /** The short name of the thing itself, without any qualifier. */
147
- label: string;
148
- /** What it is, in one line: a description, a prompt, or where it lives. */
149
- detail?: string;
150
- /** The repository it belongs to, when it has one. */
151
- repo?: string;
152
- /** The namespace it is, or the one it lives in. */
153
- namespace?: string;
154
- /** The recipe it is, or the one that declares it. */
155
- recipe?: string;
156
- /** The variable it is, or the one an environment variable name answers. */
157
- variable?: string;
158
- /** The environment variable name, when the reference named one. */
159
- envName?: string;
160
- /** How qualified the spelling that matched was. */
161
- qualification: Qualification;
162
- };
163
-
164
- // --- The search ---------------------------------------------------------------------------------
165
-
166
- /**
167
- * Every meaning a reference could have, in the documented order.
168
- *
169
- * Nothing is fetched and nothing is asked: this reads the context it is handed
170
- * and returns what matched. An empty result means the reference named nothing
171
- * the caller can see; one result is the answer; several are offered to the
172
- * caller by `pickReference`.
173
- *
174
- * @param search - The reference exactly as it was written.
175
- * @param scopes - The kinds of thing this command accepts.
176
- * @param context - The repositories, definitions and environment layers to search.
177
- */
178
- export function findReference(
179
- search: string,
180
- scopes: readonly SousScope[],
181
- context: ReferenceContext
182
- ): ReferenceMatch[] {
183
- const wanted = new Set(scopes);
184
- const term = search.trim();
185
- if (term.length === 0) return [];
186
-
187
- if (looksLikeLocation(term)) return findLocated(term, wanted, context);
188
-
189
- const exact = collectMatches(term, wanted, context, (one, other) => one === other);
190
- if (exact.length > 0) return exact;
191
- return collectMatches(
192
- term,
193
- wanted,
194
- context,
195
- (one, other) => one.toLowerCase() === other.toLowerCase()
196
- );
197
- }
198
-
199
- /** How two spellings are compared in one pass of the search. */
200
- type SameSpelling = (one: string, other: string) => boolean;
201
-
202
- /**
203
- * Every match of a term under one way of comparing spellings.
204
- *
205
- * @param term - The reference, trimmed.
206
- * @param wanted - The kinds of thing the command accepts.
207
- * @param context - What to search.
208
- * @param same - Whether two spellings count as the same.
209
- */
210
- function collectMatches(
211
- term: string,
212
- wanted: Set<SousScope>,
213
- context: ReferenceContext,
214
- same: SameSpelling
215
- ): ReferenceMatch[] {
216
- const matches: ReferenceMatch[] = [];
217
- const repoOrder = new Map((context.repos ?? []).map((repo, position) => [repo.name, position]));
218
-
219
- for (const repo of context.repos ?? []) {
220
- if (wanted.has(SousScope.Repository) && same(term, repo.name)) {
221
- matches.push({
222
- scope: SousScope.Repository,
223
- key: repo.name,
224
- label: repo.name,
225
- repo: repo.name,
226
- qualification: Qualification.Full,
227
- ...(repo.url === undefined ? {} : { detail: repo.url }),
228
- });
229
- }
230
-
231
- if (wanted.has(SousScope.Namespace)) {
232
- for (const namespace of repo.namespaces) {
233
- const key = `${repo.name}:${namespace.name}`;
234
- const qualification = qualificationOf(term, same, [
235
- [key, Qualification.Full],
236
- [namespace.name, Qualification.Bare],
237
- ]);
238
- if (qualification === undefined) continue;
239
- matches.push({
240
- scope: SousScope.Namespace,
241
- key,
242
- label: namespace.name,
243
- repo: repo.name,
244
- namespace: namespace.name,
245
- qualification,
246
- ...(namespace.description === undefined ? {} : { detail: namespace.description }),
247
- });
248
- }
249
- }
250
-
251
- if (wanted.has(SousScope.Recipe)) {
252
- for (const recipe of repo.recipes) {
253
- const key = `${repo.name}:${recipe.namespace}/${recipe.name}`;
254
- const qualification = qualificationOf(term, same, [
255
- [key, Qualification.Full],
256
- [`${recipe.namespace}/${recipe.name}`, Qualification.Partial],
257
- [`${repo.name}:${recipe.name}`, Qualification.Partial],
258
- [recipe.name, Qualification.Bare],
259
- ]);
260
- if (qualification === undefined) continue;
261
- matches.push({
262
- scope: SousScope.Recipe,
263
- key,
264
- label: recipe.name,
265
- repo: repo.name,
266
- namespace: recipe.namespace,
267
- recipe: recipe.name,
268
- qualification,
269
- ...(recipe.description === undefined ? {} : { detail: recipe.description }),
270
- });
271
- }
272
- }
273
- }
274
-
275
- if (wanted.has(SousScope.VariableName) || wanted.has(SousScope.EnvVarName)) {
276
- for (const defined of context.variables ?? []) {
277
- const key = variableReferenceKey(defined);
278
- const { repo, namespace, name: recipe } = defined.recipe;
279
- const variable = defined.definition.name;
280
-
281
- if (wanted.has(SousScope.VariableName)) {
282
- const qualification = qualificationOf(term, same, [
283
- [key, Qualification.Full],
284
- [`${namespace}/${recipe}.${variable}`, Qualification.Partial],
285
- [`${recipe}.${variable}`, Qualification.Partial],
286
- [`${repo}:${variable}`, Qualification.Partial],
287
- [variable, Qualification.Bare],
288
- ]);
289
- if (qualification !== undefined) {
290
- matches.push({
291
- scope: SousScope.VariableName,
292
- key,
293
- label: variable,
294
- repo,
295
- namespace,
296
- recipe,
297
- variable,
298
- qualification,
299
- detail: defined.definition.prompt,
300
- });
301
- continue;
302
- }
303
- }
304
-
305
- const envName = [...environmentNamesFor(defined, context)].find((name) => same(term, name));
306
- if (wanted.has(SousScope.EnvVarName) && envName !== undefined) {
307
- matches.push({
308
- scope: SousScope.EnvVarName,
309
- key,
310
- label: envName,
311
- repo,
312
- namespace,
313
- recipe,
314
- variable,
315
- envName,
316
- qualification: Qualification.EnvName,
317
- detail: defined.definition.prompt,
318
- });
319
- }
320
- }
321
- }
322
-
323
- return sortMatches(matches, repoOrder);
324
- }
325
-
326
- /**
327
- * Every match of a reference written as a location. Each reading of it is
328
- * looked for among the repositories at that location, whatever short name this
329
- * project gave them, and settled through that repository's index: a named
330
- * namespace or recipe exactly first and then ignoring case, a browser path
331
- * through the folder each recipe lives in. A location that does not parse, or
332
- * whose repository this project does not trust, names nothing here.
333
- *
334
- * @param term - The reference, trimmed.
335
- * @param wanted - The kinds of thing the command accepts.
336
- * @param context - What to search.
337
- */
338
- function findLocated(
339
- term: string,
340
- wanted: Set<SousScope>,
341
- context: ReferenceContext
342
- ): ReferenceMatch[] {
343
- let readings: RefReading[];
344
- try {
345
- readings = parseRef(term);
346
- } catch {
347
- return [];
348
- }
349
-
350
- const matches: ReferenceMatch[] = [];
351
- const repoOrder = new Map((context.repos ?? []).map((repo, position) => [repo.name, position]));
352
-
353
- for (const reading of readings) {
354
- for (const repo of context.repos ?? []) {
355
- if (repo.identity === undefined || repo.identity !== reading.location?.identity) continue;
356
-
357
- if (isRepositoryReading(reading)) {
358
- if (wanted.has(SousScope.Repository)) {
359
- matches.push({
360
- scope: SousScope.Repository,
361
- key: repo.name,
362
- label: repo.name,
363
- repo: repo.name,
364
- qualification: Qualification.Full,
365
- ...(repo.url === undefined ? {} : { detail: repo.url }),
366
- });
367
- }
368
- continue;
369
- }
370
-
371
- const recipes: Record<string, { path: string }> = {};
372
- for (const recipe of repo.recipes) {
373
- recipes[`${recipe.namespace}/${recipe.name}`] = { path: recipe.path ?? "" };
374
- }
375
- const settled = settleInIndex(reading, {
376
- namespaces: repo.namespaces.map((namespace) => namespace.name),
377
- recipes,
378
- });
379
-
380
- for (const found of settled) {
381
- if (found.recipe === undefined) {
382
- if (!wanted.has(SousScope.Namespace)) continue;
383
- const namespace = repo.namespaces.find((entry) => entry.name === found.namespace);
384
- matches.push({
385
- scope: SousScope.Namespace,
386
- key: `${repo.name}:${found.namespace}`,
387
- label: found.namespace,
388
- repo: repo.name,
389
- namespace: found.namespace,
390
- qualification: Qualification.Full,
391
- ...(namespace?.description === undefined ? {} : { detail: namespace.description }),
392
- });
393
- continue;
394
- }
395
- if (!wanted.has(SousScope.Recipe)) continue;
396
- const recipe = repo.recipes.find(
397
- (entry) => entry.namespace === found.namespace && entry.name === found.recipe
398
- );
399
- matches.push({
400
- scope: SousScope.Recipe,
401
- key: `${repo.name}:${found.namespace}/${found.recipe}`,
402
- label: found.recipe,
403
- repo: repo.name,
404
- namespace: found.namespace,
405
- recipe: found.recipe,
406
- qualification: Qualification.Full,
407
- ...(recipe?.description === undefined ? {} : { detail: recipe.description }),
408
- });
409
- }
410
- }
411
- }
412
-
413
- return sortMatches(matches, repoOrder);
414
- }
415
-
416
- /**
417
- * The repository a reference names.
418
- *
419
- * @param search - The reference exactly as it was written.
420
- * @param context - What to search.
421
- */
422
- export function findRepository(
423
- search: string,
424
- context: ReferenceContext
425
- ): ReferenceMatch[] {
426
- return findReference(search, [SousScope.Repository], context);
427
- }
428
-
429
- /**
430
- * The namespace a reference names, bare or written as `repository:namespace`.
431
- *
432
- * @param search - The reference exactly as it was written.
433
- * @param context - What to search.
434
- */
435
- export function findNamespace(search: string, context: ReferenceContext): ReferenceMatch[] {
436
- return findReference(search, [SousScope.Namespace], context);
437
- }
438
-
439
- /**
440
- * The recipe a reference names, at any level of qualification.
441
- *
442
- * @param search - The reference exactly as it was written.
443
- * @param context - What to search.
444
- */
445
- export function findRecipe(search: string, context: ReferenceContext): ReferenceMatch[] {
446
- return findReference(search, [SousScope.Recipe], context);
447
- }
448
-
449
- /**
450
- * The variable a reference names, by its own name or by the name of an
451
- * environment variable that answers it.
452
- *
453
- * @param search - The reference exactly as it was written.
454
- * @param context - What to search.
455
- */
456
- export function findVariable(search: string, context: ReferenceContext): ReferenceMatch[] {
457
- return findReference(search, [SousScope.VariableName, SousScope.EnvVarName], context);
458
- }
459
-
460
- // --- Describing what was found ------------------------------------------------------------------
461
-
462
- /**
463
- * Describes one match in the words a person choosing between them needs: the
464
- * fully qualified name, and what it actually means.
465
- *
466
- * @param match - The match to describe.
467
- */
468
- export function describeReference(match: ReferenceMatch): string {
469
- const summary = match.detail === undefined ? "" : `: ${match.detail}`;
470
-
471
- switch (match.scope) {
472
- case SousScope.Repository:
473
- return (
474
- `${match.key} (the repository '${match.repo}'` +
475
- `${match.detail === undefined ? "" : `, at ${match.detail}`})`
476
- );
477
- case SousScope.Namespace:
478
- return (
479
- `${match.key} (the whole namespace '${match.namespace}' in the ` +
480
- `repository '${match.repo}')`
481
- );
482
- case SousScope.Recipe:
483
- return (
484
- `${match.key} (the recipe '${match.recipe}' in the namespace ` +
485
- `'${match.namespace}' of the repository '${match.repo}'${summary})`
486
- );
487
- case SousScope.VariableName:
488
- return (
489
- `${match.key} (the variable '${match.variable}' of the recipe ` +
490
- `'${match.namespace}/${match.recipe}'${summary})`
491
- );
492
- case SousScope.EnvVarName:
493
- return (
494
- `${match.envName} (the environment variable answering '${match.variable}' ` +
495
- `of the recipe '${match.namespace}/${match.recipe}'${summary})`
496
- );
497
- }
498
- }
499
-
500
- /**
501
- * The plain-language name of what a match is, for a sentence that has to say
502
- * what kind of thing was found.
503
- *
504
- * @param match - The match to name.
505
- */
506
- export function referenceKindLabel(match: ReferenceMatch): string {
507
- return SCOPE_LABELS[match.scope];
508
- }
509
-
510
- // --- Building a context -------------------------------------------------------------------------
511
-
512
- /**
513
- * Turns cached repository indexes into the repositories a reference searches,
514
- * in the order they were given.
515
- *
516
- * @param repoOrder - The repository short names, in search order.
517
- * @param indexes - Each repository's cached index, keyed by short name.
518
- */
519
- export function referenceReposFromIndexes(
520
- repoOrder: readonly string[],
521
- indexes: Map<string, IndexFile>,
522
- urls: Record<string, string | undefined> = {}
523
- ): ReferenceRepo[] {
524
- const repos: ReferenceRepo[] = [];
525
-
526
- for (const name of repoOrder) {
527
- const index = indexes.get(name);
528
- if (index === undefined) continue;
529
-
530
- const namespaces: ReferenceNamespace[] = Object.entries(index.namespaces).map(
531
- ([namespace, declared]) => ({
532
- name: namespace,
533
- ...(declared?.description === undefined ? {} : { description: declared.description }),
534
- })
535
- );
536
-
537
- const recipes: ReferenceRecipe[] = [];
538
- for (const [key, recipe] of Object.entries(index.recipes)) {
539
- const { namespace, name: recipeName } = splitRecipeKey(key);
540
- recipes.push({
541
- namespace,
542
- name: recipeName,
543
- path: recipe.path,
544
- ...(recipe.description === undefined ? {} : { description: recipe.description }),
545
- });
546
- }
547
-
548
- const url = urls[name];
549
- const identity = url === undefined ? undefined : identityOfUrl(url);
550
- repos.push({
551
- name,
552
- namespaces,
553
- recipes,
554
- ...(url === undefined ? {} : { url }),
555
- ...(identity === undefined ? {} : { identity }),
556
- });
557
- }
558
-
559
- return repos;
560
- }
561
-
562
- /**
563
- * The canonical identity of a repository at a URL, or undefined when no
564
- * provider recognizes it.
565
- *
566
- * @param url - Where the repository lives.
567
- */
568
- function identityOfUrl(url: string): string | undefined {
569
- try {
570
- return repoIdentity(requireProvider(url).canonicalize(url));
571
- } catch {
572
- return undefined;
573
- }
574
- }
575
-
576
- /**
577
- * The repositories, namespaces and recipes the variable definitions in play
578
- * belong to, derived from the definitions themselves.
579
- *
580
- * This is the context a variables command searches: a namespace that publishes
581
- * no variable this project holds has no questions to ask, so naming it would
582
- * resolve to an empty set of questions rather than to an answer. Deriving the
583
- * context from the definitions also means `sous vars ask --file` resolves
584
- * references exactly as a subscribed project does.
585
- *
586
- * The trusted repositories, when given, lend each repository its identity and
587
- * URL and each recipe its folder, so a reference written as a location (a URL,
588
- * a locator, a URL copied from the browser) settles here exactly as it does in
589
- * every other command.
590
- *
591
- * @param variables - The variable definitions in play.
592
- * @param ladder - The environment layers, for environment variable names.
593
- * @param trusted - The trusted repositories as a reference searches them.
594
- */
595
- export function referenceContextFromVariables(
596
- variables: DefinedVariable[],
597
- ladder?: LadderContext,
598
- trusted: readonly ReferenceRepo[] = []
599
- ): ReferenceContext {
600
- const repos = new Map<string, ReferenceRepo>();
601
-
602
- for (const defined of variables) {
603
- const { repo: repoName, namespace, name: recipe } = defined.recipe;
604
- const known = trusted.find((entry) => entry.name === repoName);
605
-
606
- let repo = repos.get(repoName);
607
- if (repo === undefined) {
608
- repo = {
609
- name: repoName,
610
- namespaces: [],
611
- recipes: [],
612
- ...(known?.url === undefined ? {} : { url: known.url }),
613
- ...(known?.identity === undefined ? {} : { identity: known.identity }),
614
- };
615
- repos.set(repoName, repo);
616
- }
617
-
618
- if (!repo.namespaces.some((entry) => entry.name === namespace)) {
619
- repo.namespaces.push({ name: namespace });
620
- }
621
- if (!repo.recipes.some((entry) => entry.namespace === namespace && entry.name === recipe)) {
622
- const path = known?.recipes.find(
623
- (entry) => entry.namespace === namespace && entry.name === recipe
624
- )?.path;
625
- repo.recipes.push({ namespace, name: recipe, ...(path === undefined ? {} : { path }) });
626
- }
627
- }
628
-
629
- return {
630
- repos: [...repos.values()],
631
- variables,
632
- ...(ladder === undefined ? {} : { ladder }),
633
- };
634
- }
635
-
636
- /**
637
- * The fully qualified name of one variable: `repo:namespace/recipe.variable`.
638
- * It is what a reference to a variable resolves to, and what a command hands
639
- * back to the asking machinery when it has decided which variables to ask.
640
- *
641
- * @param defined - The definition and the recipe that published it.
642
- */
643
- export function variableReferenceKey(defined: DefinedVariable): string {
644
- const { repo, namespace, name } = defined.recipe;
645
- return `${repo}:${namespace}/${name}.${defined.definition.name}`;
646
- }
647
-
648
- // --- Turning a match back into a ref --------------------------------------------------------------
649
-
650
- /**
651
- * Turns a chosen match into a parsed ref, carrying over the version range the
652
- * original ref asked for. The repository qualifier is kept, so what gets
653
- * resolved is exactly the match that was chosen and not another repository's
654
- * recipe of the same name.
655
- *
656
- * @param match - The match that was chosen.
657
- * @param original - The ref as the user wrote it.
658
- */
659
- export function referenceToRef(match: ReferenceMatch, original: ParsedRef): ParsedRef {
660
- return {
661
- ...(match.repo === undefined ? {} : { repo: match.repo }),
662
- namespace: match.namespace ?? original.namespace,
663
- ...(match.recipe === undefined ? {} : { recipe: match.recipe }),
664
- ...(original.range === undefined ? {} : { range: original.range }),
665
- };
666
- }
667
-
668
- // --- The pieces -----------------------------------------------------------------------------------
669
-
670
- /**
671
- * How qualified a spelling of one thing the search term matched, or undefined
672
- * when it matched none of them. Spellings are given most specific first, and
673
- * the first one that matches wins.
674
- *
675
- * @param term - The search term.
676
- * @param same - Whether two spellings count as the same.
677
- * @param spellings - Each spelling of this thing, with how qualified it is.
678
- */
679
- function qualificationOf(
680
- term: string,
681
- same: SameSpelling,
682
- spellings: [string, Qualification][]
683
- ): Qualification | undefined {
684
- for (const [spelling, qualification] of spellings) {
685
- if (same(spelling, term)) return qualification;
686
- }
687
- return undefined;
688
- }
689
-
690
- /**
691
- * Every environment variable name that would be recognized as naming one
692
- * variable: the name its definition declares (a recipe may bind an existing
693
- * variable such as `GITHUB_TOKEN`), plus any generated name on the resolution
694
- * ladder that is actually in use in `.sous/.env` or `.sous/.env.local`.
695
- *
696
- * Generated names are only searched when they are in use, because every
697
- * variable generates four of them and a project would otherwise be told that a
698
- * name nothing has ever set names one of its variables.
699
- *
700
- * @param defined - The definition and the recipe that published it.
701
- * @param context - The environment layers, when the caller has them.
702
- */
703
- function environmentNamesFor(
704
- defined: DefinedVariable,
705
- context: ReferenceContext
706
- ): Set<string> {
707
- const names = new Set<string>([bareName(defined.definition)]);
708
-
709
- const ladder = context.ladder;
710
- if (ladder === undefined) return names;
711
-
712
- const inUse = new Set([...Object.keys(ladder.localEnv), ...Object.keys(ladder.sharedEnv)]);
713
- for (const candidate of variableCandidates(defined, ladder)) {
714
- if (inUse.has(candidate.envName)) names.add(candidate.envName);
715
- }
716
-
717
- return names;
718
- }
719
-
720
- /**
721
- * Sorts matches into the documented listing order and drops anything the same
722
- * search found twice.
723
- *
724
- * @param matches - Every match, in the order they were collected.
725
- * @param repoOrder - Where each repository sits in the search order.
726
- */
727
- function sortMatches(
728
- matches: ReferenceMatch[],
729
- repoOrder: Map<string, number>
730
- ): ReferenceMatch[] {
731
- const position = (repo: string | undefined): number =>
732
- repo === undefined ? 0 : (repoOrder.get(repo) ?? repoOrder.size);
733
-
734
- const sorted = [...matches].sort((left, right) => {
735
- if (left.qualification !== right.qualification) {
736
- return left.qualification - right.qualification;
737
- }
738
- const scopes = scopeRank(left.scope) - scopeRank(right.scope);
739
- if (scopes !== 0) return scopes;
740
-
741
- const repos = position(left.repo) - position(right.repo);
742
- if (repos !== 0) return repos;
743
-
744
- return compare(
745
- [left.namespace, left.recipe, left.variable],
746
- [right.namespace, right.recipe, right.variable]
747
- );
748
- });
749
-
750
- const seen = new Set<string>();
751
- return sorted.filter((match) => {
752
- const identity = `${match.scope}${match.key}${match.envName ?? ""}`;
753
- if (seen.has(identity)) return false;
754
- seen.add(identity);
755
- return true;
756
- });
757
- }
758
-
759
- /** Bytewise comparison of the parts of an identity, so listings are stable. */
760
- function compare(
761
- left: (string | undefined)[],
762
- right: (string | undefined)[]
763
- ): number {
764
- for (let index = 0; index < left.length; index += 1) {
765
- const one = left[index] ?? "";
766
- const other = right[index] ?? "";
767
- if (one !== other) return one < other ? -1 : 1;
768
- }
769
- return 0;
770
- }