@sous-io/sous 0.2.15 → 0.2.17

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 (39) hide show
  1. package/docs/markdown/commands.md +63 -16
  2. package/docs/markdown/repositories-authoring.md +52 -11
  3. package/docs/markdown/repositories-consuming.md +32 -1
  4. package/docs/markdown/repositories-file-formats.md +20 -0
  5. package/docs/markdown/repositories-providers.md +20 -10
  6. package/package.json +1 -1
  7. package/recipes/core/sous-skills/sous.recipe.yaml +8 -1
  8. package/src/commands/namespace/list.ts +42 -21
  9. package/src/commands/namespace/show.ts +32 -12
  10. package/src/commands/recipe/list.ts +40 -23
  11. package/src/commands/recipe/show.ts +28 -6
  12. package/src/commands/repo/list.ts +67 -10
  13. package/src/commands/repo/release.ts +41 -0
  14. package/src/commands/repo/search.ts +68 -15
  15. package/src/commands/repo/submit.ts +245 -35
  16. package/src/commands/subscription/list.ts +98 -19
  17. package/src/lib/build-preparation.ts +44 -1
  18. package/src/lib/repos/catalog-display.ts +101 -2
  19. package/src/lib/repos/catalog-inputs.ts +145 -17
  20. package/src/lib/repos/catalog.ts +92 -5
  21. package/src/lib/repos/formats/common.ts +20 -0
  22. package/src/lib/repos/formats/recipe-manifest.ts +7 -0
  23. package/src/lib/repos/formats/repo-manifest.ts +8 -0
  24. package/src/lib/repos/freshness.ts +56 -0
  25. package/src/lib/repos/providers/base.ts +33 -1
  26. package/src/lib/repos/providers/github.ts +276 -3
  27. package/src/lib/repos/providers/gitlab.ts +1 -0
  28. package/src/lib/repos/providers/http.ts +7 -2
  29. package/src/lib/repos/providers/index-cache.ts +56 -17
  30. package/src/lib/repos/providers/provider.ts +121 -3
  31. package/src/lib/repos/release/changelog.ts +448 -0
  32. package/src/lib/repos/release/git-state.ts +101 -15
  33. package/src/lib/repos/release/index.ts +2 -0
  34. package/src/lib/repos/release/submissions.ts +214 -0
  35. package/src/lib/repos/release/submit-checkout.ts +271 -0
  36. package/src/lib/repos/release/submit-questions.ts +153 -0
  37. package/src/lib/repos/release/submit-service.ts +581 -174
  38. package/src/lib/repos/subscription-service.ts +138 -9
  39. package/src/utils/flags.ts +24 -0
@@ -17,7 +17,8 @@ Every command that works on a project takes these four. `SOUS_CONFIG`, `SOUS_DIR
17
17
  a flag beats its variable, and both beat walk-up discovery. See [Discovery and overrides](config-discovery.md).
18
18
 
19
19
  ?> `repo init`, `repo release` and `repo submit` take none of these. They run inside a recipe repository, which
20
- has no `.sous/` directory to discover. All three still take `--non-interactive`.
20
+ has no `.sous/` directory to discover. All three still take `--non-interactive`. `repo submit` may also be run
21
+ from a project, which it finds by walking up from the working directory.
21
22
 
22
23
  ## Flags that answer questions
23
24
 
@@ -41,6 +42,22 @@ Help has four spellings. `sous --help` prints the root screen; `sous repo add --
41
42
  `sous --version` prints the version alone, as `v1.2.3`; `sous --version --verbose` adds the package name, where
42
43
  it is installed, the platform and the Node build under it.
43
44
 
45
+ ## Flags that browse
46
+
47
+ Every command that shows published versions takes the same two flags: `sous recipe list`, `sous recipe show`,
48
+ `sous namespace list`, `sous namespace show`, `sous repo list`, `sous repo search` (and `sous search`) and
49
+ `sous subscription list`. The flags combine.
50
+
51
+ | Flag | What it does |
52
+ |------|--------------|
53
+ | `--latest` | Read each repository's index from upstream instead of the cache. `--remote` is the same flag. What it fetches is never written to the cache; a repository that cannot be reached is shown from the cache and named as not checked |
54
+ | `--installed` | Show only what this project has installed, at the version its lockfile pins. A recipe whose repository is linked is marked `linked`, because builds read it from the checkout rather than the pinned version |
55
+
56
+ Without either flag a browsing command reads only the cached indexes, so it works offline and fast.
57
+ `sous recipe list --installed --latest` is the out-of-date view: each installed recipe with upstream's newest
58
+ version beside the installed one. With `--installed`, `recipe show` and `namespace show` look the reference up
59
+ among installed recipes only, and a reference to something published but not installed is an error saying so.
60
+
44
61
  Every topic answers to both spellings of its name: `repo` and `repos`, `subscription` and `subscriptions`,
