@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.
Files changed (43) hide show
  1. package/docs/markdown/_sidebar.md +1 -0
  2. package/docs/markdown/repositories-authoring.md +12 -8
  3. package/docs/markdown/repositories-consuming.md +8 -1
  4. package/docs/markdown/repositories-file-formats.md +53 -8
  5. package/docs/markdown/repositories-providers.md +8 -6
  6. package/docs/markdown/repositories-troubleshooting.md +6 -0
  7. package/docs/markdown/repositories-variables.md +9 -2
  8. package/docs/markdown/repositories.md +3 -3
  9. package/package.json +1 -1
  10. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +11 -7
  11. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  12. package/src/commands/lock/rebuild.ts +2 -2
  13. package/src/commands/repo/contribute.ts +1 -10
  14. package/src/commands/repo/gc.ts +3 -2
  15. package/src/commands/repo/release.ts +20 -3
  16. package/src/commands/repo/search.ts +2 -1
  17. package/src/commands/vars/ask.ts +32 -10
  18. package/src/lib/refs/find.ts +202 -21
  19. package/src/lib/refs/index.ts +2 -0
  20. package/src/lib/refs/parse.ts +844 -0
  21. package/src/lib/refs/scopes.ts +34 -0
  22. package/src/lib/refs/settle.ts +219 -0
  23. package/src/lib/repos/catalog.ts +44 -25
  24. package/src/lib/repos/formats/index-file.ts +2 -1
  25. package/src/lib/repos/formats/recipe-manifest.ts +10 -8
  26. package/src/lib/repos/index.ts +0 -1
  27. package/src/lib/repos/lock-service.ts +3 -2
  28. package/src/lib/repos/locked-namespace-resolver.ts +21 -15
  29. package/src/lib/repos/locked-recipes.ts +2 -2
  30. package/src/lib/repos/namespace-resolver.ts +40 -47
  31. package/src/lib/repos/providers/base.ts +30 -0
  32. package/src/lib/repos/providers/github.ts +22 -0
  33. package/src/lib/repos/providers/gitlab.ts +68 -0
  34. package/src/lib/repos/providers/provider.ts +96 -0
  35. package/src/lib/repos/release/index-builder.ts +55 -17
  36. package/src/lib/repos/release/index.ts +1 -0
  37. package/src/lib/repos/release/plan.ts +8 -6
  38. package/src/lib/repos/release/settle.ts +237 -0
  39. package/src/lib/repos/release/tags.ts +3 -7
  40. package/src/lib/repos/resolver.ts +198 -41
  41. package/src/lib/repos/subscription-service.ts +262 -38
  42. package/src/lib/vars/mappings.ts +17 -30
  43. package/src/lib/repos/ref.ts +0 -513
@@ -26,4 +26,5 @@
26
26
  - [0005: Follow-ups from the first project on the hand-off](adrs/0005-first-project-follow-ups.md)
27
27
  - [0006: `sous repo init` follows `sous-recipes`](adrs/0006-repo-init-model-home.md)
28
28
  - [0007: Frozen index entries and tolerant readers](adrs/0007-frozen-index-dependencies.md)
29
+ - [0008: One ref parser, told where the ref came from](adrs/0008-one-ref-parser.md)
29
30
  - [0009: One whole build, everywhere](adrs/0009-whole-build-everywhere.md)
@@ -151,19 +151,23 @@ depends:
151
151
  - workflow/qa-helper # a sibling, released alongside me
152
152
  - workflow/qa-helper@^1.1 # a sibling with a range; uncommon
153
153
  - github://sous-io/sous-recipes/workflow/sat@^1.0 # another repository
154
- - gitlab://gitlab.example.com/group/subgroup/proj/qa/lint # a self-hosted host, with subgroups
154
+ - gitlab://gitlab.example.com/group/subgroup/proj/-/qa/lint # a self-hosted host, with subgroups
155
+ - https://github.com/acme/recipes/tree/main/recipes/lint # a URL copied from the browser
155
156
  subscribes:
