@sous-io/sous 0.2.14 → 0.2.16

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.
@@ -41,6 +41,22 @@ Help has four spellings. `sous --help` prints the root screen; `sous repo add --
41
41
  `sous --version` prints the version alone, as `v1.2.3`; `sous --version --verbose` adds the package name, where
42
42
  it is installed, the platform and the Node build under it.
43
43
 
44
+ ## Flags that browse
45
+
46
+ Every command that shows published versions takes the same two flags: `sous recipe list`, `sous recipe show`,
47
+ `sous namespace list`, `sous namespace show`, `sous repo list`, `sous repo search` (and `sous search`) and
48
+ `sous subscription list`. The flags combine.
49
+
50
+ | Flag | What it does |
51
+ |------|--------------|
52
+ | `--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 |
53
+ | `--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 |
54
+
55
+ Without either flag a browsing command reads only the cached indexes, so it works offline and fast.
56
+ `sous recipe list --installed --latest` is the out-of-date view: each installed recipe with upstream's newest
57
+ version beside the installed one. With `--installed`, `recipe show` and `namespace show` look the reference up
58
+ among installed recipes only, and a reference to something published but not installed is an error saying so.
59
+
44
60
  Every topic answers to both spellings of its name: `repo` and `repos`, `subscription` and `subscriptions`,
45
61
  `namespace` and `namespaces`, `recipe` and `recipes`, `lock` and `locks`, `vars` and `var`, `config` and
46
62
  `configs`. Three commands also answer to one word: `sous search`, `sous subscribe` and `sous unsubscribe`.
@@ -79,6 +95,12 @@ and the command you want almost always. Takes `--dry-run`.
79
95
  - `--strict`: fail on any compilation error rather than reporting it and continuing.
80
96
  - `-w, --watch`: rebuild on every change to a source file, a config layer or a linked checkout.
81
97
 
98
+ Before it compiles, a build lists each recipe this project uses that has a newer version within the range
99
+ declared for it, beside the version pinned. It moves no pin; only always-pull moves one. The build reads upstream
100
+ for this at most once per freshness window (`store.freshnessSeconds`, five minutes by default), gives a
101
+ repository three seconds to answer, and otherwise answers from the cached index without a word about the
102
+ failed check.
103
+
82
104
  Example: `sous build --rebuild`
83
105
 
84
106
  ### `sous compile`
@@ -103,8 +125,10 @@ Example: `sous launch claude --continuous`
103
125
 
104
126
  ### `sous search TEXT`
105
127
  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`
