@sous-io/sous 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +115 -35
  2. package/bin/run.js +10 -1
  3. package/docs/markdown/README.md +27 -0
  4. package/docs/markdown/_sidebar.md +18 -0
  5. package/docs/markdown/commands.md +308 -0
  6. package/docs/markdown/config-discovery.md +74 -0
  7. package/docs/markdown/config-inspection.md +69 -0
  8. package/docs/markdown/config-layers.md +92 -0
  9. package/docs/markdown/config-variables.md +79 -0
  10. package/docs/markdown/configuration.md +71 -0
  11. package/docs/markdown/design-principles.md +59 -0
  12. package/docs/markdown/repositories-authoring.md +408 -0
  13. package/docs/markdown/repositories-consuming.md +580 -0
  14. package/docs/markdown/repositories-file-formats.md +1084 -0
  15. package/docs/markdown/repositories-variables.md +387 -0
  16. package/docs/markdown/repositories.md +303 -0
  17. package/docs/markdown/skill-categories.md +58 -0
  18. package/package.json +72 -8
  19. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
  20. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
  21. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
  22. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
  23. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
  24. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
  25. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
  26. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
  27. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
  28. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
  29. package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
  30. package/sous.config.schema.json +337 -0
  31. package/src/base-command.ts +220 -67
  32. package/src/commands/build.ts +150 -73
  33. package/src/commands/clear.ts +23 -15
  34. package/src/commands/compile.ts +74 -16
  35. package/src/commands/config/get.ts +110 -0
  36. package/src/commands/config/show.ts +32 -0
  37. package/src/commands/config/validate.ts +53 -0
  38. package/src/commands/help.ts +46 -0
  39. package/src/commands/launch.ts +36 -14
  40. package/src/commands/lock/rebuild.ts +241 -0
  41. package/src/commands/lock/show.ts +115 -0
  42. package/src/commands/namespace/list.ts +117 -0
  43. package/src/commands/namespace/show.ts +110 -0
  44. package/src/commands/prune.ts +3 -11
  45. package/src/commands/recipe/list.ts +95 -0
  46. package/src/commands/recipe/show.ts +301 -0
  47. package/src/commands/repo/add.ts +145 -0
  48. package/src/commands/repo/gc.ts +172 -0
  49. package/src/commands/repo/init.ts +136 -0
  50. package/src/commands/repo/link.ts +500 -0
  51. package/src/commands/repo/list.ts +179 -0
  52. package/src/commands/repo/release.ts +619 -0
  53. package/src/commands/repo/remove.ts +193 -0
  54. package/src/commands/repo/search.ts +189 -0
  55. package/src/commands/repo/submit.ts +133 -0
  56. package/src/commands/repo/unlink.ts +147 -0
  57. package/src/commands/subscription/add.ts +285 -0
  58. package/src/commands/subscription/list.ts +129 -0
  59. package/src/commands/subscription/remove.ts +181 -0
  60. package/src/commands/vars/ask.ts +374 -0
  61. package/src/commands/vars/index.ts +79 -0
  62. package/src/commands/vars/list.ts +67 -0
  63. package/src/commands/vars/show.ts +77 -0
  64. package/src/config-command.ts +30 -0
  65. package/src/lib/build-service.ts +206 -54
  66. package/src/lib/config-discovery.ts +220 -27
  67. package/src/lib/config-inspect.ts +145 -0
  68. package/src/lib/config-kernel.mjs +377 -0
  69. package/src/lib/config-schema.ts +361 -0
  70. package/src/lib/env-file.ts +328 -0
  71. package/src/lib/env-local.ts +18 -1
  72. package/src/lib/errors.ts +32 -0
  73. package/src/lib/include-resolver.ts +108 -15
  74. package/src/lib/interactive.ts +165 -0
  75. package/src/lib/markdown-compiler.ts +118 -37
  76. package/src/lib/package-info.ts +25 -0
  77. package/src/lib/pid-service.ts +32 -21
  78. package/src/lib/refs/find.ts +589 -0
  79. package/src/lib/refs/index.ts +12 -0
  80. package/src/lib/refs/pick.ts +147 -0
  81. package/src/lib/refs/scopes.ts +61 -0
  82. package/src/lib/repos/catalog-display.ts +116 -0
  83. package/src/lib/repos/catalog-inputs.ts +160 -0
  84. package/src/lib/repos/catalog.ts +722 -0
  85. package/src/lib/repos/core-recipe.ts +105 -0
  86. package/src/lib/repos/defaults.ts +175 -0
  87. package/src/lib/repos/formats/common.ts +389 -0
  88. package/src/lib/repos/formats/index-file.ts +215 -0
  89. package/src/lib/repos/formats/links-map.ts +96 -0
  90. package/src/lib/repos/formats/lockfile.ts +167 -0
  91. package/src/lib/repos/formats/patterns.ts +57 -0
  92. package/src/lib/repos/formats/recipe-manifest.ts +395 -0
  93. package/src/lib/repos/formats/repo-manifest.ts +88 -0
  94. package/src/lib/repos/formats/store-entry.ts +84 -0
  95. package/src/lib/repos/freshness.ts +208 -0
  96. package/src/lib/repos/git-clone.ts +312 -0
  97. package/src/lib/repos/identity.ts +89 -0
  98. package/src/lib/repos/index.ts +58 -0
  99. package/src/lib/repos/links.ts +353 -0
  100. package/src/lib/repos/load-manifest.ts +236 -0
  101. package/src/lib/repos/lock-service.ts +453 -0
  102. package/src/lib/repos/locked-namespace-resolver.ts +90 -0
  103. package/src/lib/repos/locked-recipes.ts +254 -0
  104. package/src/lib/repos/managed-layer.ts +422 -0
  105. package/src/lib/repos/namespace-resolver.ts +370 -0
  106. package/src/lib/repos/providers/base.ts +206 -0
  107. package/src/lib/repos/providers/git.ts +233 -0
  108. package/src/lib/repos/providers/github.ts +294 -0
  109. package/src/lib/repos/providers/gitlab.ts +263 -0
  110. package/src/lib/repos/providers/http.ts +102 -0
  111. package/src/lib/repos/providers/index-cache.ts +382 -0
  112. package/src/lib/repos/providers/index.ts +106 -0
  113. package/src/lib/repos/providers/local.ts +391 -0
  114. package/src/lib/repos/providers/provider.ts +401 -0
  115. package/src/lib/repos/recipe-config-layers.ts +287 -0
  116. package/src/lib/repos/recipe-targets.ts +223 -0
  117. package/src/lib/repos/ref-search.ts +46 -0
  118. package/src/lib/repos/ref.ts +513 -0
  119. package/src/lib/repos/reference-report.ts +122 -0
  120. package/src/lib/repos/release/bump.ts +161 -0
  121. package/src/lib/repos/release/git-state.ts +305 -0
  122. package/src/lib/repos/release/index-builder.ts +635 -0
  123. package/src/lib/repos/release/index.ts +16 -0
  124. package/src/lib/repos/release/plan.ts +512 -0
  125. package/src/lib/repos/release/submit-service.ts +496 -0
  126. package/src/lib/repos/release/tags.ts +243 -0
  127. package/src/lib/repos/release/validate.ts +463 -0
  128. package/src/lib/repos/resolver.ts +789 -0
  129. package/src/lib/repos/scaffold/index.ts +238 -0
  130. package/src/lib/repos/scaffold/templates.ts +413 -0
  131. package/src/lib/repos/seed.ts +414 -0
  132. package/src/lib/repos/store/contract.ts +64 -0
  133. package/src/lib/repos/store/hash.ts +114 -0
  134. package/src/lib/repos/store/recipe-store.ts +599 -0
  135. package/src/lib/repos/store/settings.ts +58 -0
  136. package/src/lib/repos/subscription-service.ts +2678 -0
  137. package/src/lib/repos/trust.ts +447 -0
  138. package/src/lib/settings.ts +546 -189
  139. package/src/lib/sous-home.ts +104 -0
  140. package/src/lib/state.ts +52 -20
  141. package/src/lib/vars/ask.ts +1152 -0
  142. package/src/lib/vars/definition-source.ts +252 -0
  143. package/src/lib/vars/display.ts +233 -0
  144. package/src/lib/vars/index.ts +18 -0
  145. package/src/lib/vars/ladder.ts +282 -0
  146. package/src/lib/vars/mappings.ts +265 -0
  147. package/src/lib/vars/names.ts +94 -0
  148. package/src/lib/vars/preanswers.ts +395 -0
  149. package/src/lib/vars/question-plan.ts +218 -0
  150. package/src/lib/vars/report.ts +228 -0
  151. package/src/lib/vars/safe-regex.ts +235 -0
  152. package/src/lib/vars/validate.ts +312 -0
  153. package/src/lib/watch-loop.ts +148 -0
  154. package/src/templating/init-liquid-engine.ts +58 -16
  155. package/src/utils/choice-prompt.ts +143 -0
  156. package/src/utils/command-errors.ts +186 -0
  157. package/src/utils/command-help.ts +45 -0
  158. package/src/utils/confirm-prompt.ts +110 -0
  159. package/src/utils/flags.ts +153 -0
  160. package/src/utils/formatting.ts +540 -55
  161. package/src/utils/prompts.ts +35 -1
  162. package/src/utils/sous-directory.ts +245 -0
  163. package/src/utils/table.ts +603 -0
  164. package/src/utils/value-prompt.ts +119 -0
  165. package/shared-prompts/_partials/resume-task.md +0 -51
  166. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  167. package/shared-prompts/_partials/update-task-file.md +0 -52
  168. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  169. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  189. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  190. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  191. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  192. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  193. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  194. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  195. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  196. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  197. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  198. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  199. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  200. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  201. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  202. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  203. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  204. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  205. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * The repo index: `sous.index.json` at a repo's root.