156
157
  - quality/code-review # co-subscribed: its files land too
157
158
  ```
158
159
 
159
160
  A **bare ref** is a sibling in this repository; with no range it means "the version released alongside me", since
160
- one run cuts both tags and records the exact version in the index. A locator URL names another repository,
161
- and its last two segments are always the namespace and the recipe, which is the recipe's published **identity and
162
- never a path on disk**: a recipe stored at `recipes/shared/sat/` and published as `workflow/sat` is written
163
- `github://owner/repo/workflow/sat`. Everything before them is the repository, a dotted first segment naming the
164
- host. The optional range after `@` follows npm's rules. `local://` locators and `repo:` short names are refused:
165
- a local repository is a consumer's convenience, and a short name is one project's private label. See
166
- [Dependencies named by location](repositories-file-formats.md#dependencies-named-by-location).
161
+ one run cuts both tags and records the exact version in the index. A location names another repository: a
162
+ locator, an HTTPS or SSH URL, or a URL copied from the browser. A ref is stored and printed as its published
163
+ **identity** (namespace and recipe), never as a folder path. Any spelling that settles to one identity is
164
+ accepted, including a folder path or a pasted browser URL: a recipe stored at `recipes/shared/sat/` and
165
+ published as `workflow/sat` may be written `github://owner/repo/workflow/sat` or
166
+ `https://github.com/owner/repo/tree/main/recipes/shared/sat`, and the release settles the second through the
167
+ other repository's index. The optional range after `@` follows npm's rules. `local://` locators and `repo:`
168
+ short names are refused: a local repository is a consumer's convenience, and a short name is one project's
169
+ private label. See [Dependencies named by location](repositories-file-formats.md#dependencies-named-by-location)
170
+ and every accepted spelling in [Ref forms](repositories-file-formats.md#ref-forms).
167
171
 
168
172
  ## Cut a release
169
173
 
@@ -87,15 +87,22 @@ $ sous recipe show workflow/qa-variables
87
87
  sous subscription add workflow/qa-variables
88
88
  sous subscription add workflow/qa-variables@^1.2.0
89
89
  sous subscription add workflow # a whole namespace
90
+ sous subscription add https://github.com/acme/recipes/tree/main/recipes/workflow/alpha
90
91
  ```
91
92
 
93
+ A ref may be written in any form sous reads ([Ref forms](repositories-file-formats.md#ref-forms)),
94
+ including a URL copied from the browser. A location is matched to the trusted repository at it,
95
+ whatever this project calls that repository; one the project does not trust yet goes through the
96
+ trust question first, and nothing is fetched from it before the answer. The subscription is
97
+ recorded in its canonical form, `namespace/recipe`.
98
+
92
99
  The whole dependency closure resolves before anything is downloaded, and only then is anything
93
100
  written; an install is whole or not at all.
94
101
 
95
102
  | Flag | What it does |
96
103
  |------|--------------|
97
104
  | `-y, --yes` | Answer yes to both questions this command can ask: the subscribe confirmation, and the trust question for a repository it has to add |
98
- | `--accept-first` | When a one-word ref matches several things, take the first one listed |
105
+ | `--accept-first` | When a ref matches several things, take the first one listed |
99
106
  | `--answer <name>=<value>` | Answer one question ahead of time. Repeat it per answer, or read a whole file of them with `--answers-file <path>` |
100
107
  | `--always-pull` | Install a newer in-range version whenever one exists, rather than holding the locked one |
101
108
  | `--dry-run` | Print what would be installed, writing and downloading nothing |
@@ -104,15 +104,60 @@ release itself is never restricted, so whatever publishes the recipe still relea
104
104
 
105
105
  ### Dependencies named by location
106
106
 
107
- `depends` and `subscribes` share one grammar in two spellings: `workflow/qa-helper` is a sibling in this
107
+ `depends` and `subscribes` name their targets by where they live: `workflow/qa-helper` is a sibling in this
108
108
  same repository, and `github://sous-io/sous-recipes/workflow/sub-agent-delegation@^1.0` is a recipe in
109
- another one. A locator URL's scheme is the provider's identifier (`github` and `gitlab` ship today), and its
110
- path is read from the RIGHT: the last two segments are the namespace and the recipe, and everything before
111
- them names the repository, whose first segment is the host when it carries a dot and otherwise the
112
- provider's public host, so `gitlab://gitlab.example.com/group/subgroup/project/workflow/task-files`
113
- resolves. A trailing `.git` is dropped and at most one `@` range may follow. A manifest may write no `repo:`
114
- qualifier, no `local://` locator (a local repository is a convenience, not a published location) and no
115
- filesystem path such as `../qa-helper`.
109
+ another one. Every spelling in [Ref forms](#ref-forms) that names a namespace or a recipe is accepted here,
110
+ except a `repo:` qualifier (one project's private name for a repository), a local path or `local://`
111
+ locator (a convenience, not a published location), and a location naming a whole repository with nothing
112
+ inside it. Names are recorded lowercase.
113
+
114
+ A ref is stored and printed as its published identity (namespace and recipe), never as a folder path. Any
115
+ spelling that settles to one identity is accepted, including a folder path or a pasted browser URL. Two
116
+ spellings need the other repository's index to settle: a GitLab URL with nested groups, which does not say
117
+ where the project path ends (`gitlab.com/a/b/c/d` may be project `a/b` naming the recipe `c/d`, or project
118
+ `a/b/c` naming the namespace `d`), and a browser URL, which names a folder. `sous repo release` settles each
119
+ by fetching the index of every candidate repository and keeping the reading whose index publishes what was
120
+ named, then records the repository it settled on in the index, beside each key the dependency reached. A
121
+ consumer reads that record and never probes. A network failure fails the release rather than guessing, and
122
+ two readings that both publish what is named fail it too, naming the spellings that read one way:
123
+ `gitlab://a/b/c/-/d/*` for the namespace, `gitlab://a/b/-/c/d` for the recipe.
124
+
125
+ ### Ref forms
126
+
127
+ Every place a ref is written reads it with one parser, told where the ref came from. These forms are
128
+ recognized everywhere; the table after them says which each place allows. A refused form is an error saying
129
+ what to write there instead.
130
+
131
+ | Form | Example | Names |
132
+ | --- | --- | --- |
133
+ | A bare name | `workflow` | a namespace (on the command line, also a recipe or anything else by that name) |
134
+ | Namespace and recipe | `workflow/alpha` | a recipe |
135
+ | Namespace, spelled out | `workflow/*` | a namespace |
136
+ | Repository-qualified | `sous-recipes:workflow/alpha` | a recipe in the repository this project calls `sous-recipes` |
137
+ | With a range | `workflow/alpha@^1.2` | a recipe, within an npm-style range |
138
+ | Provider-scheme locator | `github://owner/repo/workflow`, `.../workflow/*`, `.../workflow/alpha` | a namespace or a recipe in that repository |
139
+ | A host other than the provider's own | `gitlab://gitlab.example.com/group/proj/-/workflow/alpha` | the same, on a self-hosted instance |
140
+ | HTTPS URL | `https://github.com/owner/repo/workflow/alpha` | the same as the locator |
141
+ | URL with no scheme | `github.com/owner/repo/workflow/alpha` | the same |
142
+ | SSH remote | `git@github.com:owner/repo.git`, `ssh://git@github.com/owner/repo.git/workflow` | the repository, or something inside it |
143
+ | Browser URL | `https://github.com/owner/repo/tree/main/recipes/workflow/alpha` | the recipe whose index `path` holds that folder (or a file in it); a folder holding one namespace's recipes names the namespace |
144
+ | GitLab separator | `https://gitlab.com/group/sub/proj/-/tree/main/recipes/workflow/alpha` | the same, with `/-/` marking where the project path ends |
145
+ | GitLab nested group | `gitlab://group/sub/proj/workflow/alpha` | every reading the path allows, settled as described above |
146
+
147
+ A `.git` suffix on the repository is dropped in every URL form. GitLab's canonical locator always marks the
148
+ end of the project path with `/-/`, so it reads one way.
149
+
150
+ | Place | Allows |
151
+ | --- | --- |
152
+ | The command line | every form; names in any case |
153
+ | A config file's subscription keys | `namespace` or `namespace/recipe`, lowercase; the repository is recorded in the lockfile and a range in the entry's `range` field |
154
+ | A recipe manifest's `depends` and `subscribes` | every form naming a namespace or a recipe, except `repo:` and a local location |
155
+ | A key sous stores (the lockfile, the index, the store) | `namespace` or `namespace/recipe`, lowercase |
156
+
157
+ Matching tries the exact spelling first, then ignores case; a name that still matches several things is a
158
+ question, answered by `--accept-first` or by choosing. What sous writes (config layers, the lockfile, the
159
+ index) and prints is always the canonical form: `namespace/recipe`, qualified with the repository's short
160
+ name where one is needed.
116
161
 
117
162
  ### Variable definitions
118
163
 
@@ -128,12 +128,14 @@ Canonicalizing also produces the two clone URLs sous hands to git, `https://<hos
128
128
  and `git@<host>:<owner>/<name>.git`. For the `local` provider the HTTPS slot carries the
129
129
  absolute directory, because that is what git is given when a recipe is fetched.
130
130
 
131
- ?> A published manifest names a dependency in another repository by a locator URL whose
132
- scheme is the provider's identifier: `github://sous-io/sous-recipes/workflow/sat@^1.1`. A first
133
- segment containing a dot is read as the host, so `gitlab://gitlab.example.com/group/project/ns/recipe`
134
- addresses a self-hosted instance; without one, the provider's public host is assumed. `local://`
135
- is refused, because a path on your disk is not a published location. The grammar is in
136
- [Dependencies named by location](repositories-file-formats.md#dependencies-named-by-location).
131
+ ?> A published manifest names a dependency in another repository by location, such as a locator
132
+ whose scheme is the provider's identifier: `github://sous-io/sous-recipes/workflow/sat@^1.1`. A
133
+ first segment containing a dot is read as the host, so `gitlab://gitlab.example.com/group/project/-/ns/recipe`
134
+ addresses a self-hosted instance; without one, the provider's public host is assumed. Where a
135
+ repository path ends, and what a browser URL's path looks like, is each provider's own rule (its
136
+ `readLocation`), so a URL copied from either host's file view works too. `local://` is refused,
137
+ because a path on your disk is not a published location. Every form is in
138
+ [Ref forms](repositories-file-formats.md#ref-forms).
137
139
 
138
140
  ## Private repositories
139
141
 
@@ -108,8 +108,14 @@ providers sous ships named (see [Providers](repositories-providers.md)). The leg
108
108
  depends:
109
109
  - workflow/sat # a sibling in this repository
110
110
  - github://acme/recipes/workflow/sat@^1.1 # a recipe in another repository
111
+ - https://github.com/acme/recipes/tree/main/sat # the same, copied from the browser
111
112
  ```
112
113
 
114
+ Every accepted spelling is in [Ref forms](repositories-file-formats.md#ref-forms). A GitLab URL
115
+ with nested groups, or a browser URL, is settled by `sous repo release` through the other
116
+ repository's index; when that index cannot be read, the release stops and names it, and when two
117
+ readings both publish what was named, it names the `/-/` and `/*` spellings that read one way.
118
+
113
119
  While developing both sides, use
114
120
  [`sous repo link`](repositories-authoring.md#edit-a-repository-in-place); it redirects resolution
115
121
  at a working copy without changing what you publish.
@@ -241,10 +241,17 @@ sous vars ask SOUS_VAR_TASK_FILE_ROOT # the same one, by a name in use
241
241
  sous vars ask workflow/task-files.taskFileRoot # the same one, spelled out in full
242
242
  sous vars ask task-files # every question one recipe asks
243
243
  sous vars ask sous-recipes # every question that repository's recipes ask
244
+ sous vars ask https://github.com/sous-io/sous-recipes/tree/main/recipes/workflow/task-files
245
+ # the same recipe, as a URL copied from the browser
244
246
  ```
245
247
 
246
- Matching is case-sensitive, and an environment variable name resolves when a definition declares it or an
247
- env file sets it. `--repo`, `--namespace` and `--var` say which kind is meant, each narrowing what the one
248
+ A repository, namespace or recipe may be written in any form in
249
+ [Ref forms](repositories-file-formats.md#ref-forms), including a location: it is matched to the
250
+ trusted repository at it and settled through that repository's cached index, then narrowed to the
251
+ variables it covers. `--repo` and `--namespace` accept a location the same way.
252
+
253
+ Matching tries the exact spelling first, then ignores case; a name that still matches several things is a
254
+ question. An environment variable name resolves when a definition declares it or an env file sets it. `--repo`, `--namespace` and `--var` say which kind is meant, each narrowing what the one
248
255
  before left, so `sous vars ask --namespace workflow apiUrl` asks about that namespace's `apiUrl` alone. When
249
256
  a name matches several things, sous lists them and asks; `--accept-first` takes the first, and a run with no
250
257
  terminal fails naming it.
@@ -101,9 +101,9 @@ subscribes:
101
101
  - quality/code-review
102
102
  ```
103
103
 
104
- Both name their targets by location, written as a **locator URL**
105
- ([the grammar](repositories-file-formats.md#dependencies-named-by-location)) when the target lives
106
- in another repository, never by a short name a consuming project chose; a curated bundle is simply
104
+ Both name their targets by location, written as a **locator URL** or any other URL form
105
+ ([the forms](repositories-file-formats.md#ref-forms)) when the target lives in another repository,
106
+ never by a short name a consuming project chose; a curated bundle is simply
107
107
  a recipe made mostly of `subscribes` entries. Because both lists are
108
108
  declarative YAML rather than code, sous can read the whole dependency closure before fetching any
109
109
  of it, which is what makes the trust decision answerable up front.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "0.2.28",
3
+ "version": "0.2.29",
4
4
  "description": "Compiles AI coding agent configuration (CLAUDE.md, skills, memories) from LiquidJS templates",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -138,13 +138,17 @@ Both lists name other recipes, and they do different things:
138
138
  - `subscribes` is a **co-subscription**. The other recipe's files land and its questions are asked,
139
139
  as if the project had subscribed to it too.
140
140
 
141
- Each entry names its target by where it lives, in one of two forms:
142
-
143
- - a bare `namespace/recipe` for a sibling in the same repository. With no range it means the
144
- version released alongside this one; an `@` range (npm's rules) is allowed but uncommon.
145
- - a locator URL for a recipe in another repository, such as
146
- `github://example-org/team-recipes/review/wording@^1.0`. The scheme is the provider, and the last
147
- two segments are always the recipe's namespace and name, never a path on disk.
141
+ Each entry names its target by where it lives:
142
+
143
+ - a bare `namespace/recipe` (or `namespace` for all of it) for a sibling in the same repository.
144
+ With no range it means the version released alongside this one; an `@` range (npm's rules) is
145
+ allowed but uncommon.
146
+ - a location for a recipe in another repository, such as
147
+ `github://example-org/team-recipes/review/wording@^1.0`, an HTTPS or SSH URL, or a URL copied
148
+ from the browser. A ref is stored and printed as its published identity (namespace and recipe),
149
+ never as a folder path. Any spelling that settles to one identity is accepted, including a
150
+ folder path or a pasted browser URL; `sous repo release` settles one that needs the other
151
+ repository's index, and fails rather than guess.
148
152
 
149
153
  `local://` locators and `repo:` short names are refused: both are one project's private view of a
150
154
  repository, and a published manifest is read everywhere.
@@ -11,7 +11,7 @@ formatVersion: 1
11
11
 
12
12
  namespace: core
13
13
  name: sous-skills
14
- version: 0.2.28
14
+ version: 0.2.29
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous
@@ -24,7 +24,7 @@ import { recipeFilesDirectory } from "../../lib/repos/catalog-inputs.js";
24
24
  import { readRecipeManifestIn } from "../../lib/repos/locked-recipes.js";
25
25
  import { createEmptyLockfile } from "../../lib/repos/formats/lockfile.js";
26
26
  import type { LockRepoInput } from "../../lib/repos/lock-service.js";
27
- import { parseRef } from "../../lib/repos/ref.js";
27
+ import { RefSource, parseShortRef } from "../../lib/refs/index.js";
28
28
  import {
29
29
  PROJECT_REQUESTER,
30
30
  resolveRefs,
@@ -133,7 +133,7 @@ export default class LockRebuild extends BaseCommand {
133
133
  .sort()
134
134
  .map((key) => {
135
135
  const entry = subscriptions[key]!;
136
- const parsed = parseRef(key);
136
+ const parsed = parseShortRef(key, RefSource.Config);
137
137
  return {
138
138
  ref: {
139
139
  ...parsed,
@@ -6,7 +6,6 @@ import {
6
6
  SousScope,
7
7
  findReference,
8
8
  pickReference,
9
- referenceReposFromIndexes,
10
9
  type ReferenceRepo,
11
10
  } from "../../lib/refs/index.js";
12
11
  import {
@@ -374,15 +373,7 @@ export default class RepoContribute extends BaseCommand {
374
373
  shellEnv: this.shellEnv,
375
374
  });
376
375
 
377
- const trusted = service.currentRepos();
378
- const repos: ReferenceRepo[] = Object.keys(trusted).map((name) => {
379
- const url = trusted[name]?.url;
380
- const index = service.cachedIndex(name);
381
- if (index === undefined) {
382
- return { name, ...(url === undefined ? {} : { url }), namespaces: [], recipes: [] };
383
- }
384
- return referenceReposFromIndexes([name], new Map([[name, index]]), { [name]: url })[0]!;
385
- });
376
+ const repos: ReferenceRepo[] = service.cachedReferenceRepos();
386
377
 
387
378
  const matches = findReference(
388
379
  ref,
@@ -17,6 +17,7 @@ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js"
17
17
  import { resolveStoreSettings } from "../../lib/repos/store/settings.js";
18
18
  import type { StoreKey } from "../../lib/repos/store/contract.js";
19
19
  import { renderTable, type TableColumn } from "../../utils/table.js";
20
+ import { splitRecipeKey } from "../../lib/refs/parse.js";
20
21
  import {
21
22
  blankLine,
22
23
  dryRunNotice,
@@ -120,12 +121,12 @@ export default class RepoGc extends BaseCommand {
120
121
  const keep: StoreKey[] = Object.entries(lock.recipes).flatMap(([key, entry]) => {
121
122
  const identity = lock.repos[entry.repo]?.identity;
122
123
  if (identity === undefined) return [];
123
- const namespace = key.slice(0, key.indexOf("/"));
124
+ const { namespace, name } = splitRecipeKey(key);
124
125
  return [
125
126
  {
126
127
  identity,
127
128
  namespace,
128
- name: key.slice(namespace.length + 1),
129
+ name,
129
130
  version: entry.version,
130
131
  },
131
132
  ];
@@ -30,6 +30,7 @@ import {
30
30
  releaseScope,
31
31
  remoteUrl,
32
32
  scopeProblems,
33
+ settleDependencyLocations,
33
34
  uncommittedChanges,
34
35
  validateRepo,
35
36
  warningsIn,
@@ -38,6 +39,7 @@ import {
38
39
  type ReleasePlan,
39
40
  type ReleaseScope,
40
41
  type RepoValidation,
42
+ type SettledDependency,
41
43
  type ValidationProblem,
42
44
  } from "../../lib/repos/release/index.js";
43
45
  import {
@@ -195,7 +197,14 @@ export default class RepoRelease extends Command {
195
197
  }
196
198
  log(` Read ${describeCount(validation.recipes.length, "recipe")}.`);
197
199
 
198
- if (flags.check) return await this.runCheck(rootDir, validation);
200
+ // A dependency that reads more than one way is settled before anything is
201
+ // planned or written, so a repository that cannot be reached stops the run
202
+ // with nothing changed.
203
+ const settling = await settleDependencyLocations(validation);
204
+ reportProblems(settling.problems);
205
+ if (hasErrors(settling.problems)) return this.stopForErrors(settling.problems);
206
+
207
+ if (flags.check) return await this.runCheck(rootDir, validation, settling.settled);
199
208
 
200
209
  // --- Plan -------------------------------------------------------------
201
210
 
@@ -284,6 +293,7 @@ export default class RepoRelease extends Command {
284
293
  existing,
285
294
  sousVersion: SOUS_VERSION,
286
295
  publishing,
296
+ settled: settling.settled,
287
297
  });
288
298
  reportProblems(rebuilt.problems);
289
299
  if (hasErrors(rebuilt.problems)) return this.stopForErrors(rebuilt.problems);
@@ -365,15 +375,22 @@ export default class RepoRelease extends Command {
365
375
  *
366
376
  * @param rootDir - The repository's root directory.
367
377
  * @param validation - The validated repository.
378
+ * @param settled - The dependencies that read more than one way, as settled.
368
379
  */
369
380
  private async runCheck(
370
381
  rootDir: string,
371
- validation: RepoValidation
382
+ validation: RepoValidation,
383
+ settled: Map<string, SettledDependency>
372
384
  ): Promise<void> {
373
385
  section("Checking the index");
374
386
 
375
387
  const existing = readExistingIndex(rootDir);
376
- const result = await buildIndex({ validation, existing, sousVersion: SOUS_VERSION });
388
+ const result = await buildIndex({
389
+ validation,
390
+ existing,
391
+ sousVersion: SOUS_VERSION,
392
+ settled,
393
+ });
377
394
  reportProblems(result.problems);
378
395
  if (hasErrors(result.problems)) return this.stopForErrors(result.problems);
379
396
 
@@ -21,6 +21,7 @@ import {
21
21
  } from "../../lib/repos/catalog-display.js";
22
22
  import { readEffectiveLinks } from "../../lib/repos/links.js";
23
23
  import { renderTable, type TableColumn } from "../../utils/table.js";
24
+ import { namespaceOfKey } from "../../lib/refs/parse.js";
24
25
  import { browsingFlags } from "../../utils/flags.js";
25
26
  import {
26
27
  blankLine,
@@ -142,7 +143,7 @@ export default class RepoSearch extends BaseCommand {
142
143
  const installed = locked?.repo === repo ? locked.version : undefined;
143
144
  if (flags.installed && installed === undefined) continue;
144
145
 
145
- const namespace = key.slice(0, key.indexOf("/"));
146
+ const namespace = namespaceOfKey(key);
146
147
  const haystack = [
147
148
  key,
148
149
  recipe.description ?? "",
@@ -48,7 +48,9 @@ import {
48
48
  referenceContextFromVariables,
49
49
  variableReferenceKey,
50
50
  type ReferenceMatch,
51
+ type ReferenceRepo,
51
52
  } from "../../lib/refs/index.js";
53
+ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
52
54
  import {
53
55
  applyProvidedAnswers,
54
56
  askForMissing,
@@ -212,7 +214,10 @@ export default class VarsAsk extends BaseCommand {
212
214
 
213
215
  // What the reference and the flags name, as the fully qualified reference of
214
216
  // every variable to ask. Undefined means everything the usual rules pick.
215
- const only = await this.resolveSelection(defined, context, selection);
217
+ // A reference written as a location settles against the trusted
218
+ // repositories, read from the cached indexes; a definitions file has none.
219
+ const trusted = flags.file === undefined ? this.trustedRepos() : [];
220
+ const only = await this.resolveSelection(defined, context, selection, trusted);
216
221
 
217
222
  const askOptions = {
218
223
  sousDir: this.configContext.sousDir,
@@ -257,13 +262,17 @@ export default class VarsAsk extends BaseCommand {
257
262
  * @param defined - Every variable definition in play.
258
263
  * @param ladder - The environment layers, for resolving environment variable names.
259
264
  * @param selection - The reference and the flags, as the caller wrote them.
265
+ * @param trusted - The trusted repositories, which settle a reference written as a location.
260
266
  * @returns The variables to ask, or undefined when nothing narrowed anything.
261
267
  */
262
268
  private async resolveSelection(
263
269
  defined: DefinedVariable[],
264
270
  ladder: LadderContext,
265
- selection: Selection
271
+ selection: Selection,
272
+ trusted: ReferenceRepo[] = []
266
273
  ): Promise<string[] | undefined> {
274
+ const contextOf = (pool: DefinedVariable[]) =>
275
+ referenceContextFromVariables(pool, ladder, trusted);
267
276
  const { name, repo, namespace, vars } = selection;
268
277
  if (name === undefined && repo === undefined && namespace === undefined && vars === undefined) {
269
278
  return undefined;
@@ -272,13 +281,13 @@ export default class VarsAsk extends BaseCommand {
272
281
  let pool = defined;
273
282
 
274
283
  if (repo !== undefined) {
275
- const match = await this.pick(findRepository(repo, contextOf(pool, ladder)), repo, selection);
284
+ const match = await this.pick(findRepository(repo, contextOf(pool)), repo, selection);
276
285
  pool = pool.filter((entry) => entry.recipe.repo === match.repo);
277
286
  }
278
287
 
279
288
  if (namespace !== undefined) {
280
289
  const match = await this.pick(
281
- findNamespace(namespace, contextOf(pool, ladder)),
290
+ findNamespace(namespace, contextOf(pool)),
282
291
  namespace,
283
292
  selection
284
293
  );
@@ -287,7 +296,7 @@ export default class VarsAsk extends BaseCommand {
287
296
 
288
297
  if (name !== undefined) {
289
298
  const match = await this.pick(
290
- findReference(name, ALL_SCOPES, contextOf(pool, ladder)),
299
+ findReference(name, ALL_SCOPES, contextOf(pool)),
291
300
  name,
292
301
  selection
293
302
  );
@@ -298,7 +307,7 @@ export default class VarsAsk extends BaseCommand {
298
307
  const chosen: DefinedVariable[] = [];
299
308
  for (const wanted of vars) {
300
309
  const match = await this.pick(
301
- findVariable(wanted, contextOf(pool, ladder)),
310
+ findVariable(wanted, contextOf(pool)),
302
311
  wanted,
303
312
  selection
304
313
  );
@@ -317,6 +326,23 @@ export default class VarsAsk extends BaseCommand {
317
326
  return [...new Set(pool.map(variableReferenceKey))];
318
327
  }
319
328
 
329
+ /**
330
+ * The trusted repositories as a reference searches them, from the cached
331
+ * indexes alone. A project whose repositories cannot be read this way still
332
+ * answers every other form of reference, so a failure here is an empty list.
333
+ */
334
+ private trustedRepos(): ReferenceRepo[] {
335
+ try {
336
+ return subscriptionServiceFor({
337
+ configContext: this.configContext,
338
+ settings: this.settings,
339
+ shellEnv: this.shellEnv,
340
+ }).cachedReferenceRepos();
341
+ } catch {
342
+ return [];
343
+ }
344
+ }
345
+
320
346
  /**
321
347
  * Settles which of the things a reference could have meant this run proceeds
322
348
  * with, through the rule every command shares.
@@ -356,10 +382,6 @@ function narrowTo(pool: DefinedVariable[], match: ReferenceMatch): DefinedVariab
356
382
  });
357
383
  }
358
384
 
359
- /** The reference context for the variables still in the running. */
360
- function contextOf(pool: DefinedVariable[], ladder: LadderContext) {
361
- return referenceContextFromVariables(pool, ladder);
362
- }
363
385
 
364
386
  /** What this run is asking about, in the words the header shows. */
365
387
  function describeSelection(selection: Selection, all: boolean): string {