@sous-io/sous 0.2.3 → 0.2.6
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 +27 -11
- package/bin/run.js +32 -20
- package/docs/markdown/README.md +31 -0
- package/docs/markdown/_sidebar.md +2 -0
- package/docs/markdown/commands.md +27 -1
- package/docs/markdown/config-discovery.md +3 -2
- package/docs/markdown/config-inspection.md +4 -1
- package/docs/markdown/configuration.md +2 -1
- package/docs/markdown/repositories-quickstart.md +4 -1
- package/package.json +1 -1
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/base-command.ts +76 -8
- package/src/commands/build.ts +3 -80
- package/src/commands/init.ts +269 -0
- package/src/lib/build-preparation.ts +86 -0
- package/src/lib/config-discovery.ts +2 -16
- package/src/lib/project-install.d.mts +31 -0
- package/src/lib/project-install.mjs +175 -0
- package/src/lib/project-scaffold/index.ts +251 -0
- package/src/lib/project-scaffold/templates.ts +230 -0
- package/src/lib/repos/links.ts +3 -1
package/README.md
CHANGED
|
@@ -26,12 +26,19 @@ formats agents actually read: `.claude/` plus `CLAUDE.md` for Claude Code, `.cod
|
|
|
26
26
|
|
|
27
27
|
## Quickstart
|
|
28
28
|
|
|
29
|
-
Install the CLI:
|
|
29
|
+
Install the CLI globally, or add it to a project, or both:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npm install -g @sous-io/sous
|
|
32
|
+
npm install -g @sous-io/sous # one sous for every project on the machine
|
|
33
|
+
npm install -D @sous-io/sous # the version this project's templates were written against
|
|
33
34
|
```
|
|
34
35
|
|
|
36
|
+
A project install pins the sous version a project builds with, and it always does the
|
|
37
|
+
building: a global `sous` run anywhere inside a project that holds `node_modules/@sous-io/sous`
|
|
38
|
+
hands the command to that copy, so `sous`, `npx sous` and a package script all produce the same
|
|
39
|
+
output. When the two versions differ, one line on standard error says which copy ran. Set
|
|
40
|
+
`SOUS_NO_DELEGATE=1` to run the copy you invoked instead.
|
|
41
|
+
|
|
35
42
|
Or run it from a clone (useful when developing sous itself):
|
|
36
43
|
|
|
37
44
|
```bash
|
|
@@ -41,16 +48,22 @@ npm install
|
|
|
41
48
|
npm link
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
Then set up a project
|
|
45
|
-
`sous.config.js`, `sous.config.mjs`, `sous.config.json`, or `sous.config.yaml`:
|
|
51
|
+
Then set up a project:
|
|
46
52
|
|
|
47
53
|
```bash
|
|
48
54
|
cd /path/to/your/project
|
|
49
|
-
|
|
50
|
-
$EDITOR .sous/sous.config.js
|
|
55
|
+
sous init
|
|
51
56
|
```
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
`sous init` writes the project's `.sous/` directory and runs the first build. It creates a
|
|
59
|
+
commented `sous.config.js` (pass `--format json` for a JSON config bound to the shipped
|
|
60
|
+
schema), a starter prompt at `.sous/prompts/AGENTS.md` that the config compiles to `AGENTS.md`
|
|
61
|
+
at the project root, the two env files described below, and a `.sous/.gitignore` covering the
|
|
62
|
+
files sous keeps local to one machine. The first build compiles the starter prompt and the
|
|
63
|
+
`core` skills every project gets, and pins them in `.sous/sous.lock.json`. A project that
|
|
64
|
+
already holds a config is left untouched.
|
|
65
|
+
|
|
66
|
+
The config it writes compiles one file:
|
|
54
67
|
|
|
55
68
|
```js
|
|
56
69
|
export const config = {
|
|
@@ -59,17 +72,19 @@ export const config = {
|
|
|
59
72
|
compilation: {
|
|
60
73
|
targets: [
|
|
61
74
|
{
|
|
62
|
-
entryPoint: "${sousDir}/AGENTS.md",
|
|
63
|
-
outputs: [{ destinationFile: "${projectRoot}/
|
|
75
|
+
entryPoint: "${sousDir}/prompts/AGENTS.md",
|
|
76
|
+
outputs: [{ destinationFile: "${projectRoot}/AGENTS.md" }],
|
|
64
77
|
},
|
|
65
78
|
],
|
|
66
79
|
},
|
|
80
|
+
recipeOutputs: { skills: ["${projectRoot}/.claude/skills"] },
|
|
67
81
|
};
|
|
68
82
|
```
|
|
69
83
|
|
|
70
84
|
`${sousDir}` is the `.sous/` directory sous found, so a config can name paths relative to
|
|
71
|
-
itself without hardcoding anything machine-specific. One config describes one project.
|
|
72
|
-
|
|
85
|
+
itself without hardcoding anything machine-specific. One config describes one project. A
|
|
86
|
+
`.sous/` may hold one config file, named `sous.config.js`, `sous.config.mjs`,
|
|
87
|
+
`sous.config.json`, `sous.config.jsonc` or `sous.config.yaml`. After editing, build:
|
|
73
88
|
|
|
74
89
|
```bash
|
|
75
90
|
sous build
|
|
@@ -95,6 +110,7 @@ Useful commands:
|
|
|
95
110
|
|
|
96
111
|
| Command | What it does |
|
|
97
112
|
|---|---|
|
|
113
|
+
| `sous init` | Set a project up: write `.sous/`, then run the first build |
|
|
98
114
|
| `sous build` | Compile, then prune stale outputs |
|
|
99
115
|
| `sous build --watch` | Rebuild on source changes |
|
|
100
116
|
| `sous compile` | Compile only |
|
package/bin/run.js
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { handOffToProjectInstall, isEnvFlagOn } from "../src/lib/project-install.mjs";
|
|
3
5
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
6
|
+
// A project that installs @sous-io/sous itself has pinned the version its
|
|
7
|
+
// templates and lockfile were written against, so that copy does the work.
|
|
8
|
+
// This runs before anything else is loaded: when a project copy is found, its
|
|
9
|
+
// own bin is imported into this process and this one loads nothing further
|
|
10
|
+
// (see src/lib/project-install.mjs for the rules, and SOUS_NO_DELEGATE to
|
|
11
|
+
// keep the invoked copy running).
|
|
12
|
+
const ownRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const handedOff = await handOffToProjectInstall({ ownRoot });
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
if (!handedOff) {
|
|
16
|
+
// The CLI runs from TypeScript source; register tsx before oclif dynamically
|
|
17
|
+
// imports any command module. Resolving "tsx" from this file (rather than a
|
|
18
|
+
// $PKG_ROOT/node_modules path) works in every install layout: repo clone,
|
|
19
|
+
// global install (nested deps), and local/npx installs (hoisted deps).
|
|
20
|
+
const { register } = await import("tsx/esm/api");
|
|
21
|
+
register();
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
// machinery is redundant; leaving it on makes every downstream run warn that
|
|
14
|
-
// the (unshipped) typescript devDependency is missing.
|
|
15
|
-
settings.enableAutoTranspile = false;
|
|
23
|
+
const { execute, settings } = await import("@oclif/core");
|
|
16
24
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
// reporting then prints its stack too.
|
|
22
|
-
const debugRequested = !["", "0", "false", "no", "off"].includes(
|
|
23
|
-
(process.env.SOUS_DEBUG ?? "").trim().toLowerCase()
|
|
24
|
-
);
|
|
25
|
+
// tsx (above) already makes .ts imports work, so oclif's own auto-transpile
|
|
26
|
+
// machinery is redundant; leaving it on makes every downstream run warn that
|
|
27
|
+
// the (unshipped) typescript devDependency is missing.
|
|
28
|
+
settings.enableAutoTranspile = false;
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
// oclif's development mode turns on its debug setting, which makes every error
|
|
31
|
+
// it prints a raw stack trace. Sous reports its own errors as sentences (see
|
|
32
|
+
// src/utils/command-errors.ts), so development mode is switched on only when
|
|
33
|
+
// SOUS_DEBUG asks for the traces; anything that gets past a command's own
|
|
34
|
+
// reporting then prints its stack too.
|
|
35
|
+
const debugRequested = isEnvFlagOn(process.env.SOUS_DEBUG);
|
|
36
|
+
|
|
37
|
+
await execute({ development: debugRequested, dir: import.meta.url });
|
|
38
|
+
}
|
package/docs/markdown/README.md
CHANGED
|
@@ -17,6 +17,37 @@ compiled 4 targets, pruned 1 stale file
|
|
|
17
17
|
?> These docs are young. **Configuration** and **Repositories** are the reference material so
|
|
18
18
|
far; more will follow.
|
|
19
19
|
|
|
20
|
+
## Installing
|
|
21
|
+
|
|
22
|
+
Sous installs three ways, and they work together:
|
|
23
|
+
|
|
24
|
+
- **Globally** (`npm install -g @sous-io/sous`): one `sous` on the path for every project on the
|
|
25
|
+
machine, and the one to reach for first.
|
|
26
|
+
- **In a project** (`npm install -D @sous-io/sous`): the project pins the version its templates
|
|
27
|
+
and its lockfile were written against, and `npx sous` or a package script runs it. This is the
|
|
28
|
+
right choice for a team, because the implicit `core` subscription asks for exactly the running
|
|
29
|
+
version and a project built by two versions in turn rewrites its committed lockfile back and
|
|
30
|
+
forth.
|
|
31
|
+
- **Both**: a global `sous` run anywhere inside a project that holds `node_modules/@sous-io/sous`
|
|
32
|
+
hands the whole command to that copy before loading any of its own code, so the project's
|
|
33
|
+
version always does the building however it was invoked. The lookup walks up from the working
|
|
34
|
+
directory the way Node resolves a package, so a copy hoisted to a monorepo root is found from
|
|
35
|
+
any package inside it.
|
|
36
|
+
|
|
37
|
+
When the two versions differ, one sentence on standard error names the copy that ran and the
|
|
38
|
+
one you invoked; standard output is untouched, so a piped command prints exactly what it always
|
|
39
|
+
did. Set `SOUS_DEBUG` and the sentence prints on every hand-off. Set `SOUS_NO_DELEGATE` to
|
|
40
|
+
anything but `0`, `false`, `no` or `off` to run the copy you invoked instead, for debugging a
|
|
41
|
+
broken project install or for deliberately using the global one:
|
|
42
|
+
|
|
43
|
+
```term
|
|
44
|
+
$ sous --version
|
|
45
|
+
Running the project's own sous 0.2.4 from /work/app/node_modules/@sous-io/sous instead of the sous 0.3.0 you invoked; set SOUS_NO_DELEGATE=1 to run the one you invoked.
|
|
46
|
+
@sous-io/sous/0.2.4 linux-x64 node-v22.21.0
|
|
47
|
+
$ SOUS_NO_DELEGATE=1 sous --version
|
|
48
|
+
@sous-io/sous/0.3.0 linux-x64 node-v22.21.0
|
|
49
|
+
```
|
|
50
|
+
|
|
20
51
|
## Where to look
|
|
21
52
|
|
|
22
53
|
- Watch the [animated introduction](../) for the full pitch
|
|
@@ -20,3 +20,5 @@
|
|
|
20
20
|
- **ADRs**
|
|
21
21
|
- [0001: Repositories](adrs/0001-repositories.md)
|
|
22
22
|
- [0002: Recipe answers in the template scope](adrs/0002-recipe-answers-in-templates.md)
|
|
23
|
+
- [0003: Project setup with sous init](adrs/0003-project-init.md)
|
|
24
|
+
- [0004: A global sous defers to the project's install](adrs/0004-project-install-handoff.md)
|
|
@@ -46,6 +46,27 @@ Every topic answers to both spellings of its name: `repo` and `repos`, `subscrip
|
|
|
46
46
|
|
|
47
47
|
## Top-level commands
|
|
48
48
|
|
|
49
|
+
### `sous init [DIRECTORY]`
|
|
50
|
+
Sets a project up for sous: writes its `.sous/` directory, then runs the first build. It is the one command that
|
|
51
|
+
runs before a config exists, and the starting point for a project that has never used sous. It writes a commented
|
|
52
|
+
primary config, a starter prompt at `.sous/prompts/AGENTS.md` that the config compiles to `AGENTS.md` at the
|
|
53
|
+
project root, `.sous/.env` and `.sous/.env.local.example`, and the sous-managed block in `.sous/.gitignore`. The
|
|
54
|
+
first build compiles the starter prompt and the `core` skills, and pins them in `.sous/sous.lock.json`.
|
|
55
|
+
|
|
56
|
+
A project whose `.sous/` already holds a primary config is refused, and nothing is written. Setting up a directory
|
|
57
|
+
inside a project that is already set up is a question rather than an error, since a subproject may want its own
|
|
58
|
+
instructions; `-y, --yes` answers it ahead of time, and a run with no terminal fails naming that flag.
|
|
59
|
+
|
|
60
|
+
- `DIRECTORY`: the project directory to set up; the current one by default. `--sous-dir` and `SOUS_DIR` also say
|
|
61
|
+
where, when no directory is given.
|
|
62
|
+
- `--format <js|json>`: which config to write, `js` by default. The JSON config carries `$schema`, bound to the
|
|
63
|
+
schema artifact published for the running sous version.
|
|
64
|
+
- `--name <name>`: the display name written into the config; the directory's own name by default.
|
|
65
|
+
- `--no-build`: write the setup without running the first build.
|
|
66
|
+
- `--dry-run`: print the files that would be written without writing them.
|
|
67
|
+
|
|
68
|
+
Example: `sous init --format json`
|
|
69
|
+
|
|
49
70
|
### `sous build`
|
|
50
71
|
Compiles this project's outputs, then removes the ones its config no longer produces. It is compile plus prune,
|
|
51
72
|
and the command you want almost always. Takes `--dry-run`.
|
|
@@ -317,7 +338,12 @@ $ sous subscription add workflow --non-interactive
|
|
|
317
338
|
No expected failure prints a stack trace. A failure sous did not expect prints the message and one more sentence
|
|
318
339
|
asking you to set `SOUS_DEBUG=1` and run the command again. Set `SOUS_DEBUG` to anything but `0`, `false`, `no`
|
|
319
340
|
or `off` and every reported failure prints its stack to standard error underneath the message:
|
|
320
|
-
`SOUS_DEBUG=1 sous build`.
|
|
341
|
+
`SOUS_DEBUG=1 sous build`. The one other thing it changes is that every hand-off to a project's own install
|
|
342
|
+
announces itself, not only one between different versions.
|
|
343
|
+
|
|
344
|
+
`SOUS_NO_DELEGATE` is the other environment variable every command reads. Set it the same way and the copy of
|
|
345
|
+
sous you invoked runs the command, even inside a project that installs its own `@sous-io/sous`; see
|
|
346
|
+
[Installing](README.md#installing) for the hand-off it switches off.
|
|
321
347
|
|
|
322
348
|
Every listing fits itself to the terminal it runs in: columns shrink, descriptions wrap, and a path or URL is
|
|
323
349
|
cut in the middle so the host and the last segment both survive. On a terminal too narrow, the least important
|
|
@@ -57,8 +57,9 @@ first, so setting a location var inside `.env.local` has no effect on discovery.
|
|
|
57
57
|
|
|
58
58
|
All are hard `ConfigError`s; sous never guesses:
|
|
59
59
|
|
|
60
|
-
- **No config found**: the error lists every directory checked during the walk and
|
|
61
|
-
|
|
60
|
+
- **No config found**: the error lists every directory checked during the walk and names the two
|
|
61
|
+
fixes: run `sous init` in the project's root directory, which writes the config and everything
|
|
62
|
+
else a first build needs, or pass `--config <path>`.
|
|
62
63
|
- **Multiple primary configs**: two or more of `sous.config.js|mjs|json|jsonc|yaml` in the same
|
|
63
64
|
`.sous/` is an error naming every candidate.
|
|
64
65
|
- **Duplicate layer baseNames**: any two loaded files (primary or conf.d) whose names differ
|
|
@@ -27,7 +27,10 @@ tooling:
|
|
|
27
27
|
{ "$schema": "./path/to/sous.config.schema.json" }
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The `$schema` key is accepted at the top level and ignored by sous itself.
|
|
30
|
+
The `$schema` key is accepted at the top level and ignored by sous itself. A config written by
|
|
31
|
+
`sous init --format json` binds it to the same artifact as published on GitHub for the running
|
|
32
|
+
version, so an editor resolves it without a path into the installed package; point it at the
|
|
33
|
+
package-local copy instead when the editor should work offline.
|
|
31
34
|
|
|
32
35
|
## sous config show
|
|
33
36
|
|
|
@@ -39,7 +39,8 @@ export const config = {
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
A JSON config may set `"$schema"` to bind the `sous.config.schema.json` artifact shipped with
|
|
42
|
-
sous for editor autocompletion; sous accepts and ignores the key.
|
|
42
|
+
sous for editor autocompletion; sous accepts and ignores the key. `sous init --format json`
|
|
43
|
+
writes one already bound to the copy of that artifact published for the running version.
|
|
43
44
|
|
|
44
45
|
?> Configs use `${var}` syntax. Template files use LiquidJS double-brace syntax instead; the two
|
|
45
46
|
are resolved at different stages and never mix.
|
|
@@ -24,7 +24,10 @@ echo 'name: my-project' > .sous/sous.config.yaml
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
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/`.
|
|
27
|
+
lockfile, the state file and the config layers sous writes land under `.sous/`. Outside a
|
|
28
|
+
walkthrough, `sous init` writes a fuller starting point (a commented config, a starter prompt,
|
|
29
|
+
the env files and the ignore block) and runs the first build for you; the one-line config is
|
|
30
|
+
used here so that each step shows one thing. `SOUS_HOME` puts
|
|
28
31
|
the machine-wide store somewhere throwaway, so this walkthrough leaves your real one alone. See
|
|
29
32
|
[The config file](configuration.md) for the keys you will want later.
|
|
30
33
|
|
package/package.json
CHANGED
package/src/base-command.ts
CHANGED
|
@@ -40,6 +40,23 @@ import { reportCommandError } from "./utils/command-errors.js";
|
|
|
40
40
|
* lives in `lib/interactive.ts`, which also treats a truthy `CI` and a
|
|
41
41
|
* non-terminal stdin or stdout the same way.
|
|
42
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* What the config-locating flags and environment said on this run, resolved to
|
|
45
|
+
* absolute paths but not yet turned into a config.
|
|
46
|
+
*/
|
|
47
|
+
export type ConfigLocator = {
|
|
48
|
+
/** The working directory discovery started from. */
|
|
49
|
+
cwd: string;
|
|
50
|
+
/**
|
|
51
|
+
* The explicit config location, when one was given: the resolved path and
|
|
52
|
+
* the flag or environment variable that supplied it (`--config`,
|
|
53
|
+
* `--sous-config`, `SOUS_CONFIG`, `--sous-dir` or `SOUS_DIR`).
|
|
54
|
+
*/
|
|
55
|
+
primary?: { value: string; source: string };
|
|
56
|
+
/** The conf.d directory override, when `--sous-confd` or `SOUS_CONFD` gave one. */
|
|
57
|
+
confDirOverride?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
43
60
|
export abstract class BaseCommand extends Command {
|
|
44
61
|
static baseFlags = {
|
|
45
62
|
config: Flags.string({
|
|
@@ -60,11 +77,29 @@ export abstract class BaseCommand extends Command {
|
|
|
60
77
|
"non-interactive": nonInteractiveFlag(),
|
|
61
78
|
};
|
|
62
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Whether a run of this command needs a project config to exist. Every
|
|
82
|
+
* command that works on a project leaves this true, and a run that finds no
|
|
83
|
+
* config fails before `run()` with the "No sous config found" block. The one
|
|
84
|
+
* command whose job is to CREATE the config sets it false: discovery still
|
|
85
|
+
* runs, so the config-locating flags still say where the project is, but
|
|
86
|
+
* finding nothing is the expected case rather than an error, and `settings`
|
|
87
|
+
* stays unset until `adoptConfig` is given the config that was written.
|
|
88
|
+
*/
|
|
89
|
+
static requiresConfig = true;
|
|
90
|
+
|
|
63
91
|
protected settings!: Settings;
|
|
64
92
|
|
|
65
93
|
/** Where the active config was found. */
|
|
66
94
|
protected configContext!: ConfigContext;
|
|
67
95
|
|
|
96
|
+
/**
|
|
97
|
+
* What the config-locating flags and environment said, before discovery
|
|
98
|
+
* turned it into a config. A command that runs without a config reads the
|
|
99
|
+
* project's location from here.
|
|
100
|
+
*/
|
|
101
|
+
protected configLocator!: ConfigLocator;
|
|
102
|
+
|
|
68
103
|
/** The full discovery result, including how the config was located. */
|
|
69
104
|
protected discovered!: DiscoveredConfig;
|
|
70
105
|
|
|
@@ -142,6 +177,16 @@ export abstract class BaseCommand extends Command {
|
|
|
142
177
|
[sousDirEnv, "SOUS_DIR"],
|
|
143
178
|
];
|
|
144
179
|
const primary = primaryCandidates.find(([value]) => value !== undefined);
|
|
180
|
+
const requiresConfig = (this.constructor as typeof BaseCommand).requiresConfig;
|
|
181
|
+
|
|
182
|
+
this.configLocator = {
|
|
183
|
+
cwd,
|
|
184
|
+
primary:
|
|
185
|
+
primary !== undefined
|
|
186
|
+
? { value: path.resolve(cwd, expandHome(primary[0] as string)), source: primary[1] }
|
|
187
|
+
: undefined,
|
|
188
|
+
confDirOverride,
|
|
189
|
+
};
|
|
145
190
|
|
|
146
191
|
let discovered: DiscoveredConfig | null;
|
|
147
192
|
|
|
@@ -150,6 +195,9 @@ export abstract class BaseCommand extends Command {
|
|
|
150
195
|
try {
|
|
151
196
|
discovered = resolveConfigFlag(primarySource, cwd, confDirOverride, sourceLabel);
|
|
152
197
|
} catch (error) {
|
|
198
|
+
// A command that creates the config is pointed at a place with none in
|
|
199
|
+
// it by design; the flag still says where, so this is not a failure.
|
|
200
|
+
if (!requiresConfig) return;
|
|
153
201
|
displayError(error instanceof Error ? error.message : String(error), this.errorSink);
|
|
154
202
|
return this.exit(1);
|
|
155
203
|
}
|
|
@@ -158,10 +206,29 @@ export abstract class BaseCommand extends Command {
|
|
|
158
206
|
}
|
|
159
207
|
|
|
160
208
|
if (!discovered) {
|
|
209
|
+
if (!requiresConfig) return;
|
|
161
210
|
displayErrorBlock(formatNotFoundMessage(), this.errorSink);
|
|
162
211
|
return this.exit(1);
|
|
163
212
|
}
|
|
164
213
|
|
|
214
|
+
await this.adoptConfig(discovered);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** True once a config has been discovered or adopted and its settings loaded. */
|
|
218
|
+
protected get hasConfig(): boolean {
|
|
219
|
+
return this.discovered !== undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Makes a discovered config THE config for the rest of the run: loads its
|
|
224
|
+
* env files, enumerates its layers again now that `SOUS_HOME` may have
|
|
225
|
+
* changed, and loads the settings. `init()` calls it for the config discovery
|
|
226
|
+
* found; a command that creates a config calls it for the one it wrote, so
|
|
227
|
+
* both go through the same steps in the same order.
|
|
228
|
+
*
|
|
229
|
+
* @param discovered - The config to adopt.
|
|
230
|
+
*/
|
|
231
|
+
protected async adoptConfig(discovered: DiscoveredConfig): Promise<void> {
|
|
165
232
|
// Inject .sous/.env.local and .sous/.env before anything resolves variables,
|
|
166
233
|
// keeping a copy of what the shell itself set so the variables layer can
|
|
167
234
|
// still tell the two apart.
|
|
@@ -172,27 +239,28 @@ export abstract class BaseCommand extends Command {
|
|
|
172
239
|
// file-settable, and it decides where the store holding a subscribed
|
|
173
240
|
// recipe's config layers is. A first pass already ran during discovery, when
|
|
174
241
|
// only the real environment was known.
|
|
242
|
+
let refreshed: DiscoveredConfig;
|
|
175
243
|
try {
|
|
176
|
-
|
|
244
|
+
refreshed = refreshDiscoveredConfig(discovered);
|
|
177
245
|
} catch (error) {
|
|
178
246
|
displayErrorBlock(error instanceof Error ? error.message : String(error), this.errorSink);
|
|
179
247
|
return this.exit(1);
|
|
180
248
|
}
|
|
181
249
|
|
|
182
|
-
this.discovered =
|
|
250
|
+
this.discovered = refreshed;
|
|
183
251
|
this.configContext = {
|
|
184
|
-
sousDir:
|
|
185
|
-
configPath:
|
|
186
|
-
confDir:
|
|
187
|
-
layerPaths:
|
|
252
|
+
sousDir: refreshed.sousDir,
|
|
253
|
+
configPath: refreshed.configPath,
|
|
254
|
+
confDir: refreshed.confDir,
|
|
255
|
+
layerPaths: refreshed.layerPaths,
|
|
188
256
|
};
|
|
189
257
|
|
|
190
258
|
// Routed through the command's error sink, not stdout: a recipe layer
|
|
191
259
|
// warning must not land in the middle of `sous config show | jq`.
|
|
192
|
-
for (const notice of
|
|
260
|
+
for (const notice of refreshed.recipeLayerWarnings) warning(notice, this.errorSink);
|
|
193
261
|
|
|
194
262
|
try {
|
|
195
|
-
this.settings = await loadSettings(
|
|
263
|
+
this.settings = await loadSettings(refreshed);
|
|
196
264
|
} catch (error) {
|
|
197
265
|
displayErrorBlock(error instanceof Error ? error.message : String(error), this.errorSink);
|
|
198
266
|
return this.exit(1);
|
package/src/commands/build.ts
CHANGED
|
@@ -5,15 +5,12 @@ import { PidService } from "../lib/pid-service.js";
|
|
|
5
5
|
import { resolveRootScope } from "../lib/settings.js";
|
|
6
6
|
import { describeLinkedRepos } from "../lib/repos/links.js";
|
|
7
7
|
import { resolveStoreSettings } from "../lib/repos/store/settings.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
type SubscriptionService,
|
|
11
|
-
} from "../lib/repos/subscription-service.js";
|
|
8
|
+
import { prepareRepositoriesForBuild } from "../lib/build-preparation.js";
|
|
9
|
+
import { subscriptionServiceFor } from "../lib/repos/subscription-service.js";
|
|
12
10
|
import { buildReloadWatchConfig, startConfigReloadWatch } from "../lib/watch-loop.js";
|
|
13
11
|
import type { WatchHandle } from "../lib/watch-service.js";
|
|
14
12
|
import { WatchService } from "../lib/watch-service.js";
|
|
15
13
|
import {
|
|
16
|
-
blankLine,
|
|
17
14
|
footer,
|
|
18
15
|
heading,
|
|
19
16
|
log,
|
|
@@ -87,7 +84,7 @@ export default class Build extends BaseCommand {
|
|
|
87
84
|
});
|
|
88
85
|
|
|
89
86
|
if (!flags["dry-run"] && !flags["no-compile"]) {
|
|
90
|
-
await
|
|
87
|
+
await prepareRepositoriesForBuild(repositories);
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
heading("Building");
|
|
@@ -196,78 +193,4 @@ export default class Build extends BaseCommand {
|
|
|
196
193
|
await new Promise(() => {}); // keep process alive
|
|
197
194
|
}
|
|
198
195
|
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Gets this project's recipes ready to compile: restores whatever the store is
|
|
202
|
-
* missing (a fresh clone, or a collected store) and then asks upstream for the
|
|
203
|
-
* repositories that prefer a newer in-range version.
|
|
204
|
-
*
|
|
205
|
-
* Restoring asks nothing and decides nothing; it fetches exactly what the
|
|
206
|
-
* lockfile pins. An upstream check that fails is reported and then ignored,
|
|
207
|
-
* because a build must not depend on the network being up.
|
|
208
|
-
*
|
|
209
|
-
* @param repositories - The subscription service for this project.
|
|
210
|
-
*/
|
|
211
|
-
private async prepareRepositories(repositories: SubscriptionService): Promise<void> {
|
|
212
|
-
const needsRestore = repositories.needsRestore();
|
|
213
|
-
if (needsRestore) {
|
|
214
|
-
heading("Restoring recipes");
|
|
215
|
-
blankLine();
|
|
216
|
-
paragraph(
|
|
217
|
-
"This project's lockfile pins recipes that are not in the store on this " +
|
|
218
|
-
"machine, so they are being fetched at exactly the versions it records."
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const { seed, subscriptions, restored, upstream } =
|
|
223
|
-
await repositories.prepareForBuild();
|
|
224
|
-
|
|
225
|
-
// Seeding the packaged core recipe is silent when it works, which is almost
|
|
226
|
-
// always; it is only worth a word when it could not be done at all.
|
|
227
|
-
if (seed.skippedBecause !== undefined) warning(seed.skippedBecause);
|
|
228
|
-
|
|
229
|
-
// A subscription the lockfile did not pin yet has just been pinned. That is
|
|
230
|
-
// a change to a committed file, so it is always announced.
|
|
231
|
-
if (subscriptions.added.length > 0 || subscriptions.moved.length > 0) {
|
|
232
|
-
heading("Locking subscribed recipes");
|
|
233
|
-
blankLine();
|
|
234
|
-
for (const entry of subscriptions.added) {
|
|
235
|
-
paragraph(` pinned: ${entry.key} at version ${entry.version}.`);
|
|
236
|
-
}
|
|
237
|
-
for (const change of subscriptions.moved) {
|
|
238
|
-
paragraph(` ${change.key} moved from version ${change.from} to version ${change.to}.`);
|
|
239
|
-
}
|
|
240
|
-
blankLine();
|
|
241
|
-
paragraph(
|
|
242
|
-
"The lockfile has been updated. Commit it, so everyone building this project " +
|
|
243
|
-
"gets exactly these versions."
|
|
244
|
-
);
|
|
245
|
-
footer();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
for (const failure of subscriptions.failed) {
|
|
249
|
-
warning(
|
|
250
|
-
`Sous could not work out which version of '${failure.key}' to use, so nothing ` +
|
|
251
|
-
`from it was compiled.\n${failure.reason}`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (restored !== undefined && restored.restored.length > 0) {
|
|
256
|
-
blankLine();
|
|
257
|
-
for (const key of restored.restored) paragraph(` restored: ${key}`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
for (const change of upstream.updated) {
|
|
261
|
-
paragraph(` ${change.key} moved from ${change.from} to ${change.to}.`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
for (const failure of upstream.failed) {
|
|
265
|
-
warning(
|
|
266
|
-
`Sous could not check the repository '${failure.repo}' for a newer version, so ` +
|
|
267
|
-
`this build uses the versions it already had.\n${failure.reason}`
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (needsRestore) footer();
|
|
272
|
-
}
|
|
273
196
|
}
|