@sous-io/sous 0.2.25 → 0.2.27

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.
@@ -25,3 +25,5 @@
25
25
  - [0004: A global sous defers to the project's install](adrs/0004-project-install-handoff.md)
26
26
  - [0005: Follow-ups from the first project on the hand-off](adrs/0005-first-project-follow-ups.md)
27
27
  - [0006: `sous repo init` follows `sous-recipes`](adrs/0006-repo-init-model-home.md)
28
+ - [0007: Frozen index entries and tolerant readers](adrs/0007-frozen-index-dependencies.md)
29
+ - [0009: One whole build, everywhere](adrs/0009-whole-build-everywhere.md)
@@ -93,10 +93,21 @@ and the command you want almost always. Takes `--dry-run`.
93
93
 
94
94
  - `--no-prune`, `--no-compile`: skip one half of the run.
95
95
  - `--rebuild`: ignore cached hashes and reprocess every output.
96
- - `--strict`: fail on any compilation error rather than reporting it and continuing.
96
+ - `--strict`: treat compile warnings as errors (a `.tpl.` file copied without being rendered, a git branch the
97
+ runtime context could not read).
97
98
  - `-w, --watch`: rebuild on every change to a source file, a config layer or a linked checkout.
98
99
 
99
- Before it compiles, a build lists each recipe this project uses that has a newer version within the range
100
+ Before it compiles, a build prepares the project's recipes: it seeds the core recipe, pins any subscription the
101
+ lockfile does not pin yet, restores whatever the store on this machine is missing, and checks upstream for the
102
+ repositories that ask for newer versions. Every command that builds (`init`, `build`, `launch`, `prune`,
103
+ `repo remove`, `repo unlink`, `subscription add`, `subscription remove`, `subscription update`) runs this same
104
+ build, so each one prints the same things.
105
+
106
+ A compile error (a missing or circular include, a template that fails to render, a file sous cannot read) fails
107
+ the build. Every target is still compiled and every error is listed, then the build exits `1`. An output whose
108
+ target had an error is not written, so the copy from the last good build stays in place.
109
+
110
+ A build also lists each recipe this project uses that has a newer version within the range
100
111
  declared for it, beside the version pinned. It moves no pin; only always-pull moves one. The build reads upstream
101
112
  for this at most once per freshness window (`store.freshnessSeconds`, five minutes by default), gives a
102
113
  repository three seconds to answer, and otherwise answers from the cached index without a word about the
@@ -106,18 +117,22 @@ Example: `sous build --rebuild`
106
117
 
107
118
  ### `sous compile`
108
119
  Compiles markdown templates into output files, and prunes nothing. Takes `--rebuild`, `--strict`, `--dry-run`
109
- and `-w, --watch`, each meaning what it means on `build`. Example: `sous compile --strict`
120
+ and `-w, --watch`, each meaning what it means on `build`, and exits `1` after a compile error the way `build`
121
+ does. It does not prepare the recipes first. Example: `sous compile --strict`
110
122
 
111
123
  ### `sous prune`
112
- Removes output files no longer in the current config. Takes `--dry-run` only. Example: `sous prune --dry-run`
124
+ Removes output files no longer in the current config. It prepares the recipes first, as `build` does, because
125
+ what counts as current depends on the recipes the lockfile pins. Takes `--dry-run` only. Example:
126
+ `sous prune --dry-run`
113
127
 
114
128
  ### `sous clear`
115
129
  Deletes every file and directory sous has written for the project, and asks first; `-f, --force` answers that
116
130
  confirmation ahead of time. Example: `sous clear --force`
117
131
 
118
132
  ### `sous launch TOOL...`
119
- Builds this project's outputs, then starts a coding agent configured under `tools` in its config. `--no-build`
120
- launches without building; `--continuous` restarts the agent whenever it exits.
133
+ Builds this project's outputs, then starts a coding agent configured under `tools` in its config. A build that
134
+ fails starts nothing and exits `1`. `--no-build` launches without building; `--continuous` restarts the agent
135
+ whenever it exits.
121
136
 
122
137
  Any argument `launch` does not recognize is forwarded to the tool; a flag that collides with one of sous's own
123
138
  goes after a bare `--`: `sous launch claude --resume`, `sous launch claude -- -c`.
@@ -447,8 +462,9 @@ Example: `sous vars ask --namespace workflow --var apiUrl`
447
462
 
448
463
  A command that succeeds exits `0`. A command line sous could not parse (a missing argument, an unknown flag, a
449
464
  value outside a flag's options) exits `2`. Every other failure exits `1`. A broken config halts sous rather than
450
- producing output built on a guess. Compilation is the one place sous reports a failure and carries on;
451
- `--strict` on `build` and `compile` turns those reports into a failed run.
465
+ producing output built on a guess. A build with a compile error carries on long enough to compile every target
466
+ and list every error, then exits `1`, leaving the last good copy of each output that had an error;
467
+ `--strict` on `build` and `compile` makes compile warnings fail the run too.
452
468
 
453
469
  A failure prints one error block, in plain language, and nothing else; a usage mistake gets the command's own
454
470
  help under it, on standard error. An error sous raises names the cause and the cure:
@@ -143,7 +143,7 @@ variables:
143
143
  ## Declare dependencies and co-subscriptions
144
144
 
145
145
  `depends` fetches and pins a recipe whose files stay OUT of a subscriber's output but are addressable from your
146
- own templates. `subscribes` is a co-subscription: its questions run and its files DO enter the output, which is
146
+ own templates, and from the templates of any project whose lockfile pins it. `subscribes` is a co-subscription: its questions run and its files DO enter the output, which is
147
147
  how a curated bundle is built. Both hold plain strings naming their target by where it lives.
148
148
 
149
149
  ```yaml
@@ -240,9 +240,10 @@ this release's scope** is fine: the release depends on the last published versio
240
240
 
241
241
  Each version's resolved dependencies are written into the index, so a consumer installing that version gets what
242
242
  it was published with rather than ranges re-resolved months later. A published version never changes: its content
243
- hash is carried forward exactly as published, and a disagreement is an error telling you to bump rather than
244
- republish. A version counts as published once its tag exists, and the tags are the backstop, so a tagged version
245
- missing from the index is rebuilt from it whenever the index is regenerated, which only happens on a run that
243
+ hash and its dependencies are carried forward exactly as published, and a disagreement is an error telling you
244
+ to bump rather than republish ([what is checked](repositories-file-formats.md#sousindexjson)). A version counts
245
+ as published once its tag exists, and the tags are the backstop, so a tagged version missing from the index is
246
+ rebuilt from it, dependencies included, whenever the index is regenerated, which only happens on a run that
246
247
  publishes something. A bump edits the manifest in place, so comments, field order and layout survive; a folded
247
248
  block of YAML prose may be re-wrapped and the space before a trailing comment collapsed to one.
248
249
 
@@ -177,6 +177,22 @@ sibling, resolved exactly) or `range` plus `repo` (the identity of the repositor
177
177
  index resolves the range); at least one is required. Installing a version installs these rather than
178
178
  re-resolving the manifest's ranges.
179
179
 
180
+ **A published version's entry is frozen.** Its dependencies are resolved once, when the version is first
181
+ recorded, and every later release carries the entry forward exactly as published, like its hash; releasing
182
+ other recipes, adding a recipe to a namespace it subscribes to, or raising a sibling it names changes nothing
183
+ in it. A release still checks the recorded list against the version's manifest: a recipe the manifest names
184
+ that the list lacks, a recipe in the list the manifest does not declare, a sibling outside its declared
185
+ range, or another repository's entry with a different repository or range is an error naming the recipe, the
186
+ version and each difference; restore the entry if the index was edited, or bump the version to publish
187
+ different dependencies. A version rebuilt from its tag, when the index lost it, has its dependencies resolved
188
+ against the repository as it stood at that tag, then frozen the same way. An entry written before sous
189
+ recorded dependencies carries none, stays that way, and installs by resolving its manifest's ranges.
190
+
191
+ **Fields this sous does not know are kept, not refused.** A later sous may add fields to the index, at any
192
+ level. This one reads such an index, validates every field it defines as strictly as before, and ignores the
193
+ rest; a release it runs carries a published version's entry forward with those fields intact. A sous older
194
+ than the one that began tolerating unknown fields refuses the whole index once a repository publishes one.
195
+
180
196
  **The `seeded` field.** Sous ships the `core` recipe in its own npm package, so a project can build before
181
197
  reaching the network. That copy is folded into the official repository's index in memory and resolved like
182
198
  any other, marked `seeded: true`; if the repository already publishes it, the published copy wins. Sous
@@ -331,9 +331,26 @@ published identity and then the path inside it, on a line of its own:
331
331
  ```
332
332
 
333
333
  The `~` is required. A bare `@path` is always a relative path or a declared alias, so an include
334
- line can never quietly stop meaning a file on disk. Inside a recipe, `~<namespace>` resolves only
335
- against that recipe's own `depends` and `subscribes` at the versions the lockfile pins, and the
336
- path after the recipe name may not be absolute or hold a `.` or `..` segment.
334
+ line can never quietly stop meaning a file on disk. In your own templates, `~<namespace>`
335
+ resolves against every recipe the lockfile pins, whatever brought it in: a recipe you subscribe
336
+ to, a member of a set you subscribe to, or a library another recipe depends on. Inside a recipe,
337
+ it resolves only against that recipe's own `depends` and `subscribes` at the versions the
338
+ lockfile pins. The path after the recipe name may not be absolute or hold a `.` or `..` segment.
339
+
340
+ An include naming a recipe the lockfile does not pin fails the build. When sous can tell what
341
+ used to bring the recipe in (a switched-off subscription, or an earlier version of a set that
342
+ listed it), the error names it:
343
+
344
+ ```text
345
+ Error: Include not found: @~workflow/member/memories/member.md
346
+ in file: .sous/prompts/AGENTS.md
347
+ namespace: workflow
348
+ recipe: workflow/member
349
+ The namespace "workflow" holds no recipe named "workflow/member".
350
+ Version 1.0.0 of "omakase/house" brought "workflow/member" in, but the version this project
351
+ pins, 1.1.0, does not, so the lockfile no longer pins it.
352
+ Subscribe to "workflow/member" directly to keep including it, or remove the include.
353
+ ```
337
354
 
338
355
  ## Where to go next
339
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
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",
@@ -148,8 +148,9 @@ written `@~<namespace>/<recipe>/<path inside that recipe>`:
148
148
 
149
149
  Scoping is deliberate. A file inside a recipe may address that recipe itself plus the
150
150
  recipes it declares under `depends` or `subscribes`; a file in the project's own templates
151
- may address the project's subscriptions. Anything else is an error naming what was missing,
152
- so a reference can never quietly pick up a recipe nobody asked for.
151
+ may address every recipe the project's lockfile pins, whatever brought it in. Anything else
152
+ is an error naming what was missing, so a reference can never quietly pick up a recipe
153
+ nobody asked for.
153
154
 
154
155
  Built-in **aliases** are reserved, always begin with `~`, and are consulted before recipe
155
156
  namespaces. There is exactly one:
@@ -11,7 +11,7 @@ formatVersion: 1
11
11
 
12
12
  namespace: core
13
13
  name: sous-skills
14
- version: 0.2.25
14
+ version: 0.2.27
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous
@@ -1,23 +1,14 @@
1
1
  import { Flags } from "@oclif/core";
2
2
  import { BaseCommand } from "../base-command.js";
3
- import { BuildService } from "../lib/build-service.js";
3
+ import { runProjectBuild } from "../lib/build-service.js";
4
4
  import { PidService } from "../lib/pid-service.js";
5
5
  import { resolveRootScope } from "../lib/settings.js";
6
- import { describeLinkedRepos } from "../lib/repos/links.js";
7
6
  import { resolveStoreSettings } from "../lib/repos/store/settings.js";
8
- import { prepareRepositoriesForBuild } from "../lib/build-preparation.js";
9
7
  import { subscriptionServiceFor } from "../lib/repos/subscription-service.js";
10
8
  import { buildReloadWatchConfig, startConfigReloadWatch } from "../lib/watch-loop.js";
11
9
  import type { WatchHandle } from "../lib/watch-service.js";
12
10
  import { WatchService } from "../lib/watch-service.js";
13
- import {
14
- footer,
15
- heading,
16
- log,
17
- paragraph,
18
- showCommandVars,
19
- warning,
20
- } from "../utils/formatting.js";
11
+ import { log, paragraph, showCommandVars } from "../utils/formatting.js";
21
12
 
22
13
  export default class Build extends BaseCommand {
23
14
  static description =
@@ -49,7 +40,7 @@ export default class Build extends BaseCommand {
49
40
  default: false,
50
41
  }),
51
42
  strict: Flags.boolean({
52
- description: "Fail on any compilation error",
43
+ description: "Treat compile warnings as errors",
53
44
  default: false,
54
45
  }),
55
46
  watch: Flags.boolean({
@@ -71,37 +62,20 @@ export default class Build extends BaseCommand {
71
62
  "No Prune": flags["no-prune"],
72
63
  });
73
64
 
74
- // A linked repository is read from a working copy instead of a published
75
- // version, so it is announced every single time; a build that silently
76
- // produced something different would be far worse than a noisy one.
77
- const linked = describeLinkedRepos(this.configContext.sousDir);
78
- if (linked.length > 0) warning(linked.join("\n"));
79
-
80
- const repositories = subscriptionServiceFor({
81
- configContext: this.configContext,
82
- settings: this.settings,
83
- shellEnv: this.shellEnv,
84
- });
85
-
86
- if (!flags["dry-run"] && !flags["no-compile"]) {
87
- await prepareRepositoriesForBuild(repositories);
88
- }
89
-
90
- heading("Building");
91
-
92
65
  const buildOptions = {
93
66
  strict: flags.strict,
94
67
  rebuild: flags.rebuild,
95
68
  dryRun: flags["dry-run"],
96
69
  noCompile: flags["no-compile"],
97
70
  noPrune: flags["no-prune"],
98
- configContext: this.configContext,
99
71
  };
100
72
 
101
- const buildService = new BuildService();
102
- const success = await buildService.build(this.settings, buildOptions);
103
-
104
- footer();
73
+ const success = await runProjectBuild({
74
+ settings: this.settings,
75
+ configContext: this.configContext,
76
+ shellEnv: this.shellEnv,
77
+ options: buildOptions,
78
+ });
105
79
 
106
80
  if (!success && !flags.watch) {
107
81
  this.exit(1);
@@ -129,17 +103,23 @@ export default class Build extends BaseCommand {
129
103
 
130
104
  const watchService = new WatchService();
131
105
 
132
- // Reruns compile + prune with the command's current settings. Called for
106
+ // Reruns the build with the command's current settings. Called for
133
107
  // partial rebuilds (with the changed file) and, after a clean reload, for
134
- // full rebuilds. Owns the "Rebuilding" heading/footer.
108
+ // full rebuilds, which prepare the recipes again the way the first build did.
135
109
  const rebuild = async (changedFile?: string) => {
136
- heading("Rebuilding");
137
- await buildService.build(this.settings, {
138
- ...buildOptions,
139
- // --rebuild means full clean build on every trigger; skip partial optimisation
140
- changedFile: buildOptions.rebuild ? undefined : changedFile,
110
+ // A failed rebuild has already listed its errors, and the watch keeps
111
+ // running so the next save can fix them.
112
+ await runProjectBuild({
113
+ settings: this.settings,
114
+ configContext: this.configContext,
115
+ shellEnv: this.shellEnv,
116
+ heading: "Rebuilding",
117
+ options: {
118
+ ...buildOptions,
119
+ // --rebuild means full clean build on every trigger; skip partial optimisation
120
+ changedFile: buildOptions.rebuild ? undefined : changedFile,
121
+ },
141
122
  });
142
- footer();
143
123
  };
144
124
 
145
125
  const { handle, triggerFullRebuild } = startConfigReloadWatch({
@@ -154,6 +134,11 @@ export default class Build extends BaseCommand {
154
134
  // and a failure never breaks the watch; the last good answer stands.
155
135
  const pollSeconds = resolveStoreSettings(this.settings).watchPollSeconds;
156
136
  if (pollSeconds > 0) {
137
+ const repositories = subscriptionServiceFor({
138
+ configContext: this.configContext,
139
+ settings: this.settings,
140
+ shellEnv: this.shellEnv,
141
+ });
157
142
  const poll = setInterval(() => {
158
143
  void repositories
159
144
  .checkUpstream()
@@ -34,7 +34,7 @@ export default class Compile extends BaseCommand {
34
34
  static flags = {
35
35
  ...BaseCommand.baseFlags,
36
36
  strict: Flags.boolean({
37
- description: "Fail immediately on any error",
37
+ description: "Treat compile warnings as errors",
38
38
  default: false,
39
39
  }),
40
40
  rebuild: Flags.boolean({
@@ -113,7 +113,7 @@ export default class Compile extends BaseCommand {
113
113
 
114
114
  footer();
115
115
 
116
- if (!success && flags.strict && !flags.watch) {
116
+ if (!success && !flags.watch) {
117
117
  this.exit(1);
118
118
  }
119
119
 
@@ -17,8 +17,7 @@ import path from "node:path";
17
17
  import { Args, Flags } from "@oclif/core";
18
18
  import { confirm } from "@inquirer/prompts";
19
19
  import { BaseCommand } from "../base-command.js";
20
- import { prepareRepositoriesForBuild } from "../lib/build-preparation.js";
21
- import { buildProjectOutputs } from "../lib/build-service.js";
20
+ import { runProjectBuild } from "../lib/build-service.js";
22
21
  import {
23
22
  CONFIG_FILE_NAMES,
24
23
  SOUS_DIR_NAME,
@@ -28,7 +27,6 @@ import {
28
27
  } from "../lib/config-discovery.js";
29
28
  import { ConfigError } from "../lib/errors.js";
30
29
  import { nonInteractiveError } from "../lib/interactive.js";
31
- import { subscriptionServiceFor } from "../lib/repos/subscription-service.js";
32
30
  import {
33
31
  PROJECT_CONFIG_FORMATS,
34
32
  SOUS_PACKAGE_NAME,
@@ -45,7 +43,6 @@ import {
45
43
  blankLine,
46
44
  dryRunNotice,
47
45
  footer,
48
- heading,
49
46
  log,
50
47
  paragraph,
51
48
  section,
@@ -262,8 +259,8 @@ export default class Init extends BaseCommand {
262
259
  }
263
260
 
264
261
  /**
265
- * Adopts the config just written and builds the project with it, in the
266
- * same two steps `sous build` takes: the repositories are prepared (which
262
+ * Adopts the config just written and builds the project with it, through
263
+ * the same build `sous build` runs: the repositories are prepared (which
267
264
  * seeds the core recipe into the store and pins it in the lockfile), then
268
265
  * the outputs are compiled.
269
266
  *
@@ -274,17 +271,12 @@ export default class Init extends BaseCommand {
274
271
  resolveConfigFlag(configPath, this.configLocator.cwd, this.configLocator.confDirOverride)
275
272
  );
276
273
 
277
- await prepareRepositoriesForBuild(
278
- subscriptionServiceFor({
279
- configContext: this.configContext,
280
- settings: this.settings,
281
- shellEnv: this.shellEnv,
282
- })
283
- );
284
-
285
- heading("Building the project");
286
-
287
- const succeeded = await buildProjectOutputs(this.settings, this.configContext);
274
+ const succeeded = await runProjectBuild({
275
+ settings: this.settings,
276
+ configContext: this.configContext,
277
+ shellEnv: this.shellEnv,
278
+ heading: "Building the project",
279
+ });
288
280
 
289
281
  if (!succeeded) {
290
282
  throw new ConfigError(
@@ -2,7 +2,8 @@ import { Args, Flags } from "@oclif/core";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import { BaseCommand } from "../base-command.js";
5
- import { BuildService } from "../lib/build-service.js";
5
+ import { runProjectBuild } from "../lib/build-service.js";
6
+ import { ConfigError } from "../lib/errors.js";
6
7
  import { resolveRootScope, resolveTools } from "../lib/settings.js";
7
8
  import { displayError, footer, heading, showCommandVars } from "../utils/formatting.js";
8
9
 
@@ -85,16 +86,21 @@ export default class Launch extends BaseCommand {
85
86
  ...(passThroughArgs.length > 0 && { "Tool Args": passThroughArgs.join(" ") }),
86
87
  });
87
88
 
88
- const buildService = new BuildService();
89
-
90
89
  do {
91
- // Build step (unless --no-build)
90
+ // Build step (unless --no-build). A failed build stops here: the agent
91
+ // would otherwise start on outputs the build could not bring up to date.
92
92
  if (!flags["no-build"]) {
93
- heading("Building");
94
- await buildService.build(this.settings, {
93
+ const built = await runProjectBuild({
94
+ settings: this.settings,
95
95
  configContext: this.configContext,
96
+ shellEnv: this.shellEnv,
96
97
  });
97
- footer();
98
+ if (!built) {
99
+ throw new ConfigError(
100
+ `The build failed, so ${args.tool} was not started. Fix the errors listed ` +
101
+ `above, or pass --no-build to start it with the outputs as they are.`
102
+ );
103
+ }
98
104
  }
99
105
 
100
106
  // Config-defined args first, then pass-through, then promptFile content
@@ -1,7 +1,7 @@
1
1
  import { Flags } from "@oclif/core";
2
2
  import { BaseCommand } from "../base-command.js";
3
- import { BuildService, resolveStateFilePath } from "../lib/build-service.js";
4
- import { footer, heading, showCommandVars } from "../utils/formatting.js";
3
+ import { runProjectBuild } from "../lib/build-service.js";
4
+ import { showCommandVars } from "../utils/formatting.js";
5
5
 
6
6
  export default class Prune extends BaseCommand {
7
7
  static description = "Remove output files that are no longer in the current config";
@@ -22,19 +22,22 @@ export default class Prune extends BaseCommand {
22
22
  async run(): Promise<void> {
23
23
  const { flags } = await this.parse(Prune);
24
24
 
25
- const stateFilePath = resolveStateFilePath(this.settings, this.configContext);
26
-
27
25
  showCommandVars({
28
26
  Project: this.projectLabel,
29
27
  Config: this.configContext.configPath,
30
28
  "Dry Run": flags["dry-run"],
31
29
  });
32
30
 
33
- heading("Pruning");
34
-
35
- const buildService = new BuildService();
36
- await buildService.prune(this.settings, stateFilePath, flags["dry-run"], this.configContext);
37
-
38
- footer();
31
+ // Prune goes through the same build path as everything else, with the
32
+ // compile step switched off: what counts as current depends on the recipes
33
+ // the lockfile pins, so they are restored first, or a recipe missing from
34
+ // the store would have its outputs pruned as if it were gone.
35
+ await runProjectBuild({
36
+ settings: this.settings,
37
+ configContext: this.configContext,
38
+ shellEnv: this.shellEnv,
39
+ heading: "Pruning",
40
+ options: { noCompile: true, dryRun: flags["dry-run"] },
41
+ });
39
42
  }
40
43
  }
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { Args, Flags } from "@oclif/core";
16
16
  import { BaseCommand } from "../../base-command.js";
17
- import { buildProjectOutputs } from "../../lib/build-service.js";
17
+ import { runProjectBuild } from "../../lib/build-service.js";
18
18
  import { ConfigError } from "../../lib/errors.js";
19
19
  import { resolveRootScope } from "../../lib/settings.js";
20
20
  import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
@@ -175,11 +175,12 @@ export default class RepoRemove extends BaseCommand {
175
175
  private async rebuildProject(name: string): Promise<void> {
176
176
  await this.reloadDiscoveredConfig();
177
177
 
178
- heading("Building the project");
179
-
180
- const succeeded = await buildProjectOutputs(this.settings, this.configContext);
181
-
182
- footer();
178
+ const succeeded = await runProjectBuild({
179
+ settings: this.settings,
180
+ configContext: this.configContext,
181
+ shellEnv: this.shellEnv,
182
+ heading: "Building the project",
183
+ });
183
184
 
184
185
  if (!succeeded) {
185
186
  throw new ConfigError(
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { Args, Flags } from "@oclif/core";
4
4
  import { BaseCommand } from "../../base-command.js";
5
- import { buildProjectOutputs } from "../../lib/build-service.js";
5
+ import { runProjectBuild } from "../../lib/build-service.js";
6
6
  import { ConfigError, isConfigError } from "../../lib/errors.js";
7
7
  import {
8
8
  NonInteractiveError,
@@ -409,11 +409,12 @@ export default class RepoUnlink extends BaseCommand {
409
409
  private async rebuildProject(name: string): Promise<void> {
410
410
  await this.reloadDiscoveredConfig();
411
411
 
412
- heading("Building the project");
413
-
414
- const succeeded = await buildProjectOutputs(this.settings, this.configContext);
415
-
416
- footer();
412
+ const succeeded = await runProjectBuild({
413
+ settings: this.settings,
414
+ configContext: this.configContext,
415
+ shellEnv: this.shellEnv,
416
+ heading: "Building the project",
417
+ });
417
418
 
418
419
  if (!succeeded) {
419
420
  throw new ConfigError(
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { Args, Flags } from "@oclif/core";
26
26
  import { BaseCommand } from "../../base-command.js";
27
- import { buildProjectOutputs } from "../../lib/build-service.js";
27
+ import { runProjectBuild } from "../../lib/build-service.js";
28
28
  import { ConfigError } from "../../lib/errors.js";
29
29
  import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
30
30
  import { formatAskReport } from "../../lib/vars/ask.js";
@@ -267,11 +267,12 @@ export default class SubscriptionAdd extends BaseCommand {
267
267
  private async rebuildProject(key: string): Promise<void> {
268
268
  await this.reloadDiscoveredConfig();
269
269
 
270
- heading("Building the project");
271
-
272
- const succeeded = await buildProjectOutputs(this.settings, this.configContext);
273
-
274
- footer();
270
+ const succeeded = await runProjectBuild({
271
+ settings: this.settings,
272
+ configContext: this.configContext,
273
+ shellEnv: this.shellEnv,
274
+ heading: "Building the project",
275
+ });
275
276
 
276
277
  if (!succeeded) {
277
278
  throw new ConfigError(
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { Args, Flags } from "@oclif/core";
14
14
  import { BaseCommand } from "../../base-command.js";
15
- import { buildProjectOutputs } from "../../lib/build-service.js";
15
+ import { runProjectBuild } from "../../lib/build-service.js";
16
16
  import { ConfigError } from "../../lib/errors.js";
17
17
  import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
18
18
  import { renderTable, type TableColumn } from "../../utils/table.js";
@@ -163,11 +163,12 @@ export default class SubscriptionRemove extends BaseCommand {
163
163
  private async rebuildProject(key: string): Promise<void> {
164
164
  await this.reloadDiscoveredConfig();
165
165
 
166
- heading("Building the project");
167
-
168
- const succeeded = await buildProjectOutputs(this.settings, this.configContext);
169
-
170
- footer();
166
+ const succeeded = await runProjectBuild({
167
+ settings: this.settings,
168
+ configContext: this.configContext,
169
+ shellEnv: this.shellEnv,
170
+ heading: "Building the project",
171
+ });
171
172
 
172
173
  if (!succeeded) {
173
174
  throw new ConfigError(
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { Args, Flags } from "@oclif/core";
21
21
  import { BaseCommand } from "../../base-command.js";
22
- import { buildProjectOutputs } from "../../lib/build-service.js";
22
+ import { runProjectBuild } from "../../lib/build-service.js";
23
23
  import { ConfigError } from "../../lib/errors.js";
24
24
  import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
25
25
  import { describeUpdateScope } from "../../lib/repos/update-plan.js";
@@ -198,11 +198,12 @@ export default class SubscriptionUpdate extends BaseCommand {
198
198
  private async rebuildProject(): Promise<void> {
199
199
  await this.reloadDiscoveredConfig();
200
200
 
201
- heading("Building the project");
202
-
203
- const succeeded = await buildProjectOutputs(this.settings, this.configContext);
204
-
205
- footer();
201
+ const succeeded = await runProjectBuild({
202
+ settings: this.settings,
203
+ configContext: this.configContext,
204
+ shellEnv: this.shellEnv,
205
+ heading: "Building the project",
206
+ });
206
207
 
207
208
  if (!succeeded) {
208
209
  throw new ConfigError(
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * The step that runs before a project's outputs are compiled, and what it says
3
- * while it runs. `sous build` runs it on every build, and `sous init` runs it
4
- * for the first build of a project it has just set up; both must say the same
5
- * things about the same events, so the reporting lives here rather than in
6
- * either command.
3
+ * while it runs. `runProjectBuild` (`build-service.ts`) calls it, and every
4
+ * command that builds goes through that one function, so the step and its
5
+ * reporting are the same whichever command the user typed.
7
6
  */
8
7
 
9
8
  import type { SubscriptionService } from "./repos/subscription-service.js";