@sous-io/sous 0.1.1 → 0.2.0

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 (205) hide show
  1. package/README.md +115 -35
  2. package/bin/run.js +10 -1
  3. package/docs/markdown/README.md +27 -0
  4. package/docs/markdown/_sidebar.md +18 -0
  5. package/docs/markdown/commands.md +308 -0
  6. package/docs/markdown/config-discovery.md +74 -0
  7. package/docs/markdown/config-inspection.md +69 -0
  8. package/docs/markdown/config-layers.md +92 -0
  9. package/docs/markdown/config-variables.md +79 -0
  10. package/docs/markdown/configuration.md +71 -0
  11. package/docs/markdown/design-principles.md +59 -0
  12. package/docs/markdown/repositories-authoring.md +408 -0
  13. package/docs/markdown/repositories-consuming.md +580 -0
  14. package/docs/markdown/repositories-file-formats.md +1084 -0
  15. package/docs/markdown/repositories-variables.md +387 -0
  16. package/docs/markdown/repositories.md +303 -0
  17. package/docs/markdown/skill-categories.md +58 -0
  18. package/package.json +72 -8
  19. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
  20. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
  21. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
  22. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
  23. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
  24. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
  25. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
  26. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
  27. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
  28. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
  29. package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
  30. package/sous.config.schema.json +337 -0
  31. package/src/base-command.ts +220 -67
  32. package/src/commands/build.ts +150 -73
  33. package/src/commands/clear.ts +23 -15
  34. package/src/commands/compile.ts +74 -16
  35. package/src/commands/config/get.ts +110 -0
  36. package/src/commands/config/show.ts +32 -0
  37. package/src/commands/config/validate.ts +53 -0
  38. package/src/commands/help.ts +46 -0
  39. package/src/commands/launch.ts +36 -14
  40. package/src/commands/lock/rebuild.ts +241 -0
  41. package/src/commands/lock/show.ts +115 -0
  42. package/src/commands/namespace/list.ts +117 -0
  43. package/src/commands/namespace/show.ts +110 -0
  44. package/src/commands/prune.ts +3 -11
  45. package/src/commands/recipe/list.ts +95 -0
  46. package/src/commands/recipe/show.ts +301 -0
  47. package/src/commands/repo/add.ts +145 -0
  48. package/src/commands/repo/gc.ts +172 -0
  49. package/src/commands/repo/init.ts +136 -0
  50. package/src/commands/repo/link.ts +500 -0
  51. package/src/commands/repo/list.ts +179 -0
  52. package/src/commands/repo/release.ts +619 -0
  53. package/src/commands/repo/remove.ts +193 -0
  54. package/src/commands/repo/search.ts +189 -0
  55. package/src/commands/repo/submit.ts +133 -0
  56. package/src/commands/repo/unlink.ts +147 -0
  57. package/src/commands/subscription/add.ts +285 -0
  58. package/src/commands/subscription/list.ts +129 -0
  59. package/src/commands/subscription/remove.ts +181 -0
  60. package/src/commands/vars/ask.ts +374 -0
  61. package/src/commands/vars/index.ts +79 -0
  62. package/src/commands/vars/list.ts +67 -0
  63. package/src/commands/vars/show.ts +77 -0
  64. package/src/config-command.ts +30 -0
  65. package/src/lib/build-service.ts +206 -54
  66. package/src/lib/config-discovery.ts +220 -27
  67. package/src/lib/config-inspect.ts +145 -0
  68. package/src/lib/config-kernel.mjs +377 -0
  69. package/src/lib/config-schema.ts +361 -0
  70. package/src/lib/env-file.ts +328 -0
  71. package/src/lib/env-local.ts +18 -1
  72. package/src/lib/errors.ts +32 -0
  73. package/src/lib/include-resolver.ts +108 -15
  74. package/src/lib/interactive.ts +165 -0
  75. package/src/lib/markdown-compiler.ts +118 -37
  76. package/src/lib/package-info.ts +25 -0
  77. package/src/lib/pid-service.ts +32 -21
  78. package/src/lib/refs/find.ts +589 -0
  79. package/src/lib/refs/index.ts +12 -0
  80. package/src/lib/refs/pick.ts +147 -0
  81. package/src/lib/refs/scopes.ts +61 -0
  82. package/src/lib/repos/catalog-display.ts +116 -0
  83. package/src/lib/repos/catalog-inputs.ts +160 -0
  84. package/src/lib/repos/catalog.ts +722 -0
  85. package/src/lib/repos/core-recipe.ts +105 -0
  86. package/src/lib/repos/defaults.ts +175 -0
  87. package/src/lib/repos/formats/common.ts +389 -0
  88. package/src/lib/repos/formats/index-file.ts +215 -0
  89. package/src/lib/repos/formats/links-map.ts +96 -0
  90. package/src/lib/repos/formats/lockfile.ts +167 -0
  91. package/src/lib/repos/formats/patterns.ts +57 -0
  92. package/src/lib/repos/formats/recipe-manifest.ts +395 -0
  93. package/src/lib/repos/formats/repo-manifest.ts +88 -0
  94. package/src/lib/repos/formats/store-entry.ts +84 -0
  95. package/src/lib/repos/freshness.ts +208 -0
  96. package/src/lib/repos/git-clone.ts +312 -0
  97. package/src/lib/repos/identity.ts +89 -0
  98. package/src/lib/repos/index.ts +58 -0
  99. package/src/lib/repos/links.ts +353 -0
  100. package/src/lib/repos/load-manifest.ts +236 -0
  101. package/src/lib/repos/lock-service.ts +453 -0
  102. package/src/lib/repos/locked-namespace-resolver.ts +90 -0
  103. package/src/lib/repos/locked-recipes.ts +254 -0
  104. package/src/lib/repos/managed-layer.ts +422 -0
  105. package/src/lib/repos/namespace-resolver.ts +370 -0
  106. package/src/lib/repos/providers/base.ts +206 -0
  107. package/src/lib/repos/providers/git.ts +233 -0
  108. package/src/lib/repos/providers/github.ts +294 -0
  109. package/src/lib/repos/providers/gitlab.ts +263 -0
  110. package/src/lib/repos/providers/http.ts +102 -0
  111. package/src/lib/repos/providers/index-cache.ts +382 -0
  112. package/src/lib/repos/providers/index.ts +106 -0
  113. package/src/lib/repos/providers/local.ts +391 -0
  114. package/src/lib/repos/providers/provider.ts +401 -0
  115. package/src/lib/repos/recipe-config-layers.ts +287 -0
  116. package/src/lib/repos/recipe-targets.ts +223 -0
  117. package/src/lib/repos/ref-search.ts +46 -0
  118. package/src/lib/repos/ref.ts +513 -0
  119. package/src/lib/repos/reference-report.ts +122 -0
  120. package/src/lib/repos/release/bump.ts +161 -0
  121. package/src/lib/repos/release/git-state.ts +305 -0
  122. package/src/lib/repos/release/index-builder.ts +635 -0
  123. package/src/lib/repos/release/index.ts +16 -0
  124. package/src/lib/repos/release/plan.ts +512 -0
  125. package/src/lib/repos/release/submit-service.ts +496 -0
  126. package/src/lib/repos/release/tags.ts +243 -0
  127. package/src/lib/repos/release/validate.ts +463 -0
  128. package/src/lib/repos/resolver.ts +789 -0
  129. package/src/lib/repos/scaffold/index.ts +238 -0
  130. package/src/lib/repos/scaffold/templates.ts +413 -0
  131. package/src/lib/repos/seed.ts +414 -0
  132. package/src/lib/repos/store/contract.ts +64 -0
  133. package/src/lib/repos/store/hash.ts +114 -0
  134. package/src/lib/repos/store/recipe-store.ts +599 -0
  135. package/src/lib/repos/store/settings.ts +58 -0
  136. package/src/lib/repos/subscription-service.ts +2678 -0
  137. package/src/lib/repos/trust.ts +447 -0
  138. package/src/lib/settings.ts +546 -189
  139. package/src/lib/sous-home.ts +104 -0
  140. package/src/lib/state.ts +52 -20
  141. package/src/lib/vars/ask.ts +1152 -0
  142. package/src/lib/vars/definition-source.ts +252 -0
  143. package/src/lib/vars/display.ts +233 -0
  144. package/src/lib/vars/index.ts +18 -0
  145. package/src/lib/vars/ladder.ts +282 -0
  146. package/src/lib/vars/mappings.ts +265 -0
  147. package/src/lib/vars/names.ts +94 -0
  148. package/src/lib/vars/preanswers.ts +395 -0
  149. package/src/lib/vars/question-plan.ts +218 -0
  150. package/src/lib/vars/report.ts +228 -0
  151. package/src/lib/vars/safe-regex.ts +235 -0
  152. package/src/lib/vars/validate.ts +312 -0
  153. package/src/lib/watch-loop.ts +148 -0
  154. package/src/templating/init-liquid-engine.ts +58 -16
  155. package/src/utils/choice-prompt.ts +143 -0
  156. package/src/utils/command-errors.ts +186 -0
  157. package/src/utils/command-help.ts +45 -0
  158. package/src/utils/confirm-prompt.ts +110 -0
  159. package/src/utils/flags.ts +153 -0
  160. package/src/utils/formatting.ts +540 -55
  161. package/src/utils/prompts.ts +35 -1
  162. package/src/utils/sous-directory.ts +245 -0
  163. package/src/utils/table.ts +603 -0
  164. package/src/utils/value-prompt.ts +119 -0
  165. package/shared-prompts/_partials/resume-task.md +0 -51
  166. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  167. package/shared-prompts/_partials/update-task-file.md +0 -52
  168. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  169. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  189. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  190. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  191. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  192. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  193. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  194. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  195. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  196. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  197. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  198. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  199. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  200. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  201. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  202. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  203. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  204. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  205. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
@@ -1,12 +1,17 @@
1
1
  import path from "node:path";
2
2
  import fs from "node:fs";
3
3
  import type { ConfigContext, Settings } from "./settings.js";
4
- import { resolveProjectCompilation, resolveRootScope, resolveScope } from "./settings.js";
4
+ import { resolveAliases, resolveCompilation, resolveRootScope } from "./settings.js";
5
5
  import { resolveIncludeCandidates } from "./include-resolver.js";
6
- import { CompilationService } from "./markdown-compiler.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, resolveOutputPath } from "./markdown-compiler.js";
7
10
  import type { CompilationConfig, CompilationTarget } from "./markdown-compiler.js";
8
11
  import { StateService } from "./state.js";
9
- import { log } from "../utils/formatting.js";
12
+ import { isProtectedPath } from "./state.js";
13
+ import { protectedRepoPaths } from "./repos/links.js";
14
+ import { log, warning } from "../utils/formatting.js";
10
15
 
11
16
  export type BuildOptions = {
12
17
  strict?: boolean;
@@ -25,8 +30,108 @@ export type BuildOptions = {
25
30
  * `${sousDir}` resolves and so state/PID paths default into `.sous/`.
26
31
  */
27
32
  configContext?: ConfigContext;
33
+ /**
34
+ * Resolves `~namespace` include and render paths against recipe namespaces.
35
+ * Passed straight through to the compiler and to the include-graph walk, so a
36
+ * partial rebuild follows namespace includes too.
37
+ *
38
+ * When it is omitted and `configContext` is given, the build builds the
39
+ * project's own resolver from its lockfile, links map and store. Pass one
40
+ * explicitly to override that, which is what tests do.
41
+ */
42
+ namespaceResolver?: NamespaceResolver;
43
+ };
44
+
45
+ /** An empty recipe-target result, for a build with no config context. */
46
+ const NO_RECIPE_TARGETS: RecipeTargets = {
47
+ targets: [],
48
+ destinations: [],
49
+ watchDirs: [],
50
+ warnings: [],
28
51
  };
29
52
 
53
+ /**
54
+ * The compile targets a project's subscribed recipes contribute, for the project
55
+ * the options describe. Empty when the caller gave no config context, which is
56
+ * the case only in tests that build a settings object by hand.
57
+ *
58
+ * @param settings - The merged project config.
59
+ * @param rootScope - The resolved settings scope, for `${var}` in destinations.
60
+ * @param configContext - Where the active config was discovered.
61
+ */
62
+ export function resolveRecipeTargets(
63
+ settings: Settings,
64
+ rootScope: Record<string, string>,
65
+ configContext?: ConfigContext
66
+ ): RecipeTargets {
67
+ if (configContext === undefined) return NO_RECIPE_TARGETS;
68
+ return buildRecipeTargets({
69
+ sousDir: configContext.sousDir,
70
+ settings,
71
+ scope: rootScope,
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Adds the recipe targets to a project's own compilation config. A project with
77
+ * no compilation block of its own still compiles its recipes, so the config is
78
+ * created when there is none and there is something to compile.
79
+ *
80
+ * @param config - The project's own compilation config, or null when it has none.
81
+ * @param recipes - The targets the subscribed recipes contribute.
82
+ * @param settings - The merged project config, for its aliases.
83
+ * @param rootScope - The resolved settings scope.
84
+ */
85
+ export function withRecipeTargets(
86
+ config: CompilationConfig | null,
87
+ recipes: RecipeTargets,
88
+ settings: Settings,
89
+ rootScope: Record<string, string>
90
+ ): CompilationConfig | null {
91
+ if (recipes.targets.length === 0) return config;
92
+ if (config === null) {
93
+ return {
94
+ targets: recipes.targets,
95
+ aliases: resolveAliases(settings, rootScope),
96
+ includeScope: rootScope,
97
+ };
98
+ }
99
+ return { ...config, targets: [...config.targets, ...recipes.targets] };
100
+ }
101
+
102
+ /**
103
+ * The directories a build's deletions must never reach into, for the project the
104
+ * options describe. Empty when the caller gave no config context, which is the
105
+ * case only in tests that build a settings object by hand.
106
+ *
107
+ * @param options - The build options, for the config context.
108
+ */
109
+ function protectedPathsFor(options: BuildOptions): string[] {
110
+ if (options.configContext === undefined) return [];
111
+ return protectedRepoPaths(options.configContext.sousDir);
112
+ }
113
+
114
+ /**
115
+ * The namespace resolver a build should use: the one the caller supplied, or the
116
+ * project's own, built from its lockfile. A project that locks no recipes gets
117
+ * undefined, which leaves `~` in an include line meaning an alias and nothing
118
+ * else.
119
+ *
120
+ * @param settings - The merged project config.
121
+ * @param options - The build options, for the config context and any override.
122
+ */
123
+ function resolveNamespaceResolver(
124
+ settings: Settings,
125
+ options: BuildOptions
126
+ ): NamespaceResolver | undefined {
127
+ if (options.namespaceResolver !== undefined) return options.namespaceResolver;
128
+ if (options.configContext === undefined) return undefined;
129
+ return createProjectNamespaceResolver({
130
+ sousDir: options.configContext.sousDir,
131
+ settings,
132
+ });
133
+ }
134
+
30
135
  /**
31
136
  * Recursively collects all file paths reachable from `filePath` via @include chains.
32
137
  * Returns a Set of absolute paths. The `visited` set prevents infinite loops.
@@ -37,7 +142,11 @@ export type BuildOptions = {
37
142
  */
38
143
  function collectIncludeGraph(
39
144
  filePath: string,
40
- resolveOpts: { aliases?: Record<string, string[]>; scope?: Record<string, string> } = {},
145
+ resolveOpts: {
146
+ aliases?: Record<string, string[]>;
147
+ scope?: Record<string, string>;
148
+ namespaceResolver?: NamespaceResolver;
149
+ } = {},
41
150
  visited: Set<string> = new Set()
42
151
  ): Set<string> {
43
152
  if (visited.has(filePath)) return visited;
@@ -62,6 +171,8 @@ function collectIncludeGraph(
62
171
  aliases: resolveOpts.aliases,
63
172
  scope: resolveOpts.scope,
64
173
  baseDir,
174
+ namespaceResolver: resolveOpts.namespaceResolver,
175
+ fromFile: filePath,
65
176
  });
66
177
  const fullPath = candidates.find((c) => fs.existsSync(c)) ?? candidates[0];
67
178
  collectIncludeGraph(fullPath, resolveOpts, visited);
@@ -77,85 +188,97 @@ function collectIncludeGraph(
77
188
  *
78
189
  * Uses a simple recursive file scan — reads each .md file and checks for
79
190
  * @<path> include lines. Does not compile; just walks the include graph.
191
+ *
192
+ * @param filePath - The changed file.
193
+ * @param config - The resolved compilation config.
194
+ * @param namespaceResolver - Optional resolver so `~namespace` includes are followed too.
80
195
  */
81
196
  export function findAffectedTargets(
82
197
  filePath: string,
83
- config: CompilationConfig
198
+ config: CompilationConfig,
199
+ namespaceResolver?: NamespaceResolver
84
200
  ): CompilationTarget[] {
85
201
  return config.targets.filter(target => {
86
202
  const graph = collectIncludeGraph(target.rootInputPath, {
87
203
  aliases: config.aliases,
88
204
  scope: config.includeScope,
205
+ namespaceResolver,
89
206
  });
90
207
  return graph.has(filePath);
91
208
  });
92
209
  }
93
210
 
94
211
  /**
95
- * Resolves the state file path for a project.
212
+ * Resolves the state file path for a config.
96
213
  *
97
- * The path is derived from the PROJECT scope (root vars → project `_vars`), so a
98
- * `stateFilePath` or `sousDir` defined at either level is honoured. Resolving
99
- * from the root scope alone was a bug: project-level values were ignored and
100
- * state silently landed in cwd.
214
+ * The path is derived from the resolved settings scope, so a `stateFilePath`
215
+ * or `sousDir` defined in `_vars` (or injected by discovery) is honoured.
101
216
  *
102
- * @param projectKey - The project's key in `settings.projects`.
103
- * @param settings - The loaded root settings.
217
+ * @param settings - The loaded settings.
104
218
  * @param configContext - Where the config was discovered (supplies `sousDir`).
105
- * @returns Absolute path to the project's state file.
219
+ * @returns Absolute path to the state file.
106
220
  */
107
221
  export function resolveStateFilePath(
108
- projectKey: string,
109
222
  settings: Settings,
110
223
  configContext?: ConfigContext
111
224
  ): string {
112
- const rootScope = resolveRootScope(settings, configContext);
113
- const project = settings.projects[projectKey];
114
- const projectScope = resolveScope(project?._vars ?? {}, rootScope);
115
- const projectCount = Object.keys(settings.projects ?? {}).length;
116
- return new StateService().getFilePath(projectKey, projectScope, projectCount);
225
+ const scope = resolveRootScope(settings, configContext);
226
+ return new StateService().getFilePath(scope);
117
227
  }
118
228
 
119
229
  export class BuildService {
120
230
  /**
121
- * Runs compile + prune for a project.
231
+ * Runs compile + prune for the configured project.
122
232
  * Returns true if all steps succeeded.
123
233
  */
124
- async build(
125
- projectKey: string,
126
- settings: Settings,
127
- options: BuildOptions = {}
128
- ): Promise<boolean> {
234
+ async build(settings: Settings, options: BuildOptions = {}): Promise<boolean> {
129
235
  const rootScope = resolveRootScope(settings, options.configContext);
130
- const project = settings.projects[projectKey];
131
-
132
- if (!project) {
133
- throw new Error(`Project '${projectKey}' not found in settings`);
134
- }
236
+ const namespaceResolver = resolveNamespaceResolver(settings, options);
237
+ const protectedPaths = protectedPathsFor(options);
135
238
 
136
239
  const stateService = new StateService();
137
- const stateFilePath = resolveStateFilePath(projectKey, settings, options.configContext);
240
+ const stateFilePath = resolveStateFilePath(settings, options.configContext);
138
241
 
139
242
  let success = true;
140
243
 
141
- // When --rebuild, clear all previously written files before compiling so that
142
- // orphaned outputs (files no longer produced by the current config) are removed.
143
- // Prune cannot catch these because compile overwrites the state file before prune runs.
244
+ // When --rebuild, clear all previously written files before compiling so the
245
+ // rebuild starts from a physically clean slate. Orphaned outputs are also
246
+ // caught by prune (compile carries prior state entries forward), so this is
247
+ // the aggressive path: delete everything up front rather than prune after.
144
248
  if (options.rebuild && !options.dryRun && !options.noCompile) {
145
249
  const existingState = await stateService.load(stateFilePath);
146
250
  if (existingState?.files.length) {
147
- stateService.deleteTrackedFiles(existingState.files, existingState.dirs);
251
+ stateService.deleteTrackedFiles(
252
+ existingState.files,
253
+ existingState.dirs,
254
+ protectedPaths
255
+ );
148
256
  }
149
257
  }
150
258
 
151
- // Compile step
259
+ // Compile step. The recipes this project subscribes to contribute compile
260
+ // targets alongside its own, so a recipe's files are compiled by exactly the
261
+ // same machinery as everything else, and are pruned and cleared by it too.
152
262
  if (!options.noCompile) {
153
- const config = resolveProjectCompilation(project, rootScope, settings, projectKey);
263
+ const recipes = resolveRecipeTargets(settings, rootScope, options.configContext);
264
+ for (const notice of recipes.warnings) warning(notice);
265
+
266
+ const config = withRecipeTargets(
267
+ resolveCompilation(settings, rootScope),
268
+ recipes,
269
+ settings,
270
+ rootScope
271
+ );
272
+
154
273
  if (config) {
155
274
  let effectiveConfig: CompilationConfig = config;
156
275
 
157
276
  if (options.changedFile) {
158
- const affectedTargets = findAffectedTargets(options.changedFile, config);
277
+ const affectedTargets = findAffectedTargets(
278
+ options.changedFile,
279
+ config,
280
+ namespaceResolver
281
+ );
159
282
  if (affectedTargets.length === 0) {
160
283
  log(` ⊘ No targets affected by change to ${options.changedFile} — skipping compilation`);
161
284
  } else {
@@ -164,6 +287,7 @@ export class BuildService {
164
287
  strict: options.strict,
165
288
  rebuild: options.rebuild,
166
289
  dryRun: options.dryRun,
290
+ namespaceResolver,
167
291
  });
168
292
  const compileOk = await compiler.compile(effectiveConfig, stateFilePath);
169
293
  if (!compileOk) success = false;
@@ -173,6 +297,7 @@ export class BuildService {
173
297
  strict: options.strict,
174
298
  rebuild: options.rebuild,
175
299
  dryRun: options.dryRun,
300
+ namespaceResolver,
176
301
  });
177
302
  const compileOk = await compiler.compile(effectiveConfig, stateFilePath);
178
303
  if (!compileOk) success = false;
@@ -182,13 +307,7 @@ export class BuildService {
182
307
 
183
308
  // Prune step
184
309
  if (!options.noPrune && success) {
185
- await this.prune(
186
- projectKey,
187
- settings,
188
- stateFilePath,
189
- options.dryRun,
190
- options.configContext
191
- );
310
+ await this.prune(settings, stateFilePath, options.dryRun, options.configContext);
192
311
  }
193
312
 
194
313
  return success;
@@ -199,7 +318,6 @@ export class BuildService {
199
318
  * Also removes Sous-created directories that are now empty.
200
319
  */
201
320
  async prune(
202
- projectKey: string,
203
321
  settings: Settings,
204
322
  stateFilePath: string,
205
323
  dryRun = false,
@@ -209,11 +327,10 @@ export class BuildService {
209
327
  const state = await stateService.load(stateFilePath);
210
328
  if (!state || state.files.length === 0) return;
211
329
 
330
+ const protectedPaths = configContext ? protectedRepoPaths(configContext.sousDir) : [];
212
331
  const rootScope = resolveRootScope(settings, configContext);
213
- const project = settings.projects[projectKey];
214
- const config = project
215
- ? resolveProjectCompilation(project, rootScope, settings, projectKey)
216
- : null;
332
+ const recipes = resolveRecipeTargets(settings, rootScope, configContext);
333
+ const config = resolveCompilation(settings, rootScope);
217
334
 
218
335
  // Collect the current output set: explicit files and active destinationDir prefixes
219
336
  const currentOutputFiles = new Set<string>();
@@ -227,6 +344,17 @@ export class BuildService {
227
344
  }
228
345
  }
229
346
 
347
+ // Recipe targets are counted file by file rather than by their destination
348
+ // directory. Every subscribed recipe writes into the same directory, so a
349
+ // directory prefix would make everything ever written there look current and
350
+ // an unsubscribed recipe's files would stay forever.
351
+ for (const target of recipes.targets) {
352
+ for (const output of target.outputs) {
353
+ const dest = resolveOutputPath(target, output);
354
+ if (dest !== undefined) currentOutputFiles.add(dest);
355
+ }
356
+ }
357
+
230
358
  // A state entry is current if it matches an explicit destinationFile, or if its dest
231
359
  // path falls under an active destinationDir (glob target output).
232
360
  function isCurrentOutput(dest: string): boolean {
@@ -237,8 +365,11 @@ export class BuildService {
237
365
  return false;
238
366
  }
239
367
 
240
- // Find files to prune
241
- const toDelete = state.files.filter(f => !isCurrentOutput(f.dest));
368
+ // Find files to prune. Anything inside a linked checkout or the shared
369
+ // recipe store is never a prune candidate, whatever the state file says.
370
+ const toDelete = state.files.filter(
371
+ f => !isCurrentOutput(f.dest) && !isProtectedPath(f.dest, protectedPaths)
372
+ );
242
373
 
243
374
  if (dryRun) {
244
375
  for (const entry of toDelete) {
@@ -247,12 +378,33 @@ export class BuildService {
247
378
  return;
248
379
  }
249
380
 
250
- stateService.deleteTrackedFiles(toDelete, state.dirs);
381
+ stateService.deleteTrackedFiles(toDelete, state.dirs, protectedPaths);
251
382
  for (const entry of toDelete) console.log(` ✗ pruned: ${entry.dest}`);
252
383
 
253
384
  // Update state: remove pruned entries and any dirs that no longer exist
254
- state.files = state.files.filter(f => isCurrentOutput(f.dest));
385
+ const deleted = new Set(toDelete.map(entry => entry.dest));
386
+ state.files = state.files.filter(f => !deleted.has(f.dest));
255
387
  state.dirs = state.dirs.filter(d => fs.existsSync(d));
256
388
  await stateService.save(stateFilePath, state);
257
389
  }
258
390
  }
391
+
392
+ /**
393
+ * Compiles and prunes a project exactly the way `sous build` does with no flags.
394
+ *
395
+ * Shared by the subscription commands, which rebuild the project the moment they
396
+ * have changed what it subscribes to: a newly subscribed recipe's files appear,
397
+ * and a removed one's files are pruned, without anyone having to remember a
398
+ * second command. Every option is left at its default on purpose, because the
399
+ * point is to run the ordinary build and nothing else.
400
+ *
401
+ * @param settings - The merged project config, reloaded after the change.
402
+ * @param configContext - Where the active config was discovered.
403
+ * @returns True when compile and prune both succeeded.
404
+ */
405
+ export async function buildProjectOutputs(
406
+ settings: Settings,
407
+ configContext: ConfigContext
408
+ ): Promise<boolean> {
409
+ return new BuildService().build(settings, { configContext });
410
+ }