3
+ *
4
+ * The index is MACHINE-WRITTEN by `sous repo release` and committed alongside
5
+ * the recipes it describes. It is the portable contract across providers: every
6
+ * provider, whatever its API looks like, can hand back this one file, and it is
7
+ * all sous needs to resolve a ref, enumerate published versions and check
8
+ * whether a cached copy is current.
9
+ *
10
+ * Adding a repo fetches only this file. Nothing else is downloaded until a
11
+ * project subscribes to something inside it.
12
+ */
13
+
14
+ import { z } from "zod";
15
+ import {
16
+ contentHashSchema,
17
+ formatVersionSchema,
18
+ isoTimestampSchema,
19
+ namespaceNameSchema,
20
+ parseFormat,
21
+ recipeKeySchema,
22
+ relativePathSchema,
23
+ repoIdentitySchema,
24
+ repoNameSchema,
25
+ semverRangeSchema,
26
+ semverVersionSchema,
27
+ stableJsonStringify,
28
+ } from "./common.js";
29
+
30
+ /**
31
+ * One dependency of one published version, as the release resolved it.
32
+ *
33
+ * A SIBLING (a recipe in this same repository) always resolves to an exact
34
+ * version, because the release that wrote this entry cut that sibling's tag or
35
+ * found it already cut. A CROSS-REPOSITORY dependency carries the identity of
36
+ * the repository it lives in, which is what a consumer needs in order to add
37
+ * that repository and find the recipe in the store; the version it resolves to
38
+ * belongs to that repository's own index, so this entry carries the range the
39
+ * manifest declared instead.
40
+ */
41
+ export const indexDependencySchema = z
42
+ .strictObject({
43
+ /** The exact version this dependency resolved to, when the release could resolve one. */
44
+ version: semverVersionSchema.optional(),
45
+ /** The range the manifest declared, recorded when no exact version could be resolved. */
46
+ range: semverRangeSchema.optional(),
47
+ /**
48
+ * The canonical identity of the repository publishing it
49
+ * (`github.com/sous-io/sous-recipes`). Omitted for a sibling, which lives in
50
+ * this same repository.
51
+ */
52
+ repo: repoIdentitySchema.optional(),
53
+ })
54
+ .refine(
55
+ (entry) => entry.version !== undefined || entry.range !== undefined,
56
+ {
57
+ message:
58
+ "must record either the exact version this dependency resolved to or the range " +
59
+ "the recipe declared",
60
+ }
61
+ );
62
+
63
+ /** One published version of one recipe. */
64
+ export const indexVersionSchema = z.strictObject({
65
+ /** Content hash of the recipe folder at this version, verified after every fetch. */
66
+ hash: contentHashSchema,
67
+ /**
68
+ * The git tag carrying this version, shaped `namespace/recipe@version`. The
69
+ * index's `superRefine` checks it against the key and version it sits under,
70
+ * so a version can never point at a branch or at another recipe's tag.
71
+ */
72
+ tag: z.string().min(1, "must not be empty"),
73
+ /** True when the version is a prerelease, which ranges skip unless opted in. */
74
+ prerelease: z.boolean(),
75
+ /**
76
+ * True when this version is not one the repository published, but the copy of
77
+ * the recipe that ships inside the installed sous package, folded into the
78
+ * index in memory so it can be resolved like anything else.
79
+ *
80
+ * Sous never writes this onto a copy of an index it fetched; a cached index
81
+ * stays exactly what upstream served. The field exists so that a listing can
82
+ * say the version came packaged with sous rather than from the repository, and
83
+ * so a reader of the stand-in index sous writes for itself can tell.
84
+ */
85
+ seeded: z.boolean().optional(),
86
+ /** When the version was released. */
87
+ releasedAt: isoTimestampSchema.optional(),
88
+ /**
89
+ * What this exact version depends on, resolved at release time and keyed
90
+ * `namespace/recipe`. A consumer installing this version installs these
91
+ * versions rather than re-resolving the ranges its manifest declared, so a
92
+ * published version means one thing forever.
93
+ *
94
+ * The field is additive: an index written before it existed still parses, and
95
+ * a consumer that finds no entry falls back to the manifest's ranges.
96
+ */
97
+ dependencies: z.record(recipeKeySchema, indexDependencySchema).optional(),
98
+ });
99
+
100
+ /** One recipe, with every version the repo publishes of it. */
101
+ export const indexRecipeSchema = z.strictObject({
102
+ /** The recipe folder, relative to the repo root. */
103
+ path: relativePathSchema("a recipe path"),
104
+ /** One-paragraph summary, copied from the recipe manifest at release time. */
105
+ description: z.string().optional(),
106
+ /** Every published version, keyed by the exact version string. */
107
+ versions: z
108
+ .record(semverVersionSchema, indexVersionSchema)
109
+ .refine((versions) => Object.keys(versions).length > 0, {
110
+ message: "must list at least one published version",
111
+ }),
112
+ });
113
+
114
+ /** One namespace declaration, copied from the repo manifest at release time. */
115
+ export const indexNamespaceSchema = z.strictObject({
116
+ description: z.string().optional(),
117
+ });
118
+
119
+ /** The repo index schema. */
120
+ export const indexFileSchema = z
121
+ .strictObject({
122
+ /**
123
+ * A plain-language note about where this copy of the index came from. JSON
124
+ * has no comment syntax and an index is machine-written, so this is the one
125
+ * place a writer can say something to whoever opens the file. Sous ignores
126
+ * the value everywhere except one place: the seed index it writes for its
127
+ * own built-in repository carries `SEED_INDEX_COMMENT`, which is how a
128
+ * later run recognizes its own placeholder and is willing to replace it.
129
+ */
130
+ $comment: z.string().optional(),
131
+ formatVersion: formatVersionSchema,
132
+ /** The repo's suggested short name, copied from its manifest. */
133
+ name: repoNameSchema,
134
+ /** When this index was generated. */
135
+ generatedAt: isoTimestampSchema,
136
+ /** The version of sous that generated it. */
137
+ generator: semverVersionSchema,
138
+ /** Every namespace the repo publishes. */
139
+ namespaces: z.record(namespaceNameSchema, indexNamespaceSchema),
140
+ /** Every recipe the repo publishes, keyed `namespace/recipe`. */
141
+ recipes: z.record(recipeKeySchema, indexRecipeSchema),
142
+ })
143
+ .superRefine((index, ctx) => {
144
+ // A recipe whose namespace is not declared could never be resolved, so a
145
+ // release that produced one is broken; say which recipe and which namespace.
146
+ for (const key of Object.keys(index.recipes)) {
147
+ const namespace = key.slice(0, key.indexOf("/"));
148
+ if (!Object.hasOwn(index.namespaces, namespace)) {
149
+ ctx.addIssue({
150
+ code: "custom",
151
+ path: ["recipes", key],
152
+ message:
153
+ `belongs to the namespace '${namespace}', which this index does not ` +
154
+ `declare under 'namespaces'`,
155
+ });
156
+ }
157
+ }
158
+
159
+ // A version's tag is what the provider fetches, so a tag that does not
160
+ // name this exact recipe and version is a version pointing somewhere else.
161
+ // Nothing on the consumer side could otherwise tell: an index publishing
162
+ // `1.0.0` with `tag: "main"` would hand `git clone --branch main` a moving
163
+ // target, whose content changes on every push and whose pinned hash then
164
+ // simply starts failing. Sous writes these tags itself, so requiring the
165
+ // shape it writes costs a correct index nothing.
166
+ for (const [key, recipe] of Object.entries(index.recipes)) {
167
+ for (const [version, published] of Object.entries(recipe.versions)) {
168
+ const expected = `${key}@${version}`;
169
+ if (published.tag === expected) continue;
170
+ ctx.addIssue({
171
+ code: "custom",
172
+ path: ["recipes", key, "versions", version, "tag"],
173
+ message:
174
+ `is '${published.tag}', but a published version's tag names the recipe and ` +
175
+ `the version it carries, so this one must be '${expected}'`,
176
+ });
177
+ }
178
+ }
179
+ });
180
+
181
+ /** A validated repo index. */
182
+ export type IndexFile = z.infer<typeof indexFileSchema>;
183
+
184
+ /** One recipe entry in a repo index. */
185
+ export type IndexRecipe = z.infer<typeof indexRecipeSchema>;
186
+
187
+ /** One published version entry in a repo index. */
188
+ export type IndexVersion = z.infer<typeof indexVersionSchema>;
189
+
190
+ /** One resolved dependency of one published version. */
191
+ export type IndexDependency = z.infer<typeof indexDependencySchema>;
192
+
193
+ /** One namespace entry in a repo index. */
194
+ export type IndexNamespace = z.infer<typeof indexNamespaceSchema>;
195
+
196
+ /**
197
+ * Validates a parsed repo index, throwing a ConfigError that names the file and
198
+ * the path of every bad field.
199
+ *
200
+ * @param value - The parsed contents of the index file.
201
+ * @param sourceLabel - The index's file path or URL, named in error messages.
202
+ */
203
+ export function parseIndexFile(value: unknown, sourceLabel: string): IndexFile {
204
+ return parseFormat(indexFileSchema, value, sourceLabel, "repo index");
205
+ }
206
+
207
+ /**
208
+ * Serializes a repo index for writing, with every object key sorted so a
209
+ * regenerated index produces a minimal diff.
210
+ *
211
+ * @param index - The index to write.
212
+ */
213
+ export function stringifyIndexFile(index: IndexFile): string {
214
+ return stableJsonStringify(index);
215
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The links map: `sous.links.json`.
3
+ *
4
+ * A link redirects a repo's resolution away from the store and at a real
5
+ * working copy on disk, which is how a maintainer edits recipes: edits happen
6
+ * in a checkout, never in the store. `sous repo link` writes an entry;
7
+ * `sous repo unlink` removes it and leaves the checkout in place.
8
+ *
9
+ * Two maps are read: the project's `.sous/sous.links.json` and the machine-wide
10
+ * `$SOUS_HOME/sous.links.json`, with the project map winning on conflict. The
11
+ * file is MACHINE-WRITTEN and machine-local; it is never committed, because a
12
+ * link bypasses versions, the lockfile and freshness checks, and those bypasses
13
+ * belong to one person's machine rather than to the team.
14
+ */
15
+
16
+ import { z } from "zod";
17
+ import {
18
+ absolutePathSchema,
19
+ formatVersionSchema,
20
+ isoTimestampSchema,
21
+ parseFormat,
22
+ repoNameSchema,
23
+ stableJsonStringify,
24
+ } from "./common.js";
25
+
26
+ /** How the linked working copy came to exist. */
27
+ export const LINK_ORIGINS = ["clone", "path"] as const;
28
+
29
+ /** One linked repo. */
30
+ export const repoLinkSchema = z.strictObject({
31
+ /** Absolute path to the working copy sous reads instead of the store. */
32
+ path: absolutePathSchema,
33
+ /** When the link was created. */
34
+ linkedAt: isoTimestampSchema,
35
+ /**
36
+ * Whether sous cloned the working copy itself ('clone') or was pointed at an
37
+ * existing checkout ('path'). Unlinking never deletes either, but the origin
38
+ * tells the user what sous put there.
39
+ */
40
+ origin: z.enum(LINK_ORIGINS),
41
+ });
42
+
43
+ /** The links map schema. */
44
+ export const linksMapSchema = z.strictObject({
45
+ formatVersion: formatVersionSchema,
46
+ /** Every linked repo, keyed by the repo's configured short name. */
47
+ links: z.record(repoNameSchema, repoLinkSchema),
48
+ });
49
+
50
+ /** A validated links map. */
51
+ export type LinksMap = z.infer<typeof linksMapSchema>;
52
+
53
+ /** One validated link entry. */
54
+ export type RepoLink = z.infer<typeof repoLinkSchema>;
55
+
56
+ /** How the linked working copy came to exist. */
57
+ export type LinkOrigin = (typeof LINK_ORIGINS)[number];
58
+
59
+ /**
60
+ * Validates a parsed links map, throwing a ConfigError that names the file and
61
+ * the path of every bad field.
62
+ *
63
+ * @param value - The parsed contents of the links file.
64
+ * @param sourceLabel - The links file's path, named in error messages.
65
+ */
66
+ export function parseLinksMap(value: unknown, sourceLabel: string): LinksMap {
67
+ return parseFormat(linksMapSchema, value, sourceLabel, "links map");
68
+ }
69
+
70
+ /** An empty links map, for a project or machine with nothing linked. */
71
+ export function createEmptyLinksMap(): LinksMap {
72
+ return { formatVersion: 1, links: {} };
73
+ }
74
+
75
+ /**
76
+ * Serializes a links map for writing, with keys sorted.
77
+ *
78
+ * @param map - The links map to write.
79
+ */
80
+ export function stringifyLinksMap(map: LinksMap): string {
81
+ return stableJsonStringify(map);
82
+ }
83
+
84
+ /**
85
+ * Merges a machine-wide links map with a project's, with the project's entries
86
+ * winning, which is the precedence `sous repo link` documents.
87
+ *
88
+ * @param global - The machine-wide map, or undefined when there is none.
89
+ * @param project - The project's map, or undefined when there is none.
90
+ */
91
+ export function mergeLinksMaps(
92
+ global: LinksMap | undefined,
93
+ project: LinksMap | undefined
94
+ ): Record<string, RepoLink> {
95
+ return { ...(global?.links ?? {}), ...(project?.links ?? {}) };
96
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The project lockfile: `.sous/sous.lock.json`, committed to the project.
3
+ *
4
+ * The lockfile is MACHINE-WRITTEN and records the exact version and content
5
+ * hash of everything a project currently uses, so a fresh clone restores
6
+ * deterministically with no prompts and no version drift. Together with repo
7
+ * trust it is the supply-chain defense: nothing new enters a project except
8
+ * through an explicit, visible change to these files.
9
+ *
10
+ * A repository appears twice over: the project's own short name is the key of
11
+ * the `repos` map and is what every recipe entry and every message names, while
12
+ * the entry's `identity` is what the machine-wide store is keyed by.
13
+ *
14
+ * `requestedBy` is what makes removal safe. Every entry lists who holds it, the
15
+ * literal string `project` for something the project subscribed to directly and
16
+ * a recipe key for something pulled in as a dependency. Unsubscribing removes
17
+ * one holder; the entry itself goes only when the last holder does.
18
+ */
19
+
20
+ import { z } from "zod";
21
+ import { repoIdentity } from "../identity.js";
22
+ import { requireProvider } from "../providers/index.js";
23
+ import {
24
+ contentHashSchema,
25
+ formatVersionSchema,
26
+ parseFormat,
27
+ recipeKeySchema,
28
+ repoIdentitySchema,
29
+ repoNameSchema,
30
+ repoUrlSchema,
31
+ semverVersionSchema,
32
+ stableJsonStringify,
33
+ } from "./common.js";
34
+
35
+ /** The literal `requestedBy` holder meaning "the project subscribed to this directly". */
36
+ export const PROJECT_HOLDER = "project";
37
+
38
+ /** How a locked recipe entered the project. */
39
+ export const LOCK_KINDS = ["subscribes", "depends"] as const;
40
+
41
+ /** One repo the project resolves against. */
42
+ export const lockedRepoSchema = z
43
+ .strictObject({
44
+ /**
45
+ * Where the repo lives, as recorded when it was added: a URL, or an absolute
46
+ * path for a repository on this machine read through the `local` provider.
47
+ */
48
+ url: repoUrlSchema,
49
+ /**
50
+ * The repository's canonical identity, derived from that URL. The keys of
51
+ * `repos` are the project's own short names, which no other project has to
52
+ * agree with; this is what the machine-wide store and the index cache file
53
+ * the repository under, so a restore finds the same cached copy every other
54
+ * project uses.
55
+ *
56
+ * Optional ON READ only, for lockfiles written before the store was keyed by
57
+ * identity: an entry without one has its identity derived from `url` below,
58
+ * exactly as the writer would have derived it. Every lockfile sous writes
59
+ * carries it, so the field fills itself in on the next write.
60
+ */
61
+ identity: repoIdentitySchema.optional(),
62
+ /** Content hash of the index this lock was resolved against, when known. */
63
+ indexHash: contentHashSchema.optional(),
64
+ })
65
+ .transform((entry, ctx) => {
66
+ if (entry.identity !== undefined) return { ...entry, identity: entry.identity };
67
+
68
+ // An older lockfile recorded only the URL. Deriving the identity through the
69
+ // provider that handles that URL is what the writer itself does, so the
70
+ // answer is the one the entry would have carried had it been written today.
71
+ try {
72
+ const identity = repoIdentity(requireProvider(entry.url).canonicalize(entry.url));
73
+ return { ...entry, identity };
74
+ } catch {
75
+ ctx.addIssue({
76
+ code: "custom",
77
+ path: ["identity"],
78
+ message:
79
+ `is missing, and sous could not work one out from the url '${entry.url}' ` +
80
+ `because no provider recognizes it. Add an 'identity' to this entry, or ` +
81
+ `remove the lockfile and subscribe again to have sous rebuild it.`,
82
+ });
83
+ return z.NEVER;
84
+ }
85
+ });
86
+
87
+ /** One locked recipe. */
88
+ export const lockedRecipeSchema = z.strictObject({
89
+ /** The short name of the repo it came from; must appear under `repos`. */
90
+ repo: repoNameSchema,
91
+ /** The exact version resolved. */
92
+ version: semverVersionSchema,
93
+ /** Content hash of that version, verified against the store after every fetch. */
94
+ hash: contentHashSchema,
95
+ /**
96
+ * Who holds this entry: `project` for a direct subscription, or the ref key of
97
+ * a recipe that requires it. Used to refcount removal.
98
+ */
99
+ requestedBy: z
100
+ .array(z.string().min(1, "must not be empty"))
101
+ .min(1, "must name at least one holder"),
102
+ /** Whether the holder relationship is a co-subscription or a build dependency. */
103
+ kind: z.enum(LOCK_KINDS),
104
+ });
105
+
106
+ /** The lockfile schema. */
107
+ export const lockfileSchema = z
108
+ .strictObject({
109
+ formatVersion: formatVersionSchema,
110
+ /** Every repo the locked recipes came from, keyed by short name. */
111
+ repos: z.record(repoNameSchema, lockedRepoSchema),
112
+ /** Every locked recipe, keyed `namespace/recipe`. */
113
+ recipes: z.record(recipeKeySchema, lockedRecipeSchema),
114
+ })
115
+ .superRefine((lock, ctx) => {
116
+ // A recipe pointing at a repo the lockfile does not describe cannot be
117
+ // restored, so name the pair rather than failing later at fetch time.
118
+ for (const [key, entry] of Object.entries(lock.recipes)) {
119
+ if (!Object.hasOwn(lock.repos, entry.repo)) {
120
+ ctx.addIssue({
121
+ code: "custom",
122
+ path: ["recipes", key, "repo"],
123
+ message:
124
+ `names the repo '${entry.repo}', which this lockfile does not describe ` +
125
+ `under 'repos'`,
126
+ });
127
+ }
128
+ }
129
+ });
130
+
131
+ /** A validated lockfile. */
132
+ export type Lockfile = z.infer<typeof lockfileSchema>;
133
+
134
+ /** One locked repo entry. */
135
+ export type LockedRepo = z.infer<typeof lockedRepoSchema>;
136
+
137
+ /** One locked recipe entry. */
138
+ export type LockedRecipe = z.infer<typeof lockedRecipeSchema>;
139
+
140
+ /** How a locked recipe entered the project. */
141
+ export type LockKind = (typeof LOCK_KINDS)[number];
142
+
143
+ /**
144
+ * Validates a parsed lockfile, throwing a ConfigError that names the file and
145
+ * the path of every bad field.
146
+ *
147
+ * @param value - The parsed contents of the lockfile.
148
+ * @param sourceLabel - The lockfile's path, named in error messages.
149
+ */
150
+ export function parseLockfile(value: unknown, sourceLabel: string): Lockfile {
151
+ return parseFormat(lockfileSchema, value, sourceLabel, "lockfile");
152
+ }
153
+
154
+ /** An empty lockfile, for a project that has locked nothing yet. */
155
+ export function createEmptyLockfile(): Lockfile {
156
+ return { formatVersion: 1, repos: {}, recipes: {} };
157
+ }
158
+
159
+ /**
160
+ * Serializes a lockfile for writing, with every object key sorted so the
161
+ * committed file changes only when its content genuinely does.
162
+ *
163
+ * @param lockfile - The lockfile to write.
164
+ */
165
+ export function stringifyLockfile(lockfile: Lockfile): string {
166
+ return stableJsonStringify(lockfile);
167
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Dependency-free regular expressions shared by the Repositories on-disk
3
+ * formats, the ref parser and the sous config schema.
4
+ *
5
+ * This module deliberately imports nothing, so `config-schema.ts` can reuse the
6
+ * patterns without pulling in zod schemas, semver, or the rest of the repos
7
+ * layer. `formats/common.ts` re-exports everything here.
8
+ */
9
+
10
+ /**
11
+ * Lowercase kebab-case identifier: starts with a letter, then letters, digits
12
+ * or hyphens. Used for repo short names, namespace names and recipe names
13
+ * (`sous-recipes`, `tool-usage`, `automated-browser-tasks`).
14
+ */
15
+ export const KEBAB_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
16
+
17
+ /** A repo's configured short name, as used by the `repo:` ref qualifier. */
18
+ export const REPO_NAME_PATTERN = KEBAB_NAME_PATTERN;
19
+
20
+ /** A namespace name. */
21
+ export const NAMESPACE_NAME_PATTERN = KEBAB_NAME_PATTERN;
22
+
23
+ /** A recipe name, unique within its namespace. */
24
+ export const RECIPE_NAME_PATTERN = KEBAB_NAME_PATTERN;
25
+
26
+ /**
27
+ * A ref key: either a bare namespace (`workflow`) or a fully qualified recipe
28
+ * (`workflow/task-files`). Never carries a repo qualifier or a version range.
29
+ */
30
+ export const REF_KEY_PATTERN = /^[a-z][a-z0-9-]*(\/[a-z][a-z0-9-]*)?$/;
31
+
32
+ /** A recipe key, which always has both segments (`workflow/task-files`). */
33
+ export const RECIPE_KEY_PATTERN = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/;
34
+
35
+ /**
36
+ * A repository's canonical identity: the host, then the path it lives at, all
37
+ * lowercase (`github.com/sous-io/sous-recipes`). It is what every machine-wide
38
+ * key uses, because a project's short name for a repository is its own label
39
+ * and no other project has to agree with it.
40
+ */
41
+ export const REPO_IDENTITY_PATTERN = /^[^\s/]+(\/[^\s/]+)+$/;
42
+
43
+ /** A content hash, written as the algorithm name followed by lowercase hex. */
44
+ export const CONTENT_HASH_PATTERN = /^sha256-[0-9a-f]{64}$/;
45
+
46
+ /** An environment variable name, as declared by a recipe variable definition. */
47
+ export const ENV_VAR_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
48
+
49
+ /** A camelCase variable name, as declared by a recipe variable definition. */
50
+ export const VARIABLE_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
51
+
52
+ /**
53
+ * An ISO 8601 timestamp carrying an explicit offset (`Z` or `+hh:mm`). Machine
54
+ * written timestamps come from `new Date().toISOString()`, which matches.
55
+ */
56
+ export const ISO_TIMESTAMP_PATTERN =
57
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;