@sous-io/sous 0.2.29 → 0.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/docs/markdown/README.md +52 -0
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +31 -6
- package/docs/markdown/repositories-authoring.md +7 -3
- package/docs/markdown/repositories-consuming.md +26 -12
- package/docs/markdown/repositories-file-formats.md +35 -5
- package/docs/markdown/repositories-variables.md +4 -1
- package/package.json +2 -1
- package/recipes/core/sous-skills/skills/about-sous-repos/SKILL.tpl.md +7 -4
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/base-command.ts +75 -2
- package/src/commands/lock/rebuild.ts +5 -4
- package/src/commands/recipe/show.ts +163 -64
- package/src/commands/update.ts +178 -0
- package/src/lib/project-install.d.mts +4 -0
- package/src/lib/project-install.mjs +46 -8
- package/src/lib/repos/catalog.ts +43 -55
- package/src/lib/repos/declarations.ts +177 -0
- package/src/lib/repos/formats/common.ts +14 -0
- package/src/lib/repos/formats/index-file.ts +40 -1
- package/src/lib/repos/formats/lockfile.ts +5 -3
- package/src/lib/repos/formats/recipe-manifest.ts +6 -204
- package/src/lib/repos/formats/variable-definition.ts +268 -0
- package/src/lib/repos/providers/http.ts +37 -16
- package/src/lib/repos/release/index-builder.ts +168 -32
- package/src/lib/repos/resolver.ts +53 -12
- package/src/lib/repos/subscription-service.ts +93 -11
- package/src/lib/repos/update-plan.ts +3 -2
- package/src/lib/self-update/index.ts +14 -0
- package/src/lib/self-update/installs.ts +412 -0
- package/src/lib/self-update/managers.ts +126 -0
- package/src/lib/self-update/plan.ts +315 -0
- package/src/lib/self-update/registry.ts +99 -0
- package/src/lib/self-update/run.ts +506 -0
- package/src/lib/self-update/versions.ts +243 -0
- package/src/lib/vars/question-plan.ts +44 -21
package/README.md
CHANGED
|
@@ -40,6 +40,18 @@ output. When the two versions differ, one line on standard error names the versi
|
|
|
40
40
|
to; `--verbose` announces every hand-off and adds where both installs are. Set
|
|
41
41
|
`SOUS_NO_DELEGATE=1` to run the copy you invoked instead.
|
|
42
42
|
|
|
43
|
+
To update, run `sous update`. It updates the global install, the project's own install, or both
|
|
44
|
+
(whichever this directory has), through the package manager that installed each one (npm, pnpm or
|
|
45
|
+
Yarn), and asks before each one. By default each install moves to the newest version in its current
|
|
46
|
+
major; `--major` goes past it, `--version <version|range|tag>` picks one (a downgrade included),
|
|
47
|
+
`--global` and `--project` narrow the run, `--yes` answers every question and `--dry-run` only
|
|
48
|
+
prints the plan. After a project update, the new copy builds the project, so its lockfile moves to
|
|
49
|
+
the new version too.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
sous update --dry-run # show what would be installed where, and install nothing
|
|
53
|
+
```
|
|
54
|
+
|
|
43
55
|
Or run it from a clone (useful when developing sous itself):
|
|
44
56
|
|
|
45
57
|
```bash
|
package/docs/markdown/README.md
CHANGED
|
@@ -59,6 +59,58 @@ $ SOUS_NO_DELEGATE=1 sous --version
|
|
|
59
59
|
v0.3.0
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
### Updating
|
|
63
|
+
|
|
64
|
+
`sous update` moves each copy of sous to a newer version (or one you choose) through the package
|
|
65
|
+
manager that installed it, so you never need to remember which one that was or its syntax:
|
|
66
|
+
|
|
67
|
+
- **Outside a project**, or in a project that does not install sous, it updates the global install.
|
|
68
|
+
Each of npm, pnpm and Yarn classic is asked where its global packages live, and every global
|
|
69
|
+
install found is updated with its own manager (`npm i -g`, `pnpm add -g`, `yarn global add`).
|
|
70
|
+
- **In a project that installs sous**, it updates the project's install with the project's own
|
|
71
|
+
manager (`npm i -D -E`, `pnpm add -D -E`, `yarn add -D -E`), which pins the exact version in
|
|
72
|
+
`package.json` and updates the lockfile. A dependency declared under `dependencies` stays there.
|
|
73
|
+
It updates the global install as well when there is one. A Yarn Plug'n'Play project, which has
|
|
74
|
+
no `node_modules`, counts as installing sous when its `package.json` declares it and a `.pnp.cjs`
|
|
75
|
+
sits beside it or above it; the installed version is read from `yarn.lock`.
|
|
76
|
+
- **After a project update**, the newly installed copy builds the project, so
|
|
77
|
+
`.sous/sous.lock.json` pins the core recipe at the new version. Under Yarn Plug'n'Play, Yarn
|
|
78
|
+
runs that build (`yarn run --binaries-only sous build`). `--no-build` skips it.
|
|
79
|
+
|
|
80
|
+
Under Yarn Plug'n'Play, sous runs only from a copy Yarn has unpacked; from inside Yarn's zip cache
|
|
81
|
+
it fails with `EBADF: bad file descriptor`. Unpack it in `package.json`, then run `yarn install`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
"dependenciesMeta": { "@sous-io/sous": { "unplugged": true } }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`update` always runs in the copy you invoked, never handed off to the project's own; a project copy
|
|
88
|
+
may be too old to have the command. A project config this copy cannot load (one written for a newer
|
|
89
|
+
sous, say) is a warning rather than a stop, since installing a newer copy may be the fix.
|
|
90
|
+
|
|
91
|
+
Every plan is worked out before anything is installed, and each install prints its plan and asks
|
|
92
|
+
its own question. Each is judged on its own version:
|
|
93
|
+
|
|
94
|
+
- By default, the newest published version in the install's current major (for a 0.x install,
|
|
95
|
+
anything below 1.0.0). A default run never downgrades.
|
|
96
|
+
- `--major`: the newest published version, whatever its major.
|
|
97
|
+
- `--version <version|range|tag>`: exactly that version, the newest one in that range, or the one a
|
|
98
|
+
dist-tag such as `next` points at. This is how you downgrade, and the plan says when it is one.
|
|
99
|
+
- `--prerelease` counts prerelease versions, and is on by default when the installed version is a
|
|
100
|
+
prerelease; `--no-prerelease` switches it off.
|
|
101
|
+
- `--global` or `--project` updates only that install; `-y, --yes` answers every question;
|
|
102
|
+
`--dry-run` prints the plans, with the command each would run, and installs nothing.
|
|
103
|
+
|
|
104
|
+
A copy run through `npx`, or one managed by Volta or by a manager sous does not drive, is listed
|
|
105
|
+
with the command to run by hand, and the rest are still updated. A package manager's own failure
|
|
106
|
+
is printed as the manager reported it; sous never retries with `sudo`.
|
|
107
|
+
|
|
108
|
+
```term
|
|
109
|
+
$ sous update --dry-run
|
|
110
|
+
// every install's plan, and nothing installed
|
|
111
|
+
$ sous update --global --yes
|
|
112
|
+
```
|
|
113
|
+
|
|
62
114
|
## Where to look
|
|
63
115
|
|
|
64
116
|
- Watch the [animated introduction](../) for the full pitch
|
|
@@ -28,3 +28,5 @@
|
|
|
28
28
|
- [0007: Frozen index entries and tolerant readers](adrs/0007-frozen-index-dependencies.md)
|
|
29
29
|
- [0008: One ref parser, told where the ref came from](adrs/0008-one-ref-parser.md)
|
|
30
30
|
- [0009: One whole build, everywhere](adrs/0009-whole-build-everywhere.md)
|
|
31
|
+
- [0010: The index describes each version's dependencies and questions](adrs/0010-index-describes-recipes.md)
|
|
32
|
+
- [0011: `sous update` updates sous through the package managers](adrs/0011-self-update.md)
|
|
@@ -146,6 +146,27 @@ so it works offline. `--limit <n>` sets how many matches to show, defaulting to
|
|
|
146
146
|
only installed recipes and adds an Installed column. Also spelled `sous repo search`.
|
|
147
147
|
Example: `sous search task --limit 50`
|
|
148
148
|
|
|
149
|
+
### `sous update`
|
|
150
|
+
Updates sous itself: the global install, this project's own install, or both, each through the package manager
|
|
151
|
+
that installed it. It works outside a project too, and it always runs in the copy you invoked, never handed off
|
|
152
|
+
to the project's own. Every plan is printed before anything is installed, and each install asks its own
|
|
153
|
+
question. After a project update the new copy builds the project, so the lockfile pins the core recipe at the new
|
|
154
|
+
version. A project config this copy cannot load is one warning, not a stop. Takes `-y, --yes` and `--dry-run`
|
|
155
|
+
(which also prints the command each install would run). See [Updating](README.md#updating).
|
|
156
|
+
|
|
157
|
+
- `--major`: install the newest published version, whatever its major. By default each install moves to the
|
|
158
|
+
newest version in its current major, and never down.
|
|
159
|
+
- `--version <version|range|tag>`: install exactly this version, the newest in this range, or the one this
|
|
160
|
+
dist-tag names. This is how you downgrade.
|
|
161
|
+
- `--prerelease`, `--no-prerelease`: count prerelease versions as candidates, or not. On by default when the
|
|
162
|
+
installed version is a prerelease. `--pre` is the same flag.
|
|
163
|
+
- `--global`, `--project`: update only that install.
|
|
164
|
+
- `--no-build`: update the project install without building the project afterwards.
|
|
165
|
+
|
|
166
|
+
`--major` and `--version` exclude each other, and so do `--global` and `--project`.
|
|
167
|
+
|
|
168
|
+
Example: `sous update --version next --dry-run`
|
|
169
|
+
|
|
149
170
|
### `sous help [COMMAND]`
|
|
150
171
|
Prints the help for sous, or for one command or topic. Works from any directory, including one with no config
|
|
151
172
|
above it. Example: `sous help repo add`
|
|
@@ -347,8 +368,9 @@ optionally carrying an `@<range>` and a `repo:` qualifier; see
|
|
|
347
368
|
### `sous subscription add REF`
|
|
348
369
|
Subscribes this project to a recipe, or to a whole namespace of them, then builds the project so the recipe's
|
|
349
370
|
files are on disk when the command returns. Also spelled `sous subscribe`. Takes `-y, --yes`, `--no-build`, and
|
|
350
|
-
`--dry-run`, which also prints the
|
|
351
|
-
|
|
371
|
+
`--dry-run`, which also prints every question the recipes would ask. A dry run downloads nothing, so a recipe
|
|
372
|
+
not on this machine is described from its repository's index; one whose index entry predates the release that
|
|
373
|
+
began recording questions there is named instead.
|
|
352
374
|
|
|
353
375
|
- `--prerelease`: let prerelease versions take part in range matching.
|
|
354
376
|
- `--always-pull`: install a newer in-range version whenever one exists, rather than holding the lock.
|
|
@@ -409,10 +431,13 @@ Lists the recipes the trusted repositories publish, across every namespace, with
|
|
|
409
431
|
`namespace show` prints. Example: `sous recipe list --installed --latest`
|
|
410
432
|
|
|
411
433
|
### `sous recipe show REF`
|
|
412
|
-
Describes one recipe completely: its repository and location, every published version, its dependencies
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
434
|
+
Describes one recipe completely: its repository and location, every published version, its dependencies (the
|
|
435
|
+
manifest entry bringing each one in, such as a whole namespace, whether it is a co-subscription or a build
|
|
436
|
+
dependency, and the version it was released against), every recipe a subscription to it would install, every
|
|
437
|
+
question that would ask, laid out as `sous subscription add --dry-run` lays them out, and the directories its
|
|
438
|
+
files are written into. Everything but the files comes from the index, so a recipe nothing has installed yet
|
|
439
|
+
is described in full, offline. `REF` is `namespace/recipe`, a recipe name alone, or either with a
|
|
440
|
+
`repository:` qualifier. Example: `sous recipe show omakase/house`
|
|
416
441
|
|
|
417
442
|
## lock
|
|
418
443
|
|
|
@@ -243,9 +243,13 @@ this release's scope** is fine: the release depends on the last published versio
|
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
Each version's resolved dependencies are written into the index, so a consumer installing that version gets what
|
|
246
|
-
it was published with rather than ranges re-resolved months later.
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
it was published with rather than ranges re-resolved months later. Each dependency also records the manifest
|
|
247
|
+
entry that brought it in and whether it is a co-subscription or a build dependency, and each version records the
|
|
248
|
+
variable definitions its manifest publishes, so `sous recipe show` and `sous subscription add --dry-run` can say
|
|
249
|
+
what a recipe installs and asks before anything is fetched. A published version never changes: its content
|
|
250
|
+
hash, its dependencies and its variable definitions are carried forward exactly as published, and a
|
|
251
|
+
disagreement is an error telling you to bump rather than republish
|
|
252
|
+
([what is checked](repositories-file-formats.md#sousindexjson)). A version counts
|
|
249
253
|
as published once its tag exists, and the tags are the backstop, so a tagged version missing from the index is
|
|
250
254
|
rebuilt from it, dependencies included, whenever the index is regenerated, which only happens on a run that
|
|
251
255
|
publishes something. A bump edits the manifest in place, so comments, field order and layout survive; a folded
|
|
@@ -65,22 +65,35 @@ The cache can be behind what a repository has published since. Every one of thes
|
|
|
65
65
|
`sous recipe list --installed --latest` is the out-of-date view: every installed recipe, with
|
|
66
66
|
upstream's newest version beside the installed one.
|
|
67
67
|
|
|
68
|
-
Read `sous recipe show` before subscribing
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
Read `sous recipe show` before subscribing. It works offline, from the index alone: every published
|
|
69
|
+
version, what the version depends on (the manifest entry that brings each dependency in, whether it
|
|
70
|
+
is a co-subscription whose files land here or a build dependency, and the version it was released
|
|
71
|
+
against), every recipe a subscription would install, and every question that would ask. Only the
|
|
72
|
+
files it publishes wait until it is installed. A preset set, a recipe that only bundles others,
|
|
73
|
+
reads like this:
|
|
71
74
|
|
|
72
75
|
```term
|
|
73
|
-
$ sous recipe show
|
|
74
|
-
Latest version :
|
|
76
|
+
$ sous recipe show omakase/house
|
|
77
|
+
Latest version : 1.0.0
|
|
75
78
|
Pinned version : this project pins none
|
|
76
79
|
Subscribed : no
|
|
77
|
-
Dependency
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
Dependency Declared as Resolved to Repository Kind
|
|
81
|
+
--------------- ------------------------------ ----------- --------------- ----------------
|
|
82
|
+
tools/lint tools/lint 1.0.0 this repository build dependency
|
|
83
|
+
workflow/notes the whole 'workflow' namespace 1.0.0 this repository co-subscription
|
|
84
|
+
workflow/review the whole 'workflow' namespace 1.0.0 this repository co-subscription
|
|
85
|
+
➔ What subscribing asks you:
|
|
86
|
+
These recipes ask 3 questions, 3 of which nothing answers yet.
|
|
87
|
+
workflow/notes asks 1 question:
|
|
88
|
+
notesDir
|
|
89
|
+
about : Where the notes skill keeps its notes.
|
|
90
|
+
stored-as : SOUS_VAR_NOTES_DIR
|
|
91
|
+
answered : no, and this recipe requires an answer
|
|
82
92
|
```
|
|
83
93
|
|
|
94
|
+
?> A recipe released before sous recorded questions in the index is described from its files
|
|
95
|
+
instead, so its questions are listed once it is installed, and named as unknown until then.
|
|
96
|
+
|
|
84
97
|
## Subscribe to a recipe
|
|
85
98
|
|
|
86
99
|
```bash
|
|
@@ -206,8 +219,9 @@ stored value nothing reads. Names are camelCase, as the recipe declares them; th
|
|
|
206
219
|
`namespace/recipe.name` key works too when two recipes publish the same name, and everything after
|
|
207
220
|
the first `=` is the answer. `--answers-file answers.yaml` reads a YAML or JSON file of them.
|
|
208
221
|
|
|
209
|
-
?> A dry run downloads nothing
|
|
210
|
-
|
|
222
|
+
?> A dry run downloads nothing. A recipe your machine does not hold yet is described from the
|
|
223
|
+
questions its repository's index records; one released before sous recorded questions there cannot
|
|
224
|
+
be, so the run still succeeds and names it.
|
|
211
225
|
|
|
212
226
|
## See what you have
|
|
213
227
|
|
|
@@ -201,8 +201,16 @@ fetches only this file; nothing more is downloaded until a project subscribes.
|
|
|
201
201
|
"namespaces": { "quality": { "description": "Recipes that exercise the edges." } },
|
|
202
202
|
"recipes": { "quality/qa-remote-dep": { "path": "recipes/quality/qa-remote-dep",
|
|
203
203
|
"versions": { "0.1.0": {
|
|
204
|
-
"dependencies": {
|
|
205
|
-
"workflow/
|
|
204
|
+
"dependencies": {
|
|
205
|
+
"workflow/qa-helper": { "version": "0.1.0", "declared": "workflow", "kind": "subscribes" },
|
|
206
|
+
"workflow/sub-agent-delegation": { "range": "^1.0", "repo": "github.com/sous-io/sous-recipes",
|
|
207
|
+
"declared": "github://sous-io/sous-recipes/workflow/sub-agent-delegation@^1.0",
|
|
208
|
+
"kind": "depends" } },
|
|
209
|
+
"depends": [ "github://sous-io/sous-recipes/workflow/sub-agent-delegation@^1.0" ],
|
|
210
|
+
"subscribes": [ "workflow" ],
|
|
211
|
+
"variables": [ { "name": "qaNotesDir", "type": "path", "prompt": "Where should notes go?",
|
|
212
|
+
"description": "The directory QA notes are written to.", "example": "docs/qa",
|
|
213
|
+
"default": ".sous/qa-notes", "required": true, "secret": false, "scope": "shared" } ],
|
|
206
214
|
"hash": "sha256-46e75442aeb368116c8830ad382707759f1ecd03974c4b5d42a5fabffce0324f",
|
|
207
215
|
"prerelease": false, "releasedAt": "2026-09-12T07:06:33.236Z",
|
|
208
216
|
"tag": "quality/qa-remote-dep@0.1.0"
|
|
@@ -216,12 +224,30 @@ fetches only this file; nothing more is downloaded until a project subscribes.
|
|
|
216
224
|
| `recipes` | top level | Keyed `namespace/recipe`; a recipe whose namespace is not declared is an error. Each entry carries `path` (the recipe folder), `description`, and `versions`, keyed by exact version |
|
|
217
225
|
| `hash`, `tag` | per version | The content hash verified after every fetch, and the tag, which must be exactly `namespace/recipe@version` so a version can never point at a branch |
|
|
218
226
|
| `prerelease`, `releasedAt`, `seeded` | per version | Whether ranges skip it unless a subscription opts in, when it was released, and whether it is the copy sous folds in from its own package |
|
|
227
|
+
| `dependencies` | per version | What the version was released against, keyed `namespace/recipe`; see below |
|
|
228
|
+
| `variables` | per version | The version's variable definitions, exactly as its manifest publishes them (see [Variable definitions](#variable-definitions)), with `required`, `secret` and `scope` filled in; `[]` when it asks nothing |
|
|
229
|
+
| `depends`, `subscribes` | per version | The version's manifest lists, exactly as written; `[]` when empty |
|
|
219
230
|
|
|
220
231
|
Dependencies are resolved at release time and keyed `namespace/recipe`. An entry carries `version` (a
|
|
221
232
|
sibling, resolved exactly) or `range` plus `repo` (the identity of the repository publishing it, whose own
|
|
222
233
|
index resolves the range); at least one is required. Installing a version installs these rather than
|
|
223
234
|
re-resolving the manifest's ranges.
|
|
224
235
|
|
|
236
|
+
**Each version describes itself.** So that a recipe can be described before anything is fetched, every
|
|
237
|
+
version a release records carries more than its dependencies. Each dependency records `declared`, the
|
|
238
|
+
manifest entry that brings it in exactly as written (a recipe such as `workflow/qa-helper@^1.0`, a whole
|
|
239
|
+
namespace such as `workflow`, or a locator), and `kind`: `subscribes` for a co-subscription, whose files
|
|
240
|
+
land and whose questions are asked, or `depends` for a build dependency. When several entries cover one
|
|
241
|
+
recipe, an entry naming it wins over a namespace, and it is a co-subscription when any entry covering it is
|
|
242
|
+
one. The version records `variables`, its manifest's definitions, and `depends` and `subscribes`, its
|
|
243
|
+
manifest's lists as written; the lists are what a consumer walks, because a namespace of another repository
|
|
244
|
+
has no recipe key to record it under. All of them are written even when empty (`"dependencies": {}`,
|
|
245
|
+
`"variables": []`), so "none" reads differently from "not recorded". From these, `sous recipe show` and
|
|
246
|
+
`sous subscription add --dry-run` list everything a subscription installs and every question it asks, and
|
|
247
|
+
the resolver walks a recipe whose files are not on the machine from its index entry just as it would walk
|
|
248
|
+
its manifest. A version recorded before a release wrote these fields keeps its entry as it is; its questions
|
|
249
|
+
are known only once its files are fetched, and the dry run names it as such.
|
|
250
|
+
|
|
225
251
|
**A published version's entry is frozen.** Its dependencies are resolved once, when the version is first
|
|
226
252
|
recorded, and every later release carries the entry forward exactly as published, like its hash; releasing
|
|
227
253
|
other recipes, adding a recipe to a namespace it subscribes to, or raising a sibling it names changes nothing
|
|
@@ -234,9 +260,13 @@ against the repository as it stood at that tag, then frozen the same way. An ent
|
|
|
234
260
|
recorded dependencies carries none, stays that way, and installs by resolving its manifest's ranges.
|
|
235
261
|
|
|
236
262
|
**Fields this sous does not know are kept, not refused.** A later sous may add fields to the index, at any
|
|
237
|
-
level. This one reads such an index, validates every field it
|
|
238
|
-
rest; a release it runs carries a published version's entry
|
|
239
|
-
|
|
263
|
+
level, recorded variable definitions included. This one reads such an index, validates every field it
|
|
264
|
+
defines as strictly as before, and ignores the rest; a release it runs carries a published version's entry
|
|
265
|
+
forward with those fields intact.
|
|
266
|
+
|
|
267
|
+
!> A sous older than 0.2.26, the first to tolerate unknown fields, refuses the whole index of any
|
|
268
|
+
repository released with `declared`, `kind`, `variables`, `depends` or `subscribes` in it, the official
|
|
269
|
+
one included. A project pinned to such a sous fails to read that index until it moves to 0.2.26 or later.
|
|
240
270
|
|
|
241
271
|
**The `seeded` field.** Sous ships the `core` recipe in its own npm package, so a project can build before
|
|
242
272
|
reaching the network. That copy is folded into the official repository's index in memory and resolved like
|
|
@@ -12,7 +12,10 @@ a separate, ceremony-free system; see [Variables](config-variables.md). The two
|
|
|
12
12
|
|
|
13
13
|
A definition ships inside the recipe that needs it, in its manifest's `variables:` array, so it versions,
|
|
14
14
|
resolves, pins and trusts like the rest of the recipe; every field it can carry is listed in
|
|
15
|
-
[Variable definitions](repositories-file-formats.md#variable-definitions).
|
|
15
|
+
[Variable definitions](repositories-file-formats.md#variable-definitions). `sous repo release` also copies
|
|
16
|
+
each version's definitions into the repository's index, which is how `sous recipe show` and
|
|
17
|
+
`sous subscription add --dry-run` list a recipe's questions before its files are on your machine. The index
|
|
18
|
+
holds definitions only, never an answer; answers come from the ladder below and nowhere else.
|
|
16
19
|
|
|
17
20
|
A definition is in play when your lockfile pins the recipe that declares it, whether you subscribed to that
|
|
18
21
|
recipe or a dependency pulled it in. A recipe the store does not hold yet contributes nothing rather than
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sous-io/sous",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
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",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"jsonc-parser": "^3.3.1",
|
|
61
61
|
"liquidjs": "^10.25.0",
|
|
62
62
|
"minimatch": "^10.2.4",
|
|
63
|
+
"package-manager-detector": "^1.8.0",
|
|
63
64
|
"semver": "^7.8.5",
|
|
64
65
|
"tiktoken": "^1.0.22",
|
|
65
66
|
"tsx": "^4",
|
|
@@ -65,10 +65,13 @@ contribute: https://github.com/harbor-labs/team-recipes/blob/main/CONTRIBUTING.m
|
|
|
65
65
|
A recipe's version lives in its manifest, which is the source of truth. A version is **published**
|
|
66
66
|
once a git tag shaped `namespace/recipe@version` exists for it (for example
|
|
67
67
|
`review/checklist@1.2.0`). The index records each published version with its content hash, its
|
|
68
|
-
tag
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
tag, the exact versions of its dependencies at release time (each with the manifest entry that
|
|
69
|
+
brought it in and whether it is a co-subscription or a build dependency) and the variable
|
|
70
|
+
definitions it publishes, which is how a project lists what a recipe installs and asks before
|
|
71
|
+
fetching it; for the length of one release commit it also records the version each manifest
|
|
72
|
+
declares, whose tag that release cuts next. A published version never changes: new files need a
|
|
73
|
+
new version, and a release refuses to publish different content under a version already tagged. A
|
|
74
|
+
lost index is rebuilt from the tags.
|
|
72
75
|
|
|
73
76
|
## Releasing
|
|
74
77
|
|
package/src/base-command.ts
CHANGED
|
@@ -88,6 +88,20 @@ export abstract class BaseCommand extends Command {
|
|
|
88
88
|
*/
|
|
89
89
|
static requiresConfig = true;
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Whether a config that cannot be located or loaded stops this command. It
|
|
93
|
+
* is false on every command but the one whose job may be to repair that
|
|
94
|
+
* config: `sous update` installs a sous that may understand a config this
|
|
95
|
+
* copy rejects (one written for a newer version, say). When true, a failure
|
|
96
|
+
* to locate or load the config becomes one warning, naming the config and
|
|
97
|
+
* quoting the error, and the run carries on with no config adopted:
|
|
98
|
+
* `hasConfig` is false, the env files are not applied, and `rejectedConfig`
|
|
99
|
+
* says where the config that failed to load was, when discovery found it.
|
|
100
|
+
* It only changes what a FAILURE does; finding no config at all is still
|
|
101
|
+
* decided by `requiresConfig`.
|
|
102
|
+
*/
|
|
103
|
+
static toleratesConfigErrors = false;
|
|
104
|
+
|
|
91
105
|
protected settings!: Settings;
|
|
92
106
|
|
|
93
107
|
/** Where the active config was found. */
|
|
@@ -103,6 +117,13 @@ export abstract class BaseCommand extends Command {
|
|
|
103
117
|
/** The full discovery result, including how the config was located. */
|
|
104
118
|
protected discovered!: DiscoveredConfig;
|
|
105
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The config discovery found but could not load, on a command that
|
|
122
|
+
* `toleratesConfigErrors`. Undefined when the config loaded, when there was
|
|
123
|
+
* none, or when the failure came before any config file was identified.
|
|
124
|
+
*/
|
|
125
|
+
protected rejectedConfig?: { configPath: string; sousDir: string };
|
|
126
|
+
|
|
106
127
|
/**
|
|
107
128
|
* The real shell environment, snapshotted BEFORE the `.sous/` env files are
|
|
108
129
|
* injected into `process.env`. The variables layer needs it to tell a value
|
|
@@ -177,7 +198,7 @@ export abstract class BaseCommand extends Command {
|
|
|
177
198
|
[sousDirEnv, "SOUS_DIR"],
|
|
178
199
|
];
|
|
179
200
|
const primary = primaryCandidates.find(([value]) => value !== undefined);
|
|
180
|
-
const requiresConfig =
|
|
201
|
+
const { requiresConfig, toleratesConfigErrors } = this.constructor as typeof BaseCommand;
|
|
181
202
|
|
|
182
203
|
this.configLocator = {
|
|
183
204
|
cwd,
|
|
@@ -195,6 +216,12 @@ export abstract class BaseCommand extends Command {
|
|
|
195
216
|
try {
|
|
196
217
|
discovered = resolveConfigFlag(primarySource, cwd, confDirOverride, sourceLabel);
|
|
197
218
|
} catch (error) {
|
|
219
|
+
if (toleratesConfigErrors) {
|
|
220
|
+
return this.warnConfigNotLoaded(
|
|
221
|
+
error,
|
|
222
|
+
`the project config at ${this.configLocator.primary?.value} (named by ${sourceLabel})`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
198
225
|
// A command that creates the config is pointed at a place with none in
|
|
199
226
|
// it by design; the flag still says where, so this is not a failure.
|
|
200
227
|
if (!requiresConfig) return;
|
|
@@ -202,7 +229,12 @@ export abstract class BaseCommand extends Command {
|
|
|
202
229
|
return this.exit(1);
|
|
203
230
|
}
|
|
204
231
|
} else {
|
|
205
|
-
|
|
232
|
+
try {
|
|
233
|
+
discovered = discoverConfig(cwd, confDirOverride);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (!toleratesConfigErrors) throw error;
|
|
236
|
+
return this.warnConfigNotLoaded(error, `the project config it found searching up from ${cwd}`);
|
|
237
|
+
}
|
|
206
238
|
}
|
|
207
239
|
|
|
208
240
|
if (!discovered) {
|
|
@@ -243,6 +275,7 @@ export abstract class BaseCommand extends Command {
|
|
|
243
275
|
try {
|
|
244
276
|
refreshed = refreshDiscoveredConfig(discovered);
|
|
245
277
|
} catch (error) {
|
|
278
|
+
if (this.toleratesConfigErrors) return this.rejectConfig(discovered, error);
|
|
246
279
|
displayErrorBlock(error instanceof Error ? error.message : String(error), this.errorSink);
|
|
247
280
|
return this.exit(1);
|
|
248
281
|
}
|
|
@@ -262,11 +295,51 @@ export abstract class BaseCommand extends Command {
|
|
|
262
295
|
try {
|
|
263
296
|
this.settings = await loadSettings(refreshed);
|
|
264
297
|
} catch (error) {
|
|
298
|
+
if (this.toleratesConfigErrors) return this.rejectConfig(refreshed, error);
|
|
265
299
|
displayErrorBlock(error instanceof Error ? error.message : String(error), this.errorSink);
|
|
266
300
|
return this.exit(1);
|
|
267
301
|
}
|
|
268
302
|
}
|
|
269
303
|
|
|
304
|
+
/** The `toleratesConfigErrors` setting of the running command's class. */
|
|
305
|
+
private get toleratesConfigErrors(): boolean {
|
|
306
|
+
return (this.constructor as typeof BaseCommand).toleratesConfigErrors;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Undoes a config adoption that failed on a command that tolerates it: the
|
|
311
|
+
* env files it injected are taken back out of `process.env`, nothing is left
|
|
312
|
+
* adopted, and one warning says what happened.
|
|
313
|
+
*/
|
|
314
|
+
private rejectConfig(discovered: DiscoveredConfig, error: unknown): void {
|
|
315
|
+
for (const name of Object.keys(process.env)) {
|
|
316
|
+
if (!(name in this.shellEnv)) delete process.env[name];
|
|
317
|
+
}
|
|
318
|
+
this.shellEnv = {};
|
|
319
|
+
this.discovered = undefined as unknown as DiscoveredConfig;
|
|
320
|
+
this.configContext = undefined as unknown as ConfigContext;
|
|
321
|
+
this.rejectedConfig = { configPath: discovered.configPath, sousDir: discovered.sousDir };
|
|
322
|
+
this.warnConfigNotLoaded(error, `the project config at ${discovered.configPath}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The one warning a command that tolerates config errors prints in place of
|
|
327
|
+
* failing.
|
|
328
|
+
*
|
|
329
|
+
* @param error - What locating or loading the config threw.
|
|
330
|
+
* @param config - Which config: its file, or where it was looked for.
|
|
331
|
+
*/
|
|
332
|
+
private warnConfigNotLoaded(error: unknown, config: string): void {
|
|
333
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
334
|
+
warning(
|
|
335
|
+
`Sous could not load ${config}, so this command carries on without it.\n` +
|
|
336
|
+
"\n" +
|
|
337
|
+
"The error was:\n" +
|
|
338
|
+
message,
|
|
339
|
+
this.errorSink
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
270
343
|
/**
|
|
271
344
|
* Renders any failure as a plain, readable message instead of an oclif stack
|
|
272
345
|
* trace: a config mistake, a mistyped command line, or a question sous could
|
|
@@ -150,7 +150,8 @@ export default class LockRebuild extends BaseCommand {
|
|
|
150
150
|
repos,
|
|
151
151
|
// A recipe's own manifest is what names its dependencies, and it is read
|
|
152
152
|
// from the files this machine already holds. Nothing is fetched here, so a
|
|
153
|
-
// recipe that is not in the store yet
|
|
153
|
+
// recipe that is not in the store yet is walked from what its index entry
|
|
154
|
+
// records, and comes back under `missingManifests` when that is nothing.
|
|
154
155
|
loadManifest: (recipe) => {
|
|
155
156
|
const directory = recipeFilesDirectory({
|
|
156
157
|
service,
|
|
@@ -208,9 +209,9 @@ export default class LockRebuild extends BaseCommand {
|
|
|
208
209
|
if (result.missingManifests.length > 0) {
|
|
209
210
|
blankLine();
|
|
210
211
|
paragraph(
|
|
211
|
-
`The files of these recipes are not on this machine,
|
|
212
|
-
`
|
|
213
|
-
|
|
212
|
+
`The files of these recipes are not on this machine, and their repository's ` +
|
|
213
|
+
`index does not record what they depend on, so their own dependencies are ` +
|
|
214
|
+
`not in the rebuilt lockfile: ${result.missingManifests.join(", ")}.`
|
|
214
215
|
);
|
|
215
216
|
}
|
|
216
217
|
|