@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,473 +0,0 @@
1
- /**
2
- * Seeding the store with the core recipe that ships inside the package.
3
- *
4
- * Every project is subscribed to the `core` namespace, and a project must be
5
- * able to build with no network at all, including the very first time sous runs
6
- * on a machine. That is what this module is for: it copies the packaged core
7
- * recipe into the machine-wide store and, when nothing has ever been fetched
8
- * from the official repository, writes a small stand-in index so the resolver
9
- * can find the recipe it has just seeded.
10
- *
11
- * Three properties matter, and each one is deliberate:
12
- *
13
- * - IDEMPOTENT. Seeding runs before every build. When the store already holds
14
- * a verifying entry at this version, nothing is copied and nothing is
15
- * written.
16
- * - OFFLINE. Nothing here touches the network, and nothing here fails a build.
17
- * A store that cannot be written is reported and the build carries on
18
- * without core, which is far better than refusing to run.
19
- * - REPLACEABLE. The stand-in index is written only when the cache holds no
20
- * real index for the official repository. It carries a note saying sous
21
- * wrote it, so a later run recognizes its own placeholder and is willing to
22
- * replace it; the first successful fetch overwrites it with the real thing.
23
- *
24
- * The stand-in alone is not enough, and that is what `coreIndexOverlay` is for.
25
- * A machine that has already fetched the real index keeps it, quite rightly, and
26
- * that index publishes whatever versions of the core recipe the repository has
27
- * released. Upgrade sous and the version it asks for is, for a while, one the
28
- * repository has not published yet: the real index wins over the stand-in, the
29
- * resolver finds nothing satisfying the range, and the project silently loses
30
- * its core skills until the release pipeline catches up. So the packaged version
31
- * is folded into the index IN MEMORY whenever it is missing, carrying the hash
32
- * of the entry that was just seeded. The cached file is never touched, so it
33
- * stays an honest record of what upstream served, and the moment upstream does
34
- * publish that version its own entry is what gets used.
35
- */
36
-
37
- import fs from "node:fs";
38
- import path from "node:path";
39
- import semver from "semver";
40
- import {
41
- parseIndexFile,
42
- stringifyIndexFile,
43
- type IndexFile,
44
- } from "./formats/index-file.js";
45
- import {
46
- INDEX_CACHE_DIRNAME,
47
- INDEX_SIDECAR_SUFFIX,
48
- type IndexOverlay,
49
- } from "./providers/index-cache.js";
50
- import { warning } from "../../utils/formatting.js";
51
- import { hashDirectorySync } from "./store/hash.js";
52
- import { identitySegments } from "./identity.js";
53
- import { ensureIndexCacheDirectory } from "../../utils/sous-directory.js";
54
- import type { RecipeStoreLike, StoreKey } from "./store/contract.js";
55
- import {
56
- CORE_NAMESPACE,
57
- CORE_RECIPE_KEY,
58
- CORE_RECIPE_NAME,
59
- CORE_RECIPE_PATH,
60
- OFFICIAL_REPO_IDENTITY,
61
- OFFICIAL_REPO_NAME,
62
- packagedCoreRecipeDir,
63
- readPackagedCoreManifest,
64
- } from "./core-recipe.js";
65
-
66
- /**
67
- * The note the seed index carries in its `$comment` field. A cached index
68
- * carrying this exact text is sous's own placeholder rather than something a
69
- * repository published, so a later seed may overwrite it.
70
- */
71
- export const SEED_INDEX_COMMENT =
72
- "Written by sous itself from the core recipe inside the installed package, so a " +
73
- "project can build before it has ever reached the network. The first successful " +
74
- "fetch of the real index replaces this file.";
75
-
76
- /** The description the seed index gives the core namespace. */
77
- const CORE_NAMESPACE_DESCRIPTION =
78
- "Skills that teach an agent about sous itself: what sous manages, how its " +
79
- "configuration works, how templates render, and how skills are written. " +
80
- "Auto-subscribed in every project, with opt-out-only semantics.";
81
-
82
- /** What the seed is asked to do. */
83
- export type SeedCoreRecipeOptions = {
84
- /** The store to seed. */
85
- store: RecipeStoreLike;
86
- /** The store's root directory, which is where the index cache lives. */
87
- storeRoot?: string;
88
- /** The version to seed, which is by rule the running sous version. */
89
- sousVersion: string;
90
- /** The installed package's root directory. Defaults to the running CLI's own. */
91
- packageRoot?: string;
92
- /** The clock, so a written timestamp is predictable in tests. */
93
- now?: () => Date;
94
- /**
95
- * The index cache to teach about the packaged version, once the seed knows
96
- * its hash. Without this the seeded recipe is resolvable only on a machine
97
- * whose cached index is sous's own stand-in.
98
- */
99
- indexCache?: IndexOverlayTarget;
100
- /** Where warnings go. Defaults to the console warning banner. */
101
- warn?: (message: string) => void;
102
- };
103
-
104
- /**
105
- * The part of the index cache the seed uses: somewhere to install what it has
106
- * learned. Named as a small structural type so the seed does not depend on the
107
- * cache's implementation.
108
- */
109
- export type IndexOverlayTarget = {
110
- setOverlay(overlay: IndexOverlay | undefined): void;
111
- };
112
-
113
- /** What the seed did. */
114
- export type SeedCoreRecipeReport = {
115
- /** True when the recipe's files were copied into the store on this run. */
116
- seeded: boolean;
117
- /** True when the store already held a verifying entry, so nothing was copied. */
118
- alreadyPresent: boolean;
119
- /** True when a stand-in index was written for the official repository. */
120
- wroteIndex: boolean;
121
- /** The version that was seeded. */
122
- version: string;
123
- /** The content hash of the seeded entry, when there is one. */
124
- hash?: string;
125
- /**
126
- * Why the seed did nothing, when it could not run. A complete sentence, meant
127
- * to be shown as a warning; the seed never throws.
128
- */
129
- skippedBecause?: string;
130
- };
131
-
132
- /**
133
- * Copies the packaged core recipe into the store and, when nothing real is
134
- * cached, writes a stand-in index for the official repository so the resolver
135
- * can see what was just seeded.
136
- *
137
- * @param options - The store to seed, the version to seed it at, and the clock.
138
- */
139
- export async function seedCoreRecipe(
140
- options: SeedCoreRecipeOptions
141
- ): Promise<SeedCoreRecipeReport> {
142
- const version = options.sousVersion;
143
- const now = options.now ?? (() => new Date());
144
- const storeRoot = options.storeRoot ?? options.store.root;
145
-
146
- const key: StoreKey = {
147
- identity: OFFICIAL_REPO_IDENTITY,
148
- namespace: CORE_NAMESPACE,
149
- name: CORE_RECIPE_NAME,
150
- version,
151
- };
152
-
153
- const report: SeedCoreRecipeReport = {
154
- seeded: false,
155
- alreadyPresent: false,
156
- wroteIndex: false,
157
- version,
158
- };
159
-
160
- try {
161
- const existing = await options.store.get(key);
162
-
163
- if (existing !== undefined) {
164
- report.alreadyPresent = true;
165
- report.hash = existing.entry.hash;
166
- } else {
167
- const source = packagedCoreRecipeDir(options.packageRoot);
168
- const entry = await options.store.put(key, source);
169
- report.seeded = true;
170
- report.hash = entry.hash;
171
- }
172
-
173
- // The cache is taught before the stand-in is considered, because the two
174
- // answer different halves of the same problem: the stand-in covers a machine
175
- // that has never fetched anything, and the overlay covers one that has.
176
- options.indexCache?.setOverlay(
177
- coreIndexOverlay({
178
- version,
179
- hash: report.hash!,
180
- ...(options.packageRoot === undefined ? {} : { packageRoot: options.packageRoot }),
181
- ...(options.warn === undefined ? {} : { warn: options.warn }),
182
- })
183
- );
184
-
185
- report.wroteIndex = writeSeedIndex({
186
- storeRoot,
187
- version,
188
- hash: report.hash!,
189
- now: now(),
190
- ...(options.packageRoot === undefined ? {} : { packageRoot: options.packageRoot }),
191
- });
192
- } catch (error) {
193
- // Seeding is a convenience, not a precondition: a read-only store or a
194
- // damaged package must not stop a build that may not even use recipes.
195
- report.skippedBecause =
196
- `Sous could not seed the core recipe it ships with, so the skills in the ` +
197
- `'core' namespace are unavailable until this is fixed.\n` +
198
- `${error instanceof Error ? error.message : String(error)}`;
199
- }
200
-
201
- return report;
202
- }
203
-
204
- /** What the overlay needs to know about the version sous ships. */
205
- export type CoreIndexOverlayOptions = {
206
- /** The packaged version, which is by rule the running sous version. */
207
- version: string;
208
- /**
209
- * The content hash of the entry the seed put in the store, or a function
210
- * that works it out the first time it is needed. The function form is what
211
- * lets an overlay be installed before anything has been seeded.
212
- */
213
- hash: string | (() => string);
214
- /** The installed package's root directory. Defaults to the running CLI's own. */
215
- packageRoot?: string;
216
- /** Where the one warning this can produce goes. Defaults to the console banner. */
217
- warn?: (message: string) => void;
218
- };
219
-
220
- /**
221
- * The overlay every index cache a command builds starts with: the packaged core
222
- * version, hashed from the package itself the first time the official
223
- * repository's index is read.
224
- *
225
- * Seeding installs a precise overlay of its own (carrying the hash of the entry
226
- * it just wrote), but only the commands that seed get that one. This is what
227
- * makes the packaged version resolvable everywhere else too: a lockfile rebuild,
228
- * a browsing command, anything that reads the official index. The hash of the
229
- * packaged folder is the hash a seeded store entry carries, because the store
230
- * copies the folder byte for byte.
231
- *
232
- * A package whose core recipe cannot be read adds nothing, silently: seeding
233
- * reports that failure in full, and a read-only command has nothing better to
234
- * say about it.
235
- *
236
- * @param options - The packaged version, and where the package is.
237
- */
238
- export function packagedCoreIndexOverlay(options: {
239
- /** The packaged version, which is by rule the running sous version. */
240
- version: string;
241
- /** The installed package's root directory. Defaults to the running CLI's own. */
242
- packageRoot?: string;
243
- /** Where the one warning the overlay can produce goes. */
244
- warn?: (message: string) => void;
245
- }): IndexOverlay {
246
- let hash: string | null | undefined;
247
- const packagedHash = (): string | null => {
248
- if (hash === undefined) {
249
- try {
250
- hash = hashDirectorySync(packagedCoreRecipeDir(options.packageRoot));
251
- } catch {
252
- hash = null;
253
- }
254
- }
255
- return hash;
256
- };
257
-
258
- const overlay = coreIndexOverlay({
259
- version: options.version,
260
- hash: () => packagedHash()!,
261
- ...(options.packageRoot === undefined ? {} : { packageRoot: options.packageRoot }),
262
- ...(options.warn === undefined ? {} : { warn: options.warn }),
263
- });
264
-
265
- return (identity, index) => {
266
- if (identity !== OFFICIAL_REPO_IDENTITY) return index;
267
- if (packagedHash() === null) return index;
268
- return overlay(identity, index);
269
- };
270
- }
271
-
272
- /**
273
- * Builds the overlay that makes the packaged core recipe resolvable whatever
274
- * the official repository has published so far.
275
- *
276
- * Three cases, and the order matters:
277
- *
278
- * - The index already publishes this version with this hash: nothing to add,
279
- * and the index is returned untouched. This is the ordinary case once the
280
- * release pipeline has caught up.
281
- * - The index publishes this version with a DIFFERENT hash: upstream wins, and
282
- * sous says so once. The two are built from the same bytes, so this should
283
- * not happen; when it does, the version a repository published is the one a
284
- * lockfile should be able to pin on any machine, seeded or not.
285
- * - The index does not publish this version at all: it is added, carrying the
286
- * hash of the entry the seed just wrote and marked `seeded` so a listing can
287
- * say it came packaged with sous.
288
- *
289
- * Nothing here writes anything. The returned index is a copy; the one passed in
290
- * is left exactly as it was read.
291
- *
292
- * @param options - The packaged version, its hash, and where a warning goes.
293
- */
294
- export function coreIndexOverlay(options: CoreIndexOverlayOptions): IndexOverlay {
295
- let warned = false;
296
- const packagedHash = (): string =>
297
- typeof options.hash === "function" ? options.hash() : options.hash;
298
-
299
- return (identity: string, index: IndexFile): IndexFile => {
300
- if (identity !== OFFICIAL_REPO_IDENTITY) return index;
301
-
302
- const published = index.recipes[CORE_RECIPE_KEY]?.versions[options.version];
303
-
304
- if (published !== undefined) {
305
- if (published.hash !== packagedHash() && !warned) {
306
- warned = true;
307
- (options.warn ?? warning)(
308
- `The repository '${OFFICIAL_REPO_NAME}' publishes version ${options.version} of ` +
309
- `'${CORE_RECIPE_KEY}' with different contents from the copy inside this ` +
310
- `installation of sous, so sous is using the published one.\n` +
311
- ` Published: ${published.hash}\n` +
312
- ` Packaged: ${packagedHash()}\n` +
313
- ` Reinstalling sous will bring the two back into line.`
314
- );
315
- }
316
- return index;
317
- }
318
-
319
- const recipe = index.recipes[CORE_RECIPE_KEY];
320
- const description = recipe?.description ?? packagedCoreDescription(options.packageRoot);
321
-
322
- return {
323
- ...index,
324
- namespaces: {
325
- ...index.namespaces,
326
- [CORE_NAMESPACE]: index.namespaces[CORE_NAMESPACE] ?? {
327
- description: CORE_NAMESPACE_DESCRIPTION,
328
- },
329
- },
330
- recipes: {
331
- ...index.recipes,
332
- [CORE_RECIPE_KEY]: {
333
- path: recipe?.path ?? CORE_RECIPE_PATH,
334
- ...(description === undefined ? {} : { description }),
335
- versions: {
336
- ...recipe?.versions,
337
- [options.version]: {
338
- hash: packagedHash(),
339
- tag: `${CORE_RECIPE_KEY}@${options.version}`,
340
- prerelease: semver.prerelease(options.version) !== null,
341
- seeded: true,
342
- },
343
- },
344
- },
345
- },
346
- };
347
- };
348
- }
349
-
350
- /**
351
- * The packaged recipe's one-paragraph summary, or undefined when the manifest
352
- * cannot be read. A description is decoration; losing it must not cost a project
353
- * its core skills.
354
- *
355
- * @param packageRoot - The installed package's root directory.
356
- */
357
- function packagedCoreDescription(packageRoot?: string): string | undefined {
358
- try {
359
- return readPackagedCoreManifest(packageRoot).description;
360
- } catch {
361
- return undefined;
362
- }
363
- }
364
-
365
- /**
366
- * Writes the stand-in index for the official repository, unless the cache
367
- * already holds one that a repository actually published.
368
- *
369
- * Returns true when a file was written.
370
- *
371
- * @param input - Where the cache is, and what the seeded entry looks like.
372
- */
373
- function writeSeedIndex(input: {
374
- storeRoot: string;
375
- version: string;
376
- hash: string;
377
- now: Date;
378
- packageRoot?: string;
379
- }): boolean {
380
- // The cache files an index under the repository's identity, which is several
381
- // directories deep; the seed writes to exactly the same place a real fetch
382
- // would, so the first successful fetch replaces this copy rather than
383
- // sitting beside it.
384
- const segments = identitySegments(OFFICIAL_REPO_IDENTITY);
385
- const last = segments.pop()!;
386
- const directory = path.join(input.storeRoot, INDEX_CACHE_DIRNAME, ...segments);
387
- const indexPath = path.join(directory, `${last}.json`);
388
- const sidecarPath = path.join(directory, `${last}${INDEX_SIDECAR_SUFFIX}`);
389
-
390
- if (!isReplaceableIndex(indexPath, input.version)) return false;
391
-
392
- const manifest = readPackagedCoreManifest(input.packageRoot);
393
- const timestamp = input.now.toISOString();
394
-
395
- const index: IndexFile = {
396
- $comment: SEED_INDEX_COMMENT,
397
- formatVersion: 1,
398
- name: OFFICIAL_REPO_NAME,
399
- generatedAt: timestamp,
400
- generator: input.version,
401
- namespaces: {
402
- [CORE_NAMESPACE]: { description: CORE_NAMESPACE_DESCRIPTION },
403
- },
404
- recipes: {
405
- [CORE_RECIPE_KEY]: {
406
- path: CORE_RECIPE_PATH,
407
- ...(manifest.description === undefined
408
- ? {}
409
- : { description: manifest.description }),
410
- versions: {
411
- [input.version]: {
412
- hash: input.hash,
413
- tag: `${CORE_RECIPE_KEY}@${input.version}`,
414
- prerelease: semver.prerelease(input.version) !== null,
415
- releasedAt: timestamp,
416
- seeded: true,
417
- },
418
- },
419
- },
420
- },
421
- };
422
-
423
- // No sidecar is written, deliberately. The sidecar is what says "this copy was
424
- // fetched at such a time", and this copy was not fetched at all. Without one,
425
- // sous treats the stand-in as infinitely old and tries upstream on the very
426
- // next command: the first run with a network gets the real index immediately
427
- // rather than waiting out a freshness window it never earned. When there is no
428
- // network the fetch fails, the stand-in is used, and sous says so, which is the
429
- // same last-good behavior every other repository gets.
430
- //
431
- // Any sidecar left over from an earlier fetch is removed for the same reason.
432
- ensureIndexCacheDirectory(path.join(input.storeRoot, INDEX_CACHE_DIRNAME));
433
- fs.mkdirSync(directory, { recursive: true });
434
- fs.writeFileSync(indexPath, stringifyIndexFile(index), "utf8");
435
- fs.rmSync(sidecarPath, { force: true });
436
- return true;
437
- }
438
-
439
- /**
440
- * True when the seed may write over whatever is at `indexPath`.
441
- *
442
- * Three cases, in order:
443
- *
444
- * - Nothing cached, or a cached file that no longer parses: write. A damaged
445
- * copy is worth less than a stand-in that works.
446
- * - A stand-in sous wrote itself that does not name the version being seeded:
447
- * write. This is how a machine that upgrades sous while offline ends up with
448
- * an index naming the new version.
449
- * - Anything else, including sous's own stand-in that already names this
450
- * version: leave it alone. Rewriting it on every build would reset the
451
- * freshness clock, and sous would then never look upstream for the real one.
452
- *
453
- * @param indexPath - Where the cached index for the official repository lives.
454
- * @param version - The version being seeded.
455
- */
456
- function isReplaceableIndex(indexPath: string, version: string): boolean {
457
- let text: string;
458
- try {
459
- text = fs.readFileSync(indexPath, "utf8");
460
- } catch {
461
- return true;
462
- }
463
-
464
- let cached: IndexFile;
465
- try {
466
- cached = parseIndexFile(JSON.parse(text), indexPath);
467
- } catch {
468
- return true;
469
- }
470
-
471
- if (cached.$comment !== SEED_INDEX_COMMENT) return false;
472
- return cached.recipes[CORE_RECIPE_KEY]?.versions[version] === undefined;
473
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * The contract between the recipe store (Phase 2a) and everything that fills or reads it
3
- * (providers, resolver, lockfile restore, the build). Both sides are written against this
4
- * file; keep it dependency-free so either side can compile without the other.
5
- */
6
- import type { StoreEntry } from "../formats/store-entry.js";
7
-
8
- /**
9
- * Identifies one recipe version inside one repository.
10
- *
11
- * The store is MACHINE-WIDE, so it is keyed by the repository's canonical
12
- * identity rather than by a project's short name for it: two projects that call
13
- * the same repository different things still share one cached copy, and two
14
- * projects that use the same short name for different repositories never
15
- * collide.
16
- */
17
- export interface StoreKey {
18
- /** The repository's canonical identity, such as `github.com/sous-io/sous-recipes`. */
19
- identity: string;
20
- namespace: string;
21
- name: string;
22
- version: string;
23
- }
24
-
25
- /** What a successful lookup returns: where the files are and the verified marker. */
26
- export interface StoreHit {
27
- dir: string;
28
- entry: StoreEntry;
29
- }
30
-
31
- /** Options for a garbage-collection pass. */
32
- export interface StoreGcOptions {
33
- /** Upper bound for the whole store in bytes; entries are evicted least-recently-used first. */
34
- maxBytes: number;
35
- /** Keys that must survive no matter what (everything a lockfile still pins). */
36
- keep?: StoreKey[];
37
- dryRun?: boolean;
38
- }
39
-
40
- export interface StoreGcReport {
41
- evicted: StoreEntry[];
42
- kept: StoreEntry[];
43
- bytesBefore: number;
44
- bytesAfter: number;
45
- }
46
-
47
- /** The recipe store: one immutable folder per recipe version, verified against its content hash. */
48
- export interface RecipeStoreLike {
49
- /** The absolute root directory of this store. */
50
- readonly root: string;
51
- /** The directory an entry lives in (whether or not it exists). */
52
- entryDir(key: StoreKey): string;
53
- /** Copies `sourceDir` into the store, hashes it, verifies against `expectedHash` when given, writes the marker. */
54
- put(key: StoreKey, sourceDir: string, expectedHash?: string): Promise<StoreEntry>;
55
- /** Returns the entry if present and its hash still verifies; touches last access. Undefined when absent. */
56
- get(key: StoreKey): Promise<StoreHit | undefined>;
57
- has(key: StoreKey): Promise<boolean>;
58
- remove(key: StoreKey): Promise<void>;
59
- list(): Promise<StoreEntry[]>;
60
- gc(options: StoreGcOptions): Promise<StoreGcReport>;
61
- }
62
-
63
- /** Computes the canonical `sha256-<hex>` content hash of a directory tree. */
64
- export type DirectoryHasher = (dir: string) => Promise<string>;