45
62
  `namespace` and `namespaces`, `recipe` and `recipes`, `lock` and `locks`, `vars` and `var`, `config` and
46
63
  `configs`. Three commands also answer to one word: `sous search`, `sous subscribe` and `sous unsubscribe`.
@@ -79,6 +96,12 @@ and the command you want almost always. Takes `--dry-run`.
79
96
  - `--strict`: fail on any compilation error rather than reporting it and continuing.
80
97
  - `-w, --watch`: rebuild on every change to a source file, a config layer or a linked checkout.
81
98
 
99
+ Before it compiles, a build lists each recipe this project uses that has a newer version within the range
100
+ declared for it, beside the version pinned. It moves no pin; only always-pull moves one. The build reads upstream
101
+ for this at most once per freshness window (`store.freshnessSeconds`, five minutes by default), gives a
102
+ repository three seconds to answer, and otherwise answers from the cached index without a word about the
103
+ failed check.
104
+
82
105
  Example: `sous build --rebuild`
83
106
 
84
107
  ### `sous compile`
@@ -103,8 +126,10 @@ Example: `sous launch claude --continuous`
103
126
 
104
127
  ### `sous search TEXT`
105
128
  Searches the recipes every trusted repository publishes, by name or description; reads the cached indexes only,
106
- so it works offline. `--limit <n>` sets how many matches to show, defaulting to 25. Also spelled
107
- `sous repo search`. Example: `sous search task --limit 50`
129
+ so it works offline. `--limit <n>` sets how many matches to show, defaulting to 25. Takes the
130
+ [browsing flags](#flags-that-browse): `--latest` searches the indexes upstream serves, and `--installed` searches
131
+ only installed recipes and adds an Installed column. Also spelled `sous repo search`.
132
+ Example: `sous search task --limit 50`
108
133
 
109
134
  ### `sous help [COMMAND]`
110
135
  Prints the help for sous, or for one command or topic. Works from any directory, including one with no config
@@ -155,7 +180,9 @@ held, the files the next build prunes, and the linked checkout if one points at
155
180
  ### `sous repo list`
156
181
  Lists the repositories this project trusts, with the provider, where the entry came from, whether it is linked,
157
182
  how many recipes it publishes (`not fetched` until its index has been downloaded) and its URL. `--verbose` adds
158
- the namespaces each one publishes, on a line under its row.
183
+ the namespaces each one publishes, on a line under its row. Takes the [browsing flags](#flags-that-browse):
184
+ `--installed` keeps only the repositories something is installed from and names each installed recipe and
185
+ version on a line under its row.
159
186
 
160
187
  ```term
161
188
  $ sous repo list
@@ -165,7 +192,8 @@ $ sous repo list
165
192
  ```
166
193
 
167
194
  ### `sous repo search TEXT`
168
- Same command as `sous search`, under its own topic; takes `--limit <n>`. Example: `sous repo search browser`
195
+ Same command as `sous search`, under its own topic; takes `--limit <n>` and the
196
+ [browsing flags](#flags-that-browse). Example: `sous repo search browser --installed`
169
197
 
170
198
  ### `sous repo gc`
171
199
  Collects the machine-wide recipe store down to its size cap. `--max-bytes <n>` collects to that cap instead of
@@ -227,17 +255,32 @@ and `git config user.email`), because it commits and cuts annotated tags. Takes
227
255
  - `--no-bump`: raise nothing; a changed recipe that was never raised is then an error.
228
256
  - `--include-unchanged`: release every recipe in scope, changed or not.
229
257
  - `--tag`, `--push`: tag even on a non-default branch, and push the commit and this run's tags.
230
- - `--check`: only validate. It fails on a problem the release would refuse, and reports, without failing, how
231
- merging would rewrite the committed index.
258
+ - `--check`: only validate. It fails on a problem the release would refuse, and on a change to a recipe that
259
+ takes no proposals (see [`submissions`](repositories-file-formats.md#the-submissions-block)), and reports,
260
+ without failing, how merging would rewrite the committed index.
232
261
  - `--ci`: the merge preset. Never bump, never ask, and fail on anything unbumped. It still needs `--yes` to
233
262
  accept the plan it prints, so a merge job runs `sous repo release --ci --yes --push`.
234
263
 
235
264
  Example: `sous repo release --recipe workflow/task-files --bump minor --push`
236
265
 
237
- ### `sous repo submit`
238
- Proposes this repository's committed changes to its maintainers. `--title <text>` defaults to the last commit's
239
- subject and `--body <text>` to a summary sous writes; `--draft` opens the proposal as a draft, and `--dry-run`
240
- prints the plan without sending anything. Example: `sous repo submit --title "Add a linting recipe" --draft`
266
+ ### `sous repo submit [REPO]`
267
+ Proposes a recipe repository's changes to its maintainers, and follows the proposal through: it opens one,
268
+ updates it when there is more to send, reports where it stands, and starts the next one once it was merged. Run
269
+ inside a recipe repository it works there; run inside a project, `REPO` names a linked repository and the
270
+ submission runs in its checkout (with no `REPO`, the only linked repository is used, and several are a
271
+ question). Takes `-y, --yes` and `--dry-run`.
272
+
273
+ - `--title <text>`, `--body <text>`: the proposal's title and description. Both are required for a new
274
+ proposal, and asked for at a terminal when missing; on an open proposal they are optional and replace its own.
275
+ The body is followed by a changelog sous generates.
276
+ - `--branch <name>`: work with this branch instead of the one checked out. A branch that does not exist is
277
+ created from the current commit.
278
+ - `--status`: only report where the branch's proposal stands; nothing is checked, written or sent.
279
+ - `--commit`: commit uncommitted changes for you, after listing them and asking once, with the title, the
280
+ description and the changelog as the message.
281
+ - `--draft`: open a new proposal as a draft.
282
+
283
+ Example: `sous repo submit --title "Add a linting recipe" --body "Adds lint rules for shell scripts." --draft`
241
284
 
242
285
  ## subscription
243
286
 
@@ -265,13 +308,16 @@ Removes a subscription and everything only it brought in, then rebuilds so those
265
308
 
266
309
  ### `sous subscription list`
267
310
  Lists the subscriptions this project declares, switched-off ones included, with the range each resolves within,
268
- the versions the lockfile pins, where it came from and whether it is on. Reads the config and the lockfile only.
269
- Example: `sous subscription list`
311
+ the versions the lockfile pins, the latest version each of those recipes has published, where it came from and
312
+ whether it is on. Reads the config, the lockfile and the cached indexes only. Takes the
313
+ [browsing flags](#flags-that-browse): `--latest` reads the latest versions from upstream, and `--installed` keeps
314
+ only the subscriptions that have pinned something. Example: `sous subscription list --latest`
270
315
 
271
316
  ## namespace
272
317
 
273
318
  `namespace` reads the cached indexes and the lockfile, so it works offline. A trusted repository whose index has
274
- never been fetched is named at the end of a listing, not left out.
319
+ never been fetched is named at the end of a listing, not left out. Both commands take the
320
+ [browsing flags](#flags-that-browse); with `--installed` the recipe count is the number installed.
275
321
 
276
322
  ### `sous namespace list`
277
323
  Lists every namespace the trusted repositories publish, how many recipes each holds, and how much of it this
@@ -284,11 +330,12 @@ Example: `sous namespace show sous-recipes:core`
284
330
 
285
331
  ## recipe
286
332
 
287
- Browses the recipes the trusted repositories publish; like `namespace`, it works offline.
333
+ Browses the recipes the trusted repositories publish; like `namespace`, it works offline, and both commands take
334
+ the [browsing flags](#flags-that-browse).
288
335
 
289
336
  ### `sous recipe list`
290
337
  Lists the recipes the trusted repositories publish, across every namespace, with the same per-recipe columns
291
- `namespace show` prints. Example: `sous recipe list`
338
+ `namespace show` prints. Example: `sous recipe list --installed --latest`
292
339
 
293
340
  ### `sous recipe show REF`
294
341
  Describes one recipe completely: its repository and location, every published version, its dependencies as
@@ -4,8 +4,9 @@ The guide to publishing recipes of your own: creating a repository, writing a re
4
4
  needs, cutting a release, editing a published repository in place, and proposing a change to somebody else's.
5
5
  [Repository file formats](repositories-file-formats.md) holds the schemas; this page is the workflow.
6
6
 
7
- ?> A recipe repository is not a sous project. It has no `.sous/` directory, and `sous repo init`, `sous repo
8
- release` and `sous repo submit` do not look for one. Run them from inside the repository itself.
7
+ ?> A recipe repository is not a sous project. It has no `.sous/` directory, and `sous repo init` and `sous repo
8
+ release` do not look for one. Run them from inside the repository itself. `sous repo submit` runs there too, and
9
+ may also be run from a project that links the repository.
9
10
 
10
11
  ## Create a repository
11
12
 
@@ -196,8 +197,9 @@ third case is what a merge looks like to continuous integration: the bump is don
196
197
  `--namespace <ns>` and `--recipe <ns/name>` narrow the run and both repeat; `--bump <level>` is `patch` (the
197
198
  default), `minor`, `major` or `prerelease`; `--no-bump` raises nothing; `--include-unchanged` releases every
198
199
  recipe in scope, changed or not; `--check` only reads, validating, failing on any problem the release would
199
- refuse, and reporting how merging would rewrite the committed index without failing on it, since that index is
200
- the release's output; and `--ci` is the merge preset: never bump, accept the plan, never ask, fail on anything unbumped. Every flag
200
+ refuse and on a change to a recipe whose [`submissions`](repositories-file-formats.md#the-submissions-block)
201
+ block says it takes no proposals, and reporting how merging would rewrite the committed index without failing
202
+ on it, since that index is the release's output; and `--ci` is the merge preset: never bump, accept the plan, never ask, fail on anything unbumped. Every flag
201
203
  this command takes, `--tag`, `--push` and `--non-interactive` among them, is in the
202
204
  [command reference](commands.md#sous-repo-release).
203
205
 
@@ -235,7 +237,8 @@ missing from the index is rebuilt from it whenever the index is regenerated, whi
235
237
  publishes something. A bump edits the manifest in place, so comments, field order and layout survive; a folded
236
238
  block of YAML prose may be re-wrapped and the space before a trailing comment collapsed to one.
237
239
 
238
- !> A release commits the version bumps and the index, and nothing else. It refuses to run while anything else is
240
+ !> A release commits the version bumps and the index, and nothing else; the only other commit sous ever makes
241
+ for you is `sous repo submit --commit`. It refuses to run while anything else is
239
242
  uncommitted, because a tag names one commit and the index records what each recipe folder holds right now. It also
240
243
  refuses when git does not know who is committing: set `git config user.name` and `git config user.email` first, or
241
244
  give that identity to the account a continuous integration job runs as (the scaffolded workflow already does).
@@ -344,22 +347,60 @@ changing its branch says it affects all of them.
344
347
 
345
348
  ## Contribute to someone else's repository
346
349
 
347
- `sous repo submit` proposes your committed changes to a repository's maintainers; it never publishes and never
348
- writes to a repository directly. It takes `--title`, `--body`, `--draft` and `--dry-run`, and runs three stages,
349
- printing each step:
350
+ `sous repo submit` proposes a change to a repository's maintainers and follows it through; it never publishes
351
+ and never writes to a repository directly. Run it inside the recipe repository you changed, or from a project
352
+ that links it: `sous repo submit sous-recipes` runs in the linked checkout, and with no argument the project's
353
+ only linked repository is used (several are a question). A repository you have since unlinked is still
354
+ submitted from the checkout sous cloned for it, with a note saying so; with no checkout at all there is no
355
+ working copy to propose from, and `submit` says so.
356
+
357
+ It prints each step as it runs them:
350
358
 
351
359
  1. **Preflight.** An `origin` remote exists, sous recognizes its provider, that provider's command line tool
352
- (`gh` or `glab`) is installed and signed in, and everything is committed.
360
+ (`gh` or `glab`) is installed and signed in, and everything is committed. `--commit` lifts that last rule:
361
+ sous lists what is uncommitted, asks once (`--yes` answers), checks git knows who is committing before
362
+ writing anything, and commits it all with the proposal's title, description and changelog as the message.
353
363
  2. **Validation.** The repository validates, and your change leaves `sous.index.json` as it found it, so a
354
364
  proposal never fails the maintainer's own checks and wastes their review. The index is written by the
355
365
  repository's own release after a merge; whether it agrees with the release tags is checked there, by
356
366
  `sous repo release --check` on a full clone, not by `submit`. That is what lets `submit` run from the shallow
357
367
  checkout `sous repo link` makes, which holds almost none of the tags. The comparison is made against the
358
368
  copy of the default branch your checkout holds (`origin/main`, for example); when it holds none, `submit`
359
- says the check was skipped.
369
+ says the check was skipped. A change that touches a recipe whose
370
+ [`submissions`](repositories-file-formats.md#the-submissions-block) block says it takes no proposals is
371
+ warned about, with where to send it instead, and proposed only if you carry on.
360
372
  3. **Delegation.** Sous asks the provider whether you can push to the repository itself, forks it onto your
361
373
  account when you cannot, pushes the branch, and asks the provider to open the proposal. A change sitting on
362
- the default branch is moved to `sous/submit-<YYYYMMDD>-<HHMM>`.
374
+ the default branch is moved to `sous/submit-<YYYYMMDD>-<HHMM>`, or to the branch `--branch` names.
375
+
376
+ **A title and a description are yours to write.** A new proposal needs both: pass `--title` and `--body`, or
377
+ answer the two questions at a terminal (Tab opens your editor for a longer description). Sous never borrows a
378
+ commit message. The body is your description followed by a changelog sous generates by comparing the manifests
379
+ your change carries with the default branch:
380
+
381
+ - recipes added, and recipes retired (a renamed recipe shows as one of each);
382
+ - version changes;
383
+ - recipes whose files changed without a version raise, which merging will release as the next patch;
384
+ - namespaces added or removed;
385
+ - variables added, removed or changed, with a warning that removing a variable or tightening its validation is
386
+ usually a major change.
387
+
388
+ The changelog explains; it never refuses. Only what the repository's own checks would reject stops a submission.
389
+
390
+ **One command for the proposal's whole life.** Every run looks up the proposal for the current branch (or the
391
+ one `--branch` names), by the fork's owner as well as the branch when you work through a fork, and then:
392
+
393
+ | The branch's proposal | What `submit` does |
394
+ |-----------------------|--------------------|
395
+ | none | Opens one |
396
+ | open, with new commits | Pushes them, which updates it; a given `--title` or `--body` replaces its own |
397
+ | open, with nothing new | Reports where it stands: review, checks, whether it can merge |
398
+ | merged | Says so, then continues on a new branch: you name one, sous generates one, or you cancel. `--yes` generates one |
399
+ | closed without merging | Says so, and opens a fresh one for the branch |
400
+
401
+ When the branch on the remote holds commits yours lacks (a maintainer pushed to it), git refuses the push and
402
+ `submit` passes git's own explanation through and stops. It never forces a push. `--status` only reports, and
403
+ `--dry-run` works everything out and writes and sends nothing.
363
404
 
364
405
  Each step is the provider's own business, and what each one can do depends on the host; see
365
406
  [Providers](repositories-providers.md#proposing-a-change). Sous sequences the steps and reports what came back. A
@@ -52,6 +52,19 @@ sous namespace show workflow # one namespace and the recipes in it
52
52
  sous recipe list # every recipe, latest version, pinned version, subscribed
53
53
  ```
54
54
 
55
+ The cache can be behind what a repository has published since. Every one of these commands, plus
56
+ `sous recipe show`, `sous repo list` and `sous subscription list`, takes two flags that combine:
57
+
58
+ - `--latest` (also `--remote`) reads each index from upstream instead. Nothing it fetches is
59
+ written to the cache; only a command that resolves versions changes what the cache holds. A
60
+ repository that cannot be reached is shown from the cache and named as not checked.
61
+ - `--installed` narrows the listing to what this project has installed, at the version the
62
+ lockfile pins. A recipe from a linked repository is marked `linked`, because builds read it from
63
+ the checkout instead of that version.
64
+
65
+ `sous recipe list --installed --latest` is the out-of-date view: every installed recipe, with
66
+ upstream's newest version beside the installed one.
67
+
55
68
  Read `sous recipe show` before subscribing: every published version, what it depends on (as
56
69
  declared, beside the version the index resolved it to), and its questions and files once it has
57
70
  them here.
@@ -201,7 +214,8 @@ $ sous subscription list
201
214
 
202
215
  A namespace subscription names every recipe it holds, each with the version the lockfile pins. A
203
216
  subscription that has never been built has nothing pinned yet, and its cell reads `pinned on first
204
- build` instead.
217
+ build` instead. The `Latest version` column beside it is read from the cache, or from upstream with
218
+ `--latest`.
205
219
 
206
220
  `sous repo list` shows each trusted repository with its provider, origin, whether it is linked, its
207
221
  recipe count and its URL; `--verbose` adds a `Namespaces:` line under each row. `sous lock show`
@@ -278,6 +292,23 @@ happens after that check, not how often it happens: a repository or subscription
278
292
  `alwaysPull` takes a newer in-range version rather than the locked one; set it with
279
293
  `--always-pull`, or on either entry in the config.
280
294
 
295
+ Every other repository the lockfile pins from is checked on the same window, only to tell you what
296
+ is newer. A build lists each recipe with a newer version inside the range declared for it, beside
297
+ the version it pins, and moves nothing:
298
+
299
+ ```term
300
+ $ sous build
301
+ ▶ Newer versions published:
302
+
303
+ workflow/qa-variables: 0.2.0 this project pins 0.1.0
304
+
305
+ This version is within the range declared for the recipe. No pin was changed, so this build
306
+ uses the pinned version.
307
+ ```
308
+
309
+ That check waits at most three seconds for a repository, and a check that fails or runs out of
310
+ time is not mentioned: the cached index answers instead, and the build carries on.
311
+
281
312
  The store is machine-wide and disposable, because everything in it is re-fetchable from a
282
313
  lockfile's pins. `sous repo gc` collects it back to its size cap, evicting least recently used
283
314
  entries first, and protects everything this project's lockfile pins whatever that does to the
@@ -54,6 +54,7 @@ recipes: # every recipe folder, relative to the repos
54
54
  | `description`, `contribute` | no | A summary for anyone reading the repository, and where to send a contribution when a provider cannot support `sous repo submit` |
55
55
  | `namespaces` | yes | Keyed by namespace name; each entry takes an optional `description` |
56
56
  | `recipes` | yes | Relative paths, each holding a recipe manifest; a path listed twice is an error |
57
+ | `submissions` | no | Whether the repository's recipes take proposed changes; see [The submissions block](#the-submissions-block) |
57
58
 
58
59
  ## `sous.recipe.yaml`: the recipe manifest
59
60
 
@@ -77,11 +78,30 @@ contents: # what a subscriber actually receives
77
78
  | `description` | no | Copied into the index at release time; shown by `sous recipe list` and `sous repo search` |
78
79
  | `depends`, `subscribes` | no | Build dependencies and co-subscriptions; within either list, no entry may repeat |
79
80
  | `contents`, `variables` | no | Contents default to an empty list, which is what a curated bundle publishes; a duplicated variable name, or two definitions claiming one `env`, is an error |
81
+ | `submissions` | no | Whether this recipe takes proposed changes, winning over the repository's block; see [The submissions block](#the-submissions-block) |
80
82
 
81
83
  `contents[].kind` is `skills`, `memories`, `prompts` or `config`. The first three are written to the
82
84
  destinations named by [`recipeOutputs`](#recipeoutputs-where-the-files-land); `config` entries become config
83
85
  layers instead. `include` needs at least one glob, and both lists are recipe-relative.
84
86
 
87
+ ### The submissions block
88
+
89
+ Both manifests accept a `submissions` block. On `sous.repo.yaml` it covers every recipe in the repository; on
90
+ `sous.recipe.yaml` it covers that recipe, and wins over the repository's.
91
+
92
+ ```yaml
93
+ submissions:
94
+ allowed: false # defaults to true
95
+ instead: Propose changes in sous-io/sous, under recipes/core/sous-skills/.
96
+ ```
97
+
98
+ It exists for a recipe whose files are copied in from somewhere else: an edit merged into the copy is
99
+ overwritten by the next copy, and a version it tagged can collide with the one the real source publishes.
100
+ `sous repo submit` warns before proposing a change that touches such a recipe, prints the `instead` text, and
101
+ proposes it if the contributor carries on. `sous repo release --check` fails a pull request that changes one,
102
+ because a pull request can be opened without `submit`, and the check is the one gate every change passes. A
103
+ release itself is never restricted, so whatever publishes the recipe still releases it.
104
+
85
105
  ### Dependencies named by location
86
106
 
87
107
  `depends` and `subscribes` share one grammar in two spellings: `workflow/qa-helper` is a sibling in this
@@ -21,8 +21,12 @@ Nothing there clones a whole repository. A provider that can also carry a contri
21
21
  the **write path**: report whether its command line tool is installed and signed in, say whether
22
22
  you may push to the repository itself, fork it onto your account, and open the proposal.
23
23
 
24
- Each provider declares which of the two features, `fetch` and `submit`, it genuinely answers, and
25
- sous consults that declaration rather than a provider's name. Asking for something outside a
24
+ A provider may also answer the **proposals** path, which is what lets a submission follow a proposal
25
+ after it is opened: find the proposal a branch was pushed for, report where it stands, and replace its
26
+ title or body.
27
+
28
+ Each provider declares which of the three features, `fetch`, `submit` and `proposals`, it genuinely
29
+ answers, and sous consults that declaration rather than a provider's name. Asking for something outside a
26
30
  provider's features is refused with a sentence naming it and what it cannot do, never a crash.
27
31
 
28
32
  ## How a URL is matched
@@ -184,11 +188,15 @@ finds the index but cannot clone has only the second half missing.
184
188
  your repository, then hands the host-specific mechanics to the provider that owns its `origin`
185
189
  remote. Providers differ, and sous says so rather than pretending otherwise:
186
190
 
187
- | Provider | Tool | Push permission | Forking | Proposal |
188
- |---|---|---|---|---|
189
- | `github` | `gh` | read from GitHub, so a contributor without it is forked automatically | `gh repo fork`, with a `fork` remote added for you | pull request |
190
- | `gitlab` | `glab` | sous cannot tell, so it pushes to `origin` and says so | not done for you | merge request |
191
- | `local` | none | not applicable | not applicable | not applicable |
191
+ | Provider | Tool | Push permission | Forking | Proposal | Following it up |
192
+ |---|---|---|---|---|---|
193
+ | `github` | `gh` | read from GitHub, so a contributor without it is forked automatically | `gh repo fork`, with a `fork` remote added for you | pull request | `gh pr list`, `gh pr view` and `gh pr edit`, matching a fork's pull request by its owner |
194
+ | `gitlab` | `glab` | sous cannot tell, so it pushes to `origin` and says so | not done for you | merge request | not done: every run opens a merge request, and `--status` is refused |
195
+ | `local` | none | not applicable | not applicable | not applicable | not applicable |
196
+
197
+ **GitLab cannot follow a proposal up yet.** It does not declare `proposals`, so a submission cannot
198
+ look for a merge request that is already open; it says so, pushes, and opens one. When the branch
199
+ already has one, the push updates it and GitLab may refuse the second.
192
200
 
193
201
  **GitLab reports "cannot tell" rather than guessing.** Sous has no cheap, reliable way to ask
194
202
  whether you may push, and a wrong guess would send you down a fork path this provider cannot
@@ -297,14 +305,16 @@ subclass overrides it. Each member is documented where it lives, in
297
305
  The subclass supplies the rest:
298
306
 
299
307
  - `id`, the identifier a repository entry and a locator scheme use;
300
- - `features`, the ones it genuinely answers (`fetch`, and `submit` only if all four write calls
301
- are real);
308
+ - `features`, the ones it genuinely answers (`fetch`; `submit` only if all four write calls are
309
+ real; `proposals` only if `findProposal`, `proposalStatus` and `updateProposal` are);
302
310
  - `matches(url)` and `canonicalize(url)`, the URL half;
303
311
  - `fetchIndex(repo, options)` and `fetchRecipeTree(repo, recipePath, tag, destDir, options)`, the
304
312
  read half;
305
313
  - `cli` and `proposalNoun` when it submits, so messages can name the tool and call a proposal
306
314
  what the host calls it;
307
- - overrides of the four write-path calls when it submits.
315
+ - overrides of the four write-path calls when it submits, and of the three proposals calls when
316
+ it can follow a proposal up. Each answers with plain data (a proposal's id, address, state,
317
+ title, review and check counts), never with a host's own vocabulary.
308
318
 
309
319
  Every call takes an options object carrying the testing seams (`cwd`, `env`, `fetchImpl`, `run`),
310
320
  which is why no provider reaches for `spawn` or the global `fetch` directly and no test in this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
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",
@@ -11,7 +11,7 @@ formatVersion: 1
11
11
 
12
12
  namespace: core
13
13
  name: sous-skills
14
- version: 0.2.15
14
+ version: 0.2.17
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous
@@ -19,6 +19,13 @@ description: >-
19
19
  debugged, the .tpl. template convention and LiquidJS syntax, what an agent
20
20
  skill is, and how to create one.
21
21
 
22
+ # The copy in sous-io/sous-recipes is overwritten by every sous release, so a
23
+ # change proposed there would be lost, and a version it tagged would collide
24
+ # with the one the release publishes.
25
+ submissions:
26
+ allowed: false
27
+ instead: Propose changes in sous-io/sous, under recipes/core/sous-skills/.
28
+
22
29
  contents:
23
30
  - kind: skills
24
31
  include:
@@ -3,17 +3,25 @@
3
3
  *
4
4
  * Shows every namespace published by the repositories this project trusts, how
5
5
  * many recipes each one holds, and how much of it the project subscribes to. It
6
- * reads only the indexes sous already has on disk, so it works offline; a
7
- * repository whose index has never been fetched is named at the end rather than
8
- * being silently left out.
6
+ * reads only the indexes sous already has on disk by default, so it works
7
+ * offline; a repository whose index has never been fetched is named at the end
8
+ * rather than being silently left out. `--latest` reads the indexes from
9
+ * upstream instead, without saving them, and `--installed` narrows the listing
10
+ * to the namespaces the lockfile pins a recipe from.
9
11
  */
10
12
 
11
13
  import { BaseCommand } from "../../base-command.js";
12
14
  import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
13
- import { catalogContextFor } from "../../lib/repos/catalog-inputs.js";
14
- import { listNamespaces } from "../../lib/repos/catalog.js";
15
- import { INDENT, describeCoverage } from "../../lib/repos/catalog-display.js";
15
+ import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
16
+ import { listNamespaces, narrowToInstalled } from "../../lib/repos/catalog.js";
17
+ import {
18
+ INDENT,
19
+ describeCoverage,
20
+ describeIndexSource,
21
+ printBrowsingNotes,
22
+ } from "../../lib/repos/catalog-display.js";
16
23
  import { renderTable, type TableColumn } from "../../utils/table.js";
24
+ import { browsingFlags } from "../../utils/flags.js";
17
25
  import {
18
26
  blankLine,
19
27
  footer,
@@ -53,19 +61,28 @@ export default class NamespaceList extends BaseCommand {
53
61
  */
54
62
  static aliases = ["namespaces:list"];
55
63
 
56
- static examples = ["<%= config.bin %> namespace list"];
64
+ static examples = [
65
+ "<%= config.bin %> namespace list",
66
+ "<%= config.bin %> namespace list --installed",
67
+ ];
57
68
 
58
- static flags = { ...BaseCommand.baseFlags };
69
+ static flags = { ...BaseCommand.baseFlags, ...browsingFlags() };
59
70
 
60
71
  async run(): Promise<void> {
61
- await this.parse(NamespaceList);
72
+ const { flags } = await this.parse(NamespaceList);
62
73
 
63
74
  showCommandVars({
64
75
  Project: this.projectLabel,
65
76
  Config: this.configContext.configPath,
77
+ Reading: describeIndexSource(flags.latest),
78
+ ...(flags.installed ? { Showing: "only what this project has installed" } : {}),
66
79
  });
67
80
 
68
- heading("Namespaces in the repositories this project trusts");
81
+ heading(
82
+ flags.installed
83
+ ? "Namespaces this project has installed recipes from"
84
+ : "Namespaces in the repositories this project trusts"
85
+ );
69
86
 
70
87
  const service = subscriptionServiceFor({
71
88
  configContext: this.configContext,
@@ -73,13 +90,21 @@ export default class NamespaceList extends BaseCommand {
73
90
  shellEnv: this.shellEnv,
74
91
  });
75
92
 
76
- const { inputs, notFetched } = catalogContextFor({
93
+ const { inputs, notFetched, notChecked } = await loadCatalogContext({
77
94
  service,
78
95
  sousDir: this.configContext.sousDir,
79
96
  settings: this.settings,
97
+ latest: flags.latest,
80
98
  });
81
99
 
82
- const listings = listNamespaces(inputs);
100
+ // Narrowed to what is installed, a namespace's recipe count is the number
101
+ // of its recipes the project has installed, and the column says so.
102
+ const listings = listNamespaces(flags.installed ? narrowToInstalled(inputs) : inputs);
103
+ const columns = flags.installed
104
+ ? COLUMNS.map((column) =>
105
+ column.key === "recipes" ? { ...column, header: "Installed" } : column
106
+ )
107
+ : COLUMNS;
83
108
 
84
109
  blankLine();
85
110
 
@@ -88,7 +113,9 @@ export default class NamespaceList extends BaseCommand {
88
113
  inputs.repos.length === 0
89
114
  ? "Sous has read no repository index for this project, so there are no " +
90
115
  "namespaces to show."
91
- : "The repositories this project trusts publish no namespaces."
116
+ : flags.installed
117
+ ? "This project has installed no recipe from the repositories it trusts."
118
+ : "The repositories this project trusts publish no namespaces."
92
119
  );
93
120
  } else {
94
121
  const rows = listings.map((entry) => ({
@@ -99,18 +126,12 @@ export default class NamespaceList extends BaseCommand {
99
126
  description: entry.description ?? "no description published",
100
127
  }));
101
128
 
102
- for (const line of renderTable(COLUMNS, rows, { indent: INDENT })) {
129
+ for (const line of renderTable(columns, rows, { indent: INDENT })) {
103
130
  log(indent(line, INDENT));
104
131
  }
105
132
  }
106
133
 
107
- if (notFetched.length > 0) {
108
- blankLine();
109
- paragraph(
110
- `These repositories are trusted and their index has not been fetched yet, so ` +
111
- `nothing in them is listed: ${notFetched.join(", ")}.`
112
- );
113
- }
134
+ printBrowsingNotes({ notFetched, notChecked });
114
135
 
115
136
  footer();
116
137
  }