@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,589 +0,0 @@
1
- /**
2
- * The GitHub provider: the read path, and the write path behind it.
3
- *
4
- * Reading a repo needs no GitHub API and no `gh`: the index file is one raw
5
- * HTTPS GET, and a recipe's subtree comes from git itself. A token is used when
6
- * one is available, so private repositories work; it is read from GITHUB_TOKEN,
7
- * or asked of the `gh` command line tool when that is installed and signed in.
8
- * A missing `gh` is never an error on the read path.
9
- *
10
- * The write path is where `gh` becomes load-bearing, and this file is the ONLY
11
- * place in sous that knows the command exists. Proposing a change is a pull
12
- * request opened by `gh pr create`, a contributor without push permission works
13
- * through a fork made by `gh repo fork`, and both are reported back as plain
14
- * data, so the service that sequences them never learns a GitHub-shaped fact.
15
- * Finding a pull request again, reporting where it stands and replacing its
16
- * text go through `gh pr list`, `gh pr view` and `gh pr edit` the same way.
17
- */
18
-
19
- import { ConfigError } from "../../errors.js";
20
- import { INDEX_FILENAME } from "../formats/common.js";
21
- import { ProviderBase, firstUrlIn } from "./base.js";
22
- import { fetchSubtree, type CommandRunner } from "./git.js";
23
- import { fetchText, type FetchLike } from "./http.js";
24
- import {
25
- buildCanonicalRepo,
26
- invalidRepoUrl,
27
- readingsAfterRepository,
28
- splitRepoUrl,
29
- withoutGitSuffix,
30
- type LocationReading,
31
- type WrittenLocation,
32
- type AuthStatus,
33
- type CanonicalRepo,
34
- type ChangeProposal,
35
- type FetchedIndex,
36
- type ForkedRepo,
37
- type ProposalChecks,
38
- type ProposalQuery,
39
- type ProposalReview,
40
- type ProposalState,
41
- type ProposalStatus,
42
- type ProposalSummary,
43
- type ProposalUpdate,
44
- type ProposedChange,
45
- type ProviderCli,
46
- type ProviderFeature,
47
- type ProviderOptions,
48
- } from "./provider.js";
49
-
50
- /** The host this provider serves when a URL does not say otherwise. */
51
- export const GITHUB_HOST = "github.com";
52
-
53
- /** The environment variable a GitHub token is read from. */
54
- export const GITHUB_TOKEN_ENV = "GITHUB_TOKEN";
55
-
56
- /**
57
- * Finds a GitHub token: the environment first, then `gh auth token` when the
58
- * `gh` command line tool is installed and signed in. Returns undefined when
59
- * there is none, because public repositories need no token at all.
60
- *
61
- * @param options - Environment and subprocess runner overrides.
62
- */
63
- export async function findGithubToken(options: {
64
- env?: NodeJS.ProcessEnv;
65
- run?: CommandRunner;
66
- } = {}): Promise<string | undefined> {
67
- return new GithubProvider().token(options);
68
- }
69
-
70
- /** The GitHub provider. */
71
- export class GithubProvider extends ProviderBase {
72
- readonly id = "github" as const;
73
-
74
- /**
75
- * Reads the index and recipe subtrees, proposes a change through the GitHub
76
- * CLI ('gh'), and finds, reports on and updates that pull request afterwards.
77
- */
78
- readonly features: ProviderFeature[] = ["fetch", "submit", "proposals"];
79
-
80
- /** The command line tool the write path is built on. */
81
- readonly cli: ProviderCli = {
82
- command: "gh",
83
- label: "the GitHub CLI",
84
- install: "https://cli.github.com",
85
- };
86
-
87
- /** What GitHub calls a proposal. */
88
- readonly proposalNoun = "pull request";
89
-
90
- /** The host a `github://` locator means when it names none. */
91
- readonly defaultHost = GITHUB_HOST;
92
-
93
- /**
94
- * A GitHub repository is always an owner and a name, so the first two
95
- * segments are the repository and everything after them names something
96
- * inside it: a namespace, a recipe, or a browser path after `tree/` or
97
- * `blob/`.
98
- *
99
- * @param location - The host and the segments after it.
100
- */
101
- readLocation(location: WrittenLocation): LocationReading[] {
102
- const { segments } = location;
103
- if (segments.length < 2) return [];
104
- const repoPath = `${segments[0]}/${withoutGitSuffix(segments[1]!)}`;
105
- return readingsAfterRepository(repoPath, segments.slice(2));
106
- }
107
-
108
- matches(url: string): boolean {
109
- const parts = splitRepoUrl(url);
110
- return parts !== undefined && parts.host === GITHUB_HOST;
111
- }
112
-
113
- canonicalize(url: string): CanonicalRepo {
114
- const parts = splitRepoUrl(url);
115
- if (parts === undefined) throw invalidRepoUrl(this.id, url);
116
- return buildCanonicalRepo(parts.host, parts.owner, parts.name);
117
- }
118
-
119
- /**
120
- * A GitHub token, from the environment or from `gh`. Public, because the
121
- * index cache and the exported `findGithubToken` both ask for one.
122
- *
123
- * @param options - Environment and subprocess runner overrides.
124
- */
125
- async token(options: ProviderOptions = {}): Promise<string | undefined> {
126
- return this.findToken(GITHUB_TOKEN_ENV, ["auth", "token"], options);
127
- }
128
-
129
- /**
130
- * Fetches the repo's index file from the raw content host at the repository's
131
- * default branch, which is what `HEAD` names there.
132
- *
133
- * @param repo - The canonicalized repository.
134
- * @param options - Environment, fetch and subprocess overrides.
135
- */
136
- async fetchIndex(
137
- repo: CanonicalRepo,
138
- options: ProviderOptions = {}
139
- ): Promise<FetchedIndex> {
140
- const token = await this.token(options);
141
- const url = this.indexUrl(repo);
142
- const fetched = await fetchText(url, {
143
- ...(token === undefined ? {} : { token }),
144
- ...(options.fetchImpl === undefined
145
- ? {}
146
- : { fetchImpl: options.fetchImpl as FetchLike }),
147
- ...(options.signal === undefined ? {} : { signal: options.signal }),
148
- label: "repo index",
149
- });
150
-
151
- return fetched.etag === undefined
152
- ? { text: fetched.text, ref: "HEAD" }
153
- : { text: fetched.text, ref: "HEAD", etag: fetched.etag };
154
- }
155
-
156
- /**
157
- * Fetches one recipe folder at one tag. Private repositories work through
158
- * git's own credential helpers, the same way a manual clone would.
159
- *
160
- * @param repo - The canonicalized repository.
161
- * @param recipePath - The recipe folder, relative to the repository root.
162
- * @param tag - The git tag carrying the version.
163
- * @param destDir - Where the recipe's files should end up.
164
- * @param options - Subprocess runner override.
165
- */
166
- async fetchRecipeTree(
167
- repo: CanonicalRepo,
168
- recipePath: string,
169
- tag: string,
170
- destDir: string,
171
- options: ProviderOptions = {}
172
- ): Promise<void> {
173
- await fetchSubtree({
174
- cloneUrl: repo.httpsUrl,
175
- tag,
176
- subPath: recipePath,
177
- destDir,
178
- ...(options.run === undefined ? {} : { run: options.run }),
179
- });
180
- }
181
-
182
- /**
183
- * The raw URL of a repository's index file at its default branch.
184
- *
185
- * @param repo - The canonicalized repository.
186
- */
187
- indexUrl(repo: CanonicalRepo): string {
188
- return `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/HEAD/${INDEX_FILENAME}`;
189
- }
190
-
191
- // --- The write path --------------------------------------------------------
192
-
193
- /**
194
- * Whether `gh` is installed and signed in. The detail is the whole
195
- * explanation, ready to print, because only this provider knows what to
196
- * install and which command signs in.
197
- *
198
- * @param options - Subprocess runner and working directory overrides.
199
- */
200
- async authStatus(options: ProviderOptions = {}): Promise<AuthStatus> {
201
- const ok = await this.commandSucceeds(this.cli.command, ["auth", "status"], options);
202
- if (ok) {
203
- return {
204
- ok: true,
205
- detail: `${this.cli.label} ('${this.cli.command}') is installed and signed in.`,
206
- };
207
- }
208
- return {
209
- ok: false,
210
- detail:
211
- `Sous proposes a change through ${this.cli.label} ('${this.cli.command}'), and it is ` +
212
- `either not installed or not signed in.\n` +
213
- ` Install it from ${this.cli.install}, then run '${this.cli.command} auth login'.`,
214
- };
215
- }
216
-
217
- /**
218
- * Whether the signed-in contributor may push to the repository itself, as
219
- * GitHub reports it. Undefined when `gh` could not answer at all, which is
220
- * not the same as a refusal.
221
- *
222
- * @param repo - The canonicalized repository.
223
- * @param options - Subprocess runner and working directory overrides.
224
- */
225
- async canPush(
226
- repo: CanonicalRepo,
227
- options: ProviderOptions = {}
228
- ): Promise<boolean | undefined> {
229
- const answer = await this.capturedOutput(
230
- this.cli.command,
231
- ["api", `repos/${repo.owner}/${repo.name}`, "--jq", ".permissions.push"],
232
- options
233
- );
234
- if (answer === undefined) return undefined;
235
- const value = answer.trim();
236
- if (value === "true") return true;
237
- if (value === "false") return false;
238
- return undefined;
239
- }
240
-
241
- /**
242
- * Forks the repository onto the contributor's own account and says where the
243
- * fork landed. No remote is added here; that is git's business, and the
244
- * service above does it with the URLs returned.
245
- *
246
- * @param repo - The canonicalized repository.
247
- * @param options - Subprocess runner and working directory overrides.
248
- */
249
- async fork(repo: CanonicalRepo, options: ProviderOptions = {}): Promise<ForkedRepo> {
250
- const forked = await this.runCommand(
251
- this.cli.command,
252
- ["repo", "fork", `${repo.owner}/${repo.name}`, "--remote=false"],
253
- options
254
- );
255
- if (forked.code !== 0) {
256
- throw new ConfigError(
257
- `'${this.cli.command} repo fork' did not succeed.\n ` +
258
- `${forked.stderr.trim() || forked.stdout.trim()}`
259
- );
260
- }
261
-
262
- const who = await this.capturedOutput(
263
- this.cli.command,
264
- ["api", "user", "--jq", ".login"],
265
- options
266
- );
267
- if (who === undefined) {
268
- throw new ConfigError(
269
- "The fork was requested, but sous could not read your GitHub login from " +
270
- `'${this.cli.command} api user', so it does not know where the fork lives.`
271
- );
272
- }
273
-
274
- const owner = who.trim();
275
- return {
276
- owner,
277
- name: repo.name,
278
- httpsUrl: `https://${repo.host}/${owner}/${repo.name}.git`,
279
- sshUrl: `git@${repo.host}:${owner}/${repo.name}.git`,
280
- };
281
- }
282
-
283
- /**
284
- * Opens a pull request for a branch that has already been pushed. A proposal
285
- * carrying a head owner came from a fork, which is what a cross-repository
286
- * pull request spells as `owner:branch`.
287
- *
288
- * @param repo - The canonicalized repository the proposal targets.
289
- * @param proposal - The branch, the text and whether it is a draft.
290
- * @param options - Subprocess runner and working directory overrides.
291
- */
292
- async proposeChange(
293
- repo: CanonicalRepo,
294
- proposal: ChangeProposal,
295
- options: ProviderOptions = {}
296
- ): Promise<ProposedChange> {
297
- const head =
298
- proposal.head === undefined
299
- ? proposal.branch
300
- : `${proposal.head.owner}:${proposal.branch}`;
301
-
302
- const args = ["pr", "create", "--repo", `${repo.owner}/${repo.name}`];
303
- if (proposal.base !== undefined) args.push("--base", proposal.base);
304
- args.push("--head", head, "--title", proposal.title, "--body", proposal.body);
305
- if (proposal.draft) args.push("--draft");
306
-
307
- const result = await this.runCommand(this.cli.command, args, options);
308
- if (result.code !== 0) {
309
- const reported = result.stderr.trim() || result.stdout.trim();
310
- throw new ConfigError(
311
- `'${this.cli.command} pr create' did not succeed, so no proposal was opened.` +
312
- (reported.length === 0 ? "" : `\n ${reported}`)
313
- );
314
- }
315
-
316
- const url = firstUrlIn(result.stdout);
317
- if (url === undefined) {
318
- return {
319
- detail:
320
- `The ${this.proposalNoun} was opened, but '${this.cli.command}' printed no address ` +
321
- `for it.`,
322
- };
323
- }
324
- return { url, detail: `The ${this.proposalNoun} is at ${url}.` };
325
- }
326
-
327
- // --- Proposals after the fact ------------------------------------------------
328
-
329
- /**
330
- * The pull request a branch was pushed for. GitHub lists pull requests by
331
- * head branch name alone, so the list is narrowed here by where the branch
332
- * lives: the repository itself, or the contributor's fork. When a branch has
333
- * had several, the open one wins, and otherwise the newest.
334
- *
335
- * @param repo - The canonicalized repository the proposal targets.
336
- * @param query - The branch, and whether it lives on a fork.
337
- * @param options - Subprocess runner and working directory overrides.
338
- */
339
- async findProposal(
340
- repo: CanonicalRepo,
341
- query: ProposalQuery,
342
- options: ProviderOptions = {}
343
- ): Promise<ProposalSummary | undefined> {
344
- const forkOwner = query.fromFork
345
- ? (query.forkOwner ?? (await this.signedInLogin(options)))
346
- : undefined;
347
-
348
- const listed = await this.ghJson<GhPullRequest[]>(
349
- [
350
- "pr",
351
- "list",
352
- "--repo",
353
- `${repo.owner}/${repo.name}`,
354
- "--head",
355
- query.branch,
356
- "--state",
357
- "all",
358
- "--limit",
359
- "50",
360
- "--json",
361
- "number,url,state,title,isDraft,baseRefName,headRepositoryOwner,isCrossRepository",
362
- ],
363
- "pr list",
364
- options
365
- );
366
-
367
- const mine = listed.filter((entry) =>
368
- query.fromFork
369
- ? entry.isCrossRepository === true && entry.headRepositoryOwner?.login === forkOwner
370
- : entry.isCrossRepository !== true
371
- );
372
- if (mine.length === 0) return undefined;
373
-
374
- const open = mine.find((entry) => entry.state === "OPEN");
375
- const chosen = open ?? [...mine].sort((a, b) => b.number - a.number)[0]!;
376
- return summarizePullRequest(chosen);
377
- }
378
-
379
- /**
380
- * Where one pull request stands: its state, its review decision, and how
381
- * its checks are going, counted.
382
- *
383
- * @param repo - The canonicalized repository the proposal targets.
384
- * @param id - The pull request number.
385
- * @param options - Subprocess runner and working directory overrides.
386
- */
387
- async proposalStatus(
388
- repo: CanonicalRepo,
389
- id: string,
390
- options: ProviderOptions = {}
391
- ): Promise<ProposalStatus> {
392
- const viewed = await this.ghJson<GhPullRequest>(
393
- [
394
- "pr",
395
- "view",
396
- id,
397
- "--repo",
398
- `${repo.owner}/${repo.name}`,
399
- "--json",
400
- "number,url,state,title,isDraft,baseRefName,reviewDecision,statusCheckRollup,mergeable",
401
- ],
402
- "pr view",
403
- options
404
- );
405
-
406
- const review = reviewFrom(viewed.reviewDecision);
407
- const checks = checksFrom(viewed.statusCheckRollup);
408
- const mergeable =
409
- viewed.mergeable === "MERGEABLE"
410
- ? true
411
- : viewed.mergeable === "CONFLICTING"
412
- ? false
413
- : undefined;
414
-
415
- return {
416
- proposal: summarizePullRequest(viewed),
417
- ...(review === undefined ? {} : { review }),
418
- ...(checks === undefined ? {} : { checks }),
419
- ...(mergeable === undefined ? {} : { mergeable }),
420
- };
421
- }
422
-
423
- /**
424
- * Replaces a pull request's title, its body, or both.
425
- *
426
- * @param repo - The canonicalized repository the proposal targets.
427
- * @param id - The pull request number.
428
- * @param update - What to replace.
429
- * @param options - Subprocess runner and working directory overrides.
430
- */
431
- async updateProposal(
432
- repo: CanonicalRepo,
433
- id: string,
434
- update: ProposalUpdate,
435
- options: ProviderOptions = {}
436
- ): Promise<ProposedChange> {
437
- const args = ["pr", "edit", id, "--repo", `${repo.owner}/${repo.name}`];
438
- if (update.title !== undefined) args.push("--title", update.title);
439
- if (update.body !== undefined) args.push("--body", update.body);
440
-
441
- const result = await this.runCommand(this.cli.command, args, options);
442
- if (result.code !== 0) {
443
- const reported = result.stderr.trim() || result.stdout.trim();
444
- throw new ConfigError(
445
- `'${this.cli.command} pr edit' did not succeed, so the ${this.proposalNoun} kept its ` +
446
- `title and body.` +
447
- (reported.length === 0 ? "" : `\n ${reported}`)
448
- );
449
- }
450
-
451
- const url = firstUrlIn(result.stdout);
452
- return url === undefined
453
- ? { detail: `The ${this.proposalNoun} was updated.` }
454
- : { url, detail: `The ${this.proposalNoun} at ${url} was updated.` };
455
- }
456
-
457
- /**
458
- * The login of the account `gh` is signed in as, which is the owner of any
459
- * fork sous made for the contributor.
460
- *
461
- * @param options - Subprocess runner and working directory overrides.
462
- */
463
- private async signedInLogin(options: ProviderOptions): Promise<string> {
464
- const who = await this.capturedOutput(
465
- this.cli.command,
466
- ["api", "user", "--jq", ".login"],
467
- options
468
- );
469
- if (who === undefined || who.trim().length === 0) {
470
- throw new ConfigError(
471
- "Sous could not read your GitHub login from " +
472
- `'${this.cli.command} api user', so it cannot tell which fork a ${this.proposalNoun} ` +
473
- "would come from."
474
- );
475
- }
476
- return who.trim();
477
- }
478
-
479
- /**
480
- * Runs a `gh` command that prints JSON, and parses what it printed.
481
- *
482
- * @param args - The arguments, ending with the `--json` field list.
483
- * @param what - The subcommand, as it is named in a failure.
484
- * @param options - Subprocess runner and working directory overrides.
485
- */
486
- private async ghJson<T>(args: string[], what: string, options: ProviderOptions): Promise<T> {
487
- const result = await this.runCommand(this.cli.command, args, options);
488
- if (result.code !== 0) {
489
- const reported = result.stderr.trim() || result.stdout.trim();
490
- throw new ConfigError(
491
- `'${this.cli.command} ${what}' did not succeed.` +
492
- (reported.length === 0 ? "" : `\n ${reported}`)
493
- );
494
- }
495
- try {
496
- return JSON.parse(result.stdout) as T;
497
- } catch {
498
- throw new ConfigError(
499
- `'${this.cli.command} ${what}' printed something that is not JSON, so sous cannot read ` +
500
- `the ${this.proposalNoun} it describes.`
501
- );
502
- }
503
- }
504
- }
505
-
506
- // --- What gh prints, and how it maps onto plain data ---------------------------------------------
507
-
508
- /** The fields sous asks `gh` for, as it prints them. */
509
- type GhPullRequest = {
510
- number: number;
511
- url?: string;
512
- state?: string;
513
- title?: string;
514
- isDraft?: boolean;
515
- baseRefName?: string;
516
- headRepositoryOwner?: { login?: string } | null;
517
- isCrossRepository?: boolean;
518
- reviewDecision?: string | null;
519
- statusCheckRollup?: GhCheck[] | null;
520
- mergeable?: string;
521
- };
522
-
523
- /** One entry of a pull request's status check rollup: a check run or a commit status. */
524
- type GhCheck = {
525
- __typename?: string;
526
- status?: string;
527
- conclusion?: string | null;
528
- state?: string;
529
- };
530
-
531
- /** A pull request's state, in the words every provider shares. */
532
- function stateFrom(state: string | undefined): ProposalState {
533
- if (state === "MERGED") return "merged";
534
- if (state === "CLOSED") return "closed";
535
- return "open";
536
- }
537
-
538
- /**
539
- * The plain summary of a pull request.
540
- *
541
- * @param entry - What `gh` printed for it.
542
- */
543
- function summarizePullRequest(entry: GhPullRequest): ProposalSummary {
544
- return {
545
- id: String(entry.number),
546
- ...(entry.url === undefined ? {} : { url: entry.url }),
547
- state: stateFrom(entry.state),
548
- title: entry.title ?? "",
549
- draft: entry.isDraft === true,
550
- ...(entry.baseRefName === undefined ? {} : { base: entry.baseRefName }),
551
- };
552
- }
553
-
554
- /** GitHub's review decision, in the words every provider shares. */
555
- function reviewFrom(decision: string | null | undefined): ProposalReview | undefined {
556
- if (decision === "APPROVED") return "approved";
557
- if (decision === "CHANGES_REQUESTED") return "changes requested";
558
- if (decision === "REVIEW_REQUIRED") return "review required";
559
- return undefined;
560
- }
561
-
562
- /** Check runs whose conclusion counts as passing. */
563
- const PASSING_CONCLUSIONS = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
564
-
565
- /**
566
- * Counts a status check rollup into passed, failed and pending. A check run
567
- * that has not completed is pending; a commit status reports its state directly.
568
- *
569
- * @param rollup - What `gh` printed as the rollup, when it printed one.
570
- */
571
- function checksFrom(rollup: GhCheck[] | null | undefined): ProposalChecks | undefined {
572
- if (rollup === null || rollup === undefined || rollup.length === 0) return undefined;
573
- const counts: ProposalChecks = { passed: 0, failed: 0, pending: 0 };
574
- for (const check of rollup) {
575
- const isStatus =
576
- check.__typename === "StatusContext" ||
577
- (check.status === undefined && check.state !== undefined);
578
- if (isStatus) {
579
- if (check.state === "SUCCESS") counts.passed += 1;
580
- else if (check.state === "PENDING" || check.state === "EXPECTED") counts.pending += 1;
581
- else counts.failed += 1;
582
- continue;
583
- }
584
- if (check.status !== "COMPLETED") counts.pending += 1;
585
- else if (PASSING_CONCLUSIONS.has(check.conclusion ?? "")) counts.passed += 1;
586
- else counts.failed += 1;
587
- }
588
- return counts;
589
- }