@sous-io/sous 0.2.0 → 0.2.2
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/docs/markdown/README.md +4 -0
- package/docs/markdown/_sidebar.md +4 -1
- package/docs/markdown/commands.md +293 -274
- package/docs/markdown/configuration.md +7 -0
- package/docs/markdown/repositories-authoring.md +210 -301
- package/docs/markdown/repositories-consuming.md +219 -468
- package/docs/markdown/repositories-file-formats.md +216 -980
- package/docs/markdown/repositories-providers.md +322 -0
- package/docs/markdown/repositories-quickstart.md +340 -0
- package/docs/markdown/repositories-troubleshooting.md +336 -0
- package/docs/markdown/repositories-variables.md +229 -296
- package/docs/markdown/repositories.md +262 -228
- package/package.json +1 -1
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/commands/repo/release.ts +14 -8
- package/src/lib/repos/scaffold/templates.ts +9 -7
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Providers
|
|
2
|
+
|
|
3
|
+
A **provider** is everything sous knows about one kind of repository host. It is the only place
|
|
4
|
+
a host-specific fact is allowed to live, so every command above it (add, subscribe, build,
|
|
5
|
+
release, submit) talks about repositories in the same terms whatever host they live on.
|
|
6
|
+
|
|
7
|
+
Sous ships three: `github`, `gitlab` and `local`. This page covers how one is chosen for a URL,
|
|
8
|
+
what a URL becomes once a provider has taken it apart, how private repositories authenticate,
|
|
9
|
+
what each provider can do when you propose a change, and what writing a fourth one involves.
|
|
10
|
+
|
|
11
|
+
## What a provider answers
|
|
12
|
+
|
|
13
|
+
Every provider answers the **read path**, which is deliberately small:
|
|
14
|
+
|
|
15
|
+
- recognize a repository URL as its own;
|
|
16
|
+
- take that URL apart into a host, an owner and a name;
|
|
17
|
+
- hand back the repository's `sous.index.json`;
|
|
18
|
+
- fetch **one** recipe's subtree at one tag.
|
|
19
|
+
|
|
20
|
+
Nothing there clones a whole repository. A provider that can also carry a contribution answers
|
|
21
|
+
the **write path**: report whether its command line tool is installed and signed in, say whether
|
|
22
|
+
you may push to the repository itself, fork it onto your account, and open the proposal.
|
|
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
|
|
26
|
+
provider's features is refused with a sentence naming it and what it cannot do, never a crash.
|
|
27
|
+
|
|
28
|
+
## How a URL is matched
|
|
29
|
+
|
|
30
|
+
When you name a repository, sous tries each provider in turn (`github`, then `gitlab`, then
|
|
31
|
+
`local`) and the first one that recognizes the URL handles it.
|
|
32
|
+
|
|
33
|
+
| Provider | Recognizes |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `github` | any URL whose host is `github.com` |
|
|
36
|
+
| `gitlab` | any URL whose host is `gitlab.com`, or whose host begins with `gitlab.` |
|
|
37
|
+
| `local` | an absolute filesystem path, or the same path in `file:///...` form |
|
|
38
|
+
|
|
39
|
+
Three spellings of a hosted URL are all understood, because all three are what people paste:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
https://github.com/sous-io/sous-recipes
|
|
43
|
+
git@github.com:sous-io/sous-recipes.git
|
|
44
|
+
ssh://git@github.com/sous-io/sous-recipes
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A trailing slash and a trailing `.git` are dropped before anything else happens, and the scheme
|
|
48
|
+
and the host are lowercased. Everything between the host and the last segment is the owner, so
|
|
49
|
+
a GitLab group path of any depth (`group/subgroup/project`) is carried whole.
|
|
50
|
+
|
|
51
|
+
`local` is tried last and matches only a path, so it can never intercept a hosted URL. A path
|
|
52
|
+
you type relative (`../my-recipes`, `~/recipes`, `.`) is expanded and resolved against your
|
|
53
|
+
working directory before any provider sees it, and the absolute form is what gets stored; a
|
|
54
|
+
repository on this machine is machine-specific either way. In a config file that same relative path
|
|
55
|
+
is refused outright, because an entry is read from a file that several working directories may run
|
|
56
|
+
against; see
|
|
57
|
+
[A local repository named by a relative path](repositories-troubleshooting.md#a-local-repository-named-by-a-relative-path).
|
|
58
|
+
|
|
59
|
+
### Naming a provider explicitly
|
|
60
|
+
|
|
61
|
+
`sous repo add --provider <id>` (and the `provider:` key on a repository entry) names the
|
|
62
|
+
provider outright. That is what a self-hosted instance behind an unfamiliar host name needs:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
sous repo add https://git.example.com/team/recipes --provider gitlab
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A named provider is honored for any host nobody recognizes. It is refused only when a
|
|
69
|
+
**different** provider plainly owns the URL, because that is a contradiction rather than a hint:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
Error: The gitlab provider does not handle https://github.com/sous-io/sous-recipes; that is a
|
|
73
|
+
github URL, which the github provider handles.
|
|
74
|
+
Drop '--provider' and let sous work it out, or name 'github'.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
With no provider named and nothing recognizing the host, sous says so and points at the flag:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
Error: Sous does not recognize the host in the repository URL
|
|
81
|
+
https://git.example.com/team/recipes.
|
|
82
|
+
Sous ships these providers: github, gitlab, local. For a self-hosted instance, name the
|
|
83
|
+
provider that host runs on the repository entry, as in 'sous repo add
|
|
84
|
+
https://git.example.com/team/recipes --provider <provider>'.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`sous repo list` shows which provider each trusted repository resolved to:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
Repository Provider Origin Linked Recipes URL
|
|
91
|
+
------------ -------- -------- ------ ----------- ---------------------------------------
|
|
92
|
+
qa-recipes local user no 0 /home/me/Projects/qa-recipes
|
|
93
|
+
sous-recipes github built in no not fetched https://github.com/sous-io/sous-recipes
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Repository identity
|
|
97
|
+
|
|
98
|
+
Canonicalizing a URL produces the repository's **identity**, `<host>/<owner path>/<name>`,
|
|
99
|
+
lowercased and with any `.git` suffix gone:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
github.com/sous-io/sous-recipes
|
|
103
|
+
gitlab.example.com/group/subgroup/project
|
|
104
|
+
localhost/home/me/projects/my-recipes
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A local repository's identity uses the host `localhost` and the directory's parent path as its
|
|
108
|
+
owner, so a path on disk keys exactly the way a hosted repository does. Identity is then what
|
|
109
|
+
every machine-wide thing keys by:
|
|
110
|
+
|
|
111
|
+
| Keyed by identity | Looks like |
|
|
112
|
+
|---|---|
|
|
113
|
+
| the store entry | `~/.sous/cache/github.com/sous-io/sous-recipes/<namespace>/<recipe>/<version>/` |
|
|
114
|
+
| the cached index | `~/.sous/cache/_indexes/github.com/sous-io/sous-recipes.json` |
|
|
115
|
+
| the index sidecar | `~/.sous/cache/_indexes/github.com/sous-io/sous-recipes.meta.json` |
|
|
116
|
+
| the lockfile's `identity` field | `github.com/sous-io/sous-recipes` |
|
|
117
|
+
|
|
118
|
+
A repository's **short name** (`sous-recipes`) is something your project chose, and it keys
|
|
119
|
+
nothing shared: two projects may call one repository different things, and two may use one name
|
|
120
|
+
for different repositories. Keying by identity is what lets them share one cached copy without
|
|
121
|
+
colliding. See [The store on disk](repositories-file-formats.md#the-store-on-disk).
|
|
122
|
+
|
|
123
|
+
Canonicalizing also produces the two clone URLs sous hands to git, `https://<host>/<owner>/<name>.git`
|
|
124
|
+
and `git@<host>:<owner>/<name>.git`. For the `local` provider the HTTPS slot carries the
|
|
125
|
+
absolute directory, because that is what git is given when a recipe is fetched.
|
|
126
|
+
|
|
127
|
+
?> A published manifest names a dependency in another repository by a locator URL whose
|
|
128
|
+
scheme is the provider's identifier: `github://sous-io/sous-recipes/workflow/sat@^1.1`. A first
|
|
129
|
+
segment containing a dot is read as the host, so `gitlab://gitlab.example.com/group/project/ns/recipe`
|
|
130
|
+
addresses a self-hosted instance; without one, the provider's public host is assumed. `local://`
|
|
131
|
+
is refused, because a path on your disk is not a published location. The grammar is in
|
|
132
|
+
[Dependencies named by location](repositories-file-formats.md#dependencies-named-by-location).
|
|
133
|
+
|
|
134
|
+
## Private repositories
|
|
135
|
+
|
|
136
|
+
Sous reads a repository in two very different ways, and they authenticate differently.
|
|
137
|
+
|
|
138
|
+
**The index** is one plain HTTPS GET of a raw file, carrying a bearer token when sous has one:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
https://raw.githubusercontent.com/<owner>/<name>/HEAD/sous.index.json
|
|
142
|
+
https://<gitlab host>/<owner>/<name>/-/raw/HEAD/sous.index.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A token is looked for in two places, in this order:
|
|
146
|
+
|
|
147
|
+
1. the environment: `GITHUB_TOKEN` for GitHub, `GITLAB_TOKEN` for GitLab;
|
|
148
|
+
2. the host's own command line tool, `gh auth token` or `glab auth token`, when it is installed
|
|
149
|
+
and signed in.
|
|
150
|
+
|
|
151
|
+
Neither is required. A public repository needs no token at all, and a missing `gh` or `glab` is
|
|
152
|
+
never an error on the read path; sous simply fetches without one. When the host refuses, the
|
|
153
|
+
error says which of the two situations you are in:
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
Sous could not fetch the repo index from https://raw.githubusercontent.com/acme/recipes/HEAD/sous.index.json.
|
|
157
|
+
The server answered 404 Not Found.
|
|
158
|
+
Either the repository publishes no sous index yet, or the URL names a repository that does
|
|
159
|
+
not exist.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A 401 or 403 says the repository is private or was not authorized, and points at the two sources:
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
Sous could not fetch the repo index from https://raw.githubusercontent.com/acme/recipes/HEAD/sous.index.json.
|
|
166
|
+
The server answered 403 Forbidden.
|
|
167
|
+
The repository is private or the request was not authorized. Sous uses a token from the
|
|
168
|
+
environment, or from the provider's command line tool when one is installed and signed in.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**A recipe's files** come from git itself: a shallow, blobless, sparse clone at the version's
|
|
172
|
+
tag, fetching only the blobs inside that one recipe folder. That means git's own credentials
|
|
173
|
+
apply, exactly as they would for a manual clone: credential helpers, the SSH agent, proxies and
|
|
174
|
+
`insteadOf` rewrites are all inherited from your git configuration, and sous adds nothing of its
|
|
175
|
+
own. Cloning a working copy with `sous repo link` works the same way.
|
|
176
|
+
|
|
177
|
+
?> For a private repository, make sure both halves work. `gh auth login` (or `GITHUB_TOKEN`)
|
|
178
|
+
covers the index, and a git credential helper or an SSH key covers the recipe files. A build that
|
|
179
|
+
finds the index but cannot clone has only the second half missing.
|
|
180
|
+
|
|
181
|
+
## Proposing a change
|
|
182
|
+
|
|
183
|
+
[`sous repo submit`](repositories-authoring.md#contribute-to-someone-elses-repository) validates
|
|
184
|
+
your repository, then hands the host-specific mechanics to the provider that owns its `origin`
|
|
185
|
+
remote. Providers differ, and sous says so rather than pretending otherwise:
|
|
186
|
+
|
|
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 |
|
|
192
|
+
|
|
193
|
+
**GitLab reports "cannot tell" rather than guessing.** Sous has no cheap, reliable way to ask
|
|
194
|
+
whether you may push, and a wrong guess would send you down a fork path this provider cannot
|
|
195
|
+
finish. So the submission announces that it could not tell and pushes to `origin` as it stands. If
|
|
196
|
+
that push is refused, the command stops at the push step and reports git's own error alongside
|
|
197
|
+
everything it had already done; forking the project and pushing there is then a manual route.
|
|
198
|
+
|
|
199
|
+
**A local repository never submits.** It declares `fetch` only, so `sous repo submit` stops before
|
|
200
|
+
anything is written and prints the repository's own contribution route instead:
|
|
201
|
+
|
|
202
|
+
```text
|
|
203
|
+
Error: The 'local' provider cannot propose a change on your behalf.
|
|
204
|
+
This repository asks that changes be sent this way:
|
|
205
|
+
https://github.com/sous-io/sous-recipes/blob/main/CONTRIBUTING.md
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
That second line is the `contribute` field from the repository's `sous.repo.yaml`; sous prints it
|
|
209
|
+
whenever a provider cannot carry the proposal, so a contributor is never left without a route. A
|
|
210
|
+
manifest that sets none gets "This repository's manifest does not say where to send a change, so
|
|
211
|
+
send it the way its maintainers prefer." Set the field in your own repository for the same reason;
|
|
212
|
+
see [the repository manifest](repositories-file-formats.md#sousrepoyaml-the-repository-manifest).
|
|
213
|
+
|
|
214
|
+
## Self-hosted GitLab
|
|
215
|
+
|
|
216
|
+
A self-hosted instance needs no special configuration beyond being recognized. A host that
|
|
217
|
+
begins with `gitlab.` is matched automatically; any other host name is matched by naming the
|
|
218
|
+
provider once, on the repository entry:
|
|
219
|
+
|
|
220
|
+
```jsonc
|
|
221
|
+
{
|
|
222
|
+
"repos": {
|
|
223
|
+
"team-recipes": {
|
|
224
|
+
"url": "https://git.example.com/platform/recipes",
|
|
225
|
+
"provider": "gitlab"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
That block goes at the top level of your primary config or of a `conf.d/` layer of your own; sous
|
|
232
|
+
writes its own entries to the `conf.d/500-repos.jsonc` layer it manages. Every key is listed under
|
|
233
|
+
[configuration keys](repositories-file-formats.md#configuration-keys).
|
|
234
|
+
|
|
235
|
+
Everything downstream then works normally: the index is read from that host's own raw endpoint,
|
|
236
|
+
`GITLAB_TOKEN` or `glab` supplies the token, and the identity is
|
|
237
|
+
`git.example.com/platform/recipes`. Group paths of any depth are preserved, so
|
|
238
|
+
`group/subgroup/project` stays one repository rather than being mistaken for a namespace.
|
|
239
|
+
|
|
240
|
+
`sous repo submit` is the exception: it runs inside the repository checkout and never reads a
|
|
241
|
+
project's config, so it picks the provider from the `origin` remote URL alone. A host that does not
|
|
242
|
+
begin with `gitlab.` goes unrecognized there, and sous prints the repository's `contribute` pointer.
|
|
243
|
+
|
|
244
|
+
## The local provider
|
|
245
|
+
|
|
246
|
+
A repository on this machine is an ordinary directory holding a `sous.repo.yaml`, read through
|
|
247
|
+
the `local` provider:
|
|
248
|
+
|
|
249
|
+
```term
|
|
250
|
+
$ sous repo add ../my-recipes --name my-recipes --trust
|
|
251
|
+
// sous resolves the path, reads the index and records the absolute form
|
|
252
|
+
▶ Adding a repository:
|
|
253
|
+
Repository: my-recipes
|
|
254
|
+
Location : /home/me/Projects/my-recipes
|
|
255
|
+
Provider : local
|
|
256
|
+
// the namespace and recipe rows are left out here
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
It exists for local development and for tests: authoring a repository, trying a recipe before
|
|
260
|
+
publishing it, or running a whole workflow with no network at all. Two details make it behave
|
|
261
|
+
like a host rather than like a shortcut.
|
|
262
|
+
|
|
263
|
+
- The index is read from the **working tree** when `sous.index.json` is there, so an index you are
|
|
264
|
+
still writing is picked up without a commit, and from `git show HEAD:sous.index.json` otherwise.
|
|
265
|
+
- A recipe's files come from a clone of the local repository at the version's **tag**, exactly as
|
|
266
|
+
a hosted repository would be fetched, so a version really is the version its tag points at. A
|
|
267
|
+
directory that is not a git repository, or one missing that tag, has no versions to honor, so
|
|
268
|
+
its working tree is copied instead.
|
|
269
|
+
|
|
270
|
+
!> A local path is added, and therefore trusted, through the same ceremony as a hosted
|
|
271
|
+
repository, because its recipes still run on this machine. See [Trust](repositories.md#trust).
|
|
272
|
+
|
|
273
|
+
A path that is not a repository is explained as a path mistake rather than as a provider
|
|
274
|
+
failure, naming what you typed, what sous resolved it to, and what it expected to find:
|
|
275
|
+
|
|
276
|
+
```text
|
|
277
|
+
Error: There is no directory at './nope'.
|
|
278
|
+
Sous read that as the path /home/me/Projects/my-project/nope, and nothing is there.
|
|
279
|
+
A repository on this machine is a directory holding a 'sous.repo.yaml' file at its root.
|
|
280
|
+
Check the path, or create one with 'sous repo init'.
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
For editing a repository you already subscribe to, reach for
|
|
284
|
+
[`sous repo link`](repositories-authoring.md#edit-a-repository-in-place) instead; it redirects
|
|
285
|
+
one repository's resolution at a working copy without changing what your project subscribes to.
|
|
286
|
+
|
|
287
|
+
## Adding a provider
|
|
288
|
+
|
|
289
|
+
Adding a provider is one file plus one line. The interface is internal for now, not a published
|
|
290
|
+
plugin API, so it can still change shape; what follows is what a new provider writes today. A
|
|
291
|
+
provider class extends `ProviderBase`, which carries the plumbing no provider should repeat:
|
|
292
|
+
running a subprocess through the injectable runner, checking whether a tool exited cleanly,
|
|
293
|
+
capturing its output, finding a host token, and refusing every write-path call by name until a
|
|
294
|
+
subclass overrides it. Each member is documented where it lives, in
|
|
295
|
+
`src/lib/repos/providers/base.ts`.
|
|
296
|
+
|
|
297
|
+
The subclass supplies the rest:
|
|
298
|
+
|
|
299
|
+
- `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);
|
|
302
|
+
- `matches(url)` and `canonicalize(url)`, the URL half;
|
|
303
|
+
- `fetchIndex(repo, options)` and `fetchRecipeTree(repo, recipePath, tag, destDir, options)`, the
|
|
304
|
+
read half;
|
|
305
|
+
- `cli` and `proposalNoun` when it submits, so messages can name the tool and call a proposal
|
|
306
|
+
what the host calls it;
|
|
307
|
+
- overrides of the four write-path calls when it submits.
|
|
308
|
+
|
|
309
|
+
Every call takes an options object carrying the testing seams (`cwd`, `env`, `fetchImpl`, `run`),
|
|
310
|
+
which is why no provider reaches for `spawn` or the global `fetch` directly and no test in this
|
|
311
|
+
layer touches the network. The last change is adding the class to the built-in provider list in
|
|
312
|
+
`providers/index.ts`; nothing above the provider layer learns its name.
|
|
313
|
+
|
|
314
|
+
## Where to go next
|
|
315
|
+
|
|
316
|
+
- [Repositories](repositories.md): the model, trust, and the lockfile
|
|
317
|
+
- [Consuming recipes](repositories-consuming.md): adding, subscribing and building
|
|
318
|
+
- [Authoring a repository](repositories-authoring.md): releasing and contributing
|
|
319
|
+
- [Repository file formats](repositories-file-formats.md): every manifest, index and lockfile
|
|
320
|
+
schema
|
|
321
|
+
- [Troubleshooting](repositories-troubleshooting.md): what the errors mean and how to clear them
|
|
322
|
+
- [Command reference](commands.md): every command and flag
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Repositories Quickstart
|
|
2
|
+
|
|
3
|
+
Twelve steps take you from an empty directory to a project whose agent skills come from the
|
|
4
|
+
official repository and from a repository you wrote yourself, with a lockfile a colleague can
|
|
5
|
+
restore from. Budget about ten minutes.
|
|
6
|
+
|
|
7
|
+
This page is the guided tour; [Repositories](repositories.md) explains the model behind it, and
|
|
8
|
+
[Consuming recipes](repositories-consuming.md) and
|
|
9
|
+
[Authoring a repository](repositories-authoring.md) are the reference guides for each half. You
|
|
10
|
+
need sous on your path (`npm install -g @sous-io/sous`), git, and a network connection for the two
|
|
11
|
+
steps that reach GitHub. Output below is trimmed: sous prints absolute paths and a banner that are
|
|
12
|
+
left out here, and a line reading `...` marks rows removed for length.
|
|
13
|
+
|
|
14
|
+
## 1. Create a project
|
|
15
|
+
|
|
16
|
+
A sous project is a directory with a `.sous/` directory in it holding one config file. One line is
|
|
17
|
+
a valid config:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export SOUS_HOME=~/tmp/sous-quickstart
|
|
21
|
+
mkdir -p ~/projects/my-project/.sous
|
|
22
|
+
cd ~/projects/my-project
|
|
23
|
+
echo 'name: my-project' > .sous/sous.config.yaml
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Everything else has a default: skills compile into `<project root>/.claude/skills`, and the
|
|
27
|
+
lockfile, the state file and the config layers sous writes land under `.sous/`. `SOUS_HOME` puts
|
|
28
|
+
the machine-wide store somewhere throwaway, so this walkthrough leaves your real one alone. See
|
|
29
|
+
[The config file](configuration.md) for the keys you will want later.
|
|
30
|
+
|
|
31
|
+
## 2. Build once
|
|
32
|
+
|
|
33
|
+
```term
|
|
34
|
+
$ sous build
|
|
35
|
+
▶ Locking subscribed recipes:
|
|
36
|
+
pinned: core/sous-skills at version 0.2.0.
|
|
37
|
+
The lockfile has been updated. Commit it, so everyone building this project
|
|
38
|
+
gets exactly these versions.
|
|
39
|
+
▶ Building:
|
|
40
|
+
✓ .claude/skills/about-sous/SKILL.md (~782 tokens)
|
|
41
|
+
✓ .claude/skills/about-sous-configuration/SKILL.md (~1,204 tokens)
|
|
42
|
+
✓ .claude/skills/about-agent-skills/SKILL.md (~1,937 tokens)
|
|
43
|
+
✓ .claude/skills/about-liquid-templates/SKILL.md (~1,486 tokens)
|
|
44
|
+
✓ .claude/skills/create-skill/SKILL.md (~655 tokens)
|
|
45
|
+
✓ Done.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You subscribed to nothing and five skills appeared. That is the `core` namespace, which every
|
|
49
|
+
project is auto-subscribed to at the version matching the CLI you are running. It carries the
|
|
50
|
+
skills that teach an agent what sous manages and why it must not hand-edit a generated file.
|
|
51
|
+
|
|
52
|
+
?> `core` ships inside the sous package and seeds the machine-wide store on first run, so this
|
|
53
|
+
step works with no network at all. It is an ordinary subscription and
|
|
54
|
+
[one line switches it off](repositories-consuming.md#opt-out-of-core).
|
|
55
|
+
|
|
56
|
+
## 3. See what is on offer
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
sous recipe list
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Recipe Repository Latest Pinned Subscribed What it is
|
|
64
|
+
-------------------------- ------------ ------ ------ ---------- ------------------------------
|
|
65
|
+
communication/control-flow sous-recipes 1.0.0 no Generic interaction skills
|
|
66
|
+
core/sous-skills sous-recipes 0.2.0 0.2.0 yes What sous is, and how it works
|
|
67
|
+
workflow/task-files sous-recipes 1.0.2 no Per-branch task files
|
|
68
|
+
...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This reads the cached index of every repository the project trusts, so it works offline and
|
|
72
|
+
downloads nothing. The official repository, `sous-recipes`, is trusted out of the box.
|
|
73
|
+
|
|
74
|
+
Before subscribing to something, read it:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sous recipe show workflow/task-files
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
Repository : sous-recipes
|
|
82
|
+
Latest version : 1.0.2
|
|
83
|
+
Subscribed : no
|
|
84
|
+
...
|
|
85
|
+
The recipe's own files are not on this machine, so the questions it asks and
|
|
86
|
+
the files it publishes are not known here. Subscribing to it fetches them.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
On a machine that has never fetched the recipe, sous shows what the index knows: every published
|
|
90
|
+
version, and every dependency with each declared range beside the exact version the index resolved
|
|
91
|
+
it to. The questions it asks are not known until it is fetched; run the same command after
|
|
92
|
+
subscribing and it adds a table of the variables the recipe publishes.
|
|
93
|
+
|
|
94
|
+
## 4. Subscribe to a recipe
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
sous subscription add workflow/task-files
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Sous states what subscribing does, asks once, resolves the whole dependency closure, downloads it,
|
|
101
|
+
then asks the questions the recipe publishes. Answering the confirmation and the two questions:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
➔ What was installed:
|
|
105
|
+
Recipe Version Repository Why
|
|
106
|
+
----------------------------- ------- ------------ --------------------------
|
|
107
|
+
workflow/sub-agent-delegation 1.0.0 sous-recipes needed by workflow/task-files
|
|
108
|
+
workflow/task-files 1.0.2 sous-recipes you subscribed to it
|
|
109
|
+
|
|
110
|
+
➔ Variables:
|
|
111
|
+
Answers stored:
|
|
112
|
+
taskFileRoot : .sous/tasks TASK_FILE_ROOT in .env
|
|
113
|
+
ticketIdExample : PROJ-1234 TICKET_ID_EXAMPLE in .env
|
|
114
|
+
|
|
115
|
+
Left unanswered:
|
|
116
|
+
ticketPrefix : no answer yet, and this variable is optional
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A script, a pipeline or a coding agent has no terminal to answer on, so it passes the answers in:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
sous subscription add workflow/task-files --yes \
|
|
123
|
+
--answer taskFileRoot=.sous/tasks \
|
|
124
|
+
--answer ticketIdExample=PROJ-1234
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The answers land in `.sous/.env`, which is committed; a definition marked secret goes to the
|
|
128
|
+
gitignored `.sous/.env.local`. [Recipe variables](repositories-variables.md) covers the rest.
|
|
129
|
+
|
|
130
|
+
## 5. Look at what landed
|
|
131
|
+
|
|
132
|
+
`sous subscription add` finishes by building, so the files are already on disk. `ls .claude/skills`
|
|
133
|
+
now lists the five `core` skills plus the seven this recipe ships, among them `about-task-files`,
|
|
134
|
+
`start-task` and `resume-task`. The build dependency contributes nothing: a recipe held through
|
|
135
|
+
`depends` is fetched and pinned, and its files stay out of your output.
|
|
136
|
+
|
|
137
|
+
Recipe files compile the way your own targets do, under the
|
|
138
|
+
[`.tpl.` convention](configuration.md#templates-and-the-tpl-convention): a file with `.tpl.` in
|
|
139
|
+
its name is rendered through LiquidJS and loses that segment (`SKILL.tpl.md` becomes `SKILL.md`),
|
|
140
|
+
and any other file is copied verbatim. They are tracked like every file sous writes, so
|
|
141
|
+
`sous prune` removes them when you unsubscribe. Where each kind lands is your project's decision,
|
|
142
|
+
under
|
|
143
|
+
[`recipeOutputs`](repositories-file-formats.md#recipeoutputs-where-the-files-land).
|
|
144
|
+
|
|
145
|
+
## 6. Start a repository of your own
|
|
146
|
+
|
|
147
|
+
A recipe repository is not a sous project; it has no `.sous/` directory, and the three commands
|
|
148
|
+
that work inside one do not look for a config. Run this outside your project:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
sous repo init ~/projects/my-recipes --name my-recipes --namespace workflow
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
▶ Creating a recipe repository:
|
|
156
|
+
wrote sous.repo.yaml
|
|
157
|
+
wrote sous.index.json
|
|
158
|
+
wrote recipes/workflow/example/sous.recipe.yaml
|
|
159
|
+
wrote recipes/workflow/example/skills/example-skill/SKILL.md
|
|
160
|
+
wrote README.md
|
|
161
|
+
wrote .github/workflows/sous-release.yml
|
|
162
|
+
wrote .gitignore
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The scaffold validates as written, and every file it leaves is heavily commented. `sous.repo.yaml`
|
|
166
|
+
declares the namespaces and lists every recipe folder; each folder's `sous.recipe.yaml` is one
|
|
167
|
+
recipe, with its own version.
|
|
168
|
+
|
|
169
|
+
## 7. Put something in the recipe
|
|
170
|
+
|
|
171
|
+
Edit `recipes/workflow/example/skills/example-skill/SKILL.md` into a real skill, then name the
|
|
172
|
+
recipe in its manifest:
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
formatVersion: 1
|
|
176
|
+
namespace: workflow
|
|
177
|
+
name: example
|
|
178
|
+
version: 0.1.0
|
|
179
|
+
description: One paragraph saying what a project gets by subscribing to it.
|
|
180
|
+
|
|
181
|
+
contents:
|
|
182
|
+
- kind: skills
|
|
183
|
+
include:
|
|
184
|
+
- skills/**/*.md
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`contents` is what a subscriber receives, one group per kind (`skills`, `memories`, `prompts` or
|
|
188
|
+
`config`), each with globs relative to the recipe folder. Copying the folder is how you start a
|
|
189
|
+
second recipe; its path goes in the `recipes` list at the root.
|
|
190
|
+
|
|
191
|
+
## 8. Release it
|
|
192
|
+
|
|
193
|
+
Releasing reads versions from the recipe manifests, regenerates the index, commits and tags, so
|
|
194
|
+
the repository has to be a git repository with your work committed:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
cd ~/projects/my-recipes
|
|
198
|
+
git init -b main . && git add -A && git commit -m "First recipe"
|
|
199
|
+
sous repo release
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Releasing commits and tags on your behalf, so `git config user.name` and `git config user.email`
|
|
203
|
+
have to be set in this repository first. Sous then prints the versions it would publish and what
|
|
204
|
+
the run would do, and asks once.
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
...
|
|
208
|
+
▶ Publishing:
|
|
209
|
+
workflow/example: publishing version 0.1.0.
|
|
210
|
+
Wrote sous.index.json.
|
|
211
|
+
Committed: Release workflow/example@0.1.0
|
|
212
|
+
Created the tag workflow/example@0.1.0.
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Add `--push` to push the commit and the tags in the same run, and `--yes` to accept the plan
|
|
216
|
+
without being asked, which is what the scaffolded workflow does. Never edit `sous.index.json` by
|
|
217
|
+
hand; the recipe manifests are the source of truth for versions, and this command regenerates the
|
|
218
|
+
catalog from them. `sous repo release --check` validates without publishing, and runs on a pull
|
|
219
|
+
request.
|
|
220
|
+
|
|
221
|
+
## 9. Add your repository to the project
|
|
222
|
+
|
|
223
|
+
Adding a repository is how you trust it, so sous asks before recording it. A path works as well as
|
|
224
|
+
a URL; sous reads a path through the built-in `local` provider:
|
|
225
|
+
|
|
226
|
+
```term
|
|
227
|
+
$ cd ~/projects/my-project
|
|
228
|
+
$ sous repo add ../my-recipes --name my-recipes
|
|
229
|
+
One repository has to be trusted before this can continue.
|
|
230
|
+
my-recipes
|
|
231
|
+
Location: /home/you/projects/my-recipes
|
|
232
|
+
Required: my-recipes (required by this project)
|
|
233
|
+
// then what trusting means, and the question
|
|
234
|
+
? Do you trust this repository? (y/N) y
|
|
235
|
+
▶ Adding a repository:
|
|
236
|
+
Repository: my-recipes
|
|
237
|
+
Location : /home/you/projects/my-recipes
|
|
238
|
+
Provider : local
|
|
239
|
+
Namespaces: workflow
|
|
240
|
+
Recipes : 1
|
|
241
|
+
This project now trusts 'my-recipes'. Nothing from it has been installed.
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
One file is fetched, the repository's `sous.index.json`, and nothing is installed. The entry lands
|
|
245
|
+
in `.sous/conf.d/500-repos.jsonc`, which is committed, so a colleague inherits the repository and
|
|
246
|
+
the trust decision together.
|
|
247
|
+
|
|
248
|
+
!> A repository on your own disk goes through the same question as a hosted one; its recipes still
|
|
249
|
+
run on this machine, and [Trust](repositories.md#trust) says what the decision covers. A local path is machine-specific, so a colleague cloning your project needs
|
|
250
|
+
that path to exist, or needs the repository pushed somewhere they can reach. To edit a repository
|
|
251
|
+
your project already subscribes to, use
|
|
252
|
+
[`sous repo link`](repositories-authoring.md#edit-a-repository-in-place) instead.
|
|
253
|
+
|
|
254
|
+
## 10. Subscribe to your own recipe
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
sous subscription add workflow/example
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
➔ What was installed:
|
|
262
|
+
Recipe Version Repository Why
|
|
263
|
+
---------------- ------- ---------- --------------------
|
|
264
|
+
workflow/example 0.1.0 my-recipes you subscribed to it
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Your skill is now in `.claude/skills/` beside the ones from the official repository. Nothing about
|
|
268
|
+
a recipe's files is special once they are on disk, and nothing distinguishes yours from anyone
|
|
269
|
+
else's.
|
|
270
|
+
|
|
271
|
+
## 11. Commit the lockfile
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
sous subscription list
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
Subscription Range Pinned version Origin Enabled
|
|
279
|
+
------------------- ----------- ------------------------- -------- -------
|
|
280
|
+
core 0.2.0 core/sous-skills 0.2.0 built in yes
|
|
281
|
+
workflow/example any version workflow/example 0.1.0 user yes
|
|
282
|
+
workflow/task-files any version workflow/task-files 1.0.2 user yes
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Everything sous needs to rebuild this project is under `.sous/`, and it is all committed:
|
|
286
|
+
|
|
287
|
+
| File | Holds |
|
|
288
|
+
|------|-------|
|
|
289
|
+
| `.sous/sous.config.yaml` | your project's own config |
|
|
290
|
+
| `.sous/sous.lock.json` | the exact version and content hash of every recipe in use |
|
|
291
|
+
| `.sous/conf.d/500-repos.jsonc` | the repositories the project trusts |
|
|
292
|
+
| `.sous/conf.d/510-subscriptions.jsonc` | what the project subscribes to |
|
|
293
|
+
| `.sous/.env` | the answers to the recipes' questions, minus the secret ones |
|
|
294
|
+
| the rest of `.sous/conf.d/` | the README sous writes there, and the agent pointers beside it |
|
|
295
|
+
|
|
296
|
+
Gitignore compiled output (`.claude/`), the state file `.sous/sous.state.json`, and the secrets
|
|
297
|
+
file `.sous/.env.local`. The store on your machine is not in the project at all.
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
git add .sous && git commit -m "Subscribe to task-files and my own example recipe"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## 12. Restore on a second machine
|
|
304
|
+
|
|
305
|
+
A clone has the lockfile and the subscriptions and no store at all. `sous build` restores exactly
|
|
306
|
+
what the lockfile pins, asking nothing:
|
|
307
|
+
|
|
308
|
+
```term
|
|
309
|
+
$ git clone git@github.com:my-team/my-project.git
|
|
310
|
+
>> 100%
|
|
311
|
+
$ cd my-project
|
|
312
|
+
$ sous build
|
|
313
|
+
▶ Restoring recipes:
|
|
314
|
+
This project's lockfile pins recipes that are not in the store on this machine,
|
|
315
|
+
so they are being fetched at exactly the versions it records.
|
|
316
|
+
restored: workflow/example
|
|
317
|
+
restored: workflow/sub-agent-delegation
|
|
318
|
+
restored: workflow/task-files
|
|
319
|
+
▶ Building:
|
|
320
|
+
✓ .claude/skills/about-task-files/SKILL.md (~942 tokens)
|
|
321
|
+
✓ Done.
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Nothing was resolved and nothing was asked, because the lockfile already said what to fetch; see
|
|
325
|
+
[The lockfile](repositories.md#the-lockfile) for why restore decides nothing.
|
|
326
|
+
|
|
327
|
+
?> The store those recipes were restored into, `$SOUS_HOME/cache`, is machine-wide and disposable;
|
|
328
|
+
everything in it is re-fetchable from a lockfile pin. `SOUS_HOME` moves it, and is what step 1 set
|
|
329
|
+
so this walkthrough keeps its own; unset, the store is `~/.sous/cache`. To undo everything above,
|
|
330
|
+
`rm -rf ~/projects/my-project ~/projects/my-recipes ~/tmp/sous-quickstart`; on a store you mean to
|
|
331
|
+
keep, `sous repo gc` reclaims what no lockfile pins instead.
|
|
332
|
+
|
|
333
|
+
## Where to go next
|
|
334
|
+
|
|
335
|
+
- [Consuming recipes](repositories-consuming.md): dry runs, one-word refs, removal, `repo gc`
|
|
336
|
+
- [Authoring a repository](repositories-authoring.md): variables, editing in place, contributing
|
|
337
|
+
- [Recipe variables](repositories-variables.md): the resolution ladder and the `sous vars` commands
|
|
338
|
+
- [Repositories](repositories.md): trust, providers, freshness, and what lives where
|
|
339
|
+
- [Command reference](commands.md): every command and flag
|
|
340
|
+
- [Troubleshooting](repositories-troubleshooting.md): what the errors mean and how to clear them
|