@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
|
@@ -57,19 +57,32 @@ import {
|
|
|
57
57
|
type Lockfile,
|
|
58
58
|
} from "./formats/lockfile.js";
|
|
59
59
|
import type { RecipeManifest } from "./formats/recipe-manifest.js";
|
|
60
|
-
import { formatRef, parseRef, refKey, type ParsedRef } from "./ref.js";
|
|
61
60
|
import {
|
|
61
|
+
RefSource,
|
|
62
|
+
Qualification,
|
|
62
63
|
SousScope,
|
|
64
|
+
formatRef,
|
|
65
|
+
isNamedReading,
|
|
66
|
+
matchNames,
|
|
67
|
+
namespaceOfKey,
|
|
68
|
+
parseRef,
|
|
69
|
+
parseShortRef,
|
|
70
|
+
refKey,
|
|
71
|
+
splitRecipeKey,
|
|
72
|
+
type ParsedRef,
|
|
73
|
+
type RefReading,
|
|
63
74
|
describeReference,
|
|
64
75
|
findReference,
|
|
65
76
|
pickReference,
|
|
66
77
|
referenceReposFromIndexes,
|
|
67
78
|
referenceToRef,
|
|
68
79
|
type ReferenceMatch,
|
|
80
|
+
type ReferenceRepo,
|
|
69
81
|
} from "../refs/index.js";
|
|
70
82
|
import { describeIndexSearch } from "./ref-search.js";
|
|
71
83
|
import {
|
|
72
84
|
PROJECT_REQUESTER,
|
|
85
|
+
proposeRepoName,
|
|
73
86
|
resolveRefs,
|
|
74
87
|
type MissingRepo,
|
|
75
88
|
type RefRequest,
|
|
@@ -639,6 +652,26 @@ export class SubscriptionService {
|
|
|
639
652
|
return identity === undefined ? undefined : this.indexCache.readCached(identity);
|
|
640
653
|
}
|
|
641
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Every trusted repository as a reference searches it, read from the cached
|
|
657
|
+
* indexes alone: what it publishes, where each recipe lives, and its
|
|
658
|
+
* canonical identity, so a reference written as a location finds it whatever
|
|
659
|
+
* this project calls it. A repository whose index has never been fetched can
|
|
660
|
+
* still be named, just not searched inside.
|
|
661
|
+
*/
|
|
662
|
+
cachedReferenceRepos(): ReferenceRepo[] {
|
|
663
|
+
const repos = this.resolverRepos();
|
|
664
|
+
return Object.keys(repos).map((name) => {
|
|
665
|
+
const { url, identity } = repos[name]!;
|
|
666
|
+
const index = this.cachedIndex(name);
|
|
667
|
+
const base =
|
|
668
|
+
index === undefined
|
|
669
|
+
? { name, url, namespaces: [], recipes: [] }
|
|
670
|
+
: referenceReposFromIndexes([name], new Map([[name, index]]), { [name]: url })[0]!;
|
|
671
|
+
return { ...base, identity };
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
642
675
|
// --- Repository identity ----------------------------------------------------------------------
|
|
643
676
|
|
|
644
677
|
/**
|
|
@@ -796,27 +829,30 @@ export class SubscriptionService {
|
|
|
796
829
|
* @param options - The ref, the prerelease and always-pull flags, and the trust flag.
|
|
797
830
|
*/
|
|
798
831
|
async subscribe(options: SubscribeOptions): Promise<SubscribeOutcome> {
|
|
799
|
-
const written = parseRef(options.ref);
|
|
800
832
|
const dryRun = options.dryRun === true;
|
|
801
833
|
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
|
|
834
|
+
// What the ref names is settled here, from the cached indexes alone: a
|
|
835
|
+
// one-word ref is a guess at a name, and a location is matched against the
|
|
836
|
+
// repository at that location (trusted first, when it is not yet). Every
|
|
837
|
+
// step after this works with a fully qualified ref, so what is confirmed
|
|
838
|
+
// is exactly what is installed.
|
|
839
|
+
const { parsed, trusted: trustedForRef } = await this.resolveWrittenRef(options.ref, options);
|
|
806
840
|
const key = refKey(parsed);
|
|
807
841
|
|
|
808
842
|
// The last gate before anything is fetched or written: what this will do to
|
|
809
843
|
// the project, in plain sentences, and a question.
|
|
810
844
|
await this.confirmSubscription(parsed, options);
|
|
811
845
|
|
|
812
|
-
const
|
|
846
|
+
const closure = await this.resolveClosure(parsed, options);
|
|
847
|
+
const { resolved, cycles } = closure;
|
|
848
|
+
const trusted = [...trustedForRef, ...closure.trusted];
|
|
813
849
|
|
|
814
850
|
const before = this.lock.read();
|
|
815
851
|
const after = this.lock.applyResolution(before, resolved, this.lockRepoInputs());
|
|
816
852
|
const diff = this.lock.diff(before, after);
|
|
817
853
|
|
|
818
854
|
const resolvedFrom =
|
|
819
|
-
|
|
855
|
+
options.ref.trim() === formatRef(parsed) ? {} : { resolvedFrom: options.ref.trim() };
|
|
820
856
|
|
|
821
857
|
if (dryRun) {
|
|
822
858
|
// The questions are planned even here, so `--dry-run` is the command an
|
|
@@ -881,6 +917,161 @@ export class SubscriptionService {
|
|
|
881
917
|
|
|
882
918
|
// --- Working out what a one-word ref meant -----------------------------------------------------
|
|
883
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Settles what a ref typed on the command line names, in any form it was
|
|
922
|
+
* written: a short ref goes through `resolveBareRef`; a location (a URL, an
|
|
923
|
+
* SSH remote, a provider-scheme locator, a browser URL) is matched against
|
|
924
|
+
* the trusted repository at that location and settled through its index.
|
|
925
|
+
* When no reading's repository is trusted yet, the trust ceremony runs for
|
|
926
|
+
* it first, exactly as it runs for a dependency's repository, and nothing is
|
|
927
|
+
* fetched from it before the answer.
|
|
928
|
+
*
|
|
929
|
+
* @param written - The ref exactly as the user wrote it.
|
|
930
|
+
* @param options - The trust and accept-first flags.
|
|
931
|
+
* @returns The fully qualified ref, and any repository trusted to settle it.
|
|
932
|
+
*/
|
|
933
|
+
private async resolveWrittenRef(
|
|
934
|
+
written: string,
|
|
935
|
+
options: SubscribeOptions
|
|
936
|
+
): Promise<{ parsed: ParsedRef; trusted: string[] }> {
|
|
937
|
+
const readings = parseRef(written, RefSource.CommandLine, this.providers);
|
|
938
|
+
const first = readings[0]!;
|
|
939
|
+
if (first.location === undefined && isNamedReading(first)) {
|
|
940
|
+
return { parsed: await this.resolveBareRef(first, options), trusted: [] };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const trusted = await this.trustLocation(written, readings, options);
|
|
944
|
+
const chosen = await this.pickLocated(written, options.acceptFirst);
|
|
945
|
+
const range = "range" in first ? first.range : undefined;
|
|
946
|
+
return {
|
|
947
|
+
parsed: referenceToRef(chosen, {
|
|
948
|
+
namespace: chosen.namespace!,
|
|
949
|
+
...(range === undefined ? {} : { range }),
|
|
950
|
+
}),
|
|
951
|
+
trusted,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* The one namespace or recipe a location names among the trusted
|
|
957
|
+
* repositories, settled through their cached indexes and chosen between the
|
|
958
|
+
* way every reference is.
|
|
959
|
+
*
|
|
960
|
+
* @param written - The location as written.
|
|
961
|
+
* @param acceptFirst - Whether `--accept-first` was passed.
|
|
962
|
+
*/
|
|
963
|
+
private async pickLocated(written: string, acceptFirst?: boolean): Promise<ReferenceMatch> {
|
|
964
|
+
const repoOrder = this.repoSearchOrder();
|
|
965
|
+
const indexes = await this.loadIndexes(repoOrder);
|
|
966
|
+
this.indexesSnapshot = indexes;
|
|
967
|
+
|
|
968
|
+
const matches = findReference(
|
|
969
|
+
written,
|
|
970
|
+
[SousScope.Namespace, SousScope.Recipe],
|
|
971
|
+
{ repos: this.referenceRepos(repoOrder, indexes) }
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
return pickReference(matches, {
|
|
975
|
+
search: written.trim(),
|
|
976
|
+
interactive: this.interactive,
|
|
977
|
+
...(acceptFirst === undefined ? {} : { acceptFirst }),
|
|
978
|
+
details: [
|
|
979
|
+
` The repository at that location is trusted, but its index publishes no namespace ` +
|
|
980
|
+
`or recipe there.`,
|
|
981
|
+
` Run 'sous repo list --latest' to read what it publishes now.`,
|
|
982
|
+
],
|
|
983
|
+
write: (message: string) => this.write(message),
|
|
984
|
+
choose: (message, offered) => this.choose(message, offered),
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* The trusted repositories as a reference searches them, each carrying its
|
|
990
|
+
* canonical identity so a location finds it whatever this project calls it.
|
|
991
|
+
*
|
|
992
|
+
* @param repoOrder - The repositories, in search order.
|
|
993
|
+
* @param indexes - Their indexes.
|
|
994
|
+
*/
|
|
995
|
+
private referenceRepos(repoOrder: string[], indexes: Map<string, IndexFile>) {
|
|
996
|
+
const repos = this.resolverRepos();
|
|
997
|
+
const urls: Record<string, string | undefined> = {};
|
|
998
|
+
for (const name of repoOrder) urls[name] = repos[name]?.url;
|
|
999
|
+
return referenceReposFromIndexes(repoOrder, indexes, urls).map((repo) => ({
|
|
1000
|
+
...repo,
|
|
1001
|
+
...(repos[repo.name] === undefined ? {} : { identity: repos[repo.name]!.identity }),
|
|
1002
|
+
}));
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Makes sure the repository a located ref names is trusted: nothing happens
|
|
1007
|
+
* when any reading's repository already is. Otherwise the one repository the
|
|
1008
|
+
* location names goes through the trust ceremony; a location that reads as
|
|
1009
|
+
* several repositories (a GitLab URL with nested groups) is first a question
|
|
1010
|
+
* of which one, since nothing may be fetched from a repository to find out
|
|
1011
|
+
* before it is trusted.
|
|
1012
|
+
*
|
|
1013
|
+
* @param written - The location as written.
|
|
1014
|
+
* @param readings - Every reading of it.
|
|
1015
|
+
* @param options - The trust and accept-first flags.
|
|
1016
|
+
* @returns The short names of the repositories trusted here.
|
|
1017
|
+
*/
|
|
1018
|
+
private async trustLocation(
|
|
1019
|
+
written: string,
|
|
1020
|
+
readings: RefReading[],
|
|
1021
|
+
options: SubscribeOptions
|
|
1022
|
+
): Promise<string[]> {
|
|
1023
|
+
const repos = this.resolverRepos();
|
|
1024
|
+
const known = new Set(Object.values(repos).map((repo) => repo.identity));
|
|
1025
|
+
const located = readings.filter((reading) => reading.location !== undefined);
|
|
1026
|
+
if (located.some((reading) => known.has(reading.location!.identity))) return [];
|
|
1027
|
+
|
|
1028
|
+
let chosen = located[0]!;
|
|
1029
|
+
const identities = new Set(located.map((reading) => reading.location!.identity));
|
|
1030
|
+
if (identities.size > 1) {
|
|
1031
|
+
const offered: ReferenceMatch[] = located.map((reading) => ({
|
|
1032
|
+
scope:
|
|
1033
|
+
isNamedReading(reading) && reading.recipe !== undefined
|
|
1034
|
+
? SousScope.Recipe
|
|
1035
|
+
: SousScope.Namespace,
|
|
1036
|
+
key: formatRef(reading, this.providers),
|
|
1037
|
+
label: formatRef(reading, this.providers),
|
|
1038
|
+
repo: reading.location!.url,
|
|
1039
|
+
...(isNamedReading(reading) ? { namespace: reading.namespace } : {}),
|
|
1040
|
+
...(isNamedReading(reading) && reading.recipe !== undefined
|
|
1041
|
+
? { recipe: reading.recipe }
|
|
1042
|
+
: {}),
|
|
1043
|
+
qualification: Qualification.Full,
|
|
1044
|
+
}));
|
|
1045
|
+
const picked = await pickReference(offered, {
|
|
1046
|
+
search: written.trim(),
|
|
1047
|
+
interactive: this.interactive,
|
|
1048
|
+
...(options.acceptFirst === undefined ? {} : { acceptFirst: options.acceptFirst }),
|
|
1049
|
+
prompt: `'${written.trim()}' can be read as more than one repository. Which one?`,
|
|
1050
|
+
write: (message: string) => this.write(message),
|
|
1051
|
+
choose: (message, list) => this.choose(message, list),
|
|
1052
|
+
});
|
|
1053
|
+
chosen = located[offered.indexOf(picked)]!;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const location = chosen.location!;
|
|
1057
|
+
const outcome = await this.trust.confirmTrust(
|
|
1058
|
+
[
|
|
1059
|
+
{
|
|
1060
|
+
name: proposeRepoName(repos, location.identity),
|
|
1061
|
+
url: location.url,
|
|
1062
|
+
identity: location.identity,
|
|
1063
|
+
provider: location.provider,
|
|
1064
|
+
requiredBy: [{ ref: written.trim(), requestedBy: PROJECT_REQUESTER }],
|
|
1065
|
+
},
|
|
1066
|
+
],
|
|
1067
|
+
{
|
|
1068
|
+
interactive: this.interactive,
|
|
1069
|
+
...(options.trust === undefined ? {} : { trustFlag: options.trust }),
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
return outcome.added;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
884
1075
|
/**
|
|
885
1076
|
* Settles what a ref names, reading nothing but the cached indexes.
|
|
886
1077
|
*
|
|
@@ -898,33 +1089,39 @@ export class SubscriptionService {
|
|
|
898
1089
|
written: ParsedRef,
|
|
899
1090
|
options: SubscribeOptions
|
|
900
1091
|
): Promise<ParsedRef> {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
1092
|
+
// A ref that names a recipe, spelled as it is published, says what it
|
|
1093
|
+
// names already. One word, or a spelling in another case, is searched.
|
|
1094
|
+
const search =
|
|
1095
|
+
written.repo === undefined ? refKey(written) : `${written.repo}:${refKey(written)}`;
|
|
1096
|
+
if (written.recipe !== undefined && refKey(written) === refKey(written).toLowerCase()) {
|
|
1097
|
+
return written;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const qualifier =
|
|
1101
|
+
written.repo === undefined
|
|
1102
|
+
? undefined
|
|
1103
|
+
: (matchNames(written.repo, Object.keys(this.currentRepos()))[0] ?? written.repo);
|
|
1104
|
+
const repoOrder = this.repoSearchOrder(qualifier);
|
|
904
1105
|
const indexes = await this.loadIndexes(repoOrder);
|
|
905
1106
|
if (written.repo === undefined) this.indexesSnapshot = indexes;
|
|
906
1107
|
|
|
907
1108
|
const context = { repos: referenceReposFromIndexes(repoOrder, indexes) };
|
|
908
|
-
const matches = findReference(
|
|
909
|
-
written.namespace,
|
|
910
|
-
[SousScope.Namespace, SousScope.Recipe],
|
|
911
|
-
context
|
|
912
|
-
);
|
|
1109
|
+
const matches = findReference(search, [SousScope.Namespace, SousScope.Recipe], context);
|
|
913
1110
|
|
|
914
1111
|
if (matches.length === 0) {
|
|
915
1112
|
throw new ConfigError(
|
|
916
1113
|
[
|
|
917
|
-
`Nothing called '${written
|
|
1114
|
+
`Nothing called '${refKey(written)}' was found: no namespace has that name, ` +
|
|
918
1115
|
`and no recipe does either.`,
|
|
919
|
-
...describeIndexSearch({ name: written
|
|
920
|
-
` Run 'sous repo search ${written
|
|
1116
|
+
...describeIndexSearch({ name: refKey(written), repoOrder, indexes }),
|
|
1117
|
+
` Run 'sous repo search ${refKey(written)}' to look for something like it, or ` +
|
|
921
1118
|
`'sous repo add <url>' to add the repository that publishes it.`,
|
|
922
1119
|
].join("\n")
|
|
923
1120
|
);
|
|
924
1121
|
}
|
|
925
1122
|
|
|
926
1123
|
const chosen = await pickReference(matches, {
|
|
927
|
-
search
|
|
1124
|
+
search,
|
|
928
1125
|
interactive: this.interactive,
|
|
929
1126
|
...(options.acceptFirst === undefined ? {} : { acceptFirst: options.acceptFirst }),
|
|
930
1127
|
write: (message: string) => this.write(message),
|
|
@@ -1272,6 +1469,26 @@ export class SubscriptionService {
|
|
|
1272
1469
|
}
|
|
1273
1470
|
}
|
|
1274
1471
|
|
|
1472
|
+
/**
|
|
1473
|
+
* The subscription key a ref typed on the command line names. A short ref
|
|
1474
|
+
* names its own key, matched against the keys in force exactly first and then
|
|
1475
|
+
* ignoring case; a location is settled through the trusted repository at that
|
|
1476
|
+
* location, the way `subscribe` settles one.
|
|
1477
|
+
*
|
|
1478
|
+
* @param written - The ref as typed.
|
|
1479
|
+
* @param keys - The subscription keys in force.
|
|
1480
|
+
*/
|
|
1481
|
+
private async subscriptionKeyFor(written: string, keys: string[]): Promise<string> {
|
|
1482
|
+
const readings = parseRef(written, RefSource.CommandLine, this.providers);
|
|
1483
|
+
const first = readings[0]!;
|
|
1484
|
+
if (first.location === undefined && isNamedReading(first)) {
|
|
1485
|
+
const key = refKey(first);
|
|
1486
|
+
return matchNames(key, new Set(keys))[0] ?? key;
|
|
1487
|
+
}
|
|
1488
|
+
const chosen = await this.pickLocated(written);
|
|
1489
|
+
return refKey(referenceToRef(chosen, { namespace: chosen.namespace! }));
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1275
1492
|
/**
|
|
1276
1493
|
* Removes one subscription and everything that was only there because of it.
|
|
1277
1494
|
* Removal is refcounted: a recipe another subscription (or another recipe)
|
|
@@ -1280,12 +1497,14 @@ export class SubscriptionService {
|
|
|
1280
1497
|
* @param options - The ref to unsubscribe from.
|
|
1281
1498
|
*/
|
|
1282
1499
|
async unsubscribe(options: UnsubscribeOptions): Promise<UnsubscribeOutcome> {
|
|
1283
|
-
const parsed = parseRef(options.ref);
|
|
1284
|
-
const key = refKey(parsed);
|
|
1285
1500
|
const dryRun = options.dryRun === true;
|
|
1286
1501
|
|
|
1287
1502
|
const managed = this.readSubscriptionEntries();
|
|
1288
1503
|
const configured = enabledSubscriptions(this.settings);
|
|
1504
|
+
const key = await this.subscriptionKeyFor(options.ref, [
|
|
1505
|
+
...Object.keys(managed),
|
|
1506
|
+
...Object.keys(configured),
|
|
1507
|
+
]);
|
|
1289
1508
|
const before = this.lock.read();
|
|
1290
1509
|
const held = this.keysHeldBySubscription(before, key);
|
|
1291
1510
|
|
|
@@ -1521,7 +1740,7 @@ export class SubscriptionService {
|
|
|
1521
1740
|
for (const key of Object.keys(subscriptions).sort()) {
|
|
1522
1741
|
let qualifier: string | undefined;
|
|
1523
1742
|
try {
|
|
1524
|
-
qualifier =
|
|
1743
|
+
qualifier = parseShortRef(key, RefSource.Config).repo;
|
|
1525
1744
|
} catch {
|
|
1526
1745
|
// A ref that does not parse cannot name this repository, and reporting
|
|
1527
1746
|
// it here would bury the removal under an unrelated complaint.
|
|
@@ -1974,7 +2193,7 @@ export class SubscriptionService {
|
|
|
1974
2193
|
const entry = subscriptions[key]!;
|
|
1975
2194
|
let request: RefRequest;
|
|
1976
2195
|
try {
|
|
1977
|
-
const parsed =
|
|
2196
|
+
const parsed = parseShortRef(key, RefSource.Config);
|
|
1978
2197
|
request = {
|
|
1979
2198
|
ref: { ...parsed, ...(entry.range === undefined ? {} : { range: entry.range }) },
|
|
1980
2199
|
requestedBy: PROJECT_REQUESTER,
|
|
@@ -2082,7 +2301,7 @@ export class SubscriptionService {
|
|
|
2082
2301
|
|
|
2083
2302
|
let parsed: ParsedRef;
|
|
2084
2303
|
try {
|
|
2085
|
-
parsed =
|
|
2304
|
+
parsed = parseShortRef(key, RefSource.Config);
|
|
2086
2305
|
} catch {
|
|
2087
2306
|
return false;
|
|
2088
2307
|
}
|
|
@@ -2127,7 +2346,7 @@ export class SubscriptionService {
|
|
|
2127
2346
|
): Lockfile {
|
|
2128
2347
|
const coverOf = (key: string): string | undefined => {
|
|
2129
2348
|
try {
|
|
2130
|
-
return refKey(
|
|
2349
|
+
return refKey(parseShortRef(key, RefSource.Config));
|
|
2131
2350
|
} catch {
|
|
2132
2351
|
return undefined;
|
|
2133
2352
|
}
|
|
@@ -2217,7 +2436,7 @@ export class SubscriptionService {
|
|
|
2217
2436
|
if (scope.kind === "all") return true;
|
|
2218
2437
|
let parsed: ParsedRef;
|
|
2219
2438
|
try {
|
|
2220
|
-
parsed =
|
|
2439
|
+
parsed = parseShortRef(key, RefSource.Config);
|
|
2221
2440
|
} catch {
|
|
2222
2441
|
return false;
|
|
2223
2442
|
}
|
|
@@ -2311,7 +2530,7 @@ export class SubscriptionService {
|
|
|
2311
2530
|
if (entry.repo !== repoName) continue;
|
|
2312
2531
|
const range = this.effectiveRangeFor(key, entry, subscriptions);
|
|
2313
2532
|
if (range === undefined) continue;
|
|
2314
|
-
const subscription = subscriptions[key] ?? subscriptions[key
|
|
2533
|
+
const subscription = subscriptions[key] ?? subscriptions[namespaceOfKey(key)];
|
|
2315
2534
|
|
|
2316
2535
|
const found = findNewerInRange({
|
|
2317
2536
|
index,
|
|
@@ -2403,7 +2622,7 @@ export class SubscriptionService {
|
|
|
2403
2622
|
|
|
2404
2623
|
let parsed: ParsedRef;
|
|
2405
2624
|
try {
|
|
2406
|
-
parsed =
|
|
2625
|
+
parsed = parseShortRef(key, RefSource.Config);
|
|
2407
2626
|
} catch (error) {
|
|
2408
2627
|
report.failed.push({ key, reason: describeError(error) });
|
|
2409
2628
|
continue;
|
|
@@ -2670,7 +2889,7 @@ export class SubscriptionService {
|
|
|
2670
2889
|
|
|
2671
2890
|
for (const [key, entry] of Object.entries(recipes)) {
|
|
2672
2891
|
if (entry.repo !== repoName) continue;
|
|
2673
|
-
const subscription = subscriptions[key] ?? subscriptions[key
|
|
2892
|
+
const subscription = subscriptions[key] ?? subscriptions[namespaceOfKey(key)];
|
|
2674
2893
|
|
|
2675
2894
|
// Always-pull re-resolves WITHIN what was declared; it never widens it.
|
|
2676
2895
|
// A recipe held only through another recipe's `depends` has no
|
|
@@ -3062,11 +3281,11 @@ export class SubscriptionService {
|
|
|
3062
3281
|
const provider = requireProvider(request.url, request.providerId, this.providers);
|
|
3063
3282
|
const canonical = provider.canonicalize(request.url);
|
|
3064
3283
|
|
|
3065
|
-
const namespace =
|
|
3284
|
+
const { namespace, name } = splitRecipeKey(request.key);
|
|
3066
3285
|
const key: StoreKey = {
|
|
3067
3286
|
identity: request.identity,
|
|
3068
3287
|
namespace,
|
|
3069
|
-
name
|
|
3288
|
+
name,
|
|
3070
3289
|
version: request.version,
|
|
3071
3290
|
};
|
|
3072
3291
|
|
|
@@ -3231,7 +3450,7 @@ export class SubscriptionService {
|
|
|
3231
3450
|
entry: LockedRecipe,
|
|
3232
3451
|
subscriptions: Record<string, SubscriptionEntry>
|
|
3233
3452
|
): string | undefined {
|
|
3234
|
-
const namespace = key
|
|
3453
|
+
const namespace = namespaceOfKey(key);
|
|
3235
3454
|
return effectiveRangeForHolders(key, entry.requestedBy, {
|
|
3236
3455
|
subscriptionRange: (held) => {
|
|
3237
3456
|
// A subscription the config no longer declares is not a hold sous can
|
|
@@ -3270,15 +3489,20 @@ export class SubscriptionService {
|
|
|
3270
3489
|
if (manifest === undefined) return undefined;
|
|
3271
3490
|
|
|
3272
3491
|
for (const dependency of manifest.depends ?? []) {
|
|
3273
|
-
|
|
3492
|
+
// Every reading counts: a dependency that reads more than one way still
|
|
3493
|
+
// declares its range for whichever reading the lockfile settled on.
|
|
3494
|
+
let readings: RefReading[];
|
|
3274
3495
|
try {
|
|
3275
|
-
|
|
3496
|
+
readings = parseRef(dependency, RefSource.Manifest, this.providers);
|
|
3276
3497
|
} catch {
|
|
3277
3498
|
continue;
|
|
3278
3499
|
}
|
|
3279
|
-
const
|
|
3280
|
-
|
|
3281
|
-
|
|
3500
|
+
for (const reading of readings) {
|
|
3501
|
+
if (!isNamedReading(reading)) continue;
|
|
3502
|
+
const dependencyKey = refKey(reading);
|
|
3503
|
+
if (dependencyKey !== key && dependencyKey !== namespace) continue;
|
|
3504
|
+
return reading.range ?? "*";
|
|
3505
|
+
}
|
|
3282
3506
|
}
|
|
3283
3507
|
|
|
3284
3508
|
return undefined;
|
package/src/lib/vars/mappings.ts
CHANGED
|
@@ -26,12 +26,9 @@ import {
|
|
|
26
26
|
managedLayerHeader,
|
|
27
27
|
updateManagedLayer,
|
|
28
28
|
} from "../repos/managed-layer.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
REPO_NAME_PATTERN,
|
|
33
|
-
VARIABLE_NAME_PATTERN,
|
|
34
|
-
} from "../repos/formats/patterns.js";
|
|
29
|
+
import { VARIABLE_NAME_PATTERN } from "../repos/formats/patterns.js";
|
|
30
|
+
import { RefSource } from "../refs/scopes.js";
|
|
31
|
+
import { formatRef, parseShortRef, type ParsedRef } from "../refs/parse.js";
|
|
35
32
|
import type { DefinedVariable } from "./definition-source.js";
|
|
36
33
|
|
|
37
34
|
/** File name of the machine-written mapping record layer. */
|
|
@@ -84,32 +81,22 @@ export function parseMappingTarget(input: string): MappingTarget {
|
|
|
84
81
|
const trimmed = typeof input === "string" ? input.trim() : "";
|
|
85
82
|
if (trimmed.length === 0) fail("a target must not be empty.");
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`the repository qualifier '${repo}' must be lowercase kebab-case: a letter, ` +
|
|
97
|
-
"then letters, digits or hyphens."
|
|
98
|
-
);
|
|
99
|
-
}
|
|
84
|
+
// A target is a recipe ref with the variable after one more slash, so the
|
|
85
|
+
// ref part is read by the one ref parser and held to the stored form.
|
|
86
|
+
const slash = trimmed.lastIndexOf("/");
|
|
87
|
+
const variable = slash === -1 ? "" : trimmed.slice(slash + 1);
|
|
88
|
+
let ref: ParsedRef = { namespace: "" };
|
|
89
|
+
try {
|
|
90
|
+
ref = parseShortRef(trimmed.slice(0, Math.max(slash, 0)), RefSource.CommandLine);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
fail((error as Error).message.split("\n")[0]!.replace(/^Invalid ref '[^']*': /, ""));
|
|
100
93
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (segments.length !== 3) {
|
|
94
|
+
const { repo, namespace, recipe } = ref;
|
|
95
|
+
if (recipe === undefined || ref.range !== undefined) {
|
|
104
96
|
fail("a target names a namespace, a recipe and a variable, joined by slashes.");
|
|
105
97
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (!NAMESPACE_NAME_PATTERN.test(namespace)) {
|
|
109
|
-
fail(`the namespace '${namespace}' must be lowercase kebab-case.`);
|
|
110
|
-
}
|
|
111
|
-
if (!RECIPE_NAME_PATTERN.test(recipe)) {
|
|
112
|
-
fail(`the recipe name '${recipe}' must be lowercase kebab-case.`);
|
|
98
|
+
if (formatRef(ref) !== formatRef(ref).toLowerCase()) {
|
|
99
|
+
fail(`the repository, namespace and recipe in '${formatRef(ref)}' must be lowercase.`);
|
|
113
100
|
}
|
|
114
101
|
if (!VARIABLE_NAME_PATTERN.test(variable)) {
|
|
115
102
|
fail(
|
|
@@ -118,7 +105,7 @@ export function parseMappingTarget(input: string): MappingTarget {
|
|
|
118
105
|
);
|
|
119
106
|
}
|
|
120
107
|
|
|
121
|
-
const parsed: MappingTarget = { namespace, recipe
|
|
108
|
+
const parsed: MappingTarget = { namespace, recipe: recipe!, variable };
|
|
122
109
|
if (repo !== undefined) parsed.repo = repo;
|
|
123
110
|
return parsed;
|
|
124
111
|
}
|