@sous-io/sous 0.2.37 → 1.0.2

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 +1094 -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,232 +0,0 @@
1
- /**
2
- * The real namespace resolver: the one the compiler uses.
3
- *
4
- * Phase 4 defined the `NamespaceResolver` contract and shipped an in-memory
5
- * implementation for tests. This is the implementation backed by the project's
6
- * actual state: the lockfile says which recipes are in play and at what version,
7
- * the links map and the store say where each one's files are, and the
8
- * lockfile's holder lists say what each recipe is allowed to address.
9
- *
10
- * The scoping rule is the whole point, and it is enforced by handing
11
- * `StaticNamespaceResolver` the right two maps:
12
- *
13
- * - a file inside a recipe may address only that recipe's own declared
14
- * dependencies (`depends` plus `subscribes`), at their pinned versions. The
15
- * lockfile records which keys each recipe holds, however its manifest wrote
16
- * them (a sibling ref, a locator, a URL copied from the browser), so that
17
- * record is what decides it;
18
- * - a file in the project's own templates may address every recipe the
19
- * lockfile pins, whatever holds it: the project, a set it subscribes to,
20
- * or another recipe's `depends`.
21
- *
22
- * When a project template names a recipe the lockfile does not pin, the error
23
- * says which recipe used to bring it in, when the cached indexes or the config
24
- * can tell.
25
- *
26
- * A project with no lockfile entries gets no resolver at all, so a project that
27
- * uses no repositories behaves exactly as it did before: a `~` reference in an
28
- * include line is just a relative path.
29
- */
30
-
31
- import semver from "semver";
32
- import type { Settings } from "../settings.js";
33
- import { resolveStoreRoot } from "../sous-home.js";
34
- import {
35
- RefSource,
36
- namespaceOfKey,
37
- parseNamedRef,
38
- shortKey,
39
- } from "../../services/ref-resolver/index.js";
40
- import {
41
- StaticNamespaceResolver,
42
- type DroppedRecipe,
43
- type NamespaceResolver,
44
- } from "./namespace-resolver.js";
45
- import {
46
- listLockedRecipes,
47
- readProjectLockfile,
48
- type LockedRecipeLocation,
49
- } from "./locked-recipes.js";
50
- import { createIndexCache } from "./providers/index.js";
51
- import type { IndexFile } from "./formats/index-file.js";
52
- import type { Lockfile } from "./formats/lockfile.js";
53
-
54
- /** How the project's namespace resolver is built. */
55
- export type ProjectNamespaceResolverOptions = {
56
- /** The project's `.sous/` directory, which holds the lockfile and the links map. */
57
- sousDir: string;
58
- /** The merged project config, read for its subscriptions. */
59
- settings: Settings;
60
- /** The environment to read; decides where the store and machine-wide links map are. */
61
- env?: NodeJS.ProcessEnv;
62
- /** The locked recipes, when the caller has already located them. */
63
- locked?: LockedRecipeLocation[];
64
- };
65
-
66
- /**
67
- * Builds the namespace resolver for a project, or undefined when the project
68
- * locks no recipes at all.
69
- *
70
- * createProjectNamespaceResolver({ sousDir, settings })
71
- * // -> resolves "@~workflow/task-files/_partials/resume.md" (or a glob such as
72
- * // "@~*" + "/*" + "/memories/*.md") to the pinned
73
- * // recipe directory, when the project (or the including recipe) may address it
74
- *
75
- * @param options - The project's `.sous/` directory, its config and the environment.
76
- */
77
- export function createProjectNamespaceResolver(
78
- options: ProjectNamespaceResolverOptions
79
- ): NamespaceResolver | undefined {
80
- const locked =
81
- options.locked ??
82
- listLockedRecipes({
83
- sousDir: options.sousDir,
84
- ...(options.env === undefined ? {} : { env: options.env }),
85
- });
86
-
87
- if (locked.length === 0) return undefined;
88
-
89
- const recipes: Record<string, string> = {};
90
- const repos: Record<string, string> = {};
91
- const dependencies: Record<string, string[]> = {};
92
-
93
- for (const recipe of locked) {
94
- recipes[recipe.key] = recipe.dir;
95
- repos[recipe.key] = recipe.repo;
96
-
97
- // Every recipe holding this one declared it, in `depends` or `subscribes`,
98
- // and the lockfile records the key that declaration resolved to.
99
- for (const holder of recipe.requestedBy) {
100
- (dependencies[holder] ??= []).push(recipe.key);
101
- }
102
- }
103
-
104
- // No `projectScope`: a project template may address every recipe the
105
- // lockfile pins, and those are exactly the recipes this resolver knows.
106
- return new StaticNamespaceResolver({
107
- recipes,
108
- repos,
109
- dependencies,
110
- explainMissing: (recipe) => explainUnpinnedRecipe(recipe, options),
111
- });
112
- }
113
-
114
- /**
115
- * Why the lockfile does not pin a recipe a project template asked for, when
116
- * that can be known without the network. Two things are checked, in order:
117
- *
118
- * - a subscription to it (or to its namespace) that is switched off;
119
- * - another published version of a pinned recipe that brought it in, read
120
- * from the cached indexes of the lockfile's repositories. That version may
121
- * declare it directly, or declare a recipe that no longer is pinned either
122
- * and that declares it in turn; the shortest such chain is reported, and
123
- * among the versions of one pinned recipe the newest wins. This is the
124
- * usual story of a set that dropped one of its members, and the members'
125
- * own libraries with it.
126
- *
127
- * Only ever runs while an error is being reported, so it reads the lockfile
128
- * and the cached indexes then rather than on every build.
129
- *
130
- * @param recipe - The `namespace/recipe` the template asked for.
131
- * @param options - The project's `.sous/` directory, its config and the environment.
132
- */
133
- export function explainUnpinnedRecipe(
134
- recipe: string,
135
- options: Pick<ProjectNamespaceResolverOptions, "sousDir" | "settings" | "env">
136
- ): DroppedRecipe | undefined {
137
- const namespace = namespaceOfKey(recipe);
138
- for (const [key, entry] of Object.entries(options.settings.subscriptions ?? {})) {
139
- if (entry?.enabled !== false) continue;
140
- let ref: string;
141
- try {
142
- ref = shortKey(parseNamedRef(key, RefSource.Config));
143
- } catch {
144
- // The config schema already refuses a key that is not a ref.
145
- continue;
146
- }
147
- if (ref === recipe || ref === namespace) {
148
- return { by: "disabled-subscription", subscription: key };
149
- }
150
- }
151
-
152
- let lock: Lockfile;
153
- try {
154
- lock = readProjectLockfile(options.sousDir);
155
- } catch {
156
- // The build reports a broken lockfile on its own; an error message is no
157
- // place to report it a second time.
158
- return undefined;
159
- }
160
-
161
- const published = publishedVersions(lock, options.env ?? process.env);
162
-
163
- /** What one published version of a recipe declares, as recipe keys. */
164
- const declaredBy = (key: string, version: string): string[] =>
165
- Object.keys(published.get(key)?.[version]?.dependencies ?? {});
166
-
167
- // Breadth first from every other version of every pinned recipe, through
168
- // recipes the lockfile does not pin, so the shortest chain is found first.
169
- type Step = { key: string; root: { recipe: string; version: string }; through: string[] };
170
- const queue: Step[] = [];
171
- for (const holder of Object.keys(lock.recipes).sort()) {
172
- const pinned = lock.recipes[holder]!.version;
173
- const versions = Object.keys(published.get(holder) ?? {})
174
- .filter((version) => version !== pinned && semver.valid(version) !== null)
175
- .sort(semver.rcompare);
176
- for (const version of versions) {
177
- for (const key of declaredBy(holder, version)) {
178
- queue.push({ key, root: { recipe: holder, version }, through: [] });
179
- }
180
- }
181
- }
182
-
183
- const seen = new Set<string>();
184
- while (queue.length > 0) {
185
- const step = queue.shift()!;
186
- if (step.key === recipe) {
187
- const root = step.root;
188
- return {
189
- by: "recipe",
190
- recipe: root.recipe,
191
- declaredAt: root.version,
192
- pinned: lock.recipes[root.recipe]!.version,
193
- ...(step.through.length > 0 ? { through: step.through } : {}),
194
- };
195
- }
196
- // A pinned recipe is explained by its own entry, and each recipe is
197
- // walked once.
198
- if (Object.hasOwn(lock.recipes, step.key) || seen.has(step.key)) continue;
199
- seen.add(step.key);
200
- for (const version of Object.keys(published.get(step.key) ?? {})) {
201
- for (const key of declaredBy(step.key, version)) {
202
- queue.push({ key, root: step.root, through: [...step.through, step.key] });
203
- }
204
- }
205
- }
206
-
207
- return undefined;
208
- }
209
-
210
- /**
211
- * Every published version of every recipe in the cached indexes of the
212
- * repositories the lockfile names, keyed by recipe. A repository with no
213
- * cached index contributes nothing.
214
- *
215
- * @param lock - The project's lockfile.
216
- * @param env - The environment, which decides where the store is.
217
- */
218
- function publishedVersions(
219
- lock: Lockfile,
220
- env: NodeJS.ProcessEnv
221
- ): Map<string, IndexFile["recipes"][string]["versions"]> {
222
- const cache = createIndexCache({ storeRoot: resolveStoreRoot(env) });
223
- const found = new Map<string, IndexFile["recipes"][string]["versions"]>();
224
- const identities = new Set(Object.values(lock.repos).map((repo) => repo.identity));
225
- for (const identity of [...identities].sort()) {
226
- const index = cache.readCached(identity);
227
- for (const [key, entry] of Object.entries(index?.recipes ?? {})) {
228
- if (!found.has(key)) found.set(key, entry.versions);
229
- }
230
- }
231
- return found;
232
- }
@@ -1,123 +0,0 @@
1
- /**
2
- * What an include line may name: the recipes the lockfile pins.
3
- *
4
- * A `RefLookup` over the pinned recipes, answering a `recipeFile` reading (the
5
- * namespace and recipe names, and the path inside the recipe) with every pinned
6
- * recipe whose names match it. Names are compared as the ref service compares
7
- * every name: exact spelling first, ignoring case as the fallback, and a name
8
- * may be a glob (`*`, `**`, `?`, `[..]`, `{a,b}`). The path inside the recipe
9
- * is carried through as written; expanding it into files is the include
10
- * resolver's job, because only it knows which directory each recipe lives in.
11
- */
12
-
13
- import {
14
- compareBytewise,
15
- matchName,
16
- repoOf,
17
- type NameMatch,
18
- type RefMatch,
19
- type SousRef,
20
- type SyncRefLookup,
21
- } from "../../services/ref-resolver/index.js";
22
-
23
- /** One pinned recipe, as far as a lookup needs to know it. */
24
- export type RecipeFileEntry = {
25
- /** The recipe's namespace. */
26
- namespace: string;
27
- /** The recipe's name. */
28
- name: string;
29
- /** The short name of the repository it came from, when known. */
30
- repo?: string;
31
- };
32
-
33
- /** The weakest of two matches: a name that matched only ignoring case spoils an exact one. */
34
- function combine(left: NameMatch, right: NameMatch): NameMatch {
35
- if (left === undefined || right === undefined) return undefined;
36
- return left === "exact" && right === "exact" ? "exact" : "folded";
37
- }
38
-
39
- /** Answers which pinned recipes an include reading names. */
40
- export class LockedRecipeFileLookup implements SyncRefLookup {
41
- /**
42
- * @param entries - Every recipe the lockfile pins.
43
- */
44
- constructor(private readonly entries: readonly RecipeFileEntry[]) {}
45
-
46
- /** The distinct namespaces of the pinned recipes, sorted. */
47
- namespaces(): string[] {
48
- return [...new Set(this.entries.map((entry) => entry.namespace))].sort(compareBytewise);
49
- }
50
-
51
- /**
52
- * The pinned recipes of the namespaces a written namespace matches, sorted
53
- * by key: those whose spelling matches exactly, or, when none does, those
54
- * that differ only in case.
55
- *
56
- * @param written - The namespace as written, possibly a glob.
57
- */
58
- recipesInNamespace(written: string): string[] {
59
- const matched = this.entries
60
- .map((entry) => ({ entry, match: matchName(written, entry.namespace) }))
61
- .filter((hit) => hit.match !== undefined);
62
- const exact = matched.filter((hit) => hit.match === "exact");
63
- return (exact.length > 0 ? exact : matched)
64
- .map((hit) => `${hit.entry.namespace}/${hit.entry.name}`)
65
- .sort(compareBytewise);
66
- }
67
-
68
- /**
69
- * True when a written namespace matches a pinned one in some spelling.
70
- *
71
- * @param written - The namespace as written, possibly a glob.
72
- */
73
- knowsNamespace(written: string): boolean {
74
- return this.entries.some((entry) => matchName(written, entry.namespace) !== undefined);
75
- }
76
-
77
- findSync(candidate: SousRef): RefMatch[] {
78
- if (candidate.kind !== "recipeFile") return [];
79
- const written = candidate.recipe;
80
- const namespaceName = written.namespace?.name;
81
- if (namespaceName === undefined) return [];
82
- const repoName = repoOf(candidate)?.name;
83
-
84
- const found: RefMatch[] = [];
85
- for (const entry of this.entries) {
86
- let match = combine(
87
- matchName(namespaceName, entry.namespace),
88
- matchName(written.name, entry.name)
89
- );
90
- if (repoName !== undefined) {
91
- match = combine(
92
- match,
93
- entry.repo === undefined ? undefined : matchName(repoName, entry.repo)
94
- );
95
- }
96
- if (match === undefined) continue;
97
- found.push({
98
- ref: {
99
- kind: "recipeFile",
100
- path: candidate.path,
101
- recipe: {
102
- kind: "recipe",
103
- name: entry.name,
104
- namespace: { kind: "namespace", name: entry.namespace },
105
- },
106
- ...(candidate.glob === undefined ? {} : { glob: true as const }),
107
- ...(candidate.vars === undefined ? {} : { vars: candidate.vars }),
108
- },
109
- exactSpelling: match === "exact",
110
- });
111
- }
112
- return found.sort((left, right) => compareBytewise(keyOf(left.ref), keyOf(right.ref)));
113
- }
114
-
115
- async find(candidate: SousRef): Promise<RefMatch[]> {
116
- return this.findSync(candidate);
117
- }
118
- }
119
-
120
- /** The `namespace/recipe` key of a recipe file ref, for ordering. */
121
- function keyOf(ref: SousRef): string {
122
- return ref.kind === "recipeFile" ? `${ref.recipe.namespace?.name}/${ref.recipe.name}` : "";
123
- }
@@ -1,254 +0,0 @@
1
- /**
2
- * Where a locked recipe's files actually are.
3
- *
4
- * The lockfile pins a recipe to a repository, a version and a content hash; it
5
- * does not say where the bytes live. Three consumers need that answer and must
6
- * all get the same one: the namespace resolver (which recipe directory a
7
- * `~namespace` include lands in), the variable definition source (which
8
- * manifests `sous vars` reads), and the recipe compile targets (which files a
9
- * build copies or renders).
10
- *
11
- * There are two possible homes, and the order matters. A LINKED repository is
12
- * read from its working copy, because a link is a deliberate instruction to
13
- * bypass versions and the lockfile; everything else is read from the immutable
14
- * store entry at the locked version. Nothing here fetches anything: a recipe the
15
- * store does not hold yet is reported as absent so the caller can restore it.
16
- *
17
- * Reading is done once per call and cached inside the returned list, because a
18
- * build asks for the same answer several times in a row.
19
- */
20
-
21
- import fs from "node:fs";
22
- import path from "node:path";
23
- import { resolveStoreRoot } from "../sous-home.js";
24
- import { parseLockfile, type LockKind, type Lockfile } from "./formats/lockfile.js";
25
- import { parseRecipeManifest, type RecipeManifest } from "./formats/recipe-manifest.js";
26
- import { parseRepoManifest, type RepoManifest } from "./formats/repo-manifest.js";
27
- import { LOCKFILE_FILENAME } from "./formats/common.js";
28
- import {
29
- findRecipeManifest,
30
- findRepoManifest,
31
- loadJsonFile,
32
- loadManifestFile,
33
- } from "./load-manifest.js";
34
- import { readEffectiveLinks } from "./links.js";
35
- import { identitySegments } from "./identity.js";
36
- import { splitRecipeKey } from "../../services/ref-resolver/index.js";
37
- import { PROJECT_HOLDER } from "./formats/lockfile.js";
38
-
39
- /** One locked recipe, together with the directory its files are read from. */
40
- export type LockedRecipeLocation = {
41
- /** The recipe key, `namespace/recipe`. */
42
- key: string;
43
- /** The short name of the repository it came from. */
44
- repo: string;
45
- /** The recipe's namespace. */
46
- namespace: string;
47
- /** The recipe's name. */
48
- name: string;
49
- /** The exact version the lockfile pins. */
50
- version: string;
51
- /** The content hash the lockfile pins. */
52
- hash: string;
53
- /** Whether anything holds it as a co-subscription, or only as a build dependency. */
54
- kind: LockKind;
55
- /** Everyone holding it: "project", and the key of every recipe that asked. */
56
- requestedBy: string[];
57
- /** Absolute directory holding the recipe's files. */
58
- dir: string;
59
- /** True when `dir` is a linked working copy rather than a store entry. */
60
- linked: boolean;
61
- /** True when `dir` exists on disk right now. */
62
- present: boolean;
63
- };
64
-
65
- /** How locked recipes are located. */
66
- export type LockedRecipeOptions = {
67
- /** The project's `.sous/` directory, which holds the lockfile and the links map. */
68
- sousDir: string;
69
- /** The environment to read; decides where the store and the machine-wide links map are. */
70
- env?: NodeJS.ProcessEnv;
71
- /** The store root to use instead of the one the environment implies. */
72
- storeRoot?: string;
73
- };
74
-
75
- /**
76
- * Reads and validates a project's lockfile, returning an empty one when the
77
- * project has locked nothing yet.
78
- *
79
- * @param sousDir - The project's `.sous/` directory.
80
- */
81
- export function readProjectLockfile(sousDir: string): Lockfile {
82
- const filePath = path.join(sousDir, LOCKFILE_FILENAME);
83
- if (!fs.existsSync(filePath)) return { formatVersion: 1, repos: {}, recipes: {} };
84
- return parseLockfile(loadJsonFile(filePath, "lockfile"), filePath);
85
- }
86
-
87
- /**
88
- * Reads and validates the recipe manifest in a directory, returning undefined
89
- * when the directory has none or does not exist. A recipe that is not on disk
90
- * yet is an ordinary state (a fresh clone before restore), never an error here.
91
- *
92
- * @param recipeDir - The directory holding the recipe's files.
93
- */
94
- export function readRecipeManifestIn(recipeDir: string): RecipeManifest | undefined {
95
- let manifestPath: string | undefined;
96
- try {
97
- manifestPath = findRecipeManifest(recipeDir);
98
- } catch {
99
- return undefined;
100
- }
101
- if (manifestPath === undefined) return undefined;
102
- return parseRecipeManifest(loadManifestFile(manifestPath), manifestPath);
103
- }
104
-
105
- /**
106
- * Reads and validates the repo manifest at the root of a checkout, returning
107
- * undefined when there is none.
108
- *
109
- * @param repoRoot - The checkout's root directory.
110
- */
111
- export function readRepoManifestIn(repoRoot: string): RepoManifest | undefined {
112
- let manifestPath: string | undefined;
113
- try {
114
- manifestPath = findRepoManifest(repoRoot);
115
- } catch {
116
- return undefined;
117
- }
118
- if (manifestPath === undefined) return undefined;
119
- return parseRepoManifest(loadManifestFile(manifestPath), manifestPath);
120
- }
121
-
122
- /**
123
- * Maps every recipe a linked checkout publishes to its directory, by reading the
124
- * checkout's repo manifest and then each recipe folder's own manifest. A folder
125
- * the repo manifest lists but that holds no readable recipe manifest is skipped
126
- * rather than failing the build: a working copy is edited by hand and is allowed
127
- * to be mid-change.
128
- *
129
- * @param checkoutDir - The linked working copy's root directory.
130
- */
131
- export function mapLinkedRecipes(checkoutDir: string): Record<string, string> {
132
- const manifest = readRepoManifestIn(checkoutDir);
133
- if (manifest === undefined) return {};
134
-
135
- const found: Record<string, string> = {};
136
- for (const relative of manifest.recipes) {
137
- const recipeDir = path.join(checkoutDir, relative);
138
- const recipe = readRecipeManifestIn(recipeDir);
139
- if (recipe === undefined) continue;
140
- found[`${recipe.namespace}/${recipe.name}`] = recipeDir;
141
- }
142
- return found;
143
- }
144
-
145
- /**
146
- * Every recipe the lockfile pins, with the directory each one's files are read
147
- * from. Linked repositories win over the store, and a recipe whose directory is
148
- * not there yet comes back with `present: false`.
149
- *
150
- * @param options - The project's `.sous/` directory and the environment.
151
- */
152
- export function listLockedRecipes(
153
- options: LockedRecipeOptions
154
- ): LockedRecipeLocation[] {
155
- const env = options.env ?? process.env;
156
- const lock = readProjectLockfile(options.sousDir);
157
- const keys = Object.keys(lock.recipes);
158
- if (keys.length === 0) return [];
159
-
160
- const storeRoot = options.storeRoot ?? resolveStoreRoot(env);
161
- const links = readEffectiveLinks(options.sousDir, env);
162
- const linkedRecipes = new Map<string, Record<string, string>>();
163
-
164
- const located: LockedRecipeLocation[] = [];
165
-
166
- for (const key of keys.sort()) {
167
- const entry = lock.recipes[key]!;
168
- const { namespace, name } = splitRecipeKey(key);
169
-
170
- let dir: string | undefined;
171
- let linked = false;
172
-
173
- const checkout = links[entry.repo]?.path;
174
- if (checkout !== undefined) {
175
- if (!linkedRecipes.has(entry.repo)) {
176
- linkedRecipes.set(entry.repo, mapLinkedRecipes(checkout));
177
- }
178
- dir = linkedRecipes.get(entry.repo)![key];
179
- linked = dir !== undefined;
180
- }
181
-
182
- if (dir === undefined) {
183
- // The store is machine-wide, so it files an entry under the repository's
184
- // canonical identity rather than under this project's short name for it.
185
- const identity = lock.repos[entry.repo]?.identity;
186
- dir =
187
- identity === undefined
188
- ? undefined
189
- : path.join(storeRoot, ...identitySegments(identity), namespace, name, entry.version);
190
- }
191
-
192
- located.push({
193
- key,
194
- repo: entry.repo,
195
- namespace,
196
- name,
197
- version: entry.version,
198
- hash: entry.hash,
199
- kind: entry.kind,
200
- requestedBy: [...entry.requestedBy],
201
- dir: dir ?? "",
202
- linked,
203
- present: dir !== undefined && fs.existsSync(dir),
204
- });
205
- }
206
-
207
- return located;
208
- }
209
-
210
- /**
211
- * The lockfile keys one subscription holds DIRECTLY. A recipe ref holds its own
212
- * key; a namespace ref holds every recipe in that namespace.
213
- *
214
- * Only entries the project itself holds count. A recipe that arrived purely as
215
- * another recipe's `depends` sits under the same namespace but was never the
216
- * project's to hold, and counting it made `sous unsubscribe <namespace>` report
217
- * it as having "stayed" when the project had never held it in the first place.
218
- *
219
- * @param lock - The lockfile as it stands.
220
- * @param key - The subscription's ref key: a namespace, or `namespace/recipe`.
221
- */
222
- export function keysHeldBySubscription(lock: Lockfile, key: string): string[] {
223
- const heldByProject = (entry: string): boolean =>
224
- lock.recipes[entry]?.requestedBy.includes(PROJECT_HOLDER) === true;
225
-
226
- if (key.includes("/")) {
227
- return heldByProject(key) ? [key] : [];
228
- }
229
- return Object.keys(lock.recipes)
230
- .filter((entry) => entry.startsWith(`${key}/`) && heldByProject(entry))
231
- .sort();
232
- }
233
-
234
- /**
235
- * Every locked recipe reachable from a set of keys through the lockfile's
236
- * holder lists: the keys themselves, whatever they hold, and so on.
237
- *
238
- * @param lock - The lockfile.
239
- * @param roots - The keys to start from.
240
- */
241
- export function lockedClosure(lock: Lockfile, roots: string[]): string[] {
242
- const reached = new Set(roots.filter((key) => Object.hasOwn(lock.recipes, key)));
243
- const queue = [...reached];
244
- while (queue.length > 0) {
245
- const holder = queue.shift()!;
246
- for (const [key, entry] of Object.entries(lock.recipes)) {
247
- if (reached.has(key) || !entry.requestedBy.includes(holder)) continue;
248
- reached.add(key);
249
- queue.push(key);
250
- }
251
- }
252
- return [...reached].sort();
253
- }
254
-