@sous-io/sous 0.2.32 → 0.2.33

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 (103) 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.md +8 -4
  10. package/package.json +2 -1
  11. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +25 -17
  12. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
  13. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +21 -9
  14. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  15. package/sous.config.schema.json +27 -15
  16. package/src/commands/lock/rebuild.ts +3 -6
  17. package/src/commands/namespace/show.ts +2 -2
  18. package/src/commands/recipe/show.ts +26 -9
  19. package/src/commands/repo/contribute.ts +32 -23
  20. package/src/commands/repo/gc.ts +1 -1
  21. package/src/commands/repo/remove.ts +1 -1
  22. package/src/commands/repo/search.ts +1 -1
  23. package/src/commands/vars/ask.ts +75 -54
  24. package/src/lib/build-service.ts +85 -11
  25. package/src/lib/config-schema.ts +59 -19
  26. package/src/lib/include-resolver.ts +307 -69
  27. package/src/lib/markdown-compiler.ts +273 -81
  28. package/src/lib/project-scaffold/templates.ts +19 -11
  29. package/src/lib/repos/catalog-inputs.ts +1 -1
  30. package/src/lib/repos/catalog.ts +98 -72
  31. package/src/lib/repos/declarations.ts +22 -12
  32. package/src/lib/repos/formats/index-file.ts +1 -1
  33. package/src/lib/repos/formats/recipe-manifest.ts +3 -4
  34. package/src/lib/repos/lock-service.ts +1 -1
  35. package/src/lib/repos/locked-namespace-resolver.ts +14 -6
  36. package/src/lib/repos/locked-recipe-lookup.ts +123 -0
  37. package/src/lib/repos/locked-recipes.ts +1 -1
  38. package/src/lib/repos/memories-hash-name.ts +33 -0
  39. package/src/lib/repos/namespace-resolver.ts +198 -119
  40. package/src/lib/repos/recipe-config-layers.ts +29 -1
  41. package/src/lib/repos/recipe-key-matcher.ts +62 -0
  42. package/src/lib/repos/recipe-memories.ts +175 -0
  43. package/src/lib/repos/recipe-targets.ts +27 -28
  44. package/src/lib/repos/ref-search.ts +1 -1
  45. package/src/lib/repos/release/index-builder.ts +23 -15
  46. package/src/lib/repos/release/plan.ts +14 -7
  47. package/src/lib/repos/release/settle.ts +38 -121
  48. package/src/lib/repos/release/submit-checkout.ts +42 -23
  49. package/src/lib/repos/release/tags.ts +1 -1
  50. package/src/lib/repos/resolver.ts +90 -72
  51. package/src/lib/repos/subscription-service.ts +192 -170
  52. package/src/lib/settings.ts +37 -33
  53. package/src/lib/vars/ask.ts +3 -3
  54. package/src/lib/vars/mappings.ts +17 -9
  55. package/src/services/ref-resolver/container.ts +107 -0
  56. package/src/services/ref-resolver/dependency.ts +60 -0
  57. package/src/services/ref-resolver/format.ts +197 -0
  58. package/src/services/ref-resolver/glob.ts +115 -0
  59. package/src/services/ref-resolver/hash-names.ts +155 -0
  60. package/src/services/ref-resolver/index.ts +55 -0
  61. package/src/services/ref-resolver/injectable.ts +36 -0
  62. package/src/services/ref-resolver/location.ts +62 -0
  63. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
  64. package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
  65. package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
  66. package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
  67. package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
  68. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
  69. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
  70. package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
  71. package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
  72. package/src/services/ref-resolver/named-refs.ts +144 -0
  73. package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
  74. package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
  75. package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
  76. package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
  77. package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
  78. package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
  79. package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
  80. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
  81. package/src/services/ref-resolver/parts.ts +115 -0
  82. package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
  83. package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
  84. package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
  85. package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
  86. package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
  87. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
  88. package/src/services/ref-resolver/ref-helpers.ts +62 -0
  89. package/src/services/ref-resolver/ref-picker-service.ts +204 -0
  90. package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
  91. package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
  92. package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
  93. package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
  94. package/src/services/ref-resolver/source.ts +48 -0
  95. package/src/services/ref-resolver/tokens.ts +25 -0
  96. package/src/services/ref-resolver/types.ts +155 -0
  97. package/src/templating/init-liquid-engine.ts +25 -18
  98. package/src/lib/refs/find.ts +0 -770
  99. package/src/lib/refs/index.ts +0 -14
  100. package/src/lib/refs/parse.ts +0 -844
  101. package/src/lib/refs/pick.ts +0 -147
  102. package/src/lib/refs/scopes.ts +0 -95
  103. package/src/lib/refs/settle.ts +0 -219
@@ -58,27 +58,31 @@ import {
58
58
  } from "./formats/lockfile.js";
59
59
  import type { RecipeManifest, VariableDefinition } from "./formats/recipe-manifest.js";
