@sous-io/sous 0.2.28 → 0.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/markdown/_sidebar.md +1 -0
- package/docs/markdown/repositories-authoring.md +12 -8
- package/docs/markdown/repositories-consuming.md +8 -1
- package/docs/markdown/repositories-file-formats.md +53 -8
- package/docs/markdown/repositories-providers.md +8 -6
- package/docs/markdown/repositories-troubleshooting.md +6 -0
- package/docs/markdown/repositories-variables.md +9 -2
- package/docs/markdown/repositories.md +3 -3
- package/package.json +1 -1
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +11 -7
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/commands/lock/rebuild.ts +2 -2
- package/src/commands/repo/contribute.ts +1 -10
- package/src/commands/repo/gc.ts +3 -2
- package/src/commands/repo/release.ts +20 -3
- package/src/commands/repo/search.ts +2 -1
- package/src/commands/vars/ask.ts +32 -10
- package/src/lib/refs/find.ts +202 -21
- package/src/lib/refs/index.ts +2 -0
- package/src/lib/refs/parse.ts +844 -0
- package/src/lib/refs/scopes.ts +34 -0
- package/src/lib/refs/settle.ts +219 -0
- package/src/lib/repos/catalog.ts +44 -25
- package/src/lib/repos/formats/index-file.ts +2 -1
- package/src/lib/repos/formats/recipe-manifest.ts +10 -8
- package/src/lib/repos/index.ts +0 -1
- package/src/lib/repos/lock-service.ts +3 -2
- package/src/lib/repos/locked-namespace-resolver.ts +21 -15
- package/src/lib/repos/locked-recipes.ts +2 -2
- package/src/lib/repos/namespace-resolver.ts +40 -47
- package/src/lib/repos/providers/base.ts +30 -0
- package/src/lib/repos/providers/github.ts +22 -0
- package/src/lib/repos/providers/gitlab.ts +68 -0
- package/src/lib/repos/providers/provider.ts +96 -0
- package/src/lib/repos/release/index-builder.ts +55 -17
- package/src/lib/repos/release/index.ts +1 -0
- package/src/lib/repos/release/plan.ts +8 -6
- package/src/lib/repos/release/settle.ts +237 -0
- package/src/lib/repos/release/tags.ts +3 -7
- package/src/lib/repos/resolver.ts +198 -41
- package/src/lib/repos/subscription-service.ts +262 -38
- package/src/lib/vars/mappings.ts +17 -30
- package/src/lib/repos/ref.ts +0 -513
package/src/lib/refs/find.ts
CHANGED
|
@@ -19,10 +19,14 @@
|
|
|
19
19
|
* taskFileRoot the same variable, bare
|
|
20
20
|
* SOUS_VAR_TASK_FILE_ROOT the environment variable answering it
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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.
|
|
26
30
|
*
|
|
27
31
|
* The order matches come back in is part of the contract, because it is the
|
|
28
32
|
* order they are offered in and the one `--accept-first` picks from. Matches
|
|
@@ -34,7 +38,17 @@
|
|
|
34
38
|
*/
|
|
35
39
|
|
|
36
40
|
import type { IndexFile } from "../repos/formats/index-file.js";
|
|
37
|
-
import
|
|
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";
|
|
38
52
|
import type { DefinedVariable } from "../vars/definition-source.js";
|
|
39
53
|
import { variableCandidates, type LadderContext } from "../vars/ladder.js";
|
|
40
54
|
import { bareName } from "../vars/names.js";
|
|
@@ -58,6 +72,8 @@ export type ReferenceRecipe = {
|
|
|
58
72
|
name: string;
|
|
59
73
|
/** Its one-paragraph summary, when the index carries one. */
|
|
60
74
|
description?: string;
|
|
75
|
+
/** The folder it lives in, relative to the repository root, when the index says. */
|
|
76
|
+
path?: string;
|
|
61
77
|
};
|
|
62
78
|
|
|
63
79
|
/** One repository a reference could name, and what it publishes. */
|
|
@@ -66,6 +82,11 @@ export type ReferenceRepo = {
|
|
|
66
82
|
name: string;
|
|
67
83
|
/** Where it lives, as the project's config records it. */
|
|
68
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;
|
|
69
90
|
/** Every namespace it publishes. */
|
|
70
91
|
namespaces: ReferenceNamespace[];
|
|
71
92
|
/** Every recipe it publishes. */
|
|
@@ -163,11 +184,40 @@ export function findReference(
|
|
|
163
184
|
const term = search.trim();
|
|
164
185
|
if (term.length === 0) return [];
|
|
165
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[] {
|
|
166
216
|
const matches: ReferenceMatch[] = [];
|
|
167
217
|
const repoOrder = new Map((context.repos ?? []).map((repo, position) => [repo.name, position]));
|
|
168
218
|
|
|
169
219
|
for (const repo of context.repos ?? []) {
|
|
170
|
-
if (wanted.has(SousScope.Repository) && term
|
|
220
|
+
if (wanted.has(SousScope.Repository) && same(term, repo.name)) {
|
|
171
221
|
matches.push({
|
|
172
222
|
scope: SousScope.Repository,
|
|
173
223
|
key: repo.name,
|
|
@@ -181,7 +231,7 @@ export function findReference(
|
|
|
181
231
|
if (wanted.has(SousScope.Namespace)) {
|
|
182
232
|
for (const namespace of repo.namespaces) {
|
|
183
233
|
const key = `${repo.name}:${namespace.name}`;
|
|
184
|
-
const qualification = qualificationOf(term, [
|
|
234
|
+
const qualification = qualificationOf(term, same, [
|
|
185
235
|
[key, Qualification.Full],
|
|
186
236
|
[namespace.name, Qualification.Bare],
|
|
187
237
|
]);
|
|
@@ -201,7 +251,7 @@ export function findReference(
|
|
|
201
251
|
if (wanted.has(SousScope.Recipe)) {
|
|
202
252
|
for (const recipe of repo.recipes) {
|
|
203
253
|
const key = `${repo.name}:${recipe.namespace}/${recipe.name}`;
|
|
204
|
-
const qualification = qualificationOf(term, [
|
|
254
|
+
const qualification = qualificationOf(term, same, [
|
|
205
255
|
[key, Qualification.Full],
|
|
206
256
|
[`${recipe.namespace}/${recipe.name}`, Qualification.Partial],
|
|
207
257
|
[`${repo.name}:${recipe.name}`, Qualification.Partial],
|
|
@@ -229,7 +279,7 @@ export function findReference(
|
|
|
229
279
|
const variable = defined.definition.name;
|
|
230
280
|
|
|
231
281
|
if (wanted.has(SousScope.VariableName)) {
|
|
232
|
-
const qualification = qualificationOf(term, [
|
|
282
|
+
const qualification = qualificationOf(term, same, [
|
|
233
283
|
[key, Qualification.Full],
|
|
234
284
|
[`${namespace}/${recipe}.${variable}`, Qualification.Partial],
|
|
235
285
|
[`${recipe}.${variable}`, Qualification.Partial],
|
|
@@ -252,16 +302,17 @@ export function findReference(
|
|
|
252
302
|
}
|
|
253
303
|
}
|
|
254
304
|
|
|
255
|
-
|
|
305
|
+
const envName = [...environmentNamesFor(defined, context)].find((name) => same(term, name));
|
|
306
|
+
if (wanted.has(SousScope.EnvVarName) && envName !== undefined) {
|
|
256
307
|
matches.push({
|
|
257
308
|
scope: SousScope.EnvVarName,
|
|
258
309
|
key,
|
|
259
|
-
label:
|
|
310
|
+
label: envName,
|
|
260
311
|
repo,
|
|
261
312
|
namespace,
|
|
262
313
|
recipe,
|
|
263
314
|
variable,
|
|
264
|
-
envName
|
|
315
|
+
envName,
|
|
265
316
|
qualification: Qualification.EnvName,
|
|
266
317
|
detail: defined.definition.prompt,
|
|
267
318
|
});
|
|
@@ -272,6 +323,96 @@ export function findReference(
|
|
|
272
323
|
return sortMatches(matches, repoOrder);
|
|
273
324
|
}
|
|
274
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
|
+
|
|
275
416
|
/**
|
|
276
417
|
* The repository a reference names.
|
|
277
418
|
*
|
|
@@ -395,22 +536,43 @@ export function referenceReposFromIndexes(
|
|
|
395
536
|
|
|
396
537
|
const recipes: ReferenceRecipe[] = [];
|
|
397
538
|
for (const [key, recipe] of Object.entries(index.recipes)) {
|
|
398
|
-
const
|
|
399
|
-
if (slash === -1) continue;
|
|
539
|
+
const { namespace, name: recipeName } = splitRecipeKey(key);
|
|
400
540
|
recipes.push({
|
|
401
|
-
namespace
|
|
402
|
-
name:
|
|
541
|
+
namespace,
|
|
542
|
+
name: recipeName,
|
|
543
|
+
path: recipe.path,
|
|
403
544
|
...(recipe.description === undefined ? {} : { description: recipe.description }),
|
|
404
545
|
});
|
|
405
546
|
}
|
|
406
547
|
|
|
407
548
|
const url = urls[name];
|
|
408
|
-
|
|
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
|
+
});
|
|
409
557
|
}
|
|
410
558
|
|
|
411
559
|
return repos;
|
|
412
560
|
}
|
|
413
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
|
+
|
|
414
576
|
/**
|
|
415
577
|
* The repositories, namespaces and recipes the variable definitions in play
|
|
416
578
|
* belong to, derived from the definitions themselves.
|
|
@@ -421,21 +583,35 @@ export function referenceReposFromIndexes(
|
|
|
421
583
|
* context from the definitions also means `sous vars ask --file` resolves
|
|
422
584
|
* references exactly as a subscribed project does.
|
|
423
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
|
+
*
|
|
424
591
|
* @param variables - The variable definitions in play.
|
|
425
592
|
* @param ladder - The environment layers, for environment variable names.
|
|
593
|
+
* @param trusted - The trusted repositories as a reference searches them.
|
|
426
594
|
*/
|
|
427
595
|
export function referenceContextFromVariables(
|
|
428
596
|
variables: DefinedVariable[],
|
|
429
|
-
ladder?: LadderContext
|
|
597
|
+
ladder?: LadderContext,
|
|
598
|
+
trusted: readonly ReferenceRepo[] = []
|
|
430
599
|
): ReferenceContext {
|
|
431
600
|
const repos = new Map<string, ReferenceRepo>();
|
|
432
601
|
|
|
433
602
|
for (const defined of variables) {
|
|
434
603
|
const { repo: repoName, namespace, name: recipe } = defined.recipe;
|
|
604
|
+
const known = trusted.find((entry) => entry.name === repoName);
|
|
435
605
|
|
|
436
606
|
let repo = repos.get(repoName);
|
|
437
607
|
if (repo === undefined) {
|
|
438
|
-
repo = {
|
|
608
|
+
repo = {
|
|
609
|
+
name: repoName,
|
|
610
|
+
namespaces: [],
|
|
611
|
+
recipes: [],
|
|
612
|
+
...(known?.url === undefined ? {} : { url: known.url }),
|
|
613
|
+
...(known?.identity === undefined ? {} : { identity: known.identity }),
|
|
614
|
+
};
|
|
439
615
|
repos.set(repoName, repo);
|
|
440
616
|
}
|
|
441
617
|
|
|
@@ -443,7 +619,10 @@ export function referenceContextFromVariables(
|
|
|
443
619
|
repo.namespaces.push({ name: namespace });
|
|
444
620
|
}
|
|
445
621
|
if (!repo.recipes.some((entry) => entry.namespace === namespace && entry.name === recipe)) {
|
|
446
|
-
|
|
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 }) });
|
|
447
626
|
}
|
|
448
627
|
}
|
|
449
628
|
|
|
@@ -494,14 +673,16 @@ export function referenceToRef(match: ReferenceMatch, original: ParsedRef): Pars
|
|
|
494
673
|
* the first one that matches wins.
|
|
495
674
|
*
|
|
496
675
|
* @param term - The search term.
|
|
676
|
+
* @param same - Whether two spellings count as the same.
|
|
497
677
|
* @param spellings - Each spelling of this thing, with how qualified it is.
|
|
498
678
|
*/
|
|
499
679
|
function qualificationOf(
|
|
500
680
|
term: string,
|
|
681
|
+
same: SameSpelling,
|
|
501
682
|
spellings: [string, Qualification][]
|
|
502
683
|
): Qualification | undefined {
|
|
503
684
|
for (const [spelling, qualification] of spellings) {
|
|
504
|
-
if (spelling
|
|
685
|
+
if (same(spelling, term)) return qualification;
|
|
505
686
|
}
|
|
506
687
|
return undefined;
|
|
507
688
|
}
|