128
+ so it works offline. `--limit <n>` sets how many matches to show, defaulting to 25. Takes the
129
+ [browsing flags](#flags-that-browse): `--latest` searches the indexes upstream serves, and `--installed` searches
130
+ only installed recipes and adds an Installed column. Also spelled `sous repo search`.
131
+ Example: `sous search task --limit 50`
108
132
 
109
133
  ### `sous help [COMMAND]`
110
134
  Prints the help for sous, or for one command or topic. Works from any directory, including one with no config
@@ -155,7 +179,9 @@ held, the files the next build prunes, and the linked checkout if one points at
155
179
  ### `sous repo list`
156
180
  Lists the repositories this project trusts, with the provider, where the entry came from, whether it is linked,
157
181
  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.
182
+ the namespaces each one publishes, on a line under its row. Takes the [browsing flags](#flags-that-browse):
183
+ `--installed` keeps only the repositories something is installed from and names each installed recipe and
184
+ version on a line under its row.
159
185
 
160
186
  ```term
161
187
  $ sous repo list
@@ -165,7 +191,8 @@ $ sous repo list
165
191
  ```
166
192
 
167
193
  ### `sous repo search TEXT`
168
- Same command as `sous search`, under its own topic; takes `--limit <n>`. Example: `sous repo search browser`
194
+ Same command as `sous search`, under its own topic; takes `--limit <n>` and the
195
+ [browsing flags](#flags-that-browse). Example: `sous repo search browser --installed`
169
196
 
170
197
  ### `sous repo gc`
171
198
  Collects the machine-wide recipe store down to its size cap. `--max-bytes <n>` collects to that cap instead of
@@ -177,10 +204,30 @@ Points a repository at a working copy on this machine instead of a published ver
177
204
  clones it into `.sous/repos` and links the clone; a name or URL with a `PATH` links the checkout at that path; a
178
205
  path alone links that checkout where it is, adding the repository first if needed. Takes `--dry-run`.
179
206
 
180
- - `--global`: link for every project on this machine, sharing one checkout.
181
- - `-y, --yes`: answer the trust question a not-yet-added repository raises (also `--trust`).
182
-
183
- Example: `sous repo link sous-recipes ~/Projects/sous-recipes`
207
+ A checkout that was already on disk is fetched (a fetch changes none of its files or branches) and compared with
208
+ upstream: its branch, whether that branch is merged into the default branch, and how many commits it is behind.
209
+ When upstream cannot be reached within a few seconds, a warning gives git's reason and says since when the
210
+ checkout may have diverged, and the link is still recorded. Nothing else changes the checkout unless a flag
211
+ asks for it; git carries out each step, and a step git refuses stops the command with git's own message.
212
+
213
+ - `--global`: link for every project on this machine, sharing one checkout. Changing that checkout's branch
214
+ says it affects every project that links it.
215
+ - `--branch <name>`: switch to an existing branch, fetching it from upstream first when it is not local.
216
+ - `--create-branch <name>`: create a new branch and switch to it; git refuses a name that already exists.
217
+ - `--generate-branch`: the same, with the generated name `sous/edit-<YYYYMMDD>-<HHMM>`, which is printed.
218
+ - `--from <branch>`: the base of the new branch, fetched first. Defaults to the repository's default branch,
219
+ not whatever is checked out, and needs `--create-branch` or `--generate-branch`.
220
+ - `--latest`: make the branch being worked from (the `--branch` target, the `--from` base, or else the default
221
+ branch) match upstream's, leaving every other branch alone. What that would discard (uncommitted changes and
222
+ local commits upstream lacks) is listed first, with one question; it fails when the fetch fails.
223
+ - `-y, --yes`: answer the trust question a not-yet-added repository raises and the question `--latest` asks
224
+ (also `-f`, `--force`, `--trust`).
225
+
226
+ `--branch`, `--create-branch` and `--generate-branch` exclude each other; each flag works on a checkout linked
227
+ by path, too.
228
+
229
+ Example: `sous repo link sous-recipes ~/Projects/sous-recipes`, or `sous repo link sous-recipes
230
+ --generate-branch --latest --yes`
184
231
 
185
232
  ### `sous repo unlink REPO`
186
233
  Stops reading a repository from a working copy and goes back to published versions; `REPO` is the short name as
@@ -245,13 +292,16 @@ Removes a subscription and everything only it brought in, then rebuilds so those
245
292
 
246
293
  ### `sous subscription list`
247
294
  Lists the subscriptions this project declares, switched-off ones included, with the range each resolves within,
248
- the versions the lockfile pins, where it came from and whether it is on. Reads the config and the lockfile only.
249
- Example: `sous subscription list`
295
+ the versions the lockfile pins, the latest version each of those recipes has published, where it came from and
296
+ whether it is on. Reads the config, the lockfile and the cached indexes only. Takes the
297
+ [browsing flags](#flags-that-browse): `--latest` reads the latest versions from upstream, and `--installed` keeps
298
+ only the subscriptions that have pinned something. Example: `sous subscription list --latest`
250
299
 
251
300
  ## namespace
252
301
 
253
302
  `namespace` reads the cached indexes and the lockfile, so it works offline. A trusted repository whose index has
254
- never been fetched is named at the end of a listing, not left out.
303
+ never been fetched is named at the end of a listing, not left out. Both commands take the
304
+ [browsing flags](#flags-that-browse); with `--installed` the recipe count is the number installed.
255
305
 
256
306
  ### `sous namespace list`
257
307
  Lists every namespace the trusted repositories publish, how many recipes each holds, and how much of it this
@@ -264,11 +314,12 @@ Example: `sous namespace show sous-recipes:core`
264
314
 
265
315
  ## recipe
266
316
 
267
- Browses the recipes the trusted repositories publish; like `namespace`, it works offline.
317
+ Browses the recipes the trusted repositories publish; like `namespace`, it works offline, and both commands take
318
+ the [browsing flags](#flags-that-browse).
268
319
 
269
320
  ### `sous recipe list`
270
321
  Lists the recipes the trusted repositories publish, across every namespace, with the same per-recipe columns
271
- `namespace show` prints. Example: `sous recipe list`
322
+ `namespace show` prints. Example: `sous recipe list --installed --latest`
272
323
 
273
324
  ### `sous recipe show REF`
274
325
  Describes one recipe completely: its repository and location, every published version, its dependencies as
@@ -289,6 +289,59 @@ Run 'sous repo unlink my-recipes' to go back to published versions.
289
289
  `sous repo unlink` removes the map entry and nothing else: the checkout stays where it is, and its path is
290
290
  printed so you can delete it. Unlinking a name linked in the other scope says which scope holds it.
291
291
 
292
+ ### How a checkout compares with upstream
293
+
294
+ A link says which checkout to read, not why, so `sous repo link` never changes a checkout on its own. When the
295
+ checkout was already on disk (a clone reused from an earlier link, or one you named by path), it runs a short
296
+ `git fetch`, which updates only the remote-tracking refs, and reports what it found. The part of the output that
297
+ reports it looks like this:
298
+
299
+ ```term
300
+ $ sous repo link my-recipes
301
+ Branch : lc/my-change
302
+ Compared with : origin/main
303
+ Merged into origin/main: yes
304
+ Behind origin/main : 12 commits
305
+ ```
306
+
307
+ A branch that is merged and behind has usually been released already, and the checkout is building from an older
308
+ state than upstream's. When the fetch fails or takes longer than a few seconds, the link is still recorded, and
309
+ a warning gives git's reason and says since when the checkout may have diverged (the last time it was fetched).
310
+ The fetch is skipped for a checkout sous has just cloned, and a checkout with no `origin` remote says it has no
311
+ upstream to compare with.
312
+
313
+ ### Choosing the branch
314
+
315
+ Every change to the checkout is a flag, and git carries each one out. When git refuses a step (a conflict with
316
+ uncommitted changes, a branch that already exists, one that does not), the command stops and shows git's message
317
+ under a line naming the step. Each flag works on a checkout linked by path, too.
318
+
319
+ ```bash
320
+ sous repo link my-recipes --branch lc/their-change # read from an existing branch
321
+ sous repo link my-recipes --create-branch my-change # start a new branch
322
+ sous repo link my-recipes --generate-branch # start one named sous/edit-<YYYYMMDD>-<HHMM>
323
+ sous repo link my-recipes --generate-branch --from next # start it from another branch
324
+ sous repo link my-recipes --latest # bring the default branch up to upstream's
325
+ ```
326
+
327
+ - `--branch <name>` switches with `git switch`, which also checks out a branch that exists only upstream. A clone
328
+ sous makes holds only the default branch, so a branch that is not local is fetched first and added to the
329
+ branches `origin` is fetched for.
330
+ - `--create-branch <name>` and `--generate-branch` create a new branch and switch to it; the generated name is
331
+ printed. The new branch starts from upstream's copy of `--from`, which defaults to the repository's default
332
+ branch rather than whatever is checked out, so a new branch never silently stacks on an old, already-merged
333
+ one. `--from` is an error without one of the two.
334
+ - `--branch`, `--create-branch` and `--generate-branch` exclude each other.
335
+ - `--latest` means upstream's latest version of what you are working from: the `--branch` target, the `--from`
336
+ base, or otherwise the default branch. It fetches that branch, switches to it and makes it match upstream's,
337
+ and leaves every other branch alone. Making a branch match upstream discards local work without git warning
338
+ about it, so this is the one place sous checks for itself: uncommitted changes to tracked files and local
339
+ commits upstream lacks are listed first, with one question. `--yes` answers it, and a run with no terminal
340
+ fails naming that flag. When the fetch fails, `--latest` fails, with git's reason.
341
+
342
+ A `--global` link's checkout is shared by every project on the machine that links the repository globally, so
343
+ changing its branch says it affects all of them.
344
+
292
345
  ## Contribute to someone else's repository
293
346
 
294
347
  `sous repo submit` proposes your committed changes to a repository's maintainers; it never publishes and never
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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.14
14
+ version: 0.2.16
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous
@@ -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
  }
@@ -4,23 +4,28 @@
4
4
  * Shows one namespace in full: the repository publishing it, what that
5
5
  * repository says it is for, how much of it this project subscribes to, and
6
6
  * every recipe in it with its latest version, its pinned version and its own
7
- * subscription state. It reads only the indexes sous already has on disk.
7
+ * subscription state. It reads only the indexes sous already has on disk,
8
+ * unless `--latest` asks it to read upstream (which saves nothing); `--installed`
9
+ * narrows the recipes to the ones the lockfile pins.
8
10
  */
9
11
 
10
12
  import { Args } from "@oclif/core";
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 { describeNamespace } from "../../lib/repos/catalog.js";
15
+ import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
16
+ import { describeInstalled, describeNamespace } from "../../lib/repos/catalog.js";
15
17
  import {
16
18
  INDENT,
17
- RECIPE_COLUMNS,
18
19
  describeCoverage,
20
+ describeIndexSource,
19
21
  factIf,
22
+ printBrowsingNotes,
20
23
  printFacts,
24
+ recipeColumns,
21
25
  recipeRows,
22
26
  } from "../../lib/repos/catalog-display.js";
23
27
  import { renderTable } from "../../utils/table.js";
28
+ import { browsingFlags } from "../../utils/flags.js";
24
29
  import {
25
30
  blankLine,
26
31
  footer,
@@ -53,15 +58,17 @@ export default class NamespaceShow extends BaseCommand {
53
58
  }),
54
59
  };
55
60
 
56
- static flags = { ...BaseCommand.baseFlags };
61
+ static flags = { ...BaseCommand.baseFlags, ...browsingFlags() };
57
62
 
58
63
  async run(): Promise<void> {
59
- const { args } = await this.parse(NamespaceShow);
64
+ const { args, flags } = await this.parse(NamespaceShow);
60
65
 
61
66
  showCommandVars({
62
67
  Project: this.projectLabel,
63
68
  Config: this.configContext.configPath,
64
69
  Namespace: args.ref,
70
+ Reading: describeIndexSource(flags.latest),
71
+ ...(flags.installed ? { Showing: "only what this project has installed" } : {}),
65
72
  });
66
73
 
67
74
  const service = subscriptionServiceFor({
@@ -70,13 +77,16 @@ export default class NamespaceShow extends BaseCommand {
70
77
  shellEnv: this.shellEnv,
71
78
  });
72
79
 
73
- const { inputs } = catalogContextFor({
80
+ const { inputs, notChecked } = await loadCatalogContext({
74
81
  service,
75
82
  sousDir: this.configContext.sousDir,
76
83
  settings: this.settings,
84
+ latest: flags.latest,
77
85
  });
78
86
 
79
- const detail = describeNamespace(inputs, args.ref);
87
+ const detail = flags.installed
88
+ ? describeInstalled(inputs, args.ref, describeNamespace, "namespace")
89
+ : describeNamespace(inputs, args.ref);
80
90
 
81
91
  heading(`The namespace ${detail.namespace}`);
82
92
  blankLine();
@@ -85,7 +95,10 @@ export default class NamespaceShow extends BaseCommand {
85
95
  { label: "Repository", lines: [detail.repo] },
86
96
  ...factIf("Location", detail.repoUrl),
87
97
  ...factIf("About", detail.description),
88
- { label: "Recipes", lines: [String(detail.recipes.length)] },
98
+ {
99
+ label: flags.installed ? "Installed recipes" : "Recipes",
100
+ lines: [String(detail.recipes.length)],
101
+ },
89
102
  { label: "Subscribed", lines: [describeCoverage(detail.subscribed)] },
90
103
  ]);
91
104
 
@@ -99,12 +112,19 @@ export default class NamespaceShow extends BaseCommand {
99
112
  return;
100
113
  }
101
114
 
102
- for (const line of renderTable(RECIPE_COLUMNS, recipeRows(detail.recipes), {
103
- indent: INDENT,
104
- })) {
115
+ for (const line of renderTable(
116
+ recipeColumns({ installed: flags.installed }),
117
+ recipeRows(detail.recipes),
118
+ { indent: INDENT }
119
+ )) {
105
120
  log(indent(line, INDENT));
106
121
  }
107
122
 
123
+ printBrowsingNotes({
124
+ notChecked: notChecked.filter((name) => name === detail.repo),
125
+ anyLinked: detail.recipes.some((entry) => entry.linkedPath !== undefined),
126
+ });
127
+
108
128
  footer();
109
129
  }
110
130
  }
@@ -3,21 +3,26 @@
3
3
  *
4
4
  * Shows every recipe the repositories this project trusts publish, with the
5
5
  * latest published version, the version this project pins, and whether it is
6
- * subscribed. It reads only the indexes sous already has on disk, 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.
6
+ * subscribed. By default it reads only the indexes sous already has on disk, so
7
+ * it works offline; a repository whose index has never been fetched is named at
8
+ * the end rather than being silently left out. `--latest` reads the indexes
9
+ * from upstream instead, without saving them, and `--installed` narrows the
10
+ * listing to what the lockfile pins.
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 { listRecipes } from "../../lib/repos/catalog.js";
15
+ import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
16
+ import { listRecipes, narrowToInstalled } from "../../lib/repos/catalog.js";
15
17
  import {
16
18
  INDENT,
17
- RECIPE_COLUMNS,
19
+ describeIndexSource,
20
+ printBrowsingNotes,
21
+ recipeColumns,
18
22
  recipeRows,
19
23
  } from "../../lib/repos/catalog-display.js";
20
24
  import { renderTable } from "../../utils/table.js";
25
+ import { browsingFlags } from "../../utils/flags.js";
21
26
  import {
22
27
  blankLine,
23
28
  footer,
@@ -37,19 +42,28 @@ export default class RecipeList extends BaseCommand {
37
42
  */
38
43
  static aliases = ["recipes:list"];
39
44
 
40
- static examples = ["<%= config.bin %> recipe list"];
45
+ static examples = [
46
+ "<%= config.bin %> recipe list",
47
+ "<%= config.bin %> recipe list --installed --latest",
48
+ ];
41
49
 
42
- static flags = { ...BaseCommand.baseFlags };
50
+ static flags = { ...BaseCommand.baseFlags, ...browsingFlags() };
43
51
 
44
52
  async run(): Promise<void> {
45
- await this.parse(RecipeList);
53
+ const { flags } = await this.parse(RecipeList);
46
54
 
47
55
  showCommandVars({
48
56
  Project: this.projectLabel,
49
57
  Config: this.configContext.configPath,
58
+ Reading: describeIndexSource(flags.latest),
59
+ ...(flags.installed ? { Showing: "only what this project has installed" } : {}),
50
60
  });
51
61
 
52
- heading("Recipes in the repositories this project trusts");
62
+ heading(
63
+ flags.installed
64
+ ? "Recipes this project has installed"
65
+ : "Recipes in the repositories this project trusts"
66
+ );
53
67
 
54
68
  const service = subscriptionServiceFor({
55
69
  configContext: this.configContext,
@@ -57,13 +71,14 @@ export default class RecipeList extends BaseCommand {
57
71
  shellEnv: this.shellEnv,
58
72
  });
59
73
 
60
- const { inputs, notFetched } = catalogContextFor({
74
+ const { inputs, notFetched, notChecked } = await loadCatalogContext({
61
75
  service,
62
76
  sousDir: this.configContext.sousDir,
63
77
  settings: this.settings,
78
+ latest: flags.latest,
64
79
  });
65
80
 
66
- const listings = listRecipes(inputs);
81
+ const listings = listRecipes(flags.installed ? narrowToInstalled(inputs) : inputs);
67
82
 
68
83
  blankLine();
69
84
 
@@ -72,23 +87,25 @@ export default class RecipeList extends BaseCommand {
72
87
  inputs.repos.length === 0
73
88
  ? "Sous has read no repository index for this project, so there are no " +
74
89
  "recipes to show."
75
- : "The repositories this project trusts publish no recipes."
90
+ : flags.installed
91
+ ? "This project has installed no recipe from the repositories it trusts."
92
+ : "The repositories this project trusts publish no recipes."
76
93
  );
77
94
  } else {
78
- for (const line of renderTable(RECIPE_COLUMNS, recipeRows(listings), {
79
- indent: INDENT,
80
- })) {
95
+ for (const line of renderTable(
96
+ recipeColumns({ installed: flags.installed }),
97
+ recipeRows(listings),
98
+ { indent: INDENT }
99
+ )) {
81
100
  log(indent(line, INDENT));
82
101
  }
83
102
  }
84
103
 
85
- if (notFetched.length > 0) {
86
- blankLine();
87
- paragraph(
88
- `These repositories are trusted and their index has not been fetched yet, so ` +
89
- `nothing in them is listed: ${notFetched.join(", ")}.`
90
- );
91
- }
104
+ printBrowsingNotes({
105
+ notFetched,
106
+ notChecked,
107
+ anyLinked: listings.some((entry) => entry.linkedPath !== undefined),
108
+ });
92
109
 
93
110
  footer();
94
111
  }