@sous-io/sous 0.2.37 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1094 -0
  4. package/docs/markdown/README.md +5 -4
  5. package/docs/markdown/_sidebar.md +21 -1
  6. package/docs/markdown/commands.md +294 -431
  7. package/docs/markdown/config-discovery.md +25 -14
  8. package/docs/markdown/config-inspection.md +53 -31
  9. package/docs/markdown/config-layers.md +168 -66
  10. package/docs/markdown/config-variables.md +44 -11
  11. package/docs/markdown/configuration.md +104 -17
  12. package/docs/markdown/design-principles.md +4 -1
  13. package/docs/markdown/repositories-authoring.md +30 -0
  14. package/docs/markdown/repositories-consuming.md +72 -14
  15. package/docs/markdown/repositories-file-formats.md +31 -13
  16. package/docs/markdown/repositories-providers.md +48 -57
  17. package/docs/markdown/repositories-quickstart.md +3 -2
  18. package/docs/markdown/repositories-troubleshooting.md +27 -1
  19. package/docs/markdown/repositories-variables.md +6 -1
  20. package/docs/markdown/repositories.md +18 -8
  21. package/package.json +48 -107
  22. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
  23. package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
  24. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
  25. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
  26. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
  27. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  28. package/sous.config.schema.json +351 -132
  29. package/bin/run.js +0 -51
  30. package/src/base-command.ts +0 -457
  31. package/src/commands/build.ts +0 -181
  32. package/src/commands/clear.ts +0 -79
  33. package/src/commands/compile.ts +0 -155
  34. package/src/commands/config/get.ts +0 -110
  35. package/src/commands/config/show.ts +0 -32
  36. package/src/commands/config/validate.ts +0 -53
  37. package/src/commands/help.ts +0 -46
  38. package/src/commands/init.ts +0 -289
  39. package/src/commands/launch.ts +0 -139
  40. package/src/commands/lock/rebuild.ts +0 -239
  41. package/src/commands/lock/show.ts +0 -115
  42. package/src/commands/namespace/list.ts +0 -138
  43. package/src/commands/namespace/show.ts +0 -130
  44. package/src/commands/prune.ts +0 -43
  45. package/src/commands/recipe/list.ts +0 -112
  46. package/src/commands/recipe/show.ts +0 -439
  47. package/src/commands/repo/add.ts +0 -145
  48. package/src/commands/repo/contribute.ts +0 -604
  49. package/src/commands/repo/gc.ts +0 -173
  50. package/src/commands/repo/init.ts +0 -136
  51. package/src/commands/repo/link.ts +0 -899
  52. package/src/commands/repo/list.ts +0 -236
  53. package/src/commands/repo/release.ts +0 -685
  54. package/src/commands/repo/remove.ts +0 -194
  55. package/src/commands/repo/search.ts +0 -243
  56. package/src/commands/repo/submit.ts +0 -341
  57. package/src/commands/repo/unlink.ts +0 -461
  58. package/src/commands/subscription/add.ts +0 -286
  59. package/src/commands/subscription/list.ts +0 -208
  60. package/src/commands/subscription/remove.ts +0 -182
  61. package/src/commands/subscription/update.ts +0 -216
  62. package/src/commands/update.ts +0 -178
  63. package/src/commands/vars/ask.ts +0 -417
  64. package/src/commands/vars/index.ts +0 -79
  65. package/src/commands/vars/list.ts +0 -67
  66. package/src/commands/vars/show.ts +0 -77
  67. package/src/config-command.ts +0 -30
  68. package/src/lib/build-preparation.ts +0 -128
  69. package/src/lib/build-service.ts +0 -580
  70. package/src/lib/config-discovery.ts +0 -378
  71. package/src/lib/config-inspect.ts +0 -145
  72. package/src/lib/config-kernel.mjs +0 -379
  73. package/src/lib/config-schema.ts +0 -401
  74. package/src/lib/env-file.ts +0 -328
  75. package/src/lib/env-local.ts +0 -212
  76. package/src/lib/errors.ts +0 -32
  77. package/src/lib/glob-pattern.ts +0 -24
  78. package/src/lib/include-resolver.ts +0 -523
  79. package/src/lib/interactive.ts +0 -165
  80. package/src/lib/markdown-compiler.ts +0 -947
  81. package/src/lib/package-info.ts +0 -25
  82. package/src/lib/pid-service.ts +0 -99
  83. package/src/lib/project-install.d.mts +0 -44
  84. package/src/lib/project-install.mjs +0 -234
  85. package/src/lib/project-scaffold/index.ts +0 -332
  86. package/src/lib/project-scaffold/templates.ts +0 -240
  87. package/src/lib/repos/catalog-display.ts +0 -215
  88. package/src/lib/repos/catalog-inputs.ts +0 -288
  89. package/src/lib/repos/catalog.ts +0 -842
  90. package/src/lib/repos/contribute.ts +0 -452
  91. package/src/lib/repos/core-recipe.ts +0 -105
  92. package/src/lib/repos/declarations.ts +0 -187
  93. package/src/lib/repos/defaults.ts +0 -175
  94. package/src/lib/repos/formats/common.ts +0 -440
  95. package/src/lib/repos/formats/index-file.ts +0 -253
  96. package/src/lib/repos/formats/links-map.ts +0 -98
  97. package/src/lib/repos/formats/lockfile.ts +0 -169
  98. package/src/lib/repos/formats/patterns.ts +0 -57
  99. package/src/lib/repos/formats/recipe-manifest.ts +0 -205
  100. package/src/lib/repos/formats/repo-manifest.ts +0 -97
  101. package/src/lib/repos/formats/store-entry.ts +0 -84
  102. package/src/lib/repos/formats/variable-definition.ts +0 -268
  103. package/src/lib/repos/freshness.ts +0 -264
  104. package/src/lib/repos/git-clone.ts +0 -850
  105. package/src/lib/repos/identity.ts +0 -89
  106. package/src/lib/repos/index.ts +0 -57
  107. package/src/lib/repos/links.ts +0 -356
  108. package/src/lib/repos/load-manifest.ts +0 -237
  109. package/src/lib/repos/lock-service.ts +0 -454
  110. package/src/lib/repos/locked-namespace-resolver.ts +0 -232
  111. package/src/lib/repos/locked-recipe-lookup.ts +0 -123
  112. package/src/lib/repos/locked-recipes.ts +0 -254
  113. package/src/lib/repos/managed-layer.ts +0 -422
  114. package/src/lib/repos/memories-hash-name.ts +0 -33
  115. package/src/lib/repos/namespace-resolver.ts +0 -527
  116. package/src/lib/repos/providers/base.ts +0 -268
  117. package/src/lib/repos/providers/git.ts +0 -245
  118. package/src/lib/repos/providers/github.ts +0 -589
  119. package/src/lib/repos/providers/gitlab.ts +0 -332
  120. package/src/lib/repos/providers/http.ts +0 -128
  121. package/src/lib/repos/providers/index-cache.ts +0 -421
  122. package/src/lib/repos/providers/index.ts +0 -106
  123. package/src/lib/repos/providers/local.ts +0 -410
  124. package/src/lib/repos/providers/provider.ts +0 -615
  125. package/src/lib/repos/recipe-config-layers.ts +0 -316
  126. package/src/lib/repos/recipe-key-matcher.ts +0 -62
  127. package/src/lib/repos/recipe-memories.ts +0 -178
  128. package/src/lib/repos/recipe-targets.ts +0 -236
  129. package/src/lib/repos/ref-search.ts +0 -46
  130. package/src/lib/repos/release/bump.ts +0 -161
  131. package/src/lib/repos/release/changelog.ts +0 -461
  132. package/src/lib/repos/release/git-state.ts +0 -460
  133. package/src/lib/repos/release/index-builder.ts +0 -1108
  134. package/src/lib/repos/release/index.ts +0 -19
  135. package/src/lib/repos/release/plan.ts +0 -521
  136. package/src/lib/repos/release/settle.ts +0 -154
  137. package/src/lib/repos/release/submissions.ts +0 -214
  138. package/src/lib/repos/release/submit-checkout.ts +0 -290
  139. package/src/lib/repos/release/submit-questions.ts +0 -153
  140. package/src/lib/repos/release/submit-service.ts +0 -898
  141. package/src/lib/repos/release/tags.ts +0 -239
  142. package/src/lib/repos/release/validate.ts +0 -463
  143. package/src/lib/repos/resolver.ts +0 -1028
  144. package/src/lib/repos/scaffold/index.ts +0 -245
  145. package/src/lib/repos/scaffold/templates.ts +0 -517
  146. package/src/lib/repos/seed.ts +0 -473
  147. package/src/lib/repos/store/contract.ts +0 -64
  148. package/src/lib/repos/store/hash.ts +0 -191
  149. package/src/lib/repos/store/recipe-store.ts +0 -599
  150. package/src/lib/repos/store/settings.ts +0 -58
  151. package/src/lib/repos/subscription-service.ts +0 -3859
  152. package/src/lib/repos/trust.ts +0 -447
  153. package/src/lib/repos/update-plan.ts +0 -235
  154. package/src/lib/self-update/index.ts +0 -14
  155. package/src/lib/self-update/installs.ts +0 -413
  156. package/src/lib/self-update/managers.ts +0 -126
  157. package/src/lib/self-update/plan.ts +0 -315
  158. package/src/lib/self-update/registry.ts +0 -99
  159. package/src/lib/self-update/run.ts +0 -506
  160. package/src/lib/self-update/versions.ts +0 -243
  161. package/src/lib/settings.ts +0 -1107
  162. package/src/lib/sous-home.ts +0 -104
  163. package/src/lib/state.ts +0 -167
  164. package/src/lib/vars/answers.ts +0 -192
  165. package/src/lib/vars/ask.ts +0 -1152
  166. package/src/lib/vars/definition-source.ts +0 -261
  167. package/src/lib/vars/display.ts +0 -231
  168. package/src/lib/vars/index.ts +0 -20
  169. package/src/lib/vars/ladder.ts +0 -282
  170. package/src/lib/vars/mappings.ts +0 -260
  171. package/src/lib/vars/names.ts +0 -94
  172. package/src/lib/vars/preanswers.ts +0 -395
  173. package/src/lib/vars/question-plan.ts +0 -241
  174. package/src/lib/vars/report.ts +0 -228
  175. package/src/lib/vars/safe-regex.ts +0 -235
  176. package/src/lib/vars/secrets.ts +0 -131
  177. package/src/lib/vars/validate.ts +0 -312
  178. package/src/lib/version-report.ts +0 -90
  179. package/src/lib/watch-loop.ts +0 -148
  180. package/src/lib/watch-service.ts +0 -116
  181. package/src/services/ref-resolver/container.ts +0 -107
  182. package/src/services/ref-resolver/dependency.ts +0 -60
  183. package/src/services/ref-resolver/format.ts +0 -197
  184. package/src/services/ref-resolver/glob.ts +0 -115
  185. package/src/services/ref-resolver/hash-names.ts +0 -155
  186. package/src/services/ref-resolver/index.ts +0 -55
  187. package/src/services/ref-resolver/injectable.ts +0 -36
  188. package/src/services/ref-resolver/location.ts +0 -62
  189. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
  190. package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
  191. package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
  192. package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
  193. package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
  194. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
  195. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
  196. package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
  197. package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
  198. package/src/services/ref-resolver/named-refs.ts +0 -144
  199. package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
  200. package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
  201. package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
  202. package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
  203. package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
  204. package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
  205. package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
  206. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
  207. package/src/services/ref-resolver/parts.ts +0 -115
  208. package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
  209. package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
  210. package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
  211. package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
  212. package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
  213. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
  214. package/src/services/ref-resolver/ref-helpers.ts +0 -62
  215. package/src/services/ref-resolver/ref-picker-service.ts +0 -204
  216. package/src/services/ref-resolver/ref-report.ts +0 -84
  217. package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
  218. package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
  219. package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
  220. package/src/services/ref-resolver/source.ts +0 -48
  221. package/src/services/ref-resolver/tokens.ts +0 -25
  222. package/src/services/ref-resolver/types.ts +0 -155
  223. package/src/templating/filters/bullet-list.ts +0 -9
  224. package/src/templating/filters/index.ts +0 -8
  225. package/src/templating/init-liquid-engine.ts +0 -139
  226. package/src/templating/lib/glob-files.ts +0 -74
  227. package/src/templating/lib/import-export.ts +0 -32
  228. package/src/templating/lib/secret-scope.ts +0 -41
  229. package/src/templating/lib/tag-args.ts +0 -19
  230. package/src/templating/tags/exportScalarVarsJs.ts +0 -46
  231. package/src/templating/tags/getFiles.ts +0 -89
  232. package/src/templating/tags/index.ts +0 -14
  233. package/src/templating/tags/listFiles.ts +0 -54
  234. package/src/templating/tags/showVars.ts +0 -28
  235. package/src/utils/choice-prompt.ts +0 -143
  236. package/src/utils/command-errors.ts +0 -186
  237. package/src/utils/command-help.ts +0 -45
  238. package/src/utils/confirm-prompt.ts +0 -110
  239. package/src/utils/flags.ts +0 -177
  240. package/src/utils/formatting.ts +0 -849
  241. package/src/utils/prompts.ts +0 -53
  242. package/src/utils/sous-directory.ts +0 -245
  243. package/src/utils/table.ts +0 -603
  244. package/src/utils/value-prompt.ts +0 -119