60
60
  import {
61
+ CachedIndexLookup,
62
+ RefPickArguments,
63
+ RefResolveArguments,
61
64
  RefSource,
62
- Qualification,
63
- SousScope,
65
+ catalogRepoOfIndex,
66
+ describeRef,
64
67
  formatRef,
65
- isNamedReading,
68
+ isNamedRef,
69
+ locationFromUrl,
70
+ locationOf,
66
71
  matchNames,
72
+ namespaceOf,
67
73
  namespaceOfKey,
68
- parseRef,
69
- parseShortRef,
70
- refKey,
74
+ parseNamedRef,
75
+ rangeOf,
76
+ repoOf,
77
+ sharedRefPicker,
78
+ sharedRefResolver,
79
+ shortKey,
71
80
  splitRecipeKey,
72
- type ParsedRef,
73
- type RefReading,
74
- describeReference,
75
- findReference,
76
- pickReference,
77
- referenceReposFromIndexes,
78
- referenceToRef,
79
- type ReferenceMatch,
80
- type ReferenceRepo,
81
- } from "../refs/index.js";
81
+ type CatalogRepo,
82
+ type NamedRef,
83
+ type RefKind,
84
+ type SousRef,
85
+ } from "../../services/ref-resolver/index.js";
82
86
  import { describeIndexSearch } from "./ref-search.js";
