@sous-io/sous 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +121 -35
  2. package/bin/run.js +10 -1
  3. package/docs/markdown/README.md +27 -0
  4. package/docs/markdown/_sidebar.md +18 -0
  5. package/docs/markdown/commands.md +308 -0
  6. package/docs/markdown/config-discovery.md +74 -0
  7. package/docs/markdown/config-inspection.md +69 -0
  8. package/docs/markdown/config-layers.md +92 -0
  9. package/docs/markdown/config-variables.md +79 -0
  10. package/docs/markdown/configuration.md +71 -0
  11. package/docs/markdown/design-principles.md +59 -0
  12. package/docs/markdown/repositories-authoring.md +408 -0
  13. package/docs/markdown/repositories-consuming.md +580 -0
  14. package/docs/markdown/repositories-file-formats.md +1084 -0
  15. package/docs/markdown/repositories-variables.md +387 -0
  16. package/docs/markdown/repositories.md +303 -0
  17. package/docs/markdown/skill-categories.md +58 -0
  18. package/package.json +73 -9
  19. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
  20. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
  21. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
  22. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
  23. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
  24. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
  25. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
  26. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
  27. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
  28. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
  29. package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
  30. package/sous.config.schema.json +337 -0
  31. package/src/base-command.ts +220 -67
  32. package/src/commands/build.ts +150 -73
  33. package/src/commands/clear.ts +23 -15
  34. package/src/commands/compile.ts +74 -16
  35. package/src/commands/config/get.ts +110 -0
  36. package/src/commands/config/show.ts +32 -0
  37. package/src/commands/config/validate.ts +53 -0
  38. package/src/commands/help.ts +46 -0
  39. package/src/commands/launch.ts +36 -14
  40. package/src/commands/lock/rebuild.ts +241 -0
  41. package/src/commands/lock/show.ts +115 -0
  42. package/src/commands/namespace/list.ts +117 -0
  43. package/src/commands/namespace/show.ts +110 -0
  44. package/src/commands/prune.ts +3 -11
  45. package/src/commands/recipe/list.ts +95 -0
  46. package/src/commands/recipe/show.ts +301 -0
  47. package/src/commands/repo/add.ts +145 -0
  48. package/src/commands/repo/gc.ts +172 -0
  49. package/src/commands/repo/init.ts +136 -0
  50. package/src/commands/repo/link.ts +500 -0
  51. package/src/commands/repo/list.ts +179 -0
  52. package/src/commands/repo/release.ts +619 -0
  53. package/src/commands/repo/remove.ts +193 -0
  54. package/src/commands/repo/search.ts +189 -0
  55. package/src/commands/repo/submit.ts +133 -0
  56. package/src/commands/repo/unlink.ts +147 -0
  57. package/src/commands/subscription/add.ts +285 -0
  58. package/src/commands/subscription/list.ts +129 -0
  59. package/src/commands/subscription/remove.ts +181 -0
  60. package/src/commands/vars/ask.ts +374 -0
  61. package/src/commands/vars/index.ts +79 -0
  62. package/src/commands/vars/list.ts +67 -0
  63. package/src/commands/vars/show.ts +77 -0
  64. package/src/config-command.ts +30 -0
  65. package/src/lib/build-service.ts +206 -54
  66. package/src/lib/config-discovery.ts +220 -27
  67. package/src/lib/config-inspect.ts +145 -0
  68. package/src/lib/config-kernel.mjs +377 -0
  69. package/src/lib/config-schema.ts +361 -0
  70. package/src/lib/env-file.ts +328 -0
  71. package/src/lib/env-local.ts +18 -1
  72. package/src/lib/errors.ts +32 -0
  73. package/src/lib/include-resolver.ts +108 -15
  74. package/src/lib/interactive.ts +165 -0
  75. package/src/lib/markdown-compiler.ts +118 -37
  76. package/src/lib/package-info.ts +25 -0
  77. package/src/lib/pid-service.ts +32 -21
  78. package/src/lib/refs/find.ts +589 -0
  79. package/src/lib/refs/index.ts +12 -0
  80. package/src/lib/refs/pick.ts +147 -0
  81. package/src/lib/refs/scopes.ts +61 -0
  82. package/src/lib/repos/catalog-display.ts +116 -0
  83. package/src/lib/repos/catalog-inputs.ts +160 -0
  84. package/src/lib/repos/catalog.ts +722 -0
  85. package/src/lib/repos/core-recipe.ts +105 -0
  86. package/src/lib/repos/defaults.ts +175 -0
  87. package/src/lib/repos/formats/common.ts +389 -0
  88. package/src/lib/repos/formats/index-file.ts +215 -0
  89. package/src/lib/repos/formats/links-map.ts +96 -0
  90. package/src/lib/repos/formats/lockfile.ts +167 -0
  91. package/src/lib/repos/formats/patterns.ts +57 -0
  92. package/src/lib/repos/formats/recipe-manifest.ts +395 -0
  93. package/src/lib/repos/formats/repo-manifest.ts +88 -0
  94. package/src/lib/repos/formats/store-entry.ts +84 -0
  95. package/src/lib/repos/freshness.ts +208 -0
  96. package/src/lib/repos/git-clone.ts +312 -0
  97. package/src/lib/repos/identity.ts +89 -0
  98. package/src/lib/repos/index.ts +58 -0
  99. package/src/lib/repos/links.ts +353 -0
  100. package/src/lib/repos/load-manifest.ts +236 -0
  101. package/src/lib/repos/lock-service.ts +453 -0
  102. package/src/lib/repos/locked-namespace-resolver.ts +90 -0
  103. package/src/lib/repos/locked-recipes.ts +254 -0
  104. package/src/lib/repos/managed-layer.ts +422 -0
  105. package/src/lib/repos/namespace-resolver.ts +370 -0
  106. package/src/lib/repos/providers/base.ts +206 -0
  107. package/src/lib/repos/providers/git.ts +233 -0
  108. package/src/lib/repos/providers/github.ts +294 -0
  109. package/src/lib/repos/providers/gitlab.ts +263 -0
  110. package/src/lib/repos/providers/http.ts +102 -0
  111. package/src/lib/repos/providers/index-cache.ts +382 -0
  112. package/src/lib/repos/providers/index.ts +106 -0
  113. package/src/lib/repos/providers/local.ts +391 -0
  114. package/src/lib/repos/providers/provider.ts +401 -0
  115. package/src/lib/repos/recipe-config-layers.ts +287 -0
  116. package/src/lib/repos/recipe-targets.ts +223 -0
  117. package/src/lib/repos/ref-search.ts +46 -0
  118. package/src/lib/repos/ref.ts +513 -0
  119. package/src/lib/repos/reference-report.ts +122 -0
  120. package/src/lib/repos/release/bump.ts +161 -0
  121. package/src/lib/repos/release/git-state.ts +305 -0
  122. package/src/lib/repos/release/index-builder.ts +635 -0
  123. package/src/lib/repos/release/index.ts +16 -0
  124. package/src/lib/repos/release/plan.ts +512 -0
  125. package/src/lib/repos/release/submit-service.ts +496 -0
  126. package/src/lib/repos/release/tags.ts +243 -0
  127. package/src/lib/repos/release/validate.ts +463 -0
  128. package/src/lib/repos/resolver.ts +789 -0
  129. package/src/lib/repos/scaffold/index.ts +238 -0
  130. package/src/lib/repos/scaffold/templates.ts +413 -0
  131. package/src/lib/repos/seed.ts +414 -0
  132. package/src/lib/repos/store/contract.ts +64 -0
  133. package/src/lib/repos/store/hash.ts +114 -0
  134. package/src/lib/repos/store/recipe-store.ts +599 -0
  135. package/src/lib/repos/store/settings.ts +58 -0
  136. package/src/lib/repos/subscription-service.ts +2678 -0
  137. package/src/lib/repos/trust.ts +447 -0
  138. package/src/lib/settings.ts +546 -189
  139. package/src/lib/sous-home.ts +104 -0
  140. package/src/lib/state.ts +52 -20
  141. package/src/lib/vars/ask.ts +1152 -0
  142. package/src/lib/vars/definition-source.ts +252 -0
  143. package/src/lib/vars/display.ts +233 -0
  144. package/src/lib/vars/index.ts +18 -0
  145. package/src/lib/vars/ladder.ts +282 -0
  146. package/src/lib/vars/mappings.ts +265 -0
  147. package/src/lib/vars/names.ts +94 -0
  148. package/src/lib/vars/preanswers.ts +395 -0
  149. package/src/lib/vars/question-plan.ts +218 -0
  150. package/src/lib/vars/report.ts +228 -0
  151. package/src/lib/vars/safe-regex.ts +235 -0
  152. package/src/lib/vars/validate.ts +312 -0
  153. package/src/lib/watch-loop.ts +148 -0
  154. package/src/templating/init-liquid-engine.ts +58 -16
  155. package/src/utils/choice-prompt.ts +143 -0
  156. package/src/utils/command-errors.ts +186 -0
  157. package/src/utils/command-help.ts +45 -0
  158. package/src/utils/confirm-prompt.ts +110 -0
  159. package/src/utils/flags.ts +153 -0
  160. package/src/utils/formatting.ts +540 -55
  161. package/src/utils/prompts.ts +35 -1
  162. package/src/utils/sous-directory.ts +245 -0
  163. package/src/utils/table.ts +603 -0
  164. package/src/utils/value-prompt.ts +119 -0
  165. package/bin/xcv +0 -5
  166. package/shared-prompts/_partials/resume-task.md +0 -51
  167. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  168. package/shared-prompts/_partials/update-task-file.md +0 -52
  169. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  189. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  190. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  191. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  192. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  193. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  194. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  195. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  196. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  197. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  198. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  199. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  200. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  201. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  202. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  203. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  204. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  205. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  206. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
