@sous-io/sous 0.2.36 → 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 -237
  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 -391
  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 -412
  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 -1105
  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,84 +0,0 @@
1
- /**
2
- * The store entry marker: `.sous.entry.json`, written beside every cached
3
- * recipe version in the machine-wide store
4
- * (`$SOUS_HOME/cache/<repository identity>/<namespace>/<recipe>/<version>/`).
5
- *
6
- * The marker is MACHINE-WRITTEN and makes an entry self-describing, so the
7
- * store can be swept without consulting any project: `hash` is checked against
8
- * the lockfile before the entry is used, `sizeBytes` and `lastAccessAt` drive
9
- * the size-capped least-recently-used collection that `sous repo gc` performs.
10
- *
11
- * The store is disposable by design; everything in it is re-fetchable from the
12
- * pins in a project's lockfile.
13
- */
14
-
15
- import { z } from "zod";
16
- import {
17
- byteCountSchema,
18
- contentHashSchema,
19
- formatVersionSchema,
20
- isoTimestampSchema,
21
- namespaceNameSchema,
22
- parseFormat,
23
- recipeNameSchema,
24
- repoIdentitySchema,
25
- semverVersionSchema,
26
- stableJsonStringify,
27
- } from "./common.js";
28
-
29
- /** The store entry marker schema. */
30
- export const storeEntrySchema = z.strictObject({
31
- formatVersion: formatVersionSchema,
32
- /**
33
- * The canonical identity of the repository the recipe came from, such as
34
- * `github.com/sous-io/sous-recipes`. The store is machine-wide, so it records
35
- * where a recipe really came from rather than what one project calls it.
36
- */
37
- repo: repoIdentitySchema,
38
- /** The recipe's namespace. */
39
- namespace: namespaceNameSchema,
40
- /** The recipe's name. */
41
- name: recipeNameSchema,
42
- /** The exact version cached here. */
43
- version: semverVersionSchema,
44
- /** Content hash of the cached files, verified before the entry is used. */
45
- hash: contentHashSchema,
46
- /** When the entry was fetched. */
47
- fetchedAt: isoTimestampSchema,
48
- /** When the entry was last read by a build. Drives least-recently-used collection. */
49
- lastAccessAt: isoTimestampSchema,
50
- /** Total size of the entry's files, so the store can be capped without a rescan. */
51
- sizeBytes: byteCountSchema,
52
- });
53
-
54
- /** A validated store entry marker. */
55
- export type StoreEntry = z.infer<typeof storeEntrySchema>;
56
-
57
- /**
58
- * Validates a parsed store entry marker, throwing a ConfigError that names the
59
- * file and the path of every bad field.
60
- *
61
- * @param value - The parsed contents of the marker file.
62
- * @param sourceLabel - The marker's path, named in error messages.
63
- */
64
- export function parseStoreEntry(value: unknown, sourceLabel: string): StoreEntry {
65
- return parseFormat(storeEntrySchema, value, sourceLabel, "store entry marker");
66
- }
67
-
68
- /**
69
- * Serializes a store entry marker for writing, with keys sorted.
70
- *
71
- * @param entry - The marker to write.
72
- */
73
- export function stringifyStoreEntry(entry: StoreEntry): string {
74
- return stableJsonStringify(entry);
75
- }
76
-
77
- /**
78
- * The recipe key (`namespace/recipe`) a store entry holds.
79
- *
80
- * @param entry - A validated store entry marker.
81
- */
82
- export function storeEntryKey(entry: StoreEntry): string {
83
- return `${entry.namespace}/${entry.name}`;
84
- }
@@ -1,268 +0,0 @@
1
- /**
2
- * A recipe's variable definitions: what each variable is called, what shape an
3
- * answer takes, and what question to ask. A recipe manifest declares them
4
- * (`recipe-manifest.ts` re-exports everything here), and `sous repo release`
5
- * copies each version's definitions into the index, so the same fields and the
6
- * same rules read both copies.
7
- *
8
- * This module imports nothing but the shared primitives, because the index
9
- * reads it, and the index sits below the ref parser the manifest needs.
10
- */
11
-
12
- import { z } from "zod";
13
- import {
14
- envVarNameSchema,
15
- extensibleObject,
16
- forwardCompatibleObject,
17
- variableNameSchema,
18
- } from "./common.js";
19
-
20
- // --- Variable definitions -----------------------------------------------------------------------
21
-
22
- /** The value types a recipe may declare for a variable. */
23
- export const VARIABLE_TYPES = [
24
- "string",
25
- "number",
26
- "boolean",
27
- "enum",
28
- "path",
29
- "url",
30
- ] as const;
31
-
32
- /** Which env file an answer is written to. */
33
- export const VARIABLE_SCOPES = ["shared", "local"] as const;
34
-
35
- /** Constraints checked against an answer before it is accepted or stored. */
36
- const variableValidationShape = {
37
- /** A regular expression the answer must match, as a string. */
38
- pattern: z
39
- .string()
40
- .refine(
41
- (value) => {
42
- try {
43
- new RegExp(value);
44
- return true;
45
- } catch {
46
- return false;
47
- }
48
- },
49
- { message: "must be a valid regular expression" }
50
- )
51
- .optional(),
52
- /** Minimum length of a string answer. */
53
- minLength: z.number().int().min(0).optional(),
54
- /** Maximum length of a string answer. */
55
- maxLength: z.number().int().min(0).optional(),
56
- /** Minimum value of a numeric answer. */
57
- min: z.number().optional(),
58
- /** Maximum value of a numeric answer. */
59
- max: z.number().optional(),
60
- /** The allowed answers. Required when the variable's type is 'enum'. */
61
- enum: z.array(z.string()).min(1, "must list at least one option").optional(),
62
- };
63
-
64
- /** Constraints checked against an answer before it is accepted or stored. */
65
- export const variableValidationSchema = extensibleObject(variableValidationShape);
66
-
67
- /**
68
- * The fields of a variable definition, shared by the two schemas that read
69
- * one: the hand-written manifest, and the copy a release records in the index.
70
- *
71
- * @param validate - The schema for the `validate` block, strict or forward compatible.
72
- */
73
- function variableDefinitionShape<Validate extends z.ZodType>(validate: Validate) {
74
- return {
75
- /** The variable's camelCase name, as templates refer to it. */
76
- name: variableNameSchema,
77
- /**
78
- * The environment variable an answer binds to. Authors may name an existing
79
- * variable such as GITHUB_TOKEN to reuse a value already in the environment.
80
- * When omitted, `sous repo release` derives an upper snake case default from
81
- * the name; the runtime never derives one.
82
- */
83
- env: envVarNameSchema.optional(),
84
- /** The answer's type, which decides how it is validated and prompted for. */
85
- type: z.enum(VARIABLE_TYPES),
86
- /** The one-line question shown when the variable is asked. */
87
- prompt: z.string().min(1, "must not be empty"),
88
- /**
89
- * The paragraph that explains the variable: what it is for, what a good
90
- * answer looks like, and what changes when it is set. Shown above the
91
- * question when sous asks, and by `sous vars <name>`. Required, because a
92
- * consumer reading the question has no other way to learn what a publisher
93
- * meant by it.
94
- */
95
- description: z
96
- .string({
97
- error:
98
- "is required: every published variable must explain itself in a sentence or " +
99
- "two. The description is shown above the question when sous asks, and by " +
100
- "'sous vars <name>'",
101
- })
102
- .min(1, "must not be empty"),
103
- /**
104
- * A realistic sample answer. It is documentation and prompt copy only; sous
105
- * never stores it, never offers it as the answer, and never falls back to it.
106
- * Use `default` for a value a project should actually start with.
107
- */
108
- example: z.union([z.string(), z.number(), z.boolean()], {
109
- error:
110
- "is required: every published variable must show what a real answer looks like. " +
111
- "The example is shown with the question and by 'sous vars <name>'; it is " +
112
- "documentation only and is never stored as the answer (use 'default' for that)",
113
- }),
114
- /** The value offered when the question is asked with nothing else in scope. */
115
- default: z.union([z.string(), z.number(), z.boolean()]).optional(),
116
- /** Whether an answer is needed for a build to run. Defaults to true. */
117
- required: z.boolean().default(true),
118
- /**
119
- * Whether the answer is a secret. A secret is always written to the
120
- * gitignored `.sous/.env.local`, never to the committed `.sous/.env`.
121
- */
122
- secret: z.boolean().default(false),
123
- /**
124
- * Which env file the answer is written to: 'shared' is `.sous/.env`, which is
125
- * committed and shared with the team; 'local' is `.sous/.env.local`, which is
126
- * gitignored and machine-specific. Defaults to 'shared'.
127
- */
128
- scope: z.enum(VARIABLE_SCOPES).default("shared"),
129
- /** Constraints checked against an answer. */
130
- validate: validate.optional(),
131
- };
132
- }
133
-
134
- /** What `checkVariableDefinition` reads from a definition. */
135
- type CheckedDefinition = {
136
- type: VariableType;
137
- secret: boolean;
138
- scope: VariableScope;
139
- default?: string | number | boolean | undefined;
140
- example: string | number | boolean;
141
- validate?:
142
- | { minLength?: number; maxLength?: number; min?: number; max?: number; enum?: string[] }
143
- | undefined;
144
- };
145
-
146
- /**
147
- * The rules a variable definition obeys beyond the shape of each field, shared
148
- * by both schemas so a definition the index records is held to exactly the rules
149
- * its manifest was.
150
- *
151
- * @param definition - The definition, already parsed field by field.
152
- * @param ctx - The refinement context issues are added to.
153
- */
154
- function checkVariableDefinition(definition: CheckedDefinition, ctx: z.RefinementCtx): void {
155
- const { type, validate } = definition;
156
-
157
- if (type === "enum" && (validate?.enum === undefined || validate.enum.length === 0)) {
158
- ctx.addIssue({
159
- code: "custom",
160
- path: ["validate", "enum"],
161
- message:
162
- "a variable of type 'enum' must list its options under 'validate.enum'",
163
- });
164
- }
165
-
166
- if (validate !== undefined) {
167
- const { minLength, maxLength, min, max } = validate;
168
- if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
169
- ctx.addIssue({
170
- code: "custom",
171
- path: ["validate", "maxLength"],
172
- message: "must not be smaller than 'validate.minLength'",
173
- });
174
- }
175
- if (min !== undefined && max !== undefined && min > max) {
176
- ctx.addIssue({
177
- code: "custom",
178
- path: ["validate", "max"],
179
- message: "must not be smaller than 'validate.min'",
180
- });
181
- }
182
- }
183
-
184
- // A secret written to the committed env file would leak, so the two fields
185
- // are not allowed to contradict each other.
186
- if (definition.secret && definition.scope === "shared") {
187
- ctx.addIssue({
188
- code: "custom",
189
- path: ["scope"],
190
- message:
191
- "a secret variable is stored in the gitignored '.sous/.env.local', so its " +
192
- "scope must be 'local' (or left unset)",
193
- });
194
- }
195
-
196
- // `default` and `example` are both literal values written by the publisher,
197
- // so both have to fit the variable they describe; an example that could
198
- // never be a valid answer is worse than no example at all.
199
- checkDeclaredValue(definition.default, "default");
200
- checkDeclaredValue(definition.example, "example");
201
-
202
- /**
203
- * Checks one publisher-written literal against the declared type and the
204
- * 'validate.enum' options.
205
- *
206
- * @param value - The literal to check, or undefined when it was omitted.
207
- * @param field - The field name, used as the issue path.
208
- */
209
- function checkDeclaredValue(
210
- value: string | number | boolean | undefined,
211
- field: "default" | "example"
212
- ): void {
213
- if (value === undefined) return;
214
-
215
- const expected =
216
- type === "boolean" ? "boolean" : type === "number" ? "number" : "string";
217
- if (typeof value !== expected) {
218
- ctx.addIssue({
219
- code: "custom",
220
- path: [field],
221
- message: `must be a ${expected}, to match the declared type '${type}'`,
222
- });
223
- } else if (
224
- type === "enum" &&
225
- validate?.enum !== undefined &&
226
- !validate.enum.includes(String(value))
227
- ) {
228
- ctx.addIssue({
229
- code: "custom",
230
- path: [field],
231
- message: "must be one of the options listed under 'validate.enum'",
232
- });
233
- }
234
- }
235
- }
236
-
237
- /**
238
- * A published variable definition: a specification, not a value. Definitions
239
- * are inert; a question is asked only when a subscribed recipe needs the
240
- * variable and no valid answer is already in scope.
241
- */
242
- export const variableDefinitionSchema = extensibleObject(
243
- variableDefinitionShape(variableValidationSchema)
244
- ).superRefine(checkVariableDefinition);
245
-
246
- /**
247
- * A variable definition as a release records it in the index, so a consumer
248
- * can list a recipe's questions before its files are fetched. It is the
249
- * manifest's definition, field for field and rule for rule; the one difference
250
- * is that a field this sous does not define is kept rather than refused,
251
- * because the index is read by every sous a project happens to pin, and a
252
- * later one may add a field.
253
- */
254
- export const publishedVariableDefinitionSchema = forwardCompatibleObject(
255
- variableDefinitionShape(forwardCompatibleObject(variableValidationShape))
256
- ).superRefine(checkVariableDefinition);
257
-
258
- /** One validated variable definition from a recipe manifest. */
259
- export type VariableDefinition = z.infer<typeof variableDefinitionSchema>;
260
-
261
- /** The constraints attached to a variable definition. */
262
- export type VariableValidation = z.infer<typeof variableValidationSchema>;
263
-
264
- /** The value type a variable definition declares. */
265
- export type VariableType = (typeof VARIABLE_TYPES)[number];
266
-
267
- /** Which env file an answer is written to. */
268
- export type VariableScope = (typeof VARIABLE_SCOPES)[number];
@@ -1,264 +0,0 @@
1
- /**
2
- * Freshness: when sous bothers to look upstream.
3
- *
4
- * A build does not want to talk to the network on every run, and a lockfile
5
- * means it usually has no reason to. So the rule is deliberately dull: a
6
- * repository is checked when it has never been checked, when the freshness
7
- * window has lapsed since the last check, or when something asks to always
8
- * pull. Watch mode passes a shorter window; a one-off command can force it.
9
- *
10
- * The last-check time is remembered in the sidecar the index cache already
11
- * writes, so there is one record per repository rather than two.
12
- *
13
- * A failed check NEVER breaks a build. That belongs to the index cache, which
14
- * falls back to the copy it already holds; this module only decides whether to
15
- * look.
16
- */
17
-
18
- import semver from "semver";
19
- import type { IndexFile } from "./formats/index-file.js";
20
- import type { IndexMeta } from "./providers/index-cache.js";
21
-
22
- /** How long a check stays good when nothing says otherwise: five minutes. */
23
- // The freshness default is shared with the store settings so there is one number.
24
- import { DEFAULT_FRESHNESS_SECONDS } from "./store/settings.js";
25
-
26
- /** The part of the index cache this module uses; IndexCache satisfies it. */
27
- export type UpstreamCheckRecord = {
28
- readMeta(repoName: string): IndexMeta | undefined;
29
- writeMeta(repoName: string, meta: IndexMeta): void;
30
- };
31
-
32
- /** What decides whether to look upstream. */
33
- export type FreshnessInput = {
34
- /** When sous last asked upstream, from the repository's sidecar. */
35
- lastCheckedAt?: string;
36
- /** How long a check stays good, in seconds. Defaults to five minutes. */
37
- freshnessSeconds?: number;
38
- /**
39
- * Whether something in play prefers a newer in-range version over the locked
40
- * one. Always-pull still respects the freshness window; it changes what
41
- * happens after the check, not how often the check happens.
42
- */
43
- alwaysPull?: boolean;
44
- /** Check regardless, which is what an explicit update command does. */
45
- force?: boolean;
46
- /** The clock. Defaults to now. */
47
- now?: Date;
48
- };
49
-
50
- /**
51
- * True when sous should ask upstream whether there is anything newer.
52
- *
53
- * shouldCheckUpstream({ lastCheckedAt: "2026-01-01T00:00:00.000Z", freshnessSeconds: 300 })
54
- * // -> false a minute later, true six minutes later
55
- *
56
- * @param input - The last check, the window, and the flags.
57
- */
58
- export function shouldCheckUpstream(input: FreshnessInput): boolean {
59
- if (input.force === true) return true;
60
- if (input.lastCheckedAt === undefined) return true;
61
-
62
- const last = Date.parse(input.lastCheckedAt);
63
- if (Number.isNaN(last)) return true;
64
-
65
- const now = (input.now ?? new Date()).getTime();
66
- const windowSeconds = input.freshnessSeconds ?? DEFAULT_FRESHNESS_SECONDS;
67
- if (windowSeconds <= 0) return true;
68
-
69
- const ageSeconds = (now - last) / 1000;
70
- // A last-check time in the future means a clock changed under sous; check
71
- // rather than trusting an answer that cannot be right.
72
- if (ageSeconds < 0) return true;
73
- return ageSeconds >= windowSeconds;
74
- }
75
-
76
- /**
77
- * Records that sous just looked upstream for a repository, whether or not the
78
- * look succeeded. Recording a failed check too is what stops an unreachable
79
- * host from being retried on every single build.
80
- *
81
- * @param record - The index cache, or anything with its sidecar methods.
82
- * @param identity - The repository's canonical identity, which the cache is keyed by.
83
- * @param when - The moment to record. Defaults to now.
84
- */
85
- export function recordUpstreamCheck(
86
- record: UpstreamCheckRecord,
87
- identity: string,
88
- when: Date = new Date()
89
- ): IndexMeta {
90
- const existing = record.readMeta(identity);
91
- const meta: IndexMeta = {
92
- fetchedAt: existing?.fetchedAt ?? when.toISOString(),
93
- ...(existing?.etag === undefined ? {} : { etag: existing.etag }),
94
- ...(existing?.ref === undefined ? {} : { ref: existing.ref }),
95
- lastCheckedAt: when.toISOString(),
96
- };
97
- record.writeMeta(identity, meta);
98
- return meta;
99
- }
100
-
101
- /** What an always-pull check found. */
102
- export type NewerVersion = {
103
- /** The recipe key. */
104
- key: string;
105
- /** The version currently locked. */
106
- from: string;
107
- /** The newer version that also satisfies the range. */
108
- to: string;
109
- };
110
-
111
- /**
112
- * Finds a newer in-range version of a locked recipe, or undefined when the
113
- * locked one is still the best the range allows. This is the whole of what
114
- * always-pull does: it re-resolves WITHIN the declared range and never widens
115
- * it, so a dependency's constraint still holds.
116
- *
117
- * @param options - The index, the recipe, its locked version and its range.
118
- */
119
- export function findNewerInRange(options: {
120
- /** The repository's index. */
121
- index: IndexFile;
122
- /** The recipe key, `namespace/recipe`. */
123
- key: string;
124
- /** The version the lockfile pins. */
125
- lockedVersion: string;
126
- /** The range the subscription or dependency declared. Defaults to any version. */
127
- range?: string;
128
- /** Whether prereleases take part. */
129
- prerelease?: boolean;
130
- }): NewerVersion | undefined {
131
- const entry = options.index.recipes[options.key];
132
- if (entry === undefined) return undefined;
133
-
134
- const prerelease = options.prerelease ?? false;
135
- const candidates = Object.keys(entry.versions).filter((version) => {
136
- if (!prerelease && entry.versions[version]!.prerelease === true) return false;
137
- return semver.satisfies(version, options.range ?? "*", { includePrerelease: prerelease });
138
- });
139
-
140
- const best = semver.maxSatisfying(candidates, "*", { includePrerelease: prerelease });
141
- if (best === null) return undefined;
142
- if (semver.lte(best, options.lockedVersion)) return undefined;
143
-
144
- return { key: options.key, from: options.lockedVersion, to: best };
145
- }
146
-
147
- /** The holder meaning the project subscribed to a recipe itself. */
148
- const PROJECT = "project";
149
-
150
- /** Every range a locked recipe's holders declared, or nothing when one cannot be told. */
151
- export type DeclaredRangeLookup = {
152
- /**
153
- * The range the project's subscription declares for a recipe key, `"*"` when
154
- * the subscription declares none, or undefined when the project holds nothing
155
- * matching it any more.
156
- */
157
- subscriptionRange(key: string): string | undefined;
158
- /**
159
- * The range one holding recipe's manifest declares for a dependency, `"*"`
160
- * when it declares none, or undefined when its manifest cannot be read or no
161
- * longer names that dependency.
162
- */
163
- dependencyRange(holder: string, key: string): string | undefined;
164
- };
165
-
166
- /**
167
- * The version range an always-pull check may move ONE locked recipe within, or
168
- * undefined when sous cannot tell and therefore must not move it at all.
169
- *
170
- * Always-pull re-resolves within what was declared; it never widens it. What was
171
- * declared depends on who holds the recipe. The project's own hold means the
172
- * subscription's range, which may genuinely be any version. A hold by another
173
- * recipe means the range that recipe's manifest declared in `depends`, and no
174
- * subscription anywhere carries it, so falling back to `*` for such an entry
175
- * would move it straight past the constraint the dependency declared. Several
176
- * holders mean every one of their ranges at once: whitespace between comparator
177
- * sets is AND in semver, which is exactly that.
178
- *
179
- * Returning undefined is the safe answer, and the entry stays where the lockfile
180
- * pins it. It happens when a holder's declaration cannot be read and when the
181
- * combined range is not one semver can express.
182
- *
183
- * @param key - The locked recipe key, `namespace/recipe`.
184
- * @param requestedBy - Its lockfile holders: "project", and any recipe keys.
185
- * @param lookup - How to read what each holder declared.
186
- */
187
- export function effectiveRangeForHolders(
188
- key: string,
189
- requestedBy: readonly string[],
190
- lookup: DeclaredRangeLookup
191
- ): string | undefined {
192
- const ranges: string[] = [];
193
-
194
- for (const holder of requestedBy) {
195
- const declared =
196
- holder === PROJECT ? lookup.subscriptionRange(key) : lookup.dependencyRange(holder, key);
197
- if (declared === undefined) return undefined;
198
- ranges.push(declared);
199
- }
200
-
201
- if (ranges.length === 0) return undefined;
202
-
203
- const constraints = [...new Set(ranges.filter((range) => range !== "*"))];
204
- if (constraints.length === 0) return "*";
205
-
206
- const combined = constraints.join(" ");
207
- return semver.validRange(combined) === null ? undefined : combined;
208
- }
209
-
210
- /**
211
- * How long a build waits for a repository that it is only checking so it can
212
- * say whether a newer version exists: three seconds. Past that the check is
213
- * abandoned, quietly, and the build goes on with what the cache already knew.
214
- */
215
- export const NEWER_VERSION_CHECK_TIMEOUT_MS = 3000;
216
-
217
- /**
218
- * Runs a piece of upstream work with a deadline. The work is handed an abort
219
- * signal that fires at the deadline, so a request that honors it is cancelled
220
- * rather than left to keep the process alive; either way the returned promise
221
- * rejects at the deadline, and the timer never holds the process open itself.
222
- *
223
- * await withDeadline((signal) => fetchSomething(signal), 3000);
224
- * // -> the result, or an error saying the repository did not answer in time
225
- *
226
- * @param work - The work to run, given the signal that cancels it.
227
- * @param milliseconds - How long to wait.
228
- */
229
- export async function withDeadline<T>(
230
- work: (signal: AbortSignal) => Promise<T>,
231
- milliseconds: number
232
- ): Promise<T> {
233
- const controller = new AbortController();
234
- let timer: ReturnType<typeof setTimeout> | undefined;
235
-
236
- const deadline = new Promise<never>((_, reject) => {
237
- timer = setTimeout(() => {
238
- controller.abort();
239
- reject(
240
- new Error(
241
- `The repository did not answer within ${formatSeconds(milliseconds)}, so sous ` +
242
- `stopped waiting for it.`
243
- )
244
- );
245
- }, milliseconds);
246
- timer.unref?.();
247
- });
248
-
249
- try {
250
- return await Promise.race([work(controller.signal), deadline]);
251
- } finally {
252
- if (timer !== undefined) clearTimeout(timer);
253
- }
254
- }
255
-
256
- /**
257
- * A duration in plain words, such as "3 seconds" or "1 second".
258
- *
259
- * @param milliseconds - The duration.
260
- */
261
- function formatSeconds(milliseconds: number): string {
262
- const seconds = Math.round((milliseconds / 1000) * 10) / 10;
263
- return seconds === 1 ? "1 second" : `${seconds} seconds`;
264
- }