83
87
  import {
84
88
  PROJECT_REQUESTER,
@@ -206,10 +210,7 @@ export type SubscriptionServiceOptions = {
206
210
  /** How a yes or no question is asked. Injected in tests. */
207
211
  ask?: (message: string) => Promise<boolean>;
208
212
  /** How a choice between candidate refs is asked. Injected in tests. */
209
- choose?: (
210
- message: string,
211
- candidates: ReferenceMatch[]
212
- ) => Promise<ReferenceMatch>;
213
+ choose?: (message: string, candidates: SousRef[]) => Promise<SousRef>;
213
214
  /** The clock, so a recorded timestamp is predictable in tests. */
214
215
  now?: () => Date;
215
216
  };
@@ -361,7 +362,7 @@ export type RemoveRepoOptions = {
361
362
  dryRun?: boolean;
362
363
  /**
363
364
  * The resolved settings scope, used to work out which output files the
364
- * removed recipes wrote. Without it a `recipeOutputs` destination holding a
365
+ * removed recipes wrote. Without it a `recipes.skills.outputs` destination holding a
365
366
  * `${var}` cannot be resolved, and the file list is left out of the report.
366
367
  */
367
368
  scope?: VarScope;
@@ -457,7 +458,7 @@ export type UpstreamCheckReport = {
457
458
  export type UpdateOptions = {
458
459
  /**
459
460
  * What to update: a repository, a namespace or a recipe, written at any level
460
- * of qualification and resolved through `src/lib/refs/`. Left out, every
461
+ * of qualification and resolved through `src/services/ref-resolver/`. Left out, every
461
462
  * subscription is updated.
462
463
  */
463
464
  ref?: string;
@@ -542,10 +543,7 @@ export class SubscriptionService {
542
543
 
543
544
  private readonly ask: (message: string) => Promise<boolean>;
544
545
 
545
- private readonly choose: (
546
- message: string,
547
- candidates: ReferenceMatch[]
548
- ) => Promise<ReferenceMatch>;
546
+ private readonly choose: (message: string, candidates: SousRef[]) => Promise<SousRef>;
549
547
 
550
548
  private readonly now: () => Date;
551
549
 
@@ -606,7 +604,7 @@ export class SubscriptionService {
606
604
  askChoice(
607
605
  message,
608
606
  candidates.map((candidate) => ({
609
- name: describeReference(candidate),
607
+ name: describeRef(candidate),
610
608
  value: candidate,
611
609
  }))
612
610
  ));
@@ -681,16 +679,16 @@ export class SubscriptionService {
681
679
  * this project calls it. A repository whose index has never been fetched can
682
680
  * still be named, just not searched inside.
683
681
  */
684
- cachedReferenceRepos(): ReferenceRepo[] {
682
+ cachedReferenceRepos(): CatalogRepo[] {
685
683
  const repos = this.resolverRepos();
686
684
  return Object.keys(repos).map((name) => {
687
- const { url, identity } = repos[name]!;
685
+ const { url } = repos[name]!;
688
686
  const index = this.cachedIndex(name);
689
- const base =
690
- index === undefined
691
- ? { name, url, namespaces: [], recipes: [] }
692
- : referenceReposFromIndexes([name], new Map([[name, index]]), { [name]: url })[0]!;
693
- return { ...base, identity };
687
+ const location = locationFromUrl(url, this.providers);
688
+ const known = { name, ...(location === undefined ? {} : { location }) };
689
+ return index === undefined
690
+ ? { ...known, namespaces: [], recipes: [] }
691
+ : catalogRepoOfIndex(index, known);
694
692
  });
695
693
  }
696
694
 
@@ -859,7 +857,7 @@ export class SubscriptionService {
859
857
  // step after this works with a fully qualified ref, so what is confirmed
860
858
  // is exactly what is installed.
861
859
  const { parsed, trusted: trustedForRef } = await this.resolveWrittenRef(options.ref, options);
862
- const key = refKey(parsed);
860
+ const key = shortKey(parsed);
863
861
 
864
862
  // The last gate before anything is fetched or written: what this will do to
865
863
  // the project, in plain sentences, and a question.
@@ -955,23 +953,18 @@ export class SubscriptionService {
955
953
  private async resolveWrittenRef(
956
954
  written: string,
957
955
  options: SubscribeOptions
958
- ): Promise<{ parsed: ParsedRef; trusted: string[] }> {
959
- const readings = parseRef(written, RefSource.CommandLine, this.providers);
960
- const first = readings[0]!;
961
- if (first.location === undefined && isNamedReading(first)) {
962
- return { parsed: await this.resolveBareRef(first, options), trusted: [] };
956
+ ): Promise<{ parsed: NamedRef; trusted: string[] }> {
957
+ const { refs: readings } = sharedRefResolver().parse(written);
958
+ const short = readings.find(
959
+ (reading): reading is NamedRef => isNamedRef(reading) && locationOf(reading) === undefined
960
+ );
961
+ if (short !== undefined) {
962
+ return { parsed: await this.resolveBareRef(short, options), trusted: [] };
963
963
  }
964
964
 
965
965
  const trusted = await this.trustLocation(written, readings, options);
966
966
  const chosen = await this.pickLocated(written, options.acceptFirst);
967
- const range = "range" in first ? first.range : undefined;
968
- return {
969
- parsed: referenceToRef(chosen, {
970
- namespace: chosen.namespace!,
971
- ...(range === undefined ? {} : { range }),
972
- }),
973
- trusted,
974
- };
967
+ return { parsed: chosen, trusted };
975
968
  }
976
969
 
977
970
  /**
@@ -982,46 +975,88 @@ export class SubscriptionService {
982
975
  * @param written - The location as written.
983
976
  * @param acceptFirst - Whether `--accept-first` was passed.
984
977
  */
985
- private async pickLocated(written: string, acceptFirst?: boolean): Promise<ReferenceMatch> {
978
+ private async pickLocated(written: string, acceptFirst?: boolean): Promise<NamedRef> {
986
979
  const repoOrder = this.repoSearchOrder();
987
980
  const indexes = await this.loadIndexes(repoOrder);
988
981
  this.indexesSnapshot = indexes;
989
982
 
990
- const matches = findReference(
991
- written,
992
- [SousScope.Namespace, SousScope.Recipe],
993
- { repos: this.referenceRepos(repoOrder, indexes) }
994
- );
983
+ const matches = await this.findNamed(written, repoOrder, indexes);
995
984
 
996
- return pickReference(matches, {
985
+ return this.pickNamed(matches, {
997
986
  search: written.trim(),
998
- interactive: this.interactive,
999
987
  ...(acceptFirst === undefined ? {} : { acceptFirst }),
1000
988
  details: [
1001
989
  ` The repository at that location is trusted, but its index publishes no namespace ` +
1002
990
  `or recipe there.`,
1003
991
  ` Run 'sous repo list --latest' to read what it publishes now.`,
1004
992
  ],
1005
- write: (message: string) => this.write(message),
1006
- choose: (message, offered) => this.choose(message, offered),
1007
993
  });
1008
994
  }
1009
995
 
1010
996
  /**
1011
- * The trusted repositories as a reference searches them, each carrying its
1012
- * canonical identity so a location finds it whatever this project calls it.
997
+ * What a ref names among the trusted repositories' cached indexes, of the
998
+ * kinds given. Each repository carries the location it lives at, so a ref
999
+ * written as a location finds it whatever this project calls it.
1013
1000
  *
1001
+ * @param input - The ref exactly as it was written.
1014
1002
  * @param repoOrder - The repositories, in search order.
1015
1003
  * @param indexes - Their indexes.
1004
+ * @param kinds - The kinds of ref this step accepts.
1016
1005
  */
1017
- private referenceRepos(repoOrder: string[], indexes: Map<string, IndexFile>) {
1006
+ private async findRefs(
1007
+ input: string,
1008
+ repoOrder: string[],
1009
+ indexes: Map<string, IndexFile>,
1010
+ kinds: readonly RefKind[]
1011
+ ): Promise<SousRef[]> {
1018
1012
  const repos = this.resolverRepos();
1019
1013
  const urls: Record<string, string | undefined> = {};
1020
1014
  for (const name of repoOrder) urls[name] = repos[name]?.url;
1021
- return referenceReposFromIndexes(repoOrder, indexes, urls).map((repo) => ({
1022
- ...repo,
1023
- ...(repos[repo.name] === undefined ? {} : { identity: repos[repo.name]!.identity }),
1024
- }));
1015
+ const lookup = new CachedIndexLookup(indexes, { order: repoOrder, urls });
1016
+ const result = await sharedRefResolver().resolve(
1017
+ new RefResolveArguments({ input, lookup, kinds, refusedIsEmpty: true })
1018
+ );
1019
+ return result.refs;
1020
+ }
1021
+
1022
+ /** What a ref names among the trusted repositories, as a namespace or a recipe. */
1023
+ private async findNamed(
1024
+ input: string,
1025
+ repoOrder: string[],
1026
+ indexes: Map<string, IndexFile>
1027
+ ): Promise<NamedRef[]> {
1028
+ return (await this.findRefs(input, repoOrder, indexes, ["namespace", "recipe"])) as NamedRef[];
1029
+ }
1030
+
1031
+ /**
1032
+ * Settles on one of several refs, the way every command does: one proceeds,
1033
+ * several are chosen between through this service's own prompt, and
1034
+ * `--accept-first` takes the first.
1035
+ *
1036
+ * @param matches - What the ref could have meant, in listing order.
1037
+ * @param args - What the shared picker is told, less this service's own seams.
1038
+ */
1039
+ private async pickRef(
1040
+ matches: SousRef[],
1041
+ args: Omit<ConstructorParameters<typeof RefPickArguments>[0], "interactive" | "write" | "choose">
1042
+ ): Promise<SousRef> {
1043
+ return sharedRefPicker().pick(
1044
+ matches,
1045
+ new RefPickArguments({
1046
+ ...args,
1047
+ interactive: this.interactive,
1048
+ write: (message: string) => this.write(message),
1049
+ choose: (message, offered) => this.choose(message, offered),
1050
+ })
1051
+ );
1052
+ }
1053
+
1054
+ /** `pickRef` for a step that only offers namespaces and recipes. */
1055
+ private async pickNamed(
1056
+ matches: NamedRef[],
1057
+ args: Omit<ConstructorParameters<typeof RefPickArguments>[0], "interactive" | "write" | "choose">
1058
+ ): Promise<NamedRef> {
1059
+ return (await this.pickRef(matches, args)) as NamedRef;
1025
1060
  }
1026
1061
 
1027
1062
  /**
@@ -1039,43 +1074,25 @@ export class SubscriptionService {
1039
1074
  */
1040
1075
  private async trustLocation(
1041
1076
  written: string,
1042
- readings: RefReading[],
1077
+ readings: SousRef[],
1043
1078
  options: SubscribeOptions
1044
1079
  ): Promise<string[]> {
1045
1080
  const repos = this.resolverRepos();
1046
1081
  const known = new Set(Object.values(repos).map((repo) => repo.identity));
1047
- const located = readings.filter((reading) => reading.location !== undefined);
1048
- if (located.some((reading) => known.has(reading.location!.identity))) return [];
1082
+ const located = readings.filter((reading) => locationOf(reading) !== undefined);
1083
+ if (located.some((reading) => known.has(locationOf(reading)!.identity))) return [];
1049
1084
 
1050
1085
  let chosen = located[0]!;
1051
- const identities = new Set(located.map((reading) => reading.location!.identity));
1086
+ const identities = new Set(located.map((reading) => locationOf(reading)!.identity));
1052
1087
  if (identities.size > 1) {
1053
- const offered: ReferenceMatch[] = located.map((reading) => ({
1054
- scope:
1055
- isNamedReading(reading) && reading.recipe !== undefined
1056
- ? SousScope.Recipe
1057
- : SousScope.Namespace,
1058
- key: formatRef(reading, this.providers),
1059
- label: formatRef(reading, this.providers),
1060
- repo: reading.location!.url,
1061
- ...(isNamedReading(reading) ? { namespace: reading.namespace } : {}),
1062
- ...(isNamedReading(reading) && reading.recipe !== undefined
1063
- ? { recipe: reading.recipe }
1064
- : {}),
1065
- qualification: Qualification.Full,
1066
- }));
1067
- const picked = await pickReference(offered, {
1088
+ chosen = await this.pickRef(located, {
1068
1089
  search: written.trim(),
1069
- interactive: this.interactive,
1070
1090
  ...(options.acceptFirst === undefined ? {} : { acceptFirst: options.acceptFirst }),
1071
1091
  prompt: `'${written.trim()}' can be read as more than one repository. Which one?`,
1072
- write: (message: string) => this.write(message),
1073
- choose: (message, list) => this.choose(message, list),
1074
1092
  });
1075
- chosen = located[offered.indexOf(picked)]!;
1076
1093
  }
1077
1094
 
1078
- const location = chosen.location!;
1095
+ const location = locationOf(chosen)!;
1079
1096
  const outcome = await this.trust.confirmTrust(
1080
1097
  [
1081
1098
  {
@@ -1098,8 +1115,8 @@ export class SubscriptionService {
1098
1115
  * Settles what a ref names, reading nothing but the cached indexes.
1099
1116
  *
1100
1117
  * A ref with two segments already says what it names and is handed back
1101
- * untouched. A ref with one segment is resolved through the shared reference
1102
- * module (`src/lib/refs/`), over the namespace and recipe scopes only:
1118
+ * untouched. A ref with one segment is resolved through the ref resolver
1119
+ * (`src/services/ref-resolver/`), over namespaces and recipes only:
1103
1120
  * nothing found is an error naming what was searched, one match is used and
1104
1121
  * reported, and several are chosen between. `--accept-first` takes the first
1105
1122
  * match in the documented order; a run that cannot ask fails and says so.
@@ -1108,49 +1125,54 @@ export class SubscriptionService {
1108
1125
  * @param options - The accept-first flag.
1109
1126
  */
1110
1127
  private async resolveBareRef(
1111
- written: ParsedRef,
1128
+ written: NamedRef,
1112
1129
  options: SubscribeOptions
1113
- ): Promise<ParsedRef> {
1130
+ ): Promise<NamedRef> {
1114
1131
  // A ref that names a recipe, spelled as it is published, says what it
1115
1132
  // names already. One word, or a spelling in another case, is searched.
1116
- const search =
1117
- written.repo === undefined ? refKey(written) : `${written.repo}:${refKey(written)}`;
1118
- if (written.recipe !== undefined && refKey(written) === refKey(written).toLowerCase()) {
1133
+ const writtenRepo = repoOf(written)?.name;
1134
+ const key = shortKey(written);
1135
+ const search = writtenRepo === undefined ? key : `${writtenRepo}:${key}`;
1136
+ if (written.kind === "recipe" && key === key.toLowerCase()) {
1119
1137
  return written;
1120
1138
  }
1121
1139
 
1122
1140
  const qualifier =
1123
- written.repo === undefined
1141
+ writtenRepo === undefined
1124
1142
  ? undefined
1125
- : (matchNames(written.repo, Object.keys(this.currentRepos()))[0] ?? written.repo);
1143
+ : (matchNames(writtenRepo, Object.keys(this.currentRepos()))[0] ?? writtenRepo);
1126
1144
  const repoOrder = this.repoSearchOrder(qualifier);
1127
1145
  const indexes = await this.loadIndexes(repoOrder);
1128
- if (written.repo === undefined) this.indexesSnapshot = indexes;
1146
+ if (writtenRepo === undefined) this.indexesSnapshot = indexes;
1129
1147
 
1130
- const context = { repos: referenceReposFromIndexes(repoOrder, indexes) };
1131
- const matches = findReference(search, [SousScope.Namespace, SousScope.Recipe], context);
1148
+ const lookup = new CachedIndexLookup(indexes, { order: repoOrder });
1149
+ const matches = (
1150
+ await sharedRefResolver().resolve(
1151
+ new RefResolveArguments({ input: search, lookup, kinds: ["namespace", "recipe"] })
1152
+ )
1153
+ ).refs as NamedRef[];
1132
1154
 
1133
1155
  if (matches.length === 0) {
1134
1156
  throw new ConfigError(
1135
1157
  [
1136
- `Nothing called '${refKey(written)}' was found: no namespace has that name, ` +
1158
+ `Nothing called '${key}' was found: no namespace has that name, ` +
1137
1159
  `and no recipe does either.`,
1138
- ...describeIndexSearch({ name: refKey(written), repoOrder, indexes }),
1139
- ` Run 'sous repo search ${refKey(written)}' to look for something like it, or ` +
1160
+ ...describeIndexSearch({ name: key, repoOrder, indexes }),
1161
+ ` Run 'sous repo search ${key}' to look for something like it, or ` +
1140
1162
  `'sous repo add <url>' to add the repository that publishes it.`,
1141
1163
  ].join("\n")
1142
1164
  );
1143
1165
  }
1144
1166
 
1145
- const chosen = await pickReference(matches, {
1167
+ const chosen = await this.pickNamed(matches, {
1146
1168
  search,
1147
- interactive: this.interactive,
1148
1169
  ...(options.acceptFirst === undefined ? {} : { acceptFirst: options.acceptFirst }),
1149
- write: (message: string) => this.write(message),
1150
- choose: (message, offered) => this.choose(message, offered),
1151
1170
  });
1152
1171
 
1153
- return referenceToRef(chosen, written);
1172
+ // The range the ref was written with says which version to use, never
1173
+ // which thing is meant, so it is carried over to whatever was chosen.
1174
+ const range = rangeOf(written);
1175
+ return chosen.kind === "recipe" && range !== undefined ? { ...chosen, range } : chosen;
1154
1176
  }
1155
1177
 
1156
1178
  /**
@@ -1207,7 +1229,7 @@ export class SubscriptionService {
1207
1229
  * @param options - The yes and dry-run flags.
1208
1230
  */
1209
1231
  private async confirmSubscription(
1210
- parsed: ParsedRef,
1232
+ parsed: NamedRef,
1211
1233
  options: SubscribeOptions
1212
1234
  ): Promise<void> {
1213
1235
  const indexes = await this.indexSnapshot();
@@ -1243,7 +1265,7 @@ export class SubscriptionService {
1243
1265
  * @param options - The prerelease flag, for the dependency peek.
1244
1266
  */
1245
1267
  private async subscriptionPlan(
1246
- parsed: ParsedRef,
1268
+ parsed: NamedRef,
1247
1269
  options: SubscribeOptions,
1248
1270
  indexes: Map<string, IndexFile>
1249
1271
  ): Promise<string[]> {
@@ -1261,12 +1283,12 @@ export class SubscriptionService {
1261
1283
  palette.warning(line)
1262
1284
  );
1263
1285
 
1264
- if (parsed.recipe === undefined) {
1286
+ if (parsed.kind === "namespace") {
1265
1287
  const published = this.namespaceRecipes(parsed, indexes);
1266
1288
  lines.push(
1267
1289
  ...sentence(
1268
1290
  `Subscribing to '${target}' subscribes this project to the whole ` +
1269
- `namespace '${parsed.namespace}', which means ${palette.highlight(
1291
+ `namespace '${parsed.name}', which means ${palette.highlight(
1270
1292
  "every recipe in it, including ones published later"
1271
1293
  )}.`
1272
1294
  )
@@ -1279,8 +1301,8 @@ export class SubscriptionService {
1279
1301
  } else {
1280
1302
  lines.push(
1281
1303
  ...sentence(
1282
- `Subscribing to '${target}' installs the recipe '${parsed.recipe}' from ` +
1283
- `the namespace '${parsed.namespace}'.`
1304
+ `Subscribing to '${target}' installs the recipe '${parsed.name}' from ` +
1305
+ `the namespace '${namespaceOf(parsed)!.name}'.`
1284
1306
  )
1285
1307
  );
1286
1308
  }
@@ -1344,11 +1366,12 @@ export class SubscriptionService {
1344
1366
  *
1345
1367
  * @param parsed - The fully qualified namespace ref.
1346
1368
  */
1347
- private namespaceRecipes(parsed: ParsedRef, indexes: Map<string, IndexFile>): string[] {
1348
- const index = parsed.repo === undefined ? undefined : indexes.get(parsed.repo);
1369
+ private namespaceRecipes(parsed: NamedRef, indexes: Map<string, IndexFile>): string[] {
1370
+ const repo = repoOf(parsed)?.name;
1371
+ const index = repo === undefined ? undefined : indexes.get(repo);
1349
1372
  if (index === undefined) return [];
1350
1373
  return Object.keys(index.recipes)
1351
- .filter((key) => key.startsWith(`${parsed.namespace}/`))
1374
+ .filter((key) => key.startsWith(`${namespaceOf(parsed)!.name}/`))
1352
1375
  .sort();
1353
1376
  }
1354
1377
 
@@ -1366,7 +1389,7 @@ export class SubscriptionService {
1366
1389
  * @param options - The prerelease flag.
1367
1390
  */
1368
1391
  private async knownUntrustedDependencyRepos(
1369
- parsed: ParsedRef,
1392
+ parsed: NamedRef,
1370
1393
  options: SubscribeOptions,
1371
1394
  indexes: Map<string, IndexFile>
1372
1395
  ): Promise<string[]> {
@@ -1413,7 +1436,7 @@ export class SubscriptionService {
1413
1436
  * @param options - The prerelease and trust flags.
1414
1437
  */
1415
1438
  private async resolveClosure(
1416
- parsed: ParsedRef,
1439
+ parsed: NamedRef,
1417
1440
  options: SubscribeOptions
1418
1441
  ): Promise<{
1419
1442
  resolved: ResolvedRecipe[];
@@ -1501,14 +1524,16 @@ export class SubscriptionService {
1501
1524
  * @param keys - The subscription keys in force.
1502
1525
  */
1503
1526
  private async subscriptionKeyFor(written: string, keys: string[]): Promise<string> {
1504
- const readings = parseRef(written, RefSource.CommandLine, this.providers);
1505
- const first = readings[0]!;
1506
- if (first.location === undefined && isNamedReading(first)) {
1507
- const key = refKey(first);
1527
+ const { refs: readings } = sharedRefResolver().parse(written);
1528
+ const short = readings.find(
1529
+ (reading): reading is NamedRef => isNamedRef(reading) && locationOf(reading) === undefined
1530
+ );
1531
+ if (short !== undefined) {
1532
+ const key = shortKey(short);
1508
1533
  return matchNames(key, new Set(keys))[0] ?? key;
1509
1534
  }
1510
1535
  const chosen = await this.pickLocated(written);
1511
- return refKey(referenceToRef(chosen, { namespace: chosen.namespace! }));
1536
+ return shortKey(chosen);
1512
1537
  }
1513
1538
 
1514
1539
  /**
@@ -1762,7 +1787,7 @@ export class SubscriptionService {
1762
1787
  for (const key of Object.keys(subscriptions).sort()) {
1763
1788
  let qualifier: string | undefined;
1764
1789
  try {
1765
- qualifier = parseShortRef(key, RefSource.Config).repo;
1790
+ qualifier = repoOf(parseNamedRef(key, RefSource.Config))?.name;
1766
1791
  } catch {
1767
1792
  // A ref that does not parse cannot name this repository, and reporting
1768
1793
  // it here would bury the removal under an unrelated complaint.
@@ -1921,7 +1946,7 @@ export class SubscriptionService {
1921
1946
  *
1922
1947
  * - Every trusted repository's index is fetched fresh first. One that
1923
1948
  * cannot be reached is reported, and its pins stay exactly where they are.
1924
- * - The reference, when there is one, is resolved through `src/lib/refs/`
1949
+ * - The reference, when there is one, is resolved through `src/services/ref-resolver/`
1925
1950
  * against those indexes, and it narrows which pins may move. Everything
1926
1951
  * outside it is held at its locked version for as long as its ranges
1927
1952
  * allow, so dependencies move with the closure and nothing else does.
@@ -2118,34 +2143,31 @@ export class SubscriptionService {
2118
2143
  const urls: Record<string, string | undefined> = {};
2119
2144
  for (const name of repoOrder) urls[name] = repos[name]?.url;
2120
2145
 
2121
- const matches = findReference(
2122
- options.ref,
2123
- [SousScope.Repository, SousScope.Namespace, SousScope.Recipe],
2124
- { repos: referenceReposFromIndexes(repoOrder, indexes, urls) }
2125
- );
2146
+ const matches = await this.findRefs(options.ref, repoOrder, indexes, [
2147
+ "repo",
2148
+ "namespace",
2149
+ "recipe",
2150
+ ]);
2126
2151
 
2127
- const chosen = await pickReference(matches, {
2152
+ const chosen = await this.pickRef(matches, {
2128
2153
  search: options.ref,
2129
- interactive: this.interactive,
2130
2154
  ...(options.acceptFirst === undefined ? {} : { acceptFirst: options.acceptFirst }),
2131
2155
  details: [
2132
2156
  ` No trusted repository, namespace or recipe has that name.`,
2133
2157
  ...describeIndexSearch({ name: options.ref, repoOrder, indexes }),
2134
2158
  ],
2135
- write: (message: string) => this.write(message),
2136
- choose: (message, offered) => this.choose(message, offered),
2137
2159
  });
2138
2160
 
2139
- switch (chosen.scope) {
2140
- case SousScope.Repository:
2141
- return { kind: "repository", repo: chosen.repo! };
2142
- case SousScope.Namespace:
2143
- return { kind: "namespace", repo: chosen.repo!, namespace: chosen.namespace! };
2161
+ switch (chosen.kind) {
2162
+ case "repo":
2163
+ return { kind: "repository", repo: chosen.name! };
2164
+ case "namespace":
2165
+ return { kind: "namespace", repo: repoOf(chosen)!.name!, namespace: chosen.name };
2144
2166
  default:
2145
2167
  return {
2146
2168
  kind: "recipe",
2147
- repo: chosen.repo!,
2148
- key: `${chosen.namespace!}/${chosen.recipe!}`,
2169
+ repo: repoOf(chosen)!.name!,
2170
+ key: shortKey(chosen as NamedRef),
2149
2171
  };
2150
2172
  }
2151
2173
  }
@@ -2215,9 +2237,9 @@ export class SubscriptionService {
2215
2237
  const entry = subscriptions[key]!;
2216
2238
  let request: RefRequest;
2217
2239
  try {
2218
- const parsed = parseShortRef(key, RefSource.Config);
2219
2240
  request = {
2220
- ref: { ...parsed, ...(entry.range === undefined ? {} : { range: entry.range }) },
2241
+ ref: parseNamedRef(key, RefSource.Config),
2242
+ ...(entry.range === undefined ? {} : { range: entry.range }),
2221
2243
  requestedBy: PROJECT_REQUESTER,
2222
2244
  kind: "subscribes",
2223
2245
  ...(entry.prerelease === true ? { prerelease: true } : {}),
@@ -2321,23 +2343,24 @@ export class SubscriptionService {
2321
2343
  ): boolean {
2322
2344
  if (scope.kind === "all") return true;
2323
2345
 
2324
- let parsed: ParsedRef;
2346
+ let parsed: NamedRef;
2325
2347
  try {
2326
- parsed = parseShortRef(key, RefSource.Config);
2348
+ parsed = parseNamedRef(key, RefSource.Config);
2327
2349
  } catch {
2328
2350
  return false;
2329
2351
  }
2352
+ const parsedRepo = repoOf(parsed)?.name;
2330
2353
 
2331
- const closure = lockedClosure(lock, keysHeldBySubscription(lock, refKey(parsed)));
2354
+ const closure = lockedClosure(lock, keysHeldBySubscription(lock, shortKey(parsed)));
2332
2355
  if (closure.some((held) => movable(held, lock.recipes[held]!.repo))) return true;
2333
2356
 
2334
2357
  // Nothing locked reaches the scope, but the subscription may still name it:
2335
2358
  // a namespace subscription gains the recipes a namespace publishes later.
2336
- if (parsed.repo !== undefined && parsed.repo !== scope.repo) return false;
2337
- const covered = refKey(parsed);
2359
+ if (parsedRepo !== undefined && parsedRepo !== scope.repo) return false;
2360
+ const covered = shortKey(parsed);
2338
2361
  switch (scope.kind) {
2339
2362
  case "repository":
2340
- return parsed.repo === scope.repo;
2363
+ return parsedRepo === scope.repo;
2341
2364
  case "namespace":
2342
2365
  return covered === scope.namespace || covered.startsWith(`${scope.namespace}/`);
2343
2366
  case "recipe":
@@ -2368,7 +2391,7 @@ export class SubscriptionService {
2368
2391
  ): Lockfile {
2369
2392
  const coverOf = (key: string): string | undefined => {
2370
2393
  try {
2371
- return refKey(parseShortRef(key, RefSource.Config));
2394
+ return shortKey(parseNamedRef(key, RefSource.Config));
2372
2395
  } catch {
2373
2396
  return undefined;
2374
2397
  }
@@ -2456,16 +2479,16 @@ export class SubscriptionService {
2456
2479
  .filter((key) => declared[key]?.enabled === false)
2457
2480
  .filter((key) => {
2458
2481
  if (scope.kind === "all") return true;
2459
- let parsed: ParsedRef;
2482
+ let parsed: NamedRef;
2460
2483
  try {
2461
- parsed = parseShortRef(key, RefSource.Config);
2484
+ parsed = parseNamedRef(key, RefSource.Config);
2462
2485
  } catch {
2463
2486
  return false;
2464
2487
  }
2465
- const covered = refKey(parsed);
2488
+ const covered = shortKey(parsed);
2466
2489
  switch (scope.kind) {
2467
2490
  case "repository":
2468
- return parsed.repo === scope.repo;
2491
+ return repoOf(parsed)?.name === scope.repo;
2469
2492
  case "namespace":
2470
2493
  return covered === scope.namespace || covered.startsWith(`${scope.namespace}/`);
2471
2494
  case "recipe":
@@ -2642,9 +2665,9 @@ export class SubscriptionService {
2642
2665
  const entry = subscriptions[key]!;
2643
2666
  if (this.subscriptionIsLocked(key, entry, before)) continue;
2644
2667
 
2645
- let parsed: ParsedRef;
2668
+ let parsed: NamedRef;
2646
2669
  try {
2647
- parsed = parseShortRef(key, RefSource.Config);
2670
+ parsed = parseNamedRef(key, RefSource.Config);
2648
2671
  } catch (error) {
2649
2672
  report.failed.push({ key, reason: describeError(error) });
2650
2673
  continue;
@@ -2653,10 +2676,8 @@ export class SubscriptionService {
2653
2676
  pending.push({
2654
2677
  key,
2655
2678
  request: {
2656
- ref: {
2657
- ...parsed,
2658
- ...(entry.range === undefined ? {} : { range: entry.range }),
2659
- },
2679
+ ref: parsed,
2680
+ ...(entry.range === undefined ? {} : { range: entry.range }),
2660
2681
  requestedBy: PROJECT_REQUESTER,
2661
2682
  kind: "subscribes",
2662
2683
  ...(entry.prerelease === true ? { prerelease: true } : {}),
@@ -3371,11 +3392,12 @@ export class SubscriptionService {
3371
3392
  */
3372
3393
  private writeSubscriptionEntry(
3373
3394
  key: string,
3374
- parsed: ParsedRef,
3395
+ parsed: NamedRef,
3375
3396
  options: SubscribeOptions
3376
3397
  ): void {
3398
+ const range = rangeOf(parsed);
3377
3399
  const entry: SubscriptionEntry = {
3378
- ...(parsed.range === undefined ? {} : { range: parsed.range }),
3400
+ ...(range === undefined ? {} : { range }),
3379
3401
  ...(options.prerelease === true ? { prerelease: true } : {}),
3380
3402
  ...(options.alwaysPull === true ? { alwaysPull: true } : {}),
3381
3403
  addedAt: this.now().toISOString(),
@@ -3513,17 +3535,17 @@ export class SubscriptionService {
3513
3535
  for (const dependency of manifest.depends ?? []) {
3514
3536
  // Every reading counts: a dependency that reads more than one way still
3515
3537
  // declares its range for whichever reading the lockfile settled on.
3516
- let readings: RefReading[];
3538
+ let readings: SousRef[];
3517
3539
  try {
3518
- readings = parseRef(dependency, RefSource.Manifest, this.providers);
3540
+ readings = sharedRefResolver().parse(dependency, RefSource.Manifest).refs;
3519
3541
  } catch {
3520
3542
  continue;
3521
3543
  }
3522
3544
  for (const reading of readings) {
3523
- if (!isNamedReading(reading)) continue;
3524
- const dependencyKey = refKey(reading);
3545
+ if (!isNamedRef(reading)) continue;
3546
+ const dependencyKey = shortKey(reading);
3525
3547
  if (dependencyKey !== key && dependencyKey !== namespace) continue;
3526
- return reading.range ?? "*";
3548
+ return rangeOf(reading) ?? "*";
3527
3549
  }
3528
3550
  }
3529
3551
 
@@ -3658,7 +3680,7 @@ export class SubscriptionService {
3658
3680
  * @param options - The indexes to read, and whether prereleases may match.
3659
3681
  */
3660
3682
  async previewSubscription(
3661
- parsed: ParsedRef,
3683
+ parsed: NamedRef,
3662
3684
  options: PreviewSubscriptionOptions = {}
3663
3685
  ): Promise<SubscriptionPreview> {
3664
3686
  const repos = this.resolverRepos();