@@ -0,0 +1,447 @@
1
+ /**
2
+ * The trust layer.
3
+ *
4
+ * Added equals trusted. There is no separate trust command and no trusted-but-
5
+ * not-added state: a repository sous will read from is one written into the
6
+ * project's `repos:` map, and removing the entry withdraws the trust. Until a
7
+ * repository is added, sous downloads nothing from it, not even its index;
8
+ * there is no peeking before trusting, because the decision rests on the URL
9
+ * and the publisher's reputation, which are inspected outside sous.
10
+ *
11
+ * Resolution can turn up repositories a dependency needs but the project has
12
+ * not added. Those come back from the resolver as `MissingRepo` entries with
13
+ * their provenance, and this is where the user is asked about them: one
14
+ * consolidated question per round, listing every new repository with its URL
15
+ * and the recipe that requires it. A refusal aborts. A non-interactive run
16
+ * fails hard, naming the repositories and the command that grants trust, and
17
+ * `--trust` acknowledges without asking.
18
+ */
19
+
20
+ import { color } from "@oclif/color";
21
+ import { ConfigError } from "../errors.js";
22
+ import type { Settings } from "../settings.js";
23
+ import {
24
+ blankLine,
25
+ formatVariable,
26
+ log as writeLine,
27
+ palette,
28
+ wrapColumns,
29
+ wrapText,
30
+ type VariableEntry,
31
+ } from "../../utils/formatting.js";
32
+ import { askYesNo } from "../../utils/prompts.js";
33
+ import {
34
+ isInteractive,
35
+ nonInteractiveReason,
36
+ NonInteractiveError,
37
+ } from "../interactive.js";
38
+ import {
39
+ REPOS_LAYER_FILENAME,
40
+ readManagedLayer,
41
+ updateManagedLayer,
42
+ type ManagedLayerOptions,
43
+ } from "./managed-layer.js";
44
+ import type { MissingRepo } from "./resolver.js";
45
+ import type { ProviderId } from "./providers/provider.js";
46
+ import { enabledRepos } from "./defaults.js";
47
+
48
+ /** The `addedBy` value meaning a person deliberately added the repository. */
49
+ export const USER_ADDED_BY = "user";
50
+
51
+ /** One repository entry, as it is written into the managed layer. */
52
+ export type TrustedRepo = {
53
+ /** Where the repository lives. */
54
+ url: string;
55
+ /** The provider that handles it, when the URL does not give it away. */
56
+ provider?: ProviderId;
57
+ /** Whether a newer in-range version is preferred over the locked one. */
58
+ alwaysPull?: boolean;
59
+ /** When it was added. */
60
+ addedAt?: string;
61
+ /** Who required it: "user", or the ref of the recipe whose dependency pulled it in. */
62
+ addedBy?: string;
63
+ };
64
+
65
+ /** What `addRepo` is told. */
66
+ export type AddRepoRequest = {
67
+ /** The repository's short name, which refs use as the `repo:` qualifier. */
68
+ name: string;
69
+ /** Where the repository lives. */
70
+ url: string;
71
+ /** The provider that handles it, when the URL does not give it away. */
72
+ provider?: ProviderId;
73
+ /** Whether a newer in-range version is preferred over the locked one. */
74
+ alwaysPull?: boolean;
75
+ /** Who required it. Defaults to "user". */
76
+ addedBy?: string;
77
+ };
78
+
79
+ /** How trust is confirmed for a round of missing repositories. */
80
+ export type ConfirmTrustOptions = {
81
+ /** Whether sous may ask. Defaults to whether both streams are a terminal. */
82
+ interactive?: boolean;
83
+ /**
84
+ * The confirmation flag, spelled `--trust` on the commands that perform this
85
+ * ceremony (and `--yes`, `-y`, `-f` or `--force`): acknowledge every
86
+ * repository this command adds, without asking.
87
+ */
88
+ trustFlag?: boolean;
89
+ };
90
+
91
+ /** What a confirmation round produced. */
92
+ export type ConfirmTrustResult = {
93
+ /** The repositories the user accepted. */
94
+ accepted: MissingRepo[];
95
+ /** The ones whose URL sous knew, and has now written into the managed layer. */
96
+ added: string[];
97
+ /**
98
+ * The ones sous still cannot add on its own, because nothing told it their
99
+ * URL. A recipe names a repository by its short name, so the URL has to come
100
+ * from the person adding it.
101
+ */
102
+ needUrl: string[];
103
+ };
104
+
105
+ /** How the trust service is built. */
106
+ export type TrustServiceOptions = ManagedLayerOptions & {
107
+ /** The project's `.sous/` directory; the managed layer lives under it. */
108
+ sousDir: string;
109
+ /** The merged settings, which is where hand-written `repos:` entries come from. */
110
+ settings?: Settings;
111
+ /** Whether sous may ask questions. Defaults to whether both streams are a terminal. */
112
+ interactive?: boolean;
113
+ /** How a yes or no question is asked. Injected in tests. */
114
+ ask?: (message: string) => Promise<boolean>;
115
+ /** Where the consolidated trust notice is written. Defaults to the console. */
116
+ write?: (message: string) => void;
117
+ /** The clock, so a recorded `addedAt` is predictable in tests. */
118
+ now?: () => Date;
119
+ };
120
+
121
+ /** Reads and changes the list of repositories a project trusts. */
122
+ export class TrustService {
123
+ private readonly sousDir: string;
124
+
125
+ private readonly layerOptions: ManagedLayerOptions;
126
+
127
+ private readonly settings: Settings | undefined;
128
+
129
+ private readonly interactive: boolean;
130
+
131
+ private readonly ask: (message: string) => Promise<boolean>;
132
+
133
+ private readonly write: (message: string) => void;
134
+
135
+ private readonly now: () => Date;
136
+
137
+ constructor(options: TrustServiceOptions) {
138
+ this.sousDir = options.sousDir;
139
+ this.layerOptions = options.confDir === undefined ? {} : { confDir: options.confDir };
140
+ this.settings = options.settings;
141
+ this.interactive = options.interactive ?? isInteractive();
142
+ this.ask = options.ask ?? ((message: string) => askYesNo(message));
143
+ this.write = options.write ?? writeLine;
144
+ this.now = options.now ?? (() => new Date());
145
+ }
146
+
147
+ /**
148
+ * Every repository the project trusts, keyed by short name. This is the
149
+ * merged view: entries hand-written in the primary config and entries sous
150
+ * wrote into the managed layer both appear, because by the time settings are
151
+ * loaded they are one map.
152
+ *
153
+ * @param settings - The merged settings. Defaults to the ones given at construction.
154
+ */
155
+ listTrusted(settings: Settings | undefined = this.settings): Record<string, TrustedRepo> {
156
+ return { ...enabledRepos(settings) } as Record<string, TrustedRepo>;
157
+ }
158
+
159
+ /**
160
+ * True when the project trusts a repository by that name.
161
+ *
162
+ * @param repoName - The repository's short name.
163
+ * @param settings - The merged settings. Defaults to the ones given at construction.
164
+ */
165
+ isTrusted(repoName: string, settings: Settings | undefined = this.settings): boolean {
166
+ return Object.hasOwn(this.listTrusted(settings), repoName);
167
+ }
168
+
169
+ /** Every repository written in the managed layer, keyed by short name. */
170
+ listManaged(): Record<string, TrustedRepo> {
171
+ const layer = readManagedLayer(this.sousDir, REPOS_LAYER_FILENAME, this.layerOptions);
172
+ const repos = layer["repos"];
173
+ if (typeof repos !== "object" || repos === null || Array.isArray(repos)) return {};
174
+ return repos as Record<string, TrustedRepo>;
175
+ }
176
+
177
+ /**
178
+ * Adds a repository to the managed layer, which is what trusting one means.
179
+ * Adding a name that is already there replaces its entry, so this is also how
180
+ * a URL or a provider is corrected.
181
+ *
182
+ * @param request - The repository's name, URL and provenance.
183
+ */
184
+ addRepo(request: AddRepoRequest): TrustedRepo {
185
+ const entry: TrustedRepo = {
186
+ url: request.url,
187
+ ...(request.provider === undefined ? {} : { provider: request.provider }),
188
+ ...(request.alwaysPull === undefined ? {} : { alwaysPull: request.alwaysPull }),
189
+ addedAt: this.now().toISOString(),
190
+ addedBy: request.addedBy ?? USER_ADDED_BY,
191
+ };
192
+
193
+ updateManagedLayer(
194
+ this.sousDir,
195
+ REPOS_LAYER_FILENAME,
196
+ [{ path: ["repos", request.name], value: entry }],
197
+ this.layerOptions
198
+ );
199
+ return entry;
200
+ }
201
+
202
+ /**
203
+ * Removes a repository from the managed layer, withdrawing the trust. A
204
+ * repository written by hand in the primary config is not sous's to remove:
205
+ * that is an error saying so, since silently doing nothing would look like it
206
+ * worked.
207
+ *
208
+ * @param name - The repository's short name.
209
+ */
210
+ removeRepo(name: string): void {
211
+ const existing = this.listManaged();
212
+ if (!Object.hasOwn(existing, name)) {
213
+ if (this.isTrusted(name)) {
214
+ throw new ConfigError(
215
+ `The repository '${name}' is written in this project's own config, not in the ` +
216
+ `layer sous manages.\n` +
217
+ ` Remove its entry from the 'repos' block of your config file; sous never ` +
218
+ `edits a config file you wrote.`
219
+ );
220
+ }
221
+ throw new ConfigError(
222
+ `This project has no repository called '${name}'.\n` +
223
+ ` Run 'sous repo list' to see the repositories it trusts.`
224
+ );
225
+ }
226
+
227
+ updateManagedLayer(
228
+ this.sousDir,
229
+ REPOS_LAYER_FILENAME,
230
+ [{ path: ["repos", name], value: undefined }],
231
+ this.layerOptions
232
+ );
233
+ }
234
+
235
+ /**
236
+ * Switches a repository off without deleting an entry, which is how trust is
237
+ * withdrawn from the repository sous provides itself. That entry is recreated
238
+ * from the installed package on every run, so only a recorded `enabled: false`
239
+ * outlives it; the shape is the same one a person writes by hand to opt out.
240
+ *
241
+ * @param name - The repository's short name.
242
+ */
243
+ disableRepo(name: string): void {
244
+ updateManagedLayer(
245
+ this.sousDir,
246
+ REPOS_LAYER_FILENAME,
247
+ [{ path: ["repos", name], value: { enabled: false } }],
248
+ this.layerOptions
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Asks about every repository a round of resolution turned up that the
254
+ * project has not added, in ONE consolidated question. Any refusal aborts,
255
+ * because a half-trusted install is not something sous will produce.
256
+ *
257
+ * @param missing - The repositories resolution says are needed.
258
+ * @param options - Whether sous may ask, and whether `--trust` was passed.
259
+ */
260
+ async confirmTrust(
261
+ missing: MissingRepo[],
262
+ options: ConfirmTrustOptions = {}
263
+ ): Promise<ConfirmTrustResult> {
264
+ if (missing.length === 0) return { accepted: [], added: [], needUrl: [] };
265
+
266
+ const interactive = options.interactive ?? this.interactive;
267
+ const trustFlag = options.trustFlag ?? false;
268
+
269
+ if (!trustFlag && !interactive) {
270
+ throw new NonInteractiveError(this.nonInteractiveMessage(missing));
271
+ }
272
+
273
+ if (!trustFlag) {
274
+ // The notice opens with a blank line of its own, so it never lands
275
+ // pressed up against whatever the command printed before it, wherever the
276
+ // ceremony runs from.
277
+ this.write(" ");
278
+ this.write(this.trustNotice(missing));
279
+ const plural = missing.length === 1 ? "this repository" : "these repositories";
280
+ const accepted = await this.ask(`Do you trust ${plural}?`);
281
+ if (!accepted) {
282
+ throw new ConfigError(
283
+ `Nothing was installed: trust was declined for ` +
284
+ `${missing.map((repo) => `'${repo.name}'`).join(", ")}.\n` +
285
+ ` Sous installs a dependency closure whole or not at all, so declining any ` +
286
+ `repository in it stops the whole install.`
287
+ );
288
+ }
289
+ }
290
+
291
+ const added: string[] = [];
292
+ const needUrl: string[] = [];
293
+ for (const repo of missing) {
294
+ if (repo.url === undefined) {
295
+ needUrl.push(repo.name);
296
+ continue;
297
+ }
298
+ // Every requester is recorded, not just the first. Removal hygiene reads
299
+ // this to say why a repository is there, and a repository three recipes
300
+ // need looks removable when the entry names only one of them.
301
+ const requesters = [
302
+ ...new Set(repo.requiredBy.map((entry) => entry.requestedBy)),
303
+ ].sort();
304
+ this.addRepo({
305
+ name: repo.name,
306
+ url: repo.url,
307
+ // A dependency's locator names the provider in its scheme, which is the
308
+ // one thing a URL alone cannot say for a self-hosted host.
309
+ ...(repo.provider === undefined ? {} : { provider: repo.provider as ProviderId }),
310
+ addedBy: requesters.length > 0 ? requesters.join(", ") : USER_ADDED_BY,
311
+ });
312
+ added.push(repo.name);
313
+ }
314
+
315
+ return { accepted: missing, added, needUrl };
316
+ }
317
+
318
+ /**
319
+ * The consolidated notice shown before the question: every new repository,
320
+ * its URL, the recipe that requires it, and what trusting it actually means.
321
+ *
322
+ * @param missing - The repositories being asked about.
323
+ */
324
+ trustNotice(missing: MissingRepo[]): string {
325
+ const width = wrapColumns() - 2;
326
+ const lines: string[] = [];
327
+ lines.push(
328
+ palette.warning(
329
+ missing.length === 1
330
+ ? "One repository has to be trusted before this can continue."
331
+ : `${missing.length} repositories have to be trusted before this can continue.`
332
+ )
333
+ );
334
+ lines.push(" ");
335
+
336
+ // Every repository is written as the same key and value block the rest of
337
+ // the CLI uses, so the facts under a repository name are read the same way
338
+ // as the facts anywhere else.
339
+ for (const repo of missing) {
340
+ lines.push(` ${color.bold(repo.name)}`);
341
+
342
+ const entries: VariableEntry[] = [
343
+ {
344
+ label: "Location",
345
+ value:
346
+ repo.url === undefined
347
+ ? "not known to sous; a ref named it by its short name only"
348
+ : repo.url,
349
+ },
350
+ ...(repo.identity === undefined
351
+ ? []
352
+ : [{ label: "Identity", value: repo.identity }]),
353
+ ...repo.requiredBy.map((entry) => ({
354
+ label: "Required",
355
+ value: entry.ref,
356
+ detail: `required by ${
357
+ entry.requestedBy === "project" ? "this project" : `'${entry.requestedBy}'`
358
+ }`,
359
+ })),
360
+ ];
361
+ const labelWidth = Math.max(...entries.map((entry) => entry.label.length));
362
+ for (const entry of entries) {
363
+ lines.push(...formatVariable(entry, { labelWidth, width }));
364
+ }
365
+ lines.push(" ");
366
+ }
367
+
368
+ // The two phrases that carry the actual risk are highlighted in orange
369
+ // inside the yellow, so a reader skimming the block still takes in the part
370
+ // that matters.
371
+ const body = [
372
+ `Trusting a repository trusts ${palette.highlight(
373
+ "every namespace and every recipe"
374
+ )} in it, including ones published later. Trusting on its own executes ` +
375
+ `nothing; subscribing to something inside it ${palette.highlight(
376
+ "can, and probably will,"
377
+ )} run scripts on this machine. This question is the last gate before ` +
378
+ `that happens.`,
379
+ "",
380
+ "Sous cannot tell you whether a repository deserves trust. Look at the " +
381
+ "location above, and at who publishes it, before answering.",
382
+ ].join("\n");
383
+
384
+ for (const line of wrapText(body, width - 2)) {
385
+ lines.push(line === "" ? " " : ` ${palette.warning(line)}`);
386
+ }
387
+
388
+ // The block closes on a blank line, so the question that follows it stands
389
+ // on its own rather than reading as the last line of the notice.
390
+ lines.push(" ");
391
+ return lines.join("\n");
392
+ }
393
+
394
+ /**
395
+ * The error a non-interactive run gets: it names every repository needing
396
+ * trust and the exact command that grants it, because a script cannot answer
397
+ * a question.
398
+ *
399
+ * @param missing - The repositories being asked about.
400
+ */
401
+ private nonInteractiveMessage(missing: MissingRepo[]): string {
402
+ const lines: string[] = [
403
+ missing.length === 1
404
+ ? "One repository has to be trusted before this can continue, and sous is not " +
405
+ "running where it can ask."
406
+ : `${missing.length} repositories have to be trusted before this can continue, ` +
407
+ `and sous is not running where it can ask.`,
408
+ ` Why: ${nonInteractiveReason() ?? "there is no terminal to ask on"}.`,
409
+ "",
410
+ ];
411
+
412
+ for (const repo of missing) {
413
+ const provenance = repo.requiredBy
414
+ .map((entry) => `${entry.ref} (required by ${entry.requestedBy})`)
415
+ .join(", ");
416
+ lines.push(` ${repo.name}: ${repo.url ?? "URL not known to sous"}`);
417
+ lines.push(` ${provenance}`);
418
+ }
419
+
420
+ lines.push("");
421
+ lines.push(" Trusting a repository trusts every namespace and recipe in it, and");
422
+ lines.push(" subscribing to something inside it can run scripts on this machine.");
423
+ lines.push(" Add each repository deliberately, with its URL:");
424
+ lines.push("");
425
+ for (const repo of missing) {
426
+ lines.push(` sous repo add ${repo.url ?? "<url>"} --name ${repo.name} --trust`);
427
+ }
428
+ lines.push("");
429
+ lines.push(
430
+ " '--trust' is one spelling of the confirmation flag; '--yes', '-y' and"
431
+ );
432
+ lines.push(" '--force' mean exactly the same thing.");
433
+
434
+ return lines.join("\n");
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Writes a blank line and then a block of text, which is how the trust notice
440
+ * reaches the console when nothing overrides it.
441
+ *
442
+ * @param message - The block to write.
443
+ */
444
+ export function writeTrustNotice(message: string): void {
445
+ blankLine();
446
+ writeLine(message);
447
+ }