@sous-io/sous 0.1.1 → 0.2.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 (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 +409 -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 +625 -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 +415 -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,238 @@
1
+ /**
2
+ * Scaffolding a new recipe repository, which is what `sous repo init` does.
3
+ *
4
+ * The scaffold is built in memory first, validated with the very parsers sous
5
+ * uses to read a real repository, and only then written to disk. A scaffold
6
+ * that sous itself cannot read would be worse than no scaffold at all, so
7
+ * `scaffoldRepo` reads every manifest back after writing it and reports the
8
+ * failure against the file it just produced.
9
+ */
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { ConfigError } from "../../errors.js";
14
+ import {
15
+ INDEX_FILENAME,
16
+ MANIFEST_EXTENSIONS,
17
+ RECIPE_MANIFEST_BASENAME,
18
+ REPO_MANIFEST_BASENAME,
19
+ } from "../formats/common.js";
20
+ import { parseIndexFile } from "../formats/index-file.js";
21
+ import { parseRecipeManifest } from "../formats/recipe-manifest.js";
22
+ import { parseRepoManifest } from "../formats/repo-manifest.js";
23
+ import { findRepoManifest, loadJsonFile, loadManifestFile } from "../load-manifest.js";
24
+ import {
25
+ buildExampleSkill,
26
+ buildGitignore,
27
+ buildIndexFile,
28
+ buildReadme,
29
+ buildRecipeManifest,
30
+ buildReleaseWorkflow,
31
+ buildRepoManifest,
32
+ exampleRecipePath,
33
+ type ScaffoldContext,
34
+ } from "./templates.js";
35
+
36
+ export * from "./templates.js";
37
+
38
+ /** The name given to the one example recipe every scaffold writes. */
39
+ export const EXAMPLE_RECIPE_NAME = "example";
40
+
41
+ /** What to scaffold, and where. */
42
+ export type ScaffoldOptions = {
43
+ /** Absolute path to the directory the repository is created in. */
44
+ directory: string;
45
+ /** The repository's short name. Defaults to the directory's own name. */
46
+ name?: string;
47
+ /** The one namespace to declare. Defaults to the repository's name. */
48
+ namespace?: string;
49
+ /** Overwrite an existing repository rather than refusing to touch it. */
50
+ force?: boolean;
51
+ /** Work out every file and validate the plan, but write nothing. */
52
+ dryRun?: boolean;
53
+ /** The version of sous recorded in the generated index. */
54
+ sousVersion: string;
55
+ /** When the scaffold ran, recorded in the generated index. Defaults to now. */
56
+ now?: Date;
57
+ };
58
+
59
+ /** What a scaffold produced. */
60
+ export type ScaffoldResult = {
61
+ /** The directory the repository was created in. */
62
+ directory: string;
63
+ /** The repository's short name. */
64
+ name: string;
65
+ /** The namespace that was declared. */
66
+ namespace: string;
67
+ /** Paths of every file, relative to the directory, in the order they were written. */
68
+ files: string[];
69
+ /** True when nothing was actually written. */
70
+ dryRun: boolean;
71
+ };
72
+
73
+ /** One planned file: where it goes, and what goes in it. */
74
+ type PlannedFile = {
75
+ /** Path relative to the repository root. */
76
+ relativePath: string;
77
+ /** The complete file contents. */
78
+ contents: string;
79
+ };
80
+
81
+ /**
82
+ * Creates a new recipe repository: a repo manifest, one example recipe with a
83
+ * placeholder skill, an empty but valid index, a README, release automation and
84
+ * a `.gitignore`.
85
+ *
86
+ * @param options - What to scaffold, and where.
87
+ */
88
+ export function scaffoldRepo(options: ScaffoldOptions): ScaffoldResult {
89
+ const directory = path.resolve(options.directory);
90
+ const name = normalizeName(options.name ?? path.basename(directory), "--name");
91
+ const namespace = normalizeName(options.namespace ?? name, "--namespace");
92
+ const dryRun = options.dryRun === true;
93
+
94
+ assertNoExistingRepo(directory, options.force === true);
95
+
96
+ const context: ScaffoldContext = {
97
+ name,
98
+ namespace,
99
+ recipe: EXAMPLE_RECIPE_NAME,
100
+ sousVersion: options.sousVersion,
101
+ generatedAt: (options.now ?? new Date()).toISOString(),
102
+ };
103
+
104
+ const files = planFiles(context);
105
+
106
+ if (!dryRun) {
107
+ for (const file of files) {
108
+ const target = path.join(directory, file.relativePath);
109
+ fs.mkdirSync(path.dirname(target), { recursive: true });
110
+ fs.writeFileSync(target, file.contents, "utf8");
111
+ }
112
+ verifyScaffold(directory, context);
113
+ }
114
+
115
+ return {
116
+ directory,
117
+ name,
118
+ namespace,
119
+ files: files.map((file) => file.relativePath),
120
+ dryRun,
121
+ };
122
+ }
123
+
124
+ /** Builds every file the scaffold writes, in the order they are written. */
125
+ function planFiles(context: ScaffoldContext): PlannedFile[] {
126
+ const recipeDir = exampleRecipePath(context);
127
+ return [
128
+ {
129
+ relativePath: `${REPO_MANIFEST_BASENAME}${MANIFEST_EXTENSIONS[0]}`,
130
+ contents: buildRepoManifest(context),
131
+ },
132
+ {
133
+ relativePath: INDEX_FILENAME,
134
+ contents: buildIndexFile(context),
135
+ },
136
+ {
137
+ relativePath: `${recipeDir}/${RECIPE_MANIFEST_BASENAME}${MANIFEST_EXTENSIONS[0]}`,
138
+ contents: buildRecipeManifest(context),
139
+ },
140
+ {
141
+ relativePath: `${recipeDir}/skills/example-skill/SKILL.md`,
142
+ contents: buildExampleSkill(context),
143
+ },
144
+ { relativePath: "README.md", contents: buildReadme(context) },
145
+ {
146
+ relativePath: ".github/workflows/sous-release.yml",
147
+ contents: buildReleaseWorkflow(),
148
+ },
149
+ { relativePath: ".gitignore", contents: buildGitignore() },
150
+ ];
151
+ }
152
+
153
+ /**
154
+ * Refuses to scaffold over a repository that already exists, unless the caller
155
+ * asked to. The manifest is the thing that makes a directory a repository, so
156
+ * that is what is checked; an ordinary directory with unrelated files in it is
157
+ * a perfectly reasonable place to create one.
158
+ */
159
+ function assertNoExistingRepo(directory: string, force: boolean): void {
160
+ if (force) return;
161
+
162
+ const existing = findRepoManifest(directory);
163
+ if (existing !== undefined) {
164
+ throw new ConfigError(
165
+ `${directory} is already a sous repository.\n` +
166
+ ` It holds ${path.basename(existing)}, which sous will not overwrite.\n` +
167
+ ` Pass --force to write the scaffold over it, or choose another directory.`
168
+ );
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Reads back every file that has to parse, using the same loaders and schemas
174
+ * that read a published repository, so a scaffold is never reported as a
175
+ * success unless sous can actually read it.
176
+ */
177
+ function verifyScaffold(directory: string, context: ScaffoldContext): void {
178
+ const manifestPath = path.join(
179
+ directory,
180
+ `${REPO_MANIFEST_BASENAME}${MANIFEST_EXTENSIONS[0]}`
181
+ );
182
+ const manifest = parseRepoManifest(loadManifestFile(manifestPath), manifestPath);
183
+
184
+ const indexPath = path.join(directory, INDEX_FILENAME);
185
+ parseIndexFile(loadJsonFile(indexPath, "repo index"), indexPath);
186
+
187
+ for (const recipePath of manifest.recipes) {
188
+ const recipeManifestPath = path.join(
189
+ directory,
190
+ recipePath,
191
+ `${RECIPE_MANIFEST_BASENAME}${MANIFEST_EXTENSIONS[0]}`
192
+ );
193
+ const recipe = parseRecipeManifest(
194
+ loadManifestFile(recipeManifestPath),
195
+ recipeManifestPath
196
+ );
197
+
198
+ if (!Object.hasOwn(manifest.namespaces, recipe.namespace)) {
199
+ throw new ConfigError(
200
+ `The scaffold in ${directory} is inconsistent.\n` +
201
+ ` The recipe at ${recipePath} declares the namespace ` +
202
+ `'${recipe.namespace}', which ${REPO_MANIFEST_BASENAME}` +
203
+ `${MANIFEST_EXTENSIONS[0]} does not declare.\n` +
204
+ ` This is a bug in sous; please report it.`
205
+ );
206
+ }
207
+ }
208
+
209
+ if (manifest.name !== context.name) {
210
+ throw new ConfigError(
211
+ `The scaffold in ${directory} is inconsistent: the repo manifest names it ` +
212
+ `'${manifest.name}' rather than '${context.name}'.\n` +
213
+ ` This is a bug in sous; please report it.`
214
+ );
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Checks that a name sous is about to write into a manifest is a name the
220
+ * manifest schema accepts, and says how to fix it when it is not. Lower-cases
221
+ * the value first, so a directory called `My-Recipes` yields `my-recipes`
222
+ * instead of an error.
223
+ */
224
+ function normalizeName(value: string, flagName: string): string {
225
+ const normalized = value.trim().toLowerCase();
226
+
227
+ if (!/^[a-z][a-z0-9-]*$/.test(normalized)) {
228
+ throw new ConfigError(
229
+ `'${value}' cannot be used as a name here.\n` +
230
+ ` A repository name and a namespace name are lowercase kebab-case: a ` +
231
+ `letter, then letters, digits or hyphens (for example 'sous-recipes').\n` +
232
+ ` Pass ${flagName} to choose one, or run the command in a directory whose ` +
233
+ `own name fits.`
234
+ );
235
+ }
236
+
237
+ return normalized;
238
+ }
@@ -0,0 +1,415 @@
1
+ /**
2
+ * The files `sous repo init` writes, as plain string builders.
3
+ *
4
+ * These are deliberately not templates run through a template engine. A
5
+ * scaffolded repository is read by a person before it is read by a machine, so
6
+ * every file here is written to be explained: the comments are the point, and a
7
+ * rendering step would only stand between the author of the scaffold and the
8
+ * author of the new repository.
9
+ *
10
+ * Every builder returns a complete file, ending in a newline.
11
+ */
12
+
13
+ import {
14
+ INDEX_FILENAME,
15
+ RECIPE_MANIFEST_BASENAME,
16
+ REPO_MANIFEST_BASENAME,
17
+ } from "../formats/common.js";
18
+ import { stringifyIndexFile, type IndexFile } from "../formats/index-file.js";
19
+
20
+ /** What every builder needs to know about the repository being scaffolded. */
21
+ export type ScaffoldContext = {
22
+ /** The repository's short name, used as its suggested name and in the README. */
23
+ name: string;
24
+ /** The one namespace the scaffold declares. */
25
+ namespace: string;
26
+ /** The example recipe's name. */
27
+ recipe: string;
28
+ /** The version of sous doing the scaffolding, recorded in the index. */
29
+ sousVersion: string;
30
+ /** When the scaffold ran, recorded in the index. */
31
+ generatedAt: string;
32
+ };
33
+
34
+ /** The path, relative to the repository root, of the example recipe's folder. */
35
+ export function exampleRecipePath(context: ScaffoldContext): string {
36
+ return `recipes/${context.namespace}/${context.recipe}`;
37
+ }
38
+
39
+ /**
40
+ * The repo manifest at the root: what the repository publishes, and where each
41
+ * recipe folder lives.
42
+ *
43
+ * @param context - The repository being scaffolded.
44
+ */
45
+ export function buildRepoManifest(context: ScaffoldContext): string {
46
+ return `# The repo manifest: what this repository publishes.
47
+ #
48
+ # It is read before anything is downloaded, and it is never executable, so
49
+ # anyone deciding whether to trust this repository can read its whole surface
50
+ # without running any of its code.
51
+
52
+ formatVersion: 1
53
+
54
+ # A suggested short name. Each project chooses the name it actually uses when it
55
+ # runs 'sous repo add', so two repositories suggesting the same name never clash.
56
+ name: ${context.name}
57
+
58
+ description: >-
59
+ One paragraph saying what this repository publishes and who it is for.
60
+
61
+ # Where to send a change. Shown to anyone whose provider cannot open a proposal
62
+ # for them, so a contributor is never left without a route.
63
+ # contribute: https://example.com/contributing
64
+
65
+ # Namespaces group recipes. They are not versioned, and a project may subscribe
66
+ # to a whole namespace, which means every recipe in it, including ones published
67
+ # later.
68
+ namespaces:
69
+ ${context.namespace}:
70
+ description: What the recipes in this namespace have in common.
71
+
72
+ # Every recipe folder in this repository, as a path relative to this file. Each
73
+ # folder holds one '${RECIPE_MANIFEST_BASENAME}.yaml'.
74
+ recipes:
75
+ - ${exampleRecipePath(context)}
76
+ `;
77
+ }
78
+
79
+ /**
80
+ * The example recipe manifest: one publishable unit, with the variables section
81
+ * shown as commented-out example.
82
+ *
83
+ * @param context - The repository being scaffolded.
84
+ */
85
+ export function buildRecipeManifest(context: ScaffoldContext): string {
86
+ return `# A recipe manifest: one publishable unit, with its own version.
87
+ #
88
+ # Copy this folder to start a new recipe, then add its path to the 'recipes'
89
+ # list in the ${REPO_MANIFEST_BASENAME}.yaml at the root of this repository.
90
+
91
+ formatVersion: 1
92
+
93
+ namespace: ${context.namespace}
94
+ name: ${context.recipe}
95
+
96
+ # Recipe metadata is the source of truth for versions. 'sous repo release'
97
+ # bumps this field and keeps the matching git tag consistent with it.
98
+ version: 0.1.0
99
+
100
+ description: >-
101
+ One paragraph saying what this recipe gives a project that subscribes to it.
102
+
103
+ # Build dependencies: fetched, pinned and addressable from this recipe's own
104
+ # files, but their files do NOT enter a subscriber's output.
105
+ # depends:
106
+ # - core/shared-partials@^1.0.0
107
+
108
+ # Co-subscriptions: subscribing to this recipe subscribes the project to these
109
+ # as well, in full. Their questions run and their files DO enter the output.
110
+ # subscribes:
111
+ # - workflow/task-files
112
+
113
+ # The files this recipe contributes, grouped by what they are. Paths are
114
+ # relative to this folder.
115
+ contents:
116
+ - kind: skills
117
+ include:
118
+ - skills/**/*.md
119
+
120
+ # Variables this recipe needs answered. A definition is a specification, never a
121
+ # value: sous asks the question only when a subscribed recipe needs the variable
122
+ # and no valid answer is already in scope.
123
+ #
124
+ # Every definition must carry a 'description' and an 'example'. The description
125
+ # is the paragraph shown above the question and by 'sous vars show <name>'; it
126
+ # says, in full sentences, what the setting is for, what the default does, and
127
+ # what else is acceptable. The prompt is one plain question, nothing more. The
128
+ # example is a realistic sample answer, shown with the question so nobody has to
129
+ # guess what a good one looks like; it is documentation only and is never
130
+ # stored, so use 'default' for a value a project should actually start with.
131
+ #
132
+ # variables:
133
+ # - name: apiBaseUrl
134
+ # type: url
135
+ # prompt: Which API should this project talk to?
136
+ # description: >-
137
+ # Every request this recipe generates is sent to one deployment of the API,
138
+ # and this setting says which one. The default points at the public
139
+ # production host, but any deployment you can reach works, including a
140
+ # staging host or a service running on your own machine.
141
+ # example: https://api.example.com
142
+ # default: https://api.example.com
143
+ # required: true
144
+ #
145
+ # - name: serviceToken
146
+ # type: string
147
+ # # Name an environment variable explicitly to reuse a value the environment
148
+ # # already carries. When omitted, 'sous repo release' derives one.
149
+ # env: SERVICE_TOKEN
150
+ # prompt: What is this project's service token?
151
+ # description: >-
152
+ # This recipe authenticates every call it makes with a service token, which
153
+ # is issued per project and is not shared between them. Create one under
154
+ # Settings, then Tokens, and give it read access to the project you are
155
+ # configuring. There is no default; a token is always specific to you, and
156
+ # it is stored in the gitignored env file so it never reaches git.
157
+ # example: svc_0123456789abcdef0123
158
+ # # A secret is always written to the gitignored '.sous/.env.local'.
159
+ # secret: true
160
+ # scope: local
161
+ # validate:
162
+ # minLength: 20
163
+ `;
164
+ }
165
+
166
+ /**
167
+ * The placeholder skill the example recipe contributes.
168
+ *
169
+ * @param context - The repository being scaffolded.
170
+ */
171
+ export function buildExampleSkill(context: ScaffoldContext): string {
172
+ return `---
173
+ name: example-skill
174
+ description: >-
175
+ Replace this with the sentence that tells an agent when to load the skill.
176
+ Say what the skill covers and name the situations that should trigger it.
177
+ ---
178
+
179
+ # Example skill
180
+
181
+ This file is a placeholder written by 'sous repo init'. Replace it with a real
182
+ skill, or delete it once the ${context.recipe} recipe has content of its own.
183
+
184
+ A skill is ordinary markdown. Everything under this recipe's 'contents' entry is
185
+ copied into a subscribing project's agent skill directory, so what you write
186
+ here is what an agent reads there.
187
+
188
+ ## What to put here
189
+
190
+ Describe the concept the skill covers, then the rules an agent should follow
191
+ when it applies. Keep it short enough to be read in full, and point at reference
192
+ files for anything long.
193
+ `;
194
+ }
195
+
196
+ /**
197
+ * The empty but valid index. `sous repo release` rewrites it; it exists from the
198
+ * first commit so the repository is readable by sous before anything is
199
+ * published.
200
+ *
201
+ * @param context - The repository being scaffolded.
202
+ */
203
+ export function buildIndexFile(context: ScaffoldContext): string {
204
+ const index: IndexFile = {
205
+ formatVersion: 1,
206
+ name: context.name,
207
+ generatedAt: context.generatedAt,
208
+ generator: context.sousVersion,
209
+ namespaces: {
210
+ [context.namespace]: {
211
+ description: "What the recipes in this namespace have in common.",
212
+ },
213
+ },
214
+ recipes: {},
215
+ };
216
+ return stringifyIndexFile(index);
217
+ }
218
+
219
+ /**
220
+ * The README, explaining the layout in plain language to whoever opens the
221
+ * repository next.
222
+ *
223
+ * @param context - The repository being scaffolded.
224
+ */
225
+ export function buildReadme(context: ScaffoldContext): string {
226
+ const recipeDir = exampleRecipePath(context);
227
+ return `# ${context.name}
228
+
229
+ A sous recipe repository. It publishes **recipes**, grouped into
230
+ **namespaces**, that other projects subscribe to.
231
+
232
+ ## Layout
233
+
234
+ \`\`\`
235
+ ${REPO_MANIFEST_BASENAME}.yaml what this repository publishes
236
+ ${INDEX_FILENAME} the published catalog, written by sous
237
+ ${recipeDir}/
238
+ ${RECIPE_MANIFEST_BASENAME}.yaml one recipe: its version, contents and variables
239
+ skills/ the files that recipe contributes
240
+ \`\`\`
241
+
242
+ ## The three files
243
+
244
+ **\`${REPO_MANIFEST_BASENAME}.yaml\`** declares the namespaces this repository
245
+ publishes and lists every recipe folder in it. It is hand-written, and it is the
246
+ first thing sous reads.
247
+
248
+ **\`${RECIPE_MANIFEST_BASENAME}.yaml\`** describes one recipe: which namespace it
249
+ belongs to, what version it is at, what it depends on, which of its files a
250
+ subscriber receives, and which variables it needs answered. It is hand-written
251
+ too, and its \`version\` field is the source of truth for versions.
252
+
253
+ **\`${INDEX_FILENAME}\`** is the catalog: every recipe, every published version,
254
+ and a content hash for each one. It is written by \`sous repo release\` and
255
+ committed. Do not edit it by hand.
256
+
257
+ ## Adding a recipe
258
+
259
+ 1. Copy \`${recipeDir}\` to a new folder under \`recipes/\`.
260
+ 2. Edit its \`${RECIPE_MANIFEST_BASENAME}.yaml\`: set the namespace, the name, the
261
+ version and the contents.
262
+ 3. Add the new folder's path to the \`recipes\` list in
263
+ \`${REPO_MANIFEST_BASENAME}.yaml\`.
264
+ 4. Open a pull request. The workflow in \`.github/workflows/sous-release.yml\`
265
+ checks that everything is consistent before it can be merged.
266
+
267
+ ## Publishing
268
+
269
+ Commit your recipe changes, then run \`sous repo release\`. It shows you what it
270
+ would publish and asks once, then raises the version of every recipe whose files
271
+ changed since the tag that last published it, regenerates \`${INDEX_FILENAME}\`,
272
+ commits both, and cuts an annotated tag for each version. Add \`--push\` to push
273
+ the commit and the tags, or push them yourself.
274
+
275
+ Useful ways to narrow or steer it:
276
+
277
+ \`\`\`bash
278
+ sous repo release --dry-run # show the plan and stop
279
+ sous repo release --namespace ${context.namespace} # only this namespace
280
+ sous repo release --recipe ${context.namespace}/${context.recipe} # only this recipe
281
+ sous repo release --bump minor # a minor step instead of a patch
282
+ sous repo release --include-unchanged # release everything in scope anyway
283
+ \`\`\`
284
+
285
+ On a branch other than \`main\`, a release bumps and commits but cuts no tags:
286
+ tags are cut on the default branch, by the workflow in
287
+ \`.github/workflows/sous-release.yml\` after the merge. Pass \`--tag\` to cut them
288
+ anyway. Tags are shaped \`namespace/recipe@version\`, and a version is published
289
+ when its tag exists.
290
+
291
+ To propose a change to a repository you do not maintain, commit it and run
292
+ \`sous repo submit\`, which validates everything first and then opens a pull
293
+ request through your provider's own command line tool.
294
+
295
+ ## Using it
296
+
297
+ In any project that has a sous config:
298
+
299
+ \`\`\`bash
300
+ sous repo add <the URL of this repository>
301
+ sous subscribe ${context.namespace}/${context.recipe}
302
+ sous build
303
+ \`\`\`
304
+
305
+ Adding a repository is what trusts it, so read a repository before you add it.
306
+
307
+ ## Working on it
308
+
309
+ \`sous repo link\` points a project at a working copy of this repository instead
310
+ of at a published version, so you can edit a recipe and rebuild without
311
+ releasing anything:
312
+
313
+ \`\`\`bash
314
+ sous repo link ${context.name} /path/to/this/checkout
315
+ \`\`\`
316
+
317
+ Builds say loudly when a repository is linked. Run \`sous repo unlink ${context.name}\`
318
+ to go back to published versions.
319
+ `;
320
+ }
321
+
322
+ /** The release workflow: validate every pull request, publish on merge. */
323
+ export function buildReleaseWorkflow(): string {
324
+ return `# Release automation for this sous recipe repository.
325
+ #
326
+ # Two jobs, both running the sous CLI straight from npm so nothing needs to be
327
+ # installed into this repository:
328
+ #
329
+ # 'sous repo release --check' validates every manifest, confirms each recipe
330
+ # folder matches what the repo manifest lists, and confirms the committed
331
+ # index agrees with the versions and dependencies the recipe manifests
332
+ # declare. It only reads; it never writes, commits or tags. That makes it the
333
+ # right thing to run on a pull request.
334
+ #
335
+ # 'sous repo release --ci --push --yes' does the same validation and then
336
+ # publishes. '--ci' raises no versions, accepts the plan it prints, and asks
337
+ # no questions: the version bump belongs in the change being merged, so a
338
+ # recipe that changed without one fails here rather than being given a version
339
+ # nobody reviewed. It cuts an annotated tag for every version that does not
340
+ # have one yet, dependency-first, and '--push' pushes the commit and those
341
+ # tags. '--ci' implies '--yes'; passing it as well keeps this workflow working
342
+ # with a sous old enough that it did not.
343
+
344
+ name: sous release
345
+
346
+ on:
347
+ pull_request:
348
+ push:
349
+ branches:
350
+ - main
351
+
352
+ jobs:
353
+ check:
354
+ name: Validate recipes
355
+ if: github.event_name == 'pull_request'
356
+ runs-on: ubuntu-latest
357
+ steps:
358
+ - uses: actions/checkout@v4
359
+ with:
360
+ # The whole history, so the tags a version is checked against are
361
+ # visible.
362
+ fetch-depth: 0
363
+ - uses: actions/setup-node@v4
364
+ with:
365
+ node-version: 22
366
+ - name: Validate every manifest, the index and the tags
367
+ run: npx --yes @sous-io/sous repo release --check
368
+
369
+ release:
370
+ name: Publish recipes
371
+ if: github.event_name == 'push'
372
+ runs-on: ubuntu-latest
373
+ permissions:
374
+ # Needed to push the release commit and the new tags.
375
+ contents: write
376
+ steps:
377
+ - uses: actions/checkout@v4
378
+ with:
379
+ # The whole history, so existing tags are visible and versions that
380
+ # were already published are not cut a second time.
381
+ fetch-depth: 0
382
+ - uses: actions/setup-node@v4
383
+ with:
384
+ node-version: 22
385
+ - name: Identify the committer, in case the index has to be rewritten
386
+ run: |
387
+ git config user.name "github-actions[bot]"
388
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
389
+ - name: Publish every new version
390
+ run: npx --yes @sous-io/sous repo release --ci --push --yes
391
+ `;
392
+ }
393
+
394
+ /**
395
+ * The repository's `.gitignore`. A recipe repository holds text, so this stays
396
+ * short: editor and operating system leftovers, and the machine-local files
397
+ * sous writes when the repository is linked into a project.
398
+ */
399
+ export function buildGitignore(): string {
400
+ return `# Operating system and editor leftovers
401
+ .DS_Store
402
+ Thumbs.db
403
+ *.swp
404
+
405
+ # Dependencies, if a recipe ever needs any
406
+ node_modules/
407
+
408
+ # Machine-local sous files, written when this repository is used from a project
409
+ .sous/sous.state.json
410
+ .sous/sous.pid
411
+ .sous/sous.links.json
412
+ .sous/repos/
413
+ .sous/.env.local
414
+ `;
415
+ }