@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,104 +0,0 @@
1
- /**
2
- * The user-level sous directory: where machine-wide, project-independent state
3
- * lives. Today that is the recipe store, globally linked checkouts and the
4
- * machine-wide links map; the user-level config LAYER is a separate effort and
5
- * is deliberately NOT read from here.
6
- *
7
- * The location is `~/.sous`, overridable with the `SOUS_HOME` environment
8
- * variable.
9
- *
10
- * IMPORTANT: `SOUS_HOME` is file-settable. Unlike `SOUS_CONFIG`, `SOUS_DIR` and
11
- * `SOUS_CONFD`, which decide which project is active and therefore must come
12
- * from the real environment only, `SOUS_HOME` may be set in `.sous/.env.local`
13
- * or `.sous/.env`. Every function here reads `process.env` at CALL time for
14
- * exactly that reason; never capture the value at import time, because the env
15
- * files are loaded after this module is first imported (see `loadEnvFiles` in
16
- * `base-command.ts`).
17
- */
18
-
19
- import os from "node:os";
20
- import path from "node:path";
21
-
22
- /** The directory name used under the user's home directory. */
23
- export const SOUS_HOME_DIR_NAME = ".sous";
24
-
25
- /** The environment variable that overrides the user-level sous directory. */
26
- export const SOUS_HOME_ENV_VAR = "SOUS_HOME";
27
-
28
- /** Subdirectory of the user-level directory holding the recipe store. */
29
- export const STORE_DIR_NAME = "cache";
30
-
31
- /** Subdirectory of the user-level directory holding globally linked checkouts. */
32
- export const GLOBAL_REPOS_DIR_NAME = "repos";
33
-
34
- /** File name of the machine-wide links map. */
35
- export const GLOBAL_LINKS_FILENAME = "sous.links.json";
36
-
37
- /** A read-only view of an environment, so callers can pass a test fixture. */
38
- export type EnvLike = Record<string, string | undefined>;
39
-
40
- /**
41
- * Expands a leading `~` in a path, so `SOUS_HOME=~/sous-home` behaves the way a
42
- * user typing it into an env file expects. A bare `~` and `~/...` expand; a
43
- * `~user/...` form does not (sous has no way to resolve another account's home
44
- * directory portably) and is left alone.
45
- *
46
- * @param value - The raw path, as written by the user.
47
- */
48
- function expandHome(value: string): string {
49
- if (value === "~") return os.homedir();
50
- if (value.startsWith("~/") || value.startsWith("~\\")) {
51
- return path.join(os.homedir(), value.slice(2));
52
- }
53
- return value;
54
- }
55
-
56
- /**
57
- * Resolves the user-level sous directory: `$SOUS_HOME` when it is set to a
58
- * non-blank value, otherwise `<home>/.sous`. The result is normalized but is
59
- * NOT created; each consumer creates the subdirectory it needs.
60
- *
61
- * A blank or whitespace-only `SOUS_HOME` is treated as unset rather than as the
62
- * current directory, matching how `base-command.ts` treats the other `SOUS_*`
63
- * variables.
64
- *
65
- * @param env - The environment to read; defaults to the live `process.env`.
66
- */
67
- export function resolveSousHome(env: EnvLike = process.env): string {
68
- const raw = env[SOUS_HOME_ENV_VAR];
69
- if (typeof raw === "string" && raw.trim().length > 0) {
70
- return path.normalize(path.resolve(expandHome(raw.trim())));
71
- }
72
- return path.join(os.homedir(), SOUS_HOME_DIR_NAME);
73
- }
74
-
75
- /**
76
- * Resolves the root of the machine-wide recipe store, `$SOUS_HOME/cache`.
77
- *
78
- * @param env - The environment to read; defaults to the live `process.env`.
79
- */
80
- export function resolveStoreRoot(env: EnvLike = process.env): string {
81
- return path.join(resolveSousHome(env), STORE_DIR_NAME);
82
- }
83
-
84
- /**
85
- * Resolves the directory holding globally linked checkouts,
86
- * `$SOUS_HOME/repos`. A `sous repo link --global` clones into
87
- * `<this dir>/<owner>/<repo>`.
88
- *
89
- * @param env - The environment to read; defaults to the live `process.env`.
90
- */
91
- export function resolveGlobalReposDir(env: EnvLike = process.env): string {
92
- return path.join(resolveSousHome(env), GLOBAL_REPOS_DIR_NAME);
93
- }
94
-
95
- /**
96
- * Resolves the machine-wide links map, `$SOUS_HOME/sous.links.json`. A
97
- * project's own `.sous/sous.links.json` is read alongside it and wins on
98
- * conflict.
99
- *
100
- * @param env - The environment to read; defaults to the live `process.env`.
101
- */
102
- export function resolveGlobalLinksPath(env: EnvLike = process.env): string {
103
- return path.join(resolveSousHome(env), GLOBAL_LINKS_FILENAME);
104
- }
package/src/lib/state.ts DELETED
@@ -1,167 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import type { VarScope } from "./settings.js";
5
-
6
- // --- Types ---------------------------------------------------------------------------------------
7
-
8
- /** A single file entry tracked by the state file. */
9
- export type StateFileEntry = {
10
- /** Absolute destination path of the output file. */
11
- dest: string;
12
- /** SHA-256 hex hash of the source content at build time. */
13
- srcHash: string;
14
- /** SHA-256 hex hash of the destination file content at build time. */
15
- destHash: string;
16
- /** File size in bytes. */
17
- size: number;
18
- /** ISO timestamp when this file was last written. */
19
- builtAt: string;
20
- };
21
-
22
- /** The full state tracked for a project across builds. */
23
- export type StateFile = {
24
- /** ISO timestamp of the last successful build. */
25
- lastBuild: string;
26
- /** The resolved variable scope used during the last build. */
27
- resolvedVars: VarScope;
28
- /** Absolute paths of directories that Sous created (not pre-existing). */
29
- dirs: string[];
30
- /** All output files written by Sous for this project. */
31
- files: StateFileEntry[];
32
- };
33
-
34
- // --- Protected paths -----------------------------------------------------------------------------
35
-
36
- /**
37
- * True when a path is one of the protected roots, or sits underneath one.
38
- *
39
- * Both sides are normalized and resolved first, so a state file written with a
40
- * different spelling of the same directory is still recognised.
41
- *
42
- * isProtectedPath("/home/me/.sous/cache/r/n/x/1.0.0/a.md", ["/home/me/.sous/cache"]);
43
- * // -> true
44
- *
45
- * @param target - The path something is about to delete.
46
- * @param protectedRoots - The directories that must never be reached into.
47
- */
48
- export function isProtectedPath(target: string, protectedRoots: string[]): boolean {
49
- if (protectedRoots.length === 0) return false;
50
- const candidate = path.resolve(target);
51
- return protectedRoots.some((root) => {
52
- const resolved = path.resolve(root);
53
- return candidate === resolved || candidate.startsWith(resolved + path.sep);
54
- });
55
- }
56
-
57
- // --- StateService --------------------------------------------------------------------------------
58
-
59
- /**
60
- * Manages reading and writing the project state file.
61
- * The state file tracks all files and directories Sous has written,
62
- * enabling sous prune and sous clear to clean up precisely.
63
- */
64
- export class StateService {
65
- /**
66
- * Derives the state file path for a config.
67
- *
68
- * Precedence:
69
- * 1. A `stateFilePath` variable in the settings scope (explicit override).
70
- * 2. `<sousDir>/sous.state.json`.
71
- * 3. `<cwd>/sous.state.json` as a last resort.
72
- *
73
- * @param vars - The resolved settings-scope variables. `sousDir` is
74
- * auto-injected by buildAutoVars when a config has been discovered.
75
- */
76
- getFilePath(vars?: VarScope): string {
77
- if (vars?.stateFilePath) {
78
- return vars.stateFilePath;
79
- }
80
- if (vars?.sousDir) {
81
- return path.join(vars.sousDir, "sous.state.json");
82
- }
83
- return path.join(process.cwd(), "sous.state.json");
84
- }
85
-
86
- /** Loads the state file for a project. Returns null if it does not exist. */
87
- async load(filePath: string): Promise<StateFile | null> {
88
- if (!fs.existsSync(filePath)) return null;
89
- try {
90
- const raw = fs.readFileSync(filePath, "utf8");
91
- return JSON.parse(raw) as StateFile;
92
- } catch {
93
- return null;
94
- }
95
- }
96
-
97
- /**
98
- * Deletes all tracked files and removes any now-empty tracked directories.
99
- *
100
- * @param files - The tracked output files to delete.
101
- * @param dirs - The directories Sous created, removed when they end up empty.
102
- * @param protectedRoots - Directories nothing here may reach into, whatever the
103
- * state file says. Pass `protectedRepoPaths(sousDir)` so a stale entry can
104
- * never make prune or clear delete a linked checkout or the shared recipe
105
- * store.
106
- */
107
- deleteTrackedFiles(
108
- files: StateFileEntry[],
109
- dirs: string[],
110
- protectedRoots: string[] = []
111
- ): void {
112
- for (const entry of files) {
113
- if (isProtectedPath(entry.dest, protectedRoots)) continue;
114
- if (fs.existsSync(entry.dest)) fs.rmSync(entry.dest);
115
- }
116
- const sortedDirs = [...dirs].sort(
117
- (a, b) => b.split(path.sep).length - a.split(path.sep).length
118
- );
119
- for (const dir of sortedDirs) {
120
- if (isProtectedPath(dir, protectedRoots)) continue;
121
- if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
122
- fs.rmdirSync(dir);
123
- }
124
- }
125
- }
126
-
127
- /** Saves the state file to disk, creating parent directories as needed. */
128
- async save(filePath: string, state: StateFile): Promise<void> {
129
- const dir = path.dirname(filePath);
130
- if (!fs.existsSync(dir)) {
131
- fs.mkdirSync(dir, { recursive: true });
132
- }
133
- fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf8");
134
- }
135
- }
136
-
137
- // --- Hash utilities ------------------------------------------------------------------------------
138
-
139
- /** Computes a SHA-256 hex hash of a file's contents. */
140
- export async function hashFile(filePath: string): Promise<string> {
141
- const content = fs.readFileSync(filePath);
142
- return crypto.createHash("sha256").update(content).digest("hex");
143
- }
144
-
145
- /** Computes a SHA-256 hex hash of an in-memory string. */
146
- export function hashContent(content: string): string {
147
- return crypto.createHash("sha256").update(content, "utf8").digest("hex");
148
- }
149
-
150
- // --- Directory tracking --------------------------------------------------------------------------
151
-
152
- /**
153
- * Records that Sous created a directory.
154
- * Only records if the directory is not already tracked.
155
- */
156
- export function recordDirCreation(dir: string, state: StateFile): void {
157
- if (!state.dirs.includes(dir)) {
158
- state.dirs.push(dir);
159
- }
160
- }
161
-
162
- /**
163
- * Returns true if Sous created this directory (i.e., it is tracked in the state file).
164
- */
165
- export function didSousCreateDir(dir: string, state: StateFile): boolean {
166
- return state.dirs.includes(dir);
167
- }
@@ -1,192 +0,0 @@
1
- /**
2
- * The answers a build renders with.
3
- *
4
- * A recipe publishes variable DEFINITIONS; the project's env files and shell
5
- * hold the ANSWERS; the ladder (`ladder.ts`) says which answer a definition
6
- * gets. This module is the one place that turns all of that into the variable
7
- * scope a template renders from, so a `{{ variable }}` in a recipe's own skill
8
- * sees the answer to its own question without the project mapping the name by
9
- * hand through `_env`.
10
- *
11
- * Three views come out of one walk over the definitions:
12
- *
13
- * - `merged`, what the project's own templates render: one value per
14
- * variable name. Two recipes may publish the same name (the shared rung of
15
- * the ladder exists for exactly that), so the first definition in lockfile
16
- * order wins the merged view, and a project that wants something else says
17
- * so in `_vars`, which sits above every answer.
18
- * - `byRecipe`, what a recipe's own files render: the answers resolved for
19
- * that recipe's definitions, which the recipe-scoped rung of the ladder can
20
- * make different from the merged view.
21
- * - `unanswered`, every required definition that no rung and no default
22
- * answered. A build reports those and carries on, because a missing answer
23
- * is something to tell the user about, not a reason to refuse to build the
24
- * rest of the project.
25
- *
26
- * Alongside them comes `secrets`: every name some definition declares
27
- * `secret: true`. The tags that dump a whole scope (`{% showVars %}`,
28
- * `{% exportScalarVarsJs %}`) read it so a secret answer never lands in
29
- * compiled output unless a template names it explicitly.
30
- *
31
- * A definition's `default` counts as an answer of last resort: the
32
- * description a publisher writes promises what the default does, and a
33
- * template rendering an empty string instead would break that promise.
34
- *
35
- * Values are laid in exactly as the ladder found them. Nothing here resolves a
36
- * path or coerces a number; the answer is the string the user stored, the same
37
- * string an `_env` mapping would deliver.
38
- */
39
-
40
- import type { Settings, VarScope } from "../settings.js";
41
- import { BULLET } from "../../utils/formatting.js";
42
- import {
43
- ProjectDefinitionSource,
44
- definingRecipeKey,
45
- type DefinedVariable,
46
- } from "./definition-source.js";
47
- import { loadLadderContext, resolveVariable, type LadderContext } from "./ladder.js";
48
-
49
- /** The answers in play for a project, in the three views a build needs. */
50
- export interface RecipeAnswers {
51
- /** One value per variable name; the first definition in lockfile order wins a name. */
52
- merged: VarScope;
53
- /** The answers resolved for each recipe's own definitions, keyed `namespace/recipe`. */
54
- byRecipe: Map<string, VarScope>;
55
- /** Every required definition that nothing answered and that has no default. */
56
- unanswered: DefinedVariable[];
57
- /** Every variable name some definition declares `secret: true`, answered or not. */
58
- secrets: Set<string>;
59
- }
60
-
61
- /** How the answers are resolved. */
62
- export interface RecipeAnswerOptions {
63
- /** The merged project config, read for its `varMappings` block. */
64
- settings: Settings;
65
- /** The project's `.sous/` directory, which holds the lockfile and both env files. */
66
- sousDir: string;
67
- /**
68
- * The definitions to answer. Defaults to every definition the project's
69
- * lockfile pins; tests hand in a fixed list.
70
- */
71
- definitions?: DefinedVariable[];
72
- /**
73
- * The environment the ladder treats as the shell. A build passes nothing and
74
- * gets `process.env`, which already holds every env-file value in precedence
75
- * order (the files are loaded first-writer-wins, shell first), so the value
76
- * that comes back is the right one; only the layer it is attributed to would
77
- * differ, and a build does not report that.
78
- */
79
- shellEnv?: NodeJS.ProcessEnv;
80
- /** The environment that decides where the store is; defaults to `process.env`. */
81
- env?: NodeJS.ProcessEnv;
82
- }
83
-
84
- /** The empty result, for a project that locks nothing. */
85
- export function noRecipeAnswers(): RecipeAnswers {
86
- return { merged: {}, byRecipe: new Map(), unanswered: [], secrets: new Set() };
87
- }
88
-
89
- /**
90
- * Resolves every definition in play through the ladder and lays the answers
91
- * out in the three views a build renders and reports with.
92
- *
93
- * @param options - The project's config, its `.sous/` directory, and optionally
94
- * the definitions and environment to use instead of the real ones.
95
- */
96
- export function resolveRecipeAnswers(options: RecipeAnswerOptions): RecipeAnswers {
97
- const definitions =
98
- options.definitions ??
99
- new ProjectDefinitionSource(options.settings, options.sousDir, options.env).loadSync();
100
- if (definitions.length === 0) return noRecipeAnswers();
101
-
102
- const context: LadderContext = loadLadderContext({
103
- sousDir: options.sousDir,
104
- settings: options.settings,
105
- ...(options.shellEnv === undefined ? {} : { shellEnv: options.shellEnv }),
106
- });
107
-
108
- const result = noRecipeAnswers();
109
-
110
- for (const defined of definitions) {
111
- const value = answerFor(defined, context);
112
- const name = defined.definition.name;
113
- if (defined.definition.secret) result.secrets.add(name);
114
-
115
- if (value === undefined) {
116
- if (defined.definition.required) result.unanswered.push(defined);
117
- continue;
118
- }
119
-
120
- if (!(name in result.merged)) result.merged[name] = value;
121
-
122
- const recipeKey = definingRecipeKey(defined.recipe);
123
- const own = result.byRecipe.get(recipeKey) ?? {};
124
- own[name] = value;
125
- result.byRecipe.set(recipeKey, own);
126
- }
127
-
128
- return result;
129
- }
130
-
131
- /**
132
- * The answer one definition renders with: what the ladder found, else the
133
- * definition's own default, else nothing.
134
- *
135
- * @param defined - The definition and the recipe that published it.
136
- * @param context - The environment layers and mapping records.
137
- */
138
- function answerFor(defined: DefinedVariable, context: LadderContext): string | undefined {
139
- const resolved = resolveVariable(defined, context);
140
- if (resolved !== undefined) return resolved.value;
141
- const fallback = defined.definition.default;
142
- return fallback === undefined ? undefined : String(fallback);
143
- }
144
-
145
- /**
146
- * The scope a recipe's own files render with: the merged view, with that
147
- * recipe's own answers laid over it.
148
- *
149
- * @param answers - The resolved answers.
150
- * @param recipeKey - The recipe, as `namespace/recipe`.
151
- */
152
- export function answersForRecipe(answers: RecipeAnswers, recipeKey: string): VarScope {
153
- return { ...answers.merged, ...(answers.byRecipe.get(recipeKey) ?? {}) };
154
- }
155
-
156
- /**
157
- * The one warning a build prints when required variables are unanswered: every
158
- * variable named with the recipe that asks for it, and the command that answers
159
- * them. Undefined when nothing is missing.
160
- *
161
- * A variable the project's config defines itself, in `_vars` or through
162
- * `_env`, is not missing: the template renders that value, whatever the ladder
163
- * found. So the check is made against the scope the templates actually render
164
- * with, not against the ladder alone.
165
- *
166
- * @param answers - The resolved answers.
167
- * @param renderScope - The scope the project's templates render with.
168
- */
169
- export function unansweredWarning(
170
- answers: RecipeAnswers,
171
- renderScope: VarScope = {}
172
- ): string | undefined {
173
- const missing = answers.unanswered.filter(
174
- (defined) => renderScope[defined.definition.name] === undefined
175
- );
176
- if (missing.length === 0) return undefined;
177
-
178
- const lines = missing.map(
179
- (defined) =>
180
- ` ${BULLET} ${defined.definition.name} (asked by ${definingRecipeKey(defined.recipe)})`
181
- );
182
- const count = missing.length;
183
- const noun = count === 1 ? "variable" : "variables";
184
- const pronoun = count === 1 ? "it" : "them";
185
-
186
- return (
187
- `${count} required recipe ${noun} ${count === 1 ? "has" : "have"} no answer, so the ` +
188
- `templates that use ${pronoun} render an empty value:\n` +
189
- `${lines.join("\n")}\n` +
190
- `Run 'sous vars ask' to answer ${pronoun}.`
191
- );
192
- }