@@ -1,214 +0,0 @@
1
- /**
2
- * Which recipes take proposed changes, and which paths a change touches.
3
- *
4
- * A repository says which of its recipes do not take proposals with a
5
- * `submissions` block, on its repo manifest (covering every recipe) or on a
6
- * recipe manifest (covering that recipe, and winning over the repository's).
7
- * The motivating case is a recipe whose files are copied in from somewhere
8
- * else: a merged edit to the copy is overwritten by the next copy, and a
9
- * version it tagged can collide with the one the real source publishes.
10
- *
11
- * Two commands read it, with different strengths. `sous repo submit` warns,
12
- * prints where to go instead, and proposes anyway if the contributor carries
13
- * on: informed consent, not prevention. `sous repo release --check` fails a
14
- * pull request that changes such a recipe, because a pull request can be opened
15
- * without `submit`, and the check is the one gate every change passes.
16
- *
17
- * Nothing here decides a comparison base; the callers hand in the paths a
18
- * change touched, as git reported them.
19
- */
20
-
21
- import path from "node:path";
22
- import semver from "semver";
23
- import { runGit, type RunOptions } from "../providers/git.js";
24
- import { defaultBranch, forkPoint, pathsChangedSince } from "./git-state.js";
25
- import { listRecipeTags, recipeTagKey } from "./tags.js";
26
- import type { RepoValidation, ValidatedRecipe } from "./validate.js";
27
-
28
- /** Whether one recipe takes proposals, and which manifest said so. */
29
- export type SubmissionPolicy = {
30
- /** True when proposed changes to the recipe are accepted. */
31
- allowed: boolean;
32
- /** Where to send a change instead, when the manifest says. */
33
- instead?: string;
34
- /** Which manifest decided: the recipe's own, the repository's, or neither. */
35
- declaredBy: "recipe" | "repository" | "default";
36
- };
37
-
38
- /** A recipe a change touches although it does not take proposals. */
39
- export type RefusingRecipe = {
40
- /** The recipe key, `namespace/name`. */
41
- key: string;
42
- /** The recipe folder, relative to the repository root. */
43
- path: string;
44
- /** Where to send the change instead, when the manifest says. */
45
- instead?: string;
46
- /** Which manifest said the recipe takes no proposals. */
47
- declaredBy: "recipe" | "repository";
48
- /** The changed paths inside the recipe folder, relative to the repository root. */
49
- changed: string[];
50
- };
51
-
52
- /**
53
- * Whether one recipe takes proposals. A recipe's own block wins over the
54
- * repository's; with neither, it does.
55
- *
56
- * submissionPolicy(validation, recipe);
57
- * // -> { allowed: false, instead: "Propose it upstream.", declaredBy: "recipe" }
58
- *
59
- * @param validation - The validated repository.
60
- * @param recipe - One of its recipes.
61
- */
62
- export function submissionPolicy(
63
- validation: RepoValidation,
64
- recipe: ValidatedRecipe
65
- ): SubmissionPolicy {
66
- const own = recipe.manifest.submissions;
67
- if (own !== undefined) {
68
- return {
69
- allowed: own.allowed,
70
- ...(own.instead === undefined ? {} : { instead: own.instead }),
71
- declaredBy: "recipe",
72
- };
73
- }
74
- const repo = validation.manifest.submissions;
75
- if (repo !== undefined) {
76
- return {
77
- allowed: repo.allowed,
78
- ...(repo.instead === undefined ? {} : { instead: repo.instead }),
79
- declaredBy: "repository",
80
- };
81
- }
82
- return { allowed: true, declaredBy: "default" };
83
- }
84
-
85
- /**
86
- * The recipes a change touches that do not take proposals, in the order the
87
- * repo manifest lists them. A path is inside a recipe when it lies under the
88
- * recipe folder.
89
- *
90
- * recipesRefusingSubmissions(validation, ["recipes/core/x/a.md"]);
91
- * // -> [{ key: "core/x", path: "recipes/core/x", changed: ["recipes/core/x/a.md"], ... }]
92
- *
93
- * @param validation - The validated repository.
94
- * @param changedPaths - The paths the change touched, relative to the repository root.
95
- */
96
- export function recipesRefusingSubmissions(
97
- validation: RepoValidation,
98
- changedPaths: ReadonlyArray<string>
99
- ): RefusingRecipe[] {
100
- const refusing: RefusingRecipe[] = [];
101
- for (const recipe of validation.recipes) {
102
- const policy = submissionPolicy(validation, recipe);
103
- if (policy.allowed || policy.declaredBy === "default") continue;
104
-
105
- const changed = pathsInside(recipe.path, changedPaths);
106
- if (changed.length === 0) continue;
107
-
108
- refusing.push({
109
- key: recipe.key,
110
- path: toPosix(recipe.path),
111
- ...(policy.instead === undefined ? {} : { instead: policy.instead }),
112
- declaredBy: policy.declaredBy,
113
- changed,
114
- });
115
- }
116
- return refusing;
117
- }
118
-
119
- /**
120
- * The paths from a list that lie inside a folder, both relative to the
121
- * repository root.
122
- *
123
- * pathsInside("recipes/core/x", ["recipes/core/x/a.md", "recipes/core/xy/b.md"]);
124
- * // -> ["recipes/core/x/a.md"]
125
- *
126
- * @param folder - The folder, relative to the repository root.
127
- * @param paths - The paths to test, relative to the repository root.
128
- */
129
- export function pathsInside(folder: string, paths: ReadonlyArray<string>): string[] {
130
- const base = toPosix(path.posix.normalize(toPosix(folder))).replace(/\/+$/, "");
131
- return paths
132
- .map(toPosix)
133
- .filter((entry) => base === "." || entry === base || entry.startsWith(`${base}/`));
134
- }
135
-
136
- /** What the pull request check found about recipes that take no proposals. */
137
- export type SubmissionsCheck = {
138
- /** The recipes the change touches although they take no proposals. */
139
- refusing: RefusingRecipe[];
140
- /**
141
- * What the change was compared with: the default branch it will merge into,
142
- * each recipe's last release tag when the checkout holds no copy of that
143
- * branch, or nothing at all when neither was there to compare with. `none
144
- * declined` means no recipe declines proposals, so there was nothing to check.
145
- */
146
- comparedWith:
147
- | { kind: "branch"; branch: string }
148
- | { kind: "tags" }
149
- | { kind: "nothing" }
150
- | { kind: "none declined" };
151
- };
152
-
153
- /**
154
- * The check `sous repo release --check` makes for a pull request: which recipes
155
- * that take no proposals the change touches.
156
- *
157
- * The change is what the checked-out commit holds beyond the point it shares
158
- * with the default branch on `origin`, which is exactly what a pull request
159
- * proposes. A checkout with no copy of that branch is compared recipe by recipe
160
- * with each one's last release tag instead: on the default branch a recipe
161
- * always equals its last tag, so a difference is a change nobody released.
162
- * A recipe that was never tagged has nothing to compare with and is left out.
163
- *
164
- * @param validation - The validated repository.
165
- * @param options - The command runner to use.
166
- */
167
- export async function checkSubmissions(
168
- validation: RepoValidation,
169
- options: RunOptions = {}
170
- ): Promise<SubmissionsCheck> {
171
- const { rootDir } = validation;
172
- const guarded = validation.recipes.filter((recipe) => {
173
- const policy = submissionPolicy(validation, recipe);
174
- return !policy.allowed && policy.declaredBy !== "default";
175
- });
176
- if (guarded.length === 0) return { refusing: [], comparedWith: { kind: "none declined" } };
177
-
178
- const branch = (await defaultBranch(rootDir, options)) ?? "main";
179
- const since = await forkPoint(rootDir, "origin", branch, options);
180
- if (since !== undefined) {
181
- const changed = await pathsChangedSince(rootDir, since, options);
182
- return {
183
- refusing: recipesRefusingSubmissions(validation, changed),
184
- comparedWith: { kind: "branch", branch },
185
- };
186
- }
187
-
188
- const tags = await listRecipeTags(rootDir, options);
189
- const changed: string[] = [];
190
- let comparedAny = false;
191
- for (const recipe of guarded) {
192
- const versions = tags
193
- .filter((tag) => recipeTagKey(tag) === recipe.key && semver.valid(tag.version) !== null)
194
- .sort((left, right) => semver.rcompare(left.version, right.version));
195
- const last = versions[0];
196
- if (last === undefined) continue;
197
- comparedAny = true;
198
- const diff = await runGit(
199
- ["diff", "--name-only", last.tag, "HEAD", "--", toPosix(recipe.path)],
200
- { cwd: rootDir, run: options.run }
201
- );
202
- if (diff.length > 0) changed.push(...diff.split("\n").filter((line) => line.length > 0));
203
- }
204
-
205
- return {
206
- refusing: recipesRefusingSubmissions(validation, changed),
207
- comparedWith: comparedAny ? { kind: "tags" } : { kind: "nothing" },
208
- };
209
- }
210
-
211
- /** Rewrites a path with forward slashes, which is how git reports them. */
212
- function toPosix(value: string): string {
213
- return value.split(path.sep).join("/");
214
- }
@@ -1,290 +0,0 @@
1
- /**
2
- * Which working copy `sous repo submit` proposes from.
3
- *
4
- * Inside a recipe repository with no argument, it is that repository, as it
5
- * always was. Inside a project, the argument names a repository the project
6
- * knows (resolved through `src/services/ref-resolver/`, like every reference), and the
7
- * submission runs in that repository's checkout:
8
- *
9
- * - a linked repository is read from the checkout its link points at;
10
- * - a repository no longer linked, whose checkout sous cloned is still where
11
- * `sous repo link` puts one, is submitted from that checkout, with a note
12
- * saying it is not linked here;
13
- * - a repository with no checkout at all cannot be submitted from, and the
14
- * error says there is no working copy to propose from.
15
- *
16
- * With no argument inside a project, exactly one linked repository is used and
17
- * named; several are a question, which a run with no terminal cannot ask.
18
- *
19
- * Nothing here writes anything. It reads the links maps and looks at the disk.
20
- */
21
-
22
- import fs from "node:fs";
23
- import path from "node:path";
24
- import { ConfigError } from "../../errors.js";
25
- import { nonInteractiveError } from "../../interactive.js";
26
- import {
27
- CatalogLookup,
28
- RefPickArguments,
29
- RefResolveArguments,
30
- locationFromUrl,
31
- repoOf,
32
- sharedRefPicker,
33
- sharedRefResolver,
34
- type CatalogRepo,
35
- } from "../../../services/ref-resolver/index.js";
36
- import type { RepoEntry } from "../../settings.js";
37
- import { isGitCheckout, remoteUrlOf, repoSlugFromUrl, sameRemote } from "../git-clone.js";
38
- import { globalReposDir, projectReposDir, readEffectiveLinks } from "../links.js";
39
- import { askChoice } from "../../../utils/prompts.js";
40
- import { findRepoRoot } from "./validate.js";
41
-
42
- /** The project a submission may be run from, when one was found. */
43
- export type SubmitProject = {
44
- /** The project's discovered `.sous/` directory. */
45
- sousDir: string;
46
- /** The repositories the project uses, keyed by short name. */
47
- repos: Record<string, RepoEntry>;
48
- };
49
-
50
- /** What `findSubmitCheckout` needs to know. */
51
- export type FindSubmitCheckoutOptions = {
52
- /** The working directory the command was run from. */
53
- cwd: string;
54
- /** The repository the command line named, when it named one. */
55
- repo?: string;
56
- /** The project around the working directory, when there is one. */
57
- project?: SubmitProject;
58
- /** Whether a question may be asked. */
59
- interactive: boolean;
60
- /** The environment, for the machine-wide links map and checkouts. */
61
- env?: NodeJS.ProcessEnv;
62
- /** How a choice between several linked repositories is asked. */
63
- choose?: (message: string, names: string[]) => Promise<string>;
64
- /** Where the facts about a resolved reference are written. */
65
- write?: (message: string) => void;
66
- };
67
-
68
- /** The working copy a submission runs in, and how it was chosen. */
69
- export type SubmitCheckout = {
70
- /** The checkout's root directory. */
71
- rootDir: string;
72
- /** The project's short name for the repository, when a project named it. */
73
- repo?: string;
74
- /** How the checkout was found, in one sentence, ready to print. */
75
- reason: string;
76
- /** Anything else worth saying about it, such as a checkout that is no longer linked. */
77
- notes: string[];
78
- };
79
-
80
- /**
81
- * Works out which checkout a submission runs in.
82
- *
83
- * @param options - The working directory, the argument, the project and the testing seams.
84
- */
85
- export async function findSubmitCheckout(
86
- options: FindSubmitCheckoutOptions
87
- ): Promise<SubmitCheckout> {
88
- const { cwd, project } = options;
89
- const env = options.env ?? process.env;
90
-
91
- if (options.repo === undefined) {
92
- const inside = recipeRepoAround(cwd);
93
- if (inside !== undefined) {
94
- return { rootDir: inside, reason: "The working directory is inside it.", notes: [] };
95
- }
96
- if (project === undefined) {
97
- // Neither a recipe repository nor a project: the recipe repository error
98
- // is the one that says what to do.
99
- findRepoRoot(cwd);
100
- }
101
- return chooseLinked(project!, env, options);
102
- }
103
-
104
- if (project === undefined) {
105
- throw new ConfigError(
106
- `'${options.repo}' names a repository a project uses, but ${path.resolve(cwd)} is not ` +
107
- `inside a sous project.\n` +
108
- ` Run the command from inside the project that links the repository, or from inside ` +
109
- `the repository's own checkout with no argument.`
110
- );
111
- }
112
-
113
- const links = readEffectiveLinks(project.sousDir, env);
114
- const reference = referenceRepos(project, links);
115
- const { refs } = await sharedRefResolver().resolve(
116
- new RefResolveArguments({
117
- input: options.repo,
118
- lookup: new CatalogLookup(reference),
119
- kinds: ["repo"],
120
- refusedIsEmpty: true,
121
- })
122
- );
123
- const match = await sharedRefPicker().pick(
124
- refs,
125
- new RefPickArguments({
126
- search: options.repo,
127
- interactive: options.interactive,
128
- ...(options.write === undefined ? {} : { write: options.write }),
129
- details: [
130
- reference.length === 0
131
- ? " This project uses no repositories."
132
- : ` This project uses: ${reference.map((entry) => entry.name).join(", ")}.`,
133
- ],
134
- })
135
- );
136
- const name = repoOf(match)!.name!;
137
-
138
- const link = links[name];
139
- if (link !== undefined) {
140
- if (!fs.existsSync(link.path)) {
141
- throw noWorkingCopy(name, `Its link points at ${link.path}, which does not exist.`);
142
- }
143
- return {
144
- rootDir: link.path,
145
- repo: name,
146
- reason: `'${name}' is linked to this checkout.`,
147
- notes: [],
148
- };
149
- }
150
-
151
- const leftover = leftoverCheckout(name, project, env);
152
- if (leftover !== undefined) {
153
- return {
154
- rootDir: leftover,
155
- repo: name,
156
- reason: `Sous cloned '${name}' into this checkout earlier.`,
157
- notes: [
158
- `'${name}' is not linked in this project, so builds read its published versions; ` +
159
- `the checkout sous cloned for it is still at ${leftover}, and the change is ` +
160
- `proposed from there.`,
161
- ],
162
- };
163
- }
164
-
165
- throw noWorkingCopy(
166
- name,
167
- `It is not linked in this project, and no checkout of it is where 'sous repo link' ` +
168
- `clones one.`
169
- );
170
- }
171
-
172
- /** The recipe repository around a directory, or undefined when there is none. */
173
- function recipeRepoAround(cwd: string): string | undefined {
174
- try {
175
- return findRepoRoot(cwd);
176
- } catch {
177
- return undefined;
178
- }
179
- }
180
-
181
- /**
182
- * With no argument inside a project: the one linked repository, or a choice
183
- * between several.
184
- */
185
- async function chooseLinked(
186
- project: SubmitProject,
187
- env: NodeJS.ProcessEnv,
188
- options: FindSubmitCheckoutOptions
189
- ): Promise<SubmitCheckout> {
190
- const links = readEffectiveLinks(project.sousDir, env);
191
- const names = Object.keys(links).sort();
192
-
193
- if (names.length === 0) {
194
- throw new ConfigError(
195
- "This project links no repository, so there is no working copy to propose from.\n" +
196
- " Link one with 'sous repo link <repo>', make your change in its checkout, then run " +
197
- "'sous repo submit <repo>'."
198
- );
199
- }
200
-
201
- let name: string;
202
- let reason: string;
203
- if (names.length === 1) {
204
- name = names[0]!;
205
- reason = `It is the only repository this project links.`;
206
- } else {
207
- if (!options.interactive) {
208
- throw nonInteractiveError({
209
- prompt: "which linked repository to propose a change from",
210
- remedy: `name it as the argument, as in 'sous repo submit ${names[0]}'.`,
211
- details: [`This project links ${names.join(", ")}.`],
212
- });
213
- }
214
- const choose =
215
- options.choose ??
216
- ((message: string, offered: string[]) =>
217
- askChoice(
218
- message,
219
- offered.map((entry) => ({ name: `${entry} ${links[entry]!.path}`, value: entry }))
220
- ));
221
- name = await choose("Which linked repository should the change be proposed from?", names);
222
- reason = "You chose it.";
223
- }
224
-
225
- const link = links[name]!;
226
- if (!fs.existsSync(link.path)) {
227
- throw noWorkingCopy(name, `Its link points at ${link.path}, which does not exist.`);
228
- }
229
- return { rootDir: link.path, repo: name, reason, notes: [] };
230
- }
231
-
232
- /**
233
- * The repositories a reference may name here: every one the project uses, and
234
- * any linked name the config does not list, so a link is never unreachable.
235
- */
236
- function referenceRepos(
237
- project: SubmitProject,
238
- links: Record<string, { path: string }>
239
- ): CatalogRepo[] {
240
- const known = (name: string, url: string): CatalogRepo => {
241
- const location = locationFromUrl(url);
242
- return { name, ...(location === undefined ? {} : { location }), namespaces: [], recipes: [] };
243
- };
244
- const repos: CatalogRepo[] = Object.entries(project.repos).map(([name, entry]) =>
245
- known(name, entry.url)
246
- );
247
- for (const name of Object.keys(links).sort()) {
248
- if (project.repos[name] === undefined) repos.push(known(name, links[name]!.path));
249
- }
250
- return repos;
251
- }
252
-
253
- /**
254
- * A checkout sous cloned for a repository that is no longer linked: the
255
- * project's own clone first, then the machine-wide one, each only when it is a
256
- * checkout of that same repository.
257
- */
258
- function leftoverCheckout(
259
- name: string,
260
- project: SubmitProject,
261
- env: NodeJS.ProcessEnv
262
- ): string | undefined {
263
- const url = project.repos[name]?.url;
264
- if (url === undefined) return undefined;
265
-
266
- let slug: { owner: string; name: string };
267
- try {
268
- slug = repoSlugFromUrl(url);
269
- } catch {
270
- return undefined;
271
- }
272
-
273
- for (const base of [projectReposDir(project.sousDir), globalReposDir(env)]) {
274
- const directory = path.join(base, slug.owner, slug.name);
275
- if (!isGitCheckout(directory)) continue;
276
- const remote = remoteUrlOf(directory);
277
- if (remote !== undefined && sameRemote(remote, url)) return directory;
278
- }
279
- return undefined;
280
- }
281
-
282
- /** The error for a repository with no working copy to propose from. */
283
- function noWorkingCopy(name: string, why: string): ConfigError {
284
- return new ConfigError(
285
- `There is no working copy of '${name}' to propose a change from.\n` +
286
- ` ${why}\n` +
287
- ` Link it with 'sous repo link ${name}', make your change in its checkout, then run ` +
288
- `the command again.`
289
- );
290
- }
@@ -1,153 +0,0 @@
1
- /**
2
- * How `sous repo submit` asks its questions.
3
- *
4
- * The sequencer (`submit-service.ts`) never prompts; it calls a
5
- * `SubmitQuestions` and carries on with the answer. This module builds that
6
- * object from the two facts a command knows: whether a question may be asked
7
- * at all (`src/lib/interactive.ts` decides), and whether the shared
8
- * confirmation flag already answered every yes-or-no question.
9
- *
10
- * A question nobody can ask raises the error that names the flag answering it,
11
- * so a script learns what to pass rather than hanging on a prompt it cannot see:
12
- * the title and description name `--title` and `--body` together, and every
13
- * confirmation names `--yes`.
14
- */
15
-
16
- import { editor } from "@inquirer/prompts";
17
- import { nonInteractiveError } from "../../interactive.js";
18
- import { blankLines, keysHelpTip, log } from "../../../utils/formatting.js";
19
- import { askChoice, askYesNo } from "../../../utils/prompts.js";
20
- import { valuePrompt } from "../../../utils/value-prompt.js";
21
- import type { ProposalSummary } from "../providers/provider.js";
22
- import type { ChangedPath } from "./git-state.js";
23
- import type { NextBranchChoice, SubmitQuestions } from "./submit-service.js";
24
-
25
- /** The prompts the questions are asked with; each one is replaceable in a test. */
26
- export type SubmitPrompts = {
27
- /** Asks for one line of text, with an editor behind Tab for a longer answer. */
28
- text(message: string): Promise<string>;
29
- /** Asks a yes-or-no question. */
30
- confirm(message: string): Promise<boolean>;
31
- /** Asks the contributor to pick one of several choices. */
32
- choose<T>(message: string, choices: Array<{ name: string; value: T }>): Promise<T>;
33
- };
34
-
35
- /** What `submitQuestions` needs to know. */
36
- export type SubmitQuestionOptions = {
37
- /** Whether a question may be asked. */
38
- interactive: boolean;
39
- /** True when the confirmation flag answered every yes-or-no question already. */
40
- yes: boolean;
41
- /** How the questions are put. Defaults to the shared terminal prompts. */
42
- prompts?: SubmitPrompts;
43
- /** Where a list shown before a question is written. Defaults to the console. */
44
- write?: (line: string) => void;
45
- };
46
-
47
- /** The flag spellings every confirmation's remedy names. */
48
- const YES_REMEDY = "pass '--yes' (spelled '-y' or '--force' if you prefer)";
49
-
50
- /** The remedy for a missing title or description, naming both flags. */
51
- const TEXT_REMEDY =
52
- "pass '--title' and '--body'. A new proposal, and a commit sous makes for you, both need " +
53
- "a title and a description written by you.";
54
-
55
- /**
56
- * Builds the questions a submission asks, bound to this run's terminal and
57
- * flags.
58
- *
59
- * @param options - Whether asking is possible, whether `--yes` was passed, and the prompts.
60
- */
61
- export function submitQuestions(options: SubmitQuestionOptions): SubmitQuestions {
62
- const prompts = options.prompts ?? terminalPrompts;
63
- const write = options.write ?? log;
64
- const { interactive, yes } = options;
65
-
66
- const askText = async (message: string): Promise<string> => {
67
- if (!interactive) {
68
- throw nonInteractiveError({
69
- prompt: "for the proposal's title and description",
70
- remedy: TEXT_REMEDY,
71
- });
72
- }
73
- return prompts.text(message);
74
- };
75
-
76
- return {
77
- title: () => askText("What is the title of this proposal?"),
78
- body: () => askText("Describe the change: what it does, and why."),
79
-
80
- async confirmCommit(paths: ReadonlyArray<ChangedPath>): Promise<boolean> {
81
- if (yes) return true;
82
- write(" These paths are uncommitted, and '--commit' would commit all of them:");
83
- for (const entry of paths) write(` ${entry.path}`);
84
- if (!interactive) {
85
- throw nonInteractiveError({
86
- prompt: "whether to commit the paths listed above",
87
- remedy: `${YES_REMEDY} to commit them without being asked.`,
88
- });
89
- }
90
- return prompts.confirm("Commit these paths?");
91
- },
92
-
93
- async proceedDespiteSubmissions(): Promise<boolean> {
94
- if (yes) return true;
95
- if (!interactive) {
96
- throw nonInteractiveError({
97
- prompt: "whether to propose a change to recipes that do not take proposals",
98
- remedy: `${YES_REMEDY} to propose it anyway.`,
99
- });
100
- }
101
- return prompts.confirm("Propose the change anyway?");
102
- },
103
-
104
- async nextBranch(merged: ProposalSummary, generated: string): Promise<NextBranchChoice> {
105
- if (yes) return { kind: "generate" };
106
- if (!interactive) {
107
- throw nonInteractiveError({
108
- prompt: "which new branch to continue on, now the proposal was merged",
109
- remedy: `${YES_REMEDY} to continue on a branch sous names for you.`,
110
- details: [`The merged proposal was '${merged.title}'.`],
111
- });
112
- }
113
- const picked = await prompts.choose<"name" | "generate" | "cancel">(
114
- "Continue on a new branch?",
115
- [
116
- { name: `Generate one: ${generated}`, value: "generate" },
117
- { name: "Name the branch myself", value: "name" },
118
- { name: "Cancel", value: "cancel" },
119
- ]
120
- );
121
- if (picked !== "name") return { kind: picked };
122
- const name = (await prompts.text("What should the new branch be called?")).trim();
123
- return name.length === 0 ? { kind: "generate" } : { kind: "name", name };
124
- },
125
- };
126
- }
127
-
128
- /**
129
- * The prompts a real terminal gets. A text question answers on Enter; Tab
130
- * opens the contributor's editor instead, which is where a description longer
131
- * than one line is written.
132
- */
133
- const terminalPrompts: SubmitPrompts = {
134
- async text(message: string): Promise<string> {
135
- blankLines(2);
136
- for (;;) {
137
- const answered = await valuePrompt({
138
- message,
139
- hint: keysHelpTip([
140
- ["⏎", "answer"],
141
- ["⇥", "advanced"],
142
- ]),
143
- validate: (value) => (value.trim().length > 0 ? true : "An answer is required."),
144
- });
145
- if (answered.kind === "value") return answered.value;
146
- const written = await editor({ message, waitForUserInput: false });
147
- if (written.trim().length > 0) return written;
148
- }
149
- },
150
- confirm: (message: string) => askYesNo(message),
151
- choose: <T>(message: string, choices: Array<{ name: string; value: T }>) =>
152
- askChoice(message, choices),
153
- };