@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,263 @@
1
+ /**
2
+ * The GitLab provider: the read path, and as much of the write path as GitLab
3
+ * honestly gives sous.
4
+ *
5
+ * The read shape is the same as the GitHub provider's: the index file is one raw
6
+ * HTTPS GET, and a recipe's subtree comes from git. A token is used when one is
7
+ * available, read from GITLAB_TOKEN or asked of the `glab` command line tool
8
+ * when that is installed and signed in; a missing `glab` is never an error on
9
+ * the read path.
10
+ *
11
+ * The write path is one merge request opened by `glab mr create`, from a branch
12
+ * the contributor can already push. Sous does not fork on GitLab: there is no
13
+ * fork-and-track flow here that sous can carry out without guessing, so
14
+ * `canPush` answers "cannot tell" rather than inventing a permission, and `fork`
15
+ * refuses with the manual route spelled out. Saying so is the point; pretending
16
+ * otherwise would strand a contributor halfway through a submission.
17
+ *
18
+ * Self-hosted instances are supported: any host whose name begins with `gitlab.`
19
+ * is recognized automatically, and a repository entry may always name its
20
+ * provider outright for a host that gives nothing away.
21
+ */
22
+
23
+ import { ConfigError } from "../../errors.js";
24
+ import { INDEX_FILENAME } from "../formats/common.js";
25
+ import { ProviderBase, firstUrlIn } from "./base.js";
26
+ import { fetchSubtree, type CommandRunner } from "./git.js";
27
+ import { fetchText, type FetchLike } from "./http.js";
28
+ import {
29
+ buildCanonicalRepo,
30
+ invalidRepoUrl,
31
+ splitRepoUrl,
32
+ type AuthStatus,
33
+ type CanonicalRepo,
34
+ type ChangeProposal,
35
+ type FetchedIndex,
36
+ type ForkedRepo,
37
+ type ProposedChange,
38
+ type ProviderCli,
39
+ type ProviderFeature,
40
+ type ProviderOptions,
41
+ } from "./provider.js";
42
+
43
+ /** The host this provider serves when a URL does not say otherwise. */
44
+ export const GITLAB_HOST = "gitlab.com";
45
+
46
+ /** The environment variable a GitLab token is read from. */
47
+ export const GITLAB_TOKEN_ENV = "GITLAB_TOKEN";
48
+
49
+ /**
50
+ * Finds a GitLab token: the environment first, then `glab auth token` when the
51
+ * `glab` command line tool is installed and signed in. Returns undefined when
52
+ * there is none, because public repositories need no token at all.
53
+ *
54
+ * @param options - Environment and subprocess runner overrides.
55
+ */
56
+ export async function findGitlabToken(options: {
57
+ env?: NodeJS.ProcessEnv;
58
+ run?: CommandRunner;
59
+ } = {}): Promise<string | undefined> {
60
+ return new GitlabProvider().token(options);
61
+ }
62
+
63
+ /** The GitLab provider. */
64
+ export class GitlabProvider extends ProviderBase {
65
+ readonly id = "gitlab" as const;
66
+
67
+ /**
68
+ * Reads the index and recipe subtrees, and proposes a change through
69
+ * the GitLab CLI ('glab').
70
+ */
71
+ readonly features: ProviderFeature[] = ["fetch", "submit"];
72
+
73
+ /** The command line tool the write path is built on. */
74
+ readonly cli: ProviderCli = {
75
+ command: "glab",
76
+ label: "the GitLab CLI",
77
+ install: "https://gitlab.com/gitlab-org/cli",
78
+ };
79
+
80
+ /** What GitLab calls a proposal. */
81
+ readonly proposalNoun = "merge request";
82
+
83
+ matches(url: string): boolean {
84
+ const parts = splitRepoUrl(url);
85
+ if (parts === undefined) return false;
86
+ return parts.host === GITLAB_HOST || parts.host.startsWith("gitlab.");
87
+ }
88
+
89
+ canonicalize(url: string): CanonicalRepo {
90
+ const parts = splitRepoUrl(url);
91
+ if (parts === undefined) throw invalidRepoUrl(this.id, url);
92
+ return buildCanonicalRepo(parts.host, parts.owner, parts.name);
93
+ }
94
+
95
+ /**
96
+ * A GitLab token, from the environment or from `glab`.
97
+ *
98
+ * @param options - Environment and subprocess runner overrides.
99
+ */
100
+ async token(options: ProviderOptions = {}): Promise<string | undefined> {
101
+ return this.findToken(GITLAB_TOKEN_ENV, ["auth", "token"], options);
102
+ }
103
+
104
+ /**
105
+ * Fetches the repo's index file from the instance's raw file endpoint at the
106
+ * repository's default branch.
107
+ *
108
+ * @param repo - The canonicalized repository.
109
+ * @param options - Environment, fetch and subprocess overrides.
110
+ */
111
+ async fetchIndex(
112
+ repo: CanonicalRepo,
113
+ options: ProviderOptions = {}
114
+ ): Promise<FetchedIndex> {
115
+ const token = await this.token(options);
116
+ const url = this.indexUrl(repo);
117
+ const fetched = await fetchText(url, {
118
+ ...(token === undefined ? {} : { token }),
119
+ ...(options.fetchImpl === undefined
120
+ ? {}
121
+ : { fetchImpl: options.fetchImpl as FetchLike }),
122
+ label: "repo index",
123
+ });
124
+
125
+ return fetched.etag === undefined
126
+ ? { text: fetched.text, ref: "HEAD" }
127
+ : { text: fetched.text, ref: "HEAD", etag: fetched.etag };
128
+ }
129
+
130
+ /**
131
+ * Fetches one recipe folder at one tag. Private repositories work through
132
+ * git's own credential helpers, the same way a manual clone would.
133
+ *
134
+ * @param repo - The canonicalized repository.
135
+ * @param recipePath - The recipe folder, relative to the repository root.
136
+ * @param tag - The git tag carrying the version.
137
+ * @param destDir - Where the recipe's files should end up.
138
+ * @param options - Subprocess runner override.
139
+ */
140
+ async fetchRecipeTree(
141
+ repo: CanonicalRepo,
142
+ recipePath: string,
143
+ tag: string,
144
+ destDir: string,
145
+ options: ProviderOptions = {}
146
+ ): Promise<void> {
147
+ await fetchSubtree({
148
+ cloneUrl: repo.httpsUrl,
149
+ tag,
150
+ subPath: recipePath,
151
+ destDir,
152
+ ...(options.run === undefined ? {} : { run: options.run }),
153
+ });
154
+ }
155
+
156
+ /**
157
+ * The raw URL of a repository's index file at its default branch.
158
+ *
159
+ * @param repo - The canonicalized repository.
160
+ */
161
+ indexUrl(repo: CanonicalRepo): string {
162
+ return `https://${repo.host}/${repo.owner}/${repo.name}/-/raw/HEAD/${INDEX_FILENAME}`;
163
+ }
164
+
165
+ // --- The write path --------------------------------------------------------
166
+
167
+ /**
168
+ * Whether `glab` is installed and signed in.
169
+ *
170
+ * @param options - Subprocess runner and working directory overrides.
171
+ */
172
+ async authStatus(options: ProviderOptions = {}): Promise<AuthStatus> {
173
+ const ok = await this.commandSucceeds(this.cli.command, ["auth", "status"], options);
174
+ if (ok) {
175
+ return {
176
+ ok: true,
177
+ detail: `${this.cli.label} ('${this.cli.command}') is installed and signed in.`,
178
+ };
179
+ }
180
+ return {
181
+ ok: false,
182
+ detail:
183
+ `Sous proposes a change through ${this.cli.label} ('${this.cli.command}'), and it is ` +
184
+ `either not installed or not signed in.\n` +
185
+ ` Install it from ${this.cli.install}, then run '${this.cli.command} auth login'.`,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Always undefined: sous has no cheap, reliable way to ask GitLab whether the
191
+ * contributor may push, and guessing would send them down a fork path this
192
+ * provider cannot finish.
193
+ *
194
+ * @param _repo - The canonicalized repository; unused.
195
+ * @param _options - Unused, because nothing is run.
196
+ */
197
+ async canPush(
198
+ _repo: CanonicalRepo,
199
+ _options: ProviderOptions = {}
200
+ ): Promise<boolean | undefined> {
201
+ return undefined;
202
+ }
203
+
204
+ /**
205
+ * Refuses, with the manual route. Sous opens a merge request from a branch
206
+ * the contributor can already push; forking on their behalf is not something
207
+ * this provider does.
208
+ *
209
+ * @param repo - The canonicalized repository.
210
+ */
211
+ async fork(repo: CanonicalRepo): Promise<ForkedRepo> {
212
+ throw new ConfigError(
213
+ `Sous does not fork ${repo.owner}/${repo.name} for you on ${this.cli.label}; it opens a ` +
214
+ `${this.proposalNoun} from a branch you can already push.\n` +
215
+ ` Fork the project yourself, push your branch to the fork, then open the ` +
216
+ `${this.proposalNoun} from there.`
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Opens a merge request from the branch in the checkout the command is run
222
+ * in, which is why the working directory matters here.
223
+ *
224
+ * @param _repo - The canonicalized repository; `glab` reads the checkout instead.
225
+ * @param proposal - The branch, the text and whether it is a draft.
226
+ * @param options - Subprocess runner and working directory overrides.
227
+ */
228
+ async proposeChange(
229
+ _repo: CanonicalRepo,
230
+ proposal: ChangeProposal,
231
+ options: ProviderOptions = {}
232
+ ): Promise<ProposedChange> {
233
+ const args = ["mr", "create", "--source-branch", proposal.branch];
234
+ if (proposal.base !== undefined) args.push("--target-branch", proposal.base);
235
+ args.push(
236
+ "--title",
237
+ proposal.title,
238
+ "--description",
239
+ proposal.body,
240
+ "--yes"
241
+ );
242
+ if (proposal.draft) args.push("--draft");
243
+
244
+ const result = await this.runCommand(this.cli.command, args, options);
245
+ if (result.code !== 0) {
246
+ const reported = result.stderr.trim() || result.stdout.trim();
247
+ throw new ConfigError(
248
+ `'${this.cli.command} mr create' did not succeed, so no proposal was opened.` +
249
+ (reported.length === 0 ? "" : `\n ${reported}`)
250
+ );
251
+ }
252
+
253
+ const url = firstUrlIn(result.stdout);
254
+ if (url === undefined) {
255
+ return {
256
+ detail:
257
+ `The ${this.proposalNoun} was opened, but '${this.cli.command}' printed no address ` +
258
+ `for it.`,
259
+ };
260
+ }
261
+ return { url, detail: `The ${this.proposalNoun} is at ${url}.` };
262
+ }
263
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The tiny HTTPS client the providers use to fetch a repo's index file.
3
+ *
4
+ * Only one kind of request is ever made: a plain GET of a raw file, optionally
5
+ * carrying a bearer token so private repositories work. `fetch` is Node's own
6
+ * global, and it is injectable so tests never reach the network.
7
+ */
8
+
9
+ import { ConfigError } from "../../errors.js";
10
+
11
+ /** The shape of `fetch` this module needs; the global satisfies it. */
12
+ export type FetchLike = (
13
+ url: string,
14
+ init?: { headers?: Record<string, string> }
15
+ ) => Promise<{
16
+ ok: boolean;
17
+ status: number;
18
+ statusText: string;
19
+ text: () => Promise<string>;
20
+ headers: { get: (name: string) => string | null };
21
+ }>;
22
+
23
+ /** What a successful raw-file GET returns. */
24
+ export type FetchedText = {
25
+ /** The file's contents. */
26
+ text: string;
27
+ /** The entity tag the server sent, when it sent one. */
28
+ etag?: string;
29
+ };
30
+
31
+ /** Options for a raw-file GET. */
32
+ export type FetchTextOptions = {
33
+ /** A bearer token, when one is available for the host. */
34
+ token?: string;
35
+ /** The fetch implementation to use. Defaults to the global `fetch`. */
36
+ fetchImpl?: FetchLike;
37
+ /** What the URL is, named in error messages (for example "repo index"). */
38
+ label?: string;
39
+ };
40
+
41
+ /**
42
+ * GETs a URL and returns its body as text. Any non-2xx response, or a transport
43
+ * failure, becomes a ConfigError that names the URL and the status, and says
44
+ * plainly what a 404 or a 401 usually means for a sous repository.
45
+ *
46
+ * @param url - The absolute HTTPS URL to fetch.
47
+ * @param options - Bearer token, fetch implementation and error label.
48
+ */
49
+ export async function fetchText(
50
+ url: string,
51
+ options: FetchTextOptions = {}
52
+ ): Promise<FetchedText> {
53
+ const fetchImpl = options.fetchImpl ?? (globalThis.fetch as unknown as FetchLike);
54
+ const label = options.label ?? "file";
55
+
56
+ if (typeof fetchImpl !== "function") {
57
+ throw new ConfigError(
58
+ `Sous cannot fetch the ${label} at ${url}: this Node runtime provides no global fetch.\n` +
59
+ ` Sous requires Node 22 or newer.`
60
+ );
61
+ }
62
+
63
+ const headers: Record<string, string> = { Accept: "application/json" };
64
+ if (options.token !== undefined && options.token.length > 0) {
65
+ headers["Authorization"] = `Bearer ${options.token}`;
66
+ }
67
+
68
+ let response: Awaited<ReturnType<FetchLike>>;
69
+ try {
70
+ response = await fetchImpl(url, { headers });
71
+ } catch (error) {
72
+ throw new ConfigError(
73
+ `Sous could not reach ${url} while fetching the ${label}.\n` +
74
+ ` ${(error as Error).message}`
75
+ );
76
+ }
77
+
78
+ if (!response.ok) {
79
+ const lines = [
80
+ `Sous could not fetch the ${label} from ${url}.`,
81
+ ` The server answered ${response.status} ${response.statusText}.`,
82
+ ];
83
+ if (response.status === 404) {
84
+ lines.push(
85
+ " Either the repository publishes no sous index yet, or the URL names a " +
86
+ "repository that does not exist."
87
+ );
88
+ }
89
+ if (response.status === 401 || response.status === 403) {
90
+ lines.push(
91
+ " The repository is private or the request was not authorized. Sous uses a " +
92
+ "token from the environment, or from the provider's command line tool when " +
93
+ "one is installed and signed in."
94
+ );
95
+ }
96
+ throw new ConfigError(lines.join("\n"));
97
+ }
98
+
99
+ const text = await response.text();
100
+ const etag = response.headers.get("etag");
101
+ return etag === null ? { text } : { text, etag };
102
+ }