@sous-io/sous 0.2.37 → 1.0.1

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.
Files changed (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1093 -0
  4. package/docs/markdown/README.md +5 -4
  5. package/docs/markdown/_sidebar.md +21 -1
  6. package/docs/markdown/commands.md +294 -431
  7. package/docs/markdown/config-discovery.md +25 -14
  8. package/docs/markdown/config-inspection.md +53 -31
  9. package/docs/markdown/config-layers.md +168 -66
  10. package/docs/markdown/config-variables.md +44 -11
  11. package/docs/markdown/configuration.md +104 -17
  12. package/docs/markdown/design-principles.md +4 -1
  13. package/docs/markdown/repositories-authoring.md +30 -0
  14. package/docs/markdown/repositories-consuming.md +72 -14
  15. package/docs/markdown/repositories-file-formats.md +31 -13
  16. package/docs/markdown/repositories-providers.md +48 -57
  17. package/docs/markdown/repositories-quickstart.md +3 -2
  18. package/docs/markdown/repositories-troubleshooting.md +27 -1
  19. package/docs/markdown/repositories-variables.md +6 -1
  20. package/docs/markdown/repositories.md +18 -8
  21. package/package.json +48 -107
  22. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
  23. package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
  24. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
  25. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
  26. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
  27. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  28. package/sous.config.schema.json +351 -132
  29. package/bin/run.js +0 -51
  30. package/src/base-command.ts +0 -457
  31. package/src/commands/build.ts +0 -181
  32. package/src/commands/clear.ts +0 -79
  33. package/src/commands/compile.ts +0 -155
  34. package/src/commands/config/get.ts +0 -110
  35. package/src/commands/config/show.ts +0 -32
  36. package/src/commands/config/validate.ts +0 -53
  37. package/src/commands/help.ts +0 -46
  38. package/src/commands/init.ts +0 -289
  39. package/src/commands/launch.ts +0 -139
  40. package/src/commands/lock/rebuild.ts +0 -239
  41. package/src/commands/lock/show.ts +0 -115
  42. package/src/commands/namespace/list.ts +0 -138
  43. package/src/commands/namespace/show.ts +0 -130
  44. package/src/commands/prune.ts +0 -43
  45. package/src/commands/recipe/list.ts +0 -112
  46. package/src/commands/recipe/show.ts +0 -439
  47. package/src/commands/repo/add.ts +0 -145
  48. package/src/commands/repo/contribute.ts +0 -604
  49. package/src/commands/repo/gc.ts +0 -173
  50. package/src/commands/repo/init.ts +0 -136
  51. package/src/commands/repo/link.ts +0 -899
  52. package/src/commands/repo/list.ts +0 -236
  53. package/src/commands/repo/release.ts +0 -685
  54. package/src/commands/repo/remove.ts +0 -194
  55. package/src/commands/repo/search.ts +0 -243
  56. package/src/commands/repo/submit.ts +0 -341
  57. package/src/commands/repo/unlink.ts +0 -461
  58. package/src/commands/subscription/add.ts +0 -286
  59. package/src/commands/subscription/list.ts +0 -208
  60. package/src/commands/subscription/remove.ts +0 -182
  61. package/src/commands/subscription/update.ts +0 -216
  62. package/src/commands/update.ts +0 -178
  63. package/src/commands/vars/ask.ts +0 -417
  64. package/src/commands/vars/index.ts +0 -79
  65. package/src/commands/vars/list.ts +0 -67
  66. package/src/commands/vars/show.ts +0 -77
  67. package/src/config-command.ts +0 -30
  68. package/src/lib/build-preparation.ts +0 -128
  69. package/src/lib/build-service.ts +0 -580
  70. package/src/lib/config-discovery.ts +0 -378
  71. package/src/lib/config-inspect.ts +0 -145
  72. package/src/lib/config-kernel.mjs +0 -379
  73. package/src/lib/config-schema.ts +0 -401
  74. package/src/lib/env-file.ts +0 -328
  75. package/src/lib/env-local.ts +0 -212
  76. package/src/lib/errors.ts +0 -32
  77. package/src/lib/glob-pattern.ts +0 -24
  78. package/src/lib/include-resolver.ts +0 -523
  79. package/src/lib/interactive.ts +0 -165
  80. package/src/lib/markdown-compiler.ts +0 -947
  81. package/src/lib/package-info.ts +0 -25
  82. package/src/lib/pid-service.ts +0 -99
  83. package/src/lib/project-install.d.mts +0 -44
  84. package/src/lib/project-install.mjs +0 -234
  85. package/src/lib/project-scaffold/index.ts +0 -332
  86. package/src/lib/project-scaffold/templates.ts +0 -240
  87. package/src/lib/repos/catalog-display.ts +0 -215
  88. package/src/lib/repos/catalog-inputs.ts +0 -288
  89. package/src/lib/repos/catalog.ts +0 -842
  90. package/src/lib/repos/contribute.ts +0 -452
  91. package/src/lib/repos/core-recipe.ts +0 -105
  92. package/src/lib/repos/declarations.ts +0 -187
  93. package/src/lib/repos/defaults.ts +0 -175
  94. package/src/lib/repos/formats/common.ts +0 -440
  95. package/src/lib/repos/formats/index-file.ts +0 -253
  96. package/src/lib/repos/formats/links-map.ts +0 -98
  97. package/src/lib/repos/formats/lockfile.ts +0 -169
  98. package/src/lib/repos/formats/patterns.ts +0 -57
  99. package/src/lib/repos/formats/recipe-manifest.ts +0 -205
  100. package/src/lib/repos/formats/repo-manifest.ts +0 -97
  101. package/src/lib/repos/formats/store-entry.ts +0 -84
  102. package/src/lib/repos/formats/variable-definition.ts +0 -268
  103. package/src/lib/repos/freshness.ts +0 -264
  104. package/src/lib/repos/git-clone.ts +0 -850
  105. package/src/lib/repos/identity.ts +0 -89
  106. package/src/lib/repos/index.ts +0 -57
  107. package/src/lib/repos/links.ts +0 -356
  108. package/src/lib/repos/load-manifest.ts +0 -237
  109. package/src/lib/repos/lock-service.ts +0 -454
  110. package/src/lib/repos/locked-namespace-resolver.ts +0 -232
  111. package/src/lib/repos/locked-recipe-lookup.ts +0 -123
  112. package/src/lib/repos/locked-recipes.ts +0 -254
  113. package/src/lib/repos/managed-layer.ts +0 -422
  114. package/src/lib/repos/memories-hash-name.ts +0 -33
  115. package/src/lib/repos/namespace-resolver.ts +0 -527
  116. package/src/lib/repos/providers/base.ts +0 -268
  117. package/src/lib/repos/providers/git.ts +0 -245
  118. package/src/lib/repos/providers/github.ts +0 -589
  119. package/src/lib/repos/providers/gitlab.ts +0 -332
  120. package/src/lib/repos/providers/http.ts +0 -128
  121. package/src/lib/repos/providers/index-cache.ts +0 -421
  122. package/src/lib/repos/providers/index.ts +0 -106
  123. package/src/lib/repos/providers/local.ts +0 -410
  124. package/src/lib/repos/providers/provider.ts +0 -615
  125. package/src/lib/repos/recipe-config-layers.ts +0 -316
  126. package/src/lib/repos/recipe-key-matcher.ts +0 -62
  127. package/src/lib/repos/recipe-memories.ts +0 -178
  128. package/src/lib/repos/recipe-targets.ts +0 -236
  129. package/src/lib/repos/ref-search.ts +0 -46
  130. package/src/lib/repos/release/bump.ts +0 -161
  131. package/src/lib/repos/release/changelog.ts +0 -461
  132. package/src/lib/repos/release/git-state.ts +0 -460
  133. package/src/lib/repos/release/index-builder.ts +0 -1108
  134. package/src/lib/repos/release/index.ts +0 -19
  135. package/src/lib/repos/release/plan.ts +0 -521
  136. package/src/lib/repos/release/settle.ts +0 -154
  137. package/src/lib/repos/release/submissions.ts +0 -214
  138. package/src/lib/repos/release/submit-checkout.ts +0 -290
  139. package/src/lib/repos/release/submit-questions.ts +0 -153
  140. package/src/lib/repos/release/submit-service.ts +0 -898
  141. package/src/lib/repos/release/tags.ts +0 -239
  142. package/src/lib/repos/release/validate.ts +0 -463
  143. package/src/lib/repos/resolver.ts +0 -1028
  144. package/src/lib/repos/scaffold/index.ts +0 -245
  145. package/src/lib/repos/scaffold/templates.ts +0 -517
  146. package/src/lib/repos/seed.ts +0 -473
  147. package/src/lib/repos/store/contract.ts +0 -64
  148. package/src/lib/repos/store/hash.ts +0 -191
  149. package/src/lib/repos/store/recipe-store.ts +0 -599
  150. package/src/lib/repos/store/settings.ts +0 -58
  151. package/src/lib/repos/subscription-service.ts +0 -3859
  152. package/src/lib/repos/trust.ts +0 -447
  153. package/src/lib/repos/update-plan.ts +0 -235
  154. package/src/lib/self-update/index.ts +0 -14
  155. package/src/lib/self-update/installs.ts +0 -413
  156. package/src/lib/self-update/managers.ts +0 -126
  157. package/src/lib/self-update/plan.ts +0 -315
  158. package/src/lib/self-update/registry.ts +0 -99
  159. package/src/lib/self-update/run.ts +0 -506
  160. package/src/lib/self-update/versions.ts +0 -243
  161. package/src/lib/settings.ts +0 -1107
  162. package/src/lib/sous-home.ts +0 -104
  163. package/src/lib/state.ts +0 -167
  164. package/src/lib/vars/answers.ts +0 -192
  165. package/src/lib/vars/ask.ts +0 -1152
  166. package/src/lib/vars/definition-source.ts +0 -261
  167. package/src/lib/vars/display.ts +0 -231
  168. package/src/lib/vars/index.ts +0 -20
  169. package/src/lib/vars/ladder.ts +0 -282
  170. package/src/lib/vars/mappings.ts +0 -260
  171. package/src/lib/vars/names.ts +0 -94
  172. package/src/lib/vars/preanswers.ts +0 -395
  173. package/src/lib/vars/question-plan.ts +0 -241
  174. package/src/lib/vars/report.ts +0 -228
  175. package/src/lib/vars/safe-regex.ts +0 -235
  176. package/src/lib/vars/secrets.ts +0 -131
  177. package/src/lib/vars/validate.ts +0 -312
  178. package/src/lib/version-report.ts +0 -90
  179. package/src/lib/watch-loop.ts +0 -148
  180. package/src/lib/watch-service.ts +0 -116
  181. package/src/services/ref-resolver/container.ts +0 -107
  182. package/src/services/ref-resolver/dependency.ts +0 -60
  183. package/src/services/ref-resolver/format.ts +0 -197
  184. package/src/services/ref-resolver/glob.ts +0 -115
  185. package/src/services/ref-resolver/hash-names.ts +0 -155
  186. package/src/services/ref-resolver/index.ts +0 -55
  187. package/src/services/ref-resolver/injectable.ts +0 -36
  188. package/src/services/ref-resolver/location.ts +0 -62
  189. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
  190. package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
  191. package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
  192. package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
  193. package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
  194. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
  195. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
  196. package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
  197. package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
  198. package/src/services/ref-resolver/named-refs.ts +0 -144
  199. package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
  200. package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
  201. package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
  202. package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
  203. package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
  204. package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
  205. package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
  206. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
  207. package/src/services/ref-resolver/parts.ts +0 -115
  208. package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
  209. package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
  210. package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
  211. package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
  212. package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
  213. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
  214. package/src/services/ref-resolver/ref-helpers.ts +0 -62
  215. package/src/services/ref-resolver/ref-picker-service.ts +0 -204
  216. package/src/services/ref-resolver/ref-report.ts +0 -84
  217. package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
  218. package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
  219. package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
  220. package/src/services/ref-resolver/source.ts +0 -48
  221. package/src/services/ref-resolver/tokens.ts +0 -25
  222. package/src/services/ref-resolver/types.ts +0 -155
  223. package/src/templating/filters/bullet-list.ts +0 -9
  224. package/src/templating/filters/index.ts +0 -8
  225. package/src/templating/init-liquid-engine.ts +0 -139
  226. package/src/templating/lib/glob-files.ts +0 -74
  227. package/src/templating/lib/import-export.ts +0 -32
  228. package/src/templating/lib/secret-scope.ts +0 -41
  229. package/src/templating/lib/tag-args.ts +0 -19
  230. package/src/templating/tags/exportScalarVarsJs.ts +0 -46
  231. package/src/templating/tags/getFiles.ts +0 -89
  232. package/src/templating/tags/index.ts +0 -14
  233. package/src/templating/tags/listFiles.ts +0 -54
  234. package/src/templating/tags/showVars.ts +0 -28
  235. package/src/utils/choice-prompt.ts +0 -143
  236. package/src/utils/command-errors.ts +0 -186
  237. package/src/utils/command-help.ts +0 -45
  238. package/src/utils/confirm-prompt.ts +0 -110
  239. package/src/utils/flags.ts +0 -177
  240. package/src/utils/formatting.ts +0 -849
  241. package/src/utils/prompts.ts +0 -53
  242. package/src/utils/sous-directory.ts +0 -245
  243. package/src/utils/table.ts +0 -603
  244. package/src/utils/value-prompt.ts +0 -119
@@ -1,580 +0,0 @@
1
- import path from "node:path";
2
- import fs from "node:fs";
3
- import type { ConfigContext, Settings } from "./settings.js";
4
- import { resolveAliases, resolveCompilation, resolveRootScope } from "./settings.js";
5
- import { resolveIncludeFiles, type ViewMap } from "./include-resolver.js";
6
- import type { NamespaceResolver } from "./repos/namespace-resolver.js";
7
- import { createProjectNamespaceResolver } from "./repos/locked-namespace-resolver.js";
8
- import { buildRecipeTargets, type RecipeTargets } from "./repos/recipe-targets.js";
9
- import { CompilationService, INCLUDE_LINE_PATTERN, resolveOutputPath } from "./markdown-compiler.js";
10
- import type { CompilationConfig, CompilationTarget } from "./markdown-compiler.js";
11
- import { StateService } from "./state.js";
12
- import { isProtectedPath } from "./state.js";
13
- import { describeLinkedRepos, protectedRepoPaths } from "./repos/links.js";
14
- import { subscriptionServiceFor } from "./repos/subscription-service.js";
15
- import { prepareRepositoriesForBuild } from "./build-preparation.js";
16
- import {
17
- noRecipeAnswers,
18
- resolveRecipeAnswers,
19
- unansweredWarning,
20
- type RecipeAnswers,
21
- } from "./vars/answers.js";
22
- import { BULLET, footer, heading, log, warning } from "../utils/formatting.js";
23
- import { sharedHashNames } from "../services/ref-resolver/index.js";
24
- import { listMemories, type MemoryFile } from "./repos/recipe-memories.js";
25
-
26
- export type BuildOptions = {
27
- strict?: boolean;
28
- rebuild?: boolean;
29
- dryRun?: boolean;
30
- noCompile?: boolean;
31
- noPrune?: boolean;
32
- /**
33
- * When set, only targets that transitively include this file are compiled.
34
- * All other targets are skipped. If no targets include this file, compilation
35
- * is skipped entirely.
36
- */
37
- changedFile?: string;
38
- /**
39
- * Where the active config was discovered. Threaded into variable resolution so
40
- * `${sousDir}` resolves and so state/PID paths default into `.sous/`.
41
- */
42
- configContext?: ConfigContext;
43
- /**
44
- * Resolves `~namespace` include and render paths against recipe namespaces.
45
- * Passed straight through to the compiler and to the include-graph walk, so a
46
- * partial rebuild follows namespace includes too.
47
- *
48
- * When it is omitted and `configContext` is given, the build builds the
49
- * project's own resolver from its lockfile, links map and store. Pass one
50
- * explicitly to override that, which is what tests do.
51
- */
52
- namespaceResolver?: NamespaceResolver;
53
- };
54
-
55
- /** An empty recipe-target result, for a build with no config context. */
56
- const NO_RECIPE_TARGETS: RecipeTargets = {
57
- targets: [],
58
- destinations: [],
59
- watchDirs: [],
60
- warnings: [],
61
- };
62
-
63
- /**
64
- * The answers to every recipe variable in play for the project the context
65
- * describes, resolved once so a build can render with them, hand them to every
66
- * scope it builds, and report what is still unanswered. Empty when the caller
67
- * gave no config context.
68
- *
69
- * @param settings - The merged project config.
70
- * @param configContext - Where the active config was discovered.
71
- */
72
- export function resolveProjectAnswers(
73
- settings: Settings,
74
- configContext?: ConfigContext
75
- ): RecipeAnswers {
76
- if (configContext === undefined) return noRecipeAnswers();
77
- return resolveRecipeAnswers({ settings, sousDir: configContext.sousDir });
78
- }
79
-
80
- /**
81
- * The compile targets a project's subscribed recipes contribute, for the project
82
- * the options describe. Empty when the caller gave no config context, which is
83
- * the case only in tests that build a settings object by hand.
84
- *
85
- * Each recipe's files render with that recipe's own view of the answers, and a
86
- * required variable nothing answered is reported through `warnings`, so every
87
- * caller that prints those tells the user what the build could not fill in.
88
- *
89
- * @param settings - The merged project config.
90
- * @param rootScope - The resolved settings scope, for `${var}` in destinations.
91
- * @param configContext - Where the active config was discovered.
92
- * @param answers - The project's recipe answers, when the caller resolved them
93
- * already; resolved here otherwise.
94
- */
95
- export function resolveRecipeTargets(
96
- settings: Settings,
97
- rootScope: Record<string, string>,
98
- configContext?: ConfigContext,
99
- answers?: RecipeAnswers
100
- ): RecipeTargets {
101
- if (configContext === undefined) return NO_RECIPE_TARGETS;
102
- const resolved = answers ?? resolveProjectAnswers(settings, configContext);
103
- const scopes = new Map<string, Record<string, string>>();
104
-
105
- const recipes = buildRecipeTargets({
106
- sousDir: configContext.sousDir,
107
- settings,
108
- scope: rootScope,
109
- scopeFor: (recipe) => {
110
- let scope = scopes.get(recipe.key);
111
- if (scope === undefined) {
112
- scope = resolveRootScope(settings, configContext, { answers: resolved, recipe: recipe.key });
113
- scopes.set(recipe.key, scope);
114
- }
115
- return scope;
116
- },
117
- });
118
-
119
- const missing = unansweredWarning(resolved, rootScope);
120
- if (missing !== undefined) recipes.warnings.push(missing);
121
- return { ...recipes, secretVariables: [...resolved.secrets] };
122
- }
123
-
124
- /**
125
- * Adds the recipe targets to a project's own compilation config. A project with
126
- * no compilation block of its own still compiles its recipes, so the config is
127
- * created when there is none and there is something to compile. The names the
128
- * recipes declare secret go onto the config either way, because a project's own
129
- * template may dump the scope too.
130
- *
131
- * @param config - The project's own compilation config, or null when it has none.
132
- * @param recipes - The targets the subscribed recipes contribute.
133
- * @param settings - The merged project config, for its aliases.
134
- * @param rootScope - The resolved settings scope.
135
- */
136
- export function withRecipeTargets(
137
- config: CompilationConfig | null,
138
- recipes: RecipeTargets,
139
- settings: Settings,
140
- rootScope: Record<string, string>
141
- ): CompilationConfig | null {
142
- const secrets = recipes.secretVariables?.length ? { secretVariables: recipes.secretVariables } : {};
143
- if (recipes.targets.length === 0) return config === null ? null : { ...config, ...secrets };
144
- if (config === null) {
145
- return {
146
- targets: recipes.targets,
147
- aliases: resolveAliases(settings, rootScope),
148
- includeScope: rootScope,
149
- ...secrets,
150
- };
151
- }
152
- return { ...config, targets: [...config.targets, ...recipes.targets], ...secrets };
153
- }
154
-
155
- /**
156
- * The memories an active recipe publishes that no output of the build included
157
- * by any route (an exact path, a glob or the `#memories` view). Recipes under
158
- * `recipes.memories.exclude` are never listed.
159
- *
160
- * @param settings - The merged project config, for `recipes.memories`.
161
- * @param sousDir - The project's `.sous/` directory.
162
- * @param included - Every file the compile read, as real paths.
163
- */
164
- export function unincludedMemories(
165
- settings: Settings,
166
- sousDir: string,
167
- included: ReadonlySet<string>
168
- ): MemoryFile[] {
169
- const memories = settings.recipes?.memories;
170
- return listMemories({ sousDir, first: memories?.first, exclude: memories?.exclude }).filter(
171
- (memory) => !included.has(realPath(memory.file))
172
- );
173
- }
174
-
175
- /** The path with symbolic links resolved, or the path itself when it cannot be read. */
176
- function realPath(filePath: string): string {
177
- try {
178
- return fs.realpathSync(filePath);
179
- } catch {
180
- return filePath;
181
- }
182
- }
183
-
184
- /**
185
- * The warning for memories no output included, or undefined when there are none.
186
- *
187
- * @param missing - What {@link unincludedMemories} found.
188
- */
189
- export function unincludedMemoriesWarning(missing: readonly MemoryFile[]): string | undefined {
190
- if (missing.length === 0) return undefined;
191
- const noun = missing.length === 1 ? "memory" : "memories";
192
- return (
193
- `${missing.length} ${noun} published by your subscribed recipes ${
194
- missing.length === 1 ? "is" : "are"
195
- } not included in any output, so no agent will read ${missing.length === 1 ? "it" : "them"}:\n` +
196
- missing.map((memory) => `${BULLET} ${memory.recipe}: ${memory.relative}`).join("\n") +
197
- `\nTo include every memory, add the line "@#memories/**/*.md" to your project's instruction ` +
198
- `source (the file your AGENTS.md or CLAUDE.md is built from). To leave a recipe's memories out on purpose, ` +
199
- `list the recipe in "recipes.memories.exclude" in your sous config.`
200
- );
201
- }
202
-
203
- /**
204
- * The directories a build's deletions must never reach into, for the project the
205
- * options describe. Empty when the caller gave no config context, which is the
206
- * case only in tests that build a settings object by hand.
207
- *
208
- * @param options - The build options, for the config context.
209
- */
210
- function protectedPathsFor(options: BuildOptions): string[] {
211
- if (options.configContext === undefined) return [];
212
- return protectedRepoPaths(options.configContext.sousDir);
213
- }
214
-
215
- /**
216
- * The namespace resolver a build should use: the one the caller supplied, or the
217
- * project's own, built from its lockfile. A project that locks no recipes gets
218
- * undefined, which leaves `~` in an include line meaning an alias and nothing
219
- * else.
220
- *
221
- * @param settings - The merged project config.
222
- * @param options - The build options, for the config context and any override.
223
- */
224
- function resolveNamespaceResolver(
225
- settings: Settings,
226
- options: BuildOptions
227
- ): NamespaceResolver | undefined {
228
- if (options.namespaceResolver !== undefined) return options.namespaceResolver;
229
- if (options.configContext === undefined) return undefined;
230
- return createProjectNamespaceResolver({
231
- sousDir: options.configContext.sousDir,
232
- settings,
233
- });
234
- }
235
-
236
- /**
237
- * Recursively collects all file paths reachable from `filePath` via @include chains.
238
- * Returns a Set of absolute paths. The `visited` set prevents infinite loops.
239
- *
240
- * Matches the same @<path>.md include lines as CompilationService.processIncludes,
241
- * including alias/`${var}` paths, resolving each via the same candidate logic
242
- * (first existing wins).
243
- */
244
- function collectIncludeGraph(
245
- filePath: string,
246
- resolveOpts: {
247
- aliases?: Record<string, string[]>;
248
- scope?: Record<string, string>;
249
- namespaceResolver?: NamespaceResolver;
250
- views?: ViewMap;
251
- } = {},
252
- visited: Set<string> = new Set()
253
- ): Set<string> {
254
- if (visited.has(filePath)) return visited;
255
- visited.add(filePath);
256
-
257
- if (!fs.existsSync(filePath)) return visited;
258
-
259
- let content: string;
260
- try {
261
- content = fs.readFileSync(filePath, "utf8");
262
- } catch {
263
- return visited;
264
- }
265
-
266
- const baseDir = path.dirname(filePath);
267
-
268
- for (const line of content.split("\n")) {
269
- const match = INCLUDE_LINE_PATTERN.exec(line.trimEnd());
270
- if (match === null) continue;
271
- const { files, candidates } = resolveIncludeFiles(match[1].trim(), {
272
- aliases: resolveOpts.aliases,
273
- views: resolveOpts.views,
274
- scope: resolveOpts.scope,
275
- baseDir,
276
- namespaceResolver: resolveOpts.namespaceResolver,
277
- fromFile: filePath,
278
- });
279
- // A file that does not exist yet is still part of the graph, as its first candidate.
280
- for (const fullPath of files.length > 0 ? files : candidates.slice(0, 1)) {
281
- collectIncludeGraph(fullPath, resolveOpts, visited);
282
- }
283
- }
284
-
285
- return visited;
286
- }
287
-
288
- /**
289
- * Returns the subset of compilation targets that transitively include `filePath`.
290
- * A target is affected if its `rootInputPath` equals `filePath`, or if `filePath`
291
- * is reachable via @include chains from `rootInputPath`.
292
- *
293
- * Uses a simple recursive file scan — reads each .md file and checks for
294
- * @<path> include lines. Does not compile; just walks the include graph.
295
- *
296
- * @param filePath - The changed file.
297
- * @param config - The resolved compilation config.
298
- * @param namespaceResolver - Optional resolver so `~namespace` includes are followed too.
299
- */
300
- export function findAffectedTargets(
301
- filePath: string,
302
- config: CompilationConfig,
303
- namespaceResolver?: NamespaceResolver
304
- ): CompilationTarget[] {
305
- return config.targets.filter(target => {
306
- const graph = collectIncludeGraph(target.rootInputPath, {
307
- aliases: config.aliases,
308
- views: config.views,
309
- scope: config.includeScope,
310
- namespaceResolver,
311
- });
312
- return graph.has(filePath);
313
- });
314
- }
315
-
316
- /**
317
- * Resolves the state file path for a config.
318
- *
319
- * The path is derived from the resolved settings scope, so a `stateFilePath`
320
- * or `sousDir` defined in `_vars` (or injected by discovery) is honoured.
321
- *
322
- * @param settings - The loaded settings.
323
- * @param configContext - Where the config was discovered (supplies `sousDir`).
324
- * @returns Absolute path to the state file.
325
- */
326
- export function resolveStateFilePath(
327
- settings: Settings,
328
- configContext?: ConfigContext
329
- ): string {
330
- const scope = resolveRootScope(settings, configContext);
331
- return new StateService().getFilePath(scope);
332
- }
333
-
334
- export class BuildService {
335
- /**
336
- * Runs compile + prune for the configured project.
337
- * Returns true if all steps succeeded.
338
- */
339
- async build(settings: Settings, options: BuildOptions = {}): Promise<boolean> {
340
- // The recipe answers are resolved once here and handed to every scope the
341
- // build assembles, so the lockfile and the manifests are read one time.
342
- const answers = resolveProjectAnswers(settings, options.configContext);
343
- const rootScope = resolveRootScope(settings, options.configContext, { answers });
344
- const namespaceResolver = resolveNamespaceResolver(settings, options);
345
- const protectedPaths = protectedPathsFor(options);
346
-
347
- const stateService = new StateService();
348
- const stateFilePath = resolveStateFilePath(settings, options.configContext);
349
-
350
- let success = true;
351
-
352
- // When --rebuild, clear all previously written files before compiling so the
353
- // rebuild starts from a physically clean slate. Orphaned outputs are also
354
- // caught by prune (compile carries prior state entries forward), so this is
355
- // the aggressive path: delete everything up front rather than prune after.
356
- if (options.rebuild && !options.dryRun && !options.noCompile) {
357
- const existingState = await stateService.load(stateFilePath);
358
- if (existingState?.files.length) {
359
- stateService.deleteTrackedFiles(
360
- existingState.files,
361
- existingState.dirs,
362
- protectedPaths
363
- );
364
- }
365
- }
366
-
367
- // Compile step. The recipes this project subscribes to contribute compile
368
- // targets alongside its own, so a recipe's files are compiled by exactly the
369
- // same machinery as everything else, and are pruned and cleared by it too.
370
- if (!options.noCompile) {
371
- const recipes = resolveRecipeTargets(settings, rootScope, options.configContext, answers);
372
- for (const notice of recipes.warnings) warning(notice);
373
-
374
- const withoutViews = withRecipeTargets(
375
- resolveCompilation(settings, rootScope),
376
- recipes,
377
- settings,
378
- rootScope
379
- );
380
- // The files each view lists (`#memories`), worked out once per build.
381
- const config: CompilationConfig | null =
382
- withoutViews === null || options.configContext === undefined
383
- ? withoutViews
384
- : {
385
- ...withoutViews,
386
- views: sharedHashNames().viewMap({
387
- sousDir: options.configContext.sousDir,
388
- settings,
389
- }),
390
- };
391
-
392
- if (config) {
393
- let effectiveConfig: CompilationConfig = config;
394
-
395
- if (options.changedFile) {
396
- const affectedTargets = findAffectedTargets(
397
- options.changedFile,
398
- config,
399
- namespaceResolver
400
- );
401
- if (affectedTargets.length === 0) {
402
- log(` ⊘ No targets affected by change to ${options.changedFile} — skipping compilation`);
403
- } else {
404
- effectiveConfig = { ...config, targets: affectedTargets };
405
- const compiler = new CompilationService({
406
- strict: options.strict,
407
- rebuild: options.rebuild,
408
- dryRun: options.dryRun,
409
- namespaceResolver,
410
- });
411
- const compileOk = await compiler.compile(effectiveConfig, stateFilePath);
412
- if (!compileOk) success = false;
413
- }
414
- } else {
415
- const compiler = new CompilationService({
416
- strict: options.strict,
417
- rebuild: options.rebuild,
418
- dryRun: options.dryRun,
419
- namespaceResolver,
420
- });
421
- const compileOk = await compiler.compile(effectiveConfig, stateFilePath);
422
- if (!compileOk) success = false;
423
-
424
- // Only a full build knows every route an include can take, so only a
425
- // full build that compiled cleanly checks that every memory got in.
426
- if (compileOk && options.configContext !== undefined) {
427
- const notice = unincludedMemoriesWarning(
428
- unincludedMemories(settings, options.configContext.sousDir, compiler.includedFiles())
429
- );
430
- if (notice !== undefined) warning(notice);
431
- }
432
- }
433
- }
434
- }
435
-
436
- // Prune step
437
- if (!options.noPrune && success) {
438
- await this.prune(settings, stateFilePath, options.dryRun, options.configContext);
439
- }
440
-
441
- return success;
442
- }
443
-
444
- /**
445
- * Removes output files that are tracked in state but no longer in the current config.
446
- * Also removes Sous-created directories that are now empty.
447
- */
448
- async prune(
449
- settings: Settings,
450
- stateFilePath: string,
451
- dryRun = false,
452
- configContext?: ConfigContext
453
- ): Promise<void> {
454
- const stateService = new StateService();
455
- const state = await stateService.load(stateFilePath);
456
- if (!state || state.files.length === 0) return;
457
-
458
- const protectedPaths = configContext ? protectedRepoPaths(configContext.sousDir) : [];
459
- const rootScope = resolveRootScope(settings, configContext);
460
- const recipes = resolveRecipeTargets(settings, rootScope, configContext);
461
- const config = resolveCompilation(settings, rootScope);
462
-
463
- // Collect the current output set: explicit files and active destinationDir prefixes
464
- const currentOutputFiles = new Set<string>();
465
- const currentOutputDirs = new Set<string>();
466
- if (config) {
467
- for (const target of config.targets) {
468
- for (const output of target.outputs) {
469
- if (output.destinationFile) currentOutputFiles.add(output.destinationFile);
470
- if (output.destinationDir) currentOutputDirs.add(output.destinationDir);
471
- }
472
- }
473
- }
474
-
475
- // Recipe targets are counted file by file rather than by their destination
476
- // directory. Every subscribed recipe writes into the same directory, so a
477
- // directory prefix would make everything ever written there look current and
478
- // an unsubscribed recipe's files would stay forever.
479
- for (const target of recipes.targets) {
480
- for (const output of target.outputs) {
481
- const dest = resolveOutputPath(target, output);
482
- if (dest !== undefined) currentOutputFiles.add(dest);
483
- }
484
- }
485
-
486
- // A state entry is current if it matches an explicit destinationFile, or if its dest
487
- // path falls under an active destinationDir (glob target output).
488
- function isCurrentOutput(dest: string): boolean {
489
- if (currentOutputFiles.has(dest)) return true;
490
- for (const dir of currentOutputDirs) {
491
- if (dest.startsWith(dir + path.sep) || dest.startsWith(dir + "/")) return true;
492
- }
493
- return false;
494
- }
495
-
496
- // Find files to prune. Anything inside a linked checkout or the shared
497
- // recipe store is never a prune candidate, whatever the state file says.
498
- const toDelete = state.files.filter(
499
- f => !isCurrentOutput(f.dest) && !isProtectedPath(f.dest, protectedPaths)
500
- );
501
-
502
- if (dryRun) {
503
- for (const entry of toDelete) {
504
- console.log(` ○ would prune: ${entry.dest}`);
505
- }
506
- return;
507
- }
508
-
509
- stateService.deleteTrackedFiles(toDelete, state.dirs, protectedPaths);
510
- for (const entry of toDelete) console.log(` ✗ pruned: ${entry.dest}`);
511
-
512
- // Update state: remove pruned entries and any dirs that no longer exist
513
- const deleted = new Set(toDelete.map(entry => entry.dest));
514
- state.files = state.files.filter(f => !deleted.has(f.dest));
515
- state.dirs = state.dirs.filter(d => fs.existsSync(d));
516
- await stateService.save(stateFilePath, state);
517
- }
518
- }
519
-
520
- /** What {@link runProjectBuild} is given. */
521
- export type ProjectBuildInput = {
522
- /** The merged project config, reloaded after any change a command made. */
523
- settings: Settings;
524
- /** Where the active config was discovered. */
525
- configContext: ConfigContext;
526
- /** The shell environment as it was before the env files were injected. */
527
- shellEnv?: NodeJS.ProcessEnv;
528
- /** The build's own options; every one defaults to what a plain `sous build` uses. */
529
- options?: Omit<BuildOptions, "configContext">;
530
- /** The heading printed above the compile and prune step. Defaults to "Building". */
531
- heading?: string;
532
- };
533
-
534
- /**
535
- * The one build every command runs: `build`, `init`, `launch`, `prune`,
536
- * `repo remove`, `repo unlink`, `subscription add`, `subscription remove` and
537
- * `subscription update` all come through here, so a project is built the same
538
- * way whichever of them the user typed.
539
- *
540
- * In order, it announces every linked repository, prepares the project's
541
- * recipes (seeds the core recipe, locks new subscriptions, restores whatever
542
- * the store is missing, checks upstream and reports newer versions), and then
543
- * compiles and prunes. A dry run prepares nothing, because preparing writes the
544
- * lockfile and the store. A partial rebuild in watch mode (one changed source
545
- * file) prepares nothing either: the config did not change, so neither did the
546
- * recipes it needs.
547
- *
548
- * @param input - The project, the build options and the heading.
549
- * @returns True when compile and prune both succeeded; false after any compile error.
550
- */
551
- export async function runProjectBuild(input: ProjectBuildInput): Promise<boolean> {
552
- const options = input.options ?? {};
553
- const partial = options.changedFile !== undefined;
554
-
555
- // A linked repository is read from a working copy instead of a published
556
- // version, so it is announced on every build; a build that silently produced
557
- // something different would be far worse than a noisy one.
558
- if (!partial) {
559
- const linked = describeLinkedRepos(input.configContext.sousDir);
560
- if (linked.length > 0) warning(linked.join("\n"));
561
- }
562
-
563
- if (!options.dryRun && !partial) {
564
- await prepareRepositoriesForBuild(
565
- subscriptionServiceFor({
566
- configContext: input.configContext,
567
- settings: input.settings,
568
- ...(input.shellEnv === undefined ? {} : { shellEnv: input.shellEnv }),
569
- })
570
- );
571
- }
572
-
573
- heading(input.heading ?? "Building");
574
- const succeeded = await new BuildService().build(input.settings, {
575
- ...options,
576
- configContext: input.configContext,
577
- });
578
- footer();
579
- return succeeded